diff --git a/desktop/src-tauri/src/commands/agent_config.rs b/desktop/src-tauri/src/commands/agent_config.rs index 554a53672..9ecad5215 100644 --- a/desktop/src-tauri/src/commands/agent_config.rs +++ b/desktop/src-tauri/src/commands/agent_config.rs @@ -12,7 +12,8 @@ use crate::{ }, writer::plan_config_write, }, - known_acp_runtime, load_managed_agents, save_managed_agents, sync_managed_agent_processes, + known_acp_runtime, load_managed_agents, load_personas, save_managed_agents, + sync_managed_agent_processes, }, }; @@ -47,11 +48,13 @@ pub async fn get_agent_config_surface( let runtime_meta = known_acp_runtime(&record.agent_command); let session_cache = state.get_session_cache(&pubkey); + let personas = load_personas(&app).unwrap_or_default(); Ok(read_config_surface( &record, runtime_meta, session_cache.as_ref(), + &personas, )) } @@ -80,7 +83,8 @@ pub async fn write_agent_config_field( let runtime_meta = known_acp_runtime(&record.agent_command); let session_cache = state.get_session_cache(&request.pubkey); - let surface = read_config_surface(&record, runtime_meta, session_cache.as_ref()); + let personas = load_personas(&app).unwrap_or_default(); + let surface = read_config_surface(&record, runtime_meta, session_cache.as_ref(), &personas); let mut result = plan_config_write(&surface, &request.field); diff --git a/desktop/src-tauri/src/managed_agents/config_bridge/reader.rs b/desktop/src-tauri/src/managed_agents/config_bridge/reader.rs index 46d57c7e9..13cc23dea 100644 --- a/desktop/src-tauri/src/managed_agents/config_bridge/reader.rs +++ b/desktop/src-tauri/src/managed_agents/config_bridge/reader.rs @@ -1,5 +1,5 @@ use crate::managed_agents::discovery::KnownAcpRuntime; -use crate::managed_agents::types::ManagedAgentRecord; +use crate::managed_agents::types::{ManagedAgentRecord, PersonaRecord}; use super::types::*; @@ -11,6 +11,7 @@ pub(crate) fn read_config_surface( record: &ManagedAgentRecord, runtime_meta: Option<&KnownAcpRuntime>, session_cache: Option<&SessionConfigCache>, + personas: &[PersonaRecord], ) -> RuntimeConfigSurface { let is_pre_spawn = session_cache.is_none(); @@ -97,19 +98,40 @@ pub(crate) fn read_config_surface( .system_prompt .clone() .or_else(|| record.env_vars.get("BUZZ_ACP_SYSTEM_PROMPT").cloned()); - record_system_prompt.as_ref().map(|v| NormalizedField { - value: Some(v.clone()), - origin: ConfigOrigin::BuzzExplicit, - is_writable: true, - write_via: ConfigWriteMechanism::RespawnWithEnvVar { - env_key: "BUZZ_ACP_SYSTEM_PROMPT".to_string(), - }, - overridden_value: file_config.system_prompt.clone(), - overridden_origin: file_config - .system_prompt - .as_ref() - .map(|_| ConfigOrigin::ConfigFile), - }) + if let Some(ref v) = record_system_prompt { + Some(NormalizedField { + value: Some(v.clone()), + origin: ConfigOrigin::BuzzExplicit, + is_writable: true, + write_via: ConfigWriteMechanism::RespawnWithEnvVar { + env_key: "BUZZ_ACP_SYSTEM_PROMPT".to_string(), + }, + overridden_value: file_config.system_prompt.clone(), + overridden_origin: file_config + .system_prompt + .as_ref() + .map(|_| ConfigOrigin::ConfigFile), + }) + } else { + // Fall back to the linked persona's system prompt. + record + .persona_id + .as_deref() + .and_then(|pid| personas.iter().find(|p| p.id == pid)) + .map(|p| NormalizedField { + value: Some(p.system_prompt.clone()), + origin: ConfigOrigin::PersonaDefault, + is_writable: true, + write_via: ConfigWriteMechanism::RespawnWithEnvVar { + env_key: "BUZZ_ACP_SYSTEM_PROMPT".to_string(), + }, + overridden_value: file_config.system_prompt.clone(), + overridden_origin: file_config + .system_prompt + .as_ref() + .map(|_| ConfigOrigin::ConfigFile), + }) + } }, }; @@ -493,7 +515,7 @@ mod tests { fn pre_spawn_surface_reports_pending_acp_tiers() { let record = test_record(); let runtime = test_runtime(); - let surface = read_config_surface(&record, Some(runtime), None); + let surface = read_config_surface(&record, Some(runtime), None, &[]); assert!(surface.is_pre_spawn); assert_eq!(surface.sources.acp_native, ConfigTierStatus::Pending); @@ -510,7 +532,7 @@ mod tests { record.model = Some("explicit-model".to_string()); let runtime = test_runtime(); - let surface = read_config_surface(&record, Some(runtime), None); + let surface = read_config_surface(&record, Some(runtime), None, &[]); let model = surface.normalized.model.unwrap(); assert_eq!(model.value.as_deref(), Some("explicit-model")); assert_eq!(model.origin, ConfigOrigin::BuzzExplicit); @@ -523,7 +545,7 @@ mod tests { provider_locked: true, ..*test_runtime() }; - let surface = read_config_surface(&record, Some(runtime), None); + let surface = read_config_surface(&record, Some(runtime), None, &[]); let provider = surface.normalized.provider.unwrap(); assert_eq!(provider.value.as_deref(), Some("Anthropic (locked)")); assert!(!provider.is_writable); @@ -548,7 +570,7 @@ mod tests { captured_at: "".to_string(), }; - let surface = read_config_surface(&record, Some(runtime), Some(&cache)); + let surface = read_config_surface(&record, Some(runtime), Some(&cache), &[]); assert!(!surface.is_pre_spawn); let model = surface.normalized.model.unwrap(); assert_eq!(model.value.as_deref(), Some("claude-opus-4")); @@ -571,7 +593,7 @@ mod tests { captured_at: "".to_string(), }; - let surface = read_config_surface(&record, Some(runtime), Some(&cache)); + let surface = read_config_surface(&record, Some(runtime), Some(&cache), &[]); let model = surface.normalized.model.unwrap(); assert_eq!(model.value.as_deref(), Some("acp-model")); assert_eq!(model.origin, ConfigOrigin::AcpConfigOption); @@ -579,4 +601,40 @@ mod tests { // the actual file in a unit test, just verify the override fields are populated // when we manually construct the scenario via build_model_field. } + + #[test] + fn persona_system_prompt_used_when_record_has_none() { + use crate::managed_agents::types::PersonaRecord; + + let mut record = test_record(); + record.persona_id = Some("persona-1".to_string()); + let runtime = test_runtime(); + + let personas = vec![PersonaRecord { + id: "persona-1".to_string(), + display_name: "Test Persona".to_string(), + avatar_url: None, + system_prompt: "You are a helpful assistant.".to_string(), + runtime: None, + model: Some("test-model".to_string()), + provider: None, + name_pool: vec![], + is_builtin: false, + is_active: true, + source_team: None, + source_team_persona_slug: None, + env_vars: BTreeMap::new(), + created_at: "".to_string(), + updated_at: "".to_string(), + }]; + + let surface = read_config_surface(&record, Some(runtime), None, &personas); + let prompt = surface.normalized.system_prompt.unwrap(); + assert_eq!( + prompt.value.as_deref(), + Some("You are a helpful assistant.") + ); + assert_eq!(prompt.origin, ConfigOrigin::PersonaDefault); + assert!(prompt.is_writable); + } } diff --git a/desktop/src/features/agents/ui/AgentConfigPanel.tsx b/desktop/src/features/agents/ui/AgentConfigPanel.tsx index decf3896d..382ad3338 100644 --- a/desktop/src/features/agents/ui/AgentConfigPanel.tsx +++ b/desktop/src/features/agents/ui/AgentConfigPanel.tsx @@ -4,12 +4,16 @@ import { ChevronDown, ChevronRight, Info } from "lucide-react"; import { useAgentConfigSurface } from "../hooks"; import { cn } from "@/shared/lib/cn"; import { Spinner } from "@/shared/ui/spinner"; +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@/shared/ui/tooltip"; import type { ConfigField, ConfigOrigin, NormalizedConfig, NormalizedField, - ConfigSourceReport, } from "@/shared/api/types"; type Props = { @@ -44,6 +48,28 @@ function originLabel( } } +function originTooltip( + origin: ConfigOrigin, + configFilePath: string | null, +): string { + switch (origin) { + case "buzzExplicit": + return "Set in Buzz UI"; + case "acpConfigOption": + return "Set via ACP session"; + case "acpNativeRead": + return "Read from ACP runtime"; + case "envVar": + return "From environment variable"; + case "configFile": + return configFilePath + ? `From config file (${configFilePath})` + : "From config file"; + case "personaDefault": + return "From persona defaults"; + } +} + function originColorClass(origin: ConfigOrigin): string { switch (origin) { case "buzzExplicit": @@ -52,8 +78,9 @@ function originColorClass(origin: ConfigOrigin): string { case "acpNativeRead": return "bg-green-100 text-green-700 dark:bg-green-900/40 dark:text-green-300"; case "configFile": - case "personaDefault": return "bg-muted text-muted-foreground"; + case "personaDefault": + return "bg-violet-100 text-violet-700 dark:bg-violet-900/40 dark:text-violet-300"; case "envVar": return "bg-amber-100 text-amber-700 dark:bg-amber-900/40 dark:text-amber-300"; } @@ -67,15 +94,21 @@ function OriginBadge({ configFilePath: string | null; }) { return ( - - {originLabel(origin, configFilePath)} - + + + + {originLabel(origin, configFilePath)} + + + + {originTooltip(origin, configFilePath)} + + ); } @@ -183,34 +216,6 @@ function AdvancedRow({ ); } -// ── Sources footer ──────────────────────────────────────────────────────────── - -const STATUS_ICON: Record = { - available: "✓", - pending: "⏳", - notApplicable: "—", -}; - -function SourcesFooter({ sources }: { sources: ConfigSourceReport }) { - const tiers = [ - { label: "Config file", status: sources.configFile }, - { label: "ACP native", status: sources.acpNative }, - { label: "ACP config", status: sources.acpConfigOptions }, - { label: "Env vars", status: sources.envVars }, - ] as const; - - return ( -

- {tiers.map((tier, i) => ( - - {i > 0 && |} - {tier.label} {STATUS_ICON[tier.status] ?? tier.status} - - ))} -

- ); -} - // ── Main component ──────────────────────────────────────────────────────────── export function AgentConfigPanel({ pubkey, isRunning: _isRunning }: Props) { @@ -302,8 +307,6 @@ export function AgentConfigPanel({ pubkey, isRunning: _isRunning }: Props) { )} )} - - ); } diff --git a/desktop/src/features/profile/ui/UserProfilePanelSections.tsx b/desktop/src/features/profile/ui/UserProfilePanelSections.tsx index 0d35e909b..3aa9ef12b 100644 --- a/desktop/src/features/profile/ui/UserProfilePanelSections.tsx +++ b/desktop/src/features/profile/ui/UserProfilePanelSections.tsx @@ -14,6 +14,7 @@ import { MessageSquare, Pencil, Server, + Settings, Terminal, UserMinus, UserPlus, @@ -22,6 +23,7 @@ import { import { toast } from "sonner"; import { MemorySection } from "@/features/agent-memory/ui/MemorySection"; +import { AgentConfigPanel } from "@/features/agents/ui/AgentConfigPanel"; import { AgentStatusBadge } from "@/features/agents/ui/AgentStatusBadge"; import { useActiveAgentTurns } from "@/features/agents/activeAgentTurnsStore"; import { formatElapsed } from "@/features/agents/ui/agentSessionUtils"; @@ -231,6 +233,23 @@ export function ProfileSummaryView({ {metadataFields.length > 0 ? ( ) : null} + + {isBot && isOwner === true && managedAgent !== undefined ? ( +
+
+ +

+ Configuration +

+
+
+ +
+
+ ) : null} ); }