Remove local docs and specs (#20896)

## Summary

We should not check local-only docs or planning specs into this
repository. Keeping those files here duplicates the canonical Codex
documentation surface and makes transient implementation notes look like
supported docs.

This PR removes the local-only docs/spec files from `docs/` and trims
`docs/config.md` back to links for the maintained configuration
documentation on developers.openai.com.
This commit is contained in:
Eric Traut
2026-05-03 10:23:09 -07:00
committed by GitHub
Unverified
parent 39555036a3
commit 67849d950d
8 changed files with 0 additions and 1060 deletions
-116
View File
@@ -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`).
-96
View File
@@ -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.
-130
View File
@@ -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
```
-348
View File
@@ -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 messages 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. Its 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
doesnt 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 _havent 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.
-41
View File
@@ -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.
-124
View File
@@ -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.
-98
View File
@@ -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`.
-107
View File
@@ -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.