From 66e17bce71d797aa8ab6f1dd61293987266a861f Mon Sep 17 00:00:00 2001 From: roboomp Date: Thu, 2 Jul 2026 16:15:50 +0000 Subject: [PATCH 1/2] fix(tui): skipped unsafe ssh keyboard fallback Skipped the xterm modifyOtherKeys fallback for unknown SSH terminals so iOS SSH clients do not lose Shift input while preserving the fallback for known/local terminals. Fixes #4325 --- packages/tui/CHANGELOG.md | 4 +++ packages/tui/src/terminal.ts | 26 +++++++++------ .../test/kitty-keyboard-da1-ordering.test.ts | 32 +++++++++++++++++++ 3 files changed, 53 insertions(+), 9 deletions(-) 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..231fe6f42 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_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..85bec45f2 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,27 @@ class ModalProbe implements Component { return ["modal"]; } } +const originalSshTty = Bun.env.SSH_TTY; +const originalSshClient = Bun.env.SSH_CLIENT; +const originalTerminalId = TERMINAL.id; + +function restoreEnv(name: "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_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 +91,22 @@ describe("ProcessTerminal kitty keyboard progressive-enhancement ordering", () = expect(out).not.toContain("\x1b[>1u"); }); + it("skips modifyOtherKeys fallback for unknown SSH terminals", async () => { + Bun.env.SSH_TTY = "/dev/pts/3"; + 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(); From d71f252cd3d4f08f0cf1f4b63eb5d7c939bf4474 Mon Sep 17 00:00:00 2001 From: roboomp Date: Thu, 2 Jul 2026 16:20:44 +0000 Subject: [PATCH 2/2] fix(tui): treated ssh connection as remote Covered SSH_CONNECTION-only sessions when skipping modifyOtherKeys for unknown SSH terminals. --- packages/tui/src/terminal.ts | 2 +- packages/tui/test/kitty-keyboard-da1-ordering.test.ts | 9 ++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/tui/src/terminal.ts b/packages/tui/src/terminal.ts index 231fe6f42..f70b454a5 100644 --- a/packages/tui/src/terminal.ts +++ b/packages/tui/src/terminal.ts @@ -36,7 +36,7 @@ export function resolveHangulCompatibilityJamoWidthFromTerminalIdentity( } function shouldEnableModifyOtherKeysFallback(env: NodeJS.ProcessEnv = Bun.env): boolean { - if (!env.SSH_TTY && !env.SSH_CLIENT) return true; + if (!env.SSH_CONNECTION && !env.SSH_TTY && !env.SSH_CLIENT) return true; return TERMINAL.id !== "base" && TERMINAL.id !== "trueColor"; } diff --git a/packages/tui/test/kitty-keyboard-da1-ordering.test.ts b/packages/tui/test/kitty-keyboard-da1-ordering.test.ts index 85bec45f2..f38a158c5 100644 --- a/packages/tui/test/kitty-keyboard-da1-ordering.test.ts +++ b/packages/tui/test/kitty-keyboard-da1-ordering.test.ts @@ -19,11 +19,12 @@ 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_TTY" | "SSH_CLIENT", value: string | undefined): void { +function restoreEnv(name: "SSH_CONNECTION" | "SSH_TTY" | "SSH_CLIENT", value: string | undefined): void { if (value === undefined) { delete Bun.env[name]; return; @@ -37,6 +38,7 @@ describe("ProcessTerminal kitty keyboard progressive-enhancement ordering", () = 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 }); @@ -91,8 +93,9 @@ describe("ProcessTerminal kitty keyboard progressive-enhancement ordering", () = expect(out).not.toContain("\x1b[>1u"); }); - it("skips modifyOtherKeys fallback for unknown SSH terminals", async () => { - Bun.env.SSH_TTY = "/dev/pts/3"; + 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);