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..fe34848f9 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,
@@ -23,16 +19,11 @@ 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). The relay watch passes nothing — read-only. */
- 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
- 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,13 +50,13 @@ export function AgentFeed({
return (
<>
{lostBanner}
-
>
)
diff --git a/packages/the-framework/dashboard/components/AgentOverview.SPEC.md b/packages/the-framework/dashboard/components/AgentOverview.SPEC.md
index a4c639a09..43fcd7102 100644
--- a/packages/the-framework/dashboard/components/AgentOverview.SPEC.md
+++ b/packages/the-framework/dashboard/components/AgentOverview.SPEC.md
@@ -1,4 +1,4 @@
-The agent-overview cards projected from the event stream — the status line and an honestly-labelled link to the live session — each rendered only once its data has arrived, with embedding views opting out of the parts their own chrome already shows.
+The agent-overview cards projected from the event stream — the status line and an honestly-labelled link to the live session — each rendered only once its data has arrived.
## Before modifying/creating SPEC.md files
diff --git a/packages/the-framework/dashboard/components/AgentOverview.tsx b/packages/the-framework/dashboard/components/AgentOverview.tsx
index a0b78c514..dff76e62e 100644
--- a/packages/the-framework/dashboard/components/AgentOverview.tsx
+++ b/packages/the-framework/dashboard/components/AgentOverview.tsx
@@ -9,32 +9,15 @@ import { cn } from '../lib/utils.js'
// in @gemstack/the-framework) — the agent's status and a link
// to the live session. Cards render only when their data has arrived, so an early agent
// shows nothing extra.
-export function AgentOverview({
- events,
- showSessionLink = true,
- showName = true,
- showStatus = true,
-}: {
- events: FrameworkEvent[]
- showSessionLink?: boolean
- /** The agent's own view sets this false: its action bar already names the session in the breadcrumb,
- * so the status line just shows the state (and reads the same whether or not the agent reported a
- * name). The relay watch and project home keep it, since they have no breadcrumb. */
- showName?: boolean
- /** The agent's own view sets this false too: the status is a label in its toolbar, beside the ⋮
- * menu, rather than a banner over the feed. The relay watch and project home have no toolbar,
- * so they keep the line. */
- showStatus?: boolean
-}) {
+export function AgentOverview({ events }: { events: FrameworkEvent[] }) {
const session = sessionInfo(events)
const progress = agentProgress(events)
- const status = showStatus ? agentStatusPill(events) : null
+ const status = agentStatusPill(events)
// The "Open session" link, labeled honestly: a headless Claude Code run has no per-session
// URL, so the generic app entry (claude.ai/code) is shown as "Open Claude Code" with the id
- // surfaced separately, not as a deep link to that id. See {@link describeSessionLink}. The
- // run's own view moves this into its action bar, so it opts out via `showSessionLink={false}`.
- const sessionLink = showSessionLink ? describeSessionLink(session) : null
+ // surfaced separately, not as a deep link to that id. See {@link describeSessionLink}.
+ const sessionLink = describeSessionLink(session)
if (!sessionLink && !status) return null
@@ -43,7 +26,7 @@ export function AgentOverview({
{status && (
)}
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
}