Skip to content

Commit ba2fdbf

Browse files
authored
refactor(desktop): consolidate packs into teams (#852)
1 parent 384a34e commit ba2fdbf

26 files changed

Lines changed: 2283 additions & 1312 deletions

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,3 +60,6 @@ identity.key
6060

6161
# Claude Code worktrees
6262
.claude/worktrees/
63+
64+
# mesh-llm build cache
65+
.cache/

AGENTS.md

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -319,8 +319,19 @@ Right-click shows "Mark as read".
319319
{{02-context-menu}}
320320
```
321321

322-
Re-runs for the same PR overwrite previous images. Cleanup:
323-
`git push origin --delete agent-screenshots/<username>`.
322+
Re-runs overwrite the image blobs on the `agent-screenshots/<username>`
323+
branch, but the script **appends a new PR comment** — it does not edit or
324+
delete the previous one. After reposting, delete the superseded comment so
325+
only the current set remains, otherwise reviewers still see the stale images:
326+
327+
```bash
328+
# List screenshot comments to find the stale one's id
329+
gh pr view <pr> --repo block/sprout --json comments \
330+
--jq '.comments[] | select(.body | test("pr-<pr>--")) | {id, url}'
331+
gh api -X DELETE repos/block/sprout/issues/comments/<stale-comment-id>
332+
```
333+
334+
Branch cleanup when fully done: `git push origin --delete agent-screenshots/<username>`.
324335

325336
### Writing E2E Screenshot Specs
326337

@@ -357,6 +368,20 @@ await menuItem.evaluate((el) =>
357368
**Cropping:** Use `clip` — full-window (1280x720) screenshots are unreadable
358369
for sidebar features. Sidebar = 256px; context menus ~450px.
359370

371+
**Distinct states — verify before posting:** when one view renders many
372+
elements at once (e.g. all team cards in a single grid), an unscoped
373+
full-page `page.screenshot()` captures the *same* pixels for every shot, so
374+
multiple PNGs come out byte-identical. Scope each shot to its subject with
375+
`locator.screenshot()` (full-page `clip` only when an overlay like an open
376+
dropdown must be included). Then gate on hash distinctness before posting:
377+
378+
```bash
379+
shasum -a 256 test-results/<dir>/*.png # every hash must be unique
380+
```
381+
382+
Identical hashes mean two shots captured the same state — fix the spec, do
383+
not post. This catches the most common screenshot regression.
384+
360385
**`general` has pre-seeded messages** making `hasUnread` always true. Use
361386
`engineering` for "muted + no unread" visual states.
362387

desktop/playwright.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ export default defineConfig({
3030
"**/channel-mute-screenshots.spec.ts",
3131
"**/channel-star-screenshots.spec.ts",
3232
"**/channel-controls-screenshots.spec.ts",
33+
"**/team-management-screenshots.spec.ts",
3334
"**/file-attachment.spec.ts",
3435
"**/mentions.spec.ts",
3536
"**/relay-reconnect.spec.ts",

desktop/scripts/check-file-sizes.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ const overrides = new Map([
4040
["src-tauri/src/nostr_convert.rs", 1116],
4141
["src/shared/api/relayClientSession.ts", 1022],
4242
["src-tauri/src/migration.rs", 1295],
43+
["src-tauri/src/managed_agents/teams.rs", 1020],
4344
]);
4445

4546
await runFileSizeCheck({

desktop/src-tauri/src/commands/agents.rs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -462,13 +462,13 @@ pub async fn create_managed_agent(
462462
requested_persona_id.as_deref().and_then(|pid| {
463463
let personas = load_personas(&app).ok()?;
464464
let persona = personas.iter().find(|p| p.id == pid)?;
465-
let pack_id = persona.source_pack.as_deref()?;
466-
let slug = persona.source_pack_persona_slug.as_deref()?;
465+
let team_id = persona.source_team.as_deref()?;
466+
let slug = persona.source_team_persona_slug.as_deref()?;
467467
let base = managed_agents_base_dir(&app).ok()?;
468-
let pack_path = base.join("packs").join(pack_id);
468+
let team_path = base.join("teams").join(team_id);
469469
// Use the validated slug stored during import — no need to
470470
// re-resolve the pack. The slug is [a-zA-Z0-9_-]+ by construction.
471-
Some((pack_path, slug.to_owned()))
471+
Some((team_path, slug.to_owned()))
472472
});
473473

474474
// Resolve the avatar URL once at creation and persist it on the record.
@@ -540,11 +540,11 @@ pub async fn create_managed_agent(
540540
backend: input.backend.clone(),
541541
backend_agent_id: None,
542542
provider_binary_path,
543-
// Pack-backed personas: record path + internal slug so the runtime
544-
// can resolve pack config at startup. Must be the slug (e.g., "lep"),
543+
// Team-backed personas: record path + internal slug so the runtime
544+
// can resolve team config at startup. Must be the slug (e.g., "lep"),
545545
// NOT the display_name — ACP's resolve_persona_by_name() matches slugs.
546-
persona_pack_path: pack_metadata.as_ref().map(|(path, _)| path.clone()),
547-
persona_name_in_pack: pack_metadata.as_ref().map(|(_, name)| name.clone()),
546+
persona_team_dir: pack_metadata.as_ref().map(|(path, _)| path.clone()),
547+
persona_name_in_team: pack_metadata.as_ref().map(|(_, name)| name.clone()),
548548
env_vars: input.env_vars.clone(),
549549
created_at: now_iso(),
550550
updated_at: now_iso(),

desktop/src-tauri/src/commands/personas.rs

Lines changed: 7 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,11 @@ use super::export_util::save_json_with_dialog;
55
use crate::{
66
app_state::AppState,
77
managed_agents::{
8-
encode_persona_json, import_persona_pack, list_installed_packs, load_managed_agents,
9-
load_personas, load_teams, parse_json_persona, parse_md_persona, parse_png_persona,
10-
parse_zip_personas, save_managed_agents, save_personas, try_regenerate_nest,
11-
uninstall_persona_pack as do_uninstall_persona_pack, validate_persona_activation_change,
12-
validate_persona_deletion, CreatePersonaRequest, PackSummary, ParsePersonaFilesResult,
13-
PersonaRecord, UpdatePersonaRequest,
8+
encode_persona_json, load_managed_agents, load_personas, load_teams, parse_json_persona,
9+
parse_md_persona, parse_png_persona, parse_zip_personas, save_managed_agents,
10+
save_personas, try_regenerate_nest, validate_persona_activation_change,
11+
validate_persona_deletion, CreatePersonaRequest, ParsePersonaFilesResult, PersonaRecord,
12+
UpdatePersonaRequest,
1413
},
1514
util::now_iso,
1615
};
@@ -79,8 +78,8 @@ pub fn create_persona(
7978
name_pool,
8079
is_builtin: false,
8180
is_active: true,
82-
source_pack: None,
83-
source_pack_persona_slug: None,
81+
source_team: None,
82+
source_team_persona_slug: None,
8483
env_vars: input.env_vars,
8584
created_at: now.clone(),
8685
updated_at: now,
@@ -374,44 +373,3 @@ pub async fn export_persona_to_json(
374373
let filename = format!("{slug}.persona.json");
375374
save_json_with_dialog(&app, &filename, &json_bytes).await
376375
}
377-
378-
// ── Pack management commands ──────────────────────────────────────────────────
379-
380-
#[tauri::command]
381-
pub fn install_persona_pack(
382-
app: AppHandle,
383-
state: State<'_, AppState>,
384-
path: String,
385-
) -> Result<Vec<PersonaRecord>, String> {
386-
let _lock = state
387-
.managed_agents_store_lock
388-
.lock()
389-
.map_err(|e| e.to_string())?;
390-
let source = std::path::PathBuf::from(&path);
391-
if !source.is_dir() {
392-
return Err(format!("pack path is not a directory: {path}"));
393-
}
394-
let result = import_persona_pack(&app, &source)?;
395-
try_regenerate_nest(&app);
396-
Ok(result)
397-
}
398-
399-
#[tauri::command]
400-
pub fn uninstall_persona_pack(
401-
app: AppHandle,
402-
state: State<'_, AppState>,
403-
pack_id: String,
404-
) -> Result<(), String> {
405-
let _lock = state
406-
.managed_agents_store_lock
407-
.lock()
408-
.map_err(|e| e.to_string())?;
409-
do_uninstall_persona_pack(&app, &pack_id)?;
410-
try_regenerate_nest(&app);
411-
Ok(())
412-
}
413-
414-
#[tauri::command]
415-
pub fn list_persona_packs(app: AppHandle) -> Result<Vec<PackSummary>, String> {
416-
list_installed_packs(&app)
417-
}

desktop/src-tauri/src/commands/teams.rs

Lines changed: 53 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,10 @@ use super::export_util::save_json_with_dialog;
55
use crate::{
66
app_state::AppState,
77
managed_agents::{
8-
encode_team_json, ensure_persona_ids_are_active, load_personas, load_teams,
9-
parse_team_json, save_teams, validate_team_deletion, CreateTeamRequest, ParsedTeamPreview,
10-
TeamRecord, UpdateTeamRequest,
8+
delete_team_with_cascade, encode_team_json, ensure_persona_ids_are_active,
9+
import_team_from_directory as do_import_team, load_personas, load_teams, parse_team_json,
10+
save_teams, sync_team_from_dir as do_sync_team, try_regenerate_nest, CreateTeamRequest,
11+
ParsedTeamPreview, SyncResult, TeamRecord, UpdateTeamRequest,
1112
},
1213
util::now_iso,
1314
};
@@ -59,6 +60,10 @@ pub fn create_team(
5960
description,
6061
persona_ids: input.persona_ids,
6162
is_builtin: false,
63+
source_dir: None,
64+
is_symlink: false,
65+
symlink_target: None,
66+
version: None,
6267
created_at: now.clone(),
6368
updated_at: now,
6469
};
@@ -104,14 +109,51 @@ pub fn delete_team(id: String, app: AppHandle, state: State<'_, AppState>) -> Re
104109
.managed_agents_store_lock
105110
.lock()
106111
.map_err(|error| error.to_string())?;
107-
let mut teams = load_teams(&app)?;
108-
let team = teams
109-
.iter()
110-
.find(|record| record.id == id)
111-
.ok_or_else(|| format!("team {id} not found"))?;
112-
validate_team_deletion(team)?;
113-
teams.retain(|record| record.id != id);
114-
save_teams(&app, &teams)
112+
delete_team_with_cascade(&app, &id)?;
113+
try_regenerate_nest(&app);
114+
Ok(())
115+
}
116+
117+
#[tauri::command]
118+
pub fn install_team_from_directory(
119+
app: AppHandle,
120+
state: State<'_, AppState>,
121+
path: String,
122+
symlink: Option<bool>,
123+
) -> Result<TeamRecord, String> {
124+
let _store_guard = state
125+
.managed_agents_store_lock
126+
.lock()
127+
.map_err(|e| e.to_string())?;
128+
let source = std::path::PathBuf::from(&path);
129+
if !source.is_dir() {
130+
return Err(format!("team path is not a directory: {path}"));
131+
}
132+
let result = do_import_team(&app, &source, symlink.unwrap_or(false))?;
133+
try_regenerate_nest(&app);
134+
Ok(result)
135+
}
136+
137+
#[tauri::command]
138+
pub fn sync_team_directory(
139+
app: AppHandle,
140+
state: State<'_, AppState>,
141+
team_id: String,
142+
) -> Result<SyncResult, String> {
143+
let _store_guard = state
144+
.managed_agents_store_lock
145+
.lock()
146+
.map_err(|e| e.to_string())?;
147+
let result = do_sync_team(&app, &team_id)?;
148+
try_regenerate_nest(&app);
149+
Ok(result)
150+
}
151+
152+
#[tauri::command]
153+
pub async fn pick_team_directory(app: AppHandle) -> Result<Option<String>, String> {
154+
use tauri_plugin_dialog::DialogExt;
155+
let path = app.dialog().file().blocking_pick_folder();
156+
Ok(path.map(|p| p.to_string()))
115157
}
116158

117159
// ---------------------------------------------------------------------------

desktop/src-tauri/src/lib.rs

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -520,12 +520,13 @@ pub fn run() {
520520
// this worktree's data directory. Must run before
521521
// restore_managed_agents_on_launch (which reads managed-agents.json).
522522
migration::sync_shared_agent_data(&app_handle);
523-
migration::reconcile_persona_pack_paths(&app_handle);
523+
migration::migrate_packs_to_teams(&app_handle);
524+
migration::reconcile_persona_team_dirs(&app_handle);
524525
migration::reconcile_provider_mcp_commands(&app_handle);
525526
migration::migrate_persona_provider_to_runtime(&app_handle);
526527

527-
if let Err(e) = managed_agents::sync_pack_personas(&app_handle) {
528-
eprintln!("sprout-desktop: sync-pack-personas: {e}");
528+
if let Err(e) = managed_agents::sync_team_personas(&app_handle) {
529+
eprintln!("sprout-desktop: sync-team-personas: {e}");
529530
}
530531

531532
// Resolve persisted identity key (env var → file → generate+save).
@@ -734,13 +735,13 @@ pub fn run() {
734735
create_team,
735736
update_team,
736737
delete_team,
738+
install_team_from_directory,
739+
sync_team_directory,
740+
pick_team_directory,
737741
export_team_to_json,
738742
parse_team_file,
739743
parse_persona_files,
740744
export_persona_to_json,
741-
install_persona_pack,
742-
uninstall_persona_pack,
743-
list_persona_packs,
744745
get_channel_workflows,
745746
get_workflow,
746747
create_workflow,

desktop/src-tauri/src/managed_agents/nest.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -947,8 +947,8 @@ mod tests {
947947
name_pool: vec![],
948948
is_builtin: false,
949949
is_active: true,
950-
source_pack: None,
951-
source_pack_persona_slug: None,
950+
source_team: None,
951+
source_team_persona_slug: None,
952952
env_vars: std::collections::BTreeMap::new(),
953953
created_at: String::new(),
954954
updated_at: String::new(),
@@ -980,8 +980,8 @@ mod tests {
980980
backend: BackendKind::default(),
981981
backend_agent_id: None,
982982
provider_binary_path: None,
983-
persona_pack_path: None,
984-
persona_name_in_pack: None,
983+
persona_team_dir: None,
984+
persona_name_in_team: None,
985985
created_at: String::new(),
986986
updated_at: String::new(),
987987
last_started_at: None,

0 commit comments

Comments
 (0)