feat(desktop): improve config bridge UX

Resolve persona system prompts in the config surface so all agents
with a linked persona display their system prompt (fixes the
inconsistency where Paul showed a prompt but Alia did not).

Surface the AgentConfigPanel in the profile pop-out sidebar for
owned bot agents, matching the existing dropdown view.

Remove the cryptic SourcesFooter (✓//— icons) and add descriptive
tooltips to per-field origin badges so users understand where each
config value comes from without documentation.

Give PersonaDefault origin a distinct violet badge color to
differentiate it from ConfigFile (gray).

Co-authored-by: Will Pfleger <wpfleger@block.xyz>
Signed-off-by: Will Pfleger <wpfleger@block.xyz>
This commit is contained in:
npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7
2026-06-16 18:57:59 -04:00
co-authored by Will Pfleger
parent fba912fc48
commit 9baa3cd198
4 changed files with 146 additions and 62 deletions
@@ -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);
@@ -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);
}
}
@@ -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 (
<span
className={cn(
"inline-block max-w-[120px] truncate rounded px-1.5 py-0.5 text-[10px] font-medium leading-none",
originColorClass(origin),
)}
title={originLabel(origin, configFilePath)}
>
{originLabel(origin, configFilePath)}
</span>
<Tooltip>
<TooltipTrigger asChild>
<span
className={cn(
"inline-block max-w-[120px] truncate rounded px-1.5 py-0.5 text-[10px] font-medium leading-none",
originColorClass(origin),
)}
>
{originLabel(origin, configFilePath)}
</span>
</TooltipTrigger>
<TooltipContent side="top">
{originTooltip(origin, configFilePath)}
</TooltipContent>
</Tooltip>
);
}
@@ -183,34 +216,6 @@ function AdvancedRow({
);
}
// ── Sources footer ────────────────────────────────────────────────────────────
const STATUS_ICON: Record<string, string> = {
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 (
<p className="mt-3 text-[10px] text-muted-foreground/70">
{tiers.map((tier, i) => (
<React.Fragment key={tier.label}>
{i > 0 && <span className="mx-1.5">|</span>}
{tier.label} <span>{STATUS_ICON[tier.status] ?? tier.status}</span>
</React.Fragment>
))}
</p>
);
}
// ── Main component ────────────────────────────────────────────────────────────
export function AgentConfigPanel({ pubkey, isRunning: _isRunning }: Props) {
@@ -302,8 +307,6 @@ export function AgentConfigPanel({ pubkey, isRunning: _isRunning }: Props) {
)}
</div>
)}
<SourcesFooter sources={sources} />
</div>
);
}
@@ -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 ? (
<ProfileFieldGroup fields={metadataFields} />
) : null}
{isBot && isOwner === true && managedAgent !== undefined ? (
<section className="space-y-2">
<div className="flex items-center gap-2 px-1">
<Settings className="h-3.5 w-3.5 text-muted-foreground" />
<h4 className="text-xs font-medium text-muted-foreground">
Configuration
</h4>
</div>
<div className="rounded-2xl bg-muted/20 px-4 py-3">
<AgentConfigPanel
pubkey={managedAgent.pubkey}
isRunning={managedAgent.status === "running"}
/>
</div>
</section>
) : null}
</div>
);
}