From 9c944dda6da4a10efeba712c935d9bcd4bb4f1c0 Mon Sep 17 00:00:00 2001 From: Suleiman Shahbari Date: Fri, 21 Aug 2026 01:48:39 +0300 Subject: [PATCH] Remove the watch relay's orphans: the view, the ?run= route, and the specs that still promised it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The watch relay went in #1536 (f395f0c1). Its client half stayed: RelayView, a `?run=` early return in App.tsx that swapped the whole shell for it, and the `local` flag that flag threaded through three hooks to switch them off on a host that no longer exists. Nothing served any of it — the id landed in the projectId slot, resolved to nothing, and the page said "This shared session isn't available." Removing the view made two more things dead by inspection: AgentFeed's showSessionLink/showName/showStatus, always false from its one remaining caller, and the AgentOverview render they fed — with all three false it returned null every time. AgentOverview itself stays; ProjectHome uses it with its defaults. The specs that still promised the feature go with it: the root SPEC's "a shared link re-serves one agent's event stream read-only", the dashboard's Watch mode flow and its actions-menu "copy a shareable watch link", App's shared-watch-link flow, and events.SPEC.md's "the relay's own agents streaming from memory" — the last two of which were findings 1 and 4 on #1609's own list. Left alone on purpose: the device relay (#1067). `/_relay`, relay-endpoints.ts and relay-dispatch.ts are a different feature that is alive and token-guarded, and the shared word is what nearly cost the wrong deletion in #1536. Suite green: 1489 framework tests, 774 dashboard tests, typecheck clean. --- packages/the-framework/SPEC.md | 2 +- packages/the-framework/dashboard/App.SPEC.md | 2 +- packages/the-framework/dashboard/App.tsx | 22 +++------- packages/the-framework/dashboard/SPEC.md | 3 +- .../dashboard/components/AgentFeed.tsx | 23 +++-------- .../components/AgentOverview.SPEC.md | 2 +- .../dashboard/components/AgentOverview.tsx | 21 ++-------- .../dashboard/components/AgentView.tsx | 3 -- .../dashboard/components/EventList.test.tsx | 6 +-- .../dashboard/components/EventList.tsx | 8 ++-- .../dashboard/components/FileTree.SPEC.md | 2 +- .../dashboard/components/RelayView.SPEC.md | 5 --- .../dashboard/components/RelayView.tsx | 40 ------------------- .../dashboard/components/SPEC.md | 2 +- .../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-live-events.ts | 10 ++--- .../dashboard/lib/use-working.ts | 6 +-- .../src/dashboard-rpc/events.SPEC.md | 2 +- 23 files changed, 46 insertions(+), 152 deletions(-) delete mode 100644 packages/the-framework/dashboard/components/RelayView.SPEC.md delete mode 100644 packages/the-framework/dashboard/components/RelayView.tsx diff --git a/packages/the-framework/SPEC.md b/packages/the-framework/SPEC.md index 929be98cc..f8c1a97af 100644 --- a/packages/the-framework/SPEC.md +++ b/packages/the-framework/SPEC.md @@ -59,7 +59,7 @@ flowchart TD **Spending limits.** The whole quota policy is one line: unattended work may spend up to the pro-rated share of the account's week that has elapsed, rising continuously with the clock. Nothing to configure — the week is read from the account itself. Two properties fall out: nothing is left on the floor (the boundary reaches the full allowance exactly as the week resets), and background work cannot starve the user (unattended work stands down past the boundary). A slider moves that stand-down line — but for work the user asked for, the slider only ever *loosens* the gate, and it is re-read live, so raising it unparks a waiting agent without a restart. The two gates fail in opposite directions on purpose: no readable quota means unattended work does not start, while user-requested work carries on. The gate is on *starting*, and only on starting: an agent already going is never interrupted to economise, because by then the tokens are spent, the work is half-done, and what is saved is the cheap part while what is lost is the expensive part. -**Surfaces.** The daemon serves the dashboard and answers all its reads from the files agents write. Non-local binds demand a shared token, because a daemon that spawns processes on a reachable port is remote code execution. For a saved remote device, the local daemon — never the browser — talks to the device's daemon and streams its events back over the local origin; the device's token is saved only in the user's own browser and handed to the local daemon per call. A shared link re-serves one agent's event stream read-only, from the same daemon that owns it. An agent can also run elsewhere: on a Claude cloud session (fire-and-forget: it opens its own PR), or on GitHub Actions (dispatch, poll, read back the uploaded transcript; continuity between turns is the branch the previous turn pushed) — with a browser extension inside the user's own claude.ai tab bridging cloud sessions back, so a question a cloud agent parks on becomes a dashboard card. An agent can launch a real Chrome that both it and a watching human attach to at once; when it hits a login wall, captcha, or 2FA it parks on a gate and hands the browser over — it never types a password. On Discord, notification watchers post agent activity and what needs a human; Discord is a way out, not a way in. +**Surfaces.** The daemon serves the dashboard and answers all its reads from the files agents write. Non-local binds demand a shared token, because a daemon that spawns processes on a reachable port is remote code execution. For a saved remote device, the local daemon — never the browser — talks to the device's daemon and streams its events back over the local origin; the device's token is saved only in the user's own browser and handed to the local daemon per call. An agent can also run elsewhere: on a Claude cloud session (fire-and-forget: it opens its own PR), or on GitHub Actions (dispatch, poll, read back the uploaded transcript; continuity between turns is the branch the previous turn pushed) — with a browser extension inside the user's own claude.ai tab bridging cloud sessions back, so a question a cloud agent parks on becomes a dashboard card. An agent can launch a real Chrome that both it and a watching human attach to at once; when it hits a login wall, captcha, or 2FA it parks on a gate and hands the browser over — it never types a password. On Discord, notification watchers post agent activity and what needs a human; Discord is a way out, not a way in. **What lands in git.** One record of what happened: each agent's own event log, archived under a per-user directory keyed by the git identity, so cleaning the repo cannot erase the past and two people on one repo do not conflict. The daemon commits those archives after an idle window, only those paths, skipping while someone holds the index. Tickets — `tickets/_.md`, the human-facing roadmap, with optional plan and claim siblings, parsed tolerantly. And the queue file plus a human-readable log of what The Framework did to the project. diff --git a/packages/the-framework/dashboard/App.SPEC.md b/packages/the-framework/dashboard/App.SPEC.md index 9104fffdc..f978ffb4c 100644 --- a/packages/the-framework/dashboard/App.SPEC.md +++ b/packages/the-framework/dashboard/App.SPEC.md @@ -6,7 +6,7 @@ The entire dashboard is this one page: it reads the selection from the address, - The page owns what the views share: the agent list, the project list (each project carrying what the daemon currently finds wrong with it), project files, the cross-project needs-you queue, and the one live event stream the main view and right rail both read. - A just-started session shows live before its record exists; with no id known yet, the page follows the output and adopts the running session once it surfaces. - Live and finished agents are the same view — only the "live" flag flips when an agent ends. -- A shared watch link renders that one agent read-only; a daemon that stops answering gets a banner, so a dead backend never looks like a quiet agent. +- A daemon that stops answering gets a banner, so a dead backend never looks like a quiet agent. ## Rationales diff --git a/packages/the-framework/dashboard/App.tsx b/packages/the-framework/dashboard/App.tsx index c89cd30e4..4088d9406 100644 --- a/packages/the-framework/dashboard/App.tsx +++ b/packages/the-framework/dashboard/App.tsx @@ -13,7 +13,6 @@ import { TicketPlanPage } from './components/TicketPlanPage.js' import { AgentView } from './components/AgentView.js' import { agentLabel } from './lib/agent-label.js' import { RightRail } from './components/RightRail.js' -import { RelayView } from './components/RelayView.js' import { NotFound } from './components/NotFound.js' import { useLiveEvents } from './lib/use-live-events.js' import { useAgents } from './lib/use-agents.js' @@ -244,7 +243,7 @@ export function App() { } // The live agent feed is owned here so both the main view and the right rail's views tab read - // one shared event stream. Hooks run before the relay early return below. + // one shared event stream. // The agent whose feed and controls are in play is simply the one in the URL; in the no-id // fallback there is none yet, and a null id resolves to the project root, as before. const { events, lost } = useLiveEvents(projectId, agentId, agentStart.tick) @@ -258,25 +257,14 @@ export function App() { // from AgentView rather than being folded here: a finished agent's events live in its archived log, // which that view is the one to read. - // On the relay (#426), the URL carries `?run=` and there is no local registry or - // files — show that one agent read-only. Guarded on `window` so the module can be loaded - // without a browser at all, where it resolves to the full shell. - const relayAgent = typeof window === 'undefined' ? null : new URLSearchParams(window.location.search).get('run') - - // Is an agent working (#875)? Drives the mark and the tab icon. Both off on the relay: there is - // no project registry behind it, so the cross-project read cannot answer, and RelayView owns - // both from its one agent's feed instead. - const local = relayAgent === null - const working = useWorking(local) - useFavicon(working, local) + // Is an agent working (#875)? Drives the mark and the tab icon. + const working = useWorking() + useFavicon(working) // Whether the daemon answers at all (#948). Without this, a dead daemon froze every surface // silently: the channels retry their transport without a verdict and the polls keep their // last value, so "the agent went quiet" and "nothing on this page is live" looked identical. - const healthy = useDaemonHealth(local) - - // Hooks above run unconditionally (rules of hooks); this early return is safe after them. - if (relayAgent) return + const healthy = useDaemonHealth() // Route the main pane: the Overview dashboard when no project is selected (#471); else the // project home/launcher, a running agent's live output, or a finished agent's replay. Each live diff --git a/packages/the-framework/dashboard/SPEC.md b/packages/the-framework/dashboard/SPEC.md index 504e72cbe..3c9de4a01 100644 --- a/packages/the-framework/dashboard/SPEC.md +++ b/packages/the-framework/dashboard/SPEC.md @@ -28,13 +28,12 @@ The dashboard UI: a browser app served by the daemon that renders everything the - The URL is the selection: the overview at `/`, a project at `/{projectId}`, one agent at `/{projectId}/{agentId}`, plus cross-project tickets, a per-ticket page and its plan page, and settings. An agent is a link you can paste, reload, and bookmark — there is no selection state to disagree with the address bar. - An agent's events stream live over one channel bound to its own log; everything else polls. A finished agent reads from the archive instead, catching up whenever the live channel outgrew it. - The dashboard is a plain client-side app: one static page the daemon serves for every address, and all the behaviour in the browser — no server rendering, no framework between the page and the app. -- Watch mode: opened against a shared link, the same app renders one agent read-only. **The overview** is ordered by what governs what: the quota bar first (a week-track with pace and projection — the one figure that decides what agents may do next), then everything that needs *you* — the open-questions hub, every agent's unanswered question across all projects, answerable right there in one scrolling view — then the agents working now, the full AI queue of every project (uncollapsed: a plan you cannot read is not a plan), routine work, and the hottest tickets. An onboarding checklist sits on top until dismissed; each step's "done" is derived from a real fact (a registered project, a ticket on disk, a granted permission, stored credentials), so a step cannot be ticked by clicking it and work done outside the dashboard shows up ticked anyway. **The composer** starts and steers agents. Typing a prompt starts an attended build; picking a preset starts an unattended one. In-editor triggers pull in presets and actions, files, projects, and macro tags; option menus write straight to the user's or project's preferences. Pre-flight checks warn before the agent is spent — a missing or logged-out GitHub CLI, a repo that can't auto-merge. On an agent, the composer is its control: a live one takes messages (options are baked at spawn and hidden), a stopped one offers to resume with reduced options, and the submit slot doubles as Stop while it works. -**The agent view** is a transcript with the controls inline: its questions render as answerable cards exactly where they happened (resolved ones collapse to a checkmark), and its live browser screencast renders inline too, degrading to a last still when the agent ends. Around the transcript: changed files with diffs, git status, the handoff panel (push, open PR, merge), agent-authored views, docs, and history rails, and an actions menu (stop, open in editor or on GitHub, remove worktree, delete it, copy a resume command, copy a shareable watch link). +**The agent view** is a transcript with the controls inline: its questions render as answerable cards exactly where they happened (resolved ones collapse to a checkmark), and its live browser screencast renders inline too, degrading to a last still when the agent ends. Around the transcript: changed files with diffs, git status, the handoff panel (push, open PR, merge), agent-authored views, docs, and history rails, and an actions menu (stop, open in editor or on GitHub, remove worktree, delete it, copy a resume command). **Tickets** are the roadmap surface: a cross-project list with client-side faceted filtering (text, priority/effort/uncertainty buckets or ranges, topics, planning stage, project), sorting, and a group-by-project toggle — the whole view mirrored to the URL so it can be shared. Each ticket row leads with a start button that spins up an unattended agent implementing that one ticket, and shows whether a plan exists: a link to a page rendering the plan when it does, a button that starts an agent to write one when it doesn't. Queueing a ticket into the AI queue happens from the ticket's own page. diff --git a/packages/the-framework/dashboard/components/AgentFeed.tsx b/packages/the-framework/dashboard/components/AgentFeed.tsx index 15f38ce8a..fbd86d10a 100644 --- a/packages/the-framework/dashboard/components/AgentFeed.tsx +++ b/packages/the-framework/dashboard/components/AgentFeed.tsx @@ -2,19 +2,13 @@ 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. Shared by the agent's own view (AgentView, which shows the session -// link in its action bar instead — `showSessionLink={false}`) and the read-only relay watch view -// (RelayView, which keeps it since it has no action bar). `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. `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, @@ -25,15 +19,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 @@ -60,7 +48,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..9fa817f13 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 feed without both halves 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..e6230a5f3 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. Absent, 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 feed without 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 88dfd82fa..187a0bfe5 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 worktree 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: with no checkout to list (the relay), the tree renders nothing. +- With no checkout to list, the tree renders nothing. ## Rationales diff --git a/packages/the-framework/dashboard/components/RelayView.SPEC.md b/packages/the-framework/dashboard/components/RelayView.SPEC.md deleted file mode 100644 index 0f13f5e39..000000000 --- a/packages/the-framework/dashboard/components/RelayView.SPEC.md +++ /dev/null @@ -1,5 +0,0 @@ -The shared watch page: a teammate opening a shared link sees that one agent's live feed read-only — no project rails, no steering — and an agent that has ended or a wrong link says so instead of waiting forever. - -## Before modifying/creating SPEC.md files - -You must always read and respect https://raw.githubusercontent.com/brillout/sdd/refs/heads/main/sdd.md diff --git a/packages/the-framework/dashboard/components/RelayView.tsx b/packages/the-framework/dashboard/components/RelayView.tsx deleted file mode 100644 index 070beb794..000000000 --- a/packages/the-framework/dashboard/components/RelayView.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import { AgentFeed } from './AgentFeed.js' -import { Badge } from './ui/badge.js' -import { useLiveEvents } from '../lib/use-live-events.js' -import { isAgentActive } from '../lib/live-state.js' -import { useFavicon } from '../lib/favicon.js' -import { Logo } from './Logo.js' - -// The shared-agent watch view (#426/#230): when the dashboard is opened on the relay at -// `/?run=`, it shows one agent read-only, streamed from the relay's in-memory event -// feed over the same `GET /_rpc/events` stream the daemon uses. No Projects/Runs/Docs -// rails and no steering — a teammate with the link watches, they do not drive. -export function RelayView({ agentId: agentId }: { agentId: string }) { - // The agent id rides in the projectId slot: the relay keys `onEvents` by it (no registry). - const { events, lost, done } = useLiveEvents(agentId) - // The mark and the tab icon (#875) follow the one agent being watched, since that is all the - // relay knows about — it has no project registry to ask. - const working = isAgentActive(events) - useFavicon(working) - return ( -
-
- - The Framework - watching - read-only shared session -
-
- {/* An unknown or ended run closes the channel cleanly with nothing streamed; saying so - beats "Waiting for the session to start…" forever (#948). */} - {events.length === 0 && done ? ( -
- This shared session isn’t available — it may have ended, or the link may be wrong. -
- ) : ( - - )} -
-
- ) -} diff --git a/packages/the-framework/dashboard/components/SPEC.md b/packages/the-framework/dashboard/components/SPEC.md index e0670b203..bf5182501 100644 --- a/packages/the-framework/dashboard/components/SPEC.md +++ b/packages/the-framework/dashboard/components/SPEC.md @@ -10,7 +10,7 @@ The dashboard's React component catalog: every page, panel and control the brows ## Flows -- One shared shell frames every route: the left sidebar (brand, New launcher, Overview / Tickets / Projects navigation, recent agents, utility footer) and a right rail of agent-pushed views, surfaced docs and project history. Its pages are the Overview board, the project home/launcher, one agent's view, the cross-project tickets pages (list, per-ticket detail, per-ticket plan), Settings, a read-only shared watch view, and not-found. +- One shared shell frames every route: the left sidebar (brand, New launcher, Overview / Tickets / Projects navigation, recent agents, utility footer) and a right rail of agent-pushed views, surfaced docs and project history. Its pages are the Overview board, the project home/launcher, one agent's view, the cross-project tickets pages (list, per-ticket detail, per-ticket plan), Settings, and not-found. - The agent surface is a transcript with its controls inline: an action bar carrying the branch / PR / handoff and the one menu of agent actions, the event feed rendering its questions as answerable cards and its browser screencast in place, the changes and handoff panels, and one composer that starts, steers, stops and resumes — in a stable frame, so an ending never blanks what you are reading. - The Overview's widgets each show one slice of what the daemon knows: quota pace, agents working now, the Human Queue, the AI queue, routine work, hot tickets, activity and outcomes, and an onboarding checklist whose steps tick off real facts rather than clicks. - The launcher's controls — presets, driver/model and option menus, the Context selector, the system-prompt preview — read and write the same preferences and mappings the agent itself uses, so no surface can disagree with the agent it configures. 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-live-events.ts b/packages/the-framework/dashboard/lib/use-live-events.ts index b506beb73..28326a47b 100644 --- a/packages/the-framework/dashboard/lib/use-live-events.ts +++ b/packages/the-framework/dashboard/lib/use-live-events.ts @@ -13,15 +13,15 @@ import { stampReceived } from './event-times.js' // // The feed is per RUN, not per project (#749): each agent tails its own worktree's log since #736, // so the selected agent id picks the log to follow. Changing it resubscribes, which is what makes -// selecting agent A vs agent B show different output. Omitted (the relay, or a Start whose id has not -// been adopted yet) falls back to the project root. +// selecting agent A vs agent B show different output. Omitted (a Start whose id has not been +// adopted yet) falls back to the project root. /** The live feed plus whether its channel is currently down (#948). */ export interface LiveEvents { events: FrameworkEvent[] /** True while the stream is lost and being retried — the feed may be behind reality. */ lost: boolean - /** The server closed the channel on purpose (relay stream ended, unknown run) — final. */ + /** The server closed the channel on purpose (device-relay stream ended, unknown run) — final. */ done: boolean } @@ -35,7 +35,7 @@ function retryDelay(attempt: number): number { /** * How long a reconnect waits for the server's end-of-replay marker before swapping anyway * (#1383). The on-disk tail sends `stream-sync` the moment its replay is delivered, so this - * deadline only fires for the in-memory sources (relay #426, relayed device runs #1067), + * deadline only fires for the in-memory source (relayed device runs #1067), * which have no replay boundary to report — their buffered history streams in well under it. */ const SYNC_GRACE_MS = 1500 @@ -70,7 +70,7 @@ export function useLiveEvents(projectId: string | null, agentId?: string | null, // A dead stream used to be silent: the daemon restarts, events just stop, and "the agent // went quiet" is indistinguishable from "the feed died" (#948). Now an errored close (or a // failed subscribe) flips `lost` and retries with backoff. A clean close is the server being - // done with the channel on purpose (relay stream ended, unknown project) — not an outage — + // done with the channel on purpose (device-relay stream ended, unknown project) — not an outage — // so it neither retries nor alarms, matching the old behavior. const retry = () => { if (cancelled) return 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 } diff --git a/packages/the-framework/src/dashboard-rpc/events.SPEC.md b/packages/the-framework/src/dashboard-rpc/events.SPEC.md index 536515ca3..18ddc14a8 100644 --- a/packages/the-framework/src/dashboard-rpc/events.SPEC.md +++ b/packages/the-framework/src/dashboard-rpc/events.SPEC.md @@ -5,7 +5,7 @@ Streams one agent's events live to the browser: everything already logged is rep - The agent id picks whose journal to follow — each agent logs in its own checkout — so the feed is that agent's alone; without it, the project root's journal is followed, which is only right for an agent that has no checkout. - The tail follows the journal when teardown archives it mid-stream, so a watcher never misses the ending — but an agent-scoped feed never falls back to the project-root journal, which is another agent's story: a deleted agent's tab goes quiet instead. - After the replay, a one-time caught-up marker lets a reconnecting viewer swap its feed whole instead of blanking and refilling; the marker travels only on the wire, never into any journal. -- An agent with no file on this host — the relay's own, or one relayed from a device — streams from memory instead. +- An agent with no file on this host — one relayed from a device — streams from memory instead. ## Before modifying/creating SPEC.md files