Skip to content

Polish TUI transcript and reasoning display#200

Merged
anandh8x merged 4 commits into
mainfrom
feat/tui-transcript-polish
Jun 14, 2026
Merged

Polish TUI transcript and reasoning display#200
anandh8x merged 4 commits into
mainfrom
feat/tui-transcript-polish

Conversation

@anandh8x

@anandh8x anandh8x commented Jun 14, 2026

Copy link
Copy Markdown
Collaborator

Summary:

  • improves TUI transcript rendering for submitted prompts, markdown tables, reasoning/thought blocks, and completion status text
  • keeps OpenAI-compatible reasoning content out of normal answer text and shows it as expandable Thought/Thinking content
  • tightens composer and selection behavior around transcript copy, command/file pickers, and prompt display

Tests:

  • GOCACHE=/tmp/zero-go-cache go test ./internal/tui ./internal/providers/openai
  • git diff --check origin/main...HEAD

Summary by CodeRabbit

Release Notes

  • New Features

    • Added separate, expandable “Reasoning” transcript blocks with clickable headers, selectable/live streaming support, and updated transcript spacing.
    • Added optional inline <think>...</think> parsing that can split streamed content into reasoning vs plain text based on provider profile/model defaults.
  • Bug Fixes

    • Improved streaming buffering to flush partial output consistently on idle, errors, malformed payloads, and context cancellation.
    • Refined TUI rendering (elapsed time, “completed” wording) and cache behavior for expanded rows.
  • Tests

    • Added streaming and parsing tests for OpenAI “think” handling and context-cancellation behavior; updated TUI rendering/mouse-selection expectations.

@coderabbitai

coderabbitai Bot commented Jun 14, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: f9867c95-a372-4ee3-8270-59cc0ec4c89e

📥 Commits

Reviewing files that changed from the base of the PR and between 7153ec7 and 2c7aa65.

📒 Files selected for processing (18)
  • internal/config/resolver_test.go
  • internal/config/types.go
  • internal/providers/factory.go
  • internal/providers/factory_test.go
  • internal/providers/openai/provider.go
  • internal/providers/openai/provider_test.go
  • internal/providers/openai/tool_state.go
  • internal/tui/flush.go
  • internal/tui/model.go
  • internal/tui/model_test.go
  • internal/tui/mouse_test.go
  • internal/tui/render_cache.go
  • internal/tui/rendering.go
  • internal/tui/rendering_lime_test.go
  • internal/tui/theme.go
  • internal/tui/transcript.go
  • internal/tui/transcript_selection.go
  • internal/tui/transcript_view.go

Walkthrough

Adds end-to-end provider reasoning support: the OpenAI provider gains a thinkTagSplitter that parses <think>…</think> regions from streamed content into separate StreamEventReasoning events, with flushContent calls added to all error/cancellation/tool-call boundaries. Configuration plumbing enables conditional parsing for reasoning-capable models. The TUI gains a rowReasoning transcript kind with live streaming state, collapsible rendering, mouse toggling, flush-safe lifecycle management, and updated spacing, theme, and render-cache logic.

Changes

Reasoning Stream: Provider → TUI

Layer / File(s) Summary
Configuration and provider factory
internal/config/types.go, internal/config/resolver_test.go, internal/providers/factory.go, internal/providers/factory_test.go
Adds ParseThinkTags optional boolean field to ProviderProfile with camelCase/snake_case JSON variants. Factory's New method conditionally enables parsing via parseThinkTagsForProfile, which respects explicit profile override or enables by default for OpenAI-compatible models matching reasoning/thinking keywords. Config resolver tests verify both field variants resolve correctly.
Provider think-tag splitter and content routing
internal/providers/openai/provider.go, internal/providers/openai/tool_state.go, internal/providers/openai/provider_test.go
thinkTagSplitter implements incremental push/flush/drain parsing with case-insensitive <think> tag detection and partial-prefix boundary handling. Content is routed through emitContent/flushContent; all error, cancellation, tool-call, and payload JSON failure paths flush buffered content before closing tool state. Tests validate single-chunk and cross-chunk tag splitting, literal tag preservation when parsing disabled, and context cancellation with partial buffered content.
Transcript row contract and model state
internal/tui/transcript.go, internal/tui/model.go
Adds rowReasoning kind constant, expanded field on transcript rows, and deduplication key logic for reasoning rows. Extends model with streamingReasoning and streamingReasoningExpanded fields plus agentReasoningMsg message type for incremental reasoning updates.
Theme, rendering, and cache updates
internal/tui/theme.go, internal/tui/rendering.go, internal/tui/render_cache.go, internal/tui/rendering_lime_test.go
Extends tuiTheme with prompt surface tokens and onUserPrompt method. Rewrites renderUserRow with prefix-based half-line layout. Adds assistantMeasure distinct from sayMeasure, renderReasoningRow/Block with collapsed/expanded states and formatElapsedSeconds. Updates doneLine label to "completed in …". Render cache now includes expanded state. Tests updated for new user-row rail, reasoning rows, elapsed time display, and "completed" terminator.
Runtime callbacks and reasoning buffering
internal/tui/model.go, internal/tui/model_test.go
runAgentWithOptions gains reasoning buffering and flushReasoning infrastructure. Wraps OnText/OnReasoning/OnToolCall callbacks so reasoning flushes into transcript rows at turn, tool, cancellation, error, and spec-draft boundaries. interimBlock renders buffered reasoning. Test verifies reasoning row persists separately from assistant answer with correct timing.
Selectable reasoning rows and mouse interaction
internal/tui/transcript_selection.go, internal/tui/flush.go, internal/tui/transcript_view.go, internal/tui/mouse_test.go
transcriptSelectableLine gains toggle and live flags. Adds renderSelectableReasoningRow/Streaming/Block helpers with column-aware hit-testing and toggleReasoningRow dispatch. flush.go and transcript_view.go insert newlines at rowUserrowReasoning transitions. Mouse tests updated for non-zero coordinates; new tests cover reasoning row expand and streaming reasoning expand with markdown-clean output.

Sequence Diagrams

sequenceDiagram
  participant Runtime as zeroruntime
  participant Provider as OpenAI provider
  participant ToolState as toolState
  participant TUI as model
  participant Transcript as transcript rows

  Runtime->>Provider: stream SSE chunks
  Provider->>ToolState: emitContent(delta.content)
  ToolState->>ToolState: thinkTagSplitter.push()
  ToolState->>TUI: StreamEventReasoning | StreamEventText
  
  Note over Provider: Error/Idle/Tool boundary
  Provider->>ToolState: flushContent()
  ToolState->>TUI: emit buffered content
  Provider->>ToolState: closeBufferedOpen()
  Provider->>TUI: StreamEventError or StreamEventDone
  
  Runtime->>Provider: OnReasoning(delta)
  Provider->>TUI: sendAgentReasoning(delta)
  TUI->>TUI: streamingReasoning += delta
  
  Runtime->>Provider: OnText | OnToolCall
  Provider->>Provider: flushReasoning()
  Provider->>TUI: agentRowMsg(rowReasoning)
  TUI->>Transcript: append reasoningTranscriptRow
  TUI->>TUI: streamingReasoning = ""
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

  • Gitlawb/zero#141: Both PRs modify internal/config/types.go's ProviderProfile and JSON unmarshal logic; the retrieved PR expands provider-profile fields for the catalog system while this PR adds ParseThinkTags with reliance on updated profile-emptiness detection.
  • Gitlawb/zero#187: The main PR's TUI rendering changes (e.g., internal/tui/rendering.go switching assistant output with updated wrapping) directly overlap with PR #187's work introducing Markdown rendering for assistant messages.
  • Gitlawb/zero#52: Both PRs touch the same streaming OpenAI provider and TUI foundations, with the main PR extending that groundwork to add <think> parsing/buffering and reasoning UI/flush behavior.

Suggested reviewers

  • gnanam1990
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 12.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: polishing the TUI transcript and reasoning display across multiple rendering, selection, and model logic improvements.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/tui-transcript-polish

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai 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.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/providers/openai/provider.go`:
- Around line 188-190: The buffered content is flushed on line 188 when the
context is cancelled, but the sendEvent function suppresses non-error events
once ctx.Done() is closed, causing the flushed content to be discarded. To fix
this, ensure that the buffered content events from state.flushContent and
state.closeOpen are transmitted to the event stream immediately and
independently from the error event, either by sending them before the context
cancellation can suppress them or by using a mechanism that bypasses the
context-based suppression in sendEvent for these final buffered fragments. The
goal is to preserve the buffered text, reasoning, and tool fragments in the
events that reach the output despite the cancellation context.

In `@internal/tui/transcript_selection.go`:
- Around line 214-236: The expanded reasoning body lines in the selectable
transcript rendering are appended to the lines slice without width clamping,
which can cause text overflow on narrow terminals and impact selection hit
detection. In the loop that processes renderedLines (after the
renderReasoningBodyLines call), apply fitStyledLine(...) to clamp the rendered
line to the viewport width before appending it to the lines slice with the
leading spacing, similar to how the non-selectable reasoning renderer handles
this. Ensure the width parameter used in fitStyledLine matches the width
constraints already being applied elsewhere in this function.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 269fab02-43c5-4514-a99a-7c920acfb478

📥 Commits

Reviewing files that changed from the base of the PR and between 7153ec7 and 566dc81.

📒 Files selected for processing (14)
  • internal/providers/openai/provider.go
  • internal/providers/openai/provider_test.go
  • internal/providers/openai/tool_state.go
  • internal/tui/flush.go
  • internal/tui/model.go
  • internal/tui/model_test.go
  • internal/tui/mouse_test.go
  • internal/tui/render_cache.go
  • internal/tui/rendering.go
  • internal/tui/rendering_lime_test.go
  • internal/tui/theme.go
  • internal/tui/transcript.go
  • internal/tui/transcript_selection.go
  • internal/tui/transcript_view.go

Comment thread internal/providers/openai/provider.go Outdated
Comment thread internal/tui/transcript_selection.go

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

🧹 Nitpick comments (1)
internal/providers/openai/provider_test.go (1)

452-457: ⚡ Quick win

Assert the exact cancel sequence here.

Filtering by type is too loose for this regression. The test still passes if buffered text is emitted after the cancellation error, or if the stream emits done after the error. Since this case is meant to lock flush-on-cancel behavior, assert the ordered sequence and explicitly forbid StreamEventDone.

Suggested assertion shape
-	if text := eventsOfType(events, zeroruntime.StreamEventText); len(text) != 2 || text[0].Content != "visible " || text[1].Content != "<thi" {
-		t.Fatalf("text events = %#v, want visible text plus buffered partial tag", text)
-	}
-	if len(eventsOfType(events, zeroruntime.StreamEventError)) != 1 {
-		t.Fatalf("events = %#v, want one context error after buffered content", events)
-	}
+	if len(events) != 3 ||
+		events[0].Type != zeroruntime.StreamEventText || events[0].Content != "visible " ||
+		events[1].Type != zeroruntime.StreamEventText || events[1].Content != "<thi" ||
+		events[2].Type != zeroruntime.StreamEventError {
+		t.Fatalf("events = %#v, want text, buffered text, then context error", events)
+	}
+	if done := eventsOfType(events, zeroruntime.StreamEventDone); len(done) != 0 {
+		t.Fatalf("events = %#v, want no done after context cancel", events)
+	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/providers/openai/provider_test.go` around lines 452 - 457, The test
assertions in the OpenAI provider test are too loose because they filter events
by type independently rather than checking their exact ordered sequence. Replace
the separate type-filtered checks for text and error events with a single
assertion that validates the complete ordered sequence of events. The assertion
should verify that events occur in the exact order: first two text events with
the expected content ("visible " and "<thi"), followed by exactly one error
event, and importantly, it should explicitly forbid any StreamEventDone event
after the error to properly lock down the flush-on-cancel behavior being tested.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/providers/openai/tool_state.go`:
- Around line 65-68: The emitContentWith method in toolState unconditionally
passes all content deltas through state.think.push() which parses <think> tags,
causing literal <think>...</think> markup in normal responses (code examples,
documentation, etc.) to be incorrectly rewritten as reasoning rows. Add an
explicit compatibility gate or model/provider opt-in check before applying the
think parsing logic: only call state.think.push() when the gate is enabled,
otherwise emit the content directly without parsing to preserve literal markup
in the response.

---

Nitpick comments:
In `@internal/providers/openai/provider_test.go`:
- Around line 452-457: The test assertions in the OpenAI provider test are too
loose because they filter events by type independently rather than checking
their exact ordered sequence. Replace the separate type-filtered checks for text
and error events with a single assertion that validates the complete ordered
sequence of events. The assertion should verify that events occur in the exact
order: first two text events with the expected content ("visible " and "<thi"),
followed by exactly one error event, and importantly, it should explicitly
forbid any StreamEventDone event after the error to properly lock down the
flush-on-cancel behavior being tested.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: cbb56d46-d4e2-4632-8381-33a923a3e07f

📥 Commits

Reviewing files that changed from the base of the PR and between 566dc81 and 6511e74.

📒 Files selected for processing (5)
  • internal/providers/openai/provider.go
  • internal/providers/openai/provider_test.go
  • internal/providers/openai/tool_state.go
  • internal/tui/rendering_lime_test.go
  • internal/tui/transcript_selection.go
🚧 Files skipped from review as they are similar to previous changes (3)
  • internal/providers/openai/provider.go
  • internal/tui/transcript_selection.go
  • internal/tui/rendering_lime_test.go

Comment thread internal/providers/openai/tool_state.go

@coderabbitai coderabbitai 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.

🧹 Nitpick comments (1)
internal/config/resolver_test.go (1)

1083-1126: ⚡ Quick win

Add an explicit parse_think_tags: false case to lock pointer-boolean semantics.

Line 1123 currently validates only the true branch. Add a sibling case asserting explicit false resolves to a non-nil pointer with *profile.ParseThinkTags == false, so regressions between unset vs explicit false are caught.

Suggested test addition
+func TestResolveProviderProfileParseThinkTagsFalseAlias(t *testing.T) {
+	path := writeConfig(t, `{
+		"activeProvider": "custom",
+		"providers": [{
+			"name": "custom",
+			"provider_kind": "openai-compatible",
+			"base_url": "https://custom.example/v1",
+			"model_id": "custom-model",
+			"parse_think_tags": false
+		}]
+	}`)
+	resolved, err := Resolve(ResolveOptions{UserConfigPath: path, Env: map[string]string{}})
+	if err != nil {
+		t.Fatalf("Resolve() error = %v", err)
+	}
+	if resolved.Provider.ParseThinkTags == nil || *resolved.Provider.ParseThinkTags {
+		t.Fatalf("ParseThinkTags = %#v, want explicit false", resolved.Provider.ParseThinkTags)
+	}
+}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/config/resolver_test.go` around lines 1083 - 1126, The current
TestResolveProviderProfileExtendedJSONAliases test only validates the case where
parse_think_tags is set to true (line 1123). Add a sibling test case that
explicitly sets parse_think_tags to false in the JSON configuration and verifies
that profile.ParseThinkTags is a non-nil pointer with the value false (using
assertions similar to the existing true case but asserting
!*profile.ParseThinkTags). This ensures regressions between unset versus
explicitly false parse_think_tags values are caught.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@internal/config/resolver_test.go`:
- Around line 1083-1126: The current
TestResolveProviderProfileExtendedJSONAliases test only validates the case where
parse_think_tags is set to true (line 1123). Add a sibling test case that
explicitly sets parse_think_tags to false in the JSON configuration and verifies
that profile.ParseThinkTags is a non-nil pointer with the value false (using
assertions similar to the existing true case but asserting
!*profile.ParseThinkTags). This ensures regressions between unset versus
explicitly false parse_think_tags values are caught.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 685abd8f-bf0c-4f85-8332-00f3d8201110

📥 Commits

Reviewing files that changed from the base of the PR and between 6511e74 and 06e7b96.

📒 Files selected for processing (7)
  • internal/config/resolver_test.go
  • internal/config/types.go
  • internal/providers/factory.go
  • internal/providers/factory_test.go
  • internal/providers/openai/provider.go
  • internal/providers/openai/provider_test.go
  • internal/providers/openai/tool_state.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • internal/providers/openai/provider_test.go
  • internal/providers/openai/tool_state.go

@anandh8x anandh8x marked this pull request as ready for review June 14, 2026 12:22
@github-actions

github-actions Bot commented Jun 14, 2026

Copy link
Copy Markdown
Contributor

Zero automated PR review

Verdict: No blockers found

Blockers

  • None found.

Validation

  • [pass] Diff hygiene: git diff --check
  • [pass] Tests: go test ./...
  • [pass] Build: go run ./cmd/zero-release build
  • [pass] Smoke build: go run ./cmd/zero-release smoke

Scope

Head: 2c7aa652d6f0
Changed files (18): internal/config/resolver_test.go, internal/config/types.go, internal/providers/factory.go, internal/providers/factory_test.go, internal/providers/openai/provider.go, internal/providers/openai/provider_test.go, internal/providers/openai/tool_state.go, internal/tui/flush.go, internal/tui/model.go, internal/tui/model_test.go, internal/tui/mouse_test.go, internal/tui/render_cache.go, and 6 more

This deterministic review checks validation status and basic diff hygiene. A human reviewer still owns product judgment and design quality.

@Vasanthdev2004 Vasanthdev2004 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Verdict: APPROVE.

I reviewed current head 06e7b96 across both sides of the PR:

  • Provider path: <think> parsing is now gated by ParseThinkTags, so normal literal <think>...</think> content is preserved by default. The cancellation/error paths flush buffered content through the buffered sender before the terminal error, so split tags/content are not dropped.
  • Factory/config path: defaults are conservative for official OpenAI and generic OpenAI-compatible models, with explicit profile override support for reasoning models that emit inline tags.
  • TUI path: reasoning rows are separated from assistant answers, collapse/expand state is modeled on the row, streaming reasoning is flushed before tool calls/cancel/done, and mouse selection/toggling has coverage.

I do not see merge-blocking issues. One old minor CodeRabbit note about selectable reasoning header clamping is not a blocker in the current implementation because the header is bounded short text; body lines are already clamped.

Validation run locally:

  • git diff --check origin/main...HEAD
  • go test ./internal/providers ./internal/providers/openai ./internal/config ./internal/tui -count=1
  • go test ./...
  • go vet ./...
  • go run ./cmd/zero-release build
  • go run ./cmd/zero-release smoke

GitHub checks are green and the PR is mergeable. If GitHub still shows CHANGES_REQUESTED, that is from stale CodeRabbit reviews on older commits; latest CodeRabbit completed successfully.

@anandh8x

Copy link
Copy Markdown
Collaborator Author

@coderabbitai The requested issues have been fixed in the latest commits. The latest CodeRabbit run says no actionable comments were generated and the CodeRabbit status check is green, but GitHub still shows CHANGES_REQUESTED from older stale reviews. Please re-review this PR or clear the stale requested-changes state.

@coderabbitai

coderabbitai Bot commented Jun 14, 2026

Copy link
Copy Markdown

@anandh8x Sure — I'll kick off a fresh full review against the current state of the PR so the review status reflects the latest commits.

✅ Action performed

Full review finished.

@anandh8x

Copy link
Copy Markdown
Collaborator Author

@coderabbitai approve

@coderabbitai

coderabbitai Bot commented Jun 14, 2026

Copy link
Copy Markdown
✅ Action performed

Comments resolved and changes approved.

@anandh8x anandh8x merged commit 6a9f44a into main Jun 14, 2026
7 checks passed
@Vasanthdev2004 Vasanthdev2004 deleted the feat/tui-transcript-polish branch June 28, 2026 08:27
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.

2 participants