@@ -9,6 +9,7 @@
|
||||
### Fixed
|
||||
|
||||
- Fixed missing `@sinclair/typebox` runtime dependency in `@mariozechner/pi-coding-agent`, so strict pnpm installs no longer fail with `ERR_MODULE_NOT_FOUND` when starting `pi` ([#3434](https://github.com/badlogic/pi-mono/issues/3434))
|
||||
- Fixed xterm uppercase typing in the interactive editor by decoding printable `modifyOtherKeys` input and normalizing shifted letter matching, so `Shift+letter` no longer disappears in `pi` ([#3436](https://github.com/badlogic/pi-mono/issues/3436))
|
||||
- Fixed `/compact` to reuse the session thinking level for compaction summaries instead of forcing `high`, avoiding invalid reasoning-effort errors on `github-copilot/claude-opus-4.7` sessions configured for `medium` thinking ([#3438](https://github.com/badlogic/pi-mono/issues/3438))
|
||||
- Fixed shared/exported plain-text tool output to preserve indentation instead of collapsing leading whitespace in the web share page ([#3440](https://github.com/badlogic/pi-mono/issues/3440))
|
||||
- Fixed skill resolution to dedupe symlinked aliases by canonical path, so `pi config` no longer shows duplicate skill entries when `~/.pi/agent/skills` points to `~/.agents/skills` ([#3405](https://github.com/badlogic/pi-mono/issues/3405))
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed xterm `modifyOtherKeys` printable input so shifted uppercase letters insert correctly in the editor and shifted letter bindings parse and match consistently ([#3436](https://github.com/badlogic/pi-mono/issues/3436))
|
||||
|
||||
## [0.67.68] - 2026-04-17
|
||||
|
||||
## [0.67.67] - 2026-04-17
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { AutocompleteProvider, AutocompleteSuggestions, CombinedAutocompleteProvider } from "../autocomplete.js";
|
||||
import { getKeybindings } from "../keybindings.js";
|
||||
import { decodeKittyPrintable, matchesKey } from "../keys.js";
|
||||
import { decodePrintableKey, matchesKey } from "../keys.js";
|
||||
import { KillRing } from "../kill-ring.js";
|
||||
import { type Component, CURSOR_MARKER, type Focusable, type TUI } from "../tui.js";
|
||||
import { UndoStack } from "../undo-stack.js";
|
||||
@@ -542,11 +542,12 @@ export class Editor implements Component, Focusable {
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.charCodeAt(0) >= 32) {
|
||||
const printable = decodePrintableKey(data) ?? (data.charCodeAt(0) >= 32 ? data : undefined);
|
||||
if (printable !== undefined) {
|
||||
// Printable character - perform the jump
|
||||
const direction = this.jumpMode;
|
||||
this.jumpMode = null;
|
||||
this.jumpToChar(data, direction);
|
||||
this.jumpToChar(printable, direction);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -807,9 +808,9 @@ export class Editor implements Component, Focusable {
|
||||
return;
|
||||
}
|
||||
|
||||
const kittyPrintable = decodeKittyPrintable(data);
|
||||
if (kittyPrintable !== undefined) {
|
||||
this.insertCharacter(kittyPrintable);
|
||||
const printable = decodePrintableKey(data);
|
||||
if (printable !== undefined) {
|
||||
this.insertCharacter(printable);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -357,6 +357,14 @@ function normalizeKittyFunctionalCodepoint(codepoint: number): number {
|
||||
return KITTY_FUNCTIONAL_KEY_EQUIVALENTS.get(codepoint) ?? codepoint;
|
||||
}
|
||||
|
||||
function normalizeShiftedLetterIdentityCodepoint(codepoint: number, modifier: number): number {
|
||||
const effectiveModifier = modifier & ~LOCK_MASK;
|
||||
if ((effectiveModifier & MODIFIERS.shift) !== 0 && codepoint >= 65 && codepoint <= 90) {
|
||||
return codepoint + 32;
|
||||
}
|
||||
return codepoint;
|
||||
}
|
||||
|
||||
const LEGACY_KEY_SEQUENCES = {
|
||||
up: ["\x1b[A", "\x1bOA"],
|
||||
down: ["\x1b[B", "\x1bOB"],
|
||||
@@ -651,8 +659,14 @@ function matchesKittySequence(data: string, expectedCodepoint: number, expectedM
|
||||
// Check if modifiers match
|
||||
if (actualMod !== expectedMod) return false;
|
||||
|
||||
const normalizedCodepoint = normalizeKittyFunctionalCodepoint(parsed.codepoint);
|
||||
const normalizedExpectedCodepoint = normalizeKittyFunctionalCodepoint(expectedCodepoint);
|
||||
const normalizedCodepoint = normalizeShiftedLetterIdentityCodepoint(
|
||||
normalizeKittyFunctionalCodepoint(parsed.codepoint),
|
||||
parsed.modifier,
|
||||
);
|
||||
const normalizedExpectedCodepoint = normalizeShiftedLetterIdentityCodepoint(
|
||||
normalizeKittyFunctionalCodepoint(expectedCodepoint),
|
||||
expectedModifier,
|
||||
);
|
||||
|
||||
// Primary match: codepoint matches directly after normalizing functional keys
|
||||
if (normalizedCodepoint === normalizedExpectedCodepoint) return true;
|
||||
@@ -751,7 +765,12 @@ function isDigitKey(key: string): boolean {
|
||||
|
||||
function matchesPrintableModifyOtherKeys(data: string, expectedKeycode: number, expectedModifier: number): boolean {
|
||||
if (expectedModifier === 0) return false;
|
||||
return matchesModifyOtherKeys(data, expectedKeycode, expectedModifier);
|
||||
const parsed = parseModifyOtherKeysSequence(data);
|
||||
if (!parsed || parsed.modifier !== expectedModifier) return false;
|
||||
return (
|
||||
normalizeShiftedLetterIdentityCodepoint(parsed.codepoint, parsed.modifier) ===
|
||||
normalizeShiftedLetterIdentityCodepoint(expectedKeycode, expectedModifier)
|
||||
);
|
||||
}
|
||||
|
||||
function formatKeyNameWithModifiers(keyName: string, modifier: number): string | undefined {
|
||||
@@ -1192,17 +1211,18 @@ export function matchesKey(data: string, keyId: KeyId): boolean {
|
||||
*/
|
||||
function formatParsedKey(codepoint: number, modifier: number, baseLayoutKey?: number): string | undefined {
|
||||
const normalizedCodepoint = normalizeKittyFunctionalCodepoint(codepoint);
|
||||
const identityCodepoint = normalizeShiftedLetterIdentityCodepoint(normalizedCodepoint, modifier);
|
||||
|
||||
// Use base layout key only when codepoint is not a recognized Latin
|
||||
// letter (a-z), digit (0-9), or symbol (/, -, [, ;, etc.). For those,
|
||||
// the codepoint is authoritative regardless of physical key position.
|
||||
// This prevents remapped layouts (Dvorak, Colemak, xremap, etc.) from
|
||||
// reporting the wrong key name based on the QWERTY physical position.
|
||||
const isLatinLetter = normalizedCodepoint >= 97 && normalizedCodepoint <= 122; // a-z
|
||||
const isDigit = normalizedCodepoint >= 48 && normalizedCodepoint <= 57; // 0-9
|
||||
const isKnownSymbol = SYMBOL_KEYS.has(String.fromCharCode(normalizedCodepoint));
|
||||
const isLatinLetter = identityCodepoint >= 97 && identityCodepoint <= 122; // a-z
|
||||
const isDigit = identityCodepoint >= 48 && identityCodepoint <= 57; // 0-9
|
||||
const isKnownSymbol = SYMBOL_KEYS.has(String.fromCharCode(identityCodepoint));
|
||||
const effectiveCodepoint =
|
||||
isLatinLetter || isDigit || isKnownSymbol ? normalizedCodepoint : (baseLayoutKey ?? normalizedCodepoint);
|
||||
isLatinLetter || isDigit || isKnownSymbol ? identityCodepoint : (baseLayoutKey ?? identityCodepoint);
|
||||
|
||||
let keyName: string | undefined;
|
||||
if (effectiveCodepoint === CODEPOINTS.escape) keyName = "escape";
|
||||
@@ -1360,3 +1380,21 @@ export function decodeKittyPrintable(data: string): string | undefined {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function decodeModifyOtherKeysPrintable(data: string): string | undefined {
|
||||
const parsed = parseModifyOtherKeysSequence(data);
|
||||
if (!parsed) return undefined;
|
||||
const modifier = parsed.modifier & ~LOCK_MASK;
|
||||
if ((modifier & ~MODIFIERS.shift) !== 0) return undefined;
|
||||
if (!Number.isFinite(parsed.codepoint) || parsed.codepoint < 32) return undefined;
|
||||
|
||||
try {
|
||||
return String.fromCodePoint(parsed.codepoint);
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export function decodePrintableKey(data: string): string | undefined {
|
||||
return decodeKittyPrintable(data) ?? decodeModifyOtherKeysPrintable(data);
|
||||
}
|
||||
|
||||
@@ -374,6 +374,22 @@ describe("Editor component", () => {
|
||||
|
||||
assert.strictEqual(editor.getText(), "");
|
||||
});
|
||||
|
||||
it("inserts shifted CSI-u letters as text", () => {
|
||||
const editor = new Editor(createTestTUI(), defaultEditorTheme);
|
||||
|
||||
editor.handleInput("\x1b[69;2u");
|
||||
|
||||
assert.strictEqual(editor.getText(), "E");
|
||||
});
|
||||
|
||||
it("inserts shifted xterm modifyOtherKeys letters as text", () => {
|
||||
const editor = new Editor(createTestTUI(), defaultEditorTheme);
|
||||
|
||||
editor.handleInput("\x1b[27;2;69~");
|
||||
|
||||
assert.strictEqual(editor.getText(), "E");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Unicode text editing behavior", () => {
|
||||
|
||||
@@ -4,7 +4,14 @@
|
||||
|
||||
import assert from "node:assert";
|
||||
import { describe, it } from "node:test";
|
||||
import { decodeKittyPrintable, Key, matchesKey, parseKey, setKittyProtocolActive } from "../src/keys.js";
|
||||
import {
|
||||
decodeKittyPrintable,
|
||||
decodePrintableKey,
|
||||
Key,
|
||||
matchesKey,
|
||||
parseKey,
|
||||
setKittyProtocolActive,
|
||||
} from "../src/keys.js";
|
||||
|
||||
function withEnv(name: string, value: string | undefined, fn: () => void): void {
|
||||
const previous = process.env[name];
|
||||
@@ -268,6 +275,14 @@ describe("matchesKey", () => {
|
||||
assert.strictEqual(parseKey("\x1b[27;2;49~"), "shift+1");
|
||||
});
|
||||
|
||||
it("should match xterm modifyOtherKeys shifted uppercase letters", () => {
|
||||
setKittyProtocolActive(false);
|
||||
assert.strictEqual(matchesKey("\x1b[27;2;69~", "shift+e"), true);
|
||||
assert.strictEqual(matchesKey("\x1b[27;6;69~", "ctrl+shift+e"), true);
|
||||
assert.strictEqual(parseKey("\x1b[27;2;69~"), "shift+e");
|
||||
assert.strictEqual(parseKey("\x1b[27;6;69~"), "shift+ctrl+e");
|
||||
});
|
||||
|
||||
it("should match Ctrl+Alt+letter via CSI-u when kitty inactive", () => {
|
||||
setKittyProtocolActive(false);
|
||||
assert.strictEqual(matchesKey("\x1b[104;7u", "ctrl+alt+h"), true);
|
||||
@@ -493,6 +508,16 @@ describe("decodeKittyPrintable", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("decodePrintableKey", () => {
|
||||
it("should decode printable xterm modifyOtherKeys sequences", () => {
|
||||
assert.strictEqual(decodePrintableKey("\x1b[27;2;69~"), "E");
|
||||
assert.strictEqual(decodePrintableKey("\x1b[27;2;196~"), "Ä");
|
||||
assert.strictEqual(decodePrintableKey("\x1b[27;2;32~"), " ");
|
||||
assert.strictEqual(decodePrintableKey("\x1b[27;2;13~"), undefined);
|
||||
assert.strictEqual(decodePrintableKey("\x1b[27;6;69~"), undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe("parseKey", () => {
|
||||
describe("Kitty protocol with alternate keys", () => {
|
||||
it("should return Latin key name when base layout key is present", () => {
|
||||
@@ -526,6 +551,13 @@ describe("parseKey", () => {
|
||||
setKittyProtocolActive(false);
|
||||
});
|
||||
|
||||
it("should parse shifted uppercase CSI-u letters as shift+letter", () => {
|
||||
setKittyProtocolActive(true);
|
||||
assert.strictEqual(matchesKey("\x1b[69;2u", "shift+e"), true);
|
||||
assert.strictEqual(parseKey("\x1b[69;2u"), "shift+e");
|
||||
setKittyProtocolActive(false);
|
||||
});
|
||||
|
||||
it("should ignore Kitty CSI-u with unsupported modifiers", () => {
|
||||
setKittyProtocolActive(true);
|
||||
assert.strictEqual(parseKey("\x1b[99;17u"), undefined);
|
||||
|
||||
Reference in New Issue
Block a user