[Bug]: Bash tool output never renders in the web client, even when the turn is expanded
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 23k
- Forks
- 5.9k
- Avg merge
- 11h 14m
- Merged PRs (30d)
- 357
Description
Before submitting
- I searched existing issues for duplicates. Closest I found are #4076/#216 (command text duplicated in place of output — different field-selection bug), #3039 (running-turn flicker, fixed pre-0.33), and #4568 (scroll jump on expand) — none of them is "persisted
tool.completedoutput is never rendered". - I included enough detail to reproduce/diagnose (server-side data verified byte-for-byte against Claude Code's own transcript).
Area
apps/web
Steps to reproduce
- Open a project in the web client, provider Claude Code.
- Ask for something that runs a single
Bashcommand with a large stdout (mine was 16,704 chars). - Let the turn finish. The assistant's closing text after the command was short (~800 chars).
- Find the turn in the timeline. It is collapsed behind a "Worked for …" row.
- Expand the turn and expand the "Command run" entry.
The command output is not shown at any point.
Expected behavior
Expanding the turn, then the "Command run" entry, shows the command's output (the tool_result content that the server persisted), or at least a truncated view of it with a way to see the rest.
Actual behavior
The expanded "Command run" entry renders only the command line and the short detail string. The 16,704-char output is not rendered anywhere in the UI — collapsed or expanded. The data is fully present server-side.
Secondary case from the same code path: when a settled turn has no terminal assistant message (tool-only turn, or the turn settles before/without a final assistant message entry), deriveTurnFolds hides every entry in the group, so the turn collapses to a bare "Worked for …" row with nothing behind it.
Impact
Major degradation or frequent failure
Version or commit
v0.0.33 @ 3b72d17cb. I re-read the relevant files on origin/main @ ac1264e2c — unchanged, so this still applies.
Environment
- Server: Linux (
t3 serve), SQLite state store - Client: web
- Provider: Claude Code
Evidence
I diffed Claude Code's own session transcript (.jsonl) against the server's SQLite projections for the same turn. Metadata only — no message content left the box.
| Transcript item | chars | Server-side counterpart | chars | Match |
|---|---|---|---|---|
| user text | 50 | projection_thread_messages (user) |
50 | yes |
Bash tool_use |
– | tool.started / tool.updated activities |
– | yes |
Bash tool_result |
16704 | projection_thread_activities tool.completed → data.result.content |
16704 | yes, exact |
| assistant text | 800 | projection_thread_messages (assistant) |
800 | yes |
The tool.completed payload decodes as data.toolName="Bash", data.input.command (167 chars), data.detail (173 chars), data.result.content (16704 chars), data.result.is_error=false. Nothing is dropped, truncated or zero-length server-side — this is purely client rendering.
(When I asked the same question again ~76 min later, Claude re-emitted the same information as a 17,100-char plain assistant message, which the client did render. That path is projection_thread_messages, not projection_thread_activities.)
Candidate code path
Two independent layers, both in the web client. Line numbers are on origin/main @ ac1264e2c.
1. The tool result never reaches the work-log entry model. WorkLogEntry (apps/web/src/session-logic.ts:64-98) has detail, command, rawCommand, changedFiles, toolData — no field for the tool result. toDerivedWorkLogEntry (apps/web/src/session-logic.ts:802) only populates toolData when itemType === "mcp_tool_call" (lines 865-870); for command_execution the data.result object is simply not read. Worse, extractToolDetail (apps/web/src/session-logic.ts:1397-1424) sources detail exclusively from payload.detail, and for command entries isCommandToolDetail short-circuits it to null before even reaching the rawOutput fallback — so nothing in the client ever reads data.result.content. Consequently buildToolCallExpandedBody (apps/web/src/components/chat/MessagesTimeline.tsx:2038) can only ever emit the command, the detail, and changed files — that is exactly what I see when I expand.
2. Folding hides the tool entry, and hides everything when there is no terminal message. deriveTurnFolds (apps/web/src/components/chat/MessagesTimeline.logic.ts:318) builds hiddenEntryIds at lines 386-398:
const hiddenEntryIds = new Set<string>();
for (const entry of group.entries) {
if (entry.id === group.terminalEntry?.id) {
continue;
}
// Agent-spawn CTA rows never fold: ...
if (entry.kind === "work" && entry.entry.agentSpawn !== undefined) {
continue;
}
hiddenEntryIds.add(entry.id);
}
group.terminalEntry is only set for an assistant message entry in terminalAssistantMessageIds (line 369; derived via deriveTerminalAssistantMessageIds at 460). With terminalEntry === null, every non-agent-spawn entry — including the tool.completed entry — is hidden behind the fold row. MAX_VISIBLE_WORK_LOG_ENTRIES = 1 (MessagesTimeline.logic.ts:13, used at 522) then adds a second, independent collapse layer over consecutive work entries.
Layer 1 alone explains the missing output. Layer 2 explains why turns can collapse to nothing.
Workaround
None in the UI. The output is recoverable from the server's SQLite (projection_thread_activities, payload_json → data.result.content) or from Claude Code's own .jsonl transcript.
Happy to send a small focused PR if wanted.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with WorkLogEntry and toDerivedWorkLogEntry in apps/web/src/session-logic.ts, then trace buildToolCallExpandedBody in apps/web/src/components/chat/MessagesTimeline.tsx and deriveTurnFolds in MessagesTimeline.logic.ts. Verify the persisted tool.completed result.content reaches the expanded work-log entry and that tool-only settled turns retain visible entries; reproduce with a large Bash command output and an expanded turn.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- frontend
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 68/100