In the codex-rs folder where the rust code lives:
- Crate names are prefixed with
codex-. For example, thecorefolder's crate is namedcodex-core - When using format! and you can inline variables into {}, always do that.
- Treat
codex-rsas a read-only mirror ofopenai/codex:main; edit Rust sources undercode-rsinstead.
Completion/build step
- Always validate using
./build-fast.shfrom the repo root. This is the single required check and must pass cleanly. ./build-fast.shcan take 20+min to run from a cold cache!!! Please use long timeout when running./build-fast.shor waiting for it to complete.- Policy: All errors AND all warnings must be fixed before you’re done. Treat any compiler warning as a failure and address it (rename unused vars with
_, removemut, delete dead code, etc.). - Do not run additional format/lint/test commands on completion (e.g.,
just fmt,just fix,cargo test) unless explicitly requested for a specific task. - NEVER run rustfmt
- Before pushing to
main, run./pre-release.shto mirror the release preflight (dev-fast build, CLI smokes, workspace nextest).
Optional regression checks (recommended when touching the Rust workspace):
cargo nextest run --no-fail-fast— runs all workspace tests with the TUI helpers automatically enabled. The suite is green after the resume fixtures/git-init fallback updates; older Git builds may print a warning when falling back from--initial-branch, but tests still pass.- Focused sweeps stay quick and green:
cargo test -p code-tui --features test-helpers,cargo test -p code-cloud-tasks --tests, andcargo test -p mcp-types --tests.
When debugging regressions or bugs, write a failing test (or targeted reproduction script) first and confirm it captures the issue before touching code—if it can’t fail, you can’t be confident the fix works.
- Keep docs clean, clear, and current; prune stale instructions instead of piling on caveats.
- Avoid excessive verbosity; prioritize concise guidance over long narratives.
- Do not document minor or non-core features; focus on system-critical flows and expectations.
- Never commit temporary "working" docs, plans, or scratch notes.
The TUI enforces strict, per‑turn ordering for all streamed content. Every
stream insert (Answer or Reasoning) must be associated with a stable
(request_ordinal, output_index, sequence_number) key provided by the model.
- A stream insert MUST carry a non‑empty stream id. The UI seeds an order key
for
(kind, id)from the event'sOrderMetabefore any insert. - The TUI WILL NOT insert streaming content without a stream id. Any attempt to insert without an id is dropped with an error log to make the issue visible during development.
- Review staged changes before every commit:
git --no-pager diff --staged --stat(and skimgit --no-pager diff --stagedif needed). - Write a descriptive subject that explains what changed and why. Avoid placeholders like "chore: commit local work".
- Prefer Conventional Commits with an optional scope:
feat(tui/history): …,fix(core/exec): …,docs(agents): …. - Keep the subject ≤ 72 chars; add a short body if rationale or context helps future readers.
- Use imperative, present tense: "add", "fix", "update" (not "added", "fixes").
- For merge commits, skip custom prefixes like
merge(main<-origin/main):. Use a clear subject such asMerge origin/main: <what changed and how conflicts were resolved>.
Examples:
feat(tui/history): show exit code and duration for Exec cellsfix(core/codex): handle SIGINT in on_exec_command_begin to avoid orphaned childdocs(agents): clarify commit-message expectations
When the user asks you to "push" local work:
- Never rebase in this flow. Do not use
git pull --rebaseor attempt to replay local commits. - Prefer a simple merge of
origin/maininto the current branch, keeping our local history intact. - If the remote only has trivial release metadata changes (e.g.,
codex-cli/package.jsonversion bumps), adopt the remote version for those files and keep ours for everything else unless the user specifies otherwise. - If in doubt or if conflicts touch non-trivial areas, pause and ask before resolving.
Quick procedure (merge-only):
- Commit your local work first:
- Review:
git --no-pager diff --statandgit --no-pager diff - Stage + commit:
git add -A && git commit -m "<descriptive message of local changes>"
- Review:
- Fetch remote:
git fetch origin - Merge without auto-commit:
git merge --no-ff --no-commit origin/main(stops before committing so you can choose sides) - Resolve policy:
- Default to ours:
git checkout --ours . - Take remote for trivial package/version files as needed, e.g.:
git checkout --theirs codex-cli/package.json
- Default to ours:
- Stage and commit the merge with a descriptive message, e.g.:
git add -A && git commit -m "Merge origin/main: adopt remote version bumps; keep ours elsewhere (<areas>)"
- Run
./build-fast.shand thengit push
The command execution flow in Codex follows an event-driven pattern:
-
Core Layer (
codex-core/src/codex.rs):on_exec_command_begin()initiates command execution- Creates
EventMsg::ExecCommandBeginevents with command details
-
TUI Layer (
codex-tui/src/chatwidget.rs):handle_codex_event()processes execution events- Manages
RunningCommandstate for active commands - Creates
HistoryCell::Execfor UI rendering
-
History Cell (
codex-tui/src/history_cell.rs):new_active_exec_command()- Creates cell for running commandnew_completed_exec_command()- Updates with final output- Handles syntax highlighting via
ParsedCommand
This architecture separates concerns between execution logic (core), UI state management (chatwidget), and rendering (history_cell).
- All Auto Drive escape routing lives in
code-rs/tui/src/chatwidget.rs. TheChatWidget::auto_should_handle_global_eschelper decides whether the global Esc handler inapp.rsshould defer to Auto Drive, andChatWidget::handle_key_eventowns the actual stop / pause behaviour. When you need to tweak Esc semantics, update those two locations together. - The approval pane must never swallow Esc.
code-rs/tui/src/bottom_pane/auto_coordinator_view.rsintentionally lets Esc (and the other approval shortcuts) bubble back to the chat widget; keep this contract intact when editing the view layer. - Avoid adding additional Esc handlers elsewhere for Auto Drive flows. Doing
so breaks the modal-first ordering in
app.rsand prevents users from reliably stopping a run.
- Start with
make_chatwidget_manual()(ormake_chatwidget_manual_with_sender()) to build aChatWidgetin isolation with in-memory channels. - Simulate user input by defining a small enum (
ScriptStep) and feeding key events viachat.handle_key_event(); seerun_script()intests.rsfor a ready-to-use helper that also pumpsAppEvents. - After the scripted interaction, render with a
ratatui::Terminal/TestBackend, then usebuffer_to_string()(wrapsstrip_ansi_escapes) to normalize ANSI output before asserting. - Prefer snapshot assertions (
assert_snapshot!) or rich string comparisons so UI regressions are obvious. Keep snapshots deterministic by trimming trailing space and driving commit ticks just like the existing tests do. - When adding fixtures or updating snapshots, gate rewrites behind an opt-in env var (e.g.,
UPDATE_IDEAL=1) so baseline refreshes remain explicit.
- The VT100 harness lives under
code-rs/tui/tests/vt100_chatwidget_snapshot.rs. It renders the liveChatWidgetUI into aTerminal<VT100Backend>so snapshots capture the exact PTY output the user sees (including frame chrome, composer rows, and streaming inserts). - Use
ChatWidgetHarnesshelpers fromcode_tui::test_helpersto seed history events and drainAppEvents. Callrender_chat_widget_to_vt100(width, height)for a single frame, orrender_chat_widget_frames_to_vt100(&[(w,h), ...])to simulate successive draws while streaming. - The harness now exports
layout_metrics()so tests can assert scroll offsets and viewport heights without spelunking through private fields. - Snapshots are deterministic: tests set
CODEX_TUI_FAKE_HOUR=12automatically so greeting text (“What can I code for you today?”) doesn’t oscillate. If you need a different hour in a test, override the env var before constructing the harness. - To add a new scenario, push history/events onto the harness, call
render_*_to_vt100, and eitherinsta::assert_snapshot!the frame(s) or manually assert string contents. For multi-frame streaming, push deltas/events first, then capture frames in the order the UI would display them. - Run all VT100 snapshots via:
cargo test -p code-tui --test vt100_chatwidget_snapshot --features test-helpers -- --nocapture
- When you intentionally change rendering, review the
.snap.newfiles that appear incode-rs/tui/tests/snapshots/and accept them withcargo insta review/cargo insta accept(limit to this test where possible).
- Use
scripts/wait-for-gh-run.shto follow GitHub Actions releases without spamming manualghcommands. - Typical release check right after a push:
scripts/wait-for-gh-run.sh --workflow Release --branch main. - If you already know the run ID (e.g., from webhook output), run
scripts/wait-for-gh-run.sh --run <run-id>. - Adjust the poll cadence via
--interval <seconds>(defaults to 8). The script exits 0 on success and 1 on failure, so it can gate local automation. - Pass
--failure-logsto automatically dump logs for any job that does not finish successfully. - Dependencies: GitHub CLI (
gh) andjqmust be available inPATH.