Skip to content

Conversation

@joshka-oai
Copy link
Collaborator

@joshka-oai joshka-oai commented Jan 8, 2026

Problem

Codex’s TUI quit behavior has historically been easy to trigger accidentally and hard to reason
about.

  • Ctrl+C/Ctrl+D could terminate the UI immediately, which is a common key to press while trying
    to dismiss a modal, cancel a command, or recover from a stuck state.
  • “Quit” and “shutdown” were not consistently separated, so some exit paths could bypass the
    shutdown/cleanup work that should run before the process terminates.

This PR makes quitting both safer (harder to do by accident) and more uniform across quit
gestures, while keeping the shutdown-first semantics explicit.

Mental model

After this change, the system treats quitting as a UI request that is coordinated by the app
layer.

  • The UI requests exit via AppEvent::Exit(ExitMode).
  • ExitMode::ShutdownFirst is the normal user path: the app triggers Op::Shutdown, continues
    rendering while shutdown runs, and only ends the UI loop once shutdown has completed.
  • ExitMode::Immediate exists as an escape hatch (and as the post-shutdown “now actually exit”
    signal); it bypasses cleanup and should not be the default for user-triggered quits.

User-facing quit gestures are intentionally “two-step” for safety:

  • Ctrl+C and Ctrl+D no longer exit immediately.
  • The first press arms a 1-second window and shows a footer hint (“ctrl + again to quit”).
  • Pressing the same key again within the window requests a shutdown-first quit; otherwise the
    hint expires and the next press starts a fresh window.

Key routing remains modal-first:

  • A modal/popup gets first chance to consume Ctrl+C.
  • If a modal handles Ctrl+C, any armed quit shortcut is cleared so dismissing a modal cannot
    prime a subsequent Ctrl+C to quit.
  • Ctrl+D only participates in quitting when the composer is empty and no modal/popup is active.

The design doc docs/exit-confirmation-prompt-design.md captures the intended routing and the
invariants the UI should maintain.

Non-goals

  • This does not attempt to redesign modal UX or make modals uniformly dismissible via Ctrl+C.
    It only ensures modals get priority and that quit arming does not leak across modal handling.
  • This does not introduce a persistent confirmation prompt/menu for quitting; the goal is to keep
    the exit gesture lightweight and consistent.
  • This does not change the semantics of core shutdown itself; it changes how the UI requests and
    sequences it.

Tradeoffs

  • Quitting via Ctrl+C/Ctrl+D now requires a deliberate second keypress, which adds friction for
    users who relied on the old “instant quit” behavior.
  • The UI now maintains a small time-bounded state machine for the armed shortcut, which increases
    complexity and introduces timing-dependent behavior.

This design was chosen over alternatives (a modal confirmation prompt or a long-lived “are you
sure” state) because it provides an explicit safety barrier while keeping the flow fast and
keyboard-native.

Architecture

  • ChatWidget owns the quit-shortcut state machine and decides when a quit gesture is allowed
    (idle vs cancellable work, composer state, etc.).
  • BottomPane owns rendering and local input routing for modals/popups. It is responsible for
    consuming cancellation keys when a view is active and for showing/expiring the footer hint.
  • App owns shutdown sequencing: translating AppEvent::Exit(ShutdownFirst) into Op::Shutdown
    and only terminating the UI loop when exit is safe.

This keeps “what should happen” decisions (quit vs interrupt vs ignore) in the chat/widget layer,
while keeping “how it looks and which view gets the key” in the bottom-pane layer.

Observability

You can tell this is working by running the TUIs and exercising the quit gestures:

  • While idle: pressing Ctrl+C (or Ctrl+D with an empty composer and no modal) shows a footer
    hint for ~1 second; pressing again within that window exits via shutdown-first.
  • While streaming/tools/review are active: Ctrl+C interrupts work rather than quitting.
  • With a modal/popup open: Ctrl+C dismisses/handles the modal (if it chooses to) and does not
    arm a quit shortcut; a subsequent quick Ctrl+C should not quit unless the user re-arms it.

Failure modes are visible as:

  • Quits that happen immediately (no hint window) from Ctrl+C/Ctrl+D.
  • Quits that occur while a modal is open and consuming Ctrl+C.
  • UI termination before shutdown completes (cleanup skipped).

Tests

  • Updated/added unit and snapshot coverage in codex-tui and codex-tui2 to validate:
    • The quit hint appears and expires on the expected key.
    • Double-press within the window triggers a shutdown-first quit request.
    • Modal-first routing prevents quit bypass and clears any armed shortcut when a modal consumes
      Ctrl+C.

These tests focus on the UI-level invariants and rendered output; they do not attempt to validate
real terminal key-repeat timing or end-to-end process shutdown behavior.


Screenshot:
Screenshot 2026-01-13 at 1 05 28 PM

@joshka-oai joshka-oai requested a review from gpeal January 8, 2026 22:02
@joshka-oai joshka-oai force-pushed the joshka/exit-flow branch 2 times, most recently from 01f8853 to bac7f58 Compare January 10, 2026 01:45
@joshka-oai joshka-oai changed the title Unify shutdown-first quit flow and exit confirmation prompt tui: double-press Ctrl+C/Ctrl+D to quit Jan 10, 2026
@joshka-oai joshka-oai force-pushed the joshka/exit-flow branch 4 times, most recently from 28f6f6d to 15ca2f5 Compare January 10, 2026 02:07
@jif-oai
Copy link
Collaborator

jif-oai commented Jan 12, 2026

@codex review

Copy link
Collaborator

@jif-oai jif-oai left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think @nornagon-openai review would be good here

Copy link
Contributor

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 15ca2f5258

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@joshka-oai joshka-oai force-pushed the joshka/exit-flow branch 2 times, most recently from d417acd to 01d8970 Compare January 13, 2026 20:54
@joshka-oai
Copy link
Collaborator Author

Added a bit more docs and fixed something that that doc review caught.

modal/popup-handled Ctrl+C now clears any armed quit shortcut/hint instead of arming it; composer-clear Ctrl+C still arms as before.

Add Ctrl+C/Ctrl+D behavior history and exit confirmation prompt
design documentation. Introduce markdownlint config with a 100
character line length to keep markdown formatting consistent.
Represent user-initiated quit as AppEvent::Exit(ExitMode) so the app event
loop owns shutdown sequencing.

This keeps all quit triggers consistent: the UI requests a shutdown-first
quit, continues rendering while shutdown runs, and only terminates the UI
loop once shutdown completes.
Replace the older Ctrl+C quit history writeup with a single document that
captures the current exit, shutdown, and interruption behavior for tui and
tui2.

This keeps the narrative close to the implementation and avoids split,
partially overlapping docs.
Replace the quit confirmation prompt menu with a transient footer hint.

The first press of Ctrl+C or Ctrl+D shows 'ctrl + <key> again to quit'
for 1s. Pressing the same key again within that window exits via a
shutdown-first flow so cleanup can run.

Remove leftover prompt-related config/events and rename the footer mode
to avoid Ctrl+C-specific naming. Update docs/snapshots and switch the
markdownlint-cli2 config to YAML.
Document the exit event model and how quit/interrupt behavior is layered
between ChatWidget and BottomPane.

This makes the double-press quit shortcut easier to reason about and
captures the current ordering caveat where the second Ctrl+C press is
checked before offering Ctrl+C to the bottom pane.
A second Ctrl+C within the quit shortcut window no longer bypasses
bottom-pane modal handling.

Use a purpose-named "no modal/popup" predicate for Ctrl+D quit gating
instead of the external-editor helper.
@joshka-oai joshka-oai enabled auto-merge (squash) January 14, 2026 00:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants