From a80fa213d57677645fa5acc40bfccea790d1f004 Mon Sep 17 00:00:00 2001 From: Vikas Singhal Date: Sat, 8 Aug 2026 23:55:05 +0530 Subject: [PATCH] feat(console): status grammar reaches Inbox, Approvals, Agents and Cockpit (v0.329.0) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Finishes the sweep started in v0.328.0. Decision cards resolved in the card TYPE's colour, so "applied" and "rejected" looked identical: every proposal (policy, agent edit, goal edit, automation, skill, secret) rendered its outcome as the same violet/indigo/amber outlined badge whatever the human decided. The per-type verb stays — it says more than "approved" (installed, granted, applied) — but the ROLE behind it is now shared (RESOLUTION_ROLE / ResolutionChip), so approved is green, rejected is red, and `cancelled` (the session ended before anyone decided) is neutral, on every card type alike. A THIRD private outcome map (OUTCOME_STYLE, after runVerdict and OUTCOME_TONE in v0.328.0) carried the same synonym gap: a run that reported completed/progressed/ blocked read `ended`, as if it had never reported. Worse, the card's own icon keyed off a bare `=== 'failure'`, so a run that reported the synonym `blocked` announced its failure with a GREEN TICK. Both go through verdictOf/VERDICT_META. Also: - a question card is amber (needsHuman) rather than sky — it is blocked on a person, exactly like every other such signal. Sky stays what it means elsewhere: chat/unread. - the last "in progress = sky" in the app (a task-event card that is neither blocked nor done) is emerald. New: - The Agents roster says which agents are busy right now — the first thing you want when deciding who to give work to, and something it never showed. Newest live run per agent, off the feed the console already polls, through the same SessionStatus. Both the roster rail and the grid card. - Cockpit candidates show whether that agent is already running, so you can see before dispatching that your best-fit agent is mid-run. The Cockpit has no status vocabulary of its own — it is a dispatcher — so this is the only status-shaped thing on the page. Verified by screenshotting Inbox (resolutions + every outcome synonym), Agents and Cockpit against stubbed data; typecheck + both builds + test:governance green. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_015ZUfffxY4hKv7M6wMCcaTz --- CHANGELOG.md | 29 ++++++++++++++ package-lock.json | 4 +- package.json | 2 +- web/src/App.tsx | 97 +++++++++++++++++++++++++++++++++++------------ 4 files changed, 105 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f39ee9ea..5f1594a2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,35 @@ new version heading in the same commit. ## [Unreleased] +## [0.329.0] — 2026-08-08 +### Changed +- **The status grammar reaches the last four surfaces** (Inbox, Approvals, Agents, Cockpit), finishing the + sweep started in v0.328.0. + - **Decision cards resolved in the card TYPE's colour, so "applied" and "rejected" looked identical** — + every proposal (policy, agent edit, goal edit, automation, skill, secret) rendered its outcome as the + same violet/indigo/amber outlined badge whatever the human decided. The per-type verb stays (it says + more than "approved": `installed`, `granted`, `applied`), but the ROLE behind it is now shared + (`RESOLUTION_ROLE` / `ResolutionChip`), so approved is green, rejected is red and `cancelled` — the + session ended before anyone decided — is neutral, on every card type alike. + - **A THIRD private outcome map** (`OUTCOME_STYLE`, after `runVerdict` and `OUTCOME_TONE` in v0.328.0) + carried the same synonym gap: a run that reported `completed`/`progressed`/`blocked` read `ended`, as + if it had never reported. Worse, the card's own icon keyed off a bare `=== 'failure'`, so **a run that + reported the synonym `blocked` announced its failure with a green tick**. Both go through + `verdictOf` / `VERDICT_META` now. + - A **question** card is amber (`needsHuman`) rather than sky — it is blocked on a person, exactly like + every other such signal. Sky stays what it always meant elsewhere: chat/unread. + - The last "in progress = sky" in the app (a task-event card that is neither blocked nor done) is + emerald. + +### Added +- **The Agents roster says which agents are busy right now** — the first thing you want when deciding who + to give work to, and something it never showed. Newest live run per agent, off the feed the console + already polls, rendered through the same `SessionStatus` (so an agent whose run needs a human rings the + same bell here as in the sidebar). Both the roster rail and the grid card. +- **Cockpit candidates show whether that agent is already running**, so you can see before dispatching + that your best-fit agent is mid-run. The Cockpit has no status vocabulary of its own — it is a + dispatcher — so this is the only status-shaped thing on the page. + ## [0.328.0] — 2026-08-08 ### Changed - **One status grammar across the whole console.** Session status was unified in v0.320–0.327; everything diff --git a/package-lock.json b/package-lock.json index 0faf3b2b..ce8ee92e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "agent-os", - "version": "0.328.0", + "version": "0.329.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "agent-os", - "version": "0.328.0", + "version": "0.329.0", "license": "MIT", "bin": { "agent-os": "bin/agent-os" diff --git a/package.json b/package.json index ec154289..4ce52cdc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "agent-os", - "version": "0.328.0", + "version": "0.329.0", "description": "A generic, governed operating system for running autonomous agents safely across brands. Ships with a local web console.", "license": "MIT", "type": "commonjs", diff --git a/web/src/App.tsx b/web/src/App.tsx index ea850cf0..77170295 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -1693,12 +1693,12 @@ function Console({ me }: { me: Member }) {
- {route === 'agents' && nav('agents', id)} run={runAgent} onEdit={openAgent} onNew={() => nav('new-agent')} onDelete={deleteAgent} onDuplicate={duplicateAgent} onRescan={rescanAgents} onImport={importAgent} onRefresh={refreshState} nav={nav} />} + {route === 'agents' && nav('agents', id)} run={runAgent} onEdit={openAgent} onNew={() => nav('new-agent')} onDelete={deleteAgent} onDuplicate={duplicateAgent} onRescan={rescanAgents} onImport={importAgent} onRefresh={refreshState} nav={nav} />} {route === 'new-agent' && { await refreshState(); nav('agents', id) }} />} {route === 'sessions' && nav('agents')} onStop={stopSession} onDelete={deleteSession} onRate={rateSession} onRename={renameSession} onTransfer={transferSession} onBulkStop={stopSessions} onBulkDelete={deleteSessions} urlQuery={urlQuery} onFiltersChange={setUrlQuery} />} {route === 'overview' && me.role === 'owner' && } {route === 'inbox' && nav('tasks', id)} onOpenGoal={(id) => nav('goals', id)} />} - {route === 'cockpit' && nav('chat', id)} onOpenTerminal={openTerminal} nav={nav} />} + {route === 'cockpit' && nav('chat', id)} onOpenTerminal={openTerminal} nav={nav} />} {route === 'chat' && nav('chat', id)} onOpenTerminal={openTerminal} />} {route === 'connectors' && nav('connectors', t)} />} {route === 'team' && } @@ -2200,10 +2200,11 @@ function ShareAgentDialog({ me, agent, open, onOpenChange }: { me: Member; agent } function AgentsPage({ - me, agents, selected, onSelect, run, onEdit, onNew, onDelete, onDuplicate, onRescan, onImport, onRefresh, nav, + me, agents, sessions, selected, onSelect, run, onEdit, onNew, onDelete, onDuplicate, onRescan, onImport, onRefresh, nav, }: { me: Member agents: AgentInfo[] + sessions: Session[] selected: string onSelect: (id: string) => void run: (agentId: string, task: string) => Promise @@ -2327,6 +2328,11 @@ function AgentsPage({ // Filter the fleet by the search box (id / description / category), then group for display. The // selected agent is resolved over the FULL list, so searching never deselects what you picked. + // Which agents are busy RIGHT NOW — the thing you want when deciding who to give work to, and the one + // thing the roster never said. Newest live run per agent, off the feed the console already polls. + const liveRunOf = (agentId: string): Session | undefined => sessions + .filter((s) => s.agent === agentId && isLive(s)) + .sort((x, y) => y.createdAt - x.createdAt)[0] const q = query.trim().toLowerCase() const filtered = q ? agents.filter((a) => a.id.toLowerCase().includes(q) || (a.description ?? '').toLowerCase().includes(q) || (a.category ?? '').toLowerCase().includes(q)) @@ -2461,6 +2467,7 @@ function AgentsPage({ {a.id} + {liveRunOf(a.id) && } @@ -2493,6 +2500,7 @@ function AgentsPage({ {a.id} + {liveRunOf(a.id) && } {a.builtIn && } @@ -4564,7 +4572,8 @@ function prettyAgent(id: string): string { * - ask → a question about the workspace is answered **inline** (no session). * - action → "schedule…/create a task…" deep-links into that primitive's surface. * Every "work" dispatch still runs through the governed chat/terminal spawn (run-as you, gate hook). */ -function CockpitPage({ onOpenChat, onOpenTerminal, nav }: { +function CockpitPage({ sessions, onOpenChat, onOpenTerminal, nav }: { + sessions: Session[] onOpenChat: (id: string) => void onOpenTerminal: (tmux: string, title?: string) => void nav: (r: Route, detail?: string) => void @@ -4653,6 +4662,9 @@ function CockpitPage({ onOpenChat, onOpenTerminal, nav }: { const isWork = !preview?.intent || preview.intent === 'work' + const liveRunOf = (agentId: string): Session | undefined => sessions + .filter((s) => s.agent === agentId && isLive(s)) + .sort((x, y) => y.createdAt - x.createdAt)[0] const card = (c: RouterCard, opts?: { primary?: boolean }) => (