Merge PR #6812: fix(tui): balance keyboard frames on emergency exit (@roboomp)

# Conflicts:
#	packages/tui/src/terminal.ts
This commit is contained in:
can1357
2026-07-28 10:59:21 +02:00
3 changed files with 31 additions and 9 deletions
+3
View File
@@ -24,6 +24,9 @@
- Fixed the multi-line prompt editor bypassing the keybindings registry for word/line delete and yank: `ctrl+backspace` (a declared default of `tui.editor.deleteWordBackward`) never fired and `keybindings.yml` remaps of `deleteWordBackward`, `deleteWordForward`, `deleteToLineStart`, `deleteToLineEnd`, `yank`, and `yankPop` were ignored, because those actions were matched with hardcoded chords instead of `keybindings.matches(...)` like cursor motion and the single-line `Input` already do ([#6782](https://github.com/can1357/oh-my-pi/issues/6782)).
- Restored the Windows Terminal raw `0x08` → `ctrl+backspace` disambiguation (`WT_SESSION` set, `SSH_*` unset) by routing the exported `matchesRawBackspace` helper through the `matchesKey`/`parseKey` seam. Remote SSH/container sessions where terminal identity is unavailable can opt in with `PI_TUI_RAW_BACKSPACE_IS_CTRL=1` ([#6782](https://github.com/can1357/oh-my-pi/issues/6782)).
- Fixed plain Backspace deleting a whole word inside tmux/GNU screen/Zellij panes launched from Windows Terminal: multiplexers inherit `WT_SESSION` but emit raw `0x08` for plain Backspace, so the automatic raw-backspace → `ctrl+backspace` heuristic misfired. The heuristic now skips multiplexer sessions (`TMUX`/`STY`/`ZELLIJ` or `TERM` starting with `tmux`/`screen`); `PI_TUI_RAW_BACKSPACE_IS_CTRL=1` remains the explicit opt-in everywhere ([#6784](https://github.com/can1357/oh-my-pi/pull/6784)).
### Fixed
- Fixed emergency exits from fullscreen overlays leaving omp's main-screen Kitty keyboard frame active, which could corrupt Arrow Up input in the terminal pane after omp exited ([#6810](https://github.com/can1357/oh-my-pi/issues/6810)).
## [17.1.4] - 2026-07-26
+7 -9
View File
@@ -275,17 +275,15 @@ export function emergencyTerminalRestore(): void {
restoreTerminalStderr();
const terminal = activeTerminal;
if (terminal) {
terminal.stop();
// stop() never touches the alternate screen — the TUI owns that
// state and exits it on the normal shutdown path. Only crash paths
// with a fullscreen overlay still hold the alt buffer here. The
// leave sequence is gated on the tracked state because it is NOT a
// universally safe no-op: Windows' VT dispatcher homes the cursor
// on DECRST 1049 even when the alt buffer is inactive.
// Keyboard enhancement state is screen-local: pop the alt-screen
// frame before leaving it, then let stop() pop omp's main-screen frame.
if (altScreenActive) {
terminal.write("\x1b[?1049l");
const keyboardExit =
terminal.keyboardEnhancementExitSequence ?? (terminal.kittyEnableSequence ? "\x1b[<u" : "");
terminal.write(`${keyboardExit}\x1b[?1049l`);
altScreenActive = false;
}
terminal.stop();
terminal.showCursor(true);
} else if (terminalEverStarted && !isTerminalHeadless()) {
// Blind restore only if we know a terminal was started but lost track of it
@@ -305,7 +303,7 @@ export function emergencyTerminalRestore(): void {
// actually holds it — on Windows, DECRST 1049 on the main
// buffer homes the cursor (unconditional CursorRestoreState
// with no prior save), corrupting the shell handoff on exit.
(altScreenActive ? "\x1b[?1049l" : "") +
(altScreenActive ? "\x1b[?1049l\x1b[?1l\x1b>\x1b[<u" : "") + // Leave alt; reset main keyboard
"\x1b[?25h", // Show cursor
);
altScreenActive = false;
@@ -92,6 +92,8 @@ describe("emergencyTerminalRestore alt-screen gating", () => {
emergencyTerminalRestore();
const firstRestore = writes.join("");
expect(firstRestore).toContain("\x1b[?1049l");
const altExit = firstRestore.indexOf("\x1b[?1049l");
expect(firstRestore.indexOf("\x1b[<u", altExit + 1)).toBeGreaterThan(altExit);
expect(firstRestore).toContain("\x1b[?1006l");
expect(firstRestore).toContain("\x1b[?1003l");
expect(firstRestore).toContain("\x1b[?1000l");
@@ -122,4 +124,23 @@ describe("emergencyTerminalRestore alt-screen gating", () => {
expect(activeRestore).toContain("\x1b[?1003l");
expect(activeRestore).toContain("\x1b[?1000l");
});
it("pops keyboard enhancement frames on both screens when crashing from a fullscreen overlay", () => {
const { terminal, writes } = startCapturedTerminal();
process.stdin.emit("data", "\x1b[?0u");
expect(terminal.kittyEnableSequence).toBe("\x1b[>1u");
terminal.write(`\x1b[?1049h${terminal.kittyEnableSequence}`);
setAltScreenActive(true);
writes.length = 0;
emergencyTerminalRestore();
const restored = writes.join("");
const altPop = restored.indexOf("\x1b[<u");
const altExit = restored.indexOf("\x1b[?1049l");
const mainPop = restored.indexOf("\x1b[<u", altExit + 1);
expect(altPop).toBeGreaterThanOrEqual(0);
expect(altPop).toBeLessThan(altExit);
expect(mainPop).toBeGreaterThan(altExit);
});
});