diff --git a/packages/tui/CHANGELOG.md b/packages/tui/CHANGELOG.md index aab3a8bfd..6ce498d68 100644 --- a/packages/tui/CHANGELOG.md +++ b/packages/tui/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- Fixed the modifyOtherKeys keyboard fallback enabling on unknown SSH terminals, avoiding broken Shift input in iOS SSH clients such as Redock ([#4325](https://github.com/can1357/oh-my-pi/issues/4325)). + ## [16.3.0] - 2026-07-02 ### Fixed diff --git a/packages/tui/src/terminal.ts b/packages/tui/src/terminal.ts index 9e0a5a753..f70b454a5 100644 --- a/packages/tui/src/terminal.ts +++ b/packages/tui/src/terminal.ts @@ -35,6 +35,11 @@ export function resolveHangulCompatibilityJamoWidthFromTerminalIdentity( return "platform"; } +function shouldEnableModifyOtherKeysFallback(env: NodeJS.ProcessEnv = Bun.env): boolean { + if (!env.SSH_CONNECTION && !env.SSH_TTY && !env.SSH_CLIENT) return true; + return TERMINAL.id !== "base" && TERMINAL.id !== "trueColor"; +} + /** * Maximum encoded UTF-8 bytes per `process.stdout.write` call on Windows. * @@ -840,13 +845,13 @@ export class ProcessTerminal implements Terminal { break; } case "keyboard": { - // Keyboard probe sentinel: kitty reply never arrived → fall back to modifyOtherKeys. - if (!this.#kittyProtocolActive && !this.#modifyOtherKeysActive && this.#modifyOtherKeysTimeout) { + // Keyboard probe sentinel: kitty reply never arrived → fall back to modifyOtherKeys + // only where the resolved terminal is known enough to tolerate it. + if (this.#modifyOtherKeysTimeout) { clearTimeout(this.#modifyOtherKeysTimeout); this.#modifyOtherKeysTimeout = undefined; - this.#safeWrite("\x1b[>4;2m"); - this.#modifyOtherKeysActive = true; } + this.#enableModifyOtherKeysFallback(); break; } case "osc99Probe": { @@ -1054,6 +1059,13 @@ export class ProcessTerminal implements Terminal { } } + #enableModifyOtherKeysFallback(): void { + if (this.#kittyProtocolActive || this.#modifyOtherKeysActive) return; + if (!shouldEnableModifyOtherKeysFallback()) return; + this.#safeWrite("\x1b[>4;2m"); + this.#modifyOtherKeysActive = true; + } + /** * Query terminal for Kitty keyboard protocol support and enable if available. * @@ -1073,11 +1085,7 @@ export class ProcessTerminal implements Terminal { this.#safeWrite("\x1b[?u\x1b[c"); this.#modifyOtherKeysTimeout = setTimeout(() => { this.#modifyOtherKeysTimeout = undefined; - if (this.#kittyProtocolActive || this.#modifyOtherKeysActive) { - return; - } - this.#safeWrite("\x1b[>4;2m"); - this.#modifyOtherKeysActive = true; + this.#enableModifyOtherKeysFallback(); }, 150); } diff --git a/packages/tui/test/kitty-keyboard-da1-ordering.test.ts b/packages/tui/test/kitty-keyboard-da1-ordering.test.ts index 94b0a899d..f38a158c5 100644 --- a/packages/tui/test/kitty-keyboard-da1-ordering.test.ts +++ b/packages/tui/test/kitty-keyboard-da1-ordering.test.ts @@ -1,5 +1,6 @@ import { afterEach, describe, expect, it } from "bun:test"; import type { Component } from "@oh-my-pi/pi-tui"; +import { TERMINAL } from "@oh-my-pi/pi-tui/terminal-capabilities"; import { createProcessTerminalRenderHarness, type ProcessTerminalRenderHarness, @@ -18,12 +19,29 @@ class ModalProbe implements Component { return ["modal"]; } } +const originalSshConnection = Bun.env.SSH_CONNECTION; +const originalSshTty = Bun.env.SSH_TTY; +const originalSshClient = Bun.env.SSH_CLIENT; +const originalTerminalId = TERMINAL.id; + +function restoreEnv(name: "SSH_CONNECTION" | "SSH_TTY" | "SSH_CLIENT", value: string | undefined): void { + if (value === undefined) { + delete Bun.env[name]; + return; + } + Bun.env[name] = value; +} + describe("ProcessTerminal kitty keyboard progressive-enhancement ordering", () => { let harness: ProcessTerminalRenderHarness | undefined; afterEach(() => { harness?.dispose(); harness = undefined; + restoreEnv("SSH_CONNECTION", originalSshConnection); + restoreEnv("SSH_TTY", originalSshTty); + restoreEnv("SSH_CLIENT", originalSshClient); + Object.defineProperty(TERMINAL, "id", { value: originalTerminalId, configurable: true }); }); it("enables kitty when the kitty reply arrives before the DA1 sentinel", async () => { @@ -75,6 +93,23 @@ describe("ProcessTerminal kitty keyboard progressive-enhancement ordering", () = expect(out).not.toContain("\x1b[>1u"); }); + it("skips modifyOtherKeys fallback for SSH_CONNECTION-only unknown terminals", async () => { + Bun.env.SSH_CONNECTION = "192.0.2.10 54321 192.0.2.20 22"; + delete Bun.env.SSH_TTY; + delete Bun.env.SSH_CLIENT; + Object.defineProperty(TERMINAL, "id", { value: "base", configurable: true }); + harness = createProcessTerminalRenderHarness(100, 30); + await harness.settle(); + harness.writes.length = 0; + + await harness.feed("\x1b[?1;2c"); + + const out = harness.writes.join(""); + expect(harness.terminal.kittyProtocolActive).toBe(false); + expect(out).not.toContain("\x1b[>4;2m"); + expect(harness.terminal.keyboardEnhancementEnterSequence).toBeNull(); + }); + it("reasserts modifyOtherKeys fallback when fullscreen overlays enter the alternate screen", async () => { harness = createProcessTerminalRenderHarness(100, 30); await harness.settle();