Skip to content

Commit 77a462c

Browse files
authored
fix(tui): default Ctrl+Z to undo on Windows (anomalyco#21138)
1 parent 9965d38 commit 77a462c

4 files changed

Lines changed: 61 additions & 2 deletions

File tree

packages/opencode/src/cli/cmd/tui/app.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -761,6 +761,7 @@ function App(props: { onSnapshot?: () => Promise<string[]> }) {
761761
keybind: "terminal_suspend",
762762
category: "System",
763763
hidden: true,
764+
enabled: tuiConfig.keybinds?.terminal_suspend !== "none",
764765
onSelect: () => {
765766
process.once("SIGCONT", () => {
766767
renderer.resume()

packages/opencode/src/cli/cmd/tui/feature-plugins/home/tips-view.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,5 +148,7 @@ const TIPS = [
148148
"Use {highlight}/review{/highlight} to review uncommitted changes, branches, or PRs",
149149
"Run {highlight}/help{/highlight} or {highlight}Ctrl+X H{/highlight} to show the help dialog",
150150
"Use {highlight}/rename{/highlight} to rename the current session",
151-
"Press {highlight}Ctrl+Z{/highlight} to suspend the terminal and return to your shell",
151+
...(process.platform === "win32"
152+
? ["Press {highlight}Ctrl+Z{/highlight} to undo changes in your prompt"]
153+
: ["Press {highlight}Ctrl+Z{/highlight} to suspend the terminal and return to your shell"]),
152154
]

packages/opencode/src/config/tui.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,15 @@ export namespace TuiConfig {
111111
}
112112
}
113113

114-
acc.result.keybinds = Config.Keybinds.parse(acc.result.keybinds ?? {})
114+
const keybinds = { ...(acc.result.keybinds ?? {}) }
115+
if (process.platform === "win32") {
116+
// Native Windows terminals do not support POSIX suspend, so prefer prompt undo.
117+
keybinds.terminal_suspend = "none"
118+
keybinds.input_undo ??= unique(["ctrl+z", ...Config.Keybinds.shape.input_undo.parse(undefined).split(",")]).join(
119+
",",
120+
)
121+
}
122+
acc.result.keybinds = Config.Keybinds.parse(keybinds)
115123

116124
const deps: Promise<void>[] = []
117125
if (acc.result.plugin?.length) {

packages/opencode/test/config/tui.test.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { Global } from "../../src/global"
99
import { Filesystem } from "../../src/util/filesystem"
1010

1111
const managedConfigDir = process.env.OPENCODE_TEST_MANAGED_CONFIG_DIR!
12+
const wintest = process.platform === "win32" ? test : test.skip
1213

1314
beforeEach(async () => {
1415
await Config.invalidate(true)
@@ -441,6 +442,53 @@ test("merges keybind overrides across precedence layers", async () => {
441442
})
442443
})
443444

445+
wintest("defaults Ctrl+Z to input undo on Windows", async () => {
446+
await using tmp = await tmpdir()
447+
448+
await Instance.provide({
449+
directory: tmp.path,
450+
fn: async () => {
451+
const config = await TuiConfig.get()
452+
expect(config.keybinds?.terminal_suspend).toBe("none")
453+
expect(config.keybinds?.input_undo).toBe("ctrl+z,ctrl+-,super+z")
454+
},
455+
})
456+
})
457+
458+
wintest("keeps explicit input undo overrides on Windows", async () => {
459+
await using tmp = await tmpdir({
460+
init: async (dir) => {
461+
await Bun.write(path.join(dir, "tui.json"), JSON.stringify({ keybinds: { input_undo: "ctrl+y" } }))
462+
},
463+
})
464+
465+
await Instance.provide({
466+
directory: tmp.path,
467+
fn: async () => {
468+
const config = await TuiConfig.get()
469+
expect(config.keybinds?.terminal_suspend).toBe("none")
470+
expect(config.keybinds?.input_undo).toBe("ctrl+y")
471+
},
472+
})
473+
})
474+
475+
wintest("ignores terminal suspend bindings on Windows", async () => {
476+
await using tmp = await tmpdir({
477+
init: async (dir) => {
478+
await Bun.write(path.join(dir, "tui.json"), JSON.stringify({ keybinds: { terminal_suspend: "alt+z" } }))
479+
},
480+
})
481+
482+
await Instance.provide({
483+
directory: tmp.path,
484+
fn: async () => {
485+
const config = await TuiConfig.get()
486+
expect(config.keybinds?.terminal_suspend).toBe("none")
487+
expect(config.keybinds?.input_undo).toBe("ctrl+z,ctrl+-,super+z")
488+
},
489+
})
490+
})
491+
444492
test("OPENCODE_TUI_CONFIG provides settings when no project config exists", async () => {
445493
await using tmp = await tmpdir({
446494
init: async (dir) => {

0 commit comments

Comments
 (0)