Skip to content

feat: add Message Flow Panel dev tool with translation boundary visualization#145

Open
teng-lin wants to merge 8 commits intomainfrom
worktree-message-ui
Open

feat: add Message Flow Panel dev tool with translation boundary visualization#145
teng-lin wants to merge 8 commits intomainfrom
worktree-message-ui

Conversation

@teng-lin
Copy link
Owner

Summary

  • Message Flow Panel — new developer panel (⌘⇧F) that visualizes all WebSocket messages between the bridge and consumer in a two-column timeline (bridge→consumer on left, consumer→bridge on right)
  • Translation boundary eventsT2/T3 translation events emitted at Claude adapter send/receive points and broadcast to consumers; flow panel renders them with boundary badges, translator labels, and native-format details
  • Thinking block streaming — streaming indicator surfaces thinking content during active streams; persisted thinking blocks render in chat as collapsible sections
  • Persisted output blocksPersistedOutputBlock component handles all tool result/output types including images
  • Session startup UX — skip auto-launching a Claude backend when sessions already exist in storage; open NewSessionDialog automatically when no sessions are present
  • Lane direction fix — corrected flow panel lane assignment: addFlowInboundListener (bridge→consumer) maps to left/outbound column; addFlowOutboundListener (consumer→bridge) maps to right/inbound column
  • Chat UI pollution fixadapter_drop and translation_event messages no longer added to chat message store; captured exclusively by flow panel listeners

Test plan

  • Open app with no existing sessions → NewSessionDialog opens automatically
  • Open app with existing sessions → no auto-launched Claude backend, sidebar shows restored sessions
  • Press ⌘⇧F to open Message Flow Panel; verify left column shows bridge→consumer traffic, right column shows consumer→bridge
  • Send a message; verify streaming thinking blocks appear during stream and collapse after completion
  • In flow panel, hover a message with a traceId — related messages across both columns highlight
  • Switch to detailed mode; verify translation boundary pills show with native-format expand/copy
  • Check chat UI — no broken/empty bubbles from adapter_drop or translation_event
  • pnpm test passes
  • pnpm build passes

🤖 Generated with Claude Code

Adds a developer-facing WebSocket message visualizer panel to the web UI,
toggled via ⌥M or the StatusBar "Flow" button. Disabled by default.

## Panel features
- Two-lane timeline: outbound (bridge→consumer) left, inbound (consumer→bridge) right
- Color-coded pills per message type with expand/collapse for full JSON
- Hover-to-connect: glowing SVG bezier lines between paired messages
  (permission_request↔response, tool_use groups) with latency badge
- LIVE/PAUSED toggle with pending count, type filter, auto-scroll, clear
- Resizable width (drag left edge, 360–1200px range)
- No horizontal scroll: pills use min-w-0 + break-all for content wrapping

## Adapter drop instrumentation
- New adapter_drop ConsumerMessage type in both type systems
- session-reducer emits adapter_drop when permission_request subtype is
  unsupported (anything other than can_use_tool), instead of silent discard
- Frontend handles adapter_drop: stored as message, shown as red pill

## Implementation
- ws.ts: addFlowInboundListener / addFlowOutboundListener tap exports
- store.ts: messageFlowOpen boolean (default false)
- useMessageFlow: ring buffer (500 cap), pause/resume, pairing index
- MessagePill, ConnectorOverlay, MessageFlowPanel components
- ⌥M shortcut in useKeyboardShortcuts, Flow button in StatusBar
…utput

Fixes two UI rendering gaps discovered during live usage:
- Thinking blocks now appear live during streaming (not just after completion)
- Large tool output wrapped in <persisted-output> is parsed and displayed with file path + preview
- Add translation_event ConsumerMessage type (T1–T4 boundary markers)
- Emit EMIT_TRANSLATION effect in session-reducer for user_message and
  assistant/result messages, surfacing normalizeInbound and mapAssistantMessage
  translation boundaries
- Add EMIT_TRANSLATION effect type and executor case
- Handle translation_event in ws.ts switch and useMessageFlow hook,
  populating boundary metadata on FlowMessage for detailed panel view
- Add detailLevel prop to MessagePill for compact/detailed display toggle;
  detailed mode shows translation boundary badge and native format inspector
- Add T1→T4 toggle button in MessageFlowPanel header
- Fix MessagePill.test.tsx: add missing detailLevel="compact" to all renders
- Fix ws.ts exhaustive switch: add translation_event case alongside adapter_drop
- adapter_drop and translation_event no longer added to chat store via
  store.addMessage; they are dev-tool metadata captured exclusively by
  flow panel listeners
- swap direction mapping in useMessageFlow: addFlowInboundListener
  (bridge→consumer) now maps to 'out' (left column), addFlowOutboundListener
  (consumer→bridge) maps to 'in' (right column)
- update handlePairing direction guards to match corrected mapping
- simplify MessagePill.test.tsx with renderPill helper (155→100 lines)
- extract shared needsAutoInit check in ws.ts content_block_delta handler
- merge queued_message_cancelled/sent into single fall-through case
- move useCallback hooks before early return in MessageFlowPanel
- replace non-null assertion with optional chain in MessagePill clipboard handler
@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances the developer experience by introducing a comprehensive Message Flow Panel for real-time WebSocket message visualization and debugging. It also refines the display of streaming content, particularly 'thinking' blocks, and improves how large tool outputs are presented. Furthermore, the session startup flow has been made more intuitive, and internal message handling has been cleaned up to prevent irrelevant messages from appearing in the chat UI.

Highlights

  • Message Flow Panel: Introduced a new developer panel (⌘⇧F) that visualizes all WebSocket messages between the bridge and consumer in a two-column timeline, aiding in debugging and protocol verification.
  • Translation Boundary Visualization: Implemented T2/T3 translation events at Claude adapter send/receive points, which are broadcast to consumers and rendered in the flow panel with boundary badges, translator labels, and native-format details.
  • Enhanced Streaming Indicators: The streaming indicator now surfaces 'thinking' content during active streams, and persisted 'thinking' blocks render in chat as collapsible sections.
  • Persisted Output Handling: A new PersistedOutputBlock component was added to handle and display all tool result/output types, including large outputs that are saved to files, providing a preview and copy functionality.
  • Improved Session Startup UX: The application now skips auto-launching a Claude backend if existing sessions are found in storage, and automatically opens the NewSessionDialog when no sessions are present.
  • Message Filtering and UI Cleanup: Corrected flow panel lane assignments and ensured that adapter_drop and translation_event messages are exclusively captured by the flow panel listeners, preventing them from polluting the main chat UI.
Changelog
  • docs/plans/2026-02-25-message-flow-panel-design.md
    • Added a new design document outlining the Message Flow Panel's aesthetics, layout, message types, pairing logic, controls, and implementation plan.
  • shared/consumer-types.ts
    • Added adapter_drop and translation_event types to the ConsumerMessage union.
  • src/adapters/claude/claude-session.ts
    • Imported randomUUID for unique message IDs.
    • Enqueued translation_event messages at T2 (toNDJSON conversion) and T3 (UnifiedMessage translation) boundaries for flow panel visualization.
  • src/bin/beamcode.ts
    • Modified the session auto-launch logic to prevent automatic backend startup if existing sessions are already present.
  • src/core/messaging/consumer-message-mapper.ts
    • Added mapTranslationEvent to correctly map UnifiedMessages of type translation_event to ConsumerMessage format.
  • src/core/session/effect-executor.ts
    • Implemented the EMIT_TRANSLATION effect type to broadcast translation events to consumers.
  • src/core/session/effect-types.ts
    • Defined the EMIT_TRANSLATION effect type for translation event visualization.
  • src/core/session/session-reducer.ts
    • Imported mapTranslationEvent for handling translation events.
    • Added EMIT_TRANSLATION effects for T1 and T4 boundaries within reduceInboundCommand and buildEffects.
    • Introduced an adapter_drop message for unsupported permission_request subtypes.
    • Handled translation_event messages by broadcasting them.
  • src/core/session/session-state-reducer.test.ts
    • Updated test expectations for INBOUND_COMMAND to include the newly added EMIT_TRANSLATION effect.
  • src/core/types/unified-message.ts
    • Added translation_event to the UnifiedMessageType and VALID_MESSAGE_TYPES lists.
  • src/types/consumer-messages.ts
    • Added adapter_drop and translation_event types to the ConsumerMessage union.
  • web/src/App.tsx
    • Imported and rendered the new MessageFlowPanel component.
    • Modified the bootstrap logic to automatically open the NewSessionDialog if no sessions are found.
    • Integrated messageFlowOpen state for controlling the visibility of the Message Flow Panel.
  • web/src/components/AgentColumn.tsx
    • Updated the AgentColumnStreamingIndicator to display agent's 'thinking' content during streaming.
  • web/src/components/AgentPane.tsx
    • Updated the AgentStreamingIndicator to display agent's 'thinking' content during streaming.
  • web/src/components/ConnectorOverlay.tsx
    • Added a new component responsible for drawing SVG connector lines between paired messages in the Message Flow Panel.
  • web/src/components/MessageFlowPanel.test.tsx
    • Added unit tests for the MessageFlowPanel component, covering rendering, session handling, and keyboard shortcuts.
  • web/src/components/MessageFlowPanel.tsx
    • Added the main UI component for the Message Flow Panel, including message display, filtering, pause/resume functionality, and resizing capabilities.
  • web/src/components/MessagePill.test.tsx
    • Added unit tests for the MessagePill component, verifying rendering, interactivity, and styling.
  • web/src/components/MessagePill.tsx
    • Added a new component to represent individual messages within the Message Flow Panel, featuring type, timestamp, payload preview, and translation boundary details.
  • web/src/components/PersistedOutputBlock.tsx
    • Added a new component to display large tool outputs that are saved to files, including a preview, file path, and copy functionality.
  • web/src/components/StatusBar.tsx
    • Added a MessageFlowButton to the status bar, allowing users to toggle the visibility of the Message Flow Panel.
  • web/src/components/StreamingIndicator.test.tsx
    • Added tests to verify the correct rendering of streaming 'thinking' content.
  • web/src/components/StreamingIndicator.tsx
    • Updated the StreamingIndicator to display streaming 'thinking' content alongside regular streaming text.
  • web/src/components/ToolResultBlock.test.tsx
    • Added tests for the new persisted output rendering within ToolResultBlock.
  • web/src/components/ToolResultBlock.tsx
    • Modified renderContent to detect and utilize the PersistedOutputBlock for displaying large tool outputs.
  • web/src/hooks/useKeyboardShortcuts.ts
    • Added a global keyboard shortcut (⌥M) to toggle the Message Flow Panel.
  • web/src/hooks/useMessageFlow.test.ts
    • Added unit tests for the useMessageFlow hook, covering message capture, buffering, pausing, and pairing logic.
  • web/src/hooks/useMessageFlow.ts
    • Added a new React hook (useMessageFlow) to manage the state and logic for the Message Flow Panel, including message buffering, pausing, and message pairing.
  • web/src/store.test.ts
    • Added tests for streamingThinking and agentStreamingThinking state management in the Zustand store.
  • web/src/store.ts
    • Added streamingThinking and messageFlowOpen properties to the AppState interface.
    • Introduced appendStreamingThinking and appendAgentStreamingThinking actions to manage 'thinking' content.
    • Updated clearStreaming to also clear streamingThinking.
  • web/src/utils/persisted-output.test.ts
    • Added unit tests for the isPersistedOutput and parsePersistedOutput utility functions.
  • web/src/utils/persisted-output.ts
    • Added new utility functions (isPersistedOutput, parsePersistedOutput) to identify and parse large tool output blocks.
  • web/src/ws.test.ts
    • Added tests to verify the handling of stream_event thinking deltas.
  • web/src/ws.ts
    • Exported addFlowInboundListener and addFlowOutboundListener to allow external components to tap into WebSocket message flow.
    • Modified flushDeltas and bufferStreamingDelta to correctly handle and buffer mainThinking and agentsThinking content.
    • Introduced bufferStreamingThinkingDelta for accumulating thinking content.
    • Integrated flow listeners into handleMessage and send functions to capture messages for the flow panel.
    • Removed queued_message_cancelled from direct store actions, allowing flow panel to handle it.
    • Configured adapter_drop and translation_event messages to be processed only by flow panel listeners, not the chat UI.
    • Updated _resetForTesting to clear flow listeners for proper test isolation.
Activity
  • The pull request introduces a significant new feature, the Message Flow Panel, along with several related enhancements to streaming and output display.
  • New types and effects were added to the core messaging system to support translation boundary events.
  • Multiple new UI components and hooks were created to implement the Message Flow Panel and handle persisted outputs.
  • Existing UI components were updated to display new 'thinking' streaming content.
  • Unit tests were added for the new components, hooks, and utility functions, and existing tests were updated to reflect changes in message processing.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

…ping

bridge→consumer (inbound listener) = 'out' (left column)
consumer→bridge (outbound listener) = 'in' (right column)
Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a significant new developer tool, the Message Flow Panel, for visualizing WebSocket traffic. The implementation is extensive, touching both frontend and backend to add the necessary instrumentation and UI components. Overall, the changes are well-structured and align with the provided design document. I've identified a few areas for improvement, primarily concerning a logic inversion in the new visualization, opportunities to reduce code duplication in the new backend instrumentation, and an accessibility enhancement for the new panel.

| `permission_request` | `permission_response` | `request.id` / `request_id` | 1:1 by ID |
| `assistant` (with `tool_use` block) | `tool_use_summary` or next `assistant` response | `content[].id` | Walk `content[]` for tool_use blocks |
| `tool_progress` | — | `tool_use_id` | Groups with originating `assistant` pill |
| `message_queued` | `update_queued_message` / `cancel_queued_message` / `queued_message_sent` | Session singleton | Only one queued message per session at a time; pair by temporal adjacency after `message_queued` outbound |
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

In the 'Pairing Logic' table, update_queued_message, cancel_queued_message, and queued_message_sent are listed under the 'Paired Inbound' column. However, these are outbound ConsumerMessage types sent from the bridge to the consumer. This might cause confusion for developers referencing this design document. The column should probably be 'Paired Message' or the directionality should be clarified.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 634932a

Comment on lines +104 to +120
// Emit T2 translation event for message flow panel
this.queue.enqueue({
id: randomUUID(),
timestamp: Date.now(),
type: "translation_event",
role: "system",
content: [],
metadata: {
boundary: "T2",
translator: "toNDJSON",
from: { format: "UnifiedMessage", body: message },
to: { format: "Claude NDJSON", body: ndjson },
trace_id: trace.traceId,
session_id: this.sessionId,
timestamp: Date.now(),
},
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This block for emitting a T2 translation event is very similar to the one for emitting the T3 event later in the file (lines 290-307). To improve maintainability and reduce code duplication, consider extracting this logic into a private helper method within the ClaudeSession class. This method could take parameters like the boundary, translator, from object, and to object.

References
  1. To improve maintainability and avoid code duplication, extract repeated logic blocks into a private helper method.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 634932a

Comment on lines +284 to +293
boundary: (m.boundary as "T1" | "T2" | "T3" | "T4") ?? "T1",
translator: (m.translator as string) ?? "unknown",
from: (m.from as { format: string; body: unknown }) ?? {
format: "unknown",
body: null,
},
to: (m.to as { format: string; body: unknown }) ?? { format: "unknown", body: null },
traceId: m.trace_id as string | undefined,
timestamp: (m.timestamp as number) ?? Date.now(),
sessionId: (m.session_id as string) ?? "",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The use of nullish coalescing with default values (e.g., ?? "T1", ?? "unknown") in mapTranslationEvent can hide bugs where metadata isn't being correctly propagated. For a debugging tool, it's preferable to see that data is missing rather than displaying a potentially incorrect default. Prefer using undefined for optional fields to represent a 'not set' state, and handle their absence explicitly in the UI, rather than providing potentially misleading default strings.

References
  1. Prefer using undefined over an empty string "" for optional fields to represent a 'not set' state, especially when the UI uses the presence of the value to determine whether to display a label.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 634932a

Comment on lines +615 to +628
// Emit T1 translation event for message flow panel
const t1Event: Effect = {
type: "EMIT_TRANSLATION",
event: {
type: "translation_event",
boundary: "T1",
translator: "normalizeInbound",
from: { format: "InboundMessage", body: inboundMsg },
to: { format: "UnifiedMessage", body: unified },
traceId: unified.metadata.trace_id as string | undefined,
timestamp: Date.now(),
sessionId: data.state.session_id,
},
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This logic for creating a t1Event effect is duplicated in a few places in this file for different translation boundaries (T1, T4). To improve maintainability and reduce code duplication, you could create a helper function like createTranslationEventEffect(boundary, translator, from, to, unified, sessionId) that constructs and returns the EMIT_TRANSLATION effect object.

References
  1. To improve maintainability and avoid code duplication, extract repeated logic blocks into a private helper method.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 634932a

Comment on lines +105 to +114
<div
className="absolute left-0 top-0 z-10 h-full w-1 cursor-col-resize bg-bc-border/30 transition-colors hover:bg-bc-accent/60 active:bg-bc-accent"
onMouseDown={handleResizeMouseDown}
role="separator"
aria-orientation="vertical"
aria-valuenow={panelWidth}
aria-valuemin={360}
aria-valuemax={1200}
aria-label="Resize message flow panel"
/>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The resize handle for the panel is implemented with mouse events (onMouseDown), but it lacks keyboard accessibility. Users who rely on keyboards for navigation won't be able to resize the panel. To improve accessibility, consider adding keyboard event handlers (e.g., for arrow keys) to the resize handle, and ensure it's focusable by adding tabIndex={0}.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 634932a

timestamp: number;
sessionId: string;
};
const direction = evt.boundary === "T1" || evt.boundary === "T2" ? "out" : "in";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The direction for translation boundary events appears to be inverted. T1 and T2 events occur on the consumer-to-backend message path, which should be considered in (right column). T3 and T4 events are on the backend-to-consumer path, which should be out (left column). The current logic assigns them to the opposite columns, which will cause them to be visualized incorrectly.

Suggested change
const direction = evt.boundary === "T1" || evt.boundary === "T2" ? "out" : "in";
const direction = evt.boundary === "T1" || evt.boundary === "T2" ? "in" : "out";

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 634932a

- rename 'Paired Inbound' to 'Paired Message' in design doc pairing table
  (the paired messages for message_queued are bridge→consumer, not inbound)
- extract ClaudeSession.enqueueTranslationEvent() private helper to reduce
  T2/T3 emission duplication in claude-session.ts
- remove misleading ?? fallback defaults in mapTranslationEvent — missing
  metadata fields should surface as undefined, not silently default to
  arbitrary values like 'T1' or 'unknown'
- extract translationEffect() helper in session-reducer.ts to deduplicate
  T1/T4/T4 EMIT_TRANSLATION effect construction
- add tabIndex={0} and onKeyDown (ArrowLeft/ArrowRight) to resize handle
  in MessageFlowPanel for keyboard accessibility
- fix T1/T2 translation event direction: consumer→backend path = 'in'
  (right column); backend→consumer path (T3/T4) = 'out' (left column)
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.

1 participant