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: 2 additions & 1 deletion desktop/scripts/check-file-sizes.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ const rules = [

// Exceptions should stay rare and temporary. Prefer splitting files instead.
const overrides = new Map([
["src-tauri/src/managed_agents/personas.rs", 830], // built-in persona system prompts + persona pack import/uninstall/list + uninstall safety check
["src-tauri/src/managed_agents/personas.rs", 900], // built-in persona system prompts (Solo + Kit + Scout) + persona pack import/uninstall/list + uninstall safety check
["src-tauri/src/managed_agents/teams.rs", 580], // built-in team registry (Kit & Scout) + merge_teams + validate_team_deletion + JSON export/import + tests
["src-tauri/src/managed_agents/persona_card.rs", 970], // PNG/ZIP/MD persona card codec + pack-zip detection + nested root finder + provider/model/namePool fields + 27 unit tests
["src/app/AppShell.tsx", 860], // message edit state + handlers + ChannelPane edit prop threading + scrollback pagination + workflows view + memory-leak safeguards
["src/features/channels/hooks.ts", 550], // canvas query + mutation hooks + DM hide mutation
Expand Down
14 changes: 8 additions & 6 deletions desktop/src-tauri/src/commands/teams.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ use crate::{
app_state::AppState,
managed_agents::{
encode_team_json, ensure_persona_ids_are_active, load_personas, load_teams,
parse_team_json, save_teams, CreateTeamRequest, ParsedTeamPreview, TeamRecord,
UpdateTeamRequest,
parse_team_json, save_teams, validate_team_deletion, CreateTeamRequest, ParsedTeamPreview,
TeamRecord, UpdateTeamRequest,
},
util::now_iso,
};
Expand Down Expand Up @@ -58,6 +58,7 @@ pub fn create_team(
name,
description,
persona_ids: input.persona_ids,
is_builtin: false,
created_at: now.clone(),
updated_at: now,
};
Expand Down Expand Up @@ -104,11 +105,12 @@ pub fn delete_team(id: String, app: AppHandle, state: State<'_, AppState>) -> Re
.lock()
.map_err(|error| error.to_string())?;
let mut teams = load_teams(&app)?;
let original_len = teams.len();
let team = teams
.iter()
.find(|record| record.id == id)
.ok_or_else(|| format!("team {id} not found"))?;
validate_team_deletion(team)?;
teams.retain(|record| record.id != id);
if teams.len() == original_len {
return Err(format!("team {id} not found"));
}
save_teams(&app, &teams)
}

Expand Down
2 changes: 1 addition & 1 deletion desktop/src-tauri/src/managed_agents/persona_avatars.rs

Large diffs are not rendered by default.

628 changes: 334 additions & 294 deletions desktop/src-tauri/src/managed_agents/personas.rs

Large diffs are not rendered by default.

61 changes: 42 additions & 19 deletions desktop/src-tauri/src/managed_agents/personas/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,12 @@ fn merge_personas_adds_missing_built_ins() {
assert!(changed);
assert_eq!(records.len(), BUILT_IN_PERSONAS.len());
assert!(records.iter().all(|record| record.is_builtin));
assert!(records.iter().all(|record| !record.is_active));
assert!(records.iter().all(|record| record.is_active));
let display_names: Vec<&str> = records
.iter()
.map(|record| record.display_name.as_str())
.collect();
assert_eq!(display_names, vec!["Solo", "Ralph", "Scout", "Reviewer"]);
assert_eq!(display_names, vec!["Solo", "Kit", "Scout"]);
}

#[test]
Expand All @@ -48,7 +48,7 @@ fn merge_personas_preserves_custom_records() {

#[test]
fn merge_personas_restores_builtin_defaults() {
let mut edited_builtin = custom_persona("builtin:reviewer", "My Reviewer");
let mut edited_builtin = custom_persona("builtin:solo", "My Solo");
edited_builtin.is_builtin = true;
edited_builtin.is_active = true;
let original_created_at = edited_builtin.created_at.clone();
Expand All @@ -57,15 +57,19 @@ fn merge_personas_restores_builtin_defaults() {
let (records, changed) = merge_personas(vec![edited_builtin], "2026-03-19T00:00:00Z");

assert!(changed);
let reviewer = records
let solo = records
.iter()
.find(|record| record.id == "builtin:reviewer")
.expect("reviewer built-in should exist");
assert_eq!(reviewer.display_name, "Reviewer");
assert_eq!(reviewer.avatar_url, None);
assert_eq!(reviewer.created_at, original_created_at);
assert_eq!(reviewer.updated_at, original_updated_at);
assert!(reviewer.is_active);
.find(|record| record.id == "builtin:solo")
.expect("solo built-in should exist");
let canonical = BUILT_IN_PERSONAS
.iter()
.find(|persona| persona.id == "builtin:solo")
.expect("solo built-in definition should exist");
assert_eq!(solo.display_name, canonical.display_name);
assert_eq!(solo.avatar_url.as_deref(), canonical.avatar_url,);
assert_eq!(solo.created_at, original_created_at);
assert_eq!(solo.updated_at, original_updated_at);
assert!(solo.is_active);
}

#[test]
Expand Down Expand Up @@ -97,7 +101,7 @@ fn merge_personas_restores_builtin_name_pool_and_preserves_is_active() {

#[test]
fn merge_personas_backfills_new_builtins_for_existing_store() {
let mut legacy_builtins = vec![custom_persona("builtin:reviewer", "Reviewer")];
let mut legacy_builtins = vec![custom_persona("builtin:solo", "Solo")];
for persona in &mut legacy_builtins {
persona.is_builtin = true;
persona.avatar_url = None;
Expand All @@ -106,26 +110,45 @@ fn merge_personas_backfills_new_builtins_for_existing_store() {
let (records, changed) = merge_personas(legacy_builtins, "2026-03-19T00:00:00Z");

assert!(changed);
assert!(records.iter().any(|record| record.id == "builtin:reviewer"));
assert!(records.iter().any(|record| record.id == "builtin:ralph"));
assert!(records.iter().any(|record| record.id == "builtin:kit"));
assert!(records.iter().any(|record| record.id == "builtin:scout"));
assert!(records.iter().any(|record| record.id == "builtin:solo"));
assert!(
records
.iter()
.find(|record| record.id == "builtin:reviewer")
.expect("reviewer built-in should exist")
.find(|record| record.id == "builtin:solo")
.expect("solo built-in should exist")
.is_active
);
assert!(
!records
records
.iter()
.find(|record| record.id == "builtin:solo")
.expect("solo built-in should exist")
.find(|record| record.id == "builtin:kit")
.expect("kit built-in should exist")
.is_active
);
}

#[test]
fn merge_personas_demotes_retired_builtins() {
let mut retired = custom_persona("builtin:reviewer", "Reviewer");
retired.is_builtin = true;
retired.is_active = true;
let original_created_at = retired.created_at.clone();

let (records, changed) = merge_personas(vec![retired], "2026-04-01T00:00:00Z");

assert!(changed);
let demoted = records
.iter()
.find(|record| record.id == "builtin:reviewer")
.expect("retired built-in should be retained as a custom persona");
assert!(!demoted.is_builtin);
assert!(demoted.is_active);
assert_eq!(demoted.created_at, original_created_at);
assert_eq!(demoted.updated_at, "2026-04-01T00:00:00Z");
}

#[test]
fn ensure_persona_is_active_rejects_missing_personas() {
let err = ensure_persona_is_active(&[], "missing").unwrap_err();
Expand Down
Loading
Loading