diff --git a/CHANGELOG.md b/CHANGELOG.md index 90d4baf2bc..c11e1d5666 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,13 +10,15 @@ file is the human-readable summary of what changed and why. ### Sessions - **`Ctrl+C` interrupts a running turn** — while the agent is working, `Ctrl+C` - aborts it on a single press, and the prompt footer now leads with - `ctrl+c interrupt` instead of `esc interrupt`. `escape` keeps its existing - double-press interrupt. With text in the prompt `Ctrl+C` still clears the - draft first, and while a turn is running it can no longer arm the - double-press quit from [#51](https://github.com/quickbeard/codev-code/pull/51) - — `Ctrl+D`, `q`, and `/exit` still quit immediately. Configurable as - the new `session_interrupt_now` keybind. + aborts it on a single press, alongside the existing double-press `escape`. + The prompt footer and the home screen tip keep advertising `esc interrupt`. + With text in the prompt `Ctrl+C` still clears the draft first, and while a + turn is running it can no longer arm the double-press quit from + [#51](https://github.com/quickbeard/codev-code/pull/51) — `Ctrl+D`, + `q`, and `/exit` still quit immediately. Configurable as the new + `session_interrupt_now` keybind. + ([#108](https://github.com/quickbeard/codev-code/pull/108), + [#109](https://github.com/quickbeard/codev-code/pull/109)) ### Sidebar diff --git a/DIVERGENCES.md b/DIVERGENCES.md index 82d3eb6b2a..f8c280b88c 100644 --- a/DIVERGENCES.md +++ b/DIVERGENCES.md @@ -104,7 +104,7 @@ from the squash message first, or use a plain merge. | `packages/tui/src/routes/session/permission.tsx`, `packages/opencode/src/cli/cmd/run/permission.shared.ts`, `packages/opencode/test/cli/run/permission.shared.test.ts`, `packages/tui/src/component/error-component.tsx`, `packages/opencode/src/cli/cmd/run/splash.ts` | Visible sentences/badges "OpenCode"/"opencode" → "CoDev Code"/"codev-code" (incl. the permission always-allow copy and its test expectations); permission.tsx also registers a local `app.exit.confirm` command mirroring its `app.exit` reject and gathers the confirm binding (double-press Ctrl+C — see the double-Ctrl+C row) | User-facing copy; keep Ctrl+C rejecting the permission prompt as before. | Keep our strings; keep permission.tsx's `app.exit.confirm` command + binding. | | `packages/tui/src/config/keybind.ts`, `packages/tui/src/routes/session/question.tsx`, `packages/tui/src/ui/toast.tsx`, `packages/tui/test/app-lifecycle.test.tsx`, `packages/web/src/content/docs/keybinds.mdx` (+ `es/`) | Double-press Ctrl+C to quit. keybind.ts: `app_exit` `ctrl+c,ctrl+d,q` → `ctrl+d,q`, added `app_exit_confirm: "ctrl+c"` definition + its `app.exit.confirm` CommandMap entry. question.tsx: a local `app.exit.confirm` command mirroring its `app.exit` reject, with the confirm binding gathered in both option branches (Ctrl+C still rejects the question — single press). toast.tsx: `ToastOptions.variant` union gains `"secondary"` (resolves to `theme.secondary`, the banner brand color, for the confirm toast). app-lifecycle.test.tsx: added the "app.exit.confirm requires two presses to exit" test. Docs (en + es `keybinds.mdx`): split the `app_exit` default and added `app_exit_confirm: "ctrl+c"`. See also the `app.tsx`, `dialog.tsx`, and `permission.tsx` rows for their `app.exit.confirm` handlers. | A stray Ctrl+C on an empty/unfocused prompt no longer quits instantly: the first press shows a "Press Ctrl+C again to exit" toast (1.5 s window), the second within that window quits. Ctrl+D, `q`, and the `/exit` slash commands stay immediate. | Keep our binding split, the `app_exit_confirm`/`app.exit.confirm` definition + mapping, the local confirm commands + gathered bindings, the toast `"secondary"` variant, the test, and the doc defaults; take upstream's surrounding changes. If upstream re-adds `ctrl+c` to `app_exit`, keep it removed. | | `packages/tui/src/config/keybind.ts`, `packages/web/src/content/docs/keybinds.mdx` (+ `es/`), `packages/opencode/test/cli/run/footer.view.test.tsx` | `sidebar_toggle` default `"b"` → `"ctrl+b,b"` (ctrl+b first so single-binding displays — the prompt-footer sidebar hint, the sidebar tip — resolve to it); `ctrl+b`'s former jobs move to `ctrl+o`: `input_move_left` `"left,ctrl+b"` → `"left,ctrl+o"`, `session_background` `"ctrl+b"` → `"ctrl+o"`; docs defaults updated to match (docs don't list `session_background`); footer.view.test.tsx asserts the run-CLI footer background hint as `"ctrl+o background"` (was `"ctrl+b background"`) since it resolves the real `session_background` default | Single-chord sidebar toggle on `ctrl+b` (tmux/VS Code sidebar muscle memory); its displaced bindings land on `ctrl+o`, which was unbound in every scope and free of terminal-encoding collisions (unlike `ctrl+h`/`ctrl+i`/`ctrl+m`) and flow control (`ctrl+s`/`ctrl+q`). | Keep our defaults on all three keybinds, the doc lines, and the footer hint assertion; take upstream's surrounding changes. If upstream binds `ctrl+o` to something else, re-audit and pick a new free key. | -| `packages/tui/src/config/keybind.ts`, `packages/tui/src/component/prompt/index.tsx`, `packages/tui/src/app.tsx`, `packages/web/src/content/docs/keybinds.mdx` (+ `es/`) | Single-press Ctrl+C interrupt. keybind.ts: added `session_interrupt_now: "ctrl+c"` + its `session.interrupt.now` CommandMap entry (`session_interrupt` keeps `escape`; its description gains "(press twice)"). prompt/index.tsx: added the `session.interrupt.now` command — same guards as upstream's `session.interrupt` but it aborts on the first press, and its `enabled` requires a busy session AND an empty prompt so ctrl+c falls through to `prompt.clear` (draft present) or `app.exit.confirm` (idle) instead of racing them on layer order; gathered the command in the `prompt.palette` binding list; the busy footer hint reads `interruptShortcut()` (resolves `session.interrupt.now`, falling back to the escape label when unbound) instead of the hardcoded `esc`, and still shows the escape label while its double-press is armed. app.tsx: split `app_exit_confirm` out of the shared exit `useBindings` into its own layer whose `enabled` also requires `sync.data.session_status[route.data.sessionID]` to be idle, so ctrl+c can't arm the quit confirm mid-turn (`app_exit` keeps the shared predicate). Docs (en + es): added the `session_interrupt_now` default. Covered by the fork-only `packages/tui/test/interrupt.view.test.tsx`. See also the double-Ctrl+C row above, whose scope this narrows. | `esc` alone was a two-press, low-discoverability interrupt; Ctrl+C is what users reach for to stop a running agent, and it now leads the busy footer. Gating on `enabled` rather than binding order keeps the ctrl+c precedence (clear draft → interrupt → exit confirm) deterministic, since same-priority layers resolve by registration order and `useBindings` re-registers on every reactive change. | Keep the `session_interrupt_now` definition + mapping, the `session.interrupt.now` command and its `enabled` predicate, the footer shortcut memos, the split exit layer, the doc lines, and the test; take upstream's surrounding changes. If upstream binds `ctrl+c` to something else in the prompt, re-audit the precedence chain end to end. | +| `packages/tui/src/config/keybind.ts`, `packages/tui/src/component/prompt/index.tsx`, `packages/tui/src/app.tsx`, `packages/web/src/content/docs/keybinds.mdx` (+ `es/`) | Single-press Ctrl+C interrupt. keybind.ts: added `session_interrupt_now: "ctrl+c"` + its `session.interrupt.now` CommandMap entry (`session_interrupt` keeps `escape`; its description gains "(press twice)"). prompt/index.tsx: added the `session.interrupt.now` command — same guards as upstream's `session.interrupt` but it aborts on the first press, and its `enabled` requires a busy session AND an empty prompt so ctrl+c falls through to `prompt.clear` (draft present) or `app.exit.confirm` (idle) instead of racing them on layer order; gathered the command in the `prompt.palette` binding list; the busy footer hint reads `interruptShortcut()` (resolves `session.interrupt`, mapping `escape` → `esc` and falling back to the `session.interrupt.now` label when escape is unbound) instead of the hardcoded `esc`. app.tsx: split `app_exit_confirm` out of the shared exit `useBindings` into its own layer whose `enabled` also requires `sync.data.session_status[route.data.sessionID]` to be idle, so ctrl+c can't arm the quit confirm mid-turn (`app_exit` keeps the shared predicate). Docs (en + es): added the `session_interrupt_now` default. Covered by the fork-only `packages/tui/test/interrupt.view.test.tsx`. See also the double-Ctrl+C row above, whose scope this narrows. | Ctrl+C is what many users reach for to stop a running agent, so it interrupts too; `escape` stays the advertised one in the busy footer and the home tip. Gating on `enabled` rather than binding order keeps the ctrl+c precedence (clear draft → interrupt → exit confirm) deterministic, since same-priority layers resolve by registration order and `useBindings` re-registers on every reactive change. | Keep the `session_interrupt_now` definition + mapping, the `session.interrupt.now` command and its `enabled` predicate, the footer shortcut memos, the split exit layer, the doc lines, and the test; take upstream's surrounding changes. If upstream binds `ctrl+c` to something else in the prompt, re-audit the precedence chain end to end. | | `packages/opencode/src/installation/index.ts` | Updater detects/queries/installs `codev-code` (npm-family); GitHub fallback → quickbeard/codev-code. `method()` drops upstream's `~/.local/bin` → `curl` check, keeping only `~/.opencode/bin`. The `curl` upgrade branch refuses instead of running upstream's install script: `upgradeCurl` + `upgradeScriptShell` + the `Stream` import are removed, and `case "curl"` returns `UpgradeFailedError` pointing at `npm i -g codev-code` | `codev-code upgrade` and the update notification target OUR npm package, never stock opencode. brew/choco/scoop probes never match (no such packages). A `curl`-detected binary (in `~/.opencode/bin`) is a stock opencode install; piping upstream's `opencode.ai/install` script would silently replace the fork with stock opencode. `~/.local/bin` is dropped because it's also a common npm/bun global-bin prefix, so a legitimate package-manager install there was misdetected as curl (now falls through to the manager probes). | Keep our package names, the `~/.local/bin` removal, and the curl refusal; keep `upgradeCurl`/`upgradeScriptShell`/`Stream` deleted (take upstream's changes elsewhere). | | `packages/opencode/src/cli/cmd/uninstall.ts` | npm-family uninstall commands (display + exec maps) `opencode-ai` → `codev-code` | `codev uninstall` must remove the fork's actual npm package; the old name was a silent no-op for every npm-installed fork user. NOT renamed: brew/choco/scoop package names (`opencode` — the fork publishes no such formula/package, and `method()` never resolves to those for a fork install) and the curl `.opencode/bin` + `# opencode` shell markers (they match upstream install-script artifacts, not the fork's). | Keep our `codev-code` names; take upstream's surrounding changes. | | `packages/opencode/src/config/config.ts` | `share` defaults to `"disabled"` when unset | Sharing publishes sessions to upstream's share service; internal users must opt in via config. | Keep our default block. | diff --git a/packages/tui/src/component/prompt/index.tsx b/packages/tui/src/component/prompt/index.tsx index 7c7d285653..dabc20ec05 100644 --- a/packages/tui/src/component/prompt/index.tsx +++ b/packages/tui/src/component/prompt/index.tsx @@ -175,9 +175,9 @@ export function Prompt(props: PromptProps) { const interruptSequence = useCommandShortcut("session.interrupt") const interruptNowSequence = useCommandShortcut("session.interrupt.now") const escapeInterruptShortcut = createMemo(() => (interruptSequence() === "escape" ? "esc" : interruptSequence())) - // Fork: the footer leads with the single-press ctrl+c interrupt and only falls - // back to the double-press escape one when ctrl+c is unbound. - const interruptShortcut = createMemo(() => interruptNowSequence() || escapeInterruptShortcut()) + // Fork: the footer advertises the double-press escape interrupt and only falls + // back to the single-press ctrl+c one when escape is unbound. + const interruptShortcut = createMemo(() => escapeInterruptShortcut() || interruptNowSequence()) const renderer = useRenderer() const exit = useExit() const dimensions = useTerminalDimensions() @@ -1636,7 +1636,7 @@ export function Prompt(props: PromptProps) { 0 ? theme.primary : theme.text}> - {store.interrupt > 0 ? escapeInterruptShortcut() : interruptShortcut()}{" "} + {interruptShortcut()}{" "} 0 ? theme.primary : theme.textMuted }}> {store.interrupt > 0 ? "again to interrupt" : "interrupt"} diff --git a/packages/tui/src/feature-plugins/home/tips-view.tsx b/packages/tui/src/feature-plugins/home/tips-view.tsx index de9f142185..e284e13bfa 100644 --- a/packages/tui/src/feature-plugins/home/tips-view.tsx +++ b/packages/tui/src/feature-plugins/home/tips-view.tsx @@ -121,7 +121,7 @@ export function Tips(props: { api: TuiPluginApi; connected?: boolean }) { modelCycleRecent: useCommandShortcut("model.cycle_recent"), modelList: useCommandShortcut("model.list"), sessionExport: configShortcut(props.api, "session.export"), - sessionInterrupt: configShortcut(props.api, "session.interrupt.now"), + sessionInterrupt: configShortcut(props.api, "session.interrupt"), sessionList: useCommandShortcut("session.list"), sessionNew: useCommandShortcut("session.new"), sessionParent: configShortcut(props.api, "session.parent"), diff --git a/packages/tui/test/interrupt.view.test.tsx b/packages/tui/test/interrupt.view.test.tsx index 93b8c6d0bd..e0d5a0e9ed 100644 --- a/packages/tui/test/interrupt.view.test.tsx +++ b/packages/tui/test/interrupt.view.test.tsx @@ -20,7 +20,7 @@ async function waitForFrame(setup: Awaited // The app is booted on a session the server already reports as busy, so ctrl+c // hits the running-session branch without driving a real provider turn. test("ctrl+c clears a prompt draft, then interrupts a busy session on the first press", async () => { - // Wide enough that the footer keeps "ctrl+c interrupt" on one line. + // Wide enough that the footer keeps "esc interrupt" on one line. const setup = await createTestRenderer({ width: 120, height: 30, useThread: false }) const core = await import("@opentui/core") mock.module("@opentui/core", () => ({ ...core, createCliRenderer: async () => setup.renderer })) @@ -84,8 +84,9 @@ test("ctrl+c clears a prompt draft, then interrupts a busy session on the first ) await ready - // The busy footer leads with the ctrl+c interrupt, not the escape one. - await waitForFrame(setup, (frame) => frame.includes("ctrl+c interrupt")) + // The busy footer advertises the escape interrupt; ctrl+c still works, it is + // just not the one on show. + await waitForFrame(setup, (frame) => frame.includes("esc interrupt")) // A draft takes precedence: the first ctrl+c only clears the prompt. setup.mockInput.typeText("draft")