Skip to content

Feature: Double Ctrl+C to Quit (Codex CLI-style interrupt handling) #9041

@ndycode

Description

@ndycode

Summary

Add a double Ctrl+C to quit behavior inspired by Codex CLI. Instead of immediately terminating the session when Ctrl+C is pressed on an empty prompt, show a warning message and require a second Ctrl+C press within a timeout window to actually exit.


Current Behavior

Scenario Current Action
Ctrl+C with text in prompt ✅ Clears the input (works as expected)
Ctrl+C with empty prompt Immediately exits the CLI

Problem: Users can accidentally terminate their session by pressing Ctrl+C once when the prompt is empty. This is frustrating, especially during long conversations or when users instinctively press Ctrl+C to "cancel" even when there's nothing to cancel.


Proposed Behavior

Scenario Proposed Action
Ctrl+C with text in prompt Clears the input (unchanged)
1st Ctrl+C with empty prompt Show message: "Press Ctrl+C again to exit" + arm quit shortcut
2nd Ctrl+C within timeout Exit the CLI session
Timeout expires Reset state (next Ctrl+C shows warning again)

Visual Example

┌─────────────────────────────────────────────────────────┐
│  User presses Ctrl+C on empty prompt                    │
│                                                         │
│  > [empty prompt]                                       │
│                                                         │
│  ⚠️  Press Ctrl+C again to exit (or continue typing)   │
│                                                         │
│  [User presses Ctrl+C again within 1-2 seconds]         │
│  → CLI exits gracefully                                 │
└─────────────────────────────────────────────────────────┘

Implementation Reference

Codex CLI Pattern (Rust)

Codex CLI implements this via a time-bounded arming mechanism:

// Simplified from codex-rs/tui/src/chatwidget.rs
const QUIT_SHORTCUT_TIMEOUT: Duration = Duration::from_secs(1);

fn on_ctrl_c(&mut self) {
    // 1. Try to clear prompt first
    if self.bottom_pane.on_ctrl_c() == CancellationEvent::Handled {
        self.arm_quit_shortcut(key);
        return;
    }

    // 2. Check if shortcut is still armed (within timeout)
    if self.quit_shortcut_active_for(key) {
        self.request_quit_without_confirmation();
        return;
    }

    // 3. Arm the shortcut and show hint
    self.arm_quit_shortcut(key);
    self.bottom_pane.show_quit_shortcut_hint(key);
}

OpenCode Current Implementation

The relevant code is in:

  • Signal handling disabled: packages/opencode/src/cli/cmd/tui/app.tsx#L168 (exitOnCtrlC: false)
  • Current Ctrl+C logic: packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx#L798-L815

Current logic (simplified):

// If prompt has text → clear it
// If prompt is empty → exit immediately (app_exit)

Suggested Changes

  1. Add state to track "quit armed" status and timestamp
  2. Modify the empty-prompt Ctrl+C handler:
    • If not armed: arm + show warning message + set timestamp
    • If armed and within timeout: exit
    • If armed but timeout expired: reset and re-arm
  3. Add a visible hint in the UI (e.g., status bar or inline message)
  4. Suggested timeout: 1-2 seconds (Codex uses 1s)

Benefits

  1. Prevents accidental exits - No more losing context from instinctive Ctrl+C
  2. Familiar UX - Matches behavior users expect from Codex CLI and other modern CLIs
  3. Non-breaking - Users who want to exit just press twice quickly
  4. Consistent - Aligns with the existing "double escape to abort" pattern for session interruption

Acceptance Criteria

  • Pressing Ctrl+C with text in prompt clears the input (existing behavior)
  • Pressing Ctrl+C on empty prompt shows a warning message instead of exiting
  • Pressing Ctrl+C again within timeout (1-2s) exits the CLI
  • Warning message disappears after timeout or when user starts typing
  • Timeout duration is configurable (optional, nice-to-have)

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions