From e821032aba550065ee27ac30a28c6d01dca7cab8 Mon Sep 17 00:00:00 2001 From: Suleiman Shahbari Date: Fri, 21 Aug 2026 13:06:14 +0300 Subject: [PATCH 1/2] Remove the code the watch relay's deletion left behind MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #1536 deleted the hosted watch relay and #1611 cleaned up after it, but three things it was the only reason for stayed: - AgentFeed's showSessionLink/showName/showStatus were passed `false` by AgentView, its only caller, so the AgentOverview they configured hit its `if (!sessionLink && !status) return null` on every render. The props and that render are gone; AgentOverview itself stays, since ProjectHome uses it with the defaults. - The `enabled` parameter on useWorking/useDaemonHealth/useFavicon was never passed `false` by anything — the relay view had been its only caller. Gone, along with the two tests that existed only to cover it. - Comments and specs across nine files still explained live behaviour by naming the deleted relay: FileTree "has no checkout", GitStatusBar "on the relay", EventList's "read-only relay watch", RoutineWork's sweep note, favicon, use-daemon-health's tests. The behaviour is real in each case, only the reason was a ghost — reworded to state the condition itself. The device relay (#1067) is untouched: relay-endpoints, relay-dispatch and relay-agent are a different feature that shares the word. No FEATURES-SPEC.md change: none of this was user-visible. Suite 1489 + 774, typecheck clean. --- .../dashboard/components/AgentFeed.SPEC.md | 2 +- .../dashboard/components/AgentFeed.tsx | 23 +++++----------- .../components/AgentOverview.SPEC.md | 2 +- .../dashboard/components/AgentOverview.tsx | 27 ++++--------------- .../dashboard/components/AgentView.tsx | 3 --- .../dashboard/components/EventList.test.tsx | 6 ++--- .../dashboard/components/EventList.tsx | 8 +++--- .../dashboard/components/FileTree.SPEC.md | 2 +- .../dashboard/components/FileTree.tsx | 3 +-- .../dashboard/components/GitStatusBar.tsx | 3 +-- .../dashboard/components/RoutineWork.tsx | 4 +-- .../dashboard/lib/favicon.SPEC.md | 2 +- .../dashboard/lib/favicon.test.ts | 6 ----- .../the-framework/dashboard/lib/favicon.ts | 13 +++------ .../lib/use-daemon-health.test.SPEC.md | 2 +- .../dashboard/lib/use-daemon-health.test.tsx | 11 ++------ .../dashboard/lib/use-daemon-health.ts | 5 ++-- .../dashboard/lib/use-working.ts | 6 ++--- 18 files changed, 38 insertions(+), 90 deletions(-) diff --git a/packages/the-framework/dashboard/components/AgentFeed.SPEC.md b/packages/the-framework/dashboard/components/AgentFeed.SPEC.md index 744446c6e..7f60a4d79 100644 --- a/packages/the-framework/dashboard/components/AgentFeed.SPEC.md +++ b/packages/the-framework/dashboard/components/AgentFeed.SPEC.md @@ -1,4 +1,4 @@ -One agent's feed: the overview plus the live or replayed event log — with a waiting placeholder before anything streams, and a banner while the live stream is down, so a dead connection never reads as the agent going quiet. +One agent's feed: the live or replayed event log — with a waiting placeholder before anything streams, and a banner while the live stream is down, so a dead connection never reads as the agent going quiet. ## Before modifying/creating SPEC.md files diff --git a/packages/the-framework/dashboard/components/AgentFeed.tsx b/packages/the-framework/dashboard/components/AgentFeed.tsx index a7cb43bc1..39d41daa8 100644 --- a/packages/the-framework/dashboard/components/AgentFeed.tsx +++ b/packages/the-framework/dashboard/components/AgentFeed.tsx @@ -2,18 +2,14 @@ import type { ReactNode } from 'react' import type { FrameworkEvent } from '../../src/index.js' import { TriangleAlert } from 'lucide-react' import { EventList } from './EventList.js' -import { AgentOverview } from './AgentOverview.js' -// One agent's feed: the agent overview plus the live/replayed event log, or a waiting placeholder -// before anything has streamed. Rendered by the agent's own view (AgentView, which shows the -// session link in its action bar instead — `showSessionLink={false}`). `lost` is the live -// channel's health (#948): while the stream is down the feed is behind reality, and saying so -// beats letting "the agent went quiet" and "the connection died" look identical. +// One agent's feed: the live/replayed event log, or a waiting placeholder before anything has +// streamed. Rendered by the agent's own view (AgentView), whose action bar already carries the +// session link, the session name and the status. `lost` is the live channel's health (#948): +// while the stream is down the feed is behind reality, and saying so beats letting "the agent +// went quiet" and "the connection died" look identical. export function AgentFeed({ events, - showSessionLink = true, - showName = true, - showStatus = true, lost = false, stick = true, openAt, @@ -24,15 +20,9 @@ export function AgentFeed({ }: { events: FrameworkEvent[] /** The feed's own project/run (#1455 item 6): with a projectId the log's `choice` rows become - * the interaction (inline panels/answered cards). The relay watch passes nothing — read-only. */ + * the interaction (inline panels/answered cards). */ projectId?: string | undefined agentId?: string | null | undefined - showSessionLink?: boolean - /** The agent's own view sets this false: its action bar's breadcrumb already names the session. */ - showName?: boolean - /** The agent's own view sets this false: its action bar carries the status beside the ⋮ menu. */ - showStatus?: boolean - /** The agent's own view sets this false: its right rail pins the loop's verdict under the tabs. */ lost?: boolean /** A finished log is static (#1026): it does not follow new output, and opens at its end. */ stick?: boolean @@ -59,7 +49,6 @@ export function AgentFeed({ return ( <> {lostBanner} - - {showName && progress.sessionName && {progress.sessionName}} + {progress.sessionName && {progress.sessionName}} {status.label} )} diff --git a/packages/the-framework/dashboard/components/AgentView.tsx b/packages/the-framework/dashboard/components/AgentView.tsx index f9195262e..faea61bb4 100644 --- a/packages/the-framework/dashboard/components/AgentView.tsx +++ b/packages/the-framework/dashboard/components/AgentView.tsx @@ -239,9 +239,6 @@ export function AgentView({ events={shown} projectId={projectId} agentId={agentId} - showSessionLink={false} - showName={false} - showStatus={false} lost={lost} {...(feedLive ? {} : { stick: false, openAt: 'end' as const, emptyLabel: 'This agent has no events.' })} // A web agent's log dead-ends at the hand-off (#1265): the mirror box rides the tail of diff --git a/packages/the-framework/dashboard/components/EventList.test.tsx b/packages/the-framework/dashboard/components/EventList.test.tsx index ecf28cf4c..ccaefaceb 100644 --- a/packages/the-framework/dashboard/components/EventList.test.tsx +++ b/packages/the-framework/dashboard/components/EventList.test.tsx @@ -193,7 +193,7 @@ describe('EventList inline choice rows (#1455 item 6)', () => { expect(sendChoice).toHaveBeenCalledWith('p1', 'gate-1', 'work', 'user', 'r1') }) - test('without a projectId the row keeps the formatter text (the read-only relay watch)', () => { + test('without a projectId the row keeps the formatter text', () => { render() expect(screen.queryByRole('button', { name: /Work on it/ })).toBeNull() expect(screen.getByText(/Start the next backlog item\?/)).toBeTruthy() @@ -231,7 +231,7 @@ describe('EventList inline choice rows (#1455 item 6)', () => { }) // The latest `browser` row hosts the live inline preview (#1455 item 6b); earlier rows and the -// read-only relay watch keep the formatter's text, and an ended agent's pane degrades (#1359). +// a log rendered without them keeps the formatter's text, and an ended agent's pane degrades (#1359). describe('EventList inline browser rows (#1455 item 6b)', () => { const browser = (url = 'https://app.test/'): FrameworkEvent => ({ kind: 'browser', url }) @@ -259,7 +259,7 @@ describe('EventList inline browser rows (#1455 item 6b)', () => { expect(screen.getByText(/browser · https:\/\/a\.test\//)).toBeTruthy() }) - test('without a agentId the row keeps the formatter text (the read-only relay watch)', () => { + test('without a agentId the row keeps the formatter text', () => { render() expect(screen.queryByAltText("The agent's browser")).toBeNull() expect(screen.getByText(/browser: https:\/\/app\.test\//)).toBeTruthy() diff --git a/packages/the-framework/dashboard/components/EventList.tsx b/packages/the-framework/dashboard/components/EventList.tsx index 682c66f37..a9dfdb779 100644 --- a/packages/the-framework/dashboard/components/EventList.tsx +++ b/packages/the-framework/dashboard/components/EventList.tsx @@ -282,15 +282,15 @@ export function EventList({ * live mirror box — that must scroll (and stick) with the log rather than float over it. */ tail?: ReactNode /** The log's own project (#1455 item 6): with it, a `choice` row IS the interaction — an open - * gate renders the inline ChoicePanel, a resolved one the collapsed ✓ card. Absent (the - * read-only relay watch), every row keeps the formatter's text. */ + * gate renders the inline ChoicePanel, a resolved one the collapsed ✓ card. Without it, every + * row keeps the formatter's text. */ projectId?: string | undefined /** Which run an inline pick resolves (#749), forwarded to the panel with projectId. */ agentId?: string | null | undefined }) { const choiceRows = useMemo(() => (projectId ? foldChoiceRows(events) : undefined), [projectId, events]) - // The inline pane needs both halves of the proxy path, so the read-only relay watch (no - // projectId/agentId) keeps every browser row as formatter text. + // The inline pane needs both halves of the proxy path, so a log rendered without a + // projectId/agentId keeps every browser row as formatter text. const browserRows = useMemo(() => (projectId && agentId ? foldBrowserRows(events) : undefined), [projectId, agentId, events]) const shown = promptFirst(events).filter(e => !choiceRows?.hidden.has(e) && !browserRows?.hidden.has(e)) return ( diff --git a/packages/the-framework/dashboard/components/FileTree.SPEC.md b/packages/the-framework/dashboard/components/FileTree.SPEC.md index b25b4a039..bc1d60034 100644 --- a/packages/the-framework/dashboard/components/FileTree.SPEC.md +++ b/packages/the-framework/dashboard/components/FileTree.SPEC.md @@ -5,7 +5,7 @@ The project panel's file tree — a context picker, not an editor: clicking a fi - Per-file git-status marks, read from the selected agent's own checkout and refreshed as it edits, roll up to folders so dirty work is spottable even while a folder is closed. A file says which change it is; a folder only says that something under it changed. - A filter box narrows to matching files, and zero matches say so instead of rendering an empty pane that reads as broken. - Every file previews on hover — its diff when changed, its contents when not — with the tree's own status deciding which. -- Localhost-only: the relay — watching an agent that executes on another machine — has no checkout to list here, so the tree renders nothing. +- With no files to list, the tree renders nothing rather than an empty frame. ## Rationales diff --git a/packages/the-framework/dashboard/components/FileTree.tsx b/packages/the-framework/dashboard/components/FileTree.tsx index 3ebfc5de8..2564cefcf 100644 --- a/packages/the-framework/dashboard/components/FileTree.tsx +++ b/packages/the-framework/dashboard/components/FileTree.tsx @@ -55,8 +55,7 @@ const EMPTY_STATUS: Record = {} // The project panel's file tree (#492): a lazy, collapsible tree built from the flat // `git ls-files` list (onProjectFiles, shared with the `#` picker #504). It is a file-level // CONTEXT PICKER, not an editor — clicking a file toggles it in the agent Context, the same -// set the `#` chips and the whole-repo Context selector feed. Localhost-only: no files (the -// relay has no checkout) renders nothing. +// set the `#` chips and the whole-repo Context selector feed. With no files, it renders nothing. // // Folders are native `
`: open/closed state, keyboard operation and the disclosure // semantics come from the browser. This used to be 1,225 lines of vendored animate-ui — a copied diff --git a/packages/the-framework/dashboard/components/GitStatusBar.tsx b/packages/the-framework/dashboard/components/GitStatusBar.tsx index 5d5e24fec..f17d0cf28 100644 --- a/packages/the-framework/dashboard/components/GitStatusBar.tsx +++ b/packages/the-framework/dashboard/components/GitStatusBar.tsx @@ -8,8 +8,7 @@ import { cn } from '../lib/utils.js' import { Tooltip, TooltipTrigger, TooltipContent } from './ui/tooltip.js' // The checkout in play (#491, part of #488): active branch, a clean/dirty dot, the linked PR. -// Polled, so it tracks an agent committing or branching. Hidden when there is no git repo (or on -// the relay, which has no local checkout). +// Polled, so it tracks an agent committing or branching. Hidden when there is no git repo. // // One component for both pages (#809). With a `agentId` it reads that session's own worktree, which // also carries its size on disk and the path it lives at; without one it reads the project's diff --git a/packages/the-framework/dashboard/components/RoutineWork.tsx b/packages/the-framework/dashboard/components/RoutineWork.tsx index fa8111625..d66cd7d36 100644 --- a/packages/the-framework/dashboard/components/RoutineWork.tsx +++ b/packages/the-framework/dashboard/components/RoutineWork.tsx @@ -104,8 +104,8 @@ export function RoutineWork({ setSweepNote(null) const result = await sendAutoPmSweep().catch(() => ({ ok: false as const })) setSweeping(false) - // A host with no loop is the honest failure here, and the only one: the relay serves this - // same dashboard, and there the button has nothing to fire. + // A host with no loop is the honest failure here, and the only one: a dashboard served by + // something that does not run the sweep has nothing for this button to fire. if (!result.ok) setSweepNote('This dashboard is not running the sweep, so there is nothing to trigger here.') else setSweepNote(describeOutcomes('outcomes' in result ? result.outcomes : undefined)) } diff --git a/packages/the-framework/dashboard/lib/favicon.SPEC.md b/packages/the-framework/dashboard/lib/favicon.SPEC.md index 83334efbc..60d65bdfd 100644 --- a/packages/the-framework/dashboard/lib/favicon.SPEC.md +++ b/packages/the-framework/dashboard/lib/favicon.SPEC.md @@ -1,4 +1,4 @@ -The tab icon follows the work: the still logo while nothing is running, the animated one while an agent is working — and a view that cannot know (the relay) leaves the icon alone. +The tab icon follows the work: the still logo while nothing is running, the animated one while an agent is working. ## Before modifying/creating SPEC.md files diff --git a/packages/the-framework/dashboard/lib/favicon.test.ts b/packages/the-framework/dashboard/lib/favicon.test.ts index d8dcbc3d8..7dc177af2 100644 --- a/packages/the-framework/dashboard/lib/favicon.test.ts +++ b/packages/the-framework/dashboard/lib/favicon.test.ts @@ -24,12 +24,6 @@ describe('useFavicon', () => { expect(icon()).toBe(WORKING_FAVICON) }) - test('leaves the tab alone when it is not the caller\'s to set', () => { - document.head.innerHTML = `` - renderHook(() => useFavicon(true, false)) - expect(icon()).toBe(IDLE_FAVICON) - }) - test('names the two icon files', () => { expect(faviconHref(true)).toBe(WORKING_FAVICON) expect(faviconHref(false)).toBe(IDLE_FAVICON) diff --git a/packages/the-framework/dashboard/lib/favicon.ts b/packages/the-framework/dashboard/lib/favicon.ts index 3e11b8007..d4dc79bda 100644 --- a/packages/the-framework/dashboard/lib/favicon.ts +++ b/packages/the-framework/dashboard/lib/favicon.ts @@ -15,15 +15,10 @@ export function faviconHref(working: boolean): string { return working ? WORKING_FAVICON : IDLE_FAVICON } -/** - * Point the tab icon at {@link faviconHref} (client-only). - * - * `enabled` is false where the caller is not the one that knows: the shell hands the tab over to - * the relay view, which reads a single agent's feed rather than the project registry. - */ -export function useFavicon(working: boolean, enabled = true): void { +/** Point the tab icon at {@link faviconHref} (client-only). */ +export function useFavicon(working: boolean): void { useEffect(() => { - if (!enabled || typeof document === 'undefined') return + if (typeof document === 'undefined') return // `rel~=` because the emitted rel can carry more than one token. let link = document.querySelector('link[rel~="icon"]') if (!link) { @@ -35,5 +30,5 @@ export function useFavicon(working: boolean, enabled = true): void { // Guarded: writing the same href re-fetches the icon in some browsers, which restarts the // animation on every render. if (link.getAttribute('href') !== href) link.setAttribute('href', href) - }, [working, enabled]) + }, [working]) } diff --git a/packages/the-framework/dashboard/lib/use-daemon-health.test.SPEC.md b/packages/the-framework/dashboard/lib/use-daemon-health.test.SPEC.md index 7c04f7ca8..06c26d062 100644 --- a/packages/the-framework/dashboard/lib/use-daemon-health.test.SPEC.md +++ b/packages/the-framework/dashboard/lib/use-daemon-health.test.SPEC.md @@ -1,4 +1,4 @@ -Covers the liveness probe: an answering daemon reads healthy, a failing one flips to down, and the shared watch view (which has no daemon of its own) never probes. +Covers the liveness probe: an answering daemon reads healthy, and a failing one flips to down. ## Before modifying/creating SPEC.md files diff --git a/packages/the-framework/dashboard/lib/use-daemon-health.test.tsx b/packages/the-framework/dashboard/lib/use-daemon-health.test.tsx index 910240b87..b0c8f73a7 100644 --- a/packages/the-framework/dashboard/lib/use-daemon-health.test.tsx +++ b/packages/the-framework/dashboard/lib/use-daemon-health.test.tsx @@ -11,8 +11,8 @@ afterEach(() => { onProjects.mockReset() }) -function Probe({ enabled = true }: { enabled?: boolean }) { - return {useDaemonHealth(enabled) ? 'healthy' : 'down'} +function Probe() { + return {useDaemonHealth() ? 'healthy' : 'down'} } // #948: a dead daemon froze every surface silently — the probe is what lets the shell say so. @@ -29,11 +29,4 @@ describe('useDaemonHealth', () => { render() await waitFor(() => expect(screen.getByText('down')).toBeTruthy()) }) - - test('disabled (the relay) never probes and stays healthy', async () => { - render() - await new Promise(resolve => setTimeout(resolve, 50)) - expect(onProjects).not.toHaveBeenCalled() - expect(screen.getByText('healthy')).toBeTruthy() - }) }) diff --git a/packages/the-framework/dashboard/lib/use-daemon-health.ts b/packages/the-framework/dashboard/lib/use-daemon-health.ts index 3aed21644..5fe553e6e 100644 --- a/packages/the-framework/dashboard/lib/use-daemon-health.ts +++ b/packages/the-framework/dashboard/lib/use-daemon-health.ts @@ -10,11 +10,10 @@ const PROBE_MS = 5000 // from a quiet agent. One cheap read on a fixed cadence turns "unreachable" into a fact the // shell can say out loud. Recovery needs no action here: the channels reconcile and the polls // resume on their own once the daemon answers again. -export function useDaemonHealth(enabled = true): boolean { +export function useDaemonHealth(): boolean { const [healthy, setHealthy] = useState(true) useEffect(() => { - if (!enabled) return let cancelled = false let timer: ReturnType | undefined const probe = () => { @@ -36,7 +35,7 @@ export function useDaemonHealth(enabled = true): boolean { cancelled = true if (timer) clearTimeout(timer) } - }, [enabled]) + }, []) return healthy } diff --git a/packages/the-framework/dashboard/lib/use-working.ts b/packages/the-framework/dashboard/lib/use-working.ts index 0ff55bd17..de5566f9a 100644 --- a/packages/the-framework/dashboard/lib/use-working.ts +++ b/packages/the-framework/dashboard/lib/use-working.ts @@ -13,8 +13,8 @@ import { usePolled } from './use-async.js' /** Stable initial, so the poll does not churn on every render. */ const IDLE: Overview = { active: [], queueOpen: 0, recent: [] } -/** True while any project has a running agent. `enabled` false skips the poll and answers false. */ -export function useWorking(enabled = true): boolean { - const { value } = usePolled(enabled ? onOverview : null, IDLE, 5000, [enabled]) +/** True while any project has a running agent. */ +export function useWorking(): boolean { + const { value } = usePolled(onOverview, IDLE, 5000, []) return value.active.length > 0 } From c42ed6679abacc97d8db6d52b2bfa13dc0eb00a1 Mon Sep 17 00:00:00 2001 From: Suleiman Shahbari Date: Fri, 21 Aug 2026 14:41:53 +0300 Subject: [PATCH 2/2] AgentFeed's projectId is required: an open gate must never render as log text MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The last of the same shape #1615 removes. AgentView is AgentFeed's only caller and always passes projectId, so the optional prop had exactly one production value — and the conditional forward below it could only ever take one branch. Required is the point, not tidiness: with it optional, a future caller that omits it gets an open choice gate silently rendered as plain log text, which is a run parked with nothing to answer it (#846). A required prop makes that a compile error instead of a dead-quiet downgrade. EventList's own projectId stays optional. It is a leaf presentational component with 35 direct test renders, and its browser pane already degrades on a genuinely nullable agentId, so the absent case is a real render mode there rather than a fiction. --- .../the-framework/dashboard/components/AgentFeed.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/the-framework/dashboard/components/AgentFeed.tsx b/packages/the-framework/dashboard/components/AgentFeed.tsx index 39d41daa8..fe34848f9 100644 --- a/packages/the-framework/dashboard/components/AgentFeed.tsx +++ b/packages/the-framework/dashboard/components/AgentFeed.tsx @@ -19,9 +19,10 @@ export function AgentFeed({ agentId: agentId, }: { events: FrameworkEvent[] - /** The feed's own project/run (#1455 item 6): with a projectId the log's `choice` rows become - * the interaction (inline panels/answered cards). */ - projectId?: string | undefined + /** The feed's own project/run (#1455 item 6): the log's `choice` rows are the interaction + * (inline panels/answered cards). Required, so no caller can silently downgrade an open gate + * to log text — a gate rendered as text is a run parked with nothing to answer it (#846). */ + projectId: string agentId?: string | null | undefined lost?: boolean /** A finished log is static (#1026): it does not follow new output, and opens at its end. */ @@ -54,7 +55,8 @@ export function AgentFeed({ stick={stick} {...(openAt ? { openAt } : {})} {...(tail ? { tail } : {})} - {...(projectId ? { projectId, agentId: agentId } : {})} + projectId={projectId} + agentId={agentId} /> )