Skip to content

feat(messages): persist agent-to-agent messages, surface in sidebar + History#736

Merged
selfcontained merged 21 commits into
mainfrom
agt_5e6a36c5bb67/agent-c5bb67
Jul 10, 2026
Merged

feat(messages): persist agent-to-agent messages, surface in sidebar + History#736
selfcontained merged 21 commits into
mainfrom
agt_5e6a36c5bb67/agent-c5bb67

Conversation

@niiyeboah

Copy link
Copy Markdown
Collaborator

What & why

Agent-to-agent messages (dispatch_send_message) were ephemeral: the handler injected the text into the target's tmux session and kept no record, so there was nothing to view. This PR makes messages persisted artifacts (like pins/brain/feedback) and surfaces them in two places: a per-agent Messages tab in the sidebar, and a Messages tab in the History detail view.

The existing tmux delivery and the same-repo-only send rule are unchanged; this only adds a durable write + read path alongside them.

Changes

Backend

  • New agent_messages table + MessageStore (0029_agent-messages.sql, apps/server/src/messages/store.ts).
  • sendMessage handler now persists each message and publishes a message.created UI event. Delivery-first ordering: tmux injection happens before the DB write, the write is wrapped so a persistence failure can't block delivery, and failed deliveries are recorded with delivered=false (the original delivery error still propagates).
  • New REST endpoints: GET /api/v1/agents/:id/messages and POST /api/v1/agents/:id/messages/read.
  • Backend UiEvent union gains message.created / message.read.
  • History detail (GET /api/v1/history/agents/:id) now returns the agent's messages.

Frontend

  • useAgentMessages hook + SSE wiring (message.created invalidates both participants' message caches; message.read invalidates the one agent).
  • MessagesPanel: the selected agent's sent/received messages grouped by the other participant, with loading/empty states and a "not delivered" affordance.
  • Fourth Messages sidebar tab with an unread badge that clears when opened (mark-read).
  • Messages tab in the History detail view (MessageTimeline).

Scope

  • Persist + view only. The schema stores sender_repo_root / recipient_repo_root now (equal today) so enabling cross-repo later is a config change, not a migration.
  • View-only UI — no composing/replying from the browser; agents still send via the MCP tool.
  • History is intentionally not live-invalidated on message.created (it's retrospective, matching its sibling Events/Media/Pins/Feedback tabs).

Testing

  • Unit (vitest, real DB): MessageStore, the send handler's persist + broadcast on both the delivered and failed-delivery paths, the two REST endpoints (incl. 404s and idempotent mark-read), and the history-detail query. 52 tests across the messages suites pass.
  • E2E (Playwright): seeds messages via DB (agents run inert in E2E, so the live send path isn't exercisable there) and asserts both surfaces render, plus the sidebar unread badge appearing and clearing on open.
  • @dispatch/server and @dispatch/web type-checks pass. Rebased onto current main (v0.26.3) and re-verified.

Notes for reviewers

  • The design spec and implementation plan are included under docs/superpowers/.
  • E2E can't exercise the real agent-to-agent send (inert agents / no tmux), so that path's coverage is the backend unit tests; the E2E covers the read/render surfaces.

🤖 Generated with Claude Code

niiyeboah and others added 17 commits July 10, 2026 12:12
Persist agent-to-agent messages, surface them in a per-agent sidebar
Messages tab, and add a Messages tab to the History detail view.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Task-by-task TDD plan: agent_messages table + MessageStore, persist/
broadcast on send, REST endpoints, useAgentMessages hook + SSE, sidebar
Messages tab, History detail Messages tab, and E2E coverage.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Add GET /api/v1/agents/:id/messages and POST /api/v1/agents/:id/messages/read,
backed by MessageStore. Mark-read publishes a message.read UI event. Also add
the message.created and message.read variants to the backend UiEvent union so
sender/mark-read publishes type-check against the broker.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Expose isLoading from useAgentMessages hook and render a spinner while
loading, preventing misleading empty state flash on initial mount.
Modeled on BrainTabContent's loading pattern for consistency.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Add a messages query to GET /api/v1/history/agents/:id and a Messages
tab to the History detail view, backed by a new MessageTimeline
component.
Add seedAgentMessageViaDB helper and agent-messages.spec.ts covering the
sidebar Messages tab (unread badge appears then clears on open) and the
History detail Messages tab. Also fix a markRead useCallback dependency
(depended on the unstable mutation object, causing a request storm and a
flickering unread badge).

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Final review found the data-flow diagram implied the History detail tab
refetches on message.created; it does not. History is retrospective and
matches its sibling tabs (refetch on remount/focus/staleTime).

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
CI `pnpm run format` (prettier --check) flagged these files; the local
pre-commit hook was unavailable so formatting wasn't applied on commit.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
- Bound MessageStore.listForAgent to the most recent 500 (mirrors the
  history-detail LIMIT 500); the sidebar refetches this eagerly, so it
  must not be unbounded. (security #360, architecture #362)
- Only publish message.created when the row actually persisted; the insert
  failure path stays swallow-and-log so delivery is never blocked, but the
  UI no longer gets an event with no backing row. (security #361)
- Serve unreadCount from GET /agents/:id/messages via countUnreadForAgent
  (uses the partial unread index) and drive the sidebar badge from it, so
  the count stays accurate despite the list cap. (architecture #363)
- Drop the unused agentManager dep from the messages routes. (architecture #364)

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
- Only mark messages read when the sidebar is actually open on the
  Messages tab; gating on the persisted tab alone silently cleared unread
  state for a closed sidebar. (#365)
- Surface unread messages on the collapsed sidebar toggle (combined with
  the unseen-media count) so unread is discoverable while collapsed. (#366)
- Add an explanatory tooltip to the "not delivered" label in both the
  sidebar panel and the History timeline. (#367)
- Give the populated message list min-h-0 flex-1 so it scrolls within the
  sidebar instead of overflowing past the bottom edge. (#368)

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
…mprove icons

Split useAgentMessages into three focused hooks (messages list, unread
count, mark-read mutation). Unify MessageTimeline and MessageBubble into
a single exported component used in both sidebar and history. Add
collapsible agent-grouped threads with framer-motion animation and
persisted collapse state. Add generic Bot fallback icon for unknown agent
types. Replace non-reactive queryClient.getQueryData with reactive
useQuery+select for agent type enrichment. Delete unused plan doc.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
Fix history query to return newest 500 messages (subquery pattern),
resolve recipientRepoRoot from target agent's cwd, add tab overflow
scrolling for sidebar and history tabs, improve copy button touch target.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
@selfcontained
selfcontained force-pushed the agt_5e6a36c5bb67/agent-c5bb67 branch from f1873df to 9f305c7 Compare July 10, 2026 21:21
selfcontained and others added 4 commits July 10, 2026 15:24
Brain data is accessible from the global area; per-agent sidebar tab
was redundant. Reverts icon experiment and restores text labels for the
remaining 4 tabs (Pins, Media, Reviews, Messages).

Co-Authored-By: Claude Opus 4.6 <[email protected]>
Resolves duplicate prefix conflict with 0029_reviews.sql from main.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
Brain tab was removed from the sidebar in this PR.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
The jotai atom reads localStorage asynchronously after hydration;
5s was sometimes too short in CI.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
@selfcontained
selfcontained force-pushed the agt_5e6a36c5bb67/agent-c5bb67 branch from 1f17466 to eabe2c8 Compare July 10, 2026 21:36
@selfcontained
selfcontained merged commit 86d8550 into main Jul 10, 2026
1 check passed
@selfcontained
selfcontained deleted the agt_5e6a36c5bb67/agent-c5bb67 branch July 10, 2026 21:42
selfcontained added a commit that referenced this pull request Jul 11, 2026
#754)

The media sidebar replaced the Brain tab with a Messages tab (PR #736),
but the in-app docs still referenced the old layout. Updates all three
doc sections (media, agents, tools) and adds an ambient tip.

Co-authored-by: Claude Opus 4.6 <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants