Commit Graph

7 Commits

  • tui2: stop baking streaming wraps; reflow agent markdown (#8761)
    Background
    Streaming assistant prose in tui2 was being rendered with viewport-width
    wrapping during streaming, then stored in history cells as already split
    `Line`s. Those width-derived breaks became indistinguishable from hard
    newlines, so the transcript could not "un-split" on resize. This also
    degraded copy/paste, since soft wraps looked like hard breaks.
    
    What changed
    - Introduce width-agnostic `MarkdownLogicalLine` output in
    `tui2/src/markdown_render.rs`, preserving markdown wrap semantics:
    initial/subsequent indents, per-line style, and a preformatted flag.
    - Update the streaming collector (`tui2/src/markdown_stream.rs`) to emit
    logical lines (newline-gated) and remove any captured viewport width.
    - Update streaming orchestration (`tui2/src/streaming/*`) to queue and
    emit logical lines, producing `AgentMessageCell::new_logical(...)`.
    - Make `AgentMessageCell` store logical lines and wrap at render time in
    `HistoryCell::transcript_lines_with_joiners(width)`, emitting joiners so
    copy/paste can join soft-wrap continuations correctly.
    
    Overlay deferral
    When an overlay is active, defer *cells* (not rendered `Vec<Line>`) and
    render them at overlay close time. This avoids baking width-derived
    wraps based on a stale width.
    
    Tests + docs
    - Add resize/reflow regression tests + snapshots for streamed agent
    output.
    - Expand module/API docs for the new logical-line streaming pipeline and
    clarify joiner semantics.
    - Align scrollback-related docs/comments with current tui2 behavior
    (main draw loop does not flush queued "history lines" to the terminal).
    
    More details
    See `codex-rs/tui2/docs/streaming_wrapping_design.md` for the full
    problem statement and solution approach, and
    `codex-rs/tui2/docs/tui_viewport_and_history.md` for viewport vs printed
    output behavior.
  • feat(tui2): add multi-click transcript selection (#8471)
    Support multi-click transcript selection using transcript/viewport
    coordinates
    (wrapped visual line index + content column), not terminal buffer
    positions.
    
    Gestures:
    - double click: select word-ish token under cursor
    - triple click: select entire wrapped line
    - quad click: select paragraph (contiguous non-empty wrapped lines)
    - quint+ click: select the entire history cell (all wrapped lines
    belonging to a
      single `HistoryCell`, including blank lines inside the cell)
    
    Selection expansion rebuilds the wrapped transcript view from
    `HistoryCell::display_lines(width)` so boundaries match on-screen
    wrapping during
    scroll/resize/streaming reflow. Click grouping is resilient to minor
    drag jitter
    (some terminals emit tiny Drag events during clicks) and becomes more
    tolerant as
    the sequence progresses so quad/quint clicks are practical.
    
    Tests cover expansion (word/line/paragraph/cell), sequence resets
    (timing, motion,
    line changes, real drags), drag jitter, and behavior on spacer lines
    between
    history cells (paragraph/cell selection prefers the cell above).
  • feat(tui2): add copy selection shortcut + UI affordance (#8462)
    - Detect Ctrl+Shift+C vs VS Code Ctrl+Y and surface in footer hints
    - Render clickable “⧉ copy” pill near transcript selection (hidden while
    dragging)
    - Handle copy hotkey + click to copy selection
    - Document updated copy UX
    
    VSCode:
    <img width="1095" height="413" alt="image"
    src="https://github.com/user-attachments/assets/84be0c82-4762-4c3e-80a4-c751c078bdaa"
    />
    
    Ghosty:
    <img width="505" height="68" alt="image"
    src="https://github.com/user-attachments/assets/109cc1a1-f029-4f7e-a141-4c6ed2da7338"
    />
  • fix(tui2): copy transcript selection outside viewport (#8449)
    Copy now operates on the full logical selection range (anchor..head),
    not just the visible viewport, so selections that include offscreen
    lines copy the expected text.
    
    Selection extraction is factored into `transcript_selection` to make the
    logic easier to test and reason about. It reconstructs the wrapped
    visual transcript, renders each wrapped line into a 1-row offscreen
    Buffer, and reads the selected cells. This keeps clipboard text aligned
    with what is rendered (gutter, indentation, wrapping).
    
    Additional behavior:
    - Skip continuation cells for wide glyphs (e.g. CJK) so copied text does
    not include spurious spaces like "コ X".
    - Avoid copying right-margin padding spaces.
    
    Manual tested performed:
    - "tell me a story" a few times
    - scroll up, select text, scroll down, copy text
    - confirm copied text is what you expect
  • feat(tui2): tune scrolling inpu based on (#8357)
    ## TUI2: Normalize Mouse Scroll Input Across Terminals (Wheel +
    Trackpad)
    
    This changes TUI2 scrolling to a stream-based model that normalizes
    terminal scroll event density into consistent wheel behavior (default:
    ~3 transcript lines per physical wheel notch) while keeping trackpad
    input higher fidelity via fractional accumulation.
    
    Primary code: `codex-rs/tui2/src/tui/scrolling/mouse.rs`
    
    Doc of record (model + probe-derived data):
    `codex-rs/tui2/docs/scroll_input_model.md`
    
    ### Why
    
    Terminals encode both mouse wheels and trackpads as discrete scroll
    up/down events with direction but no magnitude, and they vary widely in
    how many raw events they emit per physical wheel notch (commonly 1, 3,
    or 9+). Timing alone doesn’t reliably distinguish wheel vs trackpad, so
    cadence-based heuristics are unstable across terminals/hardware.
    
    This PR treats scroll input as short *streams* separated by silence or
    direction flips, normalizes raw event density into tick-equivalents,
    coalesces redraws for dense streams, and exposes explicit config
    overrides.
    
    ### What Changed
    
    #### Scroll Model (TUI2)
    
    - Stream detection
      - Start a stream on the first scroll event.
      - End a stream on an idle gap (`STREAM_GAP_MS`) or a direction flip.
    - Normalization
    - Convert raw events into tick-equivalents using per-terminal
    `tui.scroll_events_per_tick`.
    - Wheel-like vs trackpad-like behavior
    - Wheel-like: fixed “classic” lines per wheel notch; flush immediately
    for responsiveness.
    - Trackpad-like: fractional accumulation + carry across stream
    boundaries; coalesce flushes to ~60Hz to avoid floods and reduce “stop
    lag / overshoot”.
    - Trackpad divisor is intentionally capped: `min(scroll_events_per_tick,
    3)` so terminals with dense wheel ticks (e.g. 9 events per notch) don’t
    make trackpads feel artificially slow.
    - Auto mode (default)
      - Start conservatively as trackpad-like (avoid overshoot).
    - Promote to wheel-like if the first tick-worth of events arrives
    quickly.
    - Fallback for 1-event-per-tick terminals (no tick-completion timing
    signal).
    
    #### Trackpad Acceleration
    
    Some terminals produce relatively low vertical event density for
    trackpad gestures, which makes large/faster swipes feel sluggish even
    when small motions feel correct. To address that, trackpad-like streams
    apply a bounded multiplier based on event count:
    
    - `multiplier = clamp(1 + abs(events) / scroll_trackpad_accel_events,
    1..scroll_trackpad_accel_max)`
    
    The multiplier is applied to the trackpad stream’s computed line delta
    (including carried fractional remainder). Defaults are conservative and
    bounded.
    
    #### Config Knobs (TUI2)
    
    All keys live under `[tui]`:
    
    - `scroll_wheel_lines`: lines per physical wheel notch (default: 3).
    - `scroll_events_per_tick`: raw vertical scroll events per physical
    wheel notch (terminal-specific default; fallback: 3).
    - Wheel-like per-event contribution: `scroll_wheel_lines /
    scroll_events_per_tick`.
    - `scroll_trackpad_lines`: baseline trackpad sensitivity (default: 1).
    - Trackpad-like per-event contribution: `scroll_trackpad_lines /
    min(scroll_events_per_tick, 3)`.
    - `scroll_trackpad_accel_events` / `scroll_trackpad_accel_max`: bounded
    trackpad acceleration (defaults: 30 / 3).
    - `scroll_mode = auto|wheel|trackpad`: force behavior or use the
    heuristic (default: `auto`).
    - `scroll_wheel_tick_detect_max_ms`: auto-mode promotion threshold (ms).
    - `scroll_wheel_like_max_duration_ms`: auto-mode fallback for
    1-event-per-tick terminals (ms).
    - `scroll_invert`: invert scroll direction (applies to wheel +
    trackpad).
    
    Config docs: `docs/config.md` and field docs in
    `codex-rs/core/src/config/types.rs`.
    
    #### App Integration
    
    - The app schedules follow-up ticks to close idle streams (via
    `ScrollUpdate::next_tick_in` and `schedule_frame_in`) and finalizes
    streams on draw ticks.
      - `codex-rs/tui2/src/app.rs`
    
    #### Docs
    
    - Single doc of record describing the model + preserved probe
    findings/spec:
      - `codex-rs/tui2/docs/scroll_input_model.md`
    
    #### Other (jj-only friendliness)
    
    - `codex-rs/tui2/src/diff_render.rs`: prefer stable cwd-relative paths
    when the file is under the cwd even if there’s no `.git`.
    
    ### Terminal Defaults
    
    Per-terminal defaults are derived from scroll-probe logs (see doc).
    Notable:
    
    - Ghostty currently defaults to `scroll_events_per_tick = 3` even though
    logs measured ~9 in one setup. This is a deliberate stopgap; if your
    Ghostty build emits ~9 events per wheel notch, set:
    
      ```toml
      [tui]
      scroll_events_per_tick = 9
      ```
    
    ### Testing
    
    - `just fmt`
    - `just fix -p codex-core --allow-no-vcs`
    - `cargo test -p codex-core --lib` (pass)
    - `cargo test -p codex-tui2` (scroll tests pass; remaining failures are
    known flaky VT100 color tests in `insert_history`)
    
    ### Review Focus
    
    - Stream finalization + frame scheduling in `codex-rs/tui2/src/app.rs`.
    - Auto-mode promotion thresholds and the 1-event-per-tick fallback
    behavior.
    - Trackpad divisor cap (`min(events_per_tick, 3)`) and acceleration
    defaults.
    - Ghostty default tradeoff (3 vs ~9) and whether we should change it.
  • docs: refine tui2 viewport roadmap (#8122)
    Update the tui2 viewport/history design doc with current status and a
    prioritized roadmap (scroll feel, selection/copy correctness, streaming
    wrap polish, terminal integration, and longer-term per-cell
    interactivity ideas).
  • WIP: Rework TUI viewport, history printing, and selection/copy (#7601)
    > large behavior change to how the TUI owns its viewport, history, and
    suspend behavior.
    > Core model is in place; a few items are still being polished before
    this is ready to merge.
    
    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:
    
    - Ensure history is complete, ordered, and never silently dropped.
    - Print each logical history cell at most once into scrollback, even
    with resizes and suspends.
    - Make scrolling, selection, and copy match the visible transcript, not
    the terminal’s notion of
      scrollback.
    - Keep suspend/alt‑screen behavior predictable across terminals.
    
    ---
    
    ## Core Design Changes
    
    ### Transcript & viewport ownership
    
    - Treat the transcript as a list of **cells** (user prompts, agent
    messages, system/info rows,
      streaming segments).
    - On each frame:
    - Compute a **transcript region** as “full terminal frame minus the
    bottom input area”.
    - Flatten all cells into visual lines plus metadata (which cell + which
    line within that cell).
    - Use scroll state to choose which visual line is at the top of the
    region.
      - Clear that region and draw just the visible slice of lines.
    - The terminal’s scrollback is no longer part of the live layout
    algorithm; it is only ever written
      to when we decide to print history.
    
    ### User message styling
    
    - User prompts now render as clear blocks with:
      - A blank padding line above and below.
    - A full‑width background for every line in the block (including the
    prompt line itself).
    - The same block styling is used when we print history into scrollback,
    so the transcript looks
    consistent whether you are in the TUI or scrolling back after
    exit/suspend.
    
    ---
    
    ## Scrolling, Mouse, Selection, and Copy
    
    ### Scrolling
    
    - Scrolling is defined in terms of the flattened transcript lines:
      - Mouse wheel scrolls up/down by fixed line increments.
      - PgUp/PgDn/Home/End operate on the same scroll model.
    - The footer shows:
      - Whether you are “following live output” vs “scrolled up”.
      - Current scroll position (line / total).
    - When there is no history yet, the bottom pane is **pegged high** and
    gradually moves down as the
      transcript fills, matching the existing UX.
    
    ### Selection
    
    - Click‑and‑drag defines a **linear selection** over transcript
    line/column coordinates, not raw
      screen rows.
    - Selection is **content‑anchored**:
    - When you scroll, the selection moves with the underlying lines instead
    of sticking to a fixed
        Y position.
    - This holds both when scrolling manually and when new content streams
    in, as long as you are in
        “follow” mode.
    - The selection only covers the “transcript text” area:
      - Left gutter/prefix (bullets, markers) is intentionally excluded.
    - This keeps copy/paste cleaner and avoids including structural margin
    characters.
    
    ### Copy (`Ctrl+Y`)
    
    - Introduce a small clipboard abstraction (`ClipboardManager`‑style) and
    use a cross‑platform
      clipboard crate under the hood.
    - When `Ctrl+Y` is pressed and a non‑empty selection exists:
    - Re‑render the transcript region off‑screen using the same wrapping as
    the visible viewport.
    - Walk the selected line/column range over that buffer to reconstruct
    the exact text:
        - Includes spaces between words.
        - Preserves empty lines within the selection.
      - Send the resulting text to the system clipboard.
    - Show a short status message in the footer indicating success/failure.
    - Copy is **best‑effort**:
    - Clipboard failures (headless environment, sandbox, remote sessions)
    are handled gracefully via
        status messages; they do not crash the TUI.
    - Copy does *not* insert a new history entry; it only affects the status
    bar.
    
    ---
    
    ## Streaming and Wrapping
    
    ### Previous behavior
    
    Previously, streamed markdown:
    
    - Was wrapped at a fixed width **at commit time** inside the streaming
    collector.
    - Those wrapped `Line<'static>` values were then wrapped again at
    display time.
    - As a result, streamed paragraphs could not “un‑wrap” when the terminal
    width increased; they were
      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`:
    
    - Streaming collector is constructed **without** a fixed width for
    wrapping.
      - It still:
        - Buffers the full markdown source for the current stream.
        - Commits only at newline boundaries.
        - Emits logical lines as new content becomes available.
    - Agent message cells now wrap streamed content only at **display
    time**, based on the current
      viewport width, just like non‑streaming messages.
    - Consequences:
      - Streamed responses reflow correctly when the terminal is resized.
    - Animation steps are per logical line instead of per “pre‑wrapped”
    visual line; this makes some
    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
    
    - Introduce a **cell‑based high‑water mark** (`printed_history_cells`)
    on the transcript:
    - Represents “how many cells at the front of the transcript have already
    been printed”.
      - Completely independent of wrapped line counts or terminal geometry.
    - Whenever we print history (suspend or exit):
    - Take the suffix of `transcript_cells` beyond `printed_history_cells`.
      - Render just that suffix into styled lines at the **current** width.
      - Write those lines to stdout.
      - Advance `printed_history_cells` to cover all cells we just printed.
    - Older cells are never re‑rendered for scrollback. They stay in
    whatever wrapping they had when
    printed, which is acceptable as long as the logical content is present
    once.
    
    ### Suspend (`Ctrl+Z`)
    
    - On suspend:
      - Leave alt screen if active and restore normal terminal modes.
    - Render the not‑yet‑printed suffix of the transcript and append it to
    normal scrollback.
      - Advance the high‑water mark.
      - Suspend the process.
    - On resume (`fg`):
      - Re‑enter the TUI mode (alt screen + input modes).
    - Clear the viewport region and fully redraw from in‑memory transcript
    and state.
    
    This gives predictable behavior across terminals without trying to
    maintain scrollback live.
    
    ### Exit
    
    - On exit:
      - Render any remaining unprinted cells once and write them to stdout.
    - Add an extra blank line after the final Codex history cell before
    printing token usage, so the
        transcript and usage info are visually separated.
    - If you never suspended, exit prints the entire transcript exactly
    once.
    - If you suspended one or more times, exit prints only the cells
    appended after the last suspend.
    
    ---
    
    ## Configuration: Suspend Printing
    
    This PR also adds configuration to control **when** we print history:
    
    - New TUI config option to gate printing on suspend:
      - At minimum:
    - `print_on_suspend = true` – current behavior: print new history at
    each suspend *and* on exit.
        - `print_on_suspend = false` – only print on exit.
    - Default is tuned to preserve current behavior, but this can be
    revisited based on feedback.
    - The config is respected in the suspend path:
    - If disabled, suspend only restores terminal modes and stops rendering
    but does not print new
        history.
      - Exit still prints the full not‑yet‑printed suffix once.
    
    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:
    
    - A single authoritative history model (the in‑memory transcript).
    - Deterministic viewport rendering independent of terminal quirks.
    - Suspend/exit flows that:
      - Print each logical history cell exactly once.
      - Work across resizes and different terminals.
      - Interact cleanly with alt screen and raw‑mode toggling.
    - Consistent, content‑anchored scrolling, selection, and copy.
    - Streaming messages that reflow correctly with the viewport width.
    
    What we accept:
    
    - Scrollback may contain older cells wrapped differently than newer
    ones.
    - Streaming responses appear in scrollback as a sequence of blocks
    corresponding to their streaming
      structure, not as a single retroactively reflowed paragraph.
    - We do not attempt to rewrite or reflow already‑printed scrollback.
    
    For deeper rationale and diagrams, see
    `docs/tui_viewport_and_history.md` and
    `codex-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**
      - Double‑check all streaming paths use display‑time wrapping only.
      - Ensure tests cover resizing after streaming has started.
    
    - [ ] **Suspend printing config**
    - Finalize config shape and default (keep existing behavior vs opt‑out).
    - Wire config through TUI startup and document it in the appropriate
    config docs.
    
    - [x] **Bottom pane positioning**
    - Ensure the bottom pane is pegged high when there’s no history and
    smoothly moves down as the
    transcript fills, matching the current behavior across startup and
    resume.
    
    - [x] **Transcript mouse scrolling**
    - Re‑enable wheel‑based transcript scrolling on top of the new scroll
    model.
    - Make sure mouse scroll does not get confused with “alternate scroll”
    modes from terminals.
    
    - [x] **Mouse selection vs streaming**
    - When selection is active, stop auto‑scrolling on streaming so the
    selection remains stable on
        the selected content.
    - Ensure that when streaming continues after selection is cleared,
    “follow latest output” mode
        resumes correctly.
    
    - [ ] **Auto‑scroll during drag**
    - While the user is dragging a selection, auto‑scroll when the cursor is
    at/near the top or bottom
    of the transcript viewport to allow selecting beyond the current visible
    window.
    
    - [ ] **Feature flag / rollout**
    - Investigate gating the new viewport/history behavior behind a feature
    flag for initial rollout,
    so we can fall back to the old behavior if needed during early testing.
    
    - [ ] **Before/after videos**
      - Capture short clips showing:
        - Scrolling (mouse + keys).
        - Selection and copy.
        - Streaming behavior under resize.
        - Suspend/resume and exit printing.
      - Use these to validate UX and share context in the PR discussion.