Skip to content

Inbox V3 β€” The Clarity Pass (Superhuman, not Outlook)

Status: DRAFT (under /autoplan review) Branch: claude/compassionate-golick-2d4aaf Builds on: docs/designs/inbox-is-the-product.md (V2, shipped) and the live apps/webapp/src/pages/inbox.astro + components/inbox/*.

The inbox V2 shipped the data model and the cockpit, but the surface still reads like Outlook: every row carries ~11 competing visual objects, and one /inbox route serves two different humans (the clerk who works items and the supervisor who watches a team), so both see controls that aren’t for them.

The two users β€” make the split structural, not conditional

Section titled β€œThe two users β€” make the split structural, not conditional”
Clerk (CSR / operator)Supervisor (manager / order desk)
JobWork my queue, one item at a timeWatch the team, keep SLAs green
Question on open”What’s mine? What can I grab?""Who has what? What’s breaching?”
Primary actionclaim β†’ review β†’ submitassign, escalate, report
Ideal surfacefast list, keyboard-driventable / kanban with people columns

Today these are merged behind ?view= params and capability checks inside one page. The clerk sees a disabled Dashboard button and a β€œUnified queue” tab they have no use for; the supervisor reaches their view through a URL param nobody discovers. The conflation is the root cause of the clutter β€” every band-aid control (scope dropdown, All tab, view toggle) exists to serve both users from one surface.

  • /inbox β†’ Clerk surface only. No supervisor controls, no disabled buttons, no ?view=. This is InboxCockpit.
  • /team β†’ Supervisor surface. First-class table + kanban (today’s SupervisorInboxRoot), team selector, SLA threshold, export. Gets its own left-rail nav entry, gated on inbox.view.team.
  • A supervisor working their own queue uses /inbox like everyone else; /team is for the monitoring job, not a second copy of the list.

Server-side API gates already re-validate caps, so this is mostly routing: a new astro page that renders SupervisorInboxRoot + CockpitShell, and removal of the supervisor branches from inbox.astro.

Today InboxItemRow.tsx renders, per row: checkbox, source icon, sender, title, tier badge (T1/T2/T3), PO number, broker, duplicate flag, status badge, β€βœ“ Extracted” badge, β€βœ• Error” badge, assignee label, SLA time. Eight of those are colored. The user cannot triage by scanning β€” they must read every row.

V3 row contents:

β”‚ [sender] [title / subject] [time] β”‚
β”” left-edge SLA strip (3px, color only)

Three things: sender, title, time. The left-edge is a 3px vertical strip whose color encodes urgency/health, with zero text:

Strip colorMeaningToken
transparentno SLA threshold configuredβ€”
greenplenty of SLA budget--color-success
amberapproaching SLA--color-warning
redbreached, or item in error state--color-danger

Everything else moves to where it belongs:

Removed from rowNew home
Status badge (new/in_progress/awaiting_review/submitted)Group header β€” rows are already grouped by uxState; the header says β€œAwaiting Review (3)”, so the per-row badge is pure redundancy
βœ“ Extracted / βœ• Error badgesStrip color (error = red strip) + detail pane
Tier T1/T2/T3Detail pane header
PO numberDetail pane (prominent)
Broker nameDetail pane
Assignee labelDetail pane. In the list, assigned simply = not in the β€œAvailable” queue. That IS the signal.
Source icon (email/upload/clone)Removed from the scan path; upload/clone keep a tiny muted pip only

Exceptions that earn a per-row chip (rare, judgment-bearing, not a normal state): Dup? (amber β€” needs human check before claiming), Test (amber β€” changes how you treat the submission), Price mismatch (the one extraction exception worth surfacing pre-open). Nothing else.

Unread stays as the existing left dot + bold sender. New rows keep the subtle bg-blue-50/30 tint.

Personal and Organization are data-model words, not operator words. A CSR thinks β€œmy work” and β€œstuff I can grab,” not β€œthe organization queue.”

TodayV3Why
PersonalMineitems assigned to me
OrganizationAvailableunassigned, anyone can claim
BranchBranch: [name]unchanged, already clear
AllAllsupervisor-only, lives on /team now

Two string changes in InboxTabNav.tsx plus the smart-default logic in inbox.astro (which already routes an empty personal queue to the org queue β€” reword the comments, keep the behavior).

inbox.astro already computes queueCounts {user, organization, all} server-side and passes it to InboxCockpit. Render them inline on the queue pills:

Mine (3) Available (12)

Counts are scoped to the current lifecycle view (so β€œMine 3” matches what the Mine tab shows) and deliberately not narrowed by search/filter β€” they’re stable queue-size badges, not filtered-result counts. No new query.

Keyboard-first β€” make the shortcuts visible, not modal-only

Section titled β€œKeyboard-first β€” make the shortcuts visible, not modal-only”

The cockpit already has full keyboard nav (KEYBOARD_HINTS_LIST, the ? help modal). The gap is discoverability: today you only learn the keys by opening a modal. Superhuman teaches you in context.

  • On the focused row, show ghost-text hints at the right edge in --om-surface-ink-3 at 10px: e archive Β· r review Β· z snooze. They appear on focus/hover, never shout, and vanish otherwise.
  • Keep the ? modal as the full reference.
  • / focuses search, hinted near the search box.

Pure additive UI, zero behavior change β€” the handlers already exist.

Today, above the list: (1) tab nav, (2) search + lifecycle + date + dashboard + filters + refresh, (3) a collapsible filter drawer. For a clerk that’s too much chrome. V3 clerk toolbar:

  1. Queue pills (always visible, with counts): Mine (3) Β· Available (12)
  2. Search β€” one debounced input, no submit button
  3. Lifecycle β€” Active / Archived / Trash as a compact icon-toggle group

Active filter presets become dismissible chips above the list (visible only when a preset is on), instead of living hidden in a drawer. The drawer stays for power users but isn’t the default surface. The supervisor toolbar (/team) is a separate component with team selector / SLA / export β€” not this one.

Two parallel status systems exist: raw item.status and derived item.uxState (deriveUxState). The row shows both β€” the raw badge plus the uxState group it sits under. V3: the group header is the status. The per-row badge is dropped for normal states; only the three exception chips above remain.

  • No data-model changes. workItems schema, SLA fields, polling all stay.
  • No detail-pane rework. The split-pane review surface is V2 and stays.
  • No supervisor feature changes. /team is the existing SupervisorInboxRoot moved to its own route, not a redesign of it.
  • No new endpoints. Queue counts and caps already exist server-side.
  • Mobile detail layout, onboarding banner, dashboard internals: unchanged.
Sub-problemExisting code
Supervisor surfacecomponents/inbox/SupervisorInboxRoot.tsx, CockpitShell.astro
Clerk list + keyboardInboxCockpit.tsx, cockpit/lib.ts (KEYBOARD_HINTS_LIST, groupItemsByState)
RowInboxItemRow.tsx (and cockpit/CockpitRow.tsx)
Queue countsinbox.astro queueCounts (already passed as prop)
uxState groupinglib/inbox/work-item-ux-state.ts deriveUxState
SLA color logicgetSlaDisplay in InboxItemRow.tsx (reuse the ratio→color map for the strip)
Tab renameInboxTabNav.tsx tabLabel

Phases (ORIGINAL β€” superseded by the reframe below)

Section titled β€œPhases (ORIGINAL β€” superseded by the reframe below)”
  1. Route split β†’ DEFERRED (see Phase-1 outcome)
  2. Row simplification (hard strip) β†’ DEFERRED (re-spec as density toggle)
  3. Naming + counts β†’ SHIP (premise-independent)
  4. Toolbar collapse + keyboard hints β†’ partial SHIP (keyboard hints only; toolbar collapse folded into the deferred clerk-surface work)
  • A clerk on /inbox sees zero supervisor controls and zero disabled buttons.
  • A row in the list shows exactly: sender, title, time, and a color strip (plus at most one exception chip). Measured by reading the rendered DOM.
  • β€œMine” and β€œAvailable” replace β€œPersonal”/β€œOrganization” everywhere a clerk sees.
  • Queue counts visible on the pills without opening anything.
  • A new clerk can discover e/r/z// without opening the ? modal.

═══════════════════════════════════════════════════════════════════════
Dimension Claude Codex Consensus
───────────────────────────────────── ─────── ────── ──────────────
1. Premise valid (clutter/role = NO NO CONFIRMED-NO
the real adoption blocker)? (unvalidated)
2. Right problem to solve? PARTLY NO DISAGREE→lean NO
3. Route split reduces confusion? NO NO CONFIRMED-NO
(vs fragments dual-role supervisor) (needs design)
4. Row strip preserves triage? NO NO CONFIRMED-NO
(tier/PO/broker are scan signals) (info debt)
5. "Superhuman" is the right star? NO NO CONFIRMED-NO
(B2B work-queue β‰  consumer email) (cargo-cult)
6. Alternatives explored? NO NO CONFIRMED-NO
(density toggle / progressive) (dismissed)
═══════════════════════════════════════════════════════════════════════

5/6 CONFIRMED concerns, 1 lean-negative. This is unusually strong cross-model agreement against the plan as written β€” it is a User Challenge, not a set of taste decisions.

10 findings (2 critical, 5 high, 3 medium). Highlights:

  • C1 (critical): Wrong root cause. Repo’s own docs/product/email-inbox-ux.md names the real adoption killers β€” false-positive noise (L77), lack of unified oversight/delegation (L104), duplicate work because Ordermatic can’t yet replace source-inbox actions (L160). Reframe around β€œqueue trust” + β€œworkflow replacement,” prove drop-off before spending a quarter on visual cleanup.
  • C2 (critical): /inbox vs /team is clean for the designer, not the supervisor β€” who is a hybrid operator triaging their own queue while rebalancing others. Route split adds constant context switches + fragmented state. If you split, require instant mode-switch + preserved selection/filters.
  • H3: β€œSuperhuman” is the wrong north star for a B2B exception console. Replace with β€œhigh-trust order desk”; measure throughput, mis-triage, supervisor intervention β€” not elegance.
  • H4: Row strip is information debt; same red strip = SLA breach OR extraction error collapses two different actions into one ambiguous cue. Don’t rely on color alone.
  • H6: Density is a product capability, not a design failure β€” offer Focused/Dense, persist per-user, instrument which correlates with faster resolution.
  • M9: Success criteria are UI assertions, not outcomes. Require claim-to-open time, items/active-hour, opened-then-abandoned %, before approval.

10 findings (3 critical). Key independent code finding:

  • F3 (critical): The clean-split premise is contradicted by the code. SupervisorInboxRoot’s ViewSwitcher already has a 'cockpit' mode and the supervisor table/kanban links point back to /inbox?inbox_view=.... The two surfaces are one continuum with a shared switcher today; the dual-role lead flips a view toggle in place. A route split forces a route change (/teamβ†’/inbox), losing scroll/filters/selection β€” for the most valuable operator.
  • F1 (critical): No usage evidence row clutter is the blocker. For an ops tool the dominant risks are extraction accuracy, claim/submit latency, trust in AI output β€” not row density.
  • F6 (critical): Tier IS the triage signal; PO/broker are how a CSR recognizes β€œthe Walmart reorder” without reading the subject. Color-only strip fails ~8% colorblind. Honest version is a density toggle, not forced minimalism.
  • Both models agree the surviving cheap wins: naming (Personalβ†’Mine, Organizationβ†’Available), rendering existing queue counts, and in-context keyboard ghost-hints. Ship those independently; hold the route split + row strip.
Sub-problemExisting codeImplication
Clerk/supervisor β€œseparation”SupervisorInboxRoot ViewSwitcher has cockpit mode; links target /inboxSurfaces already entangled β€” split is a refactor, not routing
Real adoption leversdocs/product/email-inbox-ux.md (trust, delegation, writeback)The strategic moat is elsewhere
Cheap winsInboxTabNav labels, queueCounts prop, KEYBOARD_HINTS_LISTShippable today, reversible, premise-independent
  • Detail-pane rework, data-model changes, supervisor feature changes (as written).
  • New deferral flagged by both models: queue-trust / false-positive reduction and source-inbox workflow replacement β€” the likely real levers β€” are out of this plan and should be weighed against it, not buried under it.

CURRENT: shipped V2, cluttered row, conflated route. THIS PLAN: cleaner clerk list, split route, renamed queues. 12-MONTH IDEAL (per both models): a trusted order desk where CSRs stop falling back to Outlook because the queue is accurate and the workflow is complete β€” not merely prettier. This plan moves the cosmetic needle; it does not move the trust needle. The cheap wins (naming/counts/hints) are pure-positive; the contested half (split + strip) may be motion, not progress, on an unvalidated premise.

Premise Gate Outcome β€” REFRAMED SCOPE (user decision)

Section titled β€œPremise Gate Outcome β€” REFRAMED SCOPE (user decision)”

User chose: Reframe β€” ship cheap wins, hold bold half. Both models recommended this.

SHIP NOW (reversible, premise-independent):

  • S1 β€” Naming. Personalβ†’Mine, Organizationβ†’Available in InboxTabNav.tsx tabLabel. (Codex M8 caveat: confirm β€œAvailable” only labels items any clerk may actually claim; if claimability isn’t guaranteed, use β€œUnassigned.” Resolve in Design.)
  • S2 β€” Queue counts on pills. Render existing queueCounts {user, organization} inline: Mine (3) Β· Available (12). Show count semantics so β€œ12 but I see 4” confusion (Codex M8) can’t happen.
  • S3 β€” In-context keyboard ghost-hints. Surface e/r/z// as muted ghost text on the focused row; keep the ? modal as full reference. Purely additive.

HOLD (deferred until unblock conditions met):

  • H-Route-Split β€” blocked on: design the dual-role supervisor’s in-place cockpit↔team mode-switch (preserve scroll/filters/selection) AND untangle the shared ViewSwitcher (SupervisorInboxRoot already has a cockpit mode whose links point at /inbox?inbox_view=...). Not β€œjust routing.”
  • H-Row-Density β€” blocked on: (a) one piece of real CSR usage/qualitative data on what slows triage, and (b) re-spec as a Focused/Dense toggle (default Focused = 3-signal; Dense keeps tier/PO/broker/assignee), persisted per-user, instrumented. No color-only state (colorblind + SLA-vs-error ambiguity). No irreversible deletion of the data path.

Both models’ strategic note (logged, not actioned here): the larger adoption levers per docs/product/email-inbox-ux.md are queue trust (false-positive reduction), unified oversight/delegation, and source-inbox workflow replacement. Tracked as separate scope, weighed against (not buried under) this polish sprint.

#PhaseDecisionClassificationPrincipleRationale
1CEORun both dual voicesMechanicalP6Always run available voices
2CEOPremise (clutter/role = blocker) is unvalidatedβ€”β€”Both models CONFIRMED-NO; surfaced at gate
3CEOReframe to ship-cheap / hold-boldUser Challengeuser decisionUser chose A at premise gate
4CEOShip S1 namingAuto (post-reframe)P3/P5Reversible string change, premise-free
5CEOShip S2 countsAutoP3Data already computed; just render
6CEOShip S3 keyboard hintsAutoP1/P5Additive, zero behavior change
7CEODefer route splitAutoP6Both models: not just routing + dual-role cost
8CEODefer hard row strip; re-spec as toggleAutoP5Reversible > irreversible on unvalidated premise
═══════════════════════════════════════════════════════════════════════
Dimension Claude Codex Consensus
───────────────────────────────────── ─────── ────── ──────────────
1. Targets the live surface? NO NO CONFIRMED-NO
(S1/S2/S3 hit legacy flag-off files) (re-point cockpit)
2. "Available" honest for the queue? NO NO CONFIRMED-NO β†’ "Unassigned"
3. Count semantics unambiguous? NO NO CONFIRMED-NO (add "N of M")
4. Keyboard hints land on real keys? NO NO CONFIRMED-NO (r,/ dead)
5. Hint placement collision-free? NO NO CONFIRMED-NO (right edge taken)
6. A11y (contrast/aria) sufficient? NO NO CONFIRMED-NO (10px ink-3 fails)
7. Density-toggle default Focused Dense DISAGREE β†’ taste (deferred)
═══════════════════════════════════════════════════════════════════════
  • N-1 Naming: Organization β†’ Unassigned (not β€œAvailable”). Both flag β€œAvailable” over-claims claimability; repo already uses β€œUnassigned” preset and empty-state copy β€œNo unclaimed orders.” Personal β†’ Mine (unambiguous).
  • QC-1 Counts: pill = stable queue total; when a filter/search is active, render a separate Showing N of M line (role="status" aria-live="polite"), mono/tabular digits. Never make the pill track filtered results.
  • KH Hints: do NOT hint r (no binding) or / until wired. Prefer extending the always-on ListFooter legend (KEYBOARD_HINTS_LIST) over per-row right-edge hints (the right edge already holds Claim/snooze/SLA). If a focused-row hint is added, put it in the SLA/date column, focus-only, β‰₯12px, β‰₯4.5:1 contrast (the --om-surface-ink-3 token is a light-surface token; on the dark cockpit pane use text-white/45-55).
  • DISAGREE (taste, deferred): density-toggle default β€” Claude=Focused (clean default), Codex=Dense (don’t strip scan signals from existing operators).
═══════════════════════════════════════════════════════════════════════
Dimension Claude Codex Consensus
───────────────────────────────────── ─────── ────── ──────────────
1. Rename scope complete? NO NO CONFIRMED-NO (6 surfaces)
2. "N of M" has a trustworthy M? NO NO CONFIRMED-NO (page vs total)
3. "/" binding edge-case safe? NO NO CONFIRMED-NO (IME/CE/modifier)
4. withCount fits aria/tabular work? NO NO CONFIRMED-NO (restructure)
5. Test coverage sufficient? NO NO CONFIRMED-NO (extract+test)
6. SSR/hydration safe? YES YES CONFIRMED-YES (client:only)
═══════════════════════════════════════════════════════════════════════

Architecture note β€” the rename is a vocabulary migration, not a string swap

Section titled β€œArchitecture note β€” the rename is a vocabulary migration, not a string swap”

Six clerk-visible surfaces carry Personal/Organization, several abbreviated so a naive find/replace silently skips them:

  1. InboxCockpit.tsx:624 QueueSwitcher (LIVE) β€” withCount('Personal'|'Organization')
  2. InboxTabNav.tsx:25-32 tabLabel (legacy)
  3. InboxSidebar.tsx:20-39 sidebar labels (legacy β€” missed by subagent first pass)
  4. InboxToolbar.tsx:301-310 stats noun 'personal'/'org' (legacy)
  5. InboxItemRow.tsx:253-266 All-tab origin chip β€” abbreviated 'Org', not β€œOrganization”
  6. InboxEmailRow.tsx:111-120 All-tab origin chip β€” the ACTUAL flag-off row path (!useWorkItems renders InboxEmailRow, per inbox.astro:855 / InboxPage.tsx:623) Fix (both voices): centralize queue/origin labels in ONE shared helper; every consumer reads from it so live and legacy can’t fork. Grep gate before merge: grep -rn "Personal\|Organization" components/inbox returns zero clerk-visible label hits.

”N of M” β€” define M before building (HIGH, both)

Section titled β€œβ€N of M” β€” define M before building (HIGH, both)”

The cockpit has no trustworthy queue-total in scope: items.length is the loaded page (≀50, inbox.astro:617 offset), queueCounts ignores lifecycle/branch/search by design (inbox.astro:620-661), flatList is a client-only pre-debounce filter. Resolution: N = flatList.length (filtered); M = totalWorkItemCount (server count for the committed query) and ONLY after the query is server-synced; make copy page-aware when page>1 (Showing 51-100 of 120) or suppress the total until synced. Never compare against queueCounts. Gate the line on an active filter so the clean case stays clean.

”/” binding β€” guards required (HIGH, both)

Section titled β€œβ€/” binding β€” guards required (HIGH, both)”

Existing handler guards INPUT/SELECT/TEXTAREA only (InboxCockpit.tsx:302-305). HelpDrawer.tsx:99-103 already shows the correct pattern (adds isContentEditable). New branch, ordered before generic single-char branches:

} else if (e.key === '/' && !e.metaKey && !e.ctrlKey && !e.altKey) {
e.preventDefault();
searchRef.current?.focus();

Plus add if (e.isComposing || e.keyCode === 229) return; and target.isContentEditable to the top-of-handler guard. preventDefault only when actually focusing search.

withCount bakes label+count into one string β†’ blocks per-pill aria-label and mono/tabular digit styling. Replace with structured option data {label, count, ariaLabel} (or a count-badge subcomponent); add optional ariaLabel to SegmentedOption (defaults to label; lifecycle/view switchers pass through unchanged). Keep the aria-live β€œN of M” node always mounted (empty when unfiltered) so SRs announce it.

Final β€” Corrected Shipping Scope + Implementation Tasks

Section titled β€œFinal β€” Corrected Shipping Scope + Implementation Tasks”

The reframe (ship cheap wins) plus the design/eng corrections collapse the shippable work to a small, precise set. The original plan pointed at legacy files; the live target is the cockpit.

  • T1 (P1, human ~2h / CC ~20m) β€” Centralized label rename. Add a shared queueLabel/origin-label helper; rename Personalβ†’Mine, Organizationβ†’Unassigned across all 6 surfaces (cockpit QueueSwitcher, InboxTabNav, InboxSidebar, InboxToolbar stats, InboxItemRow 'Org' chip, InboxEmailRow chip). Grep gate must pass. Tests: helper unit + legacy-flag-off manual check.
  • T2 (P2, human ~3h / CC ~30m) β€” β€œShowing N of M” filtered indicator. Structured count {label,count,ariaLabel} replaces withCount; mono/tabular digits; role=status aria-live=polite line, always mounted, filled only when a filter is active; M = server-synced totalWorkItemCount, page-aware (Showing 51-100 of 120), never queueCounts. Tests: formatFilteredCount unit (not-filtered/page1/page>1/unsynced/edges).
  • T3 (P2, human ~2h / CC ~20m) β€” Wire / + footer hint. New guarded / branch (!meta/ctrl/alt, isComposing/keyCode 229, isContentEditable); add ['/', 'search'] to KEYBOARD_HINTS_LIST. Do NOT add r. Extract key dispatch to a pure helper for testing. Tests: ”/” guard matrix.
  • T4 (P3, human ~1h / CC ~15m) β€” Pill a11y. Optional ariaLabel on SegmentedOption (defaults to label); "Mine, 3 items" / "Unassigned, 12 items".
  • D-Route-Split β€” design dual-role supervisor in-place cockpit↔team mode-switch (preserve scroll/filters/selection) + untangle shared ViewSwitcher before any /team route.
  • D-Density-Toggle β€” Focused/Dense segmented control, localStorage inbox:density, per-user later, instrumented (resolution-time by density). OPEN TASTE: default Focused (Claude) vs Dense (Codex, β€œdon’t strip existing operators”). Blocked on one piece of real CSR usage data.
  • D-Queue-Trust (strategic, separate scope) β€” false-positive reduction, delegation, source-inbox writeback per docs/product/email-inbox-ux.md β€” the likely real adoption levers.
#PhaseDecisionClassificationPrincipleRationale
9Design”Availableβ€β†’β€œUnassigned”AutoP5 (explicit/honest)Both voices; over-claims claimability
10DesignCounts: pill=total + β€œN of M” lineAutoP1Kills β€œ12 but I see 4”
11DesignDrop r hint; gate /AutoP5Don’t teach dead keys
12DesignDensity default Focused vs DenseTasteβ€”DISAGREE β†’ final gate (deferred work)
13EngRename = centralized helper, 6 surfacesAutoP4 (DRY)Both voices; prevents fork
14EngM = server total, page-awareAutoP1queueCounts/items.length untrustworthy
15Eng/ IME+CE+modifier guardsAutoP1Real correctness bugs once reachable
16EngRestructure withCount for aria/tabularAutoP5Baked string blocks a11y
17EngExtract pure helpers + unit testAutoP1Handler currently untested