Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -1284,6 +1284,21 @@ Alert rows of the table above from the `--droplet-*` roles in `src/styles.css`.
It changes only the Agents list; herdr's order and the workspace rows stay as they are.
A raw counter rollback clears synthetic recency even on older bridges without an epoch.
This ordering correction never changes finish identities or read records.
- **Inbox** is a mobile-first sheet opened from the header at every width, with a live red count.
Its primary **Needs you** list projects the same all-PC Agents roster and shown status as the
sidebar: connected PCs only, agent panes whose authoritative state is `blocked`, in PC roster
order and each PC's Activity order. It never scans transcript text or fans out prompt reads;
selecting a row closes the sheet and opens that exact PC and pane in Chat, where the existing
prompt card reads and answers the current prompt. Each row gives the waiting kind and the agent's
own reported message as a two-line decision preview, then names the PC, workspace, tab, pane ID and
agent kind/name beside an explicit **Open chat** route. It does not poll prompts or send anything
to the agent merely to fill the Inbox. Pending rosters, no blocked agents and offline PCs have
explicit states, and live roster/status updates remove resolved rows while the sheet is open.
The design combines the Codex app's focused agent threads, Linear Inbox's priority queue and
close-to-work actions, and AlphaAgent Inbox's context-before-navigation. Agent Cockpit's descriptive
approval cards informed the waiting-kind/message split; Ovis deliberately omits risk labels because
no authoritative risk value exists, and omits read/snooze state because this is a live-state
projection rather than a stored notification queue.
- Appearance's **Group agents by state** (off by default, kept on this device, as the sidebar
filter is) moves the Agents list above the workspaces and splits its unpinned rows into
groups: **Needs you** (blocked), **Working** (BG included), **Done** (a finish not read yet)
Expand Down
3 changes: 3 additions & 0 deletions changelog.d/agent-inbox.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
### Added
- A live, mobile-friendly Inbox now gathers every connected PC's agents that need an answer, shows
each agent's own decision context without interrupting it, and opens the exact pane in Chat.
16 changes: 16 additions & 0 deletions site/demo/transport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,22 @@ function layOutRoster(): void {
layOutRoster();
for (const pane of snapshot().panes) if (keyOfPane.get(pane.pane_id) === "api") Object.assign(pane, { plan: { done: 1, total: planSteps.length, current: "Adding the replay metric" } });

// The demo's already-blocked Codex reports why it waits, so Inbox can show its real no-extra-read
// decision preview. `?demo_inbox=1` on demo/app/ stages a second kind for screenshot review.
const inboxDecisions = new Map([
["web", { agent_kind: "permission", agent_message: "Run the browser regression suite before opening the pull request?" }],
...(new URLSearchParams(location.search).get("demo_inbox") === "1"
? [["flaky", { agent_kind: "question", agent_message: "Keep the jitter fix minimal, or also add the replay metric while this branch is open?" }]] as const
: []),
]);
for (const pane of snapshot().panes) {
const decision = inboxDecisions.get(keyOfPane.get(pane.pane_id) ?? "");
if (!decision) continue;
Object.assign(pane, { agent_status: "blocked", ...decision });
const agent = snapshot().agents.find((candidate) => candidate.pane_id === pane.pane_id);
if (agent) agent.agent_status = "blocked";
}

/**
* The flaky-retry pane's background work (GET /api/pane/omo-tasks): a Claude subagent still
* reviewing, a background command, and a subagent that ended. The subagent's own transcript
Expand Down
2 changes: 2 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ import { useConnectionChip, useServerMessageHandler } from "./lib/useConnectionC
import { useHeaderConnection } from "./lib/headerConnection.ts";
import { ConnectionRouteMark } from "./components/ConnectionRouteMark.tsx";
import { HeaderSessions } from "./components/HeaderSessions.tsx";
import { AgentInboxLauncher } from "./components/AgentInbox.tsx";
import { nextWaitingPane, sessionCounts, tabPosition } from "./lib/headerSessions.ts";
import { selectionStill } from "./lib/selectionMark.ts";
import { carryMovedPane, movedView } from "./lib/paneMove.ts";
Expand Down Expand Up @@ -779,6 +780,7 @@ export function App() {
</>
)}
<div className="header-meta">
<AgentInboxLauncher machines={machines} source={activity} loaded={machineRosterLoaded} onOpen={(machineId, paneId, targetView) => selectTarget(machineId, paneId, targetView)} />
{!sessionsInStrip && !sessionsInMore && sessionsPill}
{/* the chip speaks for the pane's terminal: the setup guide stands in the pane's place and has none */}
<ConnectionRouteMark portal={auth?.via === "portal"} ingress={security.state === "known" ? security.summary.observed_ingress : null} />
Expand Down
293 changes: 293 additions & 0 deletions src/components/AgentInbox.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,293 @@
.agent-inbox-button {
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
min-width: var(--control-h);
height: var(--control-h);
gap: var(--space-2);
padding: 0 var(--space-2);
background: transparent;
border: var(--hairline) solid transparent;
border-radius: var(--radius-md);
color: var(--text-dim);
font: inherit;
font-size: var(--fs-sm);
cursor: pointer;
}

.agent-inbox-button:hover,
.agent-inbox-button:focus-visible,
.agent-inbox-button[aria-expanded="true"] {
background: var(--bg-elevated);
color: var(--text-strong);
}

.agent-inbox-button > svg {
width: var(--icon-size);
height: var(--icon-size);
}

.agent-inbox-count,
.agent-inbox-total {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: var(--chip-h);
height: var(--chip-h);
padding: 0 var(--space-1);
border-radius: var(--radius-pill);
background: var(--status-blocked);
color: var(--bg-panel);
font-size: var(--fs-2xs);
font-weight: var(--fw-bold);
line-height: 1;
}

.agent-inbox {
width: min(100%, 640px);
}

.agent-inbox .modal-title {
display: inline-flex;
align-items: center;
gap: var(--space-2);
min-width: 0;
}

.agent-inbox .modal-title svg {
width: var(--icon-size);
height: var(--icon-size);
}

.agent-inbox-total {
margin-left: auto;
}

.agent-inbox-body {
padding: 0;
}

.agent-inbox-intro {
padding: var(--space-4) var(--space-5) 0;
}

.agent-inbox-intro p {
max-width: 52ch;
margin: 0;
color: var(--text-dim);
font-size: var(--fs-sm);
line-height: 1.45;
}

.agent-inbox-section-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--space-3);
padding: var(--space-4) var(--space-5) var(--space-2);
}

.agent-inbox-section-head h3 {
margin: 0;
color: var(--text-strong);
font-size: var(--fs-sm);
font-weight: var(--fw-semibold);
}

.agent-inbox-section-head > span {
color: var(--text-dim);
font-size: var(--fs-xs);
}

.agent-inbox-list {
display: grid;
gap: var(--space-2);
margin: 0;
padding: 0 var(--space-3) var(--space-4);
list-style: none;
}

.agent-inbox-item {
display: grid;
grid-template-columns: var(--avatar-size) minmax(0, 1fr);
align-items: start;
width: 100%;
min-height: calc(var(--row-h) + var(--space-6));
gap: var(--space-3);
padding: var(--space-3);
background: var(--bg-panel);
border: var(--hairline) solid color-mix(in srgb, var(--border) 65%, transparent);
border-radius: var(--radius-lg);
color: var(--text);
font: inherit;
text-align: left;
cursor: pointer;
transition: background-color 120ms ease-out, border-color 120ms ease-out, transform 120ms ease-out;
}

.agent-inbox-item:hover,
.agent-inbox-item:focus-visible {
background: var(--bg-hover);
border-color: var(--border);
}

.agent-inbox-item:active {
transform: translateY(1px);
}

.agent-inbox-mark {
display: inline-flex;
align-items: center;
justify-content: center;
width: var(--avatar-size);
height: var(--avatar-size);
border-radius: var(--radius-md);
background: var(--bg-elevated);
}

.agent-inbox-copy {
display: flex;
flex-direction: column;
min-width: 0;
gap: var(--space-2);
}

.agent-inbox-heading,
.agent-inbox-footer {
display: flex;
align-items: center;
min-width: 0;
gap: var(--space-2);
}

.agent-inbox-title,
.agent-inbox-place,
.agent-inbox-identity {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}

.agent-inbox-title {
min-width: 0;
color: var(--text-strong);
font-size: var(--fs-md);
font-weight: var(--fw-semibold);
}

.agent-inbox-need {
flex: none;
padding: 2px var(--space-2);
border-radius: var(--radius-pill);
background: color-mix(in srgb, var(--status-blocked) 14%, transparent);
color: var(--status-blocked);
font-size: var(--fs-2xs);
font-weight: var(--fw-semibold);
line-height: 1.5;
}

.agent-inbox-context {
display: -webkit-box;
overflow: hidden;
color: var(--text);
font-size: var(--fs-sm);
line-height: 1.45;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
}

.agent-inbox-place,
.agent-inbox-identity {
color: var(--text-dim);
font-size: var(--fs-xs);
}

.agent-inbox-footer {
justify-content: space-between;
}

.agent-inbox-identity {
min-width: 0;
font-family: var(--font-mono);
}

.agent-inbox-open {
display: inline-flex;
flex: none;
align-items: center;
gap: var(--space-1);
color: var(--text-strong);
font-size: var(--fs-xs);
font-weight: var(--fw-semibold);
}

.agent-inbox-open svg {
width: 14px;
height: 14px;
stroke-width: 2;
}

.agent-inbox-empty,
.agent-inbox-offline {
margin: 0;
color: var(--text-dim);
font-size: var(--fs-sm);
text-align: center;
}

.agent-inbox-empty {
padding: var(--space-8) var(--space-5);
}

.agent-inbox-offline {
padding: 0 var(--space-5) var(--space-4);
}

@media (max-width: 768px) {
.agent-inbox-button {
width: var(--touch-target-header);
height: var(--touch-target-header);
padding: 0;
}

.agent-inbox-button-label {
display: none;
}

.agent-inbox-count {
position: absolute;
top: 2px;
right: 0;
min-width: 17px;
height: 17px;
padding: 0 3px;
}
}

@media (max-width: 480px) {
.agent-inbox-intro {
padding-right: var(--space-4);
padding-left: var(--space-4);
}

.agent-inbox-section-head {
padding-right: var(--space-4);
padding-left: var(--space-4);
}

.agent-inbox-item {
gap: var(--space-2);
min-height: calc(var(--row-h) + var(--space-6));
}

.agent-inbox-heading {
align-items: flex-start;
flex-direction: column;
gap: var(--space-1);
}

.agent-inbox-footer {
align-items: flex-end;
}
}
Loading