diff --git a/desktop/scripts/check-file-sizes.mjs b/desktop/scripts/check-file-sizes.mjs index d34611613..96746eb7c 100644 --- a/desktop/scripts/check-file-sizes.mjs +++ b/desktop/scripts/check-file-sizes.mjs @@ -483,7 +483,7 @@ const overrides = new Map([ // hidden-key projection keeps the top-level secret out of Advanced rows. ["src/features/agents/ui/AgentInstanceEditDialog.tsx", 1195], // AgentDefinitionDialog grew past 1000 with the following load-bearing fixes: - // isRuntimeAutoSeededRef tracking for edit-mode seeding (Fizz shows models); + // isRuntimeAutoSeededRef tracking for edit-mode seeding (built-ins show models); // runtimeSupportsLlmProviderSelection guard on discovery provider (codex fix); // hideProviderIds computation for Databricks v1 gate. Queued to split. ["src/features/agents/ui/AgentDefinitionDialog.tsx", 1035], diff --git a/desktop/src-tauri/src/managed_agents/personas.rs b/desktop/src-tauri/src/managed_agents/personas.rs index 605148b19..2cc8d9106 100644 --- a/desktop/src-tauri/src/managed_agents/personas.rs +++ b/desktop/src-tauri/src/managed_agents/personas.rs @@ -24,25 +24,43 @@ const WORK_COORDINATOR_AVATAR: &str = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0 const SUPPORT_GUIDE_AVATAR: &str = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMjggMTI4Ij48cmVjdCB3aWR0aD0iMTI4IiBoZWlnaHQ9IjEyOCIgcng9IjMyIiBmaWxsPSIjNmE3MDMxIi8+PGNpcmNsZSBjeD0iOTYiIGN5PSIzMiIgcj0iMTYiIGZpbGw9IiNmMmY2ZDciIGZpbGwtb3BhY2l0eT0iLjI4Ii8+PHBhdGggZD0iTTI0IDk0YzE0LTI1IDI3LTM3IDQwLTM3czI2IDEyIDQwIDM3IiBmaWxsPSJub25lIiBzdHJva2U9IiNmMmY2ZDciIHN0cm9rZS13aWR0aD0iOCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIi8+PHRleHQgeD0iNjQiIHk9IjcyIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LWZhbWlseT0iSW50ZXIsQXJpYWwsc2Fucy1zZXJpZiIgZm9udC1zaXplPSIzNCIgZm9udC13ZWlnaHQ9IjcwMCIgZmlsbD0iI2YyZjZkNyI+U0c8L3RleHQ+PC9zdmc+"; const EXPERIMENT_DESIGNER_AVATAR: &str = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMjggMTI4Ij48cmVjdCB3aWR0aD0iMTI4IiBoZWlnaHQ9IjEyOCIgcng9IjMyIiBmaWxsPSIjOGIzZjVlIi8+PGNpcmNsZSBjeD0iOTYiIGN5PSIzMiIgcj0iMTYiIGZpbGw9IiNmZmU3ZjAiIGZpbGwtb3BhY2l0eT0iLjI4Ii8+PHBhdGggZD0iTTI0IDk0YzE0LTI1IDI3LTM3IDQwLTM3czI2IDEyIDQwIDM3IiBmaWxsPSJub25lIiBzdHJva2U9IiNmZmU3ZjAiIHN0cm9rZS13aWR0aD0iOCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIi8+PHRleHQgeD0iNjQiIHk9IjcyIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LWZhbWlseT0iSW50ZXIsQXJpYWwsc2Fucy1zZXJpZiIgZm9udC1zaXplPSIzNCIgZm9udC13ZWlnaHQ9IjcwMCIgZmlsbD0iI2ZmZTdmMCI+RUQ8L3RleHQ+PC9zdmc+"; -const FIZZ_SYSTEM_PROMPT: &str = r#"You are Fizz. You are a careful, direct engineering agent with a subtle bee theme: collaborative, industrious, and precise. Keep the bee motif light — no catchphrases, no cartoon impersonation, and no performative roleplay. Reliability beats performance theater. +const BRAIN_SYSTEM_PROMPT: &str = r#"You are Brain, the research and planning lead. Turn ambiguous requests into well-grounded plans, then make sure the work is completed and validated. -# Subagents and Peers +# Focus -Other agents are peers, not tools. Collaborate when useful, but partition ownership by file or task so two writers never edit the same path. Front-load setup before tagging someone, agree on the base and handoff contract, and integrate their results without also doing their exact work. +Start by understanding the goal, reading the relevant context, and resolving important unknowns. Produce a concrete approach with clear boundaries, dependencies, risks, and validation. Keep planning proportional to the task: simple work needs only a short internal plan, while complex work deserves explicit phases. -Use subagents when: +# Solo and collaborative work -- You can decompose research into unrelated areas explored in parallel. -- You can decompose build work into independent, non-overlapping file sets. -- A task needs a long-running command while you continue other work. +Before starting, inspect the current thread context to determine whether another agent is already participating. Channel membership alone does not mean an agent is part of the thread. If no other agent is participating, briefly confirm that you will handle the task solo, then own the research, implementation, tests, validation, and final response yourself. -Don't use subagents when the briefing overhead exceeds the parallelism payoff or you could just read the file yourself. +If another agent is participating, collaborate when it improves the outcome. When that agent is named Brawn or Worker and the task includes implementation, delegate the implementation with an exact @mention. Give it the goal, your plan, relevant paths and constraints, the expected validation, and the deliverable you need back. Do not duplicate work you delegated. Continue any research or coordination that helps, then review the result and synthesize the final response for the user. You remain accountable for the complete outcome either way. -# Communication +# Collaboration -- Bee-themed emoji are okay, but use them sparingly — at most one when it genuinely adds warmth or clarity, and skip them entirely in serious, blocked, or failure updates. +Use the exact display name when mentioning another agent. Keep assignments scoped and actionable. Wait for delegated work to report back before declaring the task complete. Surface blockers early, and make decisions explicit when tradeoffs matter. -Your name is Fizz. You are friendly, helpful, and quietly industrious — more honeycomb than hornet."#; +Your name is Brain. Be thoughtful, decisive, and concise."#; + +const BRAWN_SYSTEM_PROMPT: &str = r#"You are Brawn, an implementation specialist. Turn an approved direction into working, verified changes. + +# Focus + +Write code, edit configuration, run commands, add or update tests, and validate the result. Read enough surrounding code to follow existing patterns, keep edits scoped to the assignment, and fix failures before reporting completion. Do not stop at a patch when you can safely run the relevant checks. + +# Working with Brain + +Brain or another Planner agent may delegate work to you. Treat that assignment and its stated boundaries as the source of truth. Ask a focused question only when a missing decision would materially change the implementation. Otherwise, make reasonable local decisions and keep moving. + +Before starting, inspect the current thread context to determine whether another agent is already participating. Channel membership alone does not mean an agent is part of the thread. If no other agent is participating, briefly confirm that you will handle the task solo, then own the implementation and validation end to end. If another agent is participating, coordinate with them when useful while keeping ownership boundaries clear. + +When the work is complete, report back in the same channel with an exact @mention of the delegating Brain or Planner. Include what changed, validation performed and its result, and any remaining risk or blocker. Do not hand the task to another agent unless the delegator asks you to. + +# Collaboration + +Be direct and execution-focused. Avoid re-planning settled work, do not edit outside your assigned scope, and never claim completion without checking the result. + +Your name is Brawn. Be practical, careful, and thorough."#; const PRODUCT_STRATEGIST_SYSTEM_PROMPT: &str = r#"You are a product strategy agent. You help turn broad ideas into clear product and design direction. @@ -106,15 +124,21 @@ Stay curious, practical, and optimistic. Make the next experiment easy to try."# const BUILT_IN_PERSONAS: &[BuiltInPersona] = &[ BuiltInPersona { - id: "builtin:fizz", - display_name: "Fizz", + id: "builtin:brain", + display_name: "Brain", avatar_url: Some(FIZZ_AVATAR), - system_prompt: FIZZ_SYSTEM_PROMPT, - name_pool: &[ - "Nectar", "Comet", "Bramble", "Clover", "Pollen", "Amber", "Daisy", "Mason", "Bumble", - "Thistle", "Honey", "Waxwing", "Hive", "Meadow", "Juniper", "Aster", "Sage", "Willow", - "Orchard", "Buzz", - ], + system_prompt: BRAIN_SYSTEM_PROMPT, + name_pool: &["Brain"], + model: None, + runtime: None, + default_active: true, + }, + BuiltInPersona { + id: "builtin:brawn", + display_name: "Brawn", + avatar_url: Some(IMPLEMENTATION_PARTNER_AVATAR), + system_prompt: BRAWN_SYSTEM_PROMPT, + name_pool: &["Brawn"], model: None, runtime: None, default_active: true, @@ -182,6 +206,10 @@ const BUILT_IN_PERSONAS: &[BuiltInPersona] = &[ ]; const RETIRED_PERSONAS: &[(&str, &str)] = &[ + ( + "builtin:fizz", + "", + ), ( "builtin:solo", "", @@ -321,9 +349,8 @@ fn merge_personas(mut stored: Vec, now: &str) -> (Vec>(); - assert_eq!(fizz.name_pool, expected_name_pool); - assert!(fizz.is_active); + assert_eq!(brain.name_pool, expected_name_pool); + assert!(brain.is_active); } #[test] -fn merge_personas_adds_fizz_and_retires_old_builtins_for_existing_store() { +fn merge_personas_adds_brain_and_brawn_and_retires_old_builtins_for_existing_store() { let mut legacy_builtins = vec![custom_persona("builtin:solo", "Solo")]; for persona in &mut legacy_builtins { persona.is_builtin = true; @@ -168,12 +172,18 @@ fn merge_personas_adds_fizz_and_retires_old_builtins_for_existing_store() { let (records, changed) = merge_personas(legacy_builtins, "2026-03-19T00:00:00Z"); assert!(changed); - let fizz = records + let brain = records .iter() - .find(|record| record.id == "builtin:fizz") - .expect("fizz built-in should exist"); - assert!(fizz.is_builtin); - assert!(fizz.is_active); + .find(|record| record.id == "builtin:brain") + .expect("brain built-in should exist"); + assert!(brain.is_builtin); + assert!(brain.is_active); + let brawn = records + .iter() + .find(|record| record.id == "builtin:brawn") + .expect("brawn built-in should exist"); + assert!(brawn.is_builtin); + assert!(brawn.is_active); let solo = records .iter() @@ -209,6 +219,26 @@ fn merge_personas_demotes_retired_builtins() { assert_eq!(demoted.updated_at, "2026-04-01T00:00:00Z"); } +#[test] +fn merge_personas_retires_fizz_when_brain_and_brawn_replace_it() { + let mut fizz = custom_persona("builtin:fizz", "Fizz"); + fizz.is_builtin = true; + fizz.is_active = true; + + let (records, changed) = merge_personas(vec![fizz], "2026-07-15T00:00:00Z"); + + assert!(changed); + let retired_fizz = records + .iter() + .find(|record| record.id == "builtin:fizz") + .expect("existing Fizz record should be retained for managed-agent references"); + assert!(!retired_fizz.is_builtin); + assert!(!retired_fizz.is_active); + assert_eq!(retired_fizz.display_name, "Fizz (retired)"); + assert!(records.iter().any(|record| record.id == "builtin:brain")); + assert!(records.iter().any(|record| record.id == "builtin:brawn")); +} + #[test] fn ensure_persona_is_active_rejects_missing_personas() { let err = ensure_persona_is_active(&[], "missing").unwrap_err(); @@ -218,15 +248,15 @@ fn ensure_persona_is_active_rejects_missing_personas() { #[test] fn ensure_persona_is_active_rejects_inactive_personas() { - let mut persona = custom_persona("builtin:fizz", "Fizz"); + let mut persona = custom_persona("builtin:brain", "Brain"); persona.is_builtin = true; persona.is_active = false; - let err = ensure_persona_is_active(&[persona], "builtin:fizz").unwrap_err(); + let err = ensure_persona_is_active(&[persona], "builtin:brain").unwrap_err(); assert_eq!( err, - "Fizz is not in My Agents. Choose it from Agent Catalog first." + "Brain is not in My Agents. Choose it from Agent Catalog first." ); } @@ -258,33 +288,33 @@ fn validate_persona_activation_change_rejects_non_builtins() { #[test] fn validate_persona_activation_change_rejects_managed_agent_references() { - let mut persona = custom_persona("builtin:fizz", "Fizz"); + let mut persona = custom_persona("builtin:brain", "Brain"); persona.is_builtin = true; let err = validate_persona_activation_change(&persona, false, true, false).unwrap_err(); assert_eq!( err, - "Fizz is still assigned to a managed agent. Remove or reassign those agents first." + "Brain is still assigned to a managed agent. Remove or reassign those agents first." ); } #[test] fn validate_persona_activation_change_rejects_team_references() { - let mut persona = custom_persona("builtin:fizz", "Fizz"); + let mut persona = custom_persona("builtin:brain", "Brain"); persona.is_builtin = true; let err = validate_persona_activation_change(&persona, false, false, true).unwrap_err(); assert_eq!( err, - "Fizz is still referenced by a team. Remove it from those teams first." + "Brain is still referenced by a team. Remove it from those teams first." ); } #[test] fn validate_persona_activation_change_allows_safe_builtin_updates() { - let mut persona = custom_persona("builtin:fizz", "Fizz"); + let mut persona = custom_persona("builtin:brain", "Brain"); persona.is_builtin = true; assert!(validate_persona_activation_change(&persona, true, false, false).is_ok()); @@ -293,7 +323,7 @@ fn validate_persona_activation_change_allows_safe_builtin_updates() { #[test] fn validate_persona_deletion_rejects_builtins() { - let mut persona = custom_persona("builtin:fizz", "Fizz"); + let mut persona = custom_persona("builtin:brain", "Brain"); persona.is_builtin = true; let err = validate_persona_deletion(&persona, false).unwrap_err(); @@ -325,8 +355,8 @@ fn validate_persona_deletion_allows_safe_custom_personas() { #[test] fn migrate_retires_unmodified_personas() { let now = "2026-04-01T00:00:00Z"; - // Simulate a store from before the Fizz transition: all 6 - // retired personas with original system prompts. + // Simulate a store containing every retired built-in persona with its + // original system prompt. let mut stored: Vec = RETIRED_PERSONAS .iter() .map(|(id, prompt)| AgentDefinition { @@ -428,36 +458,61 @@ fn migrate_is_idempotent() { assert!(!migrate_retired_personas(&mut stored_pre_demotion, now)); } -// ── Fizz default harness ────────────────────────────────────────────────────── +// ── Brain/Brawn default harness ─────────────────────────────────────────────── #[test] -fn fizz_builtin_has_no_pinned_runtime() { - // The Fizz built-in must not hard-pin a runtime so it inherits the - // bundled default (buzz-agent) rather than requiring goose on PATH. +fn brain_and_brawn_prompts_use_thread_participation_with_a_solo_fallback() { let records = built_in_persona_records("2026-01-01T00:00:00Z"); - let fizz = records - .iter() - .find(|r| r.id == "builtin:fizz") - .expect("builtin:fizz must exist"); - assert_eq!( - fizz.runtime, None, - "Fizz built-in must not pin a runtime — it should inherit the default" - ); + + for id in ["builtin:brain", "builtin:brawn"] { + let persona = records + .iter() + .find(|record| record.id == id) + .expect("built-in must exist"); + assert!(persona + .system_prompt + .contains("inspect the current thread context")); + assert!(persona + .system_prompt + .contains("Channel membership alone does not mean")); + assert!(persona + .system_prompt + .contains("briefly confirm that you will handle the task solo")); + } } #[test] -fn fizz_builtin_resolves_to_buzz_agent() { +fn brain_and_brawn_builtins_have_no_pinned_runtime() { + // The default built-ins must not hard-pin a runtime so they inherit the + // bundled default (buzz-agent) rather than requiring goose on PATH. + let records = built_in_persona_records("2026-01-01T00:00:00Z"); + for id in ["builtin:brain", "builtin:brawn"] { + let persona = records + .iter() + .find(|r| r.id == id) + .expect("built-in must exist"); + assert_eq!( + persona.runtime, None, + "{id} must not pin a runtime — it should inherit the default" + ); + } +} + +#[test] +fn brain_and_brawn_builtins_resolve_to_buzz_agent() { // With no runtime pin, effective_agent_command must fall through to // default_agent_command(), which resolves the bundled buzz-agent. let records = built_in_persona_records("2026-01-01T00:00:00Z"); - assert_eq!( - effective_agent_command(Some("builtin:fizz"), &records, None), - default_agent_command(), - "Fizz must resolve to the bundled default harness, not goose" - ); - assert_eq!( - effective_agent_command(Some("builtin:fizz"), &records, None), - "buzz-agent", - "Fizz must resolve to buzz-agent specifically" - ); + for id in ["builtin:brain", "builtin:brawn"] { + assert_eq!( + effective_agent_command(Some(id), &records, None), + default_agent_command(), + "{id} must resolve to the bundled default harness, not goose" + ); + assert_eq!( + effective_agent_command(Some(id), &records, None), + "buzz-agent", + "{id} must resolve to buzz-agent specifically" + ); + } } diff --git a/desktop/src/features/agents/lib/useBotRecents.test.mjs b/desktop/src/features/agents/lib/useBotRecents.test.mjs index 11d2e9fb4..002c59396 100644 --- a/desktop/src/features/agents/lib/useBotRecents.test.mjs +++ b/desktop/src/features/agents/lib/useBotRecents.test.mjs @@ -20,7 +20,8 @@ function createPersona(id, displayName) { test("pickQuickBotPersonas prefers recents before defaults", () => { const personas = [ - createPersona("builtin:fizz", "Fizz"), + createPersona("builtin:brain", "Brain"), + createPersona("builtin:brawn", "Brawn"), createPersona("builtin:reviewer", "Reviewer"), ]; @@ -28,7 +29,7 @@ test("pickQuickBotPersonas prefers recents before defaults", () => { pickQuickBotPersonas(personas, ["builtin:reviewer"]).map( (persona) => persona.id, ), - ["builtin:reviewer", "builtin:fizz"], + ["builtin:reviewer", "builtin:brain", "builtin:brawn"], ); }); @@ -47,17 +48,18 @@ test("pickQuickBotPersonas falls back to any active personas when defaults are m test("pickQuickBotPersonas skips duplicate and missing recents", () => { const personas = [ - createPersona("builtin:fizz", "Fizz"), + createPersona("builtin:brain", "Brain"), + createPersona("builtin:brawn", "Brawn"), createPersona("custom:honey", "Honey"), ]; assert.deepEqual( pickQuickBotPersonas(personas, [ - "builtin:fizz", + "builtin:brain", "missing", - "builtin:fizz", + "builtin:brain", "custom:honey", ]).map((persona) => persona.id), - ["builtin:fizz", "custom:honey"], + ["builtin:brain", "custom:honey", "builtin:brawn"], ); }); diff --git a/desktop/src/features/agents/lib/useBotRecents.ts b/desktop/src/features/agents/lib/useBotRecents.ts index cd5e1693d..dad1e3807 100644 --- a/desktop/src/features/agents/lib/useBotRecents.ts +++ b/desktop/src/features/agents/lib/useBotRecents.ts @@ -7,7 +7,7 @@ const MAX_RECENTS = 8; // Default persona display names to seed the list when empty. // These are resolved to IDs by the consumer. -export const DEFAULT_PERSONA_NAMES = ["Fizz"] as const; +export const DEFAULT_PERSONA_NAMES = ["Brain", "Brawn"] as const; export function pickQuickBotPersonas( personas: readonly AgentPersona[], diff --git a/desktop/src/features/agents/ui/AgentDefinitionDialog.tsx b/desktop/src/features/agents/ui/AgentDefinitionDialog.tsx index 526fa19c8..153bd627a 100644 --- a/desktop/src/features/agents/ui/AgentDefinitionDialog.tsx +++ b/desktop/src/features/agents/ui/AgentDefinitionDialog.tsx @@ -730,7 +730,7 @@ export function AgentDefinitionDialog({ disabled={isPending} id="persona-display-name" onChange={(event) => setDisplayName(event.target.value)} - placeholder="Fizz" + placeholder="Brain" value={displayName} /> diff --git a/desktop/src/features/channels/ui/WelcomeComposerBanner.tsx b/desktop/src/features/channels/ui/WelcomeComposerBanner.tsx index 79978925e..0cb1fa9ee 100644 --- a/desktop/src/features/channels/ui/WelcomeComposerBanner.tsx +++ b/desktop/src/features/channels/ui/WelcomeComposerBanner.tsx @@ -4,7 +4,7 @@ import { Bot, Check } from "lucide-react"; import { cn } from "@/shared/lib/cn"; -const WELCOME_PERSONA_NAMES = ["Fizz"] as const; +const WELCOME_PERSONA_NAMES = ["Brain", "Brawn"] as const; export const WELCOME_PERSONA_ROTATION_MS = 3200; export const WELCOME_PERSONA_EASE = [0.22, 1, 0.36, 1] as const; const WELCOME_PERSONA_EXIT_EASE = [0.64, 0, 0.78, 0] as const; @@ -248,7 +248,7 @@ function WelcomeComposerPersonaMention() { ...(mentionWidth === null ? {} : { width: mentionWidth }), }} > - @Fizz + {activeMention} { try { - const allowedMemberPubkeys = await getWelcomeGuideAgentPubkeys( + const allowedMemberPubkeys = await getWelcomeAgentPubkeys( communityScope, ).catch(() => []); const welcomeChannel = await ensureWelcomeChannel( diff --git a/desktop/src/features/onboarding/welcomeGuide.test.mjs b/desktop/src/features/onboarding/welcomeGuide.test.mjs index 49a717d44..6d260ba45 100644 --- a/desktop/src/features/onboarding/welcomeGuide.test.mjs +++ b/desktop/src/features/onboarding/welcomeGuide.test.mjs @@ -60,8 +60,8 @@ test("pickWelcomeGuideAgent reuses a legacy Kit guide", () => { assert.equal(pickWelcomeGuideAgent([legacyKit]), legacyKit); }); -test("pickWelcomeGuideAgent prefers a running legacy guide over stopped builtin Fizz", () => { - const stoppedBuiltinFizz = makeAgent({ +test("pickWelcomeGuideAgent prefers a running legacy guide over stopped builtin Brain", () => { + const stoppedBuiltinBrain = makeAgent({ pubkey: PUB_A, personaId: WELCOME_GUIDE_PERSONA_ID, status: "stopped", @@ -74,7 +74,7 @@ test("pickWelcomeGuideAgent prefers a running legacy guide over stopped builtin }); assert.equal( - pickWelcomeGuideAgent([stoppedBuiltinFizz, runningLegacyKit]), + pickWelcomeGuideAgent([stoppedBuiltinBrain, runningLegacyKit]), runningLegacyKit, ); }); @@ -85,22 +85,22 @@ test("pickWelcomeGuideAgent ignores non-Kit agents with the legacy prompt", () = name: "Scout", systemPrompt: LEGACY_WELCOME_GUIDE_SYSTEM_PROMPT, }); - const fizz = makeAgent({ + const brain = makeAgent({ pubkey: PUB_C, personaId: WELCOME_GUIDE_PERSONA_ID, }); - assert.equal(pickWelcomeGuideAgent([nonKit, fizz]), fizz); + assert.equal(pickWelcomeGuideAgent([nonKit, brain]), brain); }); -test("pickWelcomeGuideAgentForRelay ignores Fizz agents from other communities", () => { - const otherCommunityFizz = makeAgent({ +test("pickWelcomeGuideAgentForRelay ignores Brain agents from other communities", () => { + const otherCommunityBrain = makeAgent({ pubkey: PUB_A, personaId: WELCOME_GUIDE_PERSONA_ID, relayUrl: RELAY_A, status: "running", }); - const currentCommunityFizz = makeAgent({ + const currentCommunityBrain = makeAgent({ pubkey: PUB_B, personaId: WELCOME_GUIDE_PERSONA_ID, relayUrl: RELAY_B, @@ -109,22 +109,22 @@ test("pickWelcomeGuideAgentForRelay ignores Fizz agents from other communities", assert.equal( pickWelcomeGuideAgentForRelay( - [otherCommunityFizz, currentCommunityFizz], + [otherCommunityBrain, currentCommunityBrain], RELAY_B, ), - currentCommunityFizz, + currentCommunityBrain, ); }); -test("pickWelcomeGuideAgentForRelay returns null when Fizz only exists in another community", () => { - const otherCommunityFizz = makeAgent({ +test("pickWelcomeGuideAgentForRelay returns null when Brain only exists in another community", () => { + const otherCommunityBrain = makeAgent({ pubkey: PUB_A, personaId: WELCOME_GUIDE_PERSONA_ID, relayUrl: RELAY_A, }); assert.equal( - pickWelcomeGuideAgentForRelay([otherCommunityFizz], RELAY_B), + pickWelcomeGuideAgentForRelay([otherCommunityBrain], RELAY_B), null, ); }); diff --git a/desktop/src/features/onboarding/welcomeGuide.ts b/desktop/src/features/onboarding/welcomeGuide.ts index 051cfb8c7..59b16084f 100644 --- a/desktop/src/features/onboarding/welcomeGuide.ts +++ b/desktop/src/features/onboarding/welcomeGuide.ts @@ -9,14 +9,29 @@ import { listPersonas, setPersonaActive } from "@/shared/api/tauriPersonas"; import type { ManagedAgent } from "@/shared/api/types"; import { normalizePubkey } from "@/shared/lib/pubkey"; -export const WELCOME_GUIDE_AGENT_NAME = "Fizz"; -export const WELCOME_GUIDE_PERSONA_ID = "builtin:fizz"; +export const WELCOME_GUIDE_AGENT_NAME = "Brain"; +export const WELCOME_GUIDE_PERSONA_ID = "builtin:brain"; +export const WELCOME_WORKER_AGENT_NAME = "Brawn"; +export const WELCOME_WORKER_PERSONA_ID = "builtin:brawn"; export const WELCOME_GUIDE_INTRO_MARKER = "buzz-welcome-intro.v1"; const LEGACY_WELCOME_GUIDE_AGENT_NAME = "Kit"; export const LEGACY_WELCOME_GUIDE_SYSTEM_PROMPT = "You are Kit, Sprout's friendly welcome guide. Help new users understand the community, channels, messages, and agents. Keep introductions concise, practical, and warm."; export const WELCOME_GUIDE_INTRO_MESSAGE = - "Hi, I'm Fizz. Welcome to Buzz.\n\nI can help you get oriented, answer questions, and make the first few steps feel less mysterious.\n\nFeel free to ask me what else you can do in Buzz, or just talk through what you want to build."; + "Hi, I'm Brain. Welcome to Buzz.\n\nI focus on research and planning, and Brawn focuses on implementation and validation. Ask either of us for help, or bring us a goal and we'll work through it together."; + +const WELCOME_AGENT_DEFINITIONS = [ + { + name: WELCOME_GUIDE_AGENT_NAME, + personaId: WELCOME_GUIDE_PERSONA_ID, + }, + { + name: WELCOME_WORKER_AGENT_NAME, + personaId: WELCOME_WORKER_PERSONA_ID, + }, +] as const; + +type WelcomeAgentDefinition = (typeof WELCOME_AGENT_DEFINITIONS)[number]; function normalizeRelayUrl(relayUrl: string | null | undefined) { return relayUrl?.trim().replace(/\/+$/, "") ?? null; @@ -30,17 +45,22 @@ function isAgentScopedToRelay(agent: ManagedAgent, relayUrl?: string | null) { return normalizeRelayUrl(agent.relayUrl) === targetRelayUrl; } -function isNamedWelcomeGuideAgent(agent: ManagedAgent) { +function isNamedAgent(agent: ManagedAgent, name: string) { + return agent.name.trim().toLowerCase() === name.toLowerCase(); +} + +function isBuiltInWelcomeAgent( + agent: ManagedAgent, + definition: WelcomeAgentDefinition, +) { return ( - agent.name.trim().toLowerCase() === WELCOME_GUIDE_AGENT_NAME.toLowerCase() + agent.personaId === definition.personaId && + isNamedAgent(agent, definition.name) ); } function isBuiltInWelcomeGuideAgent(agent: ManagedAgent) { - return ( - agent.personaId === WELCOME_GUIDE_PERSONA_ID && - isNamedWelcomeGuideAgent(agent) - ); + return isBuiltInWelcomeAgent(agent, WELCOME_AGENT_DEFINITIONS[0]); } function isLegacyKitWelcomeGuideAgent(agent: ManagedAgent) { @@ -57,6 +77,14 @@ function isWelcomeGuideAgent(agent: ManagedAgent) { ); } +function isWelcomeAgent(agent: ManagedAgent) { + return ( + WELCOME_AGENT_DEFINITIONS.some((definition) => + isBuiltInWelcomeAgent(agent, definition), + ) || isLegacyKitWelcomeGuideAgent(agent) + ); +} + function pickAgentByStatus(agents: ManagedAgent[]) { return ( agents.find((agent) => agent.status === "running") ?? @@ -82,39 +110,47 @@ export function pickWelcomeGuideAgentForRelay( ); } -export async function getWelcomeGuideAgentPubkeys(relayUrl?: string | null) { +export async function getWelcomeAgentPubkeys(relayUrl?: string | null) { return (await listManagedAgents()) .filter( - (agent) => - isWelcomeGuideAgent(agent) && isAgentScopedToRelay(agent, relayUrl), + (agent) => isWelcomeAgent(agent) && isAgentScopedToRelay(agent, relayUrl), ) .map((agent) => agent.pubkey); } -async function ensureWelcomeGuidePersonaActive() { - const guidePersona = (await listPersonas()).find( - (persona) => persona.id === WELCOME_GUIDE_PERSONA_ID, +async function ensureWelcomePersonaActive(definition: WelcomeAgentDefinition) { + const persona = (await listPersonas()).find( + (candidate) => candidate.id === definition.personaId, ); - if (!guidePersona) { - throw new Error(`${WELCOME_GUIDE_AGENT_NAME} agent not found.`); + if (!persona) { + throw new Error(`${definition.name} agent not found.`); } - if (!guidePersona.isActive) { - await setPersonaActive(WELCOME_GUIDE_PERSONA_ID, true); + if (!persona.isActive) { + await setPersonaActive(definition.personaId, true); } } -async function ensureWelcomeGuideAgent(relayUrl?: string | null) { +async function ensureWelcomeAgent( + definition: WelcomeAgentDefinition, + relayUrl?: string | null, +) { const agents = await listManagedAgents(); - const existing = pickWelcomeGuideAgentForRelay(agents, relayUrl); + const existing = pickAgentByStatus( + agents.filter( + (agent) => + isBuiltInWelcomeAgent(agent, definition) && + isAgentScopedToRelay(agent, relayUrl), + ), + ); if (existing) { return existing; } - await ensureWelcomeGuidePersonaActive(); + await ensureWelcomePersonaActive(definition); const created = await createManagedAgent({ - name: WELCOME_GUIDE_AGENT_NAME, - personaId: WELCOME_GUIDE_PERSONA_ID, + name: definition.name, + personaId: definition.personaId, relayUrl: relayUrl ?? undefined, spawnAfterCreate: false, startOnAppLaunch: false, @@ -153,14 +189,22 @@ export async function ensureWelcomeGuideIntro( channelId: string, relayUrl?: string | null, ) { - const agent = await ensureWelcomeGuideAgent(relayUrl); - await ensureWelcomeGuideMembership(channelId, agent); + const guide = await ensureWelcomeAgent( + WELCOME_AGENT_DEFINITIONS[0], + relayUrl, + ); + const worker = await ensureWelcomeAgent( + WELCOME_AGENT_DEFINITIONS[1], + relayUrl, + ); + await ensureWelcomeGuideMembership(channelId, guide); + await ensureWelcomeGuideMembership(channelId, worker); await sendManagedAgentChannelMessage({ - agentPubkey: agent.pubkey, + agentPubkey: guide.pubkey, channelId, content: WELCOME_GUIDE_INTRO_MESSAGE, marker: WELCOME_GUIDE_INTRO_MARKER, markerScope: "channel", }); - return agent; + return guide; } diff --git a/desktop/src/testing/e2eBridge.ts b/desktop/src/testing/e2eBridge.ts index 44232b4b9..dce1cc6fa 100644 --- a/desktop/src/testing/e2eBridge.ts +++ b/desktop/src/testing/e2eBridge.ts @@ -1870,6 +1870,20 @@ function resetMockPersonas(config?: E2eConfig) { const now = new Date().toISOString(); const activePersonaIds = new Set(config?.mock?.activePersonaIds ?? []); const builtInPersonas = [ + { + id: "builtin:brain", + display_name: "Brain", + avatar_url: null, + system_prompt: + "You are Brain, the research and planning lead. Turn ambiguous requests into well-grounded plans, then make sure the work is completed and validated.", + }, + { + id: "builtin:brawn", + display_name: "Brawn", + avatar_url: BUILT_IN_PERSONA_AVATAR_URLS.implementationPartner, + system_prompt: + "You are Brawn, an implementation specialist. Turn an approved direction into working, verified changes.", + }, { id: "builtin:product-strategist", display_name: "Product Strategist", @@ -1891,12 +1905,6 @@ function resetMockPersonas(config?: E2eConfig) { system_prompt: "You are a QA reviewer agent. You look for the ways a change might break.\n\n# Focus\n\nInspect state transitions, empty states, permissions, accessibility, and failure paths.", }, - { - id: "builtin:fizz", - display_name: "Fizz", - avatar_url: null, - system_prompt: "You are Fizz.", - }, { id: "builtin:work-coordinator", display_name: "Work Coordinator", diff --git a/desktop/tests/e2e/agents.spec.ts b/desktop/tests/e2e/agents.spec.ts index 25b02bc7d..5986a1e22 100644 --- a/desktop/tests/e2e/agents.spec.ts +++ b/desktop/tests/e2e/agents.spec.ts @@ -161,7 +161,7 @@ test("built-in personas are used from the catalog dialog", async ({ page }) => { await expect(page.getByTestId("agents-library-personas")).toBeVisible(); await openPersonaCatalog(page); await expect(page.getByTestId("persona-catalog-dialog")).toContainText( - "Fizz", + "Brain", ); const previewPersonas = [ ["builtin:product-strategist", "Product Strategist"], @@ -207,22 +207,22 @@ test("built-in personas are used from the catalog dialog", async ({ page }) => { await expect(page.getByRole("tooltip")).toHaveCount(0); const initialCatalogOrder = await getCatalogOrder(page); - await selectCatalogPersona(page, "builtin:fizz"); - await useCatalogPersona(page, "builtin:fizz"); + await selectCatalogPersona(page, "builtin:brain"); + await useCatalogPersona(page, "builtin:brain"); await expect( page .locator("[data-sonner-toast]") - .filter({ hasText: "Selected Fizz for My Agents." }), + .filter({ hasText: "Selected Brain for My Agents." }), ).toBeVisible(); await expect(page.getByTestId("agents-library-personas")).toContainText( - "Fizz", + "Brain", ); await expect( - page.getByTestId("persona-catalog-use-agent-target-builtin:fizz"), + page.getByTestId("persona-catalog-use-agent-target-builtin:brain"), ).toHaveText("Added to My Agents"); await expect( - page.getByTestId("persona-catalog-use-agent-target-builtin:fizz"), + page.getByTestId("persona-catalog-use-agent-target-builtin:brain"), ).toBeDisabled(); await expect(page.getByTestId("persona-catalog-dialog")).not.toContainText( "Remove from My Agents", @@ -280,19 +280,19 @@ test("agent catalog can reopen from the populated library header", async ({ await page.getByTestId("open-agents-view").click(); await openPersonaCatalog(page); - await selectCatalogPersona(page, "builtin:fizz"); - await useCatalogPersona(page, "builtin:fizz"); + await selectCatalogPersona(page, "builtin:brain"); + await useCatalogPersona(page, "builtin:brain"); await expect(page.getByTestId("agents-library-personas")).toContainText( - "Fizz", + "Brain", ); await page.keyboard.press("Escape"); await openPersonaCatalog(page); await expect(page.getByTestId("persona-catalog-dialog")).toBeVisible(); - await selectCatalogPersona(page, "builtin:fizz"); + await selectCatalogPersona(page, "builtin:brain"); await expect( - page.getByTestId("persona-catalog-use-agent-target-builtin:fizz"), + page.getByTestId("persona-catalog-use-agent-target-builtin:brain"), ).toBeDisabled(); }); @@ -305,12 +305,12 @@ test("agent catalog chooser order stays stable when selection changes", async ({ const before = await getCatalogOrder(page); - await selectCatalogPersona(page, "builtin:fizz"); - await useCatalogPersona(page, "builtin:fizz"); + await selectCatalogPersona(page, "builtin:brain"); + await useCatalogPersona(page, "builtin:brain"); await expect( page .locator("[data-sonner-toast]") - .filter({ hasText: "Selected Fizz for My Agents." }), + .filter({ hasText: "Selected Brain for My Agents." }), ).toBeVisible(); expect(await getCatalogOrder(page)).toEqual(before); @@ -321,19 +321,19 @@ test("catalog detail pane shows the full persona details", async ({ page }) => { await page.getByTestId("open-agents-view").click(); await openPersonaCatalog(page); - await selectCatalogPersona(page, "builtin:fizz"); + await selectCatalogPersona(page, "builtin:brain"); const useAgentTarget = page.getByTestId( - "persona-catalog-use-agent-target-builtin:fizz", + "persona-catalog-use-agent-target-builtin:brain", ); await expect(page.getByTestId("persona-catalog-detail-pane")).toContainText( - "Fizz", + "Brain", ); await expect( page.getByTestId("persona-catalog-detail-pane"), ).not.toContainText("Added by You"); await expect(page.getByTestId("persona-catalog-detail-pane")).toContainText( - "You are Fizz.", + "You are Brain.", ); await expect(page.getByTestId("persona-catalog-detail-pane")).toContainText( "Built-in agent", @@ -349,13 +349,13 @@ test("catalog detail pane shows the full persona details", async ({ page }) => { ); await expect(useAgentTarget).toHaveAttribute( "aria-label", - "Add Fizz from Agent Catalog", + "Add Brain from Agent Catalog", ); await expect(useAgentTarget).toHaveText("Add agent"); await useAgentTarget.click(); await expect(page.getByTestId("agents-library-personas")).toContainText( - "Fizz", + "Brain", ); }); @@ -451,13 +451,13 @@ test("inactive built-ins cannot be used to create teams", async ({ page }) => { const error = await invokeTauriExpectError(page, "create_team", { input: { - name: "Fizzes", - personaIds: ["builtin:fizz"], + name: "Brain Team", + personaIds: ["builtin:brain"], }, }); expect(error).toBe( - "Fizz is not in My Agents. Choose it from Agent Catalog first.", + "Brain is not in My Agents. Choose it from Agent Catalog first.", ); }); @@ -466,24 +466,24 @@ test("built-in removal failures show up from My Agents", async ({ page }) => { await page.getByTestId("open-agents-view").click(); await openPersonaCatalog(page); - await selectCatalogPersona(page, "builtin:fizz"); - await useCatalogPersona(page, "builtin:fizz"); + await selectCatalogPersona(page, "builtin:brain"); + await useCatalogPersona(page, "builtin:brain"); await invokeTauri(page, "create_team", { input: { - name: "Fizzes", - personaIds: ["builtin:fizz"], + name: "Brain Team", + personaIds: ["builtin:brain"], }, }); await page.keyboard.press("Escape"); - await page.getByLabel("Open actions for Fizz").click(); + await page.getByLabel("Open actions for Brain").click(); await page.getByRole("menuitem", { name: "Remove from My Agents" }).click(); await expect( page .locator("[data-sonner-toast]") - .filter({ hasText: "Fizz is still referenced by a team." }), + .filter({ hasText: "Brain is still referenced by a team." }), ).toBeVisible(); }); diff --git a/desktop/tests/e2e/channels.spec.ts b/desktop/tests/e2e/channels.spec.ts index 44be46b0d..ee71e9911 100644 --- a/desktop/tests/e2e/channels.spec.ts +++ b/desktop/tests/e2e/channels.spec.ts @@ -651,7 +651,7 @@ test("sends the first message from the new direct message composer", async ({ test("creates the DM before preparing a persona mention", async ({ page }) => { await installMockBridge(page, { - activePersonaIds: ["builtin:fizz"], + activePersonaIds: ["builtin:brain"], createManagedAgentDelayMs: 1_000, }); await page.goto("/"); @@ -663,12 +663,12 @@ test("creates the DM before preparing a persona mention", async ({ page }) => { .click(); const input = page.getByTestId("message-input"); - await input.fill("Ask @fi"); + await input.fill("Ask @br"); await expect( page .getByTestId("message-composer") .getByTestId("mention-autocomplete") - .locator("button", { hasText: "Fizz" }), + .locator("button", { hasText: "Brain" }), ).toBeVisible(); await input.press("Enter"); await page.keyboard.type(" for a hand"); @@ -697,7 +697,7 @@ test("creates the DM before preparing a persona mention", async ({ page }) => { ) .toBeGreaterThan(baselineCreateCount); await expect(page.getByTestId("chat-title")).toContainText("charlie"); - await expect(page.getByTestId("chat-title")).toContainText("Fizz"); + await expect(page.getByTestId("chat-title")).toContainText("Brain"); const sendCommands = (await readCommandLog(page)).slice( baselineCommands.length, @@ -753,7 +753,7 @@ test("creates the DM before preparing a persona mention", async ({ page }) => { page .getByTestId("message-row") .last() - .locator("[data-mention].agent-mention-highlight", { hasText: "Fizz" }), + .locator("[data-mention].agent-mention-highlight", { hasText: "Brain" }), ).toBeVisible(); }); @@ -761,7 +761,7 @@ test("routes an agent mention from an existing DM to the expanded conversation", page, }) => { await installMockBridge(page, { - activePersonaIds: ["builtin:fizz"], + activePersonaIds: ["builtin:brain"], }); await page.goto("/"); @@ -773,12 +773,12 @@ test("routes an agent mention from an existing DM to the expanded conversation", const messageTail = "in this DM"; const input = page.getByTestId("message-input"); - await input.fill("Ask @fi"); + await input.fill("Ask @br"); await expect( page .getByTestId("message-composer") .getByTestId("mention-autocomplete") - .locator("button", { hasText: "Fizz" }), + .locator("button", { hasText: "Brain" }), ).toBeVisible(); await input.press("Enter"); await page.keyboard.type(" in this DM"); @@ -794,8 +794,8 @@ test("routes an agent mention from an existing DM to the expanded conversation", page.locator("[data-active='true'][data-channel-id]"), ).toHaveAttribute("data-channel-id", sentChannelId ?? ""); await expect(page.getByTestId("chat-title")).toContainText("alice"); - await expect(page.getByTestId("chat-title")).toContainText("Fizz"); - await expect(sourceDm).not.toContainText("Fizz"); + await expect(page.getByTestId("chat-title")).toContainText("Brain"); + await expect(sourceDm).not.toContainText("Brain"); const sendCommands = (await readCommandPayloadLog(page)).slice( baselineCommands.length, ); @@ -876,7 +876,7 @@ test("does not reroute an expanded DM after the user navigates away", async ({ page, }) => { await installMockBridge(page, { - activePersonaIds: ["builtin:fizz"], + activePersonaIds: ["builtin:brain"], sendMessageDelayMs: 1_000, }); await page.goto("/"); @@ -884,12 +884,12 @@ test("does not reroute an expanded DM after the user navigates away", async ({ await expect(page.getByTestId("chat-title")).toHaveText("alice-tyler"); const input = page.getByTestId("message-input"); - await input.fill("Ask @fi"); + await input.fill("Ask @br"); await expect( page .getByTestId("message-composer") .getByTestId("mention-autocomplete") - .locator("button", { hasText: "Fizz" }), + .locator("button", { hasText: "Brain" }), ).toBeVisible(); await input.press("Enter"); await page.keyboard.type(" while I leave"); @@ -908,7 +908,7 @@ test("does not reroute an expanded DM after the channel pane unmounts", async ({ page, }) => { await installMockBridge(page, { - activePersonaIds: ["builtin:fizz"], + activePersonaIds: ["builtin:brain"], openDmDelayMs: 1_000, }); await page.goto("/"); @@ -916,12 +916,12 @@ test("does not reroute an expanded DM after the channel pane unmounts", async ({ await expect(page.getByTestId("chat-title")).toHaveText("alice-tyler"); const input = page.getByTestId("message-input"); - await input.fill("Ask @fi"); + await input.fill("Ask @br"); await expect( page .getByTestId("message-composer") .getByTestId("mention-autocomplete") - .locator("button", { hasText: "Fizz" }), + .locator("button", { hasText: "Brain" }), ).toBeVisible(); await input.press("Enter"); await page.keyboard.type(" while I open settings"); @@ -941,7 +941,7 @@ test("drops an expanded DM after the first message fails", async ({ page }) => { const retryMessage = "Retry without the agent"; const sendError = "Mock first DM send failed."; await installMockBridge(page, { - activePersonaIds: ["builtin:fizz"], + activePersonaIds: ["builtin:brain"], sendMessageErrors: [sendError], }); await page.goto("/"); @@ -953,19 +953,19 @@ test("drops an expanded DM after the first message fails", async ({ page }) => { .click(); const input = page.getByTestId("message-input"); - await input.fill("Ask @fi"); + await input.fill("Ask @br"); await expect( page .getByTestId("message-composer") .getByTestId("mention-autocomplete") - .locator("button", { hasText: "Fizz" }), + .locator("button", { hasText: "Brain" }), ).toBeVisible(); await input.press("Enter"); await page.keyboard.type(" for a hand"); await page.getByTestId("send-message").click(); await expect(page.getByText(sendError)).toBeVisible(); - await expect(input).toContainText("Fizz"); + await expect(input).toContainText("Brain"); const commandsAfterFailure = await readCommandPayloadLog(page); const failedSendChannelId = await readOutgoingChannelId(page, "for a hand"); @@ -1029,7 +1029,7 @@ test("drops an expanded DM after agent startup fails", async ({ page }) => { const retryMessage = "Retry after agent startup failed"; const startError = "Mock agent startup failed."; await installMockBridge(page, { - activePersonaIds: ["builtin:fizz"], + activePersonaIds: ["builtin:brain"], startManagedAgentErrors: [startError], }); await page.goto("/"); @@ -1041,12 +1041,12 @@ test("drops an expanded DM after agent startup fails", async ({ page }) => { .click(); const input = page.getByTestId("message-input"); - await input.fill("Ask @fi"); + await input.fill("Ask @br"); await expect( page .getByTestId("message-composer") .getByTestId("mention-autocomplete") - .locator("button", { hasText: "Fizz" }), + .locator("button", { hasText: "Brain" }), ).toBeVisible(); await input.press("Enter"); await page.keyboard.type(" before startup fails"); @@ -1055,7 +1055,7 @@ test("drops an expanded DM after agent startup fails", async ({ page }) => { await expect( page.getByText(startError, { exact: false }).first(), ).toBeVisible(); - await expect(input).toContainText("Fizz"); + await expect(input).toContainText("Brain"); const commandsAfterFailure = await readCommandPayloadLog(page); const openDmCallsAfterFailure = commandsAfterFailure.filter( @@ -1089,7 +1089,7 @@ test("drops an expanded DM after agent startup fails", async ({ page }) => { expect( (retryOpenDm?.payload as { pubkeys?: string[] } | undefined)?.pubkeys, ).toEqual([TEST_IDENTITIES.charlie.pubkey]); - await expect(page.getByTestId("chat-title")).not.toContainText("Fizz"); + await expect(page.getByTestId("chat-title")).not.toContainText("Brain"); }); test("closes direct message results while opening", async ({ page }) => { @@ -2768,13 +2768,13 @@ test("members sidebar collapses same-persona managed agents", async ({ { pubkey: outOfChannelAgentPubkey, name: "Pinky", - personaId: "builtin:fizz", + personaId: "builtin:brain", status: "stopped", }, { pubkey: inChannelAgentPubkey, name: "Pinky", - personaId: "builtin:fizz", + personaId: "builtin:brain", status: "running", channelNames: ["general"], }, diff --git a/desktop/tests/e2e/config-bridge-screenshots.spec.ts b/desktop/tests/e2e/config-bridge-screenshots.spec.ts index 11d4a73fa..ddb47ebdc 100644 --- a/desktop/tests/e2e/config-bridge-screenshots.spec.ts +++ b/desktop/tests/e2e/config-bridge-screenshots.spec.ts @@ -90,7 +90,7 @@ async function invokeMockCommand( } async function activatePersonas(page: import("@playwright/test").Page) { - for (const id of ["builtin:fizz"]) { + for (const id of ["builtin:brain"]) { await invokeMockCommand(page, "set_persona_active", { id, active: true }); } } diff --git a/desktop/tests/e2e/human-edit-agent-content.spec.ts b/desktop/tests/e2e/human-edit-agent-content.spec.ts index 0994fce23..e970b239d 100644 --- a/desktop/tests/e2e/human-edit-agent-content.spec.ts +++ b/desktop/tests/e2e/human-edit-agent-content.spec.ts @@ -67,7 +67,7 @@ test.beforeEach(async ({ page }) => { // in mockProfiles when a managed agent is seeded. pubkey: OWNED_AGENT_PUBKEY, name: "OwnedBot", - personaId: "builtin:fizz", + personaId: "builtin:brain", status: "running", // Seed into #agents so the bridge seeds a message from this agent. channelNames: ["agents"], diff --git a/desktop/tests/e2e/mentions.spec.ts b/desktop/tests/e2e/mentions.spec.ts index 4355709f7..b7b48ce1c 100644 --- a/desktop/tests/e2e/mentions.spec.ts +++ b/desktop/tests/e2e/mentions.spec.ts @@ -187,7 +187,7 @@ test("@ trigger prioritizes channel members before runnable personas and other a page, }) => { await installMockBridge(page, { - activePersonaIds: ["builtin:fizz"], + activePersonaIds: ["builtin:brain"], }); await page.goto("/"); await page.getByTestId("channel-general").click(); @@ -200,7 +200,7 @@ test("@ trigger prioritizes channel members before runnable personas and other a await expect(dropdown).toBeVisible(); await expect(dropdown.getByText("alice")).toBeVisible(); await expect(dropdown.getByText("bob")).toBeVisible(); - await expect(dropdown.getByText("Fizz")).toBeVisible(); + await expect(dropdown.getByText("Brain")).toBeVisible(); await expect(dropdown.getByText("charlie")).toBeVisible(); await expect(dropdown.getByText("outsider")).toHaveCount(0); const charlieRow = dropdown.locator("button", { hasText: "charlie" }); @@ -214,7 +214,7 @@ test("@ trigger prioritizes channel members before runnable personas and other a const suggestions = dropdown.locator("button"); const suggestionText = await suggestions.allInnerTexts(); - const fizzIndex = suggestionText.findIndex((text) => text.includes("Fizz")); + const brainIndex = suggestionText.findIndex((text) => text.includes("Brain")); const aliceIndex = suggestionText.findIndex((text) => text.includes("alice")); const bobIndex = suggestionText.findIndex((text) => text.includes("bob")); const charlieIndex = suggestionText.findIndex((text) => @@ -223,14 +223,14 @@ test("@ trigger prioritizes channel members before runnable personas and other a const outsiderIndex = suggestionText.findIndex((text) => text.includes("outsider"), ); - expect(fizzIndex).toBeGreaterThanOrEqual(0); + expect(brainIndex).toBeGreaterThanOrEqual(0); expect(aliceIndex).toBeGreaterThanOrEqual(0); expect(bobIndex).toBeGreaterThanOrEqual(0); expect(charlieIndex).toBeGreaterThanOrEqual(0); expect(outsiderIndex).toEqual(-1); - expect(aliceIndex).toBeLessThan(fizzIndex); - expect(bobIndex).toBeLessThan(fizzIndex); - expect(fizzIndex).toBeLessThan(charlieIndex); + expect(aliceIndex).toBeLessThan(brainIndex); + expect(bobIndex).toBeLessThan(brainIndex); + expect(brainIndex).toBeLessThan(charlieIndex); }); test("thread autocomplete keeps multiple long names readable in a narrow panel", async ({ @@ -309,7 +309,7 @@ test("blocks non-participant persona mentions in DM threads", async ({ page, }) => { await installMockBridge(page, { - activePersonaIds: ["builtin:fizz"], + activePersonaIds: ["builtin:brain"], }); await page.goto("/"); await page.getByTestId("channel-bob-tyler").click(); @@ -330,11 +330,11 @@ test("blocks non-participant persona mentions in DM threads", async ({ const threadPanel = page.getByTestId("message-thread-panel"); const input = threadPanel.getByTestId("message-input"); - await input.fill("Ask @fi"); + await input.fill("Ask @br"); await expect( threadPanel .getByTestId("mention-autocomplete") - .locator("button", { hasText: "Fizz" }), + .locator("button", { hasText: "Brain" }), ).toBeVisible(); await input.press("Enter"); await page.keyboard.type(" in this thread"); @@ -354,7 +354,7 @@ test("blocks non-participant persona mentions in DM threads", async ({ expect(commandCount(commands, "add_channel_members")).toBe( commandCount(baselineCommands, "add_channel_members"), ); - await expect(input).toContainText("Fizz"); + await expect(input).toContainText("Brain"); await expect(page.getByTestId("chat-title")).toHaveText("bob-tyler"); }); @@ -544,29 +544,29 @@ test("selecting a persona mention creates a channel agent before sending", async page, }) => { await installMockBridge(page, { - activePersonaIds: ["builtin:fizz"], + activePersonaIds: ["builtin:brain"], }); await page.goto("/"); await page.getByTestId("channel-general").click(); await expect(page.getByTestId("chat-title")).toHaveText("general"); const input = page.getByTestId("message-input"); - await input.fill("Ask @fi"); + await input.fill("Ask @br"); const dropdown = autocomplete(page); - const fizzRow = dropdown.locator("button", { hasText: "Fizz" }); - await expect(fizzRow).toBeVisible(); - await expect(fizzRow.getByTestId("mention-agent-icon")).toBeVisible(); - await expect(fizzRow.getByText("agent")).toBeVisible(); - await expect(fizzRow.getByText("not in channel")).toBeVisible(); + const brainRow = dropdown.locator("button", { hasText: "Brain" }); + await expect(brainRow).toBeVisible(); + await expect(brainRow.getByTestId("mention-agent-icon")).toBeVisible(); + await expect(brainRow.getByText("agent")).toBeVisible(); + await expect(brainRow.getByText("not in channel")).toBeVisible(); await input.press("Enter"); await page.keyboard.type(" for a hand"); const composerChip = input.locator(".agent-mention-highlight", { - hasText: "Fizz", + hasText: "Brain", }); await expect(composerChip).toBeVisible(); - await expect(composerChip).toHaveText("Fizz"); + await expect(composerChip).toHaveText("Brain"); const baselineCommands = await readCommandLog(page); const baselineCreateCount = commandCount( @@ -616,21 +616,21 @@ test("selecting a persona mention creates a channel agent before sending", async const mentionChip = page .getByTestId("message-row") .last() - .locator("[data-mention].agent-mention-highlight", { hasText: "Fizz" }); + .locator("[data-mention].agent-mention-highlight", { hasText: "Brain" }); await expect(mentionChip).toBeVisible(); - await expect(mentionChip).toHaveText("Fizz"); + await expect(mentionChip).toHaveText("Brain"); }); test("selecting a persona mention reuses an existing persona agent", async ({ page, }) => { await installMockBridge(page, { - activePersonaIds: ["builtin:fizz"], + activePersonaIds: ["builtin:brain"], managedAgents: [ { pubkey: REUSABLE_PERSONA_AGENT_PUBKEY, - name: "Fizz", - personaId: "builtin:fizz", + name: "Brain", + personaId: "builtin:brain", status: "stopped", }, ], @@ -640,11 +640,11 @@ test("selecting a persona mention reuses an existing persona agent", async ({ await expect(page.getByTestId("chat-title")).toHaveText("general"); const input = page.getByTestId("message-input"); - await input.fill("Ask @fi"); + await input.fill("Ask @br"); const dropdown = autocomplete(page); - const fizzRow = dropdown.locator("button", { hasText: "Fizz" }); - await expect(fizzRow).toBeVisible(); + const brainRow = dropdown.locator("button", { hasText: "Brain" }); + await expect(brainRow).toBeVisible(); await input.press("Enter"); await page.keyboard.type(" for a hand"); @@ -682,9 +682,9 @@ test("selecting a persona mention reuses an existing persona agent", async ({ const mentionChip = page .getByTestId("message-row") .last() - .locator("[data-mention].agent-mention-highlight", { hasText: "Fizz" }); + .locator("[data-mention].agent-mention-highlight", { hasText: "Brain" }); await expect(mentionChip).toBeVisible(); - await expect(mentionChip).toHaveText("Fizz"); + await expect(mentionChip).toHaveText("Brain"); }); test("relay-profile agents with member roles use the agent composer style", async ({ @@ -810,7 +810,7 @@ test("mentioning an in-channel stopped managed agent starts it before sending", managedAgents: [ { pubkey: IN_CHANNEL_MANAGED_AGENT_PUBKEY, - name: "fizz", + name: "brain", status: "stopped", channelNames: ["general"], }, @@ -821,10 +821,10 @@ test("mentioning an in-channel stopped managed agent starts it before sending", await expect(page.getByTestId("chat-title")).toHaveText("general"); const input = page.getByTestId("message-input"); - await input.fill("Hey @fizz"); + await input.fill("Hey @brain"); const dropdown = autocomplete(page); - await expect(dropdown.getByText("fizz")).toBeVisible(); + await expect(dropdown.getByText("brain")).toBeVisible(); await expect(dropdown.getByText("agent")).toBeVisible(); await input.press("Enter"); await page.keyboard.type(" can you help?"); @@ -844,7 +844,7 @@ test("mentioning an in-channel stopped managed agent starts it before sending", const mentionChip = page .getByTestId("message-row") .last() - .locator("[data-mention].agent-mention-highlight", { hasText: "fizz" }); + .locator("[data-mention].agent-mention-highlight", { hasText: "brain" }); await expect(mentionChip).toBeVisible(); }); @@ -905,7 +905,7 @@ test("mentioning a non-member managed agent adds and starts it before sending", managedAgents: [ { pubkey: OUT_OF_CHANNEL_MANAGED_AGENT_PUBKEY, - name: "fizz", + name: "brain", status: "stopped", }, ], @@ -915,12 +915,12 @@ test("mentioning a non-member managed agent adds and starts it before sending", await expect(page.getByTestId("chat-title")).toHaveText("general"); const input = page.getByTestId("message-input"); - await input.fill("Loop in @fizz"); + await input.fill("Loop in @brain"); const dropdown = autocomplete(page); - const fizzRow = dropdown.locator("button", { hasText: "fizz" }); - await expect(fizzRow).toBeVisible(); - await expect(fizzRow.getByText("not in channel")).toBeVisible(); + const brainRow = dropdown.locator("button", { hasText: "brain" }); + await expect(brainRow).toBeVisible(); + await expect(brainRow.getByText("not in channel")).toBeVisible(); await input.press("Enter"); const baselineCommands = await readCommandLog(page); @@ -950,7 +950,7 @@ test("mentioning a non-member managed agent adds and starts it before sending", const mentionChip = page .getByTestId("message-row") .last() - .locator("[data-mention].agent-mention-highlight", { hasText: "fizz" }); + .locator("[data-mention].agent-mention-highlight", { hasText: "brain" }); await expect(mentionChip).toBeVisible(); }); diff --git a/desktop/tests/e2e/needs-restart-screenshots.spec.ts b/desktop/tests/e2e/needs-restart-screenshots.spec.ts index 7c836974f..909d0ed25 100644 --- a/desktop/tests/e2e/needs-restart-screenshots.spec.ts +++ b/desktop/tests/e2e/needs-restart-screenshots.spec.ts @@ -25,7 +25,7 @@ const STANDALONE_AGENT = { const PERSONA_AGENT = { pubkey: TEST_IDENTITIES.bob.pubkey, name: "Persona Agent", - personaId: "builtin:fizz", + personaId: "builtin:brain", status: "running" as const, needsRestart: true, }; @@ -95,7 +95,7 @@ test.describe("needs-restart screenshots", () => { test("02-grid-persona-restart-badge", async ({ page }) => { await installMockBridge(page, { - activePersonaIds: ["builtin:fizz"], + activePersonaIds: ["builtin:brain"], managedAgents: [PERSONA_AGENT], }); diff --git a/desktop/tests/e2e/onboarding.spec.ts b/desktop/tests/e2e/onboarding.spec.ts index 7603afa35..8dd34f63c 100644 --- a/desktop/tests/e2e/onboarding.spec.ts +++ b/desktop/tests/e2e/onboarding.spec.ts @@ -195,8 +195,11 @@ async function expectWelcomePersonaMention(page: Page) { const banner = page.getByTestId("welcome-composer-guide-banner"); const personaMention = page.getByTestId("welcome-composer-persona-mention"); await expect(personaMention).toBeVisible(); - await expect(personaMention).toHaveAttribute("data-persona-options", "Fizz"); - await expect(personaMention).toHaveAttribute("data-active-persona", "Fizz"); + await expect(personaMention).toHaveAttribute( + "data-persona-options", + "Brain,Brawn", + ); + await expect(personaMention).toHaveAttribute("data-active-persona", "Brain"); await expect(personaMention).toHaveAttribute( "data-animation-target", "per-character", @@ -307,7 +310,7 @@ async function expectWelcomeComposerBannerCompletesAfterPersonaMention( const banner = page.getByTestId("welcome-composer-guide-banner"); const channelIntro = page.getByTestId("message-channel-intro"); - await page.getByTestId("message-input").fill("Thanks @Fizz"); + await page.getByTestId("message-input").fill("Thanks @Brain"); await page.getByTestId("send-message").click(); await expect(banner).toHaveAttribute("data-state", "complete"); @@ -427,7 +430,7 @@ async function expectPrivateWelcomeChannel(page: Page) { .toEqual({ channelType: "stream", isMember: true, - memberCount: 2, + memberCount: 3, ttlSeconds: null, visibility: "private", }); @@ -454,57 +457,69 @@ async function expectWelcomeGuideIntro( invokeMockCommand<{ hits: Array<{ pubkey: string; content: string }>; }>(page, "search_messages", { - q: "Hi, I'm Fizz", + q: "Hi, I'm Brain", limit: 10, }), ]); - const fizz = agents.find( - (agent) => agent.name === "Fizz" && agent.persona_id === "builtin:fizz", + const brain = agents.find( + (agent) => + agent.name === "Brain" && agent.persona_id === "builtin:brain", ); - const fizzMember = fizz - ? members.members.find((member) => member.pubkey === fizz.pubkey) + const brawn = agents.find( + (agent) => + agent.name === "Brawn" && agent.persona_id === "builtin:brawn", + ); + const brainMember = brain + ? members.members.find((member) => member.pubkey === brain.pubkey) : null; - const intro = fizz - ? introSearch.hits.find((hit) => hit.pubkey === fizz.pubkey) + const brawnMember = brawn + ? members.members.find((member) => member.pubkey === brawn.pubkey) : null; - const fizzWelcomeHits = fizz - ? introSearch.hits.filter((hit) => hit.pubkey === fizz.pubkey) + const intro = brain + ? introSearch.hits.find((hit) => hit.pubkey === brain.pubkey) + : null; + const brainWelcomeHits = brain + ? introSearch.hits.filter((hit) => hit.pubkey === brain.pubkey) : []; - const profileAvatarUrl = fizz + const profileAvatarUrl = brain ? ( await invokeMockCommand<{ profiles: Record; }>(page, "get_users_batch", { - pubkeys: [fizz.pubkey], + pubkeys: [brain.pubkey], }) - ).profiles[fizz.pubkey]?.avatar_url + ).profiles[brain.pubkey]?.avatar_url : null; return { - fizzIsBot: fizzMember?.role === "bot" && fizzMember.is_agent, - fizzPersonaId: fizz?.persona_id ?? null, + brainIsBot: brainMember?.role === "bot" && brainMember.is_agent, + brainPersonaId: brain?.persona_id ?? null, + brawnIsBot: brawnMember?.role === "bot" && brawnMember.is_agent, + brawnPersonaId: brawn?.persona_id ?? null, introContent: intro?.content ?? null, - introMatchesFizz: Boolean(fizz && intro?.pubkey === fizz.pubkey), - fizzWelcomeHitCount: fizzWelcomeHits.length, + introMatchesBrain: Boolean(brain && intro?.pubkey === brain.pubkey), + brainWelcomeHitCount: brainWelcomeHits.length, profileAvatarUrl, }; }) .toEqual({ - fizzIsBot: true, - fizzPersonaId: "builtin:fizz", + brainIsBot: true, + brainPersonaId: "builtin:brain", + brawnIsBot: true, + brawnPersonaId: "builtin:brawn", introContent: - "Hi, I'm Fizz. Welcome to Buzz.\n\nI can help you get oriented, answer questions, and make the first few steps feel less mysterious.\n\nFeel free to ask me what else you can do in Buzz, or just talk through what you want to build.", - introMatchesFizz: true, - fizzWelcomeHitCount: 1, + "Hi, I'm Brain. Welcome to Buzz.\n\nI focus on research and planning, and Brawn focuses on implementation and validation. Ask either of us for help, or bring us a goal and we'll work through it together.", + introMatchesBrain: true, + brainWelcomeHitCount: 1, profileAvatarUrl: null, }); if (expectVisible) { await expect(page.getByTestId("message-timeline")).toContainText( - "Hi, I'm Fizz. Welcome to Buzz.", + "Hi, I'm Brain. Welcome to Buzz.", ); await expect(page.getByTestId("message-timeline")).toContainText( - "Feel free to ask me what else you can do in Buzz", + "Brawn focuses on implementation and validation", ); await expect( page.getByTestId("message-timeline-day-divider"), diff --git a/docs/buzz-shared-compute-dev.md b/docs/buzz-shared-compute-dev.md index 3712bb796..e13a74df9 100644 --- a/docs/buzz-shared-compute-dev.md +++ b/docs/buzz-shared-compute-dev.md @@ -1,6 +1,6 @@ # Buzz shared compute: local GUI verification -This runbook verifies the actual desktop path used by the built-in **Fizz** agent: +This runbook verifies the actual desktop path used by the built-in **Brain** agent: `Buzz Desktop → buzz-acp → buzz-agent → MeshLLM SDK → local/remote compute` @@ -73,7 +73,7 @@ runtime are behind the `mesh-llm` feature. inference is reachable while still failing the agent's long prompt and required message-send tool call. 4. Turn on **Share this machine**. -5. Wait until the card says it is sharing/running. Do not start Fizz while the +5. Wait until the card says it is sharing/running. Do not start Brain while the card says downloading, preparing, or starting. Buzz may download the model on first use. The model picker ranks models for the @@ -87,26 +87,26 @@ current hardware; avoid entering a model the card marks too large. 3. Set **Default model** to **Default (auto)**. 4. Click **Save defaults** and wait for **Saved**. -Fizz has no pinned runtime/provider/model, so it inherits these defaults and +Brain has no pinned runtime/provider/model, so it inherits these defaults and resolves to the bundled `buzz-agent`. No API key is required. -## 4. Start the real Fizz path +## 4. Start the real Brain path -1. Find the **Fizz** card on the Agents screen. -2. If Fizz is stopped, click the small play badge over its avatar. If it is +1. Find the **Brain** card on the Agents screen. +2. If Brain is stopped, click the small play badge over its avatar. If it is running, the badge is a green status dot instead of a stop control. 3. Wait for its runtime indicator to become active. -4. Add Fizz to a channel if it is not already a channel member. +4. Add Brain to a channel if it is not already a channel member. 5. In that channel, send: ```text - @Fizz Reply exactly: FIZZ_MESH_OK + @Brain Reply exactly: BRAIN_MESH_OK ``` -6. Confirm that Fizz replies `FIZZ_MESH_OK` in the channel. +6. Confirm that Brain replies `BRAIN_MESH_OK` in the channel. That channel response is the end-to-end proof. A green Compute card alone proves -only model serving; it does not prove the Fizz harness and provider inheritance. +only model serving; it does not prove the Brain harness and provider inheritance. To stop a running agent, click the body/name of its card to open its profile, then click **Stop** near the top. The green avatar badge is status-only while the @@ -130,17 +130,17 @@ lsof -nP -iTCP:9337 -iTCP:3131 # The embedded OpenAI-compatible ingress should advertise the model. curl -sS http://127.0.0.1:9337/v1/models | jq '.data[].id' -# Fizz should resolve through the real managed-agent subprocesses. +# Brain should resolve through the real managed-agent subprocesses. ps -eo pid,ppid,command | grep -E '[b]uzz-(desktop|acp|agent)' ``` -If Fizz fails, open its runtime details from the Agents screen first. Common +If Brain fails, open its runtime details from the Agents screen first. Common causes are: - launched with `just dev` instead of `just mesh=1 dev`; - a stale process owns `9337`/`3131`; - the model is still downloading or preparing; -- Fizz is not a member of the channel; +- Brain is not a member of the channel; - defaults were changed but not saved; - no current Buzz membership snapshot is available (admission fails closed).