Cockpit Inbox Design
internal prototype · canonical JSON + Dreamborn Forge HTML
internal generated
design_doc · markdown

Cockpit Inbox Design

Cockpit Inbox Design Date: 2026 04 27 Status: Approved Branch: feature/marketing agents Overview Replace the "Exec Inbox" section in the cockpit right sidebar with a nav level inbox badge and a 40% width slide in drawer. The drawer provides a two pane interface for reading messages, approving exec gates, replying to agents, and composing ad hoc messages to a...

Agent Handoff
Start Here

Cockpit Inbox Design Date: 2026 04 27 Status: Approved Branch: feature/marketing agents Overview Replace the "Exec Inbox" section in the cockpit right sidebar with a nav level inbox badge and a 40% width slide in drawer. The drawer provides a two pane interface for reading messages, approving exec gates, replying to agents, and composing ad hoc messages to a...

Completion Evidence

No explicit evidence field yet. Require tests, screenshots, linked PRs, or reviewed outputs before marking complete.

Cockpit Inbox Design

Date: 2026-04-27 Status: Approved Branch: feature/marketing-agents

---

Overview

Replace the "Exec Inbox" section in the cockpit right sidebar with a nav-level inbox badge and a 40%-width slide-in drawer. The drawer provides a two-pane interface for reading messages, approving exec gates, replying to agents, and composing ad hoc messages to any agent.

---

Nav Badge

A ✉ N badge is added to the nav bar between the "testnet" badge and the clock.

  • Count = unread inbox rows where to_agent = 'justin' and processed = false + agent_tasks rows where role = 'exec' and status = 'assigned'
  • Badge pulses amber when count > 0
  • Clicking the badge toggles the drawer open/closed
  • Badge shows is-open styling when drawer is open
  • The existing "Exec Inbox" label and container in the right sidebar are removed

---

Drawer
  • Width: 40% of viewport
  • Position: Fixed to the right edge, full height below the nav
  • Behavior: Slides in with a 200ms ease transition; clicking the overlay or pressing Escape closes it
  • Border: Left edge uses --color-accent to signal it is an interactive layer above the cockpit
  • A semi-transparent overlay covers the rest of the cockpit when the drawer is open (does not block interaction — just visual depth)
Drawer Header

`` ✉ Inbox · N unread [+ Compose] [✕] ``

  • Title shows live unread count
  • + Compose button opens the compose form in the right pane; it gets an is-active style while compose is open
  • closes the drawer

---

Two-Pane Layout

The drawer body is split into two columns:

| Column | Width | Content | |--------|-------|---------| | Message list | 38% | Scrollable list of all inbox items | | Detail pane | 62% | Selected message body + action area |

Message List

Messages are ordered: exec gate tasks first (green), then unread inbox messages by created_at desc, then read/processed messages dimmed at the bottom.

  • From agent name (amber for regular; green for exec gates) + unread dot if unprocessed
  • Subject (truncated to one line)
  • Meta: message_type · ref_id · time ago

Clicking an item selects it (amber border) and loads it in the detail pane. Read messages are dimmed (opacity 0.55).

Detail Pane
  • From → To header
  • message_type · ref_id · timestamp
  • Subject line
  • Full message body (scrollable)
  • HashScan link for the ref topic_id
  • Action area pinned to the bottom (see below)

---

Exec Gate (agent_tasks role=exec, status=assigned)

Action area shows two buttons:

  • Posts { type: 'task.complete', task_id: topicId, agent: 'justin', output: 'Approved via cockpit' } to the task's topic_id via POST /post-hcs-message
  • Button changes to ✓ Approved (green) on success
  • Re-renders the message list after 2 seconds
  • Clicking Block expands a reason text input inline (replaces the button row)
  • User types a reason; a Confirm Block button posts { type: 'task.blocked', task_id: topicId, agent: 'justin', error_message: reason } to the task's topic_id via HCS
  • Cancel collapses back to Approve/Block buttons
Inbox Message (inbox table, message_type != exec.gate)

Action area shows a reply textarea:

`` Reply to <agent> [textarea — placeholder: "Type your reply…"] sends as justin → <agent> via HCS [Dismiss] [Send] ``

  • Looks up the sender agent's inbound_topic_id from agent_definitions
  • Posts { type: 'inbox.message', from: 'justin', to: senderAgent, subject: 'Re: ' + originalSubject, body: replyText, ref_id: originalRefId, ref_type: originalRefType } via POST /post-hcs-message
  • Listener picks up the HCS message and writes it to the inbox table
  • On success: textarea clears, message item dims (marked read)
  • Patches inbox row processed = true directly via Supabase REST (no HCS needed — just clears the unread state)
  • Message dims in the list; badge count decrements

---

Compose (Ad Hoc Message)

Clicking + Compose replaces the detail pane with a compose form. Cancelling returns to the previously selected message.

Fields:

| Field | Type | Options | |-------|------|---------| | To | Dropdown | All agents from AGENT_DEPT map | | Type | Dropdown | question, feedback, fyi, decision.needed, review.request | | Subject | Text input | One-line summary | | Message | Textarea | Full message body |

  • Looks up the selected agent's inbound_topic_id from agent_definitions
  • Posts { type: 'inbox.message', from: 'justin', to: selectedAgent, message_type: selectedType, subject, body } via POST /post-hcs-message
  • Listener writes to inbox table on receipt
  • On success: compose form clears, drawer shows empty detail pane with "Message sent" confirmation

Cancel — returns to previously selected message (or empty detail if none was selected)

---

Loading inbox items

```js // Inbox messages sb.from('inbox') .select('id, from_agent, message_type, subject, payload, ref_id, ref_type, priority, processed, created_at') .eq('to_agent', 'justin') .order('created_at', { ascending: false }) .limit(50)

// Exec gate tasks sb.from('agent_tasks') .select('topic_id, brief, project_id, created_at') .eq('role', 'exec') .eq('status', 'assigned') .order('created_at', { ascending: true }) ```