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 }) => (