feat(tui): add raw scrollback mode (#20819)

## Why

Granular copy is particularly difficult with the current output. Part of
it was solved with the introduction of the `/copy` command but when you
only need to copy parts of a response, you still encounter some issues:

- When you copy a paragraph, the result is a sequence of separate lines
instead of one correctly joined paragraph.
- When a word wraps, part of it stays on the original line and the rest
appears at the start of the next line.
- When you copy a long command, extra line breaks are often inserted,
and command arguments can be split across multiple lines.


https://github.com/user-attachments/assets/0ef85c84-9363-4aad-b43a-15fce062a443

## Solution

Now that we own the scrollback and we re-create it when we resize, we
have the opportunity of toggling between the raw text and the rich text
we see today.

- Add TUI raw scrollback mode with `tui.raw_output_mode`, `/raw
[on|off]`, and the configurable `tui.keymap.global.toggle_raw_output`
action.
- Render transcript cells through rich/raw-aware paths so raw mode
preserves source text and lets the terminal soft-wrap selection-friendly
output.
- Bind raw-mode toggle to `alt-r` by default, with the keybinding path
toggling silently while `/raw` continues to emit confirmation messages.

## Related Issues

Likely addressed by raw mode:

- #12200: clean copy for multiline and soft-wrapped output. Raw mode
removes Codex-inserted wrapping/indentation and lets the terminal
soft-wrap logical lines.
- #9252: command suggestions gain unwanted leading spaces when copied.
Raw mode renders transcript text without the rich-mode left
padding/gutter.
- #8258: prompt output is hard to copy because of leading indentation.
Raw mode renders user/source-backed transcript text without that
decorative indentation.

Partially or conditionally addressed:

- #2880: copy/export message as Markdown. Raw mode exposes raw Markdown
for terminal selection, but this PR does not add a dedicated
export/copy-message command.
- #19820: mouse drag selection + copy in the TUI. Raw mode improves
terminal-native selection of output/history text, but this PR does not
implement in-TUI mouse selection, highlighting, auto-copy, or composer
selection.
- #18979: copied content is divided into two parts. This should improve
cases caused by Codex-inserted wraps/padding in rendered output; if the
report is about pasting into the composer/input path, that remains
outside this PR.

## Validation

- `just write-config-schema`
- `just fmt`
- `cargo test -p codex-config`
- `cargo test -p codex-tui`
- `just fix -p codex-tui`
- `just argument-comment-lint`
- `cargo test -p codex-tui
raw_output_mode_can_change_without_inserting_notice -- --nocapture`
- `cargo test -p codex-tui
raw_slash_command_toggles_and_accepts_on_off_args -- --nocapture`
- `cargo test -p codex-tui raw_output_toggle -- --nocapture`
- `git diff --check`
- `cargo insta pending-snapshots`
This commit is contained in:
Felipe Coury
2026-05-05 11:17:47 -07:00
committed by GitHub
parent 172303bbfa
commit 5e0a4adbe5
39 changed files with 1141 additions and 58 deletions
+3
View File
@@ -92,6 +92,7 @@ pub(super) const KEYMAP_ACTIONS: &[KeymapActionDescriptor] = &[
action("global", "Global", "clear_terminal", "Clear the terminal UI."),
action("global", "Global", "toggle_vim_mode", "Turn Vim composer mode on or off."),
gated_action("global", "Global", "toggle_fast_mode", "Turn Fast mode on or off.", KeymapActionFeature::FastMode),
action("global", "Global", "toggle_raw_output", "Toggle raw scrollback mode."),
action("chat", "Chat", "decrease_reasoning_effort", "Decrease reasoning effort."),
action("chat", "Chat", "increase_reasoning_effort", "Increase reasoning effort."),
action("chat", "Chat", "edit_queued_message", "Edit the most recently queued message."),
@@ -213,6 +214,7 @@ pub(super) fn binding_slot<'a>(
("global", "clear_terminal") => Some(&mut keymap.global.clear_terminal),
("global", "toggle_vim_mode") => Some(&mut keymap.global.toggle_vim_mode),
("global", "toggle_fast_mode") => Some(&mut keymap.global.toggle_fast_mode),
("global", "toggle_raw_output") => Some(&mut keymap.global.toggle_raw_output),
("chat", "decrease_reasoning_effort") => Some(&mut keymap.chat.decrease_reasoning_effort),
("chat", "increase_reasoning_effort") => Some(&mut keymap.chat.increase_reasoning_effort),
("chat", "edit_queued_message") => Some(&mut keymap.chat.edit_queued_message),
@@ -316,6 +318,7 @@ pub(super) fn bindings_for_action<'a>(
("global", "clear_terminal") => Some(runtime_keymap.app.clear_terminal.as_slice()),
("global", "toggle_vim_mode") => Some(runtime_keymap.app.toggle_vim_mode.as_slice()),
("global", "toggle_fast_mode") => Some(runtime_keymap.app.toggle_fast_mode.as_slice()),
("global", "toggle_raw_output") => Some(runtime_keymap.app.toggle_raw_output.as_slice()),
("chat", "decrease_reasoning_effort") => Some(runtime_keymap.chat.decrease_reasoning_effort.as_slice()),
("chat", "increase_reasoning_effort") => Some(runtime_keymap.chat.increase_reasoning_effort.as_slice()),
("chat", "edit_queued_message") => Some(runtime_keymap.chat.edit_queued_message.as_slice()),