New request
#3397: Feature: render session transcript as interleaved AI-comment / CLI-action bubbles
We'll provision a sandbox, run an agent against the issue, and open a draft PR. You can pull the branch and iterate from there.
Feature: render session transcript as interleaved AI-comment / CLI-action bubbles
Summary
The session transcript view (and session replay) currently groups all tool calls for a turn into a single assistant bubble (with tool cards collapsed inside it), with any AI commentary emitted before/during those tool calls also collapsed into the same bubble. The dash TUI shows the same data chronologically: each AI text segment and each tool call as a separate row, in the order they were actually emitted.
I'd like the WebUI to offer the same chronological / interleaved view.
Current behavior
Given a turn where the assistant thinks, calls a tool, thinks again, calls another tool, and emits final text, the WebUI shows:
💬 user: "fix the bug" 🤖 assistant: "I'll check the file first." [▸ terminal: cat /path/to/file] [▸ terminal: patch -p1 < fix.patch] "Done — here's what changed."
The "I'll check the file first." text, the two terminal tool cards, and the "Done — here's what changed." text are all rendered inside one assistant bubble. You have to expand each tool card to see what it did, and the chronological relationship between the AI commentary and the tool calls is hard to follow.
Desired behavior
A new view mode that renders the turn chronologically as separate bubbles, in the order they were actually emitted:
💬 user: "fix the bug"
🤖 ai: "I'll check the file first."
⏵ terminal: cat /path/to/file
(output: …)
🤖 ai: "Now I'll apply the patch."
⏵ terminal: patch -p1 < fix.patch
(output: …)
🤖 ai: "Done — here's what changed."
This matches what the dash TUI already shows and is what most users expect when they read a "what did the agent actually do" log.
Why it matters
- Readability — long turns with many tool calls are hard to scan when everything is collapsed into one bubble.
- Debugging — when something goes wrong mid-turn, the user has to expand each tool card to find which step failed and what the assistant was thinking at the time.
- Audit / log review — the chronological view IS the source of truth (it's how the data is stored in the journal); the current view is a derived layout that drops temporal information.
- Parity with dash TUI — users switching between the two UIs shouldn't have to mentally re-arrange the same data.
Proposed approach
The session message data already preserves the ordering (assistant
message has content[] with text and tool_use parts in sequence;
separate role: 'tool' messages have tool_call_id linking back).
The render layer just needs to walk those parts in order and emit
one row per part instead of merging them into a single assistant
row.
Sketch of the change, roughly localized to static/ui.js's
renderMessages function (line 6453 in current master):
// Where today (line ~475) the code does:
// (m.tool_calls||[]).forEach(tc => { … render tool card … });
// inside an outer "render one assistant row" loop, change to:
function* walkMessageParts(m) {
// For each assistant message, yield interleaved
// {kind:'text', text} and {kind:'tool', toolCall} in order.
for (const part of (m.content || [])) {
if (part?.type === 'text' && part.text) yield {kind:'text', text: part.text};
else if (part?.type === 'tool_use') yield {kind:'tool', toolCall: part};
}
// Then any top-level tool_calls array entries.
for (const tc of (m.tool_calls || [])) yield {kind:'tool', toolCall: tc};
}
Then in the render loop, instead of "one row per message", walk
each emitted part and render text parts as assistant text bubbles
and tool parts as their own tool cards. role: 'tool' result
messages stay as separate bubbles, already keyed by
tool_call_id so they can be inserted adjacent to their parent
tool card (or remain grouped, depending on view mode).
Scope of change
static/ui.js— therenderMessagesfunction (~700 lines). Most of the change is in the assistant-message rendering branch.- Possibly
static/sessions.js— if a "view mode" toggle is added (collapsed / interleaved / TUI-mode), state lives here. - Likely a small CSS addition for the new "tui-bubble" row style.
- No backend changes — all the needed data is already on each assistant message.
Open questions for maintainers
- Default view? Stay collapsed (current default) and add a per-session "tui view" toggle, or flip the default? (I'd vote keep collapsed default to avoid regressing long transcripts; offer the toggle.)
- Reuse / re-derive tool result bubbles? Today
role:'tool'messages are rendered too. In interleaved mode, should the tool result stay adjacent to the tool card (preferred) or remain a separate bubble? - Live streaming? Mid-stream, the assistant message is still being filled — render it in collapsed mode until streaming ends, then "promote" to interleaved on settle? (See #3005 for related live-stream work.)
- Compaction messages? Compressed turns synthesize a single assistant message with no parts. Should those be skipped in interleaved mode, or rendered with a "summary" marker?
Related
- #3005 — reattach SSE on session-switch return (preserves live progress; related to streaming half of this feature)
- #3015 — Restore visible WebUI progress contract
- #2555 — Steer / tool-call distinct-entry preservation
- #1282 — feat: add compact tool activity setting (existing alternative: compact mode groups all tool activity, opposite direction)
Implementation offer
Happy to send a PR for this. Suggest splitting into two:
- Backend-agnostic: the
walkMessagePartshelper + unit test against a few representative session transcripts. - UI: wire it into
renderMessageswith a view-mode toggle, default off, behind a per-session preference.
Reference: dash TUI's transcript renderer (in hermes-agent dash
code) is the inspiration and the reference for ordering.
New chronological transcript view requires refactoring assistant-bubble rendering and styling logic across the chat canvas and replay components.
- static/chat.js
- static/session-replay.js
- static/style.css
- static/sessions.js