-
Notifications
You must be signed in to change notification settings - Fork 6.9k
WIP: Rework TUI viewport, history printing, and selection/copy #7601
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
|
@codex review |
There was a problem hiding this 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.
ℹ️ 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".
fa397c8 to
e17a99d
Compare
58ce53d to
e72a8f2
Compare
Capture the tui2 viewport/transcript model and streaming wrapping design so future viewport work has a stable reference point.
Render the chat composer in a bottom-aligned area and draw transcript lines in the remaining space, with the viewport reset each frame so we control scrolling and selection behavior end-to-end.
Clear the transcript region and render each row with viewport-aware wrapping, keeping a single blank line between non-streaming cells while streaming continuations stay tight. Enable mouse capture so wheel scrolling arrives as mouse events instead of terminal scrollback.
Track transcript scroll intent with TranscriptScroll while keeping the current rendering path unchanged, and plumb mouse events through the TUI event stream so future viewport scrolling can react to wheel input. Run the main UI in alt screen to give the chat+transcript view the full terminal without polluting normal scrollback.
Teach the v2 TUI to maintain a stable transcript viewport when scrolled
while preserving bottom-pinned behavior by default.
This refactors inline transcript rendering to use a shared
`build_transcript_lines` helper that flattens `HistoryCell::display_lines`
into `Line` rows alongside metadata mapping each row back to its
`(cell_index, line_in_cell)` origin (or `None` for spacer lines). The
rendering path uses this metadata with `TranscriptScroll` to compute a
top offset: when `ToBottom`, it shows the tail that fits in the
transcript area; when `Scrolled`, it anchors to the requested cell/line
if present and falls back to bottom if the anchor has been pruned.
Mouse wheel events over the transcript area now route through
`handle_mouse_event`/`scroll_transcript`, deriving a ±3-line delta,
clamping within available history, and updating `transcript_scroll` back
to `ToBottom` or a `Scrolled { cell_index, line_in_cell }` anchor based
on the nearest visible line. The draw loop clones `transcript_cells`
before entering the closure to satisfy the borrow checker, and scroll
adjustments schedule a new frame via the existing `FrameRequester`.
Snapshot behavior stays unchanged: when the user has not scrolled, the
transcript remains pinned to the bottom, and existing `codex-tui2` UI
snapshots continue to match.
Refine the draw layout so the bottom chat pane sits directly under the transcript when history is short instead of being pegged to the bottom of the terminal. When the transcript occupies fewer lines than the available viewport, the inline renderer now: - Computes the number of visible transcript rows based on the current scroll state and a bounded transcript height above the composer. - Lifts the chat area so it starts immediately below the rendered transcript, leaving at most a single blank spacer line, and clears only the region above the chat before drawing. - Fills any remaining rows below the chat with a clear so stale content from previous frames does not linger when the layout changes. This is implemented by having `render_transcript_cells` return the computed `chat_top` row (given the desired chat height) and updating the draw loop to use that when positioning the composer, while preserving the existing wrapped transcript rendering and scroll behavior introduced earlier. Snapshot behavior stays unchanged: when the user has not scrolled, the transcript remains pinned to the bottom, and existing `codex-tui2` UI snapshots continue to match.
Restore mouse wheel scrolling for full-screen transcript and diff overlays without relying on terminal-specific alternate scroll modes. `PagerView` now handles scroll wheel events by adjusting `scroll_offset` in fixed 3-line steps and scheduling a redraw via `FrameRequester`, and both transcript and static overlays route `TuiEvent::Mouse` through this path so wheel input works consistently across overlay types. On the terminal side, the TUI stays in application mouse mode throughout: custom alternate scroll commands are removed, `set_modes` only enables mouse capture, and suspend/alt-screen restore paths rely on `EnterAlternateScreen`/`LeaveAlternateScreen` without toggling alt scroll. This keeps scrolling behavior predictable across terminals while preserving existing snapshots.
Add a TranscriptSelection state (anchor/head) to track mouse-driven selection over the transcript viewport and render it inline. Mouse handling now ignores overlays, clamps events to the transcript area above the composer, and interprets left-click/drag/up to start, update, and clear selections while clearing any selection on wheel scroll. Selection rendering runs after transcript lines are drawn: it scans each visible row for non-space glyphs, skips the left gutter, and applies REVERSED styling only to the intersecting text range so padding isn’t highlighted.
Ensure transcript selections stop auto-following while streaming, then resume bottom-follow once the user scrolls back to the end. Selection start/drag now checks `chat_widget.is_task_running()` and, when streaming, converts `TranscriptScroll::ToBottom` into a fixed anchor via `lock_transcript_scroll_to_current_view` so new output doesn’t move the selection. The new helper derives a stable `(cell_index, line_in_cell)` anchor from the current top row, and scroll events can still move the viewport back to `ToBottom` when appropriate. `ChatWidget` exposes `is_task_running` so the viewport can make this decision without poking into bottom pane internals.
- clear the terminal in `PreparedResumeAction::RealignViewport` so resume/overlay exits do not leave old buffer content - keep job-control viewport realignment aligned with alt-screen cleanup expectations
- render transcript lines to ANSI with line-level style merging for scrollback - pad user rows to full width so prompt backgrounds are solid and readable - capture `session_lines` on exit for post-TUI transcript printing
- write rendered `session_lines` to stdout after leaving alt-screen so they land in scrollback - add a separating blank line before the final token-usage summary
- add clipboard_copy with an arboard-backed manager and platform fallback - render transcript rows into a buffer and extract selection text without gutter chrome - wire Ctrl+Y to copy the current screen-space selection respecting scroll
- track transcript view top + total lines while rendering the viewport - propagate scroll state into the bottom pane for footer rendering - show scroll shortcuts and current line position when scrolled
- map PageUp/PageDown to scroll the inline viewport by a screenful - jump to top or bottom with Home/End and clear any active selection - reuse viewport sizing to ignore scroll requests when the prompt fills the screen
- show a Ctrl+Y copy-selection hint in the footer when a transcript selection is active - add a footer snapshot for the selection hint alongside scroll position
- track wrapped user rows alongside line wrapping to keep selection styles on scroll - stop clearing selection state on mouse wheel and page-scroll actions - restyle selected rows when rendering the inline transcript viewport
- store selection endpoints as wrapped line/column indices instead of screen coords - map mouse positions through transcript_view_top so selection follows scroll and resize - apply/copy selection by rebuilding the visible wrapped viewport range - add a unit test asserting selection moves with scroll
- keep a one-row gap between short transcript history and the composer - anchor auto-follow only after a drag extends the selection while streaming - keep click-only selection from altering scroll behavior - document the drag-based scroll lock in the mouse handler
- collapse nested if-let checks when resolving transcript anchors - swap manual repeat/take loops for repeat_n in wrapped row tracking - use infallible conversions for scroll deltas and iterator helpers
e72a8f2 to
7c66bc5
Compare
|
@codex review |
There was a problem hiding this 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.
ℹ️ 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".
| let mut clamped_y = mouse_event.row; | ||
|
|
||
| if clamped_y < transcript_area.y || clamped_y >= transcript_area.bottom() { | ||
| clamped_y = transcript_area.y; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ignore mouse events outside transcript area
In handle_mouse_event, Y coordinates outside the transcript are clamped to transcript_area.y instead of being ignored, so a click in the composer or footer will still start a transcript selection anchored at the top line. With mouse capture enabled, this leads to accidental selections whenever the user clicks below the transcript. Consider returning early when mouse_event.row is outside the transcript bounds (or only clamping during drag) so non-transcript clicks don’t mutate selection state.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Deferring this to another PR
| for (row_index, line_index) in (visible_start..visible_end).enumerate() { | ||
| let row_area = Rect { | ||
| x: transcript_area.x, | ||
| y: transcript_area.y + row_index as u16, | ||
| width: transcript_area.width, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Copy selection should include offscreen lines
copy_transcript_selection renders and iterates only visible_start..visible_end, so any selected lines that are outside the current viewport are silently dropped. This shows up if the user selects text, then scrolls, or if the selection spans more lines than fit on screen—Ctrl+Y will copy only the visible slice or even nothing if the selection is fully offscreen. Consider rebuilding the buffer for the full transcript (or at least the selected line range) so copy matches the selection regardless of scroll position.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Deferring this to another PR
We've moved this over to a new tui2 crate from being directly on the tui crate.
To enable use --enable tui2 (or the equivalent in your config.toml). See https://developers.openai.com/codex/local-config#feature-flags
Note that this serves as a baseline for the changes that we're making to be applied rapidly. Tui2 may not track later changes in the main tui. It's experimental and may not be where we land on things.
Summary
This PR moves the Codex TUI off of “cooperating” with the terminal’s scrollback and onto a model
where the in‑memory transcript is the single source of truth. The TUI now owns scrolling, selection,
copy, and suspend/exit printing based on that transcript, and only writes to terminal scrollback in
append‑only fashion on suspend/exit. It also fixes streaming wrapping so streamed responses reflow
with the viewport, and introduces configuration to control whether we print history on suspend or
only on exit.
High‑level goals:
scrollback.
Core Design Changes
Transcript & viewport ownership
streaming segments).
to when we decide to print history.
User message styling
consistent whether you are in the TUI or scrolling back after exit/suspend.
Scrolling, Mouse, Selection, and Copy
Scrolling
transcript fills, matching the existing UX.
Selection
screen rows.
Y position.
“follow” mode.
Copy (
Ctrl+Y)ClipboardManager‑style) and use a cross‑platformclipboard crate under the hood.
Ctrl+Yis pressed and a non‑empty selection exists:status messages; they do not crash the TUI.
Streaming and Wrapping
Previous behavior
Previously, streamed markdown:
Line<'static>values were then wrapped again at display time.permanently split according to the width at the start of the stream.
New behavior
This PR implements the first step from
codex-rs/tui/streaming_wrapping_design.md:viewport width, just like non‑streaming messages.
commits slightly larger but keeps the behavior simple and predictable.
Streaming responses are still represented as a sequence of logical history entries (first line +
continuations) and integrate with the same scrolling, selection, and printing model.
Printing History on Suspend and Exit
High‑water mark and append‑only scrollback
printed_history_cells) on the transcript:transcript_cellsbeyondprinted_history_cells.printed_history_cellsto cover all cells we just printed.printed, which is acceptable as long as the logical content is present once.
Suspend (
Ctrl+Z)fg):This gives predictable behavior across terminals without trying to maintain scrollback live.
Exit
transcript and usage info are visually separated.
Configuration: Suspend Printing
This PR also adds configuration to control when we print history:
print_on_suspend = true– current behavior: print new history at each suspend and on exit.print_on_suspend = false– only print on exit.history.
This keeps the core viewport logic agnostic to preference, while letting users who care about
quiet scrollback opt out of suspend printing.
Tradeoffs
What we gain:
What we accept:
structure, not as a single retroactively reflowed paragraph.
For deeper rationale and diagrams, see
docs/tui_viewport_and_history.mdandcodex-rs/tui/streaming_wrapping_design.md.Still to Do Before This PR Is Ready
These are scoped to this PR (not long‑term future work):
Streaming wrapping polish
Suspend printing config
Bottom pane positioning
transcript fills, matching the current behavior across startup and resume.
Transcript mouse scrolling
Mouse selection vs streaming
the selected content.
resumes correctly.
Auto‑scroll during drag
of the transcript viewport to allow selecting beyond the current visible window.
Feature flag / rollout
so we can fall back to the old behavior if needed during early testing.
Before/after videos