feat: add Message Flow Panel dev tool with translation boundary visualization#145
feat: add Message Flow Panel dev tool with translation boundary visualization#145
Conversation
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
Summary of ChangesHello, 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
Changelog
Activity
Using Gemini Code AssistThe 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
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 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
|
…ping bridge→consumer (inbound listener) = 'out' (left column) consumer→bridge (outbound listener) = 'in' (right column)
There was a problem hiding this comment.
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 | |
There was a problem hiding this comment.
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.
| // 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(), | ||
| }, | ||
| }); |
There was a problem hiding this comment.
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
- To improve maintainability and avoid code duplication, extract repeated logic blocks into a private helper method.
| 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) ?? "", |
There was a problem hiding this comment.
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
- Prefer using
undefinedover 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.
src/core/session/session-reducer.ts
Outdated
| // 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, | ||
| }, | ||
| }; |
There was a problem hiding this comment.
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
- To improve maintainability and avoid code duplication, extract repeated logic blocks into a private helper method.
| <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" | ||
| /> |
There was a problem hiding this comment.
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}.
web/src/hooks/useMessageFlow.ts
Outdated
| timestamp: number; | ||
| sessionId: string; | ||
| }; | ||
| const direction = evt.boundary === "T1" || evt.boundary === "T2" ? "out" : "in"; |
There was a problem hiding this comment.
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.
| const direction = evt.boundary === "T1" || evt.boundary === "T2" ? "out" : "in"; | |
| const direction = evt.boundary === "T1" || evt.boundary === "T2" ? "in" : "out"; |
- 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)
Summary
⌘⇧F) that visualizes all WebSocket messages between the bridge and consumer in a two-column timeline (bridge→consumer on left, consumer→bridge on right)T2/T3translation events emitted at Claude adapter send/receive points and broadcast to consumers; flow panel renders them with boundary badges, translator labels, and native-format detailsthinkingblocks render in chat as collapsible sectionsPersistedOutputBlockcomponent handles all tool result/output types including imagesNewSessionDialogautomatically when no sessions are presentaddFlowInboundListener(bridge→consumer) maps to left/outbound column;addFlowOutboundListener(consumer→bridge) maps to right/inbound columnadapter_dropandtranslation_eventmessages no longer added to chat message store; captured exclusively by flow panel listenersTest plan
NewSessionDialogopens automatically⌘⇧Fto open Message Flow Panel; verify left column shows bridge→consumer traffic, right column shows consumer→bridgetraceId— related messages across both columns highlightdetailedmode; verify translation boundary pills show with native-format expand/copyadapter_droportranslation_eventpnpm testpassespnpm buildpasses🤖 Generated with Claude Code