From e452a1fdc584daf52cde8f77e64ab34f64bb3429 Mon Sep 17 00:00:00 2001 From: khush-ubuntu-WSL Date: Mon, 19 Jan 2026 20:21:47 +0530 Subject: [PATCH 1/3] fix(core): recognize Ctrl+Backspace on Windows Terminal and WSL Windows Terminal and many terminals send \b (ASCII 8) for Ctrl+Backspace, but \x7f (ASCII 127) for regular Backspace. The parser was treating both identically, causing Ctrl+Backspace to not trigger delete-word-backward. Changes: - \x7f now correctly parses as regular backspace (ctrl: false) - \b now correctly parses as Ctrl+Backspace (ctrl: true) - \x1b\x7f parses as Alt+Backspace (meta: true) - \x1b\b parses as Alt+Ctrl+Backspace (meta: true, ctrl: true) Fixes anomalyco/opencode#6991 --- packages/core/src/lib/parse.keypress.test.ts | 38 +++++++++++++++++++- packages/core/src/lib/parse.keypress.ts | 14 +++++--- 2 files changed, 47 insertions(+), 5 deletions(-) diff --git a/packages/core/src/lib/parse.keypress.test.ts b/packages/core/src/lib/parse.keypress.test.ts index 670568201..e1959678d 100644 --- a/packages/core/src/lib/parse.keypress.test.ts +++ b/packages/core/src/lib/parse.keypress.test.ts @@ -111,7 +111,7 @@ test("parseKeypress - special keys", () => { source: "raw", }) - expect(parseKeypress("\b")).toEqual({ + expect(parseKeypress("\x7f")).toEqual({ eventType: "press", name: "backspace", ctrl: false, @@ -119,6 +119,19 @@ test("parseKeypress - special keys", () => { shift: false, option: false, number: false, + sequence: "\x7f", + raw: "\x7f", + source: "raw", + }) + + expect(parseKeypress("\b")).toEqual({ + eventType: "press", + name: "backspace", + ctrl: true, + meta: false, + shift: false, + option: false, + number: false, sequence: "\b", raw: "\b", source: "raw", @@ -752,6 +765,28 @@ test("parseKeypress - backspace key with modifiers (modifyOtherKeys format)", () expect(metaBackspace.shift).toBe(false) }) +test("parseKeypress - raw backspace sequences (Windows Terminal / WSL)", () => { + const ctrlBackspace = parseKeypress("\b")! + expect(ctrlBackspace.name).toBe("backspace") + expect(ctrlBackspace.ctrl).toBe(true) + expect(ctrlBackspace.meta).toBe(false) + + const altCtrlBackspace = parseKeypress("\x1b\b")! + expect(altCtrlBackspace.name).toBe("backspace") + expect(altCtrlBackspace.ctrl).toBe(true) + expect(altCtrlBackspace.meta).toBe(true) + + const regularBackspace = parseKeypress("\x7f")! + expect(regularBackspace.name).toBe("backspace") + expect(regularBackspace.ctrl).toBe(false) + expect(regularBackspace.meta).toBe(false) + + const altBackspace = parseKeypress("\x1b\x7f")! + expect(altBackspace.name).toBe("backspace") + expect(altBackspace.ctrl).toBe(false) + expect(altBackspace.meta).toBe(true) +}) + test("parseKeypress - backspace key with modifiers (Kitty keyboard protocol)", () => { // Backspace key in Kitty protocol uses code 127 // Ctrl+Backspace: \x1b[127;5u @@ -1420,6 +1455,7 @@ test("parseKeypress - does not filter valid key sequences that might look simila const backspace = parseKeypress("\b") expect(backspace).not.toBeNull() expect(backspace?.name).toBe("backspace") + expect(backspace?.ctrl).toBe(true) const backspace2 = parseKeypress("\x7f") expect(backspace2).not.toBeNull() diff --git a/packages/core/src/lib/parse.keypress.ts b/packages/core/src/lib/parse.keypress.ts index a11f91cf3..15a803d10 100644 --- a/packages/core/src/lib/parse.keypress.ts +++ b/packages/core/src/lib/parse.keypress.ts @@ -267,11 +267,17 @@ export const parseKeypress = (s: Buffer | string = "", options: ParseKeypressOpt } else if (s === "\t") { // tab key.name = "tab" - } else if (s === "\b" || s === "\x1b\b" || s === "\x7f" || s === "\x1b\x7f") { - // backspace or ctrl+h - // On OSX, \x7f is also backspace + } else if (s === "\x7f" || s === "\x1b\x7f") { + // Regular backspace (\x7f = ASCII 127 DEL) + // On macOS and most terminals, regular Backspace sends \x7f key.name = "backspace" - key.meta = s.charAt(0) === "\x1b" + key.meta = s.length === 2 // \x1b\x7f = Alt+Backspace + } else if (s === "\b" || s === "\x1b\b") { + // Ctrl+Backspace sends \b (ASCII 8, same as Ctrl+H) on Windows Terminal and many terminals + // \x1b\b = Alt+Ctrl+Backspace + key.name = "backspace" + key.ctrl = true + key.meta = s.length === 2 } else if (s === "\x1b" || s === "\x1b\x1b") { // escape key key.name = "escape" From 18363d75be7108163afec5774b7193ef1b4979c7 Mon Sep 17 00:00:00 2001 From: khush-ubuntu-WSL Date: Tue, 20 Jan 2026 11:10:58 +0530 Subject: [PATCH 2/3] fix(core): use platform detection for backspace handling On Windows, \b (ASCII 8) is Ctrl+Backspace. On Unix/macOS, \b (ASCII 8) is Ctrl+H. This preserves backwards compatibility on macOS/Linux while enabling Ctrl+Backspace word deletion on Windows Terminal/WSL. --- packages/core/src/lib/parse.keypress.test.ts | 62 ++++++++++++-------- packages/core/src/lib/parse.keypress.ts | 21 +++++-- 2 files changed, 55 insertions(+), 28 deletions(-) diff --git a/packages/core/src/lib/parse.keypress.test.ts b/packages/core/src/lib/parse.keypress.test.ts index e1959678d..f1f964656 100644 --- a/packages/core/src/lib/parse.keypress.test.ts +++ b/packages/core/src/lib/parse.keypress.test.ts @@ -124,18 +124,20 @@ test("parseKeypress - special keys", () => { source: "raw", }) - expect(parseKeypress("\b")).toEqual({ - eventType: "press", - name: "backspace", - ctrl: true, - meta: false, - shift: false, - option: false, - number: false, - sequence: "\b", - raw: "\b", - source: "raw", - }) + const backspaceB = parseKeypress("\b")! + expect(backspaceB.ctrl).toBe(true) + expect(backspaceB.meta).toBe(false) + expect(backspaceB.shift).toBe(false) + expect(backspaceB.option).toBe(false) + expect(backspaceB.number).toBe(false) + expect(backspaceB.sequence).toBe("\b") + expect(backspaceB.raw).toBe("\b") + expect(backspaceB.source).toBe("raw") + if (process.platform === "win32") { + expect(backspaceB.name).toBe("backspace") + } else { + expect(backspaceB.name).toBe("h") + } expect(parseKeypress("\x1b")).toEqual({ name: "escape", @@ -765,16 +767,26 @@ test("parseKeypress - backspace key with modifiers (modifyOtherKeys format)", () expect(metaBackspace.shift).toBe(false) }) -test("parseKeypress - raw backspace sequences (Windows Terminal / WSL)", () => { - const ctrlBackspace = parseKeypress("\b")! - expect(ctrlBackspace.name).toBe("backspace") - expect(ctrlBackspace.ctrl).toBe(true) - expect(ctrlBackspace.meta).toBe(false) - - const altCtrlBackspace = parseKeypress("\x1b\b")! - expect(altCtrlBackspace.name).toBe("backspace") - expect(altCtrlBackspace.ctrl).toBe(true) - expect(altCtrlBackspace.meta).toBe(true) +test("parseKeypress - raw backspace sequences (platform-aware)", () => { + const ctrlHOrBackspace = parseKeypress("\b")! + if (process.platform === "win32") { + expect(ctrlHOrBackspace.name).toBe("backspace") + expect(ctrlHOrBackspace.ctrl).toBe(true) + } else { + expect(ctrlHOrBackspace.name).toBe("h") + expect(ctrlHOrBackspace.ctrl).toBe(true) + } + expect(ctrlHOrBackspace.meta).toBe(false) + + const altCtrlHOrBackspace = parseKeypress("\x1b\b")! + if (process.platform === "win32") { + expect(altCtrlHOrBackspace.name).toBe("backspace") + expect(altCtrlHOrBackspace.ctrl).toBe(true) + } else { + expect(altCtrlHOrBackspace.name).toBe("h") + expect(altCtrlHOrBackspace.ctrl).toBe(true) + } + expect(altCtrlHOrBackspace.meta).toBe(true) const regularBackspace = parseKeypress("\x7f")! expect(regularBackspace.name).toBe("backspace") @@ -1454,7 +1466,11 @@ test("parseKeypress - does not filter valid key sequences that might look simila const backspace = parseKeypress("\b") expect(backspace).not.toBeNull() - expect(backspace?.name).toBe("backspace") + if (process.platform === "win32") { + expect(backspace?.name).toBe("backspace") + } else { + expect(backspace?.name).toBe("h") + } expect(backspace?.ctrl).toBe(true) const backspace2 = parseKeypress("\x7f") diff --git a/packages/core/src/lib/parse.keypress.ts b/packages/core/src/lib/parse.keypress.ts index 15a803d10..2bd2ebd73 100644 --- a/packages/core/src/lib/parse.keypress.ts +++ b/packages/core/src/lib/parse.keypress.ts @@ -273,11 +273,22 @@ export const parseKeypress = (s: Buffer | string = "", options: ParseKeypressOpt key.name = "backspace" key.meta = s.length === 2 // \x1b\x7f = Alt+Backspace } else if (s === "\b" || s === "\x1b\b") { - // Ctrl+Backspace sends \b (ASCII 8, same as Ctrl+H) on Windows Terminal and many terminals - // \x1b\b = Alt+Ctrl+Backspace - key.name = "backspace" - key.ctrl = true - key.meta = s.length === 2 + // \b (ASCII 8) has different meanings depending on platform: + // - Windows Terminal: Ctrl+Backspace sends \b → should be "backspace" with ctrl=true + // - Unix/macOS: \b = Ctrl+H → should be "h" with ctrl=true (handled by generic ctrl+letter below) + // We use platform detection to maintain backwards compatibility + if (process.platform === "win32") { + key.name = "backspace" + key.ctrl = true + key.meta = s.length === 2 // \x1b\b = Alt+Ctrl+Backspace + } else { + // On non-Windows, treat \b as Ctrl+H (traditional Unix behavior) + // This falls through to the ctrl+letter handler below by NOT matching here + // But since we're in an else-if, we need to handle it explicitly + key.name = "h" + key.ctrl = true + key.meta = s.length === 2 // \x1b\b = Alt+Ctrl+H + } } else if (s === "\x1b" || s === "\x1b\x1b") { // escape key key.name = "escape" From 91d10f2004ac4117f8a8c8f25d8d126c697540c1 Mon Sep 17 00:00:00 2001 From: khush-ubuntu-WSL Date: Tue, 20 Jan 2026 11:23:33 +0530 Subject: [PATCH 3/3] fix(core): detect WSL environment for Ctrl+Backspace handling WSL reports process.platform as 'linux' but Windows Terminal still sends Windows-style key sequences. Added isWindowsTerminal() helper that checks for WSL_DISTRO_NAME and WSL_INTEROP environment variables. --- packages/core/src/lib/parse.keypress.test.ts | 15 ++++++++--- packages/core/src/lib/parse.keypress.ts | 28 +++++++++++++------- 2 files changed, 29 insertions(+), 14 deletions(-) diff --git a/packages/core/src/lib/parse.keypress.test.ts b/packages/core/src/lib/parse.keypress.test.ts index f1f964656..4298c82de 100644 --- a/packages/core/src/lib/parse.keypress.test.ts +++ b/packages/core/src/lib/parse.keypress.test.ts @@ -2,6 +2,13 @@ import { test, expect } from "bun:test" import { parseKeypress, nonAlphanumericKeys, type ParsedKey, type KeyEventType } from "./parse.keypress" import { Buffer } from "node:buffer" +const isWindowsTerminal = (): boolean => { + if (process.platform === "win32") return true + if (process.env.WSL_DISTRO_NAME) return true + if (process.env.WSL_INTEROP) return true + return false +} + test("parseKeypress - basic letters", () => { expect(parseKeypress("a")).toEqual({ name: "a", @@ -133,7 +140,7 @@ test("parseKeypress - special keys", () => { expect(backspaceB.sequence).toBe("\b") expect(backspaceB.raw).toBe("\b") expect(backspaceB.source).toBe("raw") - if (process.platform === "win32") { + if (isWindowsTerminal()) { expect(backspaceB.name).toBe("backspace") } else { expect(backspaceB.name).toBe("h") @@ -769,7 +776,7 @@ test("parseKeypress - backspace key with modifiers (modifyOtherKeys format)", () test("parseKeypress - raw backspace sequences (platform-aware)", () => { const ctrlHOrBackspace = parseKeypress("\b")! - if (process.platform === "win32") { + if (isWindowsTerminal()) { expect(ctrlHOrBackspace.name).toBe("backspace") expect(ctrlHOrBackspace.ctrl).toBe(true) } else { @@ -779,7 +786,7 @@ test("parseKeypress - raw backspace sequences (platform-aware)", () => { expect(ctrlHOrBackspace.meta).toBe(false) const altCtrlHOrBackspace = parseKeypress("\x1b\b")! - if (process.platform === "win32") { + if (isWindowsTerminal()) { expect(altCtrlHOrBackspace.name).toBe("backspace") expect(altCtrlHOrBackspace.ctrl).toBe(true) } else { @@ -1466,7 +1473,7 @@ test("parseKeypress - does not filter valid key sequences that might look simila const backspace = parseKeypress("\b") expect(backspace).not.toBeNull() - if (process.platform === "win32") { + if (isWindowsTerminal()) { expect(backspace?.name).toBe("backspace") } else { expect(backspace?.name).toBe("h") diff --git a/packages/core/src/lib/parse.keypress.ts b/packages/core/src/lib/parse.keypress.ts index 2bd2ebd73..fd11a6a11 100644 --- a/packages/core/src/lib/parse.keypress.ts +++ b/packages/core/src/lib/parse.keypress.ts @@ -106,6 +106,20 @@ const isCtrlKey = (code: string) => { return ["Oa", "Ob", "Oc", "Od", "Oe", "[2^", "[3^", "[5^", "[6^", "[7^", "[8^"].includes(code) } +/** + * Detect if running on Windows or in WSL (Windows Subsystem for Linux). + * In WSL, process.platform returns 'linux' but the terminal emulator (Windows Terminal) + * still sends Windows-style key sequences like \b for Ctrl+Backspace. + */ +const isWindowsTerminal = (): boolean => { + if (process.platform === "win32") return true + // Check for WSL via environment variable (more reliable than reading /proc/version) + if (process.env.WSL_DISTRO_NAME) return true + // Also check for WSL_INTEROP which is set in WSL2 + if (process.env.WSL_INTEROP) return true + return false +} + export type KeyEventType = "press" | "repeat" | "release" export interface ParsedKey { @@ -273,21 +287,15 @@ export const parseKeypress = (s: Buffer | string = "", options: ParseKeypressOpt key.name = "backspace" key.meta = s.length === 2 // \x1b\x7f = Alt+Backspace } else if (s === "\b" || s === "\x1b\b") { - // \b (ASCII 8) has different meanings depending on platform: - // - Windows Terminal: Ctrl+Backspace sends \b → should be "backspace" with ctrl=true - // - Unix/macOS: \b = Ctrl+H → should be "h" with ctrl=true (handled by generic ctrl+letter below) - // We use platform detection to maintain backwards compatibility - if (process.platform === "win32") { + // \b (ASCII 8): Windows Terminal sends this for Ctrl+Backspace, Unix terminals for Ctrl+H + if (isWindowsTerminal()) { key.name = "backspace" key.ctrl = true - key.meta = s.length === 2 // \x1b\b = Alt+Ctrl+Backspace + key.meta = s.length === 2 } else { - // On non-Windows, treat \b as Ctrl+H (traditional Unix behavior) - // This falls through to the ctrl+letter handler below by NOT matching here - // But since we're in an else-if, we need to handle it explicitly key.name = "h" key.ctrl = true - key.meta = s.length === 2 // \x1b\b = Alt+Ctrl+H + key.meta = s.length === 2 } } else if (s === "\x1b" || s === "\x1b\x1b") { // escape key