Skip to content

feat(tui): add Alt+C and /copy to copy last agent response as markdown#11333

Closed
fcoury wants to merge 18 commits intoopenai:mainfrom
fcoury:feat/copy-as-markdown
Closed

feat(tui): add Alt+C and /copy to copy last agent response as markdown#11333
fcoury wants to merge 18 commits intoopenai:mainfrom
fcoury:feat/copy-as-markdown

Conversation

@fcoury
Copy link
Contributor

@fcoury fcoury commented Feb 10, 2026

Problem

Users want to quickly copy the last agent response as markdown — for pasting into docs, issues, or chat. The TUI had no clipboard integration, and over SSH even if it did the clipboard would write to the remote machine.

Mental model

The copy feature maintains a parallel timeline of agent response markdown alongside the existing transcript cells. The transcript's history stores rendered Line/Span objects for display — the original markdown syntax (# headings, **bold**, code fences) is lost after rendering. Copying needs the raw markdown, so we keep it separately rather than modifying the shared display model that the rest of the TUI relies on for rendering, scrolling, and rollback.

Alternatives considered:

  • Store raw_markdown on AgentMessageCell — couples copy logic to the display model, bloats every cell with an Option<String> even though only the last one matters, and rollback has to walk the history vec instead of a simple Vec::pop.
  • Query the backend transcript on demandChatWidget only receives events; it has no direct access to the backend transcript. Plumbing an async query path or shared reference would be a bigger architectural change for a single feature.
  • Store just the turn ordinal, reconstruct from plain_text() on copy — eliminates string duplication but makes every copy lossy (not just the deep-rollback edge case), defeating the purpose of raw markdown.
  • Store only markdown, render lazily in display_lines() — the most principled approach (markdown as single source of truth), but the streaming pipeline currently renders deltas incrementally into pre-rendered Line/Span cells, and a single agent response is split across multiple AgentMessageCell instances (one per stream chunk). This would require restructuring streaming to accumulate raw markdown, merging cells into one per response, and re-rendering on every draw (or caching, which is back to storing both). Valid long-term direction but too large a refactor for a copy-feature PR.

The current approach (parallel bounded vec) is the simplest that gives lossless copy in the common case without coupling to either the display model or the backend.

Two data structures track this:

  • last_agent_markdown — the single string that Alt+C / /copy will send to the clipboard.
  • agent_turn_markdowns: Vec<AgentTurnMarkdown> — a bounded (256-entry) vec with drain-on-overflow of (ordinal, markdown) pairs so that transcript rollbacks can truncate the timeline and recover the correct copy source.

The timeline is fed from four event sources (last writer wins within a turn):

  1. AgentMessage — primary source; records commentary or full response text.
  2. PlanItemCompleted — overwrites commentary with the completed plan text, so Alt+C copies the plan the user actually sees rather than a brief preamble.
  3. ExitedReviewMode — records the rendered review output (explanation and/or findings) so Alt+C copies the review result.
  4. TurnComplete.last_agent_message — fallback for providers that don't emit AgentMessage.

A saw_copy_source_this_turn flag prevents TurnComplete from recording a duplicate entry when any copy source (AgentMessage, plan item, or review output) was already recorded during the turn.

Clipboard writes use a two-tier strategy:

  1. SSH detected → OSC 52 only (reaches local terminal).
  2. Local → arboard (native) first, OSC 52 fallback.

What is OSC 52? OSC 52 is a terminal escape sequence (Operating System
Command #52) that asks the terminal emulator to write to the host's system
clipboard. This is critical for SSH sessions: the TUI runs on the remote
machine and has no access to the local clipboard, but the user's terminal
emulator on the local machine interprets OSC 52 and writes to the local
clipboard on their behalf. Most modern terminals (iTerm2, WezTerm, kitty,
Windows Terminal, ghostty) support it.

OSC 52 is written to /dev/tty to bypass ratatui's alternate-screen buffer on stdout.

Non-goals

  • Copying tool-call output or reasoning traces. Note: commentary (AgentMessage) is recorded and copyable while the turn is still running; once a plan item or review output completes, it overwrites the commentary.

Tradeoffs

Decision Why
Bounded 256-entry history Keeps memory bounded in long sessions; deep rollbacks past the boundary use a lossy transcript-cell reconstruction.
plain_text() fallback is lossy The rendered Line/Span cells lose original markdown syntax. Acceptable because it only triggers on deep rollbacks past the 256-entry window.
Transcript fallback only when history is empty After a rollback, the transcript-derived fallback is only inserted when copy history is empty after truncation. If surviving entries remain, they are used as-is.
OSC 52 payload limit of 100 kB Most terminals silently truncate larger payloads; we reject early with a clear error.
Alt+C instead of Cmd+C / Ctrl+C Cmd+C is not available to terminal apps — the terminal emulator intercepts it before it reaches the TUI. Ctrl+C is already wired to SIGINT. Alt+C is the conventional modifier for terminal apps that need a copy binding (e.g. tmux, Midnight Commander).
macOS SuppressStderr guard around arboard arboard::Clipboard::new() initializes NSPasteboard, which triggers os_log/NSLog output on stderr. In raw-mode TUI this corrupts the display. RAII guard redirects fd 2 to /dev/null around the call, serialized by a process-wide mutex. No-op on Linux/Windows.
Plan text unconditionally overwrites commentary copy source In plan mode, the model often emits brief commentary ("I'll create a plan") before the plan itself. The plan is what the user sees and wants to copy — not the preamble.

Architecture

Alt+C / /copy
│
▼
ChatWidget::copy_last_agent_markdown()
│ reads self.last_agent_markdown
▼
clipboard_copy::copy_to_clipboard()
│
├─ SSH? ──► osc52_copy() ──► /dev/tty or stdout
│
└─ Local? ─► arboard_copy() ──► [fail] ──► osc52_copy()

Copy-state lifecycle:

SessionConfigured    → clear all copy state
AgentMessage         → record_agent_markdown (primary source)
PlanItemCompleted    → record_agent_markdown (overwrites commentary with plan text)
ExitedReviewMode     → record_agent_markdown (captures rendered review output)
TurnComplete         → record_agent_markdown (fallback, only if no copy source recorded this turn)
TurnStarted          → reset saw_copy_source_this_turn
ContextCompacted     → no-op on copy state (display-only info event)
Rollback             → truncate_agent_turn_markdowns_to_turn_count + transcript fallback

Copy source priority within a turn:

AgentMessage (commentary) → PlanItemCompleted (plan text overwrites) → ExitedReviewMode (review output overwrites) → TurnComplete (fallback, only if no source recorded yet)

Observability

  • tracing::warn on native clipboard failure before OSC 52 fallback.
  • tracing::debug on /dev/tty open failure before stdout fallback.
  • Visible history-cell feedback: "Copied last message to clipboard" / "Copy failed: ..." / "No agent response to copy".

Tests

~28 new tests covering:

  • Rollback recomputes copy source to surviving agent message
  • Rollback past bounded history uses transcript fallback
  • Deep rollback (300 turns → rollback to turn 20) recovers correct source
  • Context-compacted marker does not replace copy source
  • TurnComplete fallback seeds copy when AgentMessage is missing
  • TurnComplete does not duplicate when AgentMessage already recorded
  • Plan item completion overwrites commentary as copy source
  • Review output (explanation-only) updates copy source on ExitedReviewMode
  • Review output (with findings) updates copy source on ExitedReviewMode
  • Session reset clears copy state before replay
  • Bounded history evicts oldest entries
  • Alt+C clears armed quit-shortcut state
  • /copy available during running task
  • Clipboard strategy: SSH/local, native/OSC52, both-fail error aggregation
  • OSC 52 encoding roundtrip, payload size rejection, writer passthrough
  • Transcript fallback joins stream-continuation groups
  • Transcript fallback ignores info-event markers

Manual testing performed on:

  • macOS (local, native clipboard via arboard)
  • Linux (local)
  • Windows (local)
  • macOS → macOS over SSH (OSC 52 to local terminal)

Copilot AI review requested due to automatic review settings February 10, 2026 17:06
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR adds the ability to copy the last agent response as markdown to the clipboard using Alt+C or the /copy slash command. The implementation maintains a parallel timeline of raw markdown alongside the rendered transcript cells, with a bounded history of 256 entries. The feature supports both native clipboard (via arboard) and OSC 52 terminal escape sequences for SSH sessions.

Changes:

  • Added clipboard copy functionality with native and OSC 52 support
  • Implemented copy state management tracking agent message markdown
  • Integrated copy feature with keyboard shortcut (Alt+C) and slash command (/copy)

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
codex-rs/tui/src/clipboard_copy.rs New module implementing clipboard copy with OSC 52 and arboard, including SSH detection and macOS stderr suppression
codex-rs/tui/src/chatwidget.rs Added copy state fields and logic, event handlers for recording agent markdown, Alt+C binding, and /copy command handler
codex-rs/tui/src/app_backtrack.rs Integrated copy state with rollback, added transcript fallback for recovering copy source after rollback
codex-rs/tui/src/history_cell.rs Added plain_text() method to AgentMessageCell for lossy markdown reconstruction
codex-rs/tui/src/slash_command.rs Added Copy command to slash command enum
codex-rs/tui/src/lib.rs Added clipboard_copy module import
codex-rs/tui/src/chatwidget/tests.rs Comprehensive tests for copy state management, rollback, and edge cases
codex-rs/tui/src/app.rs Tests for rollback integration with copy state, including deep rollback scenarios

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@etraut-openai
Copy link
Collaborator

@fcoury, please resolve the merge conflicts.

@etraut-openai
Copy link
Collaborator

@codex review

Copy link
Contributor

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

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: c1b46516de

ℹ️ 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".

@fcoury fcoury force-pushed the feat/copy-as-markdown branch from c1b4651 to 8dcd492 Compare February 10, 2026 19:36
@J3m5
Copy link

J3m5 commented Feb 10, 2026

Will this feature resolve this issue?
#10486

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated 3 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@etraut-openai
Copy link
Collaborator

@codex review

Copy link
Contributor

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

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: 5a1a1d5fe5

ℹ️ 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".

…ategy

Add module-level, type-level, and function-level documentation to the
copy/clipboard feature so reviewers and future maintainers can understand
the design without reading every line of implementation.

No runtime behavior changes.
Promote non-empty TurnItem::Plan text to the active copy source so Alt+C and /copy copy the final plan output instead of prior commentary in the same turn. Adds regression coverage for replayed user message + commentary + completed plan item flow.
Rename the per-turn copy-source flag to reflect non-AgentMessage sources, clarify ordinal docs for copy-history entries, and make the backtrack continuation-group test assert real merged output.
Record rendered review output on ExitedReviewMode so Alt+C and /copy use the latest review content, including explanation-only and findings-based review results. Adds regression tests for both paths.
@fcoury fcoury force-pushed the feat/copy-as-markdown branch from 67c2cdf to d82a2bc Compare February 10, 2026 22:31
@etraut-openai etraut-openai added the oai PRs contributed by OpenAI employees label Feb 10, 2026
@fcoury
Copy link
Contributor Author

fcoury commented Feb 10, 2026

Will this feature resolve this issue? #10486

Yes, you just need to press Alt+C when the plan is output and it will be copied to the clipboard as markdown.

@fcoury
Copy link
Contributor Author

fcoury commented Feb 24, 2026

Closing in favor of #12613

@fcoury fcoury closed this Feb 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

oai PRs contributed by OpenAI employees

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants