Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions desktop/scripts/check-file-sizes.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ const rules = [
const overrides = new Map([
["src-tauri/src/managed_agents/persona_card.rs", 700], // PNG/ZIP persona card codec + 21 unit tests (~300 lines of tests)
["src/app/AppShell.tsx", 775],
["src/features/agents/ui/AgentsView.tsx", 625], // persona/team orchestration plus import/export wiring
["src/features/channels/hooks.ts", 550], // canvas query + mutation hooks + DM hide mutation
["src/features/channels/ui/ChannelManagementSheet.tsx", 800],
["src/features/messages/ui/MessageComposer.tsx", 665], // media upload handlers (paste, drop, dialog) + channelId reset effect
Expand All @@ -43,7 +42,7 @@ const overrides = new Map([
["src/shared/api/tauri.ts", 1100], // remote agent provider API bindings + canvas API functions
["src-tauri/src/commands/agents.rs", 820], // remote agent lifecycle routing (local + provider branches) + scope enforcement
["src-tauri/src/managed_agents/backend.rs", 530], // provider IPC, validation, discovery, binary resolution + tests
["src/features/agents/ui/AgentsView.tsx", 730], // remote agent stop/delete + channel UUID resolution + presence-aware delete guard + persona import
["src/features/agents/ui/AgentsView.tsx", 775], // remote agent stop/delete + channel UUID resolution + presence-aware delete guard + persona/team import
["src/features/agents/ui/CreateAgentDialog.tsx", 685], // provider selector + config form + schema-typed config coercion + required field validation + locked scopes
["src/features/channels/ui/AddChannelBotDialog.tsx", 600], // provider mode: Run on selector, trust warning, probe effect, single-agent enforcement
]);
Expand Down
33 changes: 33 additions & 0 deletions desktop/src-tauri/src/commands/export_util.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
use tauri::AppHandle;
use tauri_plugin_dialog::DialogExt;

/// Show a save-file dialog for a JSON export and write `data` to the chosen
/// path. Returns `Ok(true)` when the file was written, `Ok(false)` when the
/// user cancelled the dialog.
pub async fn save_json_with_dialog(
app: &AppHandle,
suggested_filename: &str,
data: &[u8],
) -> Result<bool, String> {
let (tx, rx) = tokio::sync::oneshot::channel();
app.dialog()
.file()
.add_filter("JSON", &["json"])
.set_file_name(suggested_filename)
.save_file(move |path| {
let _ = tx.send(path);
});

let selected = rx.await.map_err(|_| "dialog cancelled".to_string())?;
let file_path = match selected {
Some(p) => p,
None => return Ok(false),
};

let dest = file_path
.as_path()
.ok_or_else(|| "Save dialog returned an invalid path".to_string())?;
std::fs::write(dest, data).map_err(|e| format!("Failed to write file: {e}"))?;

Ok(true)
}
1 change: 1 addition & 0 deletions desktop/src-tauri/src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ mod agents;
mod canvas;
mod channels;
mod dms;
mod export_util;
mod identity;
mod media;
mod messages;
Expand Down
39 changes: 4 additions & 35 deletions desktop/src-tauri/src/commands/personas.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use tauri::{AppHandle, State};
use tauri_plugin_dialog::DialogExt;
use uuid::Uuid;

use super::export_util::save_json_with_dialog;
use crate::{
app_state::AppState,
managed_agents::{
Expand Down Expand Up @@ -235,38 +235,7 @@ pub async fn export_persona_to_json(
let json_bytes =
encode_persona_json(&display_name, &system_prompt, avatar_url.as_deref())?;

// Slugify display name for filename.
let slug: String = display_name
.to_lowercase()
.chars()
.map(|c| if c.is_ascii_alphanumeric() { c } else { '-' })
.collect::<String>()
.trim_matches('-')
.to_string();
let slug = if slug.is_empty() { "persona" } else { &slug };
let slug = if slug.len() > 50 { &slug[..50] } else { slug };
let slug = slug.trim_end_matches('-');

let (tx, rx) = tokio::sync::oneshot::channel();
app.dialog()
.file()
.add_filter("JSON", &["json"])
.set_file_name(&format!("{slug}.persona.json"))
.save_file(move |path| {
let _ = tx.send(path);
});

let selected = rx.await.map_err(|_| "dialog cancelled".to_string())?;
let file_path = match selected {
Some(p) => p,
None => return Ok(false),
};

let dest = file_path
.as_path()
.ok_or_else(|| "Save dialog returned an invalid path".to_string())?;
std::fs::write(dest, &json_bytes)
.map_err(|e| format!("Failed to write file: {e}"))?;

Ok(true)
let slug = crate::util::slugify(&display_name, "persona", 50);
let filename = format!("{slug}.persona.json");
save_json_with_dialog(&app, &filename, &json_bytes).await
}
54 changes: 53 additions & 1 deletion desktop/src-tauri/src/commands/teams.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
use tauri::{AppHandle, State};
use uuid::Uuid;

use super::export_util::save_json_with_dialog;
use crate::{
app_state::AppState,
managed_agents::{load_teams, save_teams, CreateTeamRequest, TeamRecord, UpdateTeamRequest},
managed_agents::{
encode_team_json, load_personas, load_teams, parse_team_json, save_teams,
CreateTeamRequest, ParsedTeamPreview, TeamRecord, UpdateTeamRequest,
},
util::now_iso,
};

Expand Down Expand Up @@ -106,3 +110,51 @@ pub fn delete_team(
}
save_teams(&app, &teams)
}

// ---------------------------------------------------------------------------
// Import / Export
// ---------------------------------------------------------------------------

const MAX_TEAM_JSON_BYTES: usize = 5 * 1024 * 1024;

#[tauri::command]
pub async fn export_team_to_json(
id: String,
app: AppHandle,
state: State<'_, AppState>,
) -> Result<bool, String> {
// Load team and personas under lock, then drop lock before dialog.
let (team, personas) = {
let _store_guard = state
.managed_agents_store_lock
.lock()
.map_err(|e| e.to_string())?;
let teams = load_teams(&app)?;
let team = teams
.into_iter()
.find(|t| t.id == id)
.ok_or_else(|| format!("team {id} not found"))?;
let personas = load_personas(&app)?;
(team, personas)
};

let json_bytes = encode_team_json(&team, &personas)?;

let slug = crate::util::slugify(&team.name, "team", 50);
let filename = format!("{slug}.team.json");
save_json_with_dialog(&app, &filename, &json_bytes).await
}

#[tauri::command]
pub fn parse_team_file(
file_bytes: Vec<u8>,
_file_name: String,
) -> Result<ParsedTeamPreview, String> {
if file_bytes.is_empty() {
return Err("File is empty.".to_string());
}
if file_bytes.len() > MAX_TEAM_JSON_BYTES {
return Err("File is too large (max 5 MB).".to_string());
}
parse_team_json(&file_bytes)
}
2 changes: 2 additions & 0 deletions desktop/src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,8 @@ pub fn run() {
create_team,
update_team,
delete_team,
export_team_to_json,
parse_team_file,
parse_persona_files,
export_persona_to_json,
])
Expand Down
Loading
Loading