Skip to content

fix(tui): store raw assistant text for Ctrl+Y copy#45

Merged
cnjack merged 3 commits intomainfrom
fix/copy-without-divider
Apr 25, 2026
Merged

fix(tui): store raw assistant text for Ctrl+Y copy#45
cnjack merged 3 commits intomainfrom
fix/copy-without-divider

Conversation

@cnjack
Copy link
Copy Markdown
Owner

@cnjack cnjack commented Apr 25, 2026

Summary

Previously Ctrl+Y copied the last assistant response by reverse-scanning m.lines and stripping ANSI codes. This included structural divider lines (── ◇ model via provider ──) in the clipboard output.

Changes

  • Add lastAssistantRawText field to the TUI Model struct
  • Store the raw markdown text in flushText() before rendering
  • Store the raw text during session resume (SessionResumedMsg / EntryAssistant)
  • Simplify Ctrl+Y handler to read directly from the variable
  • Remove getLastAssistantText() and isDividerLine() — no more reverse line scanning

Test Plan

  • Run an agent query, wait for completion, press Ctrl+Y — clipboard should contain only the assistant markdown, no divider
  • Resume a session, then press Ctrl+Y — clipboard should contain the last assistant response from the resumed session
  • Press Ctrl+Y during streaming — should copy the current streaming text

Summary by CodeRabbit

Release Notes

  • New Features

    • Added Ctrl+E keyboard shortcut to toggle expanded/collapsed view of subagent outputs
    • Subagent outputs now render with markdown formatting for improved readability
    • Collapsible subagent sections display helpful control hints and line counts
  • Improvements

    • Ctrl+Y now reliably copies the raw assistant response text
  • Style

    • Applied new purple-accented border styling to subagent output sections

cnjack added 2 commits April 25, 2026 14:13
Subagent output is typically markdown text, but was previously displayed
as raw plain text with a truncated 8-line preview. This change renders
subagent output through glamour for proper markdown formatting (headings,
lists, code blocks, etc.) and adds interactive expand/collapse support.

Changes:
- formatSubagentOutput: render markdown via glamour before display
- Collapsed state: show first 12 rendered lines with hint to expand
- Expanded state: show full rendered output with hint to collapse
- Use purple left border (subagentBodyStyle) to distinguish from normal
  tool output
- Add ctrl+e keybinding to toggle expand/collapse of nearest subagent
  result in viewport
- Simplify SubagentDoneMsg to show concise summary without duplicating
  truncated content (full content shown via ToolResultMsg)
- Session replay also stores subagent results as toolResultContentLine
  for consistent markdown rendering
- Add help panel entry for ctrl+e
…g lines

Previously Ctrl+Y copied the last assistant response by reverse-scanning
m.lines and stripping ANSI codes. This included structural divider lines
(── ◇ model via provider ──) in the clipboard output.

Now the raw markdown text is stored in lastAssistantRawText during
flushText() and session resume, so Ctrl+Y always copies clean content
without dividers or other rendered decorations.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 25, 2026

Warning

Rate limit exceeded

@cnjack has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 51 minutes and 20 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 51 minutes and 20 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c6fdc5c4-7651-4c41-9ec6-78ad84f59fe1

📥 Commits

Reviewing files that changed from the base of the PR and between bc0642f and 38e65a9.

📒 Files selected for processing (1)
  • internal/tui/format.go
📝 Walkthrough

Walkthrough

The PR enhances subagent output rendering in the TUI by adding expandable/collapsible display with optional Markdown formatting. Ctrl+E toggles expansion state of subagent tool results, Ctrl+Y now copies the stored assistant raw response, and a purple-accented style differentiates subagent output from other tool outputs.

Changes

Cohort / File(s) Summary
Subagent Output Formatting
internal/tui/format.go
formatToolResult and formatToolResultBody now accept expanded flag and optional *glamour.TermRenderer. Subagent output is rendered with Markdown when renderer available, then displayed in full (if expanded or short) or truncated with "more lines" hint. Expansion state includes control hints ("▲ ctrl+e 收起" or "(ctrl+e 展开)").
TUI State & Interaction
internal/tui/tui.go
Added persistent storage of assistant raw response (lastAssistantRawText), updated Ctrl+Y to copy stored response, introduced subagent expansion state tracking (toolResultData.expanded), and implemented Ctrl+E toggle handler (toggleSubagentExpand) using viewport-to-content mapping. Subagent completion output now emitted as structured toolResultContentLine with expansion control.
Subagent Output Styling
internal/tui/styles.go
New exported variable subagentBodyStyle defines left-bordered styling for subagent output with purple accent foreground, mirroring tool output structure.
Help Panel
internal/tui/pickers.go
Added Ctrl+E keyboard shortcut to help panel indicating "Expand subagent output".

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant TUI as TUI Controller
    participant Formatter
    participant Renderer as Markdown Renderer

    User->>TUI: Press Ctrl+E on subagent output
    activate TUI
    TUI->>TUI: Toggle expanded state
    TUI->>Formatter: formatToolResultBody(expanded=true, mdRenderer)
    deactivate TUI
    
    activate Formatter
    alt Markdown renderer available
        Formatter->>Renderer: Render markdown content
        Renderer-->>Formatter: Rendered output
        Formatter->>Formatter: Display full rendered content
    else Renderer nil or render fails
        Formatter->>Formatter: Display raw content truncated/full
    end
    deactivate Formatter
    
    Formatter-->>User: Display with control hint (▲ ctrl+e 收起)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

A rabbit hops through markdown streams,
With Ctrl+E and purple dreams,
Subagents expand and collapse with glee,
Formatted prose in harmony! ✨🐰

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 71.43% 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 focuses on storing raw assistant text for Ctrl+Y copy, but the changeset is broader: it includes subagent markdown rendering with expand/collapse, Ctrl+E toggling, styling updates, and more. The title captures only one aspect of the work.
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 unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/copy-without-divider

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
internal/tui/tui.go (1)

2111-2126: ⚠️ Potential issue | 🔴 Critical

Missing result output in live subagent completion.

The success branch only appends a ✓ Subagent Done header — there is no toolResultContentLine("subagent", ...) emission for msg.Result (the subagent's output). Compare with the session-resume path at line 1818 which emits the result via toolResultContentLine("subagent", sanitize(e.Output), nil).

Since SubagentDoneMsg.Result is populated with output data but never appended to m.lines on completion, users would only see output flicker in the in-progress box (renderSubagentBox) during the run. The final output would be missing from the conversation history, defeating the purpose of the ctrl+e expand toggle and making Ctrl+Y copy ineffective on recent live subagent messages.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/tui/tui.go` around lines 2111 - 2126, The SubagentDoneMsg success
branch currently only appends the "✓ Subagent Done" header and omits the
subagent output; update the success branch in the SubagentDoneMsg case to append
the subagent result into m.lines by calling toolResultContentLine("subagent",
sanitize(msg.Result), nil) (or use truncate(sanitize(msg.Result),
maxToolOutputLen) if consistent with other uses), mirroring the session-resume
path’s behavior; ensure you only emit the result when msg.Result is non-empty
and keep the existing refreshViewport() and spinner.Tick append.
🧹 Nitpick comments (2)
internal/tui/tui.go (2)

2682-2697: Minor: backward scan finds the earliest, not nearest, subagent block.

The function comment says it finds the nearest subagent block, and the forward scan starting at startIdx honors that. The fallback loop, however, iterates i := 0; i < startIdx, which returns the first subagent block from the top of history rather than the one closest to the viewport top. For a session with many subagent results, pressing Ctrl+E with no subagent below the fold will jump all the way back to the very first one.

♻️ Suggested fix to scan backward from `startIdx-1`
 	// If not found forward, search from beginning to viewport top.
-	for i := 0; i < startIdx; i++ {
+	for i := startIdx - 1; i >= 0; i-- {
 		if m.lines[i].tool != nil && m.lines[i].tool.name == "subagent" {
 			m.lines[i].tool.expanded = !m.lines[i].tool.expanded
 			m.refreshViewport()
 			return
 		}
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/tui/tui.go` around lines 2682 - 2697, The fallback loop scans from
the top and returns the earliest subagent instead of the nearest before
startIdx; change the backward search to iterate from startIdx-1 down to 0 so it
finds the closest prior m.lines entry whose tool.name == "subagent", toggle its
tool.expanded and call m.refreshViewport() just like the forward branch; update
the second loop logic that currently uses for i := 0; i < startIdx to a reverse
loop (starting at startIdx-1, decrementing to 0) to locate the nearest previous
subagent block.

2620-2646: maxRenderedLines ignores markdown rendering and expanded/collapsed state.

For subagent tool results, the actual rendered height depends on:

  • Whether expanded is true (full glamour output) vs false (capped at collapsedLines = 12 + hint).
  • Glamour markdown rendering, which can substantially expand line count (headings, code-block framing, padding).

Estimating from the raw output (with simple wrap math) can mis-locate the viewport top by many lines when a long expanded subagent block is on screen, causing toggleSubagentExpand to pick the wrong block in some cases. The current heuristic is good enough for common cases, but consider either:

  • Caching the last rendered line count per contentLine, or
  • Accounting for tool.expanded and the collapsedLines cap when tool.name == "subagent".

Not a blocker — the dual forward/backward scan masks small inaccuracies — but worth a follow-up if users report ctrl+e toggling unexpected blocks.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/tui/tui.go` around lines 2620 - 2646, maxRenderedLines currently
estimates height from raw text and ignores subagent expanded/collapsed state and
markdown glamour rendering; update maxRenderedLines to special-case when
tool.name == "subagent": if tool.expanded is false return min(estimatedLines,
collapsedLines)+1 (include the hint line), and if tool.expanded is true compute
the rendered line count by either (a) rendering the tool.output through the same
glamour/markdown renderer used elsewhere and counting the resulting lines, or
(b) using a cached rendered line value on toolResultData (e.g.,
tool.lastRenderedLines) that the renderer updates when drawing; ensure you
reference tool.expanded, tool.name == "subagent", collapsedLines, and optionally
tool.lastRenderedLines so toggleSubagentExpand will get accurate heights.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@internal/tui/format.go`:
- Around line 222-260: The function formatSubagentOutput currently shows the "▲
ctrl+e 收起" hint only when expanded AND len(rawLines) > collapsedLines, but it
doesn't indicate when expanded==true and len(rawLines) <= collapsedLines; update
formatSubagentOutput to either (A) suppress the expand/collapse hint entirely
for short content by ensuring the hint is only added when len(rawLines) >
collapsedLines, or (B) explicitly render a no-op/disabled hint when expanded is
true but content is short so the UI reflects the state; alternatively, also
update toggleSubagentExpand to be a no-op when len(rawLines) <= collapsedLines
to prevent state flips—make the change around the expanded check and the place
that appends lipgloss.NewStyle().Render("▲ ctrl+e 收起") so the behavior is
consistent.
- Around line 253-275: The UI hints in the subagent output box are in Chinese;
update the two rendered hint strings so they’re in English and match the rest of
the TUI (replace "▲ ctrl+e 收起" with something like "▲ ctrl+e Collapse" and the
trailing hint "… %d more lines (ctrl+e 展开)" with "… %d more lines (ctrl+e
Expand)") inside the code that builds boxContent (references: rawLines,
collapsedLines, colorMuted, lipgloss.NewStyle(), boxContent.WriteString()) so
both expand/collapse prompts are consistent with other labels (e.g., "Expand
subagent output").

---

Outside diff comments:
In `@internal/tui/tui.go`:
- Around line 2111-2126: The SubagentDoneMsg success branch currently only
appends the "✓ Subagent Done" header and omits the subagent output; update the
success branch in the SubagentDoneMsg case to append the subagent result into
m.lines by calling toolResultContentLine("subagent", sanitize(msg.Result), nil)
(or use truncate(sanitize(msg.Result), maxToolOutputLen) if consistent with
other uses), mirroring the session-resume path’s behavior; ensure you only emit
the result when msg.Result is non-empty and keep the existing refreshViewport()
and spinner.Tick append.

---

Nitpick comments:
In `@internal/tui/tui.go`:
- Around line 2682-2697: The fallback loop scans from the top and returns the
earliest subagent instead of the nearest before startIdx; change the backward
search to iterate from startIdx-1 down to 0 so it finds the closest prior
m.lines entry whose tool.name == "subagent", toggle its tool.expanded and call
m.refreshViewport() just like the forward branch; update the second loop logic
that currently uses for i := 0; i < startIdx to a reverse loop (starting at
startIdx-1, decrementing to 0) to locate the nearest previous subagent block.
- Around line 2620-2646: maxRenderedLines currently estimates height from raw
text and ignores subagent expanded/collapsed state and markdown glamour
rendering; update maxRenderedLines to special-case when tool.name == "subagent":
if tool.expanded is false return min(estimatedLines, collapsedLines)+1 (include
the hint line), and if tool.expanded is true compute the rendered line count by
either (a) rendering the tool.output through the same glamour/markdown renderer
used elsewhere and counting the resulting lines, or (b) using a cached rendered
line value on toolResultData (e.g., tool.lastRenderedLines) that the renderer
updates when drawing; ensure you reference tool.expanded, tool.name ==
"subagent", collapsedLines, and optionally tool.lastRenderedLines so
toggleSubagentExpand will get accurate heights.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8097cb5d-b688-42c4-a08d-bbc59f709012

📥 Commits

Reviewing files that changed from the base of the PR and between 4b449d7 and bc0642f.

📒 Files selected for processing (4)
  • internal/tui/format.go
  • internal/tui/pickers.go
  • internal/tui/styles.go
  • internal/tui/tui.go

Comment thread internal/tui/format.go
Comment on lines +222 to 260
func formatSubagentOutput(output string, termWidth int, expanded bool, mdRenderer *glamour.TermRenderer) []string {
output = strings.TrimRight(output, "\n")
if output == "" {
return nil
}

shown := rawLines
hidden := 0
if len(rawLines) > tailLines {
shown = rawLines[:tailLines]
hidden = len(rawLines) - tailLines
// Render markdown via glamour if available.
rendered := output
if mdRenderer != nil {
if md, err := mdRenderer.Render(output); err == nil {
rendered = strings.TrimRight(md, "\n")
}
}

const collapsedLines = 12
rawLines := strings.Split(rendered, "\n")

boxWidth := termWidth - 8
if boxWidth < 30 {
boxWidth = 30
}

if expanded || len(rawLines) <= collapsedLines {
// Show all content.
var boxContent strings.Builder
for i, line := range rawLines {
boxContent.WriteString(line)
if i < len(rawLines)-1 {
boxContent.WriteString("\n")
}
}
if expanded && len(rawLines) > collapsedLines {
boxContent.WriteString("\n")
boxContent.WriteString(lipgloss.NewStyle().Foreground(colorMuted).Italic(true).
Render("▲ ctrl+e 收起"))
}
box := subagentBodyStyle.Width(boxWidth).Render(boxContent.String())
return []string{box}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Edge case: short rendered content + expanded=true shows no collapse hint.

When expanded is true but len(rawLines) <= collapsedLines, the user sees the same output as the collapsed/short state with no indication that they're already in "expanded" mode or that Ctrl+E will toggle. This is a minor UX wart since pressing Ctrl+E will silently flip a hidden state, then a second press still does nothing visible. Not a blocker — the content is already fully visible — but worth confirming intent. If desired, you could also skip the toggle entirely for short content in toggleSubagentExpand.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/tui/format.go` around lines 222 - 260, The function
formatSubagentOutput currently shows the "▲ ctrl+e 收起" hint only when expanded
AND len(rawLines) > collapsedLines, but it doesn't indicate when expanded==true
and len(rawLines) <= collapsedLines; update formatSubagentOutput to either (A)
suppress the expand/collapse hint entirely for short content by ensuring the
hint is only added when len(rawLines) > collapsedLines, or (B) explicitly render
a no-op/disabled hint when expanded is true but content is short so the UI
reflects the state; alternatively, also update toggleSubagentExpand to be a
no-op when len(rawLines) <= collapsedLines to prevent state flips—make the
change around the expanded check and the place that appends
lipgloss.NewStyle().Render("▲ ctrl+e 收起") so the behavior is consistent.

Comment thread internal/tui/format.go Outdated
@cnjack cnjack merged commit 0a1c1a3 into main Apr 25, 2026
1 check passed
@cnjack cnjack deleted the fix/copy-without-divider branch April 26, 2026 13:26
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.

1 participant