From 7491b891e8617adb83ca39c242d1412fb0e97d17 Mon Sep 17 00:00:00 2001 From: Will Pfleger Date: Tue, 14 Jul 2026 10:33:42 -0400 Subject: [PATCH 1/4] feat(desktop): surface needsRestart badge on live UI surfaces The "Restart required" badge existed only on the unreachable ManagedAgentRow (orphaned since #1199 replaced the list with the AgentIdentityCard grid). Wire needsRestart into the two live surfaces: 1. AgentIdentityCard: new optional statusBadge prop, rendered below the label. Both PersonaAgentCard and StandaloneAgentCard pass a warning badge (Badge variant="warning" + RefreshCw) when needsRestart is true. 2. Profile panel Runtime tab: needsRestart banner at the top of ProfileRuntimeTabContent with explainer copy ("will restart automatically after ~3 minutes idle, or restart manually for immediate effect"). No behavior changes to the auto-restart policy loop, AUTO_RESTART_QUIESCENCE_MS, or the summary builder. --- .../features/agents/ui/AgentIdentityCard.tsx | 4 +++ .../agents/ui/UnifiedAgentsSection.tsx | 25 +++++++++++++++- .../profile/ui/UserProfilePanelSections.tsx | 1 + .../profile/ui/UserProfilePanelTabs.tsx | 30 ++++++++++++++++++- 4 files changed, 58 insertions(+), 2 deletions(-) diff --git a/desktop/src/features/agents/ui/AgentIdentityCard.tsx b/desktop/src/features/agents/ui/AgentIdentityCard.tsx index e508c8b687..69f2770338 100644 --- a/desktop/src/features/agents/ui/AgentIdentityCard.tsx +++ b/desktop/src/features/agents/ui/AgentIdentityCard.tsx @@ -13,6 +13,8 @@ type AgentIdentityCardProps = { label: string; modelLabel?: string | null; onClick: () => void; + /** Optional badge rendered below the label (e.g. "Restart required"). */ + statusBadge?: ReactNode; }; export function AgentIdentityCard({ @@ -24,6 +26,7 @@ export function AgentIdentityCard({ label, modelLabel, onClick, + statusBadge, }: AgentIdentityCardProps) { const trimmedAvatarUrl = avatarUrl?.trim() || null; @@ -74,6 +77,7 @@ export function AgentIdentityCard({ {modelLabel} ) : null} + {statusBadge} ); diff --git a/desktop/src/features/agents/ui/UnifiedAgentsSection.tsx b/desktop/src/features/agents/ui/UnifiedAgentsSection.tsx index da4d412aa0..a67758bdd9 100644 --- a/desktop/src/features/agents/ui/UnifiedAgentsSection.tsx +++ b/desktop/src/features/agents/ui/UnifiedAgentsSection.tsx @@ -1,5 +1,11 @@ import * as React from "react"; -import { ChevronDown, ChevronRight, Ellipsis, OctagonX } from "lucide-react"; +import { + ChevronDown, + ChevronRight, + Ellipsis, + OctagonX, + RefreshCw, +} from "lucide-react"; import { formatAgentModelLabel } from "@/features/agents/lib/formatAgentModelLabel"; import { friendlyAgentLastError } from "@/features/agents/lib/friendlyAgentLastError"; @@ -9,6 +15,7 @@ import type { AgentPersona, ManagedAgent } from "@/shared/api/types"; import type { ProfilePanelOpenOptions } from "@/shared/context/ProfilePanelContext"; import { useFeedbackToasts } from "@/shared/hooks/useToastEffect"; import { useFileImportZone } from "@/shared/hooks/useFileImportZone"; +import { Badge } from "@/shared/ui/badge"; import { Button } from "@/shared/ui/button"; import { DropdownMenu, @@ -344,6 +351,14 @@ function AgentPersonaCard({ } onOpenPersonaProfile(persona); }} + statusBadge={ + agent?.needsRestart ? ( + + + Restart required + + ) : null + } /> ); } @@ -400,6 +415,14 @@ function StandaloneAgentCard({ opensRuntimeTab ? { tab: "runtime" } : undefined, ); }} + statusBadge={ + agent.needsRestart ? ( + + + Restart required + + ) : null + } /> ); } diff --git a/desktop/src/features/profile/ui/UserProfilePanelSections.tsx b/desktop/src/features/profile/ui/UserProfilePanelSections.tsx index 3e11b81858..79aa028dd2 100644 --- a/desktop/src/features/profile/ui/UserProfilePanelSections.tsx +++ b/desktop/src/features/profile/ui/UserProfilePanelSections.tsx @@ -400,6 +400,7 @@ export function ProfileSummaryView({ agentInstruction={agentInstruction} diagnosticsFields={diagnosticsFields} diagnosticsSummary={diagnosticsTrailing} + needsRestart={managedAgent?.needsRestart ?? false} onOpenDiagnostics={onOpenDiagnostics} onOpenInstructions={onOpenInstructions} runtimeConfigurationFields={runtimeConfigurationFields} diff --git a/desktop/src/features/profile/ui/UserProfilePanelTabs.tsx b/desktop/src/features/profile/ui/UserProfilePanelTabs.tsx index 7659aa9068..7c3317f59e 100644 --- a/desktop/src/features/profile/ui/UserProfilePanelTabs.tsx +++ b/desktop/src/features/profile/ui/UserProfilePanelTabs.tsx @@ -1,6 +1,13 @@ import * as React from "react"; import type { LucideIcon } from "lucide-react"; -import { Activity, Archive, ChevronRight, Info, Wrench } from "lucide-react"; +import { + Activity, + Archive, + ChevronRight, + Info, + RefreshCw, + Wrench, +} from "lucide-react"; import type { ActiveTurnSummary } from "@/features/agents/activeAgentTurnsStore"; import { ManagedAgentSessionPanel } from "@/features/agents/ui/ManagedAgentSessionPanel"; @@ -728,6 +735,7 @@ export function ProfileRuntimeTabContent({ agentInstruction, diagnosticsFields, diagnosticsSummary, + needsRestart = false, onOpenDiagnostics, onOpenInstructions, runtimeConfigurationFields, @@ -738,6 +746,8 @@ export function ProfileRuntimeTabContent({ agentInstruction: string | null; diagnosticsFields: ProfileField[]; diagnosticsSummary: React.ReactNode; + /** True when the running agent's config has drifted from what it was spawned with. */ + needsRestart?: boolean; onOpenDiagnostics: () => void; onOpenInstructions: () => void; runtimeConfigurationFields: ProfileField[]; @@ -766,6 +776,24 @@ export function ProfileRuntimeTabContent({ return (
+ {needsRestart ? ( +
+ +
+

+ Restart required +

+

+ Configuration changed since this agent started. It will restart + automatically after ~3 minutes idle, or restart manually for + immediate effect. +

+
+
+ ) : null} {showInstructionBlock ? (
Date: Tue, 14 Jul 2026 10:58:52 -0400 Subject: [PATCH 2/4] test(desktop): add screenshot spec for needsRestart badge and banner Wire `needs_restart` through the e2eBridge mock so E2E tests can seed agents with config drift. Three screenshots: standalone card badge, persona card badge, and Runtime tab amber banner. --- desktop/playwright.config.ts | 1 + desktop/src/testing/e2eBridge.ts | 4 + .../e2e/needs-restart-screenshots.spec.ts | 122 ++++++++++++++++++ desktop/tests/helpers/bridge.ts | 1 + 4 files changed, 128 insertions(+) create mode 100644 desktop/tests/e2e/needs-restart-screenshots.spec.ts diff --git a/desktop/playwright.config.ts b/desktop/playwright.config.ts index b118e69e02..e6c59756af 100644 --- a/desktop/playwright.config.ts +++ b/desktop/playwright.config.ts @@ -42,6 +42,7 @@ export default defineConfig({ "**/local-archive-screenshots.spec.ts", "**/agent-readiness-screenshots.spec.ts", "**/agent-error-state-screenshots.spec.ts", + "**/needs-restart-screenshots.spec.ts", "**/edit-agent.spec.ts", "**/doctor-cta-screenshots.spec.ts", "**/pubkey-display-screenshots.spec.ts", diff --git a/desktop/src/testing/e2eBridge.ts b/desktop/src/testing/e2eBridge.ts index 41054deab2..df993da573 100644 --- a/desktop/src/testing/e2eBridge.ts +++ b/desktop/src/testing/e2eBridge.ts @@ -68,6 +68,7 @@ type MockManagedAgentSeed = { backend?: RawManagedAgent["backend"]; lastError?: string | null; lastErrorCode?: number | null; + needsRestart?: boolean; respondTo?: RawManagedAgent["respond_to"]; respondToAllowlist?: string[]; }; @@ -537,6 +538,7 @@ type RawManagedAgent = { last_exit_code: number | null; last_error: string | null; last_error_code: number | null; + needs_restart?: boolean; log_path: string; start_on_app_launch: boolean; auto_restart_on_config_change?: boolean; @@ -1197,6 +1199,7 @@ function cloneManagedAgent(agent: MockManagedAgent): RawManagedAgent { last_exit_code: agent.last_exit_code, last_error: agent.last_error, last_error_code: agent.last_error_code, + needs_restart: agent.needs_restart ?? false, log_path: agent.log_path, start_on_app_launch: agent.start_on_app_launch, auto_restart_on_config_change: agent.auto_restart_on_config_change ?? true, @@ -1728,6 +1731,7 @@ function buildSeededManagedAgent(seed: MockManagedAgentSeed): MockManagedAgent { last_exit_code: null, last_error: seed.lastError ?? null, last_error_code: seed.lastErrorCode ?? null, + needs_restart: seed.needsRestart ?? false, log_path: `/tmp/mock-agent-${seed.pubkey}.log`, start_on_app_launch: true, auto_restart_on_config_change: true, diff --git a/desktop/tests/e2e/needs-restart-screenshots.spec.ts b/desktop/tests/e2e/needs-restart-screenshots.spec.ts new file mode 100644 index 0000000000..26478f454e --- /dev/null +++ b/desktop/tests/e2e/needs-restart-screenshots.spec.ts @@ -0,0 +1,122 @@ +/** + * Screenshot spec for the needsRestart badge and banner (PR #1853). + * + * Exercises two surfaces: + * - Agent grid card: warning badge ("Restart required") on standalone and + * persona-backed cards when `needsRestart: true`. + * - Profile panel Runtime tab: amber banner explaining auto-restart behavior. + */ + +import { expect, test } from "@playwright/test"; + +import { installMockBridge, TEST_IDENTITIES } from "../helpers/bridge"; +import { waitForAnimations } from "../helpers/animations"; + +const SHOTS = "test-results/pr-1853-screenshots"; + +const STANDALONE_AGENT = { + pubkey: TEST_IDENTITIES.alice.pubkey, + name: "Local Agent", + status: "running" as const, + needsRestart: true, +}; + +const PERSONA_AGENT = { + pubkey: TEST_IDENTITIES.bob.pubkey, + name: "Persona Agent", + personaId: "builtin:fizz", + status: "running" as const, + needsRestart: true, +}; + +async function gotoAgentsView(page: import("@playwright/test").Page) { + await page.goto("/", { waitUntil: "domcontentloaded" }); + await expect(page.getByTestId("open-agents-view")).toBeVisible({ + timeout: 10_000, + }); + await page.getByTestId("open-agents-view").click(); + await expect(page.getByTestId("agents-library-personas")).toBeVisible({ + timeout: 10_000, + }); +} + +test.describe("needs-restart screenshots", () => { + test.use({ viewport: { width: 1280, height: 900 } }); + + test.beforeEach(async ({ page }) => { + page.on("pageerror", (err) => { + console.error( + "PAGE ERROR:", + err.message, + err.stack?.split("\n").slice(0, 5).join("\n"), + ); + }); + }); + + test("01-grid-standalone-restart-badge", async ({ page }) => { + await installMockBridge(page, { + managedAgents: [STANDALONE_AGENT], + }); + + await gotoAgentsView(page); + + const agentCard = page.getByTestId( + `managed-agent-${STANDALONE_AGENT.pubkey}`, + ); + await expect(agentCard).toBeVisible({ timeout: 10_000 }); + await waitForAnimations(page); + + await agentCard.screenshot({ + path: `${SHOTS}/01-grid-standalone-restart-badge.png`, + }); + }); + + test("02-grid-persona-restart-badge", async ({ page }) => { + await installMockBridge(page, { + activePersonaIds: ["builtin:fizz"], + managedAgents: [PERSONA_AGENT], + }); + + await gotoAgentsView(page); + + const personaCard = page.getByTestId( + `persona-agent-row-${PERSONA_AGENT.personaId}`, + ); + await expect(personaCard).toBeVisible({ timeout: 10_000 }); + await waitForAnimations(page); + + await personaCard.screenshot({ + path: `${SHOTS}/02-grid-persona-restart-badge.png`, + }); + }); + + test("03-runtime-tab-restart-banner", async ({ page }) => { + await installMockBridge(page, { + managedAgents: [STANDALONE_AGENT], + }); + + await gotoAgentsView(page); + + // Click the agent card to open the profile panel. + const agentButton = page.getByRole("button", { + name: `${STANDALONE_AGENT.name} agent profile`, + }); + await expect(agentButton).toBeVisible({ timeout: 10_000 }); + await agentButton.click(); + + const panel = page.getByTestId("user-profile-panel"); + await expect(panel).toBeVisible({ timeout: 10_000 }); + + // Switch to the Runtime tab. + await panel.getByRole("tab", { name: "Runtime" }).click(); + + // Wait for the restart banner to appear. + const banner = panel.getByTestId("needs-restart-banner"); + await expect(banner).toBeVisible({ timeout: 10_000 }); + await waitForAnimations(page); + + await banner.screenshot({ + path: `${SHOTS}/03-runtime-tab-restart-banner.png`, + }); + }); +}); diff --git a/desktop/tests/helpers/bridge.ts b/desktop/tests/helpers/bridge.ts index 27e3fef136..4eb1d2e8be 100644 --- a/desktop/tests/helpers/bridge.ts +++ b/desktop/tests/helpers/bridge.ts @@ -54,6 +54,7 @@ type MockManagedAgentSeed = { | { type: "provider"; id: string; config: Record }; lastError?: string | null; lastErrorCode?: number | null; + needsRestart?: boolean; respondTo?: "owner-only" | "allowlist" | "anyone"; respondToAllowlist?: string[]; }; From 63673977e9ed34dbb21bf5057ad5c7ca263d0631 Mon Sep 17 00:00:00 2001 From: Will Pfleger Date: Tue, 14 Jul 2026 11:21:53 -0400 Subject: [PATCH 3/4] fix(desktop): branch restart banner on auto-restart toggle + strengthen spec - ProfileRuntimeTabContent now accepts autoRestartEnabled prop; banner copy hedges when ON, states "off" when the per-agent toggle is OFF. - Spec tests 01/02 assert badge text visibility within the card scope; test 01 also verifies a non-drifted agent has no badge (false-state). - New test 04 captures the disabled-copy banner with autoRestartOnConfigChange seeded false through both bridge helpers. --- .../profile/ui/UserProfilePanelSections.tsx | 3 + .../profile/ui/UserProfilePanelTabs.tsx | 9 ++- desktop/src/testing/e2eBridge.ts | 3 +- .../e2e/needs-restart-screenshots.spec.ts | 77 +++++++++++++++++-- desktop/tests/helpers/bridge.ts | 1 + 5 files changed, 83 insertions(+), 10 deletions(-) diff --git a/desktop/src/features/profile/ui/UserProfilePanelSections.tsx b/desktop/src/features/profile/ui/UserProfilePanelSections.tsx index 79aa028dd2..53d98a25a4 100644 --- a/desktop/src/features/profile/ui/UserProfilePanelSections.tsx +++ b/desktop/src/features/profile/ui/UserProfilePanelSections.tsx @@ -398,6 +398,9 @@ export function ProfileSummaryView({ <>

- Configuration changed since this agent started. It will restart - automatically after ~3 minutes idle, or restart manually for - immediate effect. + {autoRestartEnabled + ? "Configuration changed since this agent started. Buzz can restart it automatically after ~3 minutes idle, or restart manually to apply now." + : "Configuration changed since this agent started. Automatic restart is off for this agent \u2014 restart manually to apply the changes."}

diff --git a/desktop/src/testing/e2eBridge.ts b/desktop/src/testing/e2eBridge.ts index df993da573..2786eceb82 100644 --- a/desktop/src/testing/e2eBridge.ts +++ b/desktop/src/testing/e2eBridge.ts @@ -69,6 +69,7 @@ type MockManagedAgentSeed = { lastError?: string | null; lastErrorCode?: number | null; needsRestart?: boolean; + autoRestartOnConfigChange?: boolean; respondTo?: RawManagedAgent["respond_to"]; respondToAllowlist?: string[]; }; @@ -1734,7 +1735,7 @@ function buildSeededManagedAgent(seed: MockManagedAgentSeed): MockManagedAgent { needs_restart: seed.needsRestart ?? false, log_path: `/tmp/mock-agent-${seed.pubkey}.log`, start_on_app_launch: true, - auto_restart_on_config_change: true, + auto_restart_on_config_change: seed.autoRestartOnConfigChange ?? true, backend: seed.backend ?? { type: "local" }, backend_agent_id: null, respond_to: seed.respondTo ?? "owner-only", diff --git a/desktop/tests/e2e/needs-restart-screenshots.spec.ts b/desktop/tests/e2e/needs-restart-screenshots.spec.ts index 26478f454e..7c836974f7 100644 --- a/desktop/tests/e2e/needs-restart-screenshots.spec.ts +++ b/desktop/tests/e2e/needs-restart-screenshots.spec.ts @@ -3,8 +3,9 @@ * * Exercises two surfaces: * - Agent grid card: warning badge ("Restart required") on standalone and - * persona-backed cards when `needsRestart: true`. - * - Profile panel Runtime tab: amber banner explaining auto-restart behavior. + * persona-backed cards when `needsRestart: true`, absent when false. + * - Profile panel Runtime tab: amber banner with copy branched on the + * per-agent auto-restart toggle. */ import { expect, test } from "@playwright/test"; @@ -29,6 +30,14 @@ const PERSONA_AGENT = { needsRestart: true, }; +/** A running agent with no config drift — badge must be absent. */ +const NO_DRIFT_AGENT = { + pubkey: TEST_IDENTITIES.tyler.pubkey, + name: "Stable Agent", + status: "running" as const, + needsRestart: false, +}; + async function gotoAgentsView(page: import("@playwright/test").Page) { await page.goto("/", { waitUntil: "domcontentloaded" }); await expect(page.getByTestId("open-agents-view")).toBeVisible({ @@ -55,17 +64,30 @@ test.describe("needs-restart screenshots", () => { test("01-grid-standalone-restart-badge", async ({ page }) => { await installMockBridge(page, { - managedAgents: [STANDALONE_AGENT], + managedAgents: [STANDALONE_AGENT, NO_DRIFT_AGENT], }); await gotoAgentsView(page); + // Drifted card shows the badge. const agentCard = page.getByTestId( `managed-agent-${STANDALONE_AGENT.pubkey}`, ); await expect(agentCard).toBeVisible({ timeout: 10_000 }); - await waitForAnimations(page); + await expect( + agentCard.getByText("Restart required", { exact: true }), + ).toBeVisible(); + // Non-drifted card does NOT show the badge. + const stableCard = page.getByTestId( + `managed-agent-${NO_DRIFT_AGENT.pubkey}`, + ); + await expect(stableCard).toBeVisible({ timeout: 10_000 }); + await expect( + stableCard.getByText("Restart required", { exact: true }), + ).toHaveCount(0); + + await waitForAnimations(page); await agentCard.screenshot({ path: `${SHOTS}/01-grid-standalone-restart-badge.png`, }); @@ -83,8 +105,11 @@ test.describe("needs-restart screenshots", () => { `persona-agent-row-${PERSONA_AGENT.personaId}`, ); await expect(personaCard).toBeVisible({ timeout: 10_000 }); - await waitForAnimations(page); + await expect( + personaCard.getByText("Restart required", { exact: true }), + ).toBeVisible(); + await waitForAnimations(page); await personaCard.screenshot({ path: `${SHOTS}/02-grid-persona-restart-badge.png`, }); @@ -113,10 +138,50 @@ test.describe("needs-restart screenshots", () => { // Wait for the restart banner to appear. const banner = panel.getByTestId("needs-restart-banner"); await expect(banner).toBeVisible({ timeout: 10_000 }); - await waitForAnimations(page); + // Auto-restart defaults ON — verify the enabled copy. + await expect( + banner.getByText("Buzz can restart it automatically"), + ).toBeVisible(); + + await waitForAnimations(page); await banner.screenshot({ path: `${SHOTS}/03-runtime-tab-restart-banner.png`, }); }); + + test("04-runtime-tab-restart-banner-auto-off", async ({ page }) => { + const agentAutoOff = { + ...STANDALONE_AGENT, + autoRestartOnConfigChange: false, + }; + + await installMockBridge(page, { + managedAgents: [agentAutoOff], + }); + + await gotoAgentsView(page); + + const agentButton = page.getByRole("button", { + name: `${agentAutoOff.name} agent profile`, + }); + await expect(agentButton).toBeVisible({ timeout: 10_000 }); + await agentButton.click(); + + const panel = page.getByTestId("user-profile-panel"); + await expect(panel).toBeVisible({ timeout: 10_000 }); + + await panel.getByRole("tab", { name: "Runtime" }).click(); + + const banner = panel.getByTestId("needs-restart-banner"); + await expect(banner).toBeVisible({ timeout: 10_000 }); + + // Auto-restart OFF — verify the disabled copy. + await expect(banner.getByText("Automatic restart is off")).toBeVisible(); + + await waitForAnimations(page); + await banner.screenshot({ + path: `${SHOTS}/04-runtime-tab-restart-banner-auto-off.png`, + }); + }); }); diff --git a/desktop/tests/helpers/bridge.ts b/desktop/tests/helpers/bridge.ts index 4eb1d2e8be..00e7feeb84 100644 --- a/desktop/tests/helpers/bridge.ts +++ b/desktop/tests/helpers/bridge.ts @@ -55,6 +55,7 @@ type MockManagedAgentSeed = { lastError?: string | null; lastErrorCode?: number | null; needsRestart?: boolean; + autoRestartOnConfigChange?: boolean; respondTo?: "owner-only" | "allowlist" | "anyone"; respondToAllowlist?: string[]; }; From 1754e10c8ffd30a0cf763f14c0371504e3477842 Mon Sep 17 00:00:00 2001 From: Will Pfleger Date: Tue, 14 Jul 2026 12:01:23 -0400 Subject: [PATCH 4/4] fix(desktop): align restart banner copy with available panel actions The profile panel exposes Stop (then Respawn), not a direct Restart action. Update both banner copy branches to say "stop and respawn" instead of "restart manually" so the guidance matches the available controls. --- desktop/src/features/profile/ui/UserProfilePanelTabs.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/desktop/src/features/profile/ui/UserProfilePanelTabs.tsx b/desktop/src/features/profile/ui/UserProfilePanelTabs.tsx index 41bd041ea8..d700320321 100644 --- a/desktop/src/features/profile/ui/UserProfilePanelTabs.tsx +++ b/desktop/src/features/profile/ui/UserProfilePanelTabs.tsx @@ -791,8 +791,8 @@ export function ProfileRuntimeTabContent({

{autoRestartEnabled - ? "Configuration changed since this agent started. Buzz can restart it automatically after ~3 minutes idle, or restart manually to apply now." - : "Configuration changed since this agent started. Automatic restart is off for this agent \u2014 restart manually to apply the changes."} + ? "Configuration changed since this agent started. Buzz can restart it automatically after ~3 minutes idle, or stop and respawn it to apply now." + : "Configuration changed since this agent started. Automatic restart is off for this agent \u2014 stop and respawn it to apply the changes."}