diff --git a/docs/config.md b/docs/config.md index e419a8522a31..950a631fc5bf 100644 --- a/docs/config.md +++ b/docs/config.md @@ -5,119 +5,3 @@ For basic configuration instructions, see [this documentation](https://developer For advanced configuration instructions, see [this documentation](https://developers.openai.com/codex/config-advanced). For a full configuration reference, see [this documentation](https://developers.openai.com/codex/config-reference). - -## Connecting to MCP servers - -Codex can connect to MCP servers configured in `~/.codex/config.toml`. See the configuration reference for the latest MCP server options: - -- https://developers.openai.com/codex/config-reference - -MCP tools default to serialized calls. To mark every tool exposed by one server -as eligible for parallel tool calls, set `supports_parallel_tool_calls` on that -server: - -```toml -[mcp_servers.docs] -command = "docs-server" -supports_parallel_tool_calls = true -``` - -Only enable parallel calls for MCP servers whose tools are safe to run at the -same time. If tools read and write shared state, files, databases, or external -resources, review those read/write race conditions before enabling this setting. - -## MCP tool approvals - -Codex stores approval defaults and per-tool overrides for custom MCP servers -under `mcp_servers` in `~/.codex/config.toml`. Set -`default_tools_approval_mode` on the server to apply a default to every tool, -and use per-tool `approval_mode` entries for exceptions: - -```toml -[mcp_servers.docs] -command = "docs-server" -default_tools_approval_mode = "approve" - -[mcp_servers.docs.tools.search] -approval_mode = "prompt" -``` - -## Apps (Connectors) - -Use `$` in the composer to insert a ChatGPT connector; the popover lists accessible -apps. The `/apps` command lists available and installed apps. Connected apps appear first -and are labeled as connected; others are marked as can be installed. - -Codex stores "never show again" choices for tool suggestions in `config.toml`: - -```toml -[tool_suggest] -disabled_tools = [ - { type = "plugin", id = "slack@openai-curated" }, - { type = "connector", id = "connector_google_calendar" }, -] -``` - -## Notify - -`notify` is deprecated and will be removed in a future release. Existing configurations still work for compatibility, but new automation should use lifecycle hooks instead. - -Codex can run a legacy notification command when the agent finishes a turn. See the configuration reference for the latest notification settings: - -- https://developers.openai.com/codex/config-reference - -When Codex knows which client started the turn, the legacy notify JSON payload also includes a top-level `client` field. The TUI reports `codex-tui`, and the app server reports the `clientInfo.name` value from `initialize`. - -## JSON Schema - -The generated JSON Schema for `config.toml` lives at `codex-rs/core/config.schema.json`. - -## SQLite State DB - -Codex stores the SQLite-backed state DB under `sqlite_home` (config key) or the -`CODEX_SQLITE_HOME` environment variable. When unset, WorkspaceWrite sandbox -sessions default to a temp directory; other modes default to `CODEX_HOME`. - -## Custom CA Certificates - -Codex can trust a custom root CA bundle for outbound HTTPS and secure websocket -connections when enterprise proxies or gateways intercept TLS. This applies to -login flows and to Codex's other external connections, including Codex -components that build reqwest clients or secure websocket clients through the -shared `codex-client` CA-loading path and remote MCP connections that use it. - -Set `CODEX_CA_CERTIFICATE` to the path of a PEM file containing one or more -certificate blocks to use a Codex-specific CA bundle. If -`CODEX_CA_CERTIFICATE` is unset, Codex falls back to `SSL_CERT_FILE`. If -neither variable is set, Codex uses the system root certificates. - -`CODEX_CA_CERTIFICATE` takes precedence over `SSL_CERT_FILE`. Empty values are -treated as unset. - -The PEM file may contain multiple certificates. Codex also tolerates OpenSSL -`TRUSTED CERTIFICATE` labels and ignores well-formed `X509 CRL` sections in the -same bundle. If the file is empty, unreadable, or malformed, the affected Codex -HTTP or secure websocket connection reports a user-facing error that points -back to these environment variables. - -## Notices - -Codex stores "do not show again" flags for some UI prompts under the `[notice]` table. - -## Plan mode defaults - -`plan_mode_reasoning_effort` lets you set a Plan-mode-specific default reasoning -effort override. When unset, Plan mode uses the built-in Plan preset default -(currently `medium`). When explicitly set (including `none`), it overrides the -Plan preset. The string value `none` means "no reasoning" (an explicit Plan -override), not "inherit the global default". There is currently no separate -config value for "follow the global default in Plan mode". - -## Realtime start instructions - -`experimental_realtime_start_instructions` lets you replace the built-in -developer message Codex inserts when realtime becomes active. It only affects -the realtime start message in prompt history and does not change websocket -backend prompt settings or the realtime end/inactive message. - -Ctrl+C/Ctrl+D quitting uses a ~1 second double-press hint (`ctrl + c again to quit`). diff --git a/docs/exit-confirmation-prompt-design.md b/docs/exit-confirmation-prompt-design.md deleted file mode 100644 index 814b73730bb9..000000000000 --- a/docs/exit-confirmation-prompt-design.md +++ /dev/null @@ -1,96 +0,0 @@ -# Exit and shutdown flow (tui) - -This document describes how exit, shutdown, and interruption work in the Rust TUI (`codex-rs/tui`). -It is intended for Codex developers and Codex itself when reasoning about future exit/shutdown -changes. - -This doc replaces earlier separate history and design notes. High-level history is summarized -below; full details are captured in PR #8936. - -## Terms - -- **Exit**: end the UI event loop and terminate the process. -- **Shutdown**: request a graceful agent/core shutdown (`Op::Shutdown`) and wait for - `ShutdownComplete` so cleanup can run. -- **Interrupt**: cancel a running operation (`Op::Interrupt`). - -## Event model (AppEvent) - -Exit is coordinated via a single event with explicit modes: - -- `AppEvent::Exit(ExitMode::ShutdownFirst)` - - Prefer this for user-initiated quits so cleanup runs. -- `AppEvent::Exit(ExitMode::Immediate)` - - Escape hatch for immediate exit. This bypasses shutdown and can drop - in-flight work (e.g., tasks, rollout flush, child process cleanup). - -`App` is the coordinator: it submits `Op::Shutdown` and it exits the UI loop only when -`ExitMode::Immediate` arrives (typically after `ShutdownComplete`). - -## User-triggered quit flows - -### Ctrl+C - -Priority order in the UI layer: - -1. Active modal/view gets the first chance to consume (`BottomPane::on_ctrl_c`). - - If the modal handles it, the quit flow stops. - - When a modal/popup handles Ctrl+C, the quit shortcut is cleared so dismissing a modal cannot - accidentally prime a subsequent Ctrl+C to quit. -2. If the user has already armed Ctrl+C and the 1 second window has not expired, the second Ctrl+C - triggers shutdown-first quit immediately. -3. Otherwise, `ChatWidget` arms Ctrl+C and shows the quit hint (`ctrl + c again to quit`) for - 1 second. -4. If cancellable work is active (streaming/tools/review), `ChatWidget` submits `Op::Interrupt`. - -### Ctrl+D - -- Only participates in quit when the composer is empty **and** no modal is active. - - On first press, show the quit hint (same as Ctrl+C) and start the 1 second timer. - - If pressed again while the hint is visible, request shutdown-first quit. -- With any modal/popup open, key events are routed to the view and Ctrl+D does not attempt to - quit. - -### Slash commands - -- `/quit`, `/exit`, `/logout` request shutdown-first quit **without** a prompt, - because slash commands are harder to trigger accidentally and imply clear intent to quit. - -### /new - -- Uses shutdown without exit (suppresses `ShutdownComplete`) so the app can - start a fresh session without terminating. - -## Shutdown completion and suppression - -`ShutdownComplete` is the signal that core cleanup has finished. The UI treats it as the boundary -for exit: - -- `ChatWidget` requests `Exit(Immediate)` on `ShutdownComplete`. -- `App` can suppress a single `ShutdownComplete` when shutdown is used as a - cleanup step (e.g., `/new`). - -## Edge cases and invariants - -- **Review mode** counts as cancellable work. Ctrl+C should interrupt review, not - quit. -- **Modal open** means Ctrl+C/Ctrl+D should not quit unless the modal explicitly - declines to handle Ctrl+C. -- **Immediate exit** is not a normal user path; it is a fallback for shutdown - completion or an emergency exit. Use it sparingly because it skips cleanup. - -## Testing expectations - -At a minimum, we want coverage for: - -- Ctrl+C while working interrupts, does not quit. -- Ctrl+C while idle and empty shows quit hint, then shutdown-first quit on second press. -- Ctrl+D with modal open does not quit. -- `/quit` / `/exit` / `/logout` quit without prompt, but still shutdown-first. - - Ctrl+D while idle and empty shows quit hint, then shutdown-first quit on second press. - -## History (high level) - -Codex has historically mixed "exit immediately" and "shutdown-first" across quit gestures, largely -due to incremental changes and regressions in state tracking. This doc reflects the current -unified, shutdown-first approach. See PR #8936 for the detailed history and rationale. diff --git a/docs/tui-alternate-screen.md b/docs/tui-alternate-screen.md deleted file mode 100644 index 2fe141a2f230..000000000000 --- a/docs/tui-alternate-screen.md +++ /dev/null @@ -1,130 +0,0 @@ -# TUI Alternate Screen and Terminal Multiplexers - -## Overview - -This document explains the design decision behind Codex's alternate screen handling, particularly in terminal multiplexers like Zellij. This addresses a fundamental conflict between fullscreen TUI behavior and terminal scrollback history preservation. - -## The Problem - -### Fullscreen TUI Benefits - -Codex's TUI uses the terminal's **alternate screen buffer** to provide a clean fullscreen experience. This approach: - -- Uses the entire viewport without polluting the terminal's scrollback history -- Provides a dedicated environment for the chat interface -- Mirrors the behavior of other terminal applications (vim, tmux, etc.) - -### The Zellij Conflict - -Terminal multiplexers like **Zellij** strictly follow the xterm specification, which defines that alternate screen buffers should **not** have scrollback. This is intentional design, not a bug: - -- **Zellij PR:** https://github.com/zellij-org/zellij/pull/1032 -- **Rationale:** The xterm spec explicitly states that alternate screen mode disallows scrollback -- **Configurability:** This is not configurable in Zellij—there is no option to enable scrollback in alternate screen mode - -When using Codex's TUI in Zellij, users cannot scroll back through the conversation history because: - -1. The TUI runs in alternate screen mode (fullscreen) -2. Zellij disables scrollback in alternate screen buffers (per xterm spec) -3. The entire conversation becomes inaccessible via normal terminal scrolling - -## The Solution - -Codex implements a **pragmatic workaround** with three modes, controlled by `tui.alternate_screen` in `config.toml`: - -### 1. `auto` (default) - -- **Behavior:** Automatically detect the terminal multiplexer -- **In Zellij:** Disable alternate screen mode (inline mode, preserves scrollback) -- **Elsewhere:** Enable alternate screen mode (fullscreen experience) -- **Rationale:** Provides the best UX in each environment - -### 2. `always` - -- **Behavior:** Always use alternate screen mode (original behavior) -- **Use case:** Users who prefer fullscreen and don't use Zellij, or who have found a workaround - -### 3. `never` - -- **Behavior:** Never use alternate screen mode (inline mode) -- **Use case:** Users who always want scrollback history preserved -- **Trade-off:** Pollutes the terminal scrollback with TUI output - -## Runtime Override - -The `--no-alt-screen` CLI flag can override the config setting at runtime: - -```bash -codex --no-alt-screen -``` - -This runs the TUI in inline mode regardless of the configuration, useful for: - -- One-off sessions where scrollback is critical -- Debugging terminal-related issues -- Testing alternate screen behavior - -## Implementation Details - -### Auto-Detection - -The `auto` mode detects Zellij by checking the `ZELLIJ` environment variable: - -```rust -let terminal_info = codex_core::terminal::terminal_info(); -!matches!(terminal_info.multiplexer, Some(Multiplexer::Zellij { .. })) -``` - -This detection happens in the helper function `determine_alt_screen_mode()` in `codex-rs/tui/src/lib.rs`. - -### Configuration Schema - -The `AltScreenMode` enum is defined in `codex-rs/protocol/src/config_types.rs` and serializes to lowercase TOML: - -```toml -[tui] -# Options: auto, always, never -alternate_screen = "auto" -``` - -### Why Not Just Disable Alternate Screen in Zellij Permanently? - -We use `auto` detection instead of always disabling in Zellij because: - -1. Many Zellij users don't care about scrollback and prefer the fullscreen experience -2. Some users may use tmux inside Zellij, creating a chain of multiplexers -3. Provides user choice without requiring manual configuration - -## Related Issues and References - -- **Original Issue:** [GitHub #2558](https://github.com/openai/codex/issues/2558) - "No scrollback in Zellij" -- **Implementation PR:** [GitHub #8555](https://github.com/openai/codex/pull/8555) -- **Zellij PR:** https://github.com/zellij-org/zellij/pull/1032 (why scrollback is disabled) -- **xterm Spec:** Alternate screen buffers should not have scrollback - -## Future Considerations - -### Alternative Approaches Considered - -1. **Implement custom scrollback in TUI:** Would require significant architectural changes to buffer and render all historical output -2. **Request Zellij to add a config option:** Not viable—Zellij maintainers explicitly chose this behavior to follow the spec -3. **Disable alternate screen unconditionally:** Would degrade UX for non-Zellij users - -### Transcript Pager - -Codex's transcript pager (opened with Ctrl+T) provides an alternative way to review conversation history, even in fullscreen mode. However, this is not as seamless as natural scrollback. - -## For Developers - -When modifying TUI code, remember: - -- The `determine_alt_screen_mode()` function encapsulates all the logic -- Configuration is in `config.tui_alternate_screen` -- CLI flag is in `cli.no_alt_screen` -- The behavior is applied via `tui.set_alt_screen_enabled()` - -If you encounter issues with terminal state after running Codex, you can restore your terminal with: - -```bash -reset -``` diff --git a/docs/tui-chat-composer.md b/docs/tui-chat-composer.md deleted file mode 100644 index 0ad5c693b3cf..000000000000 --- a/docs/tui-chat-composer.md +++ /dev/null @@ -1,348 +0,0 @@ -# Chat Composer state machine (TUI) - -This note documents the `ChatComposer` input state machine and the paste-related behavior added -for Windows terminals. - -Primary implementations: - -- `codex-rs/tui/src/bottom_pane/chat_composer.rs` - -Paste-burst detector: - -- `codex-rs/tui/src/bottom_pane/paste_burst.rs` - -## What problem is being solved? - -On some terminals (notably on Windows via `crossterm`), _bracketed paste_ is not reliably surfaced -as a single paste event. Instead, pasting multi-line content can show up as a rapid sequence of -key events: - -- `KeyCode::Char(..)` for text -- `KeyCode::Enter` for newlines - -If the composer treats those events as “normal typing”, it can: - -- accidentally trigger UI toggles (e.g. `?`) while the paste is still streaming, -- submit the message mid-paste when an `Enter` arrives, -- render a typed prefix, then “reclassify” it as paste once enough chars arrive (flicker). - -The solution is to detect paste-like _bursts_ and buffer them into a single explicit -`handle_paste(String)` call. - -## High-level state machines - -`ChatComposer` effectively combines two small state machines: - -1. **UI mode**: which popup (if any) is active. - - `ActivePopup::None | Command | File | Skill` -2. **Paste burst**: transient detection state for non-bracketed paste. - - implemented by `PasteBurst` - -### Key event routing - -`ChatComposer::handle_key_event` dispatches based on `active_popup`: - -- If a popup is visible, a popup-specific handler processes the key first (navigation, selection, - completion). -- Otherwise, `handle_key_event_without_popup` handles higher-level semantics (Enter submit, - history navigation, etc). -- After handling the key, `sync_popups()` runs so popup visibility/filters stay consistent with the - latest text + cursor. -- When a slash command name is completed and the user types a space, the `/command` token is - promoted into a text element so it renders distinctly and edits atomically. - -### History navigation (↑/↓) - -Up/Down recall is handled by `ChatComposerHistory` and merges two sources: - -- **Persistent history** (cross-session, fetched from `~/.codex/history.jsonl`): text-only. It - does **not** carry text element ranges or image attachments, so recalling one of these entries - only restores the text. -- **Local history** (current session): stores the full submission payload, including text - elements, local image paths, and remote image URLs. Recalling a local entry rehydrates - placeholders and attachments. - -This distinction keeps the on-disk history backward compatible and avoids persisting attachments, -while still providing a richer recall experience for in-session edits. - -### Reverse history search (Ctrl+R) - -Ctrl+R enters an incremental reverse search mode without immediately previewing the latest history entry. While search is active, the footer line becomes the editable query field and the composer body is only a preview of the currently matched entry. `Enter` accepts the preview as a normal editable draft, and `Esc` or Ctrl+C restores the exact draft that existed before search started. - -The composer owns the search session because it controls draft snapshots, footer rendering, cursor placement, and preview highlighting. `ChatComposerHistory` owns traversal: it scans persistent and local entries in one offset space, skips duplicate prompt text within a search session, keeps boundary hits on the current match, and resumes scans after asynchronous persistent history responses. - -The search query and composer text intentionally remain separate. A no-match result restores the original draft while leaving the footer query open for more typing, and accepting a match clears the search session so highlight styling disappears from the now-editable composer text. - -## Config gating for reuse - -`ChatComposer` now supports feature gating via `ChatComposerConfig` -(`codex-rs/tui/src/bottom_pane/chat_composer.rs`). The default config preserves current chat -behavior. - -Flags: - -- `popups_enabled` -- `slash_commands_enabled` -- `image_paste_enabled` - -Key effects when disabled: - -- When `popups_enabled` is `false`, `sync_popups()` forces `ActivePopup::None`. -- When `slash_commands_enabled` is `false`, the composer does not treat `/...` input as commands. -- When `slash_commands_enabled` is `false`, slash-context paste-burst exceptions are disabled. -- When `image_paste_enabled` is `false`, file-path paste image attachment is skipped. -- `ChatWidget` may toggle `image_paste_enabled` at runtime based on the selected model's - `input_modalities`; attach and submit paths also re-check support and emit a warning instead of - dropping the draft. - -Built-in slash command availability is centralized in -`codex-rs/tui/src/bottom_pane/slash_commands.rs` and reused by both the composer and the command -popup so gating stays in sync. - -## Submission flow (Enter/Tab) - -There are multiple submission paths, but they share the same core rules: - -When steer mode is enabled, `Tab` requests queuing if a task is already running; otherwise it -submits immediately. `Enter` always submits immediately in this mode. `Tab` does not submit when -the input starts with `!` (shell command). - -### Normal submit/queue path - -`handle_submission` calls `prepare_submission_text` for both submit and queue. That method: - -1. Expands any pending paste placeholders so element ranges align with the final text. -2. Trims whitespace and rebases element ranges to the trimmed buffer. -3. Prunes attachments so only placeholders that survive trimming are sent. -4. Clears pending pastes on success and suppresses submission if the final text is empty and there - are no attachments. - -The same preparation path is reused for slash commands with arguments (for example `/plan` and -`/review`) so pasted content and text elements are preserved when extracting args. - -The composer also treats the textarea kill buffer as separate editing state from the visible draft. -After submit or slash-command dispatch clears the textarea, the most recent `Ctrl+K` payload is -still available for `Ctrl+Y`. This supports flows where a user kills part of a draft, runs a -composer action such as changing reasoning level, and then yanks that text back into the cleared -draft. - -## Remote image rows (selection/deletion flow) - -Remote image URLs are shown as `[Image #N]` rows above the textarea, inside the same composer box. -They are attachment rows, not editable textarea content. - -- TUI can remove these rows, but cannot type before/between them. -- Press `Up` at textarea cursor position `0` to select the last remote image row. -- While selected, `Up`/`Down` moves selection across remote image rows. -- Pressing `Down` on the last row exits remote-row selection and returns to textarea editing. -- `Delete` or `Backspace` removes the selected remote image row. - -Image numbering is unified: - -- Remote image rows always occupy `[Image #1]..[Image #M]`. -- Local attached image placeholders start after that offset (`[Image #M+1]..`). -- Removing remote rows relabels local placeholders so numbering stays contiguous. - -## History navigation (Up/Down) and backtrack prefill - -`ChatComposerHistory` merges two kinds of history: - -- **Persistent history** (cross-session, fetched from core on demand): text-only. -- **Local history** (this UI session): full draft state. - -Local history entries capture: - -- raw text (including placeholders), -- `TextElement` ranges for placeholders, -- local image paths, -- remote image URLs, -- pending large-paste payloads (for drafts). - -Persistent history entries only restore text. They intentionally do **not** rehydrate attachments -or pending paste payloads. - -For non-empty drafts, Up/Down navigation is only treated as history recall when the current text -matches the last recalled history entry and the cursor is at a boundary (start or end of the -line). This keeps multiline cursor movement intact while preserving shell-like history traversal. - -### Draft recovery (Ctrl+C) - -Ctrl+C clears the composer but stashes the full draft state (text elements, local image paths, -remote image URLs, and pending paste payloads) into local history. Pressing Up immediately restores -that draft, including image placeholders and large-paste placeholders with their payloads. - -### Submitted message recall - -After a successful submission, the local history entry stores the submitted text, element ranges, -local image paths, and remote image URLs. Pending paste payloads are cleared during submission, so -large-paste placeholders are expanded into their full text before being recorded. This means: - -- Up/Down recall of a submitted message restores remote image rows plus local image placeholders. -- Recalled entries place the cursor at end-of-line to match typical shell history editing. -- Large-paste placeholders are not expected in recalled submitted history; the text is the - expanded paste content. - -### Backtrack prefill - -Backtrack selections read `UserHistoryCell` data from the transcript. The composer prefill now -reuses the selected message’s text elements, local image paths, and remote image URLs, so image -placeholders and attachments rehydrate when rolling back to a prior user message. - -### External editor edits - -When the composer content is replaced from an external editor, the composer rebuilds text elements -and keeps only attachments whose placeholders still appear in the new text. Image placeholders are -then normalized to `[Image #M]..[Image #N]`, where `M` starts after the number of remote image -rows, to keep attachment mapping consistent after edits. - -## Paste burst: concepts and assumptions - -The burst detector is intentionally conservative: it only processes “plain” character input -(no Ctrl/Alt modifiers). Everything else flushes and/or clears the burst window so shortcuts keep -their normal meaning. - -### Conceptual `PasteBurst` states - -- **Idle**: no buffer, no pending char. -- **Pending first char** (ASCII only): hold one fast character very briefly to avoid rendering it - and then immediately removing it if the stream turns out to be a paste. -- **Active buffer**: once a burst is classified as paste-like, accumulate the content into a - `String` buffer. -- **Enter suppression window**: keep treating `Enter` as “newline” briefly after burst activity so - multiline pastes remain grouped even if there are tiny gaps. - -### ASCII vs non-ASCII (IME) input - -Non-ASCII characters frequently come from IMEs and can legitimately arrive in quick bursts. Holding -the first character in that case can feel like dropped input. - -The composer therefore distinguishes: - -- **ASCII path**: allow holding the first fast char (`PasteBurst::on_plain_char`). -- **non-ASCII path**: never hold the first char (`PasteBurst::on_plain_char_no_hold`), but still - allow burst detection. When a burst is detected on this path, the already-inserted prefix may be - retroactively removed from the textarea and moved into the paste buffer. - -To avoid misclassifying IME bursts as paste, the non-ASCII retro-capture path runs an additional -heuristic (`PasteBurst::decide_begin_buffer`) to determine whether the retro-grabbed prefix “looks -pastey” (e.g. contains whitespace or is long). - -### Disabling burst detection - -`ChatComposer` supports `disable_paste_burst` as an escape hatch. - -When enabled: - -- The burst detector is bypassed for new input (no flicker suppression hold and no burst buffering - decisions for incoming characters). -- The key stream is treated as normal typing (including normal slash command behavior). -- Enabling the flag flushes any held/buffered burst text through the normal paste path - (`ChatComposer::handle_paste`) and then clears the burst timing and Enter-suppression windows so - transient burst state cannot leak into subsequent input. - -### Enter handling - -When paste-burst buffering is active, Enter is treated as “append `\n` to the burst” rather than -“submit the message”. This prevents mid-paste submission for multiline pastes that are emitted as -`Enter` key events. - -The composer also disables burst-based Enter suppression inside slash-command context (popup open -or the first line begins with `/`) so command dispatch is predictable. - -## PasteBurst: event-level behavior (cheat sheet) - -This section spells out how `ChatComposer` interprets the `PasteBurst` decisions. It’s intended to -make the state transitions reviewable without having to “run the code in your head”. - -### Plain ASCII `KeyCode::Char(c)` (no Ctrl/Alt modifiers) - -`ChatComposer::handle_input_basic` calls `PasteBurst::on_plain_char(c, now)` and switches on the -returned `CharDecision`: - -- `RetainFirstChar`: do **not** insert `c` into the textarea yet. A UI tick later may flush it as a - normal typed char via `PasteBurst::flush_if_due`. -- `BeginBufferFromPending`: the first ASCII char is already held/buffered; append `c` via - `PasteBurst::append_char_to_buffer`. -- `BeginBuffer { retro_chars }`: attempt a retro-capture of the already-inserted prefix: - - call `PasteBurst::decide_begin_buffer(now, before_cursor, retro_chars)`; - - if it returns `Some(grab)`, delete `grab.start_byte..cursor` from the textarea and then append - `c` to the buffer; - - if it returns `None`, fall back to normal insertion. -- `BufferAppend`: append `c` to the active buffer. - -### Plain non-ASCII `KeyCode::Char(c)` (no Ctrl/Alt modifiers) - -`ChatComposer::handle_non_ascii_char` uses a slightly different flow: - -- It first flushes any pending transient ASCII state with `PasteBurst::flush_before_modified_input` - (which includes a single held ASCII char). -- If a burst is already active, `PasteBurst::try_append_char_if_active(c, now)` appends `c` directly. -- Otherwise it calls `PasteBurst::on_plain_char_no_hold(now)`: - - `BufferAppend`: append `c` to the active buffer. - - `BeginBuffer { retro_chars }`: run `decide_begin_buffer(..)` and, if it starts buffering, delete - the retro-grabbed prefix from the textarea and append `c`. - - `None`: insert `c` into the textarea normally. - -The extra `decide_begin_buffer` heuristic on this path is intentional: IME input can arrive as -quick bursts, so the code only retro-grabs if the prefix “looks pastey” (whitespace, or a long -enough run) to avoid misclassifying IME composition as paste. - -### `KeyCode::Enter`: newline vs submit - -There are two distinct “Enter becomes newline” mechanisms: - -- **While in a burst context** (`paste_burst.is_active()`): `append_newline_if_active(now)` appends - `\n` into the burst buffer so multi-line pastes stay buffered as one explicit paste. -- **Immediately after burst activity** (enter suppression window): - `newline_should_insert_instead_of_submit(now)` inserts `\n` into the textarea and calls - `extend_window(now)` so a slightly-late Enter keeps behaving like “newline” rather than “submit”. - -Both are disabled inside slash-command context (command popup is active or the first line begins -with `/`) so Enter keeps its normal “submit/execute” semantics while composing commands. - -### Non-char keys / Ctrl+modified input - -Non-char input must not leak burst state across unrelated actions: - -- If there is buffered burst text, callers should flush it before calling - `clear_window_after_non_char` (see “Pitfalls worth calling out”), typically via - `PasteBurst::flush_before_modified_input`. -- `PasteBurst::clear_window_after_non_char` clears the “recent burst” window so the next keystroke - doesn’t get incorrectly grouped into a previous paste. - -### Pitfalls worth calling out - -- `PasteBurst::clear_window_after_non_char` clears `last_plain_char_time`. If you call it while - `buffer` is non-empty and _haven’t already flushed_, `flush_if_due()` no longer has a timestamp - to time out against, so the buffered text may never flush. Treat `clear_window_after_non_char` as - “drop classification context after flush”, not “flush”. -- `PasteBurst::flush_if_due` uses a strict `>` comparison, so tests and UI ticks should cross the - threshold by at least 1ms (see `PasteBurst::recommended_flush_delay`). - -## Notable interactions / invariants - -- The composer frequently slices `textarea.text()` using the cursor position; all code that - slices must clamp the cursor to a UTF-8 char boundary first. -- `sync_popups()` must run after any change that can affect popup visibility or filtering: - inserting, deleting, flushing a burst, applying a paste placeholder, etc. -- Shortcut overlay toggling via `?` is gated on `!is_in_paste_burst()` so pastes cannot flip UI - modes while streaming. -- Mention popup selection has two payloads: visible `$name` text and hidden - `mention_paths[name] -> canonical target` linkage. The generic - `set_text_content` path intentionally clears linkage for fresh drafts; restore - paths that rehydrate blocked/interrupted submissions must use the - mention-preserving setter so retry keeps the originally selected target. - -## Tests that pin behavior - -The `PasteBurst` logic is currently exercised through `ChatComposer` integration tests. - -- `codex-rs/tui/src/bottom_pane/chat_composer.rs` - - `non_ascii_burst_handles_newline` - - `ascii_burst_treats_enter_as_newline` - - `question_mark_does_not_toggle_during_paste_burst` - - `burst_paste_fast_small_buffers_and_flushes_on_stop` - - `burst_paste_fast_large_inserts_placeholder_on_flush` - -This document calls out some additional contracts (like “flush before clearing”) that are not yet -fully pinned by dedicated `PasteBurst` unit tests. diff --git a/docs/tui-request-user-input.md b/docs/tui-request-user-input.md deleted file mode 100644 index 8ca6f5369bb1..000000000000 --- a/docs/tui-request-user-input.md +++ /dev/null @@ -1,41 +0,0 @@ -# Request user input overlay (TUI) - -This note documents the TUI overlay used to gather answers for -`RequestUserInputEvent`. - -## Overview - -The overlay renders one question at a time and collects: - -- A single selected option (when options exist). -- Freeform notes (always available). - -When options are present, notes are stored per selected option and the first -option is selected by default, so every option question has an answer. If a -question has no options and no notes are provided, the answer is submitted as -`skipped`. - -## Focus and input routing - -The overlay tracks a small focus state: - -- **Options**: Up/Down move the selection and Space selects. -- **Notes**: Text input edits notes for the currently selected option. - -Typing while focused on options switches into notes automatically to reduce -friction for freeform input. - -## Navigation - -- Enter advances to the next question. -- Enter on the last question submits all answers. -- PageUp/PageDown navigate across questions (when multiple are present). -- Esc interrupts the run in option selection mode. -- When notes are open for an option question, Tab or Esc clears notes and returns - to option selection. - -## Layout priorities - -The layout prefers to keep the question and all options visible. Notes and -footer hints collapse as space shrinks, with notes falling back to a single-line -"Notes: ..." input in tight terminals. diff --git a/docs/tui-stream-chunking-review.md b/docs/tui-stream-chunking-review.md deleted file mode 100644 index 3722492ddf8c..000000000000 --- a/docs/tui-stream-chunking-review.md +++ /dev/null @@ -1,124 +0,0 @@ -# TUI Stream Chunking - -This document explains how stream chunking in the TUI works and why it is -implemented this way. - -## Problem - -Streaming output can arrive faster than a one-line-per-tick animation can show -it. If commit speed stays fixed while arrival speed spikes, queued lines grow -and visible output lags behind received output. - -## Design goals - -- Preserve existing baseline behavior under normal load. -- Reduce display lag when backlog builds. -- Keep output order stable. -- Avoid abrupt single-frame flushes that look jumpy. -- Keep policy transport-agnostic and based only on queue state. - -## Non-goals - -- The policy does not schedule animation ticks. -- The policy does not depend on upstream source identity. -- The policy does not reorder queued output. - -## Where the logic lives - -- `codex-rs/tui/src/streaming/chunking.rs` - - Adaptive policy, mode transitions, and drain-plan selection. -- `codex-rs/tui/src/streaming/commit_tick.rs` - - Orchestration for each commit tick: snapshot, decide, drain, trace. -- `codex-rs/tui/src/streaming/controller.rs` - - Queue/drain primitives used by commit-tick orchestration. -- `codex-rs/tui/src/chatwidget.rs` - - Integration point that invokes commit-tick orchestration and handles UI - lifecycle events. - -## Runtime flow - -On each commit tick: - -1. Build a queue snapshot across active controllers. - - `queued_lines`: total queued lines. - - `oldest_age`: max age of the oldest queued line across controllers. -2. Ask adaptive policy for a decision. - - Output: current mode and a drain plan. -3. Apply drain plan to each controller. -4. Emit drained `HistoryCell`s for insertion by the caller. -5. Emit trace logs for observability. - -In `CatchUpOnly` scope, policy state still advances, but draining is skipped -unless mode is currently `CatchUp`. - -## Modes and transitions - -Two modes are used: - -- `Smooth` - - Baseline behavior: one line drained per baseline commit tick. - - Baseline tick interval currently comes from - `tui/src/app.rs:COMMIT_ANIMATION_TICK` (~8.3ms, ~120fps). -- `CatchUp` - - Drain current queued backlog per tick via `Batch(queued_lines)`. - -Entry and exit use hysteresis: - -- Enter `CatchUp` when queue depth or queue age exceeds enter thresholds. -- Exit requires both depth and age to be below exit thresholds for a hold - window (`EXIT_HOLD`). - -This prevents oscillation when load hovers near thresholds. - -## Current experimental tuning values - -These are the current values in `streaming/chunking.rs` plus the baseline -commit tick in `tui/src/app.rs`. They are -experimental and may change as we gather more trace data. - -- Baseline commit tick: `~8.3ms` (`COMMIT_ANIMATION_TICK` in `app.rs`) -- Enter catch-up: - - `queued_lines >= 8` OR `oldest_age >= 120ms` -- Exit catch-up eligibility: - - `queued_lines <= 2` AND `oldest_age <= 40ms` -- Exit hold (`CatchUp -> Smooth`): `250ms` -- Re-entry hold after catch-up exit: `250ms` -- Severe backlog thresholds: - - `queued_lines >= 64` OR `oldest_age >= 300ms` - -## Drain planning - -In `Smooth`, plan is always `Single`. - -In `CatchUp`, plan is `Batch(queued_lines)`, which drains the currently queued -backlog for immediate convergence. - -## Why this design - -This keeps normal animation semantics intact, while making backlog behavior -adaptive: - -- Under normal load, behavior stays familiar and stable. -- Under pressure, queue age is reduced quickly without sacrificing ordering. -- Hysteresis avoids rapid mode flapping. - -## Invariants - -- Queue order is preserved. -- Empty queue resets policy back to `Smooth`. -- `CatchUp` exits only after sustained low pressure. -- Catch-up drains are immediate while in `CatchUp`. - -## Observability - -Trace events are emitted from commit-tick orchestration: - -- `stream chunking commit tick` - - `mode`, `queued_lines`, `oldest_queued_age_ms`, `drain_plan`, - `has_controller`, `all_idle` -- `stream chunking mode transition` - - `prior_mode`, `new_mode`, `queued_lines`, `oldest_queued_age_ms`, - `entered_catch_up` - -These events are intended to explain display lag by showing queue pressure, -selected drain behavior, and mode transitions over time. diff --git a/docs/tui-stream-chunking-tuning.md b/docs/tui-stream-chunking-tuning.md deleted file mode 100644 index d9a2ea5e2136..000000000000 --- a/docs/tui-stream-chunking-tuning.md +++ /dev/null @@ -1,98 +0,0 @@ -# TUI Stream Chunking Tuning Guide - -This document explains how to tune adaptive stream chunking constants without -changing the underlying policy shape. - -## Scope - -Use this guide when adjusting queue-pressure thresholds and hysteresis windows in -`codex-rs/tui/src/streaming/chunking.rs`, and baseline commit cadence in -`codex-rs/tui/src/app.rs`. - -This guide is about tuning behavior, not redesigning the policy. - -## Before tuning - -- Keep the baseline behavior intact: - - `Smooth` mode drains one line per baseline tick. - - `CatchUp` mode drains queued backlog immediately. -- Capture trace logs with: - - `codex_tui::streaming::commit_tick` -- Evaluate on sustained, bursty, and mixed-output prompts. - -See `docs/tui-stream-chunking-validation.md` for the measurement process. - -## Tuning goals - -Tune for all three goals together: - -- low visible lag under bursty output -- low mode flapping (`Smooth <-> CatchUp` chatter) -- stable catch-up entry/exit behavior under mixed workloads - -## Constants and what they control - -### Baseline commit cadence - -- `COMMIT_ANIMATION_TICK` (`tui/src/app.rs`) - - Lower values increase smooth-mode update cadence and reduce steady-state lag. - - Higher values increase smoothing and can increase perceived lag. - - This should usually move after chunking thresholds/holds are in a good range. - -### Enter/exit thresholds - -- `ENTER_QUEUE_DEPTH_LINES`, `ENTER_OLDEST_AGE` - - Lower values enter catch-up earlier (less lag, more mode switching risk). - - Higher values enter later (more lag tolerance, fewer mode switches). -- `EXIT_QUEUE_DEPTH_LINES`, `EXIT_OLDEST_AGE` - - Lower values keep catch-up active longer. - - Higher values allow earlier exit and may increase re-entry churn. - -### Hysteresis holds - -- `EXIT_HOLD` - - Longer hold reduces flip-flop exits when pressure is noisy. - - Too long can keep catch-up active after pressure has cleared. -- `REENTER_CATCH_UP_HOLD` - - Longer hold suppresses rapid re-entry after exit. - - Too long can delay needed catch-up for near-term bursts. - - Severe backlog bypasses this hold by design. - -### Severe-backlog gates - -- `SEVERE_QUEUE_DEPTH_LINES`, `SEVERE_OLDEST_AGE` - - Lower values bypass re-entry hold earlier. - - Higher values reserve hold bypass for only extreme pressure. - -## Recommended tuning order - -Tune in this order to keep cause/effect clear: - -1. Entry/exit thresholds (`ENTER_*`, `EXIT_*`) -2. Hold windows (`EXIT_HOLD`, `REENTER_CATCH_UP_HOLD`) -3. Severe gates (`SEVERE_*`) -4. Baseline cadence (`COMMIT_ANIMATION_TICK`) - -Change one logical group at a time and re-measure before the next group. - -## Symptom-driven adjustments - -- Too much lag before catch-up starts: - - lower `ENTER_QUEUE_DEPTH_LINES` and/or `ENTER_OLDEST_AGE` -- Frequent `Smooth -> CatchUp -> Smooth` chatter: - - increase `EXIT_HOLD` - - increase `REENTER_CATCH_UP_HOLD` - - tighten exit thresholds (lower `EXIT_*`) -- Catch-up engages too often for short bursts: - - increase `ENTER_QUEUE_DEPTH_LINES` and/or `ENTER_OLDEST_AGE` - - increase `REENTER_CATCH_UP_HOLD` -- Catch-up engages too late: - - lower `ENTER_QUEUE_DEPTH_LINES` and/or `ENTER_OLDEST_AGE` - - lower severe gates (`SEVERE_*`) to bypass re-entry hold sooner - -## Validation checklist after each tuning pass - -- `cargo test -p codex-tui` passes. -- Trace window shows bounded queue-age behavior. -- Mode transitions are not concentrated in repeated short-interval cycles. -- Catch-up clears backlog quickly once mode enters `CatchUp`. diff --git a/docs/tui-stream-chunking-validation.md b/docs/tui-stream-chunking-validation.md deleted file mode 100644 index a31a7b62232e..000000000000 --- a/docs/tui-stream-chunking-validation.md +++ /dev/null @@ -1,107 +0,0 @@ -# TUI Stream Chunking Validation Process - -This document records the process used to validate adaptive stream chunking -and anti-flap behavior. - -## Scope - -The goal is to verify two properties from runtime traces: - -- display lag is reduced when queue pressure rises -- mode transitions remain stable instead of rapidly flapping - -## Trace targets - -Chunking observability is emitted by: - -- `codex_tui::streaming::commit_tick` - -Two trace messages are used: - -- `stream chunking commit tick` -- `stream chunking mode transition` - -## Runtime command - -Run Codex with chunking traces enabled: - -```bash -RUST_LOG='codex_tui::streaming::commit_tick=trace,codex_tui=info,codex_core=info,codex_rmcp_client=info' \ - just codex --enable=responses_websockets -``` - -## Log capture process - -Tip: for one-off measurements, run with `-c log_dir=...` to direct logs to a fresh directory and avoid mixing sessions. - -1. Record the current size of `~/.codex/log/codex-tui.log` as a start offset. -2. Run an interactive prompt that produces sustained streamed output. -3. Stop the run. -4. Parse only log bytes written after the recorded offset. - -This avoids mixing earlier sessions with the current measurement window. - -## Metrics reviewed - -For each measured window: - -- `commit_ticks` -- `mode_transitions` -- `smooth_ticks` -- `catchup_ticks` -- drain-plan distribution (`Single`, `Batch(n)`) -- queue depth (`max`, `p95`, `p99`) -- oldest queued age (`max`, `p95`, `p99`) -- rapid re-entry count: - - number of `Smooth -> CatchUp` transitions within 1 second of a - `CatchUp -> Smooth` transition - -## Interpretation - -- Healthy behavior: - - queue age remains bounded while backlog is drained - - transition count is low relative to total ticks - - rapid re-entry events are infrequent and localized to burst boundaries -- Regressed behavior: - - repeated short-interval mode toggles across an extended window - - persistent queue-age growth while in smooth mode - - long catch-up runs without backlog reduction - -## Experiment history - -This section captures the major tuning passes so future work can build on -what has already been tried. - -- Baseline - - One-line smooth draining with a 50ms commit tick. - - This preserved familiar pacing but could feel laggy under sustained - backlog. -- Pass 1: instant catch-up, baseline tick unchanged - - Kept smooth-mode semantics but made catch-up drain the full queued - backlog each catch-up tick. - - Result: queue lag dropped faster, but perceived motion could still feel - stepped because smooth-mode cadence remained coarse. -- Pass 2: faster baseline tick (25ms) - - Improved smooth-mode cadence and reduced visible stepping. - - Result: better, but still not aligned with draw cadence. -- Pass 3: frame-aligned baseline tick (~16.7ms) - - Set baseline commit cadence to approximately 60fps. - - Result: smoother perceived progression while retaining hysteresis and - fast backlog convergence. -- Pass 4: higher frame-aligned baseline tick (~8.3ms) - - Set baseline commit cadence to approximately 120fps. - - Result: further reduced smooth-mode stepping while preserving the same - adaptive catch-up policy shape. - -Current state combines: - -- instant catch-up draining in `CatchUp` -- hysteresis for mode-entry/exit stability -- frame-aligned smooth-mode commit cadence (~8.3ms) - -## Notes - -- Validation is source-agnostic and does not rely on naming any specific - upstream provider. -- This process intentionally preserves existing baseline smooth behavior and - focuses on burst/backlog handling behavior.