Skip to content

fix(chat): render correct line count under each parallel bash marker#4003

Closed
CnsMaple wants to merge 1 commit into
esengine:main-v2from
CnsMaple:fix/consecutive-tool-markers-under-own-card
Closed

fix(chat): render correct line count under each parallel bash marker#4003
CnsMaple wants to merge 1 commit into
esengine:main-v2from
CnsMaple:fix/consecutive-tool-markers-under-own-card

Conversation

@CnsMaple

Copy link
Copy Markdown
Contributor

🤖 Generated by AI (CnsMaple + Reasonix)

Summary

Follow-up to #3951 + #3973. Rebased onto current main-v2 (c5c3093); preserves both merged fixes in place. Adds two targeted changes to collapseShellSlot's late path:

  1. Per-id line-count stash (m.toolLineCountByID). When streamToolOutput is about to switch active id, it captures (toolLineCount + (toolPartial != "")) keyed by the old id, so the count survives the reset that used to drop it on the floor.
  2. ToolResult output threaded through. collapseToolOutput / collapseShellSlot now take the result's final Output and use it as the last-resort source for the line count, after shellOutputs and toolLineCountByID have had their say.

Why this exists

After #3951 + #3973, the -1 lines rendering is gone and late-collapsed slots repaint immediately. But the visual result for a back-to-back bash dispatch with real output is now a blank line under every tool that was overtaken — ● Bash(ls) / [empty] / ● Bash(ls) / [empty] / ● Bash(ls) / ⎿ 22 lines. The third tool's marker is correct (it never got overtaken) but the first two lose their line count even though the run actually produced 22 lines each.

I want to flag this as a behavioural choice, not an oversight: the #3951 review comment by @esengine explicitly says "clearing the slot instead of fabricating a count is the right call" and accepts "a blank line — visible but harmless" as the trade-off. This PR takes the opposite trade-off — always show a real line count when one can be recovered, blank only when there's truly no signal. Either is defensible; the test below is the tripwire that makes the choice explicit so the next person reading the code can see what's being committed to.

Repro before this PR

ToolDispatch  bash call_1
ToolDispatch  bash call_2
ToolDispatch  bash call_3
22 × ToolProgress  call_1 "line\n"  (pre-empted by 2 and 3 before call_1's result)
22 × ToolProgress  call_2 "line\n"  (pre-empted by 3 before call_2's result)
22 × ToolProgress  call_3 "line\n"
ToolResult     call_1 "line\n" × 22
ToolResult     call_2 "line\n" × 22
ToolResult     call_3 "line\n" × 22

Transcript on main-v2 (post-#3951 + #3973):

● Bash(ls)

● Bash(ls)

● Bash(ls)
⎿  22 lines

Transcript with this PR

● Bash(ls)
⎿  22 lines
● Bash(ls)
⎿  22 lines
● Bash(ls)
⎿  22 lines

Fallback chain in collapseShellSlot's late path

shellOutputs[id]          (only populated for "shell-" prefixed ids)
  → toolLineCountByID[id] (what streamToolOutput saw before it switched away)
  → resultOutput          (the ToolResult's own Output — covers the "never
                           streamed, result is the only signal" case)
  → 0                     (the existing n<0=0 guard from #3951 keeps the
                           -1 lines regression at bay even when none of
                           the above are available)

Active path uses max(toolLineCount, len(resultOutput)) — the live state is sometimes lower than the result's actual end-state if the last chunk of a slow tool arrived in the same event as the result, and we'd rather render the authoritative count.

Regression tests (in internal/cli/consecutive_tool_markers_test.go)

  • TestParallelBashMarkersKeepOwnLineCount — 3 parallel Bash(ls) × 22 lines, asserts no -1 lines anywhere and that each card's marker slot reports ⎿ 22 lines. Pre-fix on main-v2: fails with c1 marker at transcript[1] should contain ⎿, got "". With this PR: passes.
  • TestNonShellToolLateResultShowsCorrectCount — 2 back-to-back bash tools with no ToolProgress between them; the result is the only signal. Verifies the slot ends with ⎿ N lines driven by e.Tool.Output, not blank.

The pre-existing TestConsecutiveNonShellToolsDoNotRenderNegativeLineCount (from #3951) and TestConsecutiveToolCallsKeepMarkersUnderOwnCard (from #3951) both continue to pass.

Scope notes

  • collapseToolOutput / collapseShellSlot signature change: id, idxid, idx, resultOutput. The new param is plumbed from ingestEvent's ToolResult case (where we have e.Tool.Output in hand) and the other three call sites pass "" (no result available in those flows).
  • toolLineCountByID is initialised in both newChatTUI and the test helper newTestChatTUI. Map entries are written only on id switches where the outgoing count is > 0; the entry is read once and discarded after the matching ToolResult is collapsed.
  • Author's transcriptDirty = true inside collapseShellSlot (fix(chat): repaint late-collapsed tool slots immediately #3973) is preserved. The duplicate transcriptDirty I originally added in collapseToolOutput's active path was dropped during the rebase — the function-body assignment covers both callers and is the cleaner location.
  • No CLI flag / config / model prompt change.

Label

tui — Terminal UI / CLI (internal/cli, internal/control).

Back-to-back tool dispatches with DeepSeek-style call_N ids rendered
the second and subsequent markers as '⎿ -1 lines' (or, with a prior
defensive guard, a blank slot) because:

  * streamToolOutput resets m.toolLineCount whenever it switches to a
    new tool id, losing the previous id's count.
  * collapseShellSlot's late path was the only place the count could be
    recovered, and it derived it from m.shellOutputs[id] — which is
    only populated for shell-prefixed ids (string-gated at the
    streamToolOutput write site). call_N ids never made it into the
    map, so n fell through to -1.

Fix:

  * Add a per-id line-count stash (m.toolLineCountByID). When
    streamToolOutput is about to switch active id, it captures the
    current (toolLineCount + (toolPartial != '')) so the old id's
    value survives.
  * Thread the ToolResult's final output through collapseToolOutput
    and collapseShellSlot as a last-resort source of truth for the
    line count. This handles a tool that never streamed (the result
    is the only signal) and lets the active path use the larger of
    the live line count and the final output (so a slow tool's last
    chunk arriving in the same event as the result isn't lost).
  * collapseShellSlot's late path now falls back through
    shellOutputs -> toolLineCountByID -> resultOutput -> 0, and the
    n<0=0 guard replaces the previous '⎿ -1 lines' rendering.

Regression coverage (internal/cli/consecutive_tool_markers_test.go):

  * TestParallelBashMarkersKeepOwnLineCount: 3 parallel Bash(ls)
    dispatch with 22 lines of streaming output each, verifying each
    card's marker slot reports '⎿ 22 lines' and the transcript
    contains no '-1 lines'.
  * TestNonShellToolLateResultShowsCorrectCount: 2 back-to-back bash
    tools with no ToolProgress between them, verifying the result's
    own output drives the marker line count.

The new fields are initialized in newChatTUI and the test helper
newTestChatTUI.
@github-actions github-actions Bot added v2 Go rewrite (1.x) — main-v2 branch, active development tui Terminal UI / CLI (internal/cli, internal/control) labels Jun 11, 2026
@esengine

Copy link
Copy Markdown
Owner

Thanks for this — the fix is correct and the regression tests are exactly what this needed. CI was only red on a gofmt miss in chat_tui.go, so I cleaned that up (plus trimmed the inline comments to match our terse-comment policy) and re-ran it as #4010, which I'll merge once green. You're credited as the author there. Appreciate the catch on the parallel-bash marker counts!

esengine added a commit that referenced this pull request Jun 11, 2026
…4010)

Parallel call_N-style bash tools lost their per-call line count: streamToolOutput reset the active id's count on every switch and collapseShellSlot's late path only recovered it from shellOutputs (shell- ids only), so call_N ids rendered '-1 lines'. Stash the per-id count before reset and accept the ToolResult output as a last-resort source.

Closes #4003
@CnsMaple CnsMaple deleted the fix/consecutive-tool-markers-under-own-card branch June 12, 2026 02:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

tui Terminal UI / CLI (internal/cli, internal/control) v2 Go rewrite (1.x) — main-v2 branch, active development

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants