@oh-my-pi/pi-tui 15.4.3 → 15.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/package.json +3 -3
- package/src/autocomplete.ts +33 -0
- package/src/stdin-buffer.ts +12 -0
- package/src/terminal.ts +57 -34
- package/src/utils.ts +4 -12
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [15.5.0] - 2026-05-26
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Fixed `@` file mention autocomplete stalling for seconds when the query references something outside the project root (e.g. `@../`, `@~/`, `@/abs/`). `CombinedAutocompleteProvider` now short-circuits to plain immediate-directory prefix listing in those cases instead of dispatching a recursive `fuzzyFind` walk over a sibling directory full of unrelated projects. Inside-cwd queries keep the existing fuzzy-then-prefix behavior. ([#1395](https://github.com/can1357/oh-my-pi/issues/1395))
|
|
10
|
+
- Gated the Hangul Compatibility Jamo width correction (U+3131..U+318E → 1 cell, originally landed in 15.0.1 for the IME / hardware-cursor displacement bug) behind `process.platform === "darwin"` in the TS path and `cfg!(target_os = "macos")` in the `pi-natives` Rust path. macOS terminals (Ghostty / Terminal.app / iTerm2) render jamo as 1 cell despite UAX#11 classifying them as Wide, but WezTerm and most Linux terminals honor UAX#11 and render them as 2 cells. The unconditional correction therefore desynced the TUI's column bookkeeping from the terminal's actual rendering off-darwin, producing corrupted layout and broken Korean input on Linux. On non-darwin the helpers now defer entirely to `Bun.stringWidth` / `UnicodeWidthStr` (also a small perf win on the multi-char-grapheme path). ([#1410](https://github.com/can1357/oh-my-pi/issues/1410))
|
|
11
|
+
|
|
5
12
|
## [15.4.0] - 2026-05-26
|
|
6
13
|
|
|
7
14
|
### Fixed
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-tui",
|
|
4
|
-
"version": "15.
|
|
4
|
+
"version": "15.5.1",
|
|
5
5
|
"description": "Terminal User Interface library with differential rendering for efficient text-based applications",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -37,8 +37,8 @@
|
|
|
37
37
|
"fmt": "biome format --write ."
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@oh-my-pi/pi-natives": "15.
|
|
41
|
-
"@oh-my-pi/pi-utils": "15.
|
|
40
|
+
"@oh-my-pi/pi-natives": "15.5.1",
|
|
41
|
+
"@oh-my-pi/pi-utils": "15.5.1",
|
|
42
42
|
"lru-cache": "11.3.6",
|
|
43
43
|
"marked": "^18.0.3"
|
|
44
44
|
},
|
package/src/autocomplete.ts
CHANGED
|
@@ -237,6 +237,17 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
|
|
|
237
237
|
const atPrefix = this.#extractAtPrefix(textBeforeCursor);
|
|
238
238
|
if (atPrefix) {
|
|
239
239
|
const { rawPrefix, isQuotedPrefix } = parsePathPrefix(atPrefix);
|
|
240
|
+
// Recursive fuzzy walks rooted outside the project (e.g. `@../`,
|
|
241
|
+
// `@~/`, `@/abs`) can be huge — a parent dir full of sibling
|
|
242
|
+
// projects blows past several seconds of latency. Outside cwd,
|
|
243
|
+
// fall back to plain prefix listing of the immediate directory
|
|
244
|
+
// (matches Claude Code's behavior). Inside cwd we keep the
|
|
245
|
+
// fuzzy-then-prefix flow.
|
|
246
|
+
if (rawPrefix.length > 0 && this.#isOutsideCwd(rawPrefix)) {
|
|
247
|
+
const items = await this.#getFileSuggestions(atPrefix);
|
|
248
|
+
if (items.length === 0) return null;
|
|
249
|
+
return { items, prefix: atPrefix };
|
|
250
|
+
}
|
|
240
251
|
const suggestions =
|
|
241
252
|
rawPrefix.length > 0
|
|
242
253
|
? await this.#getFuzzyFileSuggestions(rawPrefix, { isQuotedPrefix })
|
|
@@ -479,6 +490,28 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
|
|
|
479
490
|
return filePath;
|
|
480
491
|
}
|
|
481
492
|
|
|
493
|
+
// Resolve `rawPrefix` lexically (no I/O) and report whether it points
|
|
494
|
+
// somewhere outside `this.#basePath`. Used to skip recursive fuzzy walks
|
|
495
|
+
// rooted at parent / absolute / home paths — those routinely include
|
|
496
|
+
// thousands of unrelated files and stall the UI for seconds.
|
|
497
|
+
#isOutsideCwd(rawPrefix: string): boolean {
|
|
498
|
+
if (rawPrefix.length === 0) return false;
|
|
499
|
+
let target: string;
|
|
500
|
+
if (rawPrefix.startsWith("~")) {
|
|
501
|
+
target = this.#expandHomePath(rawPrefix);
|
|
502
|
+
} else if (path.isAbsolute(rawPrefix)) {
|
|
503
|
+
target = rawPrefix;
|
|
504
|
+
} else {
|
|
505
|
+
target = path.resolve(this.#basePath, rawPrefix);
|
|
506
|
+
}
|
|
507
|
+
const rel = path.relative(this.#basePath, target);
|
|
508
|
+
if (rel === "" || rel === ".") return false;
|
|
509
|
+
if (path.isAbsolute(rel)) return true;
|
|
510
|
+
const firstSep = rel.indexOf(path.sep);
|
|
511
|
+
const head = firstSep === -1 ? rel : rel.slice(0, firstSep);
|
|
512
|
+
return head === "..";
|
|
513
|
+
}
|
|
514
|
+
|
|
482
515
|
async #resolveScopedFuzzyQuery(
|
|
483
516
|
rawQuery: string,
|
|
484
517
|
): Promise<{ baseDir: string; query: string; displayBase: string } | null> {
|
package/src/stdin-buffer.ts
CHANGED
|
@@ -67,6 +67,18 @@ function isCompleteSequence(data: string): "complete" | "incomplete" | "not-esca
|
|
|
67
67
|
return afterEsc.length >= 2 ? "complete" : "incomplete";
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
+
// ESC-prefixed sequences (terminals with metaSendsEscape):
|
|
71
|
+
// Only when the inner ESC starts a CSI ('[') or SS3 ('O') sequence.
|
|
72
|
+
// Bare double-ESC (e.g. \x1b\x1bX) remains complete to avoid 10ms timeout lag.
|
|
73
|
+
if (afterEsc.startsWith(ESC)) {
|
|
74
|
+
const inner = data.slice(1);
|
|
75
|
+
const third = inner.charCodeAt(1);
|
|
76
|
+
if (third === 0x5b || third === 0x4f) {
|
|
77
|
+
return isCompleteSequence(inner);
|
|
78
|
+
}
|
|
79
|
+
return "complete";
|
|
80
|
+
}
|
|
81
|
+
|
|
70
82
|
// Meta key sequences: ESC followed by a single character
|
|
71
83
|
if (afterEsc.length === 1) {
|
|
72
84
|
return "complete";
|
package/src/terminal.ts
CHANGED
|
@@ -128,7 +128,7 @@ export class ProcessTerminal implements Terminal {
|
|
|
128
128
|
#osc11QueryQueued = false;
|
|
129
129
|
#osc11ResponseBuffer = "";
|
|
130
130
|
#privateCsiResponseBuffer = "";
|
|
131
|
-
#
|
|
131
|
+
#da1SentinelOwners: ("keyboard" | "osc11")[] = [];
|
|
132
132
|
#osc11PollTimer?: Timer;
|
|
133
133
|
#mode2031DebounceTimer?: Timer;
|
|
134
134
|
#progressTimer?: ReturnType<typeof setInterval>;
|
|
@@ -293,7 +293,7 @@ export class ProcessTerminal implements Terminal {
|
|
|
293
293
|
// events that would otherwise leak into the prompt as keystrokes. See #1238.
|
|
294
294
|
if (
|
|
295
295
|
this.#privateCsiResponseBuffer ||
|
|
296
|
-
(privateCsiPartialPattern.test(sequence) && this.#
|
|
296
|
+
(privateCsiPartialPattern.test(sequence) && this.#da1SentinelOwners.length > 0)
|
|
297
297
|
) {
|
|
298
298
|
if (this.#privateCsiResponseBuffer && sequence.startsWith("\x1b")) {
|
|
299
299
|
// New escape arrived mid-reassembly — abandon partial and re-process the new sequence.
|
|
@@ -324,41 +324,60 @@ export class ProcessTerminal implements Terminal {
|
|
|
324
324
|
}
|
|
325
325
|
}
|
|
326
326
|
|
|
327
|
-
//
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
327
|
+
// DA1 response: swallow our sentinel reply regardless of whether OSC 11
|
|
328
|
+
// already succeeded. Other terminal probes should never see these replies.
|
|
329
|
+
if (da1ResponsePattern.test(sequence) && this.#da1SentinelOwners.length > 0) {
|
|
330
|
+
const owner = this.#da1SentinelOwners.shift()!;
|
|
331
|
+
if (owner === "osc11") {
|
|
332
|
+
if (this.#osc11Pending) {
|
|
333
|
+
// DA1 arrived before the OSC 11 reply: terminal does not support OSC 11.
|
|
334
|
+
this.#osc11Pending = false;
|
|
335
|
+
this.#osc11ResponseBuffer = "";
|
|
336
|
+
}
|
|
337
|
+
// Start a queued OSC 11 query once the prior cycle is fully drained.
|
|
338
|
+
if (
|
|
339
|
+
this.#osc11QueryQueued &&
|
|
340
|
+
!this.#osc11Pending &&
|
|
341
|
+
!this.#da1SentinelOwners.includes("osc11") &&
|
|
342
|
+
!this.#dead
|
|
343
|
+
) {
|
|
344
|
+
this.#osc11QueryQueued = false;
|
|
345
|
+
this.#startOsc11Query();
|
|
346
|
+
}
|
|
347
|
+
} else {
|
|
348
|
+
// Keyboard probe sentinel: kitty reply never arrived → fall back to modifyOtherKeys.
|
|
349
|
+
if (!this.#kittyProtocolActive && !this.#modifyOtherKeysActive && this.#modifyOtherKeysTimeout) {
|
|
332
350
|
clearTimeout(this.#modifyOtherKeysTimeout);
|
|
333
351
|
this.#modifyOtherKeysTimeout = undefined;
|
|
352
|
+
this.#safeWrite("\x1b[>4;2m");
|
|
353
|
+
this.#modifyOtherKeysActive = true;
|
|
334
354
|
}
|
|
335
|
-
this.#kittyProtocolActive = true;
|
|
336
|
-
setKittyProtocolActive(true);
|
|
337
|
-
|
|
338
|
-
// Enable Kitty keyboard protocol (push flags)
|
|
339
|
-
// Flag 1 = disambiguate escape codes
|
|
340
|
-
// Flag 2 = report event types (press/repeat/release)
|
|
341
|
-
// Flag 4 = report alternate keys
|
|
342
|
-
this.#safeWrite("\x1b[>7u");
|
|
343
|
-
return; // Don't forward protocol response to TUI
|
|
344
355
|
}
|
|
356
|
+
return;
|
|
345
357
|
}
|
|
346
358
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
// DA1 arrived before OSC 11 response: terminal does not support
|
|
353
|
-
// OSC 11. Clear the pending state without starting a queued query
|
|
354
|
-
// (queued query is started below, after sentinel is consumed).
|
|
355
|
-
this.#osc11Pending = false;
|
|
356
|
-
this.#osc11ResponseBuffer = "";
|
|
359
|
+
const match = sequence.match(kittyResponsePattern);
|
|
360
|
+
if (match && !this.#modifyOtherKeysActive) {
|
|
361
|
+
if (this.#modifyOtherKeysTimeout) {
|
|
362
|
+
clearTimeout(this.#modifyOtherKeysTimeout);
|
|
363
|
+
this.#modifyOtherKeysTimeout = undefined;
|
|
357
364
|
}
|
|
358
|
-
//
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
365
|
+
// Any reply to `\x1b[?u` means the terminal speaks the kitty keyboard
|
|
366
|
+
// protocol. The reported flag value is the *current* stack-top — fresh
|
|
367
|
+
// terminals report 0 — so support is implied by the reply itself, not by
|
|
368
|
+
// the flag value. Pick the level we want; `\x1b[>Nu` pushes one frame
|
|
369
|
+
// that shutdown's single `\x1b[<u` pop balances.
|
|
370
|
+
const reportedFlags = parseInt(match[1]!, 10);
|
|
371
|
+
this.#kittyProtocolActive = true;
|
|
372
|
+
setKittyProtocolActive(true);
|
|
373
|
+
if (reportedFlags >= 3) {
|
|
374
|
+
// Already enriched (Ghostty/foot may keep flags from a parent app).
|
|
375
|
+
// Push level-2 to lock in event reporting.
|
|
376
|
+
this.#safeWrite("\x1b[>7u");
|
|
377
|
+
} else {
|
|
378
|
+
// Level 1 (disambiguate escape codes) — enough for Shift+Enter
|
|
379
|
+
// without the modifyOtherKeys fallback that caused regression #3259.
|
|
380
|
+
this.#safeWrite("\x1b[>1u");
|
|
362
381
|
}
|
|
363
382
|
return;
|
|
364
383
|
}
|
|
@@ -425,7 +444,7 @@ export class ProcessTerminal implements Terminal {
|
|
|
425
444
|
// consumed yet. Starting a new query while a DA1 is outstanding would
|
|
426
445
|
// increment the sentinel counter, and the old DA1 arrival would then
|
|
427
446
|
// prematurely clear the new query's pending state.
|
|
428
|
-
if (this.#osc11Pending || this.#
|
|
447
|
+
if (this.#osc11Pending || this.#da1SentinelOwners.includes("osc11")) {
|
|
429
448
|
this.#osc11QueryQueued = true;
|
|
430
449
|
return;
|
|
431
450
|
}
|
|
@@ -435,7 +454,7 @@ export class ProcessTerminal implements Terminal {
|
|
|
435
454
|
#startOsc11Query(): void {
|
|
436
455
|
this.#osc11Pending = true;
|
|
437
456
|
this.#osc11ResponseBuffer = "";
|
|
438
|
-
this.#
|
|
457
|
+
this.#da1SentinelOwners.push("osc11");
|
|
439
458
|
this.#safeWrite("\x1b]11;?\x07"); // OSC 11 query (BEL terminated)
|
|
440
459
|
this.#safeWrite("\x1b[c"); // DA1 sentinel
|
|
441
460
|
}
|
|
@@ -498,7 +517,11 @@ export class ProcessTerminal implements Terminal {
|
|
|
498
517
|
#queryAndEnableKittyProtocol(): void {
|
|
499
518
|
this.#setupStdinBuffer();
|
|
500
519
|
process.stdin.on("data", this.#stdinDataHandler!);
|
|
501
|
-
|
|
520
|
+
// Progressive enhancement query: CSI ?u asks the terminal for its current
|
|
521
|
+
// kitty keyboard flags (no side effect on the stack); the DA1 sentinel
|
|
522
|
+
// guarantees a reply even from terminals that ignore CSI ?u.
|
|
523
|
+
this.#da1SentinelOwners.push("keyboard");
|
|
524
|
+
this.#safeWrite("\x1b[?u\x1b[c");
|
|
502
525
|
this.#modifyOtherKeysTimeout = setTimeout(() => {
|
|
503
526
|
this.#modifyOtherKeysTimeout = undefined;
|
|
504
527
|
if (this.#kittyProtocolActive || this.#modifyOtherKeysActive) {
|
|
@@ -576,7 +599,7 @@ export class ProcessTerminal implements Terminal {
|
|
|
576
599
|
this.#osc11QueryQueued = false;
|
|
577
600
|
this.#osc11ResponseBuffer = "";
|
|
578
601
|
this.#privateCsiResponseBuffer = "";
|
|
579
|
-
this.#
|
|
602
|
+
this.#da1SentinelOwners.length = 0;
|
|
580
603
|
|
|
581
604
|
// Disable Kitty keyboard protocol if not already done by drainInput()
|
|
582
605
|
if (this.#kittyProtocolActive) {
|
package/src/utils.ts
CHANGED
|
@@ -101,6 +101,7 @@ export function visibleWidthRaw(str: string): number {
|
|
|
101
101
|
const tabWidth = getDefaultTabWidth();
|
|
102
102
|
let isPureAscii = true;
|
|
103
103
|
let jamoOvercount = 0;
|
|
104
|
+
const isMacOS = process.platform === "darwin";
|
|
104
105
|
for (let i = 0; i < str.length; i++) {
|
|
105
106
|
const code = str.charCodeAt(i);
|
|
106
107
|
if (code === 9) {
|
|
@@ -108,18 +109,9 @@ export function visibleWidthRaw(str: string): number {
|
|
|
108
109
|
} else if (code < 0x20 || code > 0x7e) {
|
|
109
110
|
isPureAscii = false;
|
|
110
111
|
// Hangul Compatibility Jamo (U+3131..U+318E) is EAW=W per UAX#11,
|
|
111
|
-
//
|
|
112
|
-
//
|
|
113
|
-
|
|
114
|
-
// correction every jamo a Korean IME emits during composition
|
|
115
|
-
// adds 1 cell of drift to `#extractCursorPosition`, displacing
|
|
116
|
-
// the hardware cursor (and therefore the IME candidate window)
|
|
117
|
-
// `N_jamo` cells past the visible glyph. Hangul Syllables
|
|
118
|
-
// (U+AC00..U+D7A3, e.g. `안`) are correctly 2 cells in both Bun
|
|
119
|
-
// and the terminal — leave those alone. The Halfwidth Hangul
|
|
120
|
-
// block (U+FFA0..U+FFDC) is already Narrow in Bun, so no
|
|
121
|
-
// correction needed there.
|
|
122
|
-
if (code >= 0x3131 && code <= 0x318e) {
|
|
112
|
+
// but macOS terminals render them as 1 cell. WezTerm and others
|
|
113
|
+
// follow UAX#11 at 2 cells. Only correct on macOS.
|
|
114
|
+
if (isMacOS && code >= 0x3131 && code <= 0x318e) {
|
|
123
115
|
jamoOvercount++;
|
|
124
116
|
}
|
|
125
117
|
}
|