Inbox — drawn on Kandev's own shell

Revision 4. The previous revisions got the colour tokens exactly right and the chrome around them wrong: the sidebar, the tabs, the rows and the badges were invented. This pass redraws every frame from the shipped components, and applies your two answers — the badge and the list both read the clarification-bundle path, and permission rows appear in the history tab, labelled.

Only colour Kandev already paints appears below. Everything in grey is commentary.

repo 76ab36270 data 2026-09-12 01:06 UTC, mode=ro reference your screenshot, 01:11 UTC scope workspace Kandev
01

The destination, on the real sidebar

The nav row exists already — app-sidebar-primary-nav.tsx:41-49 renders IconInbox with badge={inboxCount}, wrapped in {inOffice && …}. Kanban never sees it. Three details the earlier revisions drew wrong, each fixed here:

The active row is a 3px left bar, not a filled block. app-sidebar-constants.ts says why, in the file: "a saturated fill reads as garish against the brand theme." The badge is gated on badge > 0 — so with Kandev at zero answerable questions, no badge renders at all, and the "0" pills I drew beside other workspaces cannot exist in that component. And the sections carry their count only while shut (collapsedSummary) — that is the Automations 4 in your screenshot.

Kandev/
Inbox
Needs you
Asked, unanswered 115
Failed 17

Nothing is waiting on you in Kandev.

115 questions were asked here and never answered. The agents that asked have moved on, so they sit read-only in Asked, unanswered.

This is the honest first frame: measured at 01:06 UTC, the Kandev workspace has 0 answerable questions, so the Inbox opens empty and the sidebar badge does not render. The row design is in frame 02, drawn on the one real answerable question in the instance.

02

The row, and answering without leaving

Rebuilt on the shipped row: inbox-item-row.tsx — flat rows divided inside one bordered container, a 32px bg-muted icon chip, title, description, and timeAgo as plain muted text. Actions are Button size="sm" on the right. Gone: the per-row card border, the indigo ring, the red age chip, and the → on the link.

The icon carries the type, in the hue the board already uses. lib/ui/state-icons.ts assigns yellow + IconMessageQuestion to WAITING_FOR_INPUT and amber + IconShieldQuestion to a pending permission — the same marks on the kanban card. My earlier draft spent amber on a tab count, which already means "over WIP limit" in StepCount, and red on an age chip, where red already means failed.

Kandev/
Inbox
Needs you 1
Asked, unanswered

Pick the reply archetype before the #39 hand-off draft can be written

[FE] #39 — Cross-surface MA+WA pilot hand-off

2 questions, asked 8 days ago

The draft needs one decision before I can write it: which archetype the reply follows. I have the thread, the pilot scope and the two prior hand-offs. What I do not have is your call on tone.

Which reply archetype should the hand-off follow?

Status hand-off: what shipped, what is open, who owns each thread
Decision request: lead with the two open calls, evidence underneath
Narrative: walk the pilot chronologically, decisions in place
Resumes session e154c20c in place

Answer and resume is the row that earns the feature. It mounts ClarificationPanelSection unchanged — that component takes {pending, messages, onResolved} and reads no task-chat context, so the Inbox can render the shipped answer UI rather than a copy of it. Display-only is the version that fails: you would still be jumping around, just from a tidier list.

This row lives in Kegmil V2, not Kandev — it is the only answerable question in the whole instance, asked 2026-09-03 04:54 UTC. It is drawn here so the row design is visible on real content; on the Kandev board this tab is empty today.

03

Asked, unanswered — now with permissions, labelled

Your answer applied: permission requests appear here, read-only, with a label that says what they are. On the Kandev board that is 109 clarification rows across 25 live tasks plus 6 permission rows carrying no terminal status — 115 in total. Every one sits on a superseded turn, so nothing here is answerable; the tab has no write path at all.

85 of the 109 are parked in a step where nothing runs — 70 in PR Review, 15 in PR Fixup, whose own prompt says "NOTHING RUNS HERE." An invisible question on a parked card with no agent to re-raise it. Grouping by task rather than by row is what keeps one stuck task from filling the screen.

Kandev/
Inbox
Needs you
Asked, unanswered 115
Failed 17
Wakeup-gate transient failure retries  9 questions, parked in PR Review, oldest 16 days ago

Should the retry budget be per-run or per-workspace?

asked 16 days ago, superseded by a later turn

Run git push --no-verify to bypass the secret-scanner hook

asked 16 days ago, and the push has since landed

Permission
Chunk run-id batches in workspace sweep  4 questions, parked in PR Fixup, oldest 10 days ago

Chunk size 100 or 500? The sweep holds the lock for the whole batch.

asked 10 days ago, superseded by a later turn

Two things this tab must never do. It must not feed the badge — none of it is answerable, and a badge you cannot drive to zero is a badge you stop reading. And it must not offer Re-ask, which you cut: five of the nine push-blocked questions measured on 09-11 had already been settled by events, so re-asking restarts an agent on a question that answered itself. The honest consequence is that these 115 have no exit in v1. They had none before either — the Inbox makes them visible, it does not clear them.

04

Failed — present, and deliberately off the badge

Correcting the reason I gave you. Revision 2 kept Failed off the badge because "16 failed tasks, newest 6 days old — nothing urgent." That reason has expired: measured now, 17 live Kandev tasks have a failed primary session, the newest 9.6 hours ago, and 3 inside 24 hours. The exclusion still stands, on a reason that does not expire: a failed task is triage, not a decision you owe. The badge counts questions only.

Kandev/
Inbox
Needs you
Asked, unanswered 115
Failed 17

Lifecycle sweep: GetTask failure path

Orchestrator

failed 10 hours ago

Guard MCP_TOOL_TIMEOUT override

Agent runtime & ACP

failed 19 hours ago

Restore legacy dynamic routes sweep

Dynamic Agent

failed 23 hours ago

05

The empty state is the product

Measured 25 hours apart and again this morning: the Kandev workspace has 0 answerable questions. This is not an edge case to sketch and move past — on your board it is the screen you open most days. It gets the most design attention of anything in this deck.

One rule governs all three variants: an empty inbox must name what it is not counting. "You're all caught up" while 115 questions sit invisible is how this feature loses your trust in week one, and it is the same failure as the 351-row version — just quieter.

Kandev/
Inbox
Needs you
Asked, unanswered 115
Failed 17

Nothing is waiting on you in Kandev.

115 questions were asked here and never answered, and 17 tasks failed. Neither needs a decision from you, so both sit in the other two tabs.

Quiet here, busy next door. The count comes from inboxCountByWorkspaceId, already a per-workspace map — a number, never a row, so nothing crosses a permission boundary.

Nothing is waiting on you in Kandev.

1 question is waiting in Kegmil V2.

Empty because you filtered it. A different sentence, because it is a different situation and the fix is one click.

No questions match "rebase".

115 questions are in this tab.

Copy notes, since the words are the design here. No apology and no exclamation mark — an empty queue is a normal state, not a celebration. The number is the subject of the second line, not a footnote. And the verb on every button says exactly what happens: Switch to Kegmil V2, not View.

06

An entry gate, not a filter over a noisy state

The issue proposed showing every task that is not Running. Same database, same instant, Kandev workspace, 2026-09-12 01:06 UTC:

What the view would showRowsOf which you can act on
"Not Running" — sessions in WAITING_FOR_INPUT on live tasks2750
Every pending question row, any turn1090
Answerable now — the shipped clarification-bundle path00
Same, measured across the whole instance11

WAITING_FOR_INPUT on this board mostly means "the agent finished its turn and is idle", not "a human is blocking it". A 275-row inbox at a 100% false-positive rate trains you to stop opening it — the same failure as an empty one that lies. Paperclip's rule is the fix, and it is an entry rule rather than a filter: a row cannot enter unless it carries something concrete you can do. Rows 1 and 2 above are exactly the rows that rule excludes.

07

Which tab control, and why it is not the default one

A design call I can make from the shipped component rather than from taste. packages/ui/src/tabs.tsx has two variants. The Office inbox uses default. On the dark theme that variant's selected state resolves to the track colour it sits on: bg-muted is #2a2a2a, and the active trigger paints dark:bg-input/30 over it — --input is also #2a2a2a, so 30% of #2a2a2a on #2a2a2a is #2a2a2a. The active border is --input, the same colour again. Selection is carried by the label's brightness alone.

default — the shipped Office inbox

Needs you Asked, unanswered Failed

A secondary count badge here would be 17 on a #2a2a2a track — invisible.

line — recommended

Needs you
Asked, unanswered 115
Failed 17

Transparent track, a 2px after:bg-foreground underline. Selection reads at a glance, and the counts sit on --background where a secondary badge has contrast.

Both variants ship today, so this costs nothing to choose — <TabsList variant="line">. It also settles a smaller question: counts belong in the tab, not in the page header, because the header is where a title would go and this page has no title. The app's top bar owns that, as your screenshot shows with Home.

08

Beside Threads, on one shared signal

Your answer was "beside, shared selector", and the code makes that literal. threads-view-filter-registry.ts already ships a threadStatus dimension whose default value is needs_action and a pendingAction dimension defaulting to clarification. What is missing is that the one built-in view ships filters: [] (thread-view-builtins.ts), so nobody ever sees the preset — and there is no badge and no destination.

The Inbox is a second consumer of resolveThreadStatus, not a second classifier. That is the whole reason the two surfaces cannot drift: one function decides what needs action, two surfaces render it.

Inbox

Decisions you owe, one row each, answerable in place. Badge = this count.

same
source

Threads

Every conversation, filterable. A new built-in view carries threadStatus is needs_action.

both read

resolveThreadStatus() → clarification-bundle path

Your read-path answer, applied. The badge and the Needs you list both come from the clarification-bundle path (clarification_bundle_query.go:119), which excludes lifecycle_only turns. The other shipped resolver, pending_interactions.go:69, does not exclude them, and the two disagree today on exactly the row frame 02 draws — 1 bundle against 0. Binding the badge to one and the list to the other would give you a badge reading 1 over an empty page. One path, both consumers.

09

What you reuse, what you build

Correcting myself again, and in the expensive direction. I told you this was "a kanban route and a different count source, not a new component." The Office inbox shares the nav slot, not the feature: its InboxItem has eight types — approval, budget_alert, agent_run_failed, task_review, provider_degraded and others — and none of them is a clarification. Its rows come from /api/v1/office/workspaces/{id}/inbox. A kanban Inbox needs its own row model, read path and count producer.

PieceStatusWhere
Sidebar nav row + badgereuse unchangedAppSidebarNavItem — drop the inOffice gate
Answer UIreuse unchangedClarificationPanelSection — props only, no task-chat context
Page shell, list container, empty blockcopy the patterninbox-page-client.tsx — p-6 space-y-4, bordered divided list
Tabsreuse, variant="line"packages/ui/src/tabs.tsx
Row icons and huesreuse the board's vocabularylib/ui/state-icons.ts
needs-action classificationreuseresolveThreadStatus / lib/utils/needs-action.ts
Row model + read pathbuildnew — the Office InboxItem has no clarification type
Count producerbuild, carefullyinboxCountByWorkspaceId has one writer today; a second on the same key collides
10

The rules this mockup encodes

Lift these into the card as acceptance criteria; each one is a decision the drawings already make.

  1. Entry gate, not a state filter. A row enters only if it carries an action you can take now. "Not Running" is 275 rows at a 100% false-positive rate; the gate is 0.
  2. The badge counts decisions you owe, and nothing else. Never the queue size, never the history tab, never failures. It must be drivable to zero, and badge > 0 already means a zero renders nothing.
  3. One read path. Badge and list both read the clarification-bundle path. Two shipped resolvers disagree today; pick one for both consumers.
  4. Answering happens in the row. A display-only inbox still makes you jump around, from a tidier list. Answer and resume posts the answer and restarts the session in place.
  5. An empty inbox names what it is not counting. Never "all caught up" alone.
  6. History is read-only. No Re-ask: five of nine push-blocked questions had already settled themselves. Permissions appear here, labelled, and never in the badge.
  7. Dismiss and snooze are a per-user sidecar and never mutate the queue — Paperclip's rule, adopted verbatim.
  8. An agent may only name itself as the owner of a row it creates. 430 of 430 questions in the last ten days were agent-authored and Kandev has no equivalent gate. Paperclip enforces this one against its own agents — I had it returned to me as a 403 while trying to hand you a row.
  9. Workspace-scoped. Other workspaces contribute a count in the empty state and the switcher, never a row.
  10. Reuse the board's colour vocabulary. Yellow is a question, amber is a permission, red is a failure, emerald is running, violet is background work. The Inbox introduces no new hue.
—

What is still unverified

Three things, so nobody reads this deck as more settled than it is. The frames are drawn from source, not from the running build — I had no browser to walk :8817 with, so the fidelity claims rest on the component files at 76ab36270 and on your screenshot, not on a live comparison. Button and input metrics are approximated to within a pixel or two; the sidebar, tabs, badge, row and list metrics are taken from the class strings verbatim. And the numbers are 01:06 UTC, reused rather than re-measured — thirteen minutes on a population that turns over in days would look like verification and would not be.