Skip to content

fix(tui): render terminal assistant messages - #1059

Merged
dennisonbertram merged 3 commits into
mainfrom
codex/issue-1056-terminal-assistant-message
Aug 1, 2026
Merged

fix(tui): render terminal assistant messages#1059
dennisonbertram merged 3 commits into
mainfrom
codex/issue-1056-terminal-assistant-message

Conversation

@dennisonbertram

@dennisonbertram dennisonbertram commented Jul 30, 2026

Copy link
Copy Markdown
Owner

Closes #1056

Related to #1000 as an adjacent TUI reliability fix; this is not a scheduled-automation child issue.

Summary

The TUI now treats a non-empty terminal assistant.message event as the authoritative full assistant response. Valid non-streaming providers that emit Turn.Content without deltas therefore render a visible assistant bubble and finalize the existing transcript entry on run.completed.

The reducer remains idempotent for streaming providers: an identical delta-assembled response plus terminal full message renders once, while a differing terminal full message replaces the partial content authoritatively.

Root cause

StartSSEBridgeWithOptions already forwarded assistant.message, and harnessd already persisted and served it. Model.Update handled only assistant.message.delta; consequently lastAssistantText stayed empty for final-only responses and the existing SSEDoneMsg path had nothing to finalize into the TUI transcript.

The documented fake-provider contract permits Turn.Content with optional Deltas, so the reproduction was valid provider behavior rather than contract misuse.

Scope and impact

  • Changes only the existing TUI SSE reducer and its behavioral tests.
  • No server, provider, SSE schema, API, persistence, model-routing, usage-accounting, web, or macOS changes.
  • Empty or malformed terminal payloads remain safe no-ops.
  • Run lifecycle remains active until run.completed.
  • Rollback: revert 3c38a401 if streamed replies duplicate or terminal reconciliation regresses.

Test-first evidence

Red before implementation:

go test ./cmd/harnesscli/tui -run 'TestSSEEventMsg_AssistantMessage' -count=1

Failed because final-only content left LastAssistantText() empty and a differing terminal full message left the partial delta text authoritative.

Green after implementation:

go test ./cmd/harnesscli/tui -run 'TestSSEEventMsg_AssistantMessage' -count=1
go test ./cmd/harnesscli/tui -count=1
go test -race ./cmd/harnesscli/tui -count=1
go test ./cmd/harnesscli/... -count=1
env TMPDIR=/private/tmp/go-code-issue-1056-regression GOCACHE="$PWD/.tmp/issue-1056-regression/gocache" COVERPROFILE_PATH="$PWD/.tmp/issue-1056-regression/coverage.out" ./scripts/test-regression.sh

All passed. The full regression gate completed normal tests, full race, coverage, and reported coveragegate: PASS (total=85.6%, min=80.0%, zero-functions=0) followed by [regression] PASS.

Real TUI reproduction and proof

Unmodified origin/main at fedcf6073135deb7cce1fa49921aa698a9cc7cd7, real tmux-hosted harnessd plus real 120x40 TUI:

  • conversation / first run: run_e4e9aadf-fdf5-417e-bbc6-9640fd80e5aa
  • second run: run_f9ea72e5-3250-4152-b65b-dc177d9d3107
  • fake-provider replies: LUNA_NONSTREAM_REPLY_ONE, LUNA_NONSTREAM_REPLY_TWO
  • pane: two user prompts, no assistant bubbles
  • raw SSE: assistant.message then run.completed, with no delta
  • run API, conversation API, SQLite, and harnessd logs: both assistant replies persisted correctly

Fixed branch, same real path, three consecutive turns:

  • conversation / first run: run_35981662-52fb-461f-8908-6fa3b55a0792
  • second run: run_a9c4d801-8e7f-4a1d-a923-6517f44bbcfe
  • third run: run_73d2c720-be6f-4d81-aa8c-de9feca8a625
  • replies ISSUE1056_FIXED_REPLY_ONE, _TWO, and _THREE each rendered exactly once
  • composer remained available after each completed turn
  • raw SSE, HTTP transcript, SQLite rows, pane capture, and harnessd logs agreed

The visible 0%/200K context indicator remained expected for 25/37/49 tokens because sub-1% usage rounds to zero; cost increased each turn and this PR does not change usage accounting.

Documentation

Added the issue plan and cross-surface impact map and updated the required plan index plus long-term, engineering, observational, and system logs.

Checklist

  • Structured bug issue created before implementation with acceptance criteria and impact analysis
  • Deterministic regression observed red before the fix
  • Focused, package, adjacent CLI, race, and full regression gates pass
  • Real multi-turn tmux TUI path proven before and after
  • Required durable docs and indexes updated
  • No failing baseline waived
  • Ready for independent Luna re-review

Exact-head review follow-up (ebe2d9a8)

Addressed review comment 3687040783 and the additional Luna exact-head finding in the same #1056 acceptance boundary:

  • A tool card now closes the preceding assistant bubble's viewport-tail ownership, so a later final-only provider step appends a new assistant bubble instead of replacing the card.
  • The next streamed step starts a fresh accumulator.
  • Terminal assistant and completion replay are idempotent after the response is finalized.
  • A per-run finalization bit prevents duplicate transcript entries without clearing lastAssistantText, preserving post-run copy behavior.
  • RunStartedMsg resets finalization so later turns render and record normally.

The behavior regression drives:
assistant.message.delta -> tool.call.started -> tool.output.delta -> tool.call.completed -> assistant.message -> reconnect re-delivery -> run.completed -> terminal/completion replay -> later run.
It asserts early bubble -> tool card -> final bubble order, exact-once viewport content, exact-once transcript finalization, and a distinct later response.

Red evidence:

  • Original review case: final/tool order was corrupted (early=35 tool=85 final=59).
  • Luna finalization case: repeated completion produced two identical transcript entries.
  • Post-completion terminal replay produced two visible final bubbles.

Verification at ebe2d9a8:

  • focused mixed-step normal and race: PASS
  • complete cmd/harnesscli/tui normal and race: PASS
  • go test ./cmd/harnesscli/... -count=1: PASS
  • foreground non-TTY ./scripts/test-regression.sh: PASS (normal, full race, E2E, coverage)
  • coveragegate: PASS (total=85.6%, min=80.0%, zero-functions=0)

The hosted fixture race remains owned by #1044 / PR #1045 and is not changed here; it did not reproduce in this foreground full-race run.

Rebase verification (b3afc7ec -> 9b7cdde3)

Rebased cleanly without conflicts onto exact origin/main b3afc7e; the published PR head is 9b7cdde. Range-diff preserved the final-only assistant-message reconciliation, mixed delta/tool/final viewport ownership fix, reconnect/terminal replay idempotency, and later-turn behavior. The only contextual difference is the expected #1045 plan-index entry now supplied by the new base.

Verification on the rebased exact head:

  • go test ./cmd/harnesscli/tui -run 'TestSSEEventMsg_AssistantMessage_MixedToolStepPreservesViewportAndReconnectIdempotency' -count=1: PASS
  • go test -race ./cmd/harnesscli/tui -run 'TestSSEEventMsg_AssistantMessage_MixedToolStepPreservesViewportAndReconnectIdempotency' -count=1: PASS
  • foreground non-TTY ./scripts/test-regression.sh: PASS (normal, full race, E2E, coverage)
  • coveragegate: PASS (total=85.6%, min=80.0%, zero-functions=0)
  • No baseline failure was waived. The [Bug]: AskUserQuestion status regression test races run ID publication #1044 fixture-race fix is present through merged PR Fix AskUserQuestion status regression test race #1045 in the rebased base.

@cursor

cursor Bot commented Jul 30, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@dennisonbertram

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8745889da9

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

// providers precede it with deltas, but valid non-streaming
// providers may emit only this terminal message.
m.lastAssistantText = p.Content
m.renderActiveAssistantBubble()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve tool cards when reconciling a final-only turn

When an earlier tool-calling step emitted text deltas, responseStarted remains true after handleToolStart appends the tool card. If the later terminal step is non-streaming, this call reaches renderActiveAssistantBubble, which uses ReplaceTailLines(activeAssistantLineCount, ...) even though the viewport tail is now the tool card; it therefore removes part or all of that card and leaves its recorded line offsets stale. This can occur with the supported per-turn mix of deltas/tool calls followed by a final-only response, so the terminal reconciliation must append or replace the tracked assistant bubble without treating unrelated tail content as that bubble.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Addressed at exact head ebe2d9a85954cea45e0763cabec57ebba747e08c.

The regression now drives the realistic sequence assistant.message.delta -> tool.call.started -> tool.output.delta -> tool.call.completed -> assistant.message, replays the tool completion and terminal event, then replays terminal completion and starts a later run. It asserts the early assistant bubble, tool card, and final-only assistant bubble remain ordered and appear exactly once, the transcript finalizes once, and the later run records normally.

The minimal ownership fix closes the preceding assistant bubble when handleToolStart appends the tool card; the next provider step starts a fresh accumulator. Terminal replay is also ignored once that run's response is finalized.

Verification: focused normal/race, complete TUI normal/race, all cmd/harnesscli/..., and foreground non-TTY ./scripts/test-regression.sh all pass; coverage is 85.6% with zero uncovered functions. The unrelated hosted fixture race remains #1044 / PR #1045 and was not folded into this PR.

Copy link
Copy Markdown
Owner Author

@codex review

Please re-review exact head ebe2d9a85954cea45e0763cabec57ebba747e08c, including the mixed delta/tool/final viewport ownership path and repeated terminal/completion idempotency. Both central and Luna findings are covered by the new behavior regression. Full foreground non-TTY regression passes; #1044 / PR #1045 remains out of scope.

dennisonbertram commented Jul 31, 2026

Copy link
Copy Markdown
Owner Author

@codex review

Final exact head is now 00ccedee52dc23b6080db893e6d95f99c1a1c0a5 (the only commit after verified implementation ebe2d9a85954cea45e0763cabec57ebba747e08c is the docs-only active-plan publication status). Please review this exact head. PR remains unmerged; #1044 / PR #1045 remains out of scope.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Can't wait for the next one!

Reviewed commit: 00ccedee52

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@dennisonbertram
dennisonbertram force-pushed the codex/issue-1056-terminal-assistant-message branch from 00ccede to 9b7cdde Compare July 31, 2026 00:28

Copy link
Copy Markdown
Owner Author

@codex review

Please review exact rebased head 9b7cdde3b740b0c77902fcfb33d4485183f01ab3 against base b3afc7ec487c60762a91a1219ceb92c523ef0e78. The final-only assistant rendering, mixed delta/tool/final viewport ownership, reconnect and repeated-terminal idempotency, and later-turn behavior are preserved. Focused normal/race and the unchanged foreground non-TTY ./scripts/test-regression.sh all pass, including full race, E2E, and coverage at 85.6% with zero uncovered functions. PR remains unmerged.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Bravo.

Reviewed commit: 9b7cdde3b7

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@dennisonbertram

Copy link
Copy Markdown
Owner Author

2026-08-01 exact-head promotion evidence

Published exact candidate: 6ce18774a94ee2b014e74c5d937a80fe03b18c95

Exact base: c10c085d12b4050042d4c795708a2b7659e2a007 (current production main before this PR).

The candidate was rebased from the prior PR head and independently reviewed. The review found one additional P2: /resume could reopen per-run finalization without clearing the previous run's assistant accumulator, causing a duplicate stale transcript row if the continuation terminated before emitting assistant content.

Strict red-green follow-up:

  • TestRegression_ResumeWithoutAssistantContentDoesNotDuplicatePriorReply reproduced the duplicate for both run.completed and run.failed.
  • RunStartedMsg now clears the per-run assistant accumulator before finalization is reopened.
  • Focused normal and race TUI matrices pass.
  • Real multi-turn PTY + raw SSE + HTTP + SQLite + fresh --resume proof from the promotion worktree remains green: user/assistant/user/assistant visible exactly once in one conversation and replayed exactly once.

Authoritative foreground verification on exact 6ce18774:

DEVELOPER_DIR=/Library/Developer/CommandLineTools TMPDIR=/private/tmp GOCACHE=/private/tmp/gocode-1059-fixed-go-cache ./scripts/test-regression.sh
coveragegate: PASS (total=85.7%, min=80.0%, zero-functions=0)
[regression] PASS

No baseline failure was waived. The branch was updated with an exact force-with-lease from old remote head 9b7cdde3b740b0c77902fcfb33d4485183f01ab3.

@dennisonbertram
dennisonbertram force-pushed the codex/issue-1056-terminal-assistant-message branch from 6ce1877 to 3506e01 Compare August 1, 2026 01:56
@dennisonbertram
dennisonbertram merged commit 3506e01 into main Aug 1, 2026
2 checks passed
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.

[Bug]: TUI drops terminal assistant.message from non-streaming providers

1 participant