From d09b67916e958c671e4d50046ecf1013a4ba503d Mon Sep 17 00:00:00 2001 From: npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w <52a228d6edf316ec6812ac3c9fc0d696ab59fc7954d77e7be31eedcddf91335b@sprout-oss.stage.blox.sqprod.co> Date: Tue, 30 Jun 2026 13:26:41 -0700 Subject: [PATCH 01/11] fix(profile): flatten runtime panel metadata Co-authored-by: Taylor Ho Signed-off-by: Taylor Ho --- .../features/agents/ui/AgentConfigPanel.tsx | 299 ++++++++++++++---- .../features/profile/ui/UserProfilePanel.tsx | 47 ++- .../ui/UserProfilePanelAgentDetails.tsx | 68 +--- .../profile/ui/UserProfilePanelFields.tsx | 5 +- .../profile/ui/UserProfilePanelSections.tsx | 10 +- .../profile/ui/UserProfilePanelTabs.tsx | 17 +- desktop/src/testing/e2eBridge.ts | 8 +- .../e2e/config-bridge-screenshots.spec.ts | 21 +- 8 files changed, 277 insertions(+), 198 deletions(-) diff --git a/desktop/src/features/agents/ui/AgentConfigPanel.tsx b/desktop/src/features/agents/ui/AgentConfigPanel.tsx index bc8df5eae2..ae084078f6 100644 --- a/desktop/src/features/agents/ui/AgentConfigPanel.tsx +++ b/desktop/src/features/agents/ui/AgentConfigPanel.tsx @@ -5,12 +5,15 @@ import { Brain, ChevronDown, ChevronRight, + Copy, Cpu, Hash, Layers, + Lock, MessageSquare, Server, } from "lucide-react"; +import { toast } from "sonner"; import { useAgentConfigSurface } from "../hooks"; import { cn } from "@/shared/lib/cn"; @@ -25,37 +28,102 @@ import type { type Props = { pubkey: string; + advancedMode?: "collapsed" | "flat"; }; +async function copyToClipboard(value: string, label: string) { + await navigator.clipboard.writeText(value); + toast.success(`Copied ${label}`); +} + +function isReadOnlyField({ + origin, + writeVia, +}: { + origin: ConfigOrigin; + writeVia: ConfigWriteMechanism; +}) { + return writeVia.type === "readOnly" || origin === "harnessConstraint"; +} + +function ConfigFieldLabel({ + label, + locked, +}: { + label: string; + locked: boolean; +}) { + return ( + + {label} + {locked ? ( + + ) : null} + + ); +} + +function shouldOfferCopy({ + fieldKey, + origin, + value, +}: { + fieldKey?: keyof NormalizedConfig; + origin: ConfigOrigin; + value: string | null; +}) { + if (!value) { + return false; + } + + if ( + fieldKey === "model" || + fieldKey === "provider" || + fieldKey === "maxOutputTokens" || + fieldKey === "contextLimit" + ) { + return true; + } + + if (origin === "envVar") { + return true; + } + + return value.includes("/") || value.startsWith("~") || value.includes(":"); +} + +type RowVariant = "compact" | "profile"; + // ── Provenance sentence ────────────────────────────────────────────────────── function provenanceSentence( origin: ConfigOrigin, writeVia: ConfigWriteMechanism, configFilePath: string | null, -): string { +): string | null { switch (origin) { case "buzzExplicit": - return "Set in Buzz"; + return null; case "personaDefault": - return "Inherited from persona"; + return "Persona default"; case "runtimeOverride": return "Live override (this session only)"; case "harnessConstraint": return "Locked by harness"; case "envVar": { if (writeVia.type === "respawnWithEnvVar") { - return `From environment variable (${writeVia.envKey})`; + return `Environment variable (${writeVia.envKey})`; } - return "From environment variable"; + return "Environment variable"; } case "configFile": - return configFilePath - ? `From config file (${configFilePath})` - : "From config file"; + return configFilePath ? `Config file (${configFilePath})` : "Config file"; case "acpConfigOption": case "acpNativeRead": - return "From ACP session"; + return "ACP session"; } } @@ -87,58 +155,92 @@ function NormalizedRow({ field, isPreSpawn, configFilePath, + variant = "compact", }: { fieldKey: keyof NormalizedConfig; label: string; field: NormalizedField; isPreSpawn: boolean; configFilePath: string | null; + variant?: RowVariant; }) { const Icon = NORMALIZED_ICONS[fieldKey]; // ACP-sourced origins only become meaningful post-spawn const isAcpOnly = field.origin === "acpNativeRead" || field.origin === "acpConfigOption"; + const displayValue = + isPreSpawn && isAcpOnly + ? "Available after agent starts" + : (field.value ?? "—"); + const provenance = field.value + ? provenanceSentence(field.origin, field.writeVia, configFilePath) + : null; + const locked = isReadOnlyField(field); + const isCopyable = + variant === "profile" && + shouldOfferCopy({ + fieldKey, + origin: field.origin, + value: field.value, + }); - return ( -
+ const content = ( + <> - - - {label} - + + {variant === "profile" ? ( + + ) : ( + + {label} + + )} - {isPreSpawn && isAcpOnly ? ( - "Available after agent starts" - ) : ( - <> - {field.value ?? "—"} - {field.overriddenValue && ( - - {field.overriddenValue} - + {displayValue} + {!(isPreSpawn && isAcpOnly) && field.overriddenValue ? ( + - )} + title={field.overriddenValue ?? undefined} + > + {field.overriddenValue} + + ) : null} - {field.value && ( + {provenance ? ( - {provenanceSentence(field.origin, field.writeVia, configFilePath)} + {provenance} - )} + ) : null} -
+ {isCopyable ? ( + + ) : null} + ); + + if (isCopyable && field.value) { + return ( + + ); + } + + return
{content}
; } // ── Advanced row ────────────────────────────────────────────────────────────── @@ -146,35 +248,91 @@ function NormalizedRow({ function AdvancedRow({ field, configFilePath, + variant = "compact", }: { field: ConfigField; configFilePath: string | null; + variant?: RowVariant; }) { - return ( -
-
{field.label}
-
- {field.value ?? ( - - )} -
- {field.value && ( -
- {provenanceSentence(field.origin, field.writeVia, configFilePath)} + const provenance = field.value + ? provenanceSentence(field.origin, field.writeVia, configFilePath) + : null; + const locked = isReadOnlyField(field); + + if (variant === "compact") { + return ( +
+
{field.label}
+
+ {field.value ?? ( + + )}
- )} -
+ {provenance ? ( +
+ {provenance} +
+ ) : null} +
+ ); + } + + const isCopyable = shouldOfferCopy({ + origin: field.origin, + value: field.value, + }); + const content = ( + <> + + + + + + + {field.value ?? "—"} + + {provenance ? ( + + {provenance} + + ) : null} + + {isCopyable ? ( + + ) : null} + ); + + if (isCopyable && field.value) { + return ( + + ); + } + + return
{content}
; } // ── Main component ──────────────────────────────────────────────────────────── -export function AgentConfigPanel({ pubkey }: Props) { +export function AgentConfigPanel({ + advancedMode = "collapsed", + pubkey, +}: Props) { const [advancedOpen, setAdvancedOpen] = React.useState(false); - const { data, isLoading, error } = useAgentConfigSurface(pubkey); if (isLoading) { @@ -204,10 +362,10 @@ export function AgentConfigPanel({ pubkey }: Props) { keyof NormalizedConfig, NormalizedField | null, ][] - ).filter(([, field]) => field !== null) as [ - keyof NormalizedConfig, - NormalizedField, - ][]; + ).filter( + ([key, field]) => + field !== null && !(advancedMode === "flat" && key === "systemPrompt"), + ) as [keyof NormalizedConfig, NormalizedField][]; return (
@@ -228,18 +386,31 @@ export function AgentConfigPanel({ pubkey }: Props) { field={field} isPreSpawn={isPreSpawn} configFilePath={configFilePath} + variant={advancedMode === "flat" ? "profile" : "compact"} /> )) )}
- {/* Advanced section */} - {advanced.length > 0 && ( + {advanced.length > 0 && advancedMode === "flat" ? ( +
+ {advanced.map((field) => ( + + ))} +
+ ) : null} + + {advanced.length > 0 && advancedMode === "collapsed" ? (
- {advancedOpen && ( + {advancedOpen ? (
{advanced.map((field) => ( ))}
- )} + ) : null}
- )} + ) : null}
); } diff --git a/desktop/src/features/profile/ui/UserProfilePanel.tsx b/desktop/src/features/profile/ui/UserProfilePanel.tsx index c2c31b1e8c..5192d8be40 100644 --- a/desktop/src/features/profile/ui/UserProfilePanel.tsx +++ b/desktop/src/features/profile/ui/UserProfilePanel.tsx @@ -768,28 +768,24 @@ export function UserProfilePanel({ viewerIsOwner={viewerIsOwner} /> ); - const { - agentInfoFields, - agentSettingsFields, - diagnosticsFields, - modelLabel, - } = useProfileFieldBuckets({ - isBot, - isOwner, - managedAgent, - onOpenProfile, - ownerAvatarUrl: ownerAvatarProfile?.avatarUrl ?? null, - ownerDisplayName, - ownerHandle, - ownerProfilePubkey, - ownerPubkey, - persona: resolvedPersona, - presenceLoaded: presenceQuery.isSuccess, - presenceStatus, - profile, - pubkey: effectivePubkey, - relayAgent, - }); + const { agentInfoFields, agentSettingsFields, diagnosticsFields } = + useProfileFieldBuckets({ + isBot, + isOwner, + managedAgent, + onOpenProfile, + ownerAvatarUrl: ownerAvatarProfile?.avatarUrl ?? null, + ownerDisplayName, + ownerHandle, + ownerProfilePubkey, + ownerPubkey, + persona: resolvedPersona, + presenceLoaded: presenceQuery.isSuccess, + presenceStatus, + profile, + pubkey: effectivePubkey, + relayAgent, + }); const isDiagnosticsLikeView = view === "diagnostics" || view === "logs"; const managedAgentLogContent = managedAgentLogQuery.data?.content ?? null; const logHeaderSubtitle = @@ -846,7 +842,6 @@ export function UserProfilePanel({ managedAgent={managedAgent} memoriesLoading={memoryQuery.isLoading} memoryCount={memoryCount} - modelLabel={modelLabel} agentInfoFields={agentInfoFields} agentSettingsFields={agentSettingsFields} diagnosticsFields={diagnosticsFields} @@ -876,11 +871,7 @@ export function UserProfilePanel({ ) : null} {view === "configuration" ? ( - + ) : null} {view === "instructions" ? ( diff --git a/desktop/src/features/profile/ui/UserProfilePanelAgentDetails.tsx b/desktop/src/features/profile/ui/UserProfilePanelAgentDetails.tsx index a0f079e278..844de528e2 100644 --- a/desktop/src/features/profile/ui/UserProfilePanelAgentDetails.tsx +++ b/desktop/src/features/profile/ui/UserProfilePanelAgentDetails.tsx @@ -1,6 +1,5 @@ -import { ChevronRight, Cpu, MessageSquare } from "lucide-react"; +import { ChevronRight, MessageSquare } from "lucide-react"; -import type { ManagedAgent } from "@/shared/api/types"; import { Markdown } from "@/shared/ui/markdown"; import { type ProfileField, @@ -15,12 +14,8 @@ export const AGENT_DETAILS_FIELD_LABELS = new Set([ export function AgentConfigurationFocusedView({ fields, - managedAgent, - modelLabel, }: { fields: ProfileField[]; - managedAgent: ManagedAgent | undefined; - modelLabel: string; }) { const runtimeConfigurationFields = fields.filter((field) => AGENT_DETAILS_FIELD_LABELS.has(field.label), @@ -28,12 +23,7 @@ export function AgentConfigurationFocusedView({ return (
- +
); } @@ -41,25 +31,16 @@ export function AgentConfigurationFocusedView({ function AgentConfigurationRows({ fields, instruction, - managedAgent, - modelLabel, showInstructionPlaceholder, - showModel, }: { fields: ProfileField[]; instruction?: string | null; - managedAgent: ManagedAgent | undefined; - modelLabel: string; showInstructionPlaceholder?: boolean; - showModel: boolean; }) { const hasRows = hasAgentConfigurationRows({ fields, instruction, - managedAgent, - modelLabel, showInstructionPlaceholder, - showModel, }); if (!hasRows) { @@ -71,10 +52,7 @@ function AgentConfigurationRows({ ); @@ -83,26 +61,17 @@ function AgentConfigurationRows({ export function AgentDetailsRows({ fields, instruction, - managedAgent, - modelLabel, showInstructionPlaceholder, - showModel = false, }: { fields: ProfileField[]; instruction?: string | null; - managedAgent?: ManagedAgent | undefined; - modelLabel?: string; showInstructionPlaceholder?: boolean; - showModel?: boolean; }) { const trimmedInstruction = instruction?.trim() ?? ""; const showInstructions = trimmedInstruction.length > 0 || showInstructionPlaceholder === true; - const showModelRow = - showModel === true && - (managedAgent !== undefined || (modelLabel?.trim().length ?? 0) > 0); - if (!showInstructions && !showModelRow && fields.length === 0) { + if (!showInstructions && fields.length === 0) { return null; } @@ -112,10 +81,6 @@ export function AgentDetailsRows({ ) : null} - {showModelRow ? ( - - ) : null} - {fields.length > 0 ? : null} ); @@ -124,25 +89,17 @@ export function AgentDetailsRows({ function hasAgentConfigurationRows({ fields, instruction, - managedAgent, - modelLabel, showInstructionPlaceholder, - showModel, }: { fields: ProfileField[]; instruction?: string | null; - managedAgent: ManagedAgent | undefined; - modelLabel: string; showInstructionPlaceholder?: boolean; - showModel: boolean; }) { const trimmedInstruction = instruction?.trim() ?? ""; return ( trimmedInstruction.length > 0 || showInstructionPlaceholder === true || - (showModel === true && - (managedAgent !== undefined || modelLabel.trim().length > 0)) || fields.length > 0 ); } @@ -243,22 +200,3 @@ export function AgentInstructionsFocusedView({ ); } - -function AgentModelRow({ modelLabel }: { modelLabel: string }) { - return ( -
- - - - - Model - - {modelLabel} - - -
- ); -} diff --git a/desktop/src/features/profile/ui/UserProfilePanelFields.tsx b/desktop/src/features/profile/ui/UserProfilePanelFields.tsx index 9111047f59..f5d4b2bc55 100644 --- a/desktop/src/features/profile/ui/UserProfilePanelFields.tsx +++ b/desktop/src/features/profile/ui/UserProfilePanelFields.tsx @@ -134,10 +134,7 @@ export function useProfileFieldBuckets({ }) : []), ]; - return { - ...bucketProfileFields(metadataFields), - modelLabel: managedAgent?.model ?? persona?.model ?? "Auto", - }; + return bucketProfileFields(metadataFields); }, [ isBot, isOwner, diff --git a/desktop/src/features/profile/ui/UserProfilePanelSections.tsx b/desktop/src/features/profile/ui/UserProfilePanelSections.tsx index a257fa4478..aa201cc285 100644 --- a/desktop/src/features/profile/ui/UserProfilePanelSections.tsx +++ b/desktop/src/features/profile/ui/UserProfilePanelSections.tsx @@ -91,7 +91,6 @@ export type ProfileSummaryViewProps = { managedAgent: ManagedAgent | undefined; memoriesLoading: boolean; memoryCount: number | undefined; - modelLabel: string; agentInfoFields: ProfileField[]; agentSettingsFields: ProfileField[]; diagnosticsFields: ProfileField[]; @@ -200,7 +199,6 @@ export function ProfileSummaryView({ managedAgent, memoriesLoading, memoryCount, - modelLabel, agentInfoFields, agentSettingsFields, diagnosticsFields, @@ -240,7 +238,6 @@ export function ProfileSummaryView({ (runtimeConfigurationFields.length > 0 || runtimeSettingsFields.length > 0 || managedAgent !== undefined || - modelLabel.trim().length > 0 || diagnosticsFields.length > 0 || canOpenAgentLogs || showInstructionBlock); @@ -412,8 +409,6 @@ export function ProfileSummaryView({ agentInstruction={agentInstruction} diagnosticsFields={diagnosticsFields} diagnosticsSummary={diagnosticsTrailing} - managedAgent={managedAgent} - modelLabel={modelLabel} onOpenDiagnostics={onOpenDiagnostics} onOpenInstructions={onOpenInstructions} runtimeConfigurationFields={runtimeConfigurationFields} @@ -423,7 +418,10 @@ export function ProfileSummaryView({ /> {isOwner === true && managedAgent !== undefined ? (
- +
) : null} diff --git a/desktop/src/features/profile/ui/UserProfilePanelTabs.tsx b/desktop/src/features/profile/ui/UserProfilePanelTabs.tsx index 9f21b700a9..f19a3658b7 100644 --- a/desktop/src/features/profile/ui/UserProfilePanelTabs.tsx +++ b/desktop/src/features/profile/ui/UserProfilePanelTabs.tsx @@ -12,7 +12,6 @@ import { ProfileFieldRows, } from "@/features/profile/ui/UserProfilePanelFields"; import type { ProfilePanelTab } from "@/features/profile/ui/UserProfilePanelUtils"; -import type { ManagedAgent } from "@/shared/api/types"; import { cn } from "@/shared/lib/cn"; import { Button } from "@/shared/ui/button"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/shared/ui/tooltip"; @@ -329,8 +328,6 @@ export function ProfileRuntimeTabContent({ agentInstruction, diagnosticsFields, diagnosticsSummary, - managedAgent, - modelLabel, onOpenDiagnostics, onOpenInstructions, runtimeConfigurationFields, @@ -341,8 +338,6 @@ export function ProfileRuntimeTabContent({ agentInstruction: string | null; diagnosticsFields: ProfileField[]; diagnosticsSummary: React.ReactNode; - managedAgent: ManagedAgent | undefined; - modelLabel: string; onOpenDiagnostics: () => void; onOpenInstructions: () => void; runtimeConfigurationFields: ProfileField[]; @@ -357,10 +352,7 @@ export function ProfileRuntimeTabContent({ (field) => field.label !== "Last error" && field.label !== "Status", ); const hasRuntimeRows = - runtimeConfigurationFields.length > 0 || - runtimeSettingsFields.length > 0 || - managedAgent !== undefined || - modelLabel.trim().length > 0; + runtimeConfigurationFields.length > 0 || runtimeSettingsFields.length > 0; if ( !hasRuntimeRows && @@ -396,12 +388,7 @@ export function ProfileRuntimeTabContent({ ) : null} {hasRuntimeRows ? (
- + {runtimeSettingsFields.length > 0 ? ( ) : null} diff --git a/desktop/src/testing/e2eBridge.ts b/desktop/src/testing/e2eBridge.ts index ee3766592d..6aad5e577f 100644 --- a/desktop/src/testing/e2eBridge.ts +++ b/desktop/src/testing/e2eBridge.ts @@ -1370,10 +1370,10 @@ function buildMockConfigSurface(pubkey: string): { }, }; - // Mixed-provenance showcase — every top-level row carries a DIFFERENT origin - // so the panel witnesses four distinct provenance sentences in one frame: - // "Set in Buzz", "Inherited from persona", "From config file (...)", and - // "From environment variable (...)". + // Mixed-provenance showcase — top-level rows carry different origins so the + // panel witnesses tightened provenance labels in one frame: no label for + // Buzz-native values, plus "Persona default", "Config file (...)" and + // "Environment variable (...)". const multiOriginSurface = { runtimeId: "goose", runtimeLabel: "Goose", diff --git a/desktop/tests/e2e/config-bridge-screenshots.spec.ts b/desktop/tests/e2e/config-bridge-screenshots.spec.ts index c73f6466d4..48b2cb744f 100644 --- a/desktop/tests/e2e/config-bridge-screenshots.spec.ts +++ b/desktop/tests/e2e/config-bridge-screenshots.spec.ts @@ -175,8 +175,9 @@ test.describe("config bridge screenshots", () => { const panel = await openAgentProfileFromChannel(page, "Goose Agent"); - // The folded config panel: provenance sentences inline under each value. - await expect(panel.getByText("Set in Buzz")).toBeVisible(); + // Buzz-native fields don't need provenance chrome, but this test still needs + // a visible folded-panel row before screenshots settle. + await expect(panel.getByText("Model").first()).toBeVisible(); await settleAnimations(panel); await panel.screenshot({ path: `${SHOTS}/01-folded-config-panel.png` }); @@ -208,14 +209,13 @@ test.describe("config bridge screenshots", () => { const panel = await openAgentProfileFromChannel(page, "Multi-Origin Agent"); - // Multiple distinct provenance origins visible at once. - await expect(panel.getByText("Set in Buzz")).toBeVisible(); - await expect(panel.getByText("Inherited from persona")).toBeVisible(); + // Multiple distinct non-native provenance origins visible at once. + await expect(panel.getByText("Persona default")).toBeVisible(); await expect( - panel.getByText("From environment variable (GOOSE_MODE)"), + panel.getByText("Environment variable (GOOSE_MODE)"), ).toBeVisible(); await expect( - panel.getByText("From config file (~/.config/goose/config.yaml)").first(), + panel.getByText("Config file (~/.config/goose/config.yaml)").first(), ).toBeVisible(); await settleAnimations(panel); @@ -238,15 +238,12 @@ test.describe("config bridge screenshots", () => { await panel.screenshot({ path: `${SHOTS}/04-pre-spawn-state.png` }); }); - test("05 — advanced expanded", async ({ page }) => { + test("05 — advanced flat list", async ({ page }) => { await installMockBridge(page, { managedAgents: MANAGED_AGENTS }); const panel = await openAgentProfileFromChannel(page, "Goose Agent"); - const advancedButton = panel.getByRole("button", { name: /Advanced/i }); - await advancedButton.click(); - - // Wait for advanced fields to appear. + // Advanced runtime fields render directly in the profile panel's flat list. await expect(panel.getByText("Extension: developer")).toBeVisible(); await settleAnimations(panel); From e0602a425984c7bdf11129662d3e1929ae516241 Mon Sep 17 00:00:00 2001 From: npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w <52a228d6edf316ec6812ac3c9fc0d696ab59fc7954d77e7be31eedcddf91335b@sprout-oss.stage.blox.sqprod.co> Date: Tue, 30 Jun 2026 16:42:03 -0700 Subject: [PATCH 02/11] fix(profile): show remote runtime data to declared owners Co-authored-by: Taylor Ho Signed-off-by: Taylor Ho --- .../features/profile/ui/UserProfilePanel.tsx | 2 +- .../profile/ui/UserProfilePanelFields.tsx | 24 ++++++------ desktop/src/testing/e2eBridge.ts | 12 ++++++ desktop/tests/e2e/profile.spec.ts | 37 +++++++++++++++++++ 4 files changed, 63 insertions(+), 12 deletions(-) diff --git a/desktop/src/features/profile/ui/UserProfilePanel.tsx b/desktop/src/features/profile/ui/UserProfilePanel.tsx index 5192d8be40..01005f4ba5 100644 --- a/desktop/src/features/profile/ui/UserProfilePanel.tsx +++ b/desktop/src/features/profile/ui/UserProfilePanel.tsx @@ -771,7 +771,7 @@ export function UserProfilePanel({ const { agentInfoFields, agentSettingsFields, diagnosticsFields } = useProfileFieldBuckets({ isBot, - isOwner, + isOwner: viewerIsOwner, managedAgent, onOpenProfile, ownerAvatarUrl: ownerAvatarProfile?.avatarUrl ?? null, diff --git a/desktop/src/features/profile/ui/UserProfilePanelFields.tsx b/desktop/src/features/profile/ui/UserProfilePanelFields.tsx index f5d4b2bc55..d8a8e82b70 100644 --- a/desktop/src/features/profile/ui/UserProfilePanelFields.tsx +++ b/desktop/src/features/profile/ui/UserProfilePanelFields.tsx @@ -249,10 +249,11 @@ export function buildOwnerFields({ relayAgent: RelayAgent | undefined; }): ProfileField[] { const fields: ProfileField[] = []; - const respondToDisplayValue = managedAgent - ? managedAgent.respondTo === "owner-only" && ownerDisplayName + const respondTo = managedAgent?.respondTo ?? relayAgent?.respondTo ?? null; + const respondToDisplayValue = respondTo + ? respondTo === "owner-only" && ownerDisplayName ? ownerDisplayName - : managedAgent.respondTo.replace(/-/g, " ") + : respondTo.replace(/-/g, " ") : null; const ownerClickable = Boolean(onOpenProfile && ownerProfilePubkey); @@ -394,14 +395,15 @@ export function buildOwnerFields({ label: "Start on launch", testId: "user-profile-start-on-launch", }); - if (respondToDisplayValue) { - fields.push({ - displayValue: respondToDisplayValue, - icon: Ear, - label: "Respond to", - testId: "user-profile-respond-to", - }); - } + } + + if (respondToDisplayValue) { + fields.push({ + displayValue: respondToDisplayValue, + icon: Ear, + label: "Respond to", + testId: "user-profile-respond-to", + }); } if (managedAgent?.lastError) { diff --git a/desktop/src/testing/e2eBridge.ts b/desktop/src/testing/e2eBridge.ts index 6aad5e577f..a92b00b1f4 100644 --- a/desktop/src/testing/e2eBridge.ts +++ b/desktop/src/testing/e2eBridge.ts @@ -2867,6 +2867,18 @@ function getMockMessageStore(channelId: string): RelayEvent[] { content: "Indexing the channel catalog now.", sig: "mocksig".repeat(20).slice(0, 128), }, + // Owned remote relay agent: declared-owned by the mock viewer, + // present in the relay registry, but NOT locally managed. This + // keeps the profile Runtime-tab owner gate honest. + { + id: "mock-agents-owned-relay-nadia", + pubkey: OWNED_RELAY_AGENT_PUBKEY, + created_at: Math.floor(Date.now() / 1000) - 85, + kind: 9, + tags: [["h", channelId]], + content: "Indexing remotely for my owner.", + sig: "mocksig".repeat(20).slice(0, 128), + }, // Seed one message per managed agent that is a member of #agents. // This lets e2e specs open the profile panel by clicking the // agent's avatar in a message-row (the same pattern as charlie). diff --git a/desktop/tests/e2e/profile.spec.ts b/desktop/tests/e2e/profile.spec.ts index 0ff0e8a793..f93e112cae 100644 --- a/desktop/tests/e2e/profile.spec.ts +++ b/desktop/tests/e2e/profile.spec.ts @@ -792,6 +792,43 @@ test("renders agent profile ingress subviews from the Playwright mock bridge", a await expect(page.getByTestId("agent-memory-list")).toContainText("orphan"); }); +test("declared owner sees runtime tab for a remote relay agent", async ({ + page, +}) => { + await page.goto("/"); + + await page.getByTestId("channel-agents").click(); + await expect(page.getByTestId("chat-title")).toHaveText("agents"); + + const messageRow = page.getByTestId("message-row").filter({ + has: page.getByText("Indexing remotely for my owner."), + }); + await expect(messageRow.first()).toBeVisible({ timeout: 5_000 }); + await messageRow.first().getByRole("button").first().click(); + + const panel = page.getByTestId("user-profile-panel"); + await expect(panel).toBeVisible({ timeout: 10_000 }); + await expect(panel.getByRole("tab", { name: "Runtime" })).toBeVisible(); + await panel.getByRole("tab", { name: "Runtime" }).click(); + + await expect(panel.getByTestId("user-profile-runtime")).toContainText( + "Runtime", + ); + await expect(panel.getByTestId("user-profile-runtime")).toContainText( + "Goose", + ); + await expect(panel.getByTestId("user-profile-respond-to")).toContainText( + "anyone", + ); + + // Declared ownership grants read visibility only; local-management write UI + // stays hidden because this relay agent is not in the managed-agents list. + await expect(panel.getByText("Model").first()).toHaveCount(0); + await expect( + panel.getByRole("button", { name: /Start|Stop|Deploy/ }), + ).toHaveCount(0); +}); + test("owned agent absent from relay/managed lists still renders agent framing", async ({ page, }) => { From fd253c1ddd7d1200ac8627384ff81d8e32b0955c Mon Sep 17 00:00:00 2001 From: npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w <52a228d6edf316ec6812ac3c9fc0d696ab59fc7954d77e7be31eedcddf91335b@sprout-oss.stage.blox.sqprod.co> Date: Tue, 30 Jun 2026 17:03:06 -0700 Subject: [PATCH 03/11] fix(profile): show runtime tab for declared-owned agents Co-authored-by: Taylor Ho Signed-off-by: Taylor Ho --- .../profile/ui/UserProfilePanelFields.tsx | 9 ++++ desktop/src/testing/e2eBridge.ts | 16 ++---- desktop/tests/e2e/profile.spec.ts | 51 +++++++++++++++++++ desktop/tests/helpers/bridge.ts | 2 + 4 files changed, 66 insertions(+), 12 deletions(-) diff --git a/desktop/src/features/profile/ui/UserProfilePanelFields.tsx b/desktop/src/features/profile/ui/UserProfilePanelFields.tsx index d8a8e82b70..b6569a3ffd 100644 --- a/desktop/src/features/profile/ui/UserProfilePanelFields.tsx +++ b/desktop/src/features/profile/ui/UserProfilePanelFields.tsx @@ -60,6 +60,7 @@ const AGENT_INFO_LABELS = new Set([ ]); const AGENT_SETTINGS_LABELS = new Set([ "Runtime", + "Agent profile", "Respond to", "ACP command", "MCP command", @@ -319,6 +320,14 @@ export function buildOwnerFields({ label: "Runtime", testId: "user-profile-runtime", }); + } else if (ownerPubkey) { + fields.push({ + copyValue: ownerPubkey, + displayValue: "Declared owner verified", + icon: UserRound, + label: "Agent profile", + testId: "user-profile-agent-profile", + }); } if (managedAgent) { diff --git a/desktop/src/testing/e2eBridge.ts b/desktop/src/testing/e2eBridge.ts index a92b00b1f4..bc252811f9 100644 --- a/desktop/src/testing/e2eBridge.ts +++ b/desktop/src/testing/e2eBridge.ts @@ -54,6 +54,8 @@ type MockManagedAgentSeed = { type MockRelayAgentSeed = { pubkey: string; name: string; + agentType?: string; + capabilities?: string[]; respondTo?: RawRelayAgent["respond_to"]; respondToAllowlist?: string[]; channelNames?: string[]; @@ -1511,10 +1513,10 @@ function resetMockRelayAgents(config?: E2eConfig) { mockRelayAgents.push({ pubkey: seed.pubkey, name: seed.name, - agent_type: "goose", + agent_type: seed.agentType ?? "goose", channels: channels.map((channel) => channel.name), channel_ids: channels.map((channel) => channel.id), - capabilities: ["messages", "channels", "mcp"], + capabilities: seed.capabilities ?? ["messages", "channels", "mcp"], status: seed.status ?? "online", respond_to: seed.respondTo ?? "owner-only", respond_to_allowlist: seed.respondToAllowlist ?? [], @@ -2138,16 +2140,6 @@ const defaultMockRelayAgents: RawRelayAgent[] = [ respond_to: "anyone", respond_to_allowlist: [], }, - { - pubkey: OWNED_RELAY_AGENT_PUBKEY, - name: "nadia", - agent_type: "goose", - channels: ["agents"], - channel_ids: ["94a444a4-c0a3-5966-ab05-530c6ddc2301"], - capabilities: ["search", "summaries"], - status: "online", - respond_to: "anyone", - }, ]; let mockRelayAgents: RawRelayAgent[] = defaultMockRelayAgents.map((agent) => ({ ...agent, diff --git a/desktop/tests/e2e/profile.spec.ts b/desktop/tests/e2e/profile.spec.ts index f93e112cae..7237a4739c 100644 --- a/desktop/tests/e2e/profile.spec.ts +++ b/desktop/tests/e2e/profile.spec.ts @@ -795,6 +795,19 @@ test("renders agent profile ingress subviews from the Playwright mock bridge", a test("declared owner sees runtime tab for a remote relay agent", async ({ page, }) => { + await installMockBridge(page, { + relayAgents: [ + { + pubkey: + "a1b2c3d4e5f60718293a4b5c6d7e8f90112233445566778899aabbccddeeff00", + name: "nadia", + agentType: "goose", + capabilities: ["search", "summaries"], + channelNames: ["agents"], + respondTo: "anyone", + }, + ], + }); await page.goto("/"); await page.getByTestId("channel-agents").click(); @@ -829,6 +842,44 @@ test("declared owner sees runtime tab for a remote relay agent", async ({ ).toHaveCount(0); }); +test("declared owner sees runtime tab without a relay-agent record", async ({ + page, +}) => { + await page.goto("/"); + + await page.getByTestId("channel-agents").click(); + await expect(page.getByTestId("chat-title")).toHaveText("agents"); + + const messageRow = page.getByTestId("message-row").filter({ + has: page.getByText("Indexing remotely for my owner."), + }); + await expect(messageRow.first()).toBeVisible({ timeout: 5_000 }); + await messageRow.first().getByRole("button").first().click(); + + const panel = page.getByTestId("user-profile-panel"); + await expect(panel).toBeVisible({ timeout: 10_000 }); + await expect(panel.getByTestId("user-profile-agent-type")).toHaveCount(0); + await expect(panel.getByTestId("user-profile-capabilities")).toHaveCount(0); + await expect(panel.getByRole("tab", { name: "Runtime" })).toBeVisible(); + await panel.getByRole("tab", { name: "Runtime" }).click(); + + await expect(panel.getByTestId("user-profile-agent-profile")).toContainText( + "Agent profile", + ); + await expect(panel.getByTestId("user-profile-agent-profile")).toContainText( + "Declared owner verified", + ); + await expect(panel.getByTestId("user-profile-runtime")).toHaveCount(0); + await expect(panel.getByTestId("user-profile-respond-to")).toHaveCount(0); + + // No relay/managed runtime record means no write or management affordance — + // only the truthful NIP-OA profile signal is rendered in Runtime. + await expect(panel.getByText("Model").first()).toHaveCount(0); + await expect( + panel.getByRole("button", { name: /Start|Stop|Deploy/ }), + ).toHaveCount(0); +}); + test("owned agent absent from relay/managed lists still renders agent framing", async ({ page, }) => { diff --git a/desktop/tests/helpers/bridge.ts b/desktop/tests/helpers/bridge.ts index 2d1cc2561f..e61281845e 100644 --- a/desktop/tests/helpers/bridge.ts +++ b/desktop/tests/helpers/bridge.ts @@ -70,6 +70,8 @@ type MockSearchProfileSeed = { type MockRelayAgentSeed = { pubkey: string; name: string; + agentType?: string; + capabilities?: string[]; respondTo?: "owner-only" | "allowlist" | "anyone"; respondToAllowlist?: string[]; channelNames?: string[]; From 2f6d48fccd7bc0aa1d1f56eb3d738e06e525be3a Mon Sep 17 00:00:00 2001 From: npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w <52a228d6edf316ec6812ac3c9fc0d696ab59fc7954d77e7be31eedcddf91335b@sprout-oss.stage.blox.sqprod.co> Date: Wed, 1 Jul 2026 22:08:05 -0700 Subject: [PATCH 04/11] fix(profile): restore pre-branch provenance label wording MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Revert the runtime provenance lower-line copy to the wording on main: 'Set in Buzz', 'Inherited from persona', 'From environment variable (...)', 'From config file (...)', and 'From ACP session' — and stop suppressing the buzzExplicit provenance line. Restores the matching e2e assertions and mock-fixture comment. IA consolidation, Runtime tab split, and copy affordances are unchanged. Co-authored-by: Taylor Ho Signed-off-by: Taylor Ho --- .../src/features/agents/ui/AgentConfigPanel.tsx | 16 +++++++++------- desktop/src/testing/e2eBridge.ts | 6 +++--- .../tests/e2e/config-bridge-screenshots.spec.ts | 14 +++++++------- 3 files changed, 19 insertions(+), 17 deletions(-) diff --git a/desktop/src/features/agents/ui/AgentConfigPanel.tsx b/desktop/src/features/agents/ui/AgentConfigPanel.tsx index ae084078f6..6c7431c41d 100644 --- a/desktop/src/features/agents/ui/AgentConfigPanel.tsx +++ b/desktop/src/features/agents/ui/AgentConfigPanel.tsx @@ -103,27 +103,29 @@ function provenanceSentence( origin: ConfigOrigin, writeVia: ConfigWriteMechanism, configFilePath: string | null, -): string | null { +): string { switch (origin) { case "buzzExplicit": - return null; + return "Set in Buzz"; case "personaDefault": - return "Persona default"; + return "Inherited from persona"; case "runtimeOverride": return "Live override (this session only)"; case "harnessConstraint": return "Locked by harness"; case "envVar": { if (writeVia.type === "respawnWithEnvVar") { - return `Environment variable (${writeVia.envKey})`; + return `From environment variable (${writeVia.envKey})`; } - return "Environment variable"; + return "From environment variable"; } case "configFile": - return configFilePath ? `Config file (${configFilePath})` : "Config file"; + return configFilePath + ? `From config file (${configFilePath})` + : "From config file"; case "acpConfigOption": case "acpNativeRead": - return "ACP session"; + return "From ACP session"; } } diff --git a/desktop/src/testing/e2eBridge.ts b/desktop/src/testing/e2eBridge.ts index bc252811f9..e5063f0d44 100644 --- a/desktop/src/testing/e2eBridge.ts +++ b/desktop/src/testing/e2eBridge.ts @@ -1373,9 +1373,9 @@ function buildMockConfigSurface(pubkey: string): { }; // Mixed-provenance showcase — top-level rows carry different origins so the - // panel witnesses tightened provenance labels in one frame: no label for - // Buzz-native values, plus "Persona default", "Config file (...)" and - // "Environment variable (...)". + // panel witnesses distinct provenance labels in one frame: "Set in Buzz", + // "Inherited from persona", "From config file (...)" and + // "From environment variable (...)". const multiOriginSurface = { runtimeId: "goose", runtimeLabel: "Goose", diff --git a/desktop/tests/e2e/config-bridge-screenshots.spec.ts b/desktop/tests/e2e/config-bridge-screenshots.spec.ts index 48b2cb744f..170244ccb0 100644 --- a/desktop/tests/e2e/config-bridge-screenshots.spec.ts +++ b/desktop/tests/e2e/config-bridge-screenshots.spec.ts @@ -175,9 +175,8 @@ test.describe("config bridge screenshots", () => { const panel = await openAgentProfileFromChannel(page, "Goose Agent"); - // Buzz-native fields don't need provenance chrome, but this test still needs - // a visible folded-panel row before screenshots settle. - await expect(panel.getByText("Model").first()).toBeVisible(); + // The folded config panel: provenance sentences inline under each value. + await expect(panel.getByText("Set in Buzz").first()).toBeVisible(); await settleAnimations(panel); await panel.screenshot({ path: `${SHOTS}/01-folded-config-panel.png` }); @@ -209,13 +208,14 @@ test.describe("config bridge screenshots", () => { const panel = await openAgentProfileFromChannel(page, "Multi-Origin Agent"); - // Multiple distinct non-native provenance origins visible at once. - await expect(panel.getByText("Persona default")).toBeVisible(); + // Multiple distinct provenance origins visible at once. + await expect(panel.getByText("Set in Buzz").first()).toBeVisible(); + await expect(panel.getByText("Inherited from persona")).toBeVisible(); await expect( - panel.getByText("Environment variable (GOOSE_MODE)"), + panel.getByText("From environment variable (GOOSE_MODE)"), ).toBeVisible(); await expect( - panel.getByText("Config file (~/.config/goose/config.yaml)").first(), + panel.getByText("From config file (~/.config/goose/config.yaml)").first(), ).toBeVisible(); await settleAnimations(panel); From 3e4914dfbabc93acbdb71fcbe197cca00c01ec6a Mon Sep 17 00:00:00 2001 From: npub14vtk7pvazqrq9639qu7e560wnqtl0d53ca4gjuvq6jzf3k2el23qqlwa7f Date: Wed, 1 Jul 2026 23:09:57 -0700 Subject: [PATCH 05/11] fix(profile): move read-only icon to provenance line Co-authored-by: Taylor Ho Signed-off-by: Taylor Ho --- .../features/agents/ui/AgentConfigPanel.tsx | 37 ++++++++++--------- 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/desktop/src/features/agents/ui/AgentConfigPanel.tsx b/desktop/src/features/agents/ui/AgentConfigPanel.tsx index 6c7431c41d..befd49f94f 100644 --- a/desktop/src/features/agents/ui/AgentConfigPanel.tsx +++ b/desktop/src/features/agents/ui/AgentConfigPanel.tsx @@ -9,8 +9,8 @@ import { Cpu, Hash, Layers, - Lock, MessageSquare, + PenOff, Server, } from "lucide-react"; import { toast } from "sonner"; @@ -46,22 +46,27 @@ function isReadOnlyField({ return writeVia.type === "readOnly" || origin === "harnessConstraint"; } -function ConfigFieldLabel({ - label, +function ConfigFieldLabel({ label }: { label: string }) { + return ( + + {label} + + ); +} + +function ProvenanceHint({ locked, + provenance, }: { - label: string; locked: boolean; + provenance: string; }) { return ( - - {label} + {locked ? ( - + ) : null} + {provenance} ); } @@ -193,7 +198,7 @@ function NormalizedRow({ {variant === "profile" ? ( - + ) : ( {label} @@ -217,9 +222,7 @@ function NormalizedRow({ ) : null} {provenance ? ( - - {provenance} - + ) : null} {isCopyable ? ( @@ -292,7 +295,7 @@ function AdvancedRow({ - + {provenance ? ( - - {provenance} - + ) : null} {isCopyable ? ( From e0941c7fd162f96d16c9c5ceb0286b339a75e790 Mon Sep 17 00:00:00 2001 From: npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w <52a228d6edf316ec6812ac3c9fc0d696ab59fc7954d77e7be31eedcddf91335b@sprout-oss.stage.blox.sqprod.co> Date: Wed, 1 Jul 2026 23:38:43 -0700 Subject: [PATCH 06/11] test(profile): align e2e specs with consolidated runtime panel Two specs were still asserting pre-consolidation fixtures: - profile.spec expected the removed 'user-profile-model' testid; assert Model via the config panel's normalized row instead, since the branch deleted the duplicate Model row on purpose. - channels.spec's view-activity test relied on nadia being a default relay-agent seed, which the branch removed to make room for the no-relay-record declared-owner fixture; seed her relay registry entry explicitly in the test. Co-authored-by: Taylor Ho Signed-off-by: Taylor Ho --- desktop/tests/e2e/channels.spec.ts | 15 +++++++++++++++ desktop/tests/e2e/profile.spec.ts | 9 ++++++++- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/desktop/tests/e2e/channels.spec.ts b/desktop/tests/e2e/channels.spec.ts index 032d2ede91..94ae39f213 100644 --- a/desktop/tests/e2e/channels.spec.ts +++ b/desktop/tests/e2e/channels.spec.ts @@ -1111,6 +1111,21 @@ test("shows and clears activity indicators for active channel agents", async ({ test("members sidebar exposes view-activity for a viewer-owned relay agent", async ({ page, }) => { + // nadia is no longer in the default relay-agent seeds (the no-relay-record + // owner path claims that fixture), so seed her relay registry entry here to + // exercise the relay-bot classification. + await installMockBridge(page, { + relayAgents: [ + { + pubkey: OWNED_RELAY_AGENT_PUBKEY, + name: "nadia", + agentType: "goose", + capabilities: ["search", "summaries"], + channelNames: ["agents"], + respondTo: "anyone", + }, + ], + }); await page.goto("/"); await openMembersSidebar(page, "agents"); diff --git a/desktop/tests/e2e/profile.spec.ts b/desktop/tests/e2e/profile.spec.ts index 7237a4739c..becf1aed95 100644 --- a/desktop/tests/e2e/profile.spec.ts +++ b/desktop/tests/e2e/profile.spec.ts @@ -732,7 +732,14 @@ test("renders agent profile ingress subviews from the Playwright mock bridge", a await page.getByTestId("user-profile-tab-runtime").click(); await expectHashSearchParam(page, "profileTab", "runtime"); - await expect(page.getByTestId("user-profile-model")).toBeVisible(); + // The dedicated Model row was consolidated into the runtime config panel; + // Model now renders as a normalized config row with real provenance. + await expect( + page + .getByTestId("user-profile-panel") + .getByText("Model", { exact: true }) + .first(), + ).toBeVisible(); await expect(page.getByTestId("user-profile-respond-to")).toBeVisible(); await page.getByTestId("user-profile-settings-menu-trigger").click(); From 4c18e207df55c28abc67401461ae1a212fcbcb51 Mon Sep 17 00:00:00 2001 From: npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w <52a228d6edf316ec6812ac3c9fc0d696ab59fc7954d77e7be31eedcddf91335b@sprout-oss.stage.blox.sqprod.co> Date: Thu, 2 Jul 2026 09:48:59 -0700 Subject: [PATCH 07/11] fix(profile): delete dead hand-built Model field source MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit buildOwnerFields still pushed a Model field (user-profile-model testid) that bucketProfileFields silently dropped — 'Model' is not in any bucket set, so those lines could never render. Model now comes exclusively from the consolidated config panel with real provenance. Addresses Will's review on #1451. Co-authored-by: Taylor Ho Signed-off-by: Taylor Ho --- .../profile/ui/UserProfilePanelFields.tsx | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/desktop/src/features/profile/ui/UserProfilePanelFields.tsx b/desktop/src/features/profile/ui/UserProfilePanelFields.tsx index b6569a3ffd..d83fab6f98 100644 --- a/desktop/src/features/profile/ui/UserProfilePanelFields.tsx +++ b/desktop/src/features/profile/ui/UserProfilePanelFields.tsx @@ -348,24 +348,6 @@ export function buildOwnerFields({ }); } - if (managedAgent?.model) { - fields.push({ - copyValue: managedAgent.model, - displayValue: managedAgent.model, - icon: Cpu, - label: "Model", - testId: "user-profile-model", - }); - } else if (persona?.model) { - fields.push({ - copyValue: persona.model, - displayValue: persona.model, - icon: Cpu, - label: "Model", - testId: "user-profile-model", - }); - } - if (managedAgent?.acpCommand) { fields.push({ copyValue: managedAgent.acpCommand, From 72659d0e01a5f16b0b8a19dfc99a7f4973c0cf8b Mon Sep 17 00:00:00 2001 From: npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w <52a228d6edf316ec6812ac3c9fc0d696ab59fc7954d77e7be31eedcddf91335b@sprout-oss.stage.blox.sqprod.co> Date: Thu, 2 Jul 2026 09:49:28 -0700 Subject: [PATCH 08/11] refactor(desktop): consolidate clipboard copy into a shared helper MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit copyToClipboard was duplicated across AgentConfigPanel, UserProfilePanelFields, CopyButton, MessageActionBar, and CustomChannelSection — some copies let writeText rejections escape as unhandled promise rejections with no user feedback. Replace all five with shared/lib/clipboard's copyTextToClipboard, which surfaces failures as an error toast. Addresses Will's review on #1451. Co-authored-by: Taylor Ho Signed-off-by: Taylor Ho --- .../features/agents/ui/AgentConfigPanel.tsx | 16 +++++++-------- desktop/src/features/agents/ui/CopyButton.tsx | 7 ++----- .../features/messages/ui/MessageActionBar.tsx | 20 ++++++------------- .../profile/ui/UserProfilePanelFields.tsx | 12 ++++------- .../sidebar/ui/CustomChannelSection.tsx | 18 +++-------------- desktop/src/shared/lib/clipboard.ts | 20 +++++++++++++++++++ 6 files changed, 42 insertions(+), 51 deletions(-) create mode 100644 desktop/src/shared/lib/clipboard.ts diff --git a/desktop/src/features/agents/ui/AgentConfigPanel.tsx b/desktop/src/features/agents/ui/AgentConfigPanel.tsx index befd49f94f..631f1209ab 100644 --- a/desktop/src/features/agents/ui/AgentConfigPanel.tsx +++ b/desktop/src/features/agents/ui/AgentConfigPanel.tsx @@ -13,10 +13,9 @@ import { PenOff, Server, } from "lucide-react"; -import { toast } from "sonner"; - import { useAgentConfigSurface } from "../hooks"; import { cn } from "@/shared/lib/cn"; +import { copyTextToClipboard } from "@/shared/lib/clipboard"; import { Spinner } from "@/shared/ui/spinner"; import type { ConfigField, @@ -31,11 +30,6 @@ type Props = { advancedMode?: "collapsed" | "flat"; }; -async function copyToClipboard(value: string, label: string) { - await navigator.clipboard.writeText(value); - toast.success(`Copied ${label}`); -} - function isReadOnlyField({ origin, writeVia, @@ -236,7 +230,9 @@ function NormalizedRow({