Skip to content

feat(ui): Claude Channels TUI — message metadata, status surfaces, dialog, docs (3/3) - #3347

Draft
joestump-agent wants to merge 16 commits into
charmbracelet:mainfrom
joestump-agent:upstream/channels-3-tui
Draft

feat(ui): Claude Channels TUI — message metadata, status surfaces, dialog, docs (3/3)#3347
joestump-agent wants to merge 16 commits into
charmbracelet:mainfrom
joestump-agent:upstream/channels-3-tui

Conversation

@joestump-agent

@joestump-agent joestump-agent commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Part 3/3 of the Claude Channels series (#3298, #3304). Posted by @joestump-agent at @joestump's direction.

Note

Stacked on #3346 — rebased onto the post-#3345 stack: the first 9 commits here belong to #3346; please review commits 10–14 (40ae13d7..ad5cc91c). Marked draft until #3346 merges, then this rebases to just its own commits and flips to ready.

Summary

The TUI layer for channels, plus docs:

  • Message metadata — user messages that arrived via a channel render a sender / via channel / at time metadata line as a separate list item below the body, so channel traffic is distinguishable from typed input.
  • Status surfaces — a Channels section on the landing page and sidebar (alongside LSPs/MCPs/Skills) showing each channel server and its connection state.
  • Channels dialog — a filterable dialog (via the command palette) listing every channel-capable server, its state, and whether it's enabled for this session.
  • Busy-state scoping — with channels dispatching work into background sessions, global agent busy state made the viewed session look busy; the off-thread busy probe (main's update-loop cache) is now scoped to the viewed session. The stale queued-prompt pill half of this fix was superseded by main's session-scoped queue refresh.
  • README — documents the capability, the --channels and channel_enabled opt-ins, payload security posture, delivery semantics, two-way channels, channel_reply routing, and a Signal MCP walkthrough (signal-mcp).

Screenshots

Channels status on first boot:

Channels status on the landing page

Channel-originated message with the metadata line, and the sidebar section:

Channel message metadata and sidebar section

Notes

  • The dialog intentionally ships without a reconnect action for now — that plumbing belongs to a separate MCP-management series brewing in the fork; it'll come as a tiny follow-up.
  • No prompt or fixture changes; upstream's recorded agent cassettes remain valid.

Test plan

  • Metadata rendering tests (internal/ui/chat/user_test.go)
  • Status section tests (internal/ui/model/channels_test.go)
  • Dialog tests: filtering, navigation wrap, empty states (internal/ui/dialog/channels_test.go)
  • Busy-state scoping covered in internal/ui/model tests
  • go build ./..., gofmt clean; full suite matches upstream main baseline

💘 Generated with Crush

Assisted-by: Claude Fable 5

@joestump-agent

Copy link
Copy Markdown
Contributor Author

Heads-up for review: rebased over the newly-merged scrollable sidebar (#3294). The Channels status section is now much simpler — it renders as one more section in updateSidebarScrollState's content (same pattern as Skills), with no height-limit machinery, since virtual scrolling handles overflow.

@joestump-agent
joestump-agent force-pushed the upstream/channels-3-tui branch 2 times, most recently from 57db925 to ad5cc91 Compare July 24, 2026 22:11
joestump and others added 15 commits August 16, 2026 06:57
… needed

Incoming channel messages are dispatched into the active session as user
prompts. When no session exists yet (fresh TUI on the landing page) a
session is auto-created for the channel event instead of dropping it.

Assisted-by: Claude Fable 5
In client/server mode channel notifications now flow through the
server's event stream, so detached clients receive them the same way the
in-process backend does.

Assisted-by: Claude Fable 5
…pace

Channel routing moves out of the TUI and into the backend so each
workspace that opted in receives a channel event exactly once, picking
the viewed session (or most recent top-level session, creating one if
none) as the target.

Assisted-by: Claude Fable 5
MCP servers that declared the claude/channel capability and are enabled
via --channels show a channel marker in the MCP status list, so it is
visible which servers can push into the session.

Assisted-by: Claude Fable 5
Sessions record which channel started them (new channel column on
sessions), and the agent's channel reply tool targets exactly that
channel instead of broadcasting to every connected one. Covers the
in-process and client/server paths.

Assisted-by: Claude Fable 5
Turns that originate from a channel push (e.g. Signal MCP) previously
answered only in terminal output; the person messaging the channel saw
nothing unless the model chose to call a send tool on its own. A new
per-server channel_reply config maps direct and group pushes onto the
server's reply tools, and Crush deterministically sends the turn's final
assistant text through the matching route — to the sender for DMs, to the
group for group messages — skipping the auto-send when the model already
replied through the channel itself during the turn. Local turns and
channels without the config are unchanged.

Fixes #160

Assisted-by: Claude Fable 5
… main

The charmbracelet#3345 review added a filter that keeps channel events out of the
shared SubscribeEvents fan-out (they carry no workspace identity on the
process-global MCP broker), deferring delivery wiring to this PR. Wire
it: a channel-only SubscribeChannelEvents feeds the backend router, and
each app scopes its own subscription to servers its workspace declares
and opted in — restoring in-process injection and SSE delivery without
reopening the cross-workspace leak. Drop the events_test stopgap that
asserted channel events have no SSE representation; they do now.

Assisted-by: Claude Fable 5
Address the reaping question from review: the sessions.channel column
now reflects the most recent turn's origin rather than accumulating
forever. Every turn passes through the coordinator, which (re)binds the
session on a channel-originated turn — newest push wins — and clears
the binding on a local turn, since a session the user takes over is no
longer channel-driven. The column is dropped with the session row on
delete, so no separate reaping pass exists or is needed.

This also closes a gap where the server-side inject path never
persisted the binding at all (only the in-process TUI did): the
coordinator is shared by both paths.

Assisted-by: Claude Fable 5
Add a channel_enabled boolean to the per-server MCP config so channels
can be declared persistently in crush.json instead of requiring
--channels on every launch. The new ChannelOptIn helper ORs the config
flag with the existing --channels override at every gate site — session
creation, renewal, OAuth reconnect, the app's scoped channel event
forwarder, and backend routing — so either source works and they
compose. The claude/channel capability gate is unchanged: config
replaces the CLI opt-in, not the handshake requirement.

Ported from #181.

Assisted-by: Claude Fable 5
db.ResetPool() closes every pooled connection process-wide, so the
cleanup was closing the databases of agent-package tests still running
in parallel (TestPersistCanceledTurn failed with 'sql: database is
closed' in CI). Releasing this test's own dataDir is the scoped cleanup.

Assisted-by: Claude Fable 5
Conflict-resolution follow-ups from rebasing onto main's config-tracking
updateState: restore the stateOpt/withConfig/withPending helpers, gate
runAuthFlow's connect on ChannelOptIn so the config opt-in applies to
OAuth reconnects, and teach mcpConfigEqual about ChannelEnabled so a
config toggle reconciles the server (ChannelReply stays excluded: it is
read live at reply time, not baked into the session).
…pt queue

With channels dispatching work into background sessions, global agent
busy state made the viewed session look busy (and left the queued-prompt
pill stale). Track busy/queue state per session so activity elsewhere
does not leak into the current view.

Assisted-by: Claude Fable 5
User messages that arrived via a channel render a sender/via/at metadata
line as a separate list item below the message body, so channel traffic
is distinguishable from typed input.

Assisted-by: Claude Fable 5
Shows connected channel servers (and their enabled/connected state)
alongside the existing LSP/MCP/Skills status sections.

Assisted-by: Claude Fable 5
Adds a filterable dialog (via the command palette) showing every MCP
server that declared the claude/channel capability, its connection
state, and whether it is enabled for this session.

Assisted-by: Claude Fable 5
@joestump-agent
joestump-agent force-pushed the upstream/channels-3-tui branch 2 times, most recently from 09bcfd7 to d5f1bc4 Compare August 16, 2026 06:24
Covers the claude/channel capability, the --channels per-session opt-in,
payload validation and escaping, delivery semantics in-process and
against crush serve, two-way channels, deterministic channel_reply
routing, and a Signal MCP example.

Assisted-by: Claude Fable 5
@joestump-agent
joestump-agent force-pushed the upstream/channels-3-tui branch from d5f1bc4 to 3ca5e63 Compare August 16, 2026 07:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants