From 105360eb6d64f72710e8021b9e8cdc3454bf3c2b Mon Sep 17 00:00:00 2001 From: Logan Johnson Date: Fri, 4 Sep 2026 17:46:29 -0400 Subject: [PATCH 1/4] feat(desktop): mount Start Restart and Move controls Signed-off-by: Logan Johnson --- .../ui/DesktopLifecycleControl.test.mjs | 133 +++++++++++ .../agents/ui/DesktopLifecycleControl.tsx | 226 ++++++++++++++++++ .../src/features/agents/ui/KnownDesktops.tsx | 18 +- 3 files changed, 375 insertions(+), 2 deletions(-) create mode 100644 desktop/src/features/agents/ui/DesktopLifecycleControl.test.mjs create mode 100644 desktop/src/features/agents/ui/DesktopLifecycleControl.tsx diff --git a/desktop/src/features/agents/ui/DesktopLifecycleControl.test.mjs b/desktop/src/features/agents/ui/DesktopLifecycleControl.test.mjs new file mode 100644 index 00000000000..d6919521225 --- /dev/null +++ b/desktop/src/features/agents/ui/DesktopLifecycleControl.test.mjs @@ -0,0 +1,133 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import React from "react"; +import { JSDOM } from "jsdom"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { DesktopLifecycleControl } from "./DesktopLifecycleControl.tsx"; +import { relayClient } from "../../../shared/api/relayClient.ts"; + +test("mounted Start exposes unavailable provisioning and exact retry; Restart resolves source", async () => { + const dom = new JSDOM("
", { + url: "https://desktop.test", + }); + Object.assign(globalThis, { + window: dom.window, + document: dom.window.document, + localStorage: dom.window.localStorage, + IS_REACT_ACT_ENVIRONMENT: true, + }); + const { createRoot } = await import("react-dom/client"); + const client = new QueryClient({ + defaultOptions: { queries: { retry: false, staleTime: Infinity } }, + }); + client.setQueryData( + ["relay-agents"], + [ + { pubkey: "agent", name: "Owned agent", ownerPubkey: "owner" }, + { pubkey: "foreign", name: "Foreign agent", ownerPubkey: "other" }, + ], + ); + const scope = { owner: "owner", community: "wss://one.example" }; + const originals = { + fetch: relayClient.fetchEvents, + publish: relayClient.publishEvent, + }; + const prepared = [], + sent = []; + let stop = "failed"; + window.__TAURI_INTERNALS__ = { + invoke: async (command, args) => { + assert.equal(args.owner, scope.owner); + assert.equal(args.community, scope.community); + if (command === "observe_desktop_placement") return; + if (command === "read_desktop_placement") return ["source", "selection"]; + if ( + command === "prepare_desktop_lifecycle" || + command === "prepare_desktop_stop" + ) { + const request = { + id: `request-${prepared.length}`, + kind: command.endsWith("_stop") ? 50180 : 50182, + ...args, + }; + prepared.push(request); + return request; + } + if (command === "read_desktop_lifecycle_results") + return args.request.action === "status" + ? "running" + : "provisioning_unavailable"; + if (command === "read_desktop_stop_results") return stop; + throw Error(command); + }, + }; + relayClient.fetchEvents = async () => []; + relayClient.publishEvent = async (event, _timeout, _failure, check) => { + check(); + sent.push(event); + }; + const root = createRoot(document.getElementById("root")); + const click = (text) => + React.act(async () => + [...document.querySelectorAll("button")] + .find((b) => b.textContent === text) + .click(), + ); + const select = (label, value) => + React.act(async () => { + const element = document.querySelector(`select[aria-label="${label}"]`); + element.value = value; + element.dispatchEvent(new dom.window.Event("change", { bubbles: true })); + }); + try { + await React.act(async () => + root.render( + React.createElement( + QueryClientProvider, + { client }, + React.createElement(DesktopLifecycleControl, { + scope, + desktops: [ + { id: "source", name: "Source" }, + { id: "destination", name: "Destination" }, + ], + }), + ), + ), + ); + assert.doesNotMatch(document.body.textContent, /Foreign agent/); + await select("Agent to place", "agent"); + await select("Destination Desktop", "destination"); + await click("Start on destination"); + assert.match( + document.body.textContent, + /keyless launch provisioning is unavailable/, + ); + assert.equal(prepared[0].desktop, "destination"); + await click("Retry same request"); + assert.equal(prepared.length, 1); + assert.equal(sent[0], sent[1]); + await click("Restart on current Desktop"); + const restart = prepared.at(-1); + assert.equal( + restart.desktop, + "source", + "destination picker must not redirect Restart", + ); + assert.equal(restart.action, "restart"); + assert.equal(restart.observed, prepared.at(-2).id); + await click("Move to destination"); + assert.match(document.body.textContent, /destination was not started/); + const count = prepared.length; + stop = "stopped"; + await React.act(async () => {}); + assert.equal(prepared.length, count, "late Stop cannot resume failed Move"); + assert.doesNotMatch(document.body.textContent, /Retry same request/); + } finally { + await React.act(async () => root.unmount()); + client.clear(); + relayClient.fetchEvents = originals.fetch; + relayClient.publishEvent = originals.publish; + dom.window.close(); + } +}); diff --git a/desktop/src/features/agents/ui/DesktopLifecycleControl.tsx b/desktop/src/features/agents/ui/DesktopLifecycleControl.tsx new file mode 100644 index 00000000000..5baffa6f4cb --- /dev/null +++ b/desktop/src/features/agents/ui/DesktopLifecycleControl.tsx @@ -0,0 +1,226 @@ +import { useEffect, useRef, useState } from "react"; +import { Button } from "@/shared/ui/button"; +import type { RelayEvent } from "@/shared/api/types"; +import type { DesktopRow, DesktopScope } from "../desktopList"; +import { + lifecycleClient, + receiveLifecycle, + type LifecycleOutcome, +} from "../desktopLifecycle"; +import { useRelayAgentsQuery } from "../hooks"; + +export function DesktopLifecycleReceiver({ + scope, +}: { + scope: DesktopScope | null; +}) { + const [error, setError] = useState(""); + const { owner, community } = scope ?? {}; + useEffect(() => { + if (!owner || !community) return; + let active = true; + let close: (() => void) | undefined; + setError(""); + void receiveLifecycle({ owner, community }, () => active, setError) + .then((fn) => { + if (active) close = fn; + else fn(); + }) + .catch(() => { + if (active) setError("Desktop lifecycle receiver is unavailable."); + }); + return () => { + active = false; + close?.(); + }; + }, [owner, community]); + return error ? ( +

+ {error} +

+ ) : null; +} +function message(outcome: LifecycleOutcome) { + switch (outcome) { + case "running": + return "Desktop confirmed a running local process. This does not prove model readiness."; + case "provisioning_unavailable": + return "Destination keyless launch provisioning is unavailable. No new process was started."; + case "stopped": + return "Desktop reports the agent stopped."; + case "failed": + return "Desktop rejected or failed the operation. No successful launch was confirmed."; + default: + return "Operation unconfirmed. A dispatched effect may still finish; no automatic retry will run."; + } +} +/** Start/Move choose destination; Restart has no host picker and resolves actual current state. */ +export function DesktopLifecycleControl({ + scope, + desktops, +}: { + scope: DesktopScope; + desktops: DesktopRow[]; +}) { + const agents = useRelayAgentsQuery(); + const [agent, setAgent] = useState(""); + const [destination, setDestination] = useState(""); + const [busy, setBusy] = useState(false); + const [status, setStatus] = useState(""); + const [request, setRequest] = useState(null); + const active = useRef(true); + const generation = useRef(0); + useEffect(() => { + active.current = true; + return () => { + active.current = false; + generation.current++; + }; + }, []); + const run = async (action: "start" | "restart" | "move" | "retry") => { + const token = ++generation.current; + const valid = () => active.current && generation.current === token; + const client = lifecycleClient(scope, valid); + setBusy(true); + setStatus("Checking authenticated Desktop state…"); + if (action !== "retry") setRequest(null); + try { + if (action === "move") { + const outcome = await client.move( + agent, + destination, + desktops.map((d) => d.id), + (stage) => { + if (valid()) setStatus(stage); + }, + ); + client.check(); + setStatus(message(outcome)); + } else { + const next = + action === "retry" + ? request + : action === "start" + ? await client.start(destination, agent) + : await client.restart( + agent, + desktops.map((d) => d.id), + ); + if (!next) throw new Error("No request to retry"); + client.check(); + setRequest(next); + setStatus("Request sent. Waiting for the Desktop’s actual result…"); + const outcome = await client.send(next); + client.check(); + setStatus(message(outcome)); + } + } catch (error) { + if (valid()) + setStatus( + error instanceof Error ? error.message : "Operation unconfirmed", + ); + } finally { + if (valid()) setBusy(false); + } + }; + const reset = () => { + setRequest(null); + setStatus(""); + }; + return ( +
+

Start, restart, or move an agent

+ + + +

+ Start may overlap with an agent still running elsewhere until that + Desktop reconnects. Move starts the destination only after source Stop + is confirmed. Nothing transfers files, configuration, or keys. +

+
+ + + {request && ( + + )} +
+ {status && ( +

+ {status} +

+ )} +
+ ); +} diff --git a/desktop/src/features/agents/ui/KnownDesktops.tsx b/desktop/src/features/agents/ui/KnownDesktops.tsx index 45e7aa68eaa..5f4328b332c 100644 --- a/desktop/src/features/agents/ui/KnownDesktops.tsx +++ b/desktop/src/features/agents/ui/KnownDesktops.tsx @@ -1,4 +1,8 @@ -import { DesktopStopControl, DesktopStopReceiver } from "./DesktopStopControl"; +import { + DesktopLifecycleControl, + DesktopLifecycleReceiver, +} from "./DesktopLifecycleControl"; +import { DesktopStopControl } from "./DesktopStopControl"; import { useEffect, useState } from "react"; import { useQuery } from "@tanstack/react-query"; import { useIdentityQuery } from "@/shared/api/hooks"; @@ -62,6 +66,7 @@ function useDesktopList() { /** Startup and Agents share the existing owner/community query cache. */ export function DesktopListStartup() { + const [epoch, setEpoch] = useState(0); const { refetch } = useDesktopList(); const { refetch: pulse } = useDesktopObservations(useDesktopScope()); const { refetch: report } = useDesktopCapabilities(useDesktopScope()); @@ -71,6 +76,7 @@ export function DesktopListStartup() { void report(); }, DESKTOP_PULSE_MS); const unsubscribe = relayClient.subscribeToReconnects(() => { + setEpoch((n) => n + 1); void refetch(); void pulse(); void report(); @@ -80,7 +86,8 @@ export function DesktopListStartup() { unsubscribe(); }; }, [refetch, pulse, report]); - return ; + const scope = useDesktopScope(); + return ; } export function KnownDesktops() { @@ -170,6 +177,13 @@ export function DesktopListView({

Partial list: showing up to 100 profiles.

)} {list && !list.rows.length && !error &&

No Desktop profiles found.

} + {scope && list && ( + + )}
    {list?.rows.map((row) => (
  • From 4e7b31e0dc6ab265c6f10f173dbe503e3eeeb814 Mon Sep 17 00:00:00 2001 From: Logan Johnson Date: Fri, 4 Sep 2026 17:54:00 -0400 Subject: [PATCH 2/4] fix(multiverse): retain superseded Stop outcomes for exact retry Signed-off-by: Logan Johnson --- desktop/src-tauri/src/commands/desktop_stop.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/desktop/src-tauri/src/commands/desktop_stop.rs b/desktop/src-tauri/src/commands/desktop_stop.rs index 2c9623fb29a..d2361b92cf9 100644 --- a/desktop/src-tauri/src/commands/desktop_stop.rs +++ b/desktop/src-tauri/src/commands/desktop_stop.rs @@ -86,13 +86,14 @@ pub async fn receive_desktop_stop( if managed_agents::placement::desired(&conn, &target.agent)? .is_some_and(|(host, _)| host == desktop) { - return StopResult { + let result = StopResult { target, request: event.id.to_hex(), outcome: StopOutcome::Unknown, } - .sign(&scope.owner_keys) - .map(Some); + .sign(&scope.owner_keys)?; + remote_stop::save_result(&mut conn, &event.id.to_hex(), &result.as_json())?; + return Ok(Some(result)); } let owned = owned_local(&app, &state, &owner, &target.agent)?; remote_stop::receive( From 9800686a59d81e17528ce79fa891b357cca8f886 Mon Sep 17 00:00:00 2001 From: Logan Johnson Date: Fri, 4 Sep 2026 18:36:53 -0400 Subject: [PATCH 3/4] test(multiverse): scope Stop row and exercise mounted launch refusal Signed-off-by: Logan Johnson --- desktop/tests/e2e/desktop-stop.spec.ts | 69 +++++++++++++++++++++++++- 1 file changed, 67 insertions(+), 2 deletions(-) diff --git a/desktop/tests/e2e/desktop-stop.spec.ts b/desktop/tests/e2e/desktop-stop.spec.ts index a656aa45623..21df14154dc 100644 --- a/desktop/tests/e2e/desktop-stop.spec.ts +++ b/desktop/tests/e2e/desktop-stop.spec.ts @@ -31,12 +31,20 @@ test("remote Stop distinguishes delivery, uncertainty, and confirmed result", as confirmed: boolean; prepared: number; sends: string[]; + lifecyclePrepared: number; + lifecycleSends: string[]; }; __TAURI_INTERNALS__: { invoke: (command: string, payload?: any, options?: any) => Promise; }; }; - w.__STOP_FIXTURE__ = { confirmed: false, prepared: 0, sends: [] }; + w.__STOP_FIXTURE__ = { + confirmed: false, + prepared: 0, + sends: [], + lifecyclePrepared: 0, + lifecycleSends: [], + }; const original = w.__TAURI_INTERNALS__.invoke.bind(w.__TAURI_INTERNALS__); const now = Math.floor(Date.now() / 1000); const local = "11111111-1111-4111-8111-111111111111"; @@ -74,6 +82,19 @@ test("remote Stop distinguishes delivery, uncertainty, and confirmed result", as reported: now, runtimes: [], })); + case "observe_desktop_placement": + return; + case "read_desktop_placement": + case "receive_desktop_lifecycle": + return null; + case "prepare_desktop_lifecycle": + w.__STOP_FIXTURE__.lifecyclePrepared++; + return sign(50182, [ + ["p", payload.owner], + ["d", payload.desktop], + ]); + case "read_desktop_lifecycle_results": + return "provisioning_unavailable"; case "prepare_desktop_stop": w.__STOP_FIXTURE__.prepared++; return sign(50180, [ @@ -88,6 +109,8 @@ test("remote Stop distinguishes delivery, uncertainty, and confirmed result", as const wire = JSON.parse(payload.message.data); if (wire[0] === "EVENT" && wire[1]?.kind === 50180) w.__STOP_FIXTURE__.sends.push(JSON.stringify(wire[1])); + if (wire[0] === "EVENT" && wire[1]?.kind === 50182) + w.__STOP_FIXTURE__.lifecycleSends.push(JSON.stringify(wire[1])); break; } } @@ -98,7 +121,7 @@ test("remote Stop distinguishes delivery, uncertainty, and confirmed result", as const desktops = page.getByRole("region", { name: "Known Desktops" }); await desktops.getByRole("button", { name: "Refresh", exact: true }).click(); await expect( - desktops.getByText("Lab Desktop", { exact: true }), + desktops.getByRole("listitem").getByText("Lab Desktop", { exact: true }), ).toBeVisible(); await desktops .getByRole("combobox", { name: "Agent to stop on Lab Desktop" }) @@ -160,4 +183,46 @@ test("remote Stop distinguishes delivery, uncertainty, and confirmed result", as await desktops.screenshot({ path: "test-results/desktop-stop/04-confirmed.png", }); + + // The mounted lifecycle selector shares host labels with the Stop rows. + // IPC explicitly refuses launch; no native process is created by this fixture. + const controls = desktops.getByRole("region", { + name: "Agent placement controls", + }); + await controls + .getByRole("combobox", { name: "Agent to place" }) + .selectOption(agent); + await controls + .getByRole("combobox", { name: "Destination Desktop" }) + .selectOption("22222222-2222-4222-8222-222222222222"); + await controls + .getByRole("button", { name: "Start on destination", exact: true }) + .click(); + await expect(controls.getByRole("status")).toHaveText( + "Destination keyless launch provisioning is unavailable. No new process was started.", + ); + await waitForAnimations(page); + await desktops.screenshot({ + path: "test-results/desktop-stop/05-launch-unavailable.png", + }); + await controls + .getByRole("button", { name: "Retry same request", exact: true }) + .click(); + await expect(controls.getByRole("status")).toHaveText( + "Destination keyless launch provisioning is unavailable. No new process was started.", + ); + const lifecycle = await page.evaluate( + () => + ( + window as typeof window & { + __STOP_FIXTURE__: { + lifecyclePrepared: number; + lifecycleSends: string[]; + }; + } + ).__STOP_FIXTURE__, + ); + expect(lifecycle.lifecyclePrepared).toBe(1); + expect(lifecycle.lifecycleSends).toHaveLength(2); + expect(lifecycle.lifecycleSends[1]).toBe(lifecycle.lifecycleSends[0]); }); From 110374bd4ce1daf77509a830a56ee9725fe313f0 Mon Sep 17 00:00:00 2001 From: Logan Johnson Date: Fri, 4 Sep 2026 19:18:49 -0400 Subject: [PATCH 4/4] fix(desktop): keep lifecycle receiver failures out of shell layout Signed-off-by: Logan Johnson --- .../src/features/agents/desktopList.test.mjs | 3 + .../ui/DesktopLifecycleControl.test.mjs | 108 +++++++++++++++++- .../agents/ui/DesktopLifecycleControl.tsx | 26 +++-- desktop/src/testing/e2eBridge.ts | 6 + .../e2e/top-chrome-zoom-clearance.spec.ts | 17 ++- desktop/tests/helpers/bridge.ts | 2 + 6 files changed, 151 insertions(+), 11 deletions(-) diff --git a/desktop/src/features/agents/desktopList.test.mjs b/desktop/src/features/agents/desktopList.test.mjs index 5e21adbb316..828964f432b 100644 --- a/desktop/src/features/agents/desktopList.test.mjs +++ b/desktop/src/features/agents/desktopList.test.mjs @@ -160,6 +160,8 @@ test("rendered list distinguishes current, partial, unavailable and empty withou }); test("mounted cache clears both scopes, fences late reads and retains rows on failure", async (t) => { + const originalRaf = globalThis.requestAnimationFrame; + globalThis.requestAnimationFrame = (fn) => setTimeout(fn, 0); const { JSDOM } = await import("jsdom"); const dom = new JSDOM("
    ", { url: "https://desktop.test", @@ -375,6 +377,7 @@ test("mounted cache clears both scopes, fences late reads and retains rows on fa "reconnect producer unsubscribed on unmount", ); t.mock.timers.reset(); + globalThis.requestAnimationFrame = originalRaf; dom.window.close(); } }); diff --git a/desktop/src/features/agents/ui/DesktopLifecycleControl.test.mjs b/desktop/src/features/agents/ui/DesktopLifecycleControl.test.mjs index d6919521225..f41068c6136 100644 --- a/desktop/src/features/agents/ui/DesktopLifecycleControl.test.mjs +++ b/desktop/src/features/agents/ui/DesktopLifecycleControl.test.mjs @@ -3,7 +3,11 @@ import test from "node:test"; import React from "react"; import { JSDOM } from "jsdom"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; -import { DesktopLifecycleControl } from "./DesktopLifecycleControl.tsx"; +import { + DesktopLifecycleControl, + DesktopLifecycleReceiver, +} from "./DesktopLifecycleControl.tsx"; +import { toast } from "sonner"; import { relayClient } from "../../../shared/api/relayClient.ts"; test("mounted Start exposes unavailable provisioning and exact retry; Restart resolves source", async () => { @@ -131,3 +135,105 @@ test("mounted Start exposes unavailable provisioning and exact retry; Restart re dom.window.close(); } }); + +test("receiver failure is a scope-owned notification, not pre-shell layout", async () => { + const originalRaf = globalThis.requestAnimationFrame; + globalThis.requestAnimationFrame = (fn) => setTimeout(fn, 0); + const dom = new JSDOM("
    ", { + url: "https://desktop.test", + }); + Object.assign(globalThis, { + window: dom.window, + document: dom.window.document, + localStorage: dom.window.localStorage, + IS_REACT_ACT_ENVIRONMENT: true, + }); + const { createRoot } = await import("react-dom/client"); + const originals = { + fetch: relayClient.fetchEvents, + subscribe: relayClient.subscribeLive, + }; + let readiness; + let closed = 0; + let rejectLate; + let delayed = false; + relayClient.fetchEvents = async () => { + if (delayed) + return new Promise((_, reject) => { + rejectLate = reject; + }); + return []; + }; + relayClient.subscribeLive = async (_filter, _event, onReadiness) => { + readiness = onReadiness; + return () => { + closed++; + }; + }; + window.__TAURI_INTERNALS__ = { + invoke: async () => { + throw new Error("fixture: storage unavailable"); + }, + }; + const root = createRoot(document.getElementById("root")); + const scope = { owner: "owner", community: "wss://one.example" }; + const warnings = () => + toast + .getToasts() + .filter((t) => String(t.title).startsWith("Desktop lifecycle")); + try { + await React.act(async () => + root.render(React.createElement(DesktopLifecycleReceiver, { scope })), + ); + assert.equal( + document.getElementById("root").childElementCount, + 0, + "startup must not render in-flow failure UI", + ); + assert.equal(warnings().length, 1); + assert.equal( + warnings()[0].title, + "Desktop lifecycle receiver is unavailable.", + ); + assert.equal(warnings()[0].duration, Infinity); + assert.equal(warnings()[0].closeButton, true); + readiness("closed"); + assert.equal( + warnings().length, + 1, + "repeated failures update one notification", + ); + await React.act(async () => + root.render( + React.createElement(DesktopLifecycleReceiver, { scope: null }), + ), + ); + assert.equal(warnings().length, 0, "leaving the scope removes its warning"); + readiness("closed"); + assert.equal( + warnings().length, + 0, + "retired receiver cannot notify another scope", + ); + delayed = true; + await React.act(async () => + root.render(React.createElement(DesktopLifecycleReceiver, { scope })), + ); + assert.equal(typeof rejectLate, "function"); + await React.act(async () => root.unmount()); + await React.act(async () => rejectLate(new Error("late failure"))); + assert.equal( + warnings().length, + 0, + "late startup rejection must not recreate the warning", + ); + assert.equal(closed, 2, "both failed subscriptions are released"); + } finally { + await React.act(async () => root.unmount()); + relayClient.fetchEvents = originals.fetch; + relayClient.subscribeLive = originals.subscribe; + for (const warning of warnings()) toast.dismiss(warning.id); + globalThis.requestAnimationFrame = originalRaf; + dom.window.close(); + } +}); diff --git a/desktop/src/features/agents/ui/DesktopLifecycleControl.tsx b/desktop/src/features/agents/ui/DesktopLifecycleControl.tsx index 5baffa6f4cb..d05efee2511 100644 --- a/desktop/src/features/agents/ui/DesktopLifecycleControl.tsx +++ b/desktop/src/features/agents/ui/DesktopLifecycleControl.tsx @@ -1,4 +1,5 @@ import { useEffect, useRef, useState } from "react"; +import { toast } from "sonner"; import { Button } from "@/shared/ui/button"; import type { RelayEvent } from "@/shared/api/types"; import type { DesktopRow, DesktopScope } from "../desktopList"; @@ -14,31 +15,38 @@ export function DesktopLifecycleReceiver({ }: { scope: DesktopScope | null; }) { - const [error, setError] = useState(""); const { owner, community } = scope ?? {}; useEffect(() => { if (!owner || !community) return; let active = true; let close: (() => void) | undefined; - setError(""); - void receiveLifecycle({ owner, community }, () => active, setError) + let notification: string | number | undefined; + const reportError = (message: string) => { + if (!active) return; + // Startup mounts before the app shell: failure UI must not participate + // in layout or displace the fixed macOS window controls. Keep one visible + // notification for this receiver, and retire it with its owner/scope. + notification = toast.error(message, { + id: notification, + duration: Infinity, + closeButton: true, + }); + }; + void receiveLifecycle({ owner, community }, () => active, reportError) .then((fn) => { if (active) close = fn; else fn(); }) .catch(() => { - if (active) setError("Desktop lifecycle receiver is unavailable."); + reportError("Desktop lifecycle receiver is unavailable."); }); return () => { active = false; close?.(); + if (notification !== undefined) toast.dismiss(notification); }; }, [owner, community]); - return error ? ( -

    - {error} -

    - ) : null; + return null; } function message(outcome: LifecycleOutcome) { switch (outcome) { diff --git a/desktop/src/testing/e2eBridge.ts b/desktop/src/testing/e2eBridge.ts index 4548aea1afb..b6730c1bdd3 100644 --- a/desktop/src/testing/e2eBridge.ts +++ b/desktop/src/testing/e2eBridge.ts @@ -306,6 +306,7 @@ type E2eConfig = { mcp?: MockCommandAvailability; }; managedAgents?: MockManagedAgentSeed[]; + desktopLifecycleObservationError?: string; /** Result returned by the mocked `add_agent_to_huddle` command. */ addAgentToHuddleResult?: { ephemeral_added: boolean; @@ -13907,6 +13908,11 @@ export function maybeInstallE2eTauriMocks() { ], }; } + case "observe_desktop_placement": { + const error = activeConfig?.mock?.desktopLifecycleObservationError; + if (error) throw new Error(error); + return null; + } case "list_managed_agents": return handleListManagedAgents(activeConfig); case "get_agent_memory": diff --git a/desktop/tests/e2e/top-chrome-zoom-clearance.spec.ts b/desktop/tests/e2e/top-chrome-zoom-clearance.spec.ts index d1615dbcc64..1d4d6795a76 100644 --- a/desktop/tests/e2e/top-chrome-zoom-clearance.spec.ts +++ b/desktop/tests/e2e/top-chrome-zoom-clearance.spec.ts @@ -2,6 +2,7 @@ import { expect, test } from "@playwright/test"; import { readFileSync } from "node:fs"; import { installMockBridge } from "../helpers/bridge"; +import { waitForAnimations } from "../helpers/animations"; type TauriConfig = { app: { @@ -109,8 +110,18 @@ test.describe("top chrome macOS traffic-light clearance under text zoom", () => page, }) => { await spoofMacPlatform(page); - await installMockBridge(page); + await installMockBridge(page, { + desktopLifecycleObservationError: + "fixture: lifecycle storage unavailable", + }); await page.goto("/"); + // A failed global receiver must remain visible without entering the shell's + // layout flow. This also forces the error to settle before measuring chrome. + await expect( + page.getByText("Desktop lifecycle receiver is unavailable.", { + exact: true, + }), + ).toBeVisible(); // Lock the native and webview placements together: removing this explicit // Tauri inset or shifting the nav row regresses the macOS chrome alignment. @@ -133,6 +144,10 @@ test.describe("top chrome macOS traffic-light clearance under text zoom", () => ); await expectNavButtonsFixedSize(page); await expectTopChromeFixedHeight(page); + await waitForAnimations(page); + await page.screenshot({ + path: "test-results/desktop-lifecycle/receiver-error-chrome.png", + }); }); test("nav buttons still clear the traffic lights when zoomed out", async ({ diff --git a/desktop/tests/helpers/bridge.ts b/desktop/tests/helpers/bridge.ts index c3f4ed69f4c..7c029f9b647 100644 --- a/desktop/tests/helpers/bridge.ts +++ b/desktop/tests/helpers/bridge.ts @@ -221,6 +221,8 @@ type MockBridgeOptions = { mcp?: MockCommandAvailability; }; managedAgents?: MockManagedAgentSeed[]; + /** Fail lifecycle history admission to exercise the global receiver warning. */ + desktopLifecycleObservationError?: string; /** Result returned by the mocked `add_agent_to_huddle` command. */ addAgentToHuddleResult?: { ephemeral_added: boolean;