@oh-my-pi/pi-tui 15.5.15 → 15.6.0
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 +9 -0
- package/package.json +5 -5
- package/src/components/editor.ts +25 -0
- package/src/tui.ts +21 -15
- package/src/utils.ts +39 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [15.6.0] - 2026-05-30
|
|
6
|
+
### Added
|
|
7
|
+
|
|
8
|
+
- Added autocomplete triggering for internal URL scheme tokens such as `local://` and `skill://` while typing in the editor
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- Fixed streaming output staying invisible in Windows Terminal + WSL2 until the window was minimized + restored. The 15.5.14 WSL branch of `requiresNativeViewportProofForReplay` treated an unknown native viewport state as "scrolled into history" — but `ProcessTerminal.isNativeViewportAtBottom` can only return a real answer through `kernel32.dll` FFI, which a Linux user-space process inside WSL cannot load, so the probe was permanently `undefined`. Every row-inserting structural mutation (each new streaming token row above the bottom-anchored prompt) was therefore classified as `deferredMutation` and emitted zero bytes. Any geometry change (resize/minimize/restore) bypassed the gate via a different render intent, which is why the output became visible only on window resize. The WSL clause is removed; on platforms where the probe cannot answer, unknown is treated as at-bottom (the pre-15.5.14 behaviour) so the live render path runs again. Native Win32 keeps the conservative "assume scrolled when unknown" heuristic since `kernel32` FFI does succeed there and unknown means the probe transiently failed. ([#1534](https://github.com/can1357/oh-my-pi/issues/1534))
|
|
13
|
+
|
|
5
14
|
## [15.5.14] - 2026-05-29
|
|
6
15
|
|
|
7
16
|
### Added
|
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.6.0",
|
|
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,10 +37,10 @@
|
|
|
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.
|
|
42
|
-
"lru-cache": "11.
|
|
43
|
-
"marked": "^18.0.
|
|
40
|
+
"@oh-my-pi/pi-natives": "15.6.0",
|
|
41
|
+
"@oh-my-pi/pi-utils": "15.6.0",
|
|
42
|
+
"lru-cache": "11.5.1",
|
|
43
|
+
"marked": "^18.0.4"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
46
|
"chalk": "^5.6.2",
|
package/src/components/editor.ts
CHANGED
|
@@ -1571,6 +1571,10 @@ export class Editor implements Component, Focusable {
|
|
|
1571
1571
|
else if (textBeforeCursor.match(/(?:^|[\s([{>]):[a-zA-Z0-9_+-]*$/)) {
|
|
1572
1572
|
this.#tryTriggerAutocomplete();
|
|
1573
1573
|
}
|
|
1574
|
+
// Check if we're typing an internal URL scheme (e.g. local://, skill://)
|
|
1575
|
+
else if (this.#textTriggersUrlAutocomplete(textBeforeCursor)) {
|
|
1576
|
+
this.#tryTriggerAutocomplete();
|
|
1577
|
+
}
|
|
1574
1578
|
}
|
|
1575
1579
|
} else {
|
|
1576
1580
|
this.#debouncedUpdateAutocomplete();
|
|
@@ -1762,6 +1766,10 @@ export class Editor implements Component, Focusable {
|
|
|
1762
1766
|
else if (textBeforeCursor.match(/#[^\s#]*$/)) {
|
|
1763
1767
|
this.#tryTriggerAutocomplete();
|
|
1764
1768
|
}
|
|
1769
|
+
// internal URL scheme context (e.g. local://, skill://)
|
|
1770
|
+
else if (this.#textTriggersUrlAutocomplete(textBeforeCursor)) {
|
|
1771
|
+
this.#tryTriggerAutocomplete();
|
|
1772
|
+
}
|
|
1765
1773
|
}
|
|
1766
1774
|
}
|
|
1767
1775
|
|
|
@@ -1913,6 +1921,8 @@ export class Editor implements Component, Focusable {
|
|
|
1913
1921
|
this.#tryTriggerAutocomplete();
|
|
1914
1922
|
} else if (textBeforeCursor.match(/#[^\s#]*$/)) {
|
|
1915
1923
|
this.#tryTriggerAutocomplete();
|
|
1924
|
+
} else if (this.#textTriggersUrlAutocomplete(textBeforeCursor)) {
|
|
1925
|
+
this.#tryTriggerAutocomplete();
|
|
1916
1926
|
}
|
|
1917
1927
|
}
|
|
1918
1928
|
}
|
|
@@ -2232,6 +2242,10 @@ export class Editor implements Component, Focusable {
|
|
|
2232
2242
|
else if (textBeforeCursor.match(/#[^\s#]*$/)) {
|
|
2233
2243
|
this.#tryTriggerAutocomplete();
|
|
2234
2244
|
}
|
|
2245
|
+
// internal URL scheme context (e.g. local://, skill://)
|
|
2246
|
+
else if (this.#textTriggersUrlAutocomplete(textBeforeCursor)) {
|
|
2247
|
+
this.#tryTriggerAutocomplete();
|
|
2248
|
+
}
|
|
2235
2249
|
}
|
|
2236
2250
|
}
|
|
2237
2251
|
|
|
@@ -2463,6 +2477,17 @@ export class Editor implements Component, Focusable {
|
|
|
2463
2477
|
}
|
|
2464
2478
|
|
|
2465
2479
|
// Autocomplete methods
|
|
2480
|
+
/**
|
|
2481
|
+
* Whether the text ending at the cursor looks like a `scheme://` URL token.
|
|
2482
|
+
* Generic by design: any scheme triggers a suggestion fetch and the active
|
|
2483
|
+
* provider decides whether it has candidates (returning none is a no-op).
|
|
2484
|
+
* MUST stay in sync with the token grammar in coding-agent's
|
|
2485
|
+
* `internal-url-autocomplete.ts`.
|
|
2486
|
+
*/
|
|
2487
|
+
#textTriggersUrlAutocomplete(textBeforeCursor: string): boolean {
|
|
2488
|
+
return /(?:^|[\s"'`(<=])[a-z][a-z0-9+.-]*:\/{1,2}[^\s"'`()<>]*$/i.test(textBeforeCursor);
|
|
2489
|
+
}
|
|
2490
|
+
|
|
2466
2491
|
async #tryTriggerAutocomplete(explicitTab: boolean = false): Promise<void> {
|
|
2467
2492
|
if (!this.#autocompleteProvider) return;
|
|
2468
2493
|
// Check if we should trigger file completion on Tab
|
package/src/tui.ts
CHANGED
|
@@ -154,15 +154,6 @@ function isMultiplexerSession(): boolean {
|
|
|
154
154
|
return Boolean(Bun.env.TMUX || Bun.env.STY || Bun.env.ZELLIJ);
|
|
155
155
|
}
|
|
156
156
|
|
|
157
|
-
function requiresNativeViewportProofForReplay(): boolean {
|
|
158
|
-
return (
|
|
159
|
-
process.platform === "win32" ||
|
|
160
|
-
(process.platform === "linux" &&
|
|
161
|
-
Boolean(Bun.env.WT_SESSION) &&
|
|
162
|
-
Boolean(Bun.env.WSL_DISTRO_NAME || Bun.env.WSL_INTEROP))
|
|
163
|
-
);
|
|
164
|
-
}
|
|
165
|
-
|
|
166
157
|
/**
|
|
167
158
|
* Options for overlay positioning and sizing.
|
|
168
159
|
* Values can be absolute numbers or percentage strings (e.g., "50%").
|
|
@@ -1315,10 +1306,28 @@ export class TUI extends Container {
|
|
|
1315
1306
|
const pureAppend = diff.appendedLines && diff.firstChanged === this.#previousLines.length;
|
|
1316
1307
|
const structuralMutation = newLines.length !== this.#previousLines.length || diff.firstChanged < prevViewportTop;
|
|
1317
1308
|
if (!pureAppend && structuralMutation && !isMultiplexerSession()) {
|
|
1318
|
-
|
|
1309
|
+
const nativeViewportAtBottom = this.#readNativeViewportAtBottom();
|
|
1310
|
+
if (this.#nativeViewportIsScrolled(nativeViewportAtBottom)) {
|
|
1319
1311
|
this.#markNativeScrollbackDirty();
|
|
1320
1312
|
return { kind: "deferredMutation" };
|
|
1321
1313
|
}
|
|
1314
|
+
// Expanding a collapsed offscreen cell inserts rows before an unchanged
|
|
1315
|
+
// suffix. A viewport-only repaint makes the live bottom look correct but
|
|
1316
|
+
// leaves native scrollback holding the old collapsed rows; scrolling up then
|
|
1317
|
+
// shows a splice of stale history and the new tail. Pure tail appends with an
|
|
1318
|
+
// offscreen status/header tick are still handled by the append-tail path.
|
|
1319
|
+
if (
|
|
1320
|
+
contentGrew &&
|
|
1321
|
+
diff.firstChanged < prevViewportTop &&
|
|
1322
|
+
this.#canReplayNativeScrollbackAtCheckpoint(nativeViewportAtBottom, false)
|
|
1323
|
+
) {
|
|
1324
|
+
const appendedTailStart = diff.appendedLines ? this.#findAppendedTailStart(newLines) : newLines.length;
|
|
1325
|
+
const tailAppendCount = newLines.length - appendedTailStart;
|
|
1326
|
+
const addedCount = newLines.length - this.#previousLines.length;
|
|
1327
|
+
if (addedCount > tailAppendCount) {
|
|
1328
|
+
return { kind: "historyRebuild" };
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1322
1331
|
}
|
|
1323
1332
|
|
|
1324
1333
|
// Height changes shift the visible window. Repaint when content didn't
|
|
@@ -1422,10 +1431,7 @@ export class TUI extends Container {
|
|
|
1422
1431
|
}
|
|
1423
1432
|
|
|
1424
1433
|
#nativeViewportIsScrolled(nativeViewportAtBottom: boolean | undefined): boolean {
|
|
1425
|
-
return (
|
|
1426
|
-
nativeViewportAtBottom === false ||
|
|
1427
|
-
(nativeViewportAtBottom === undefined && requiresNativeViewportProofForReplay())
|
|
1428
|
-
);
|
|
1434
|
+
return nativeViewportAtBottom === false || (nativeViewportAtBottom === undefined && process.platform === "win32");
|
|
1429
1435
|
}
|
|
1430
1436
|
|
|
1431
1437
|
#nativeViewportIsAtBottom(nativeViewportAtBottom: boolean | undefined): boolean {
|
|
@@ -1438,7 +1444,7 @@ export class TUI extends Container {
|
|
|
1438
1444
|
): boolean {
|
|
1439
1445
|
return (
|
|
1440
1446
|
nativeViewportAtBottom === true ||
|
|
1441
|
-
(nativeViewportAtBottom === undefined && (allowUnknownViewport ||
|
|
1447
|
+
(nativeViewportAtBottom === undefined && (allowUnknownViewport || process.platform !== "win32"))
|
|
1442
1448
|
);
|
|
1443
1449
|
}
|
|
1444
1450
|
|
package/src/utils.ts
CHANGED
|
@@ -85,6 +85,44 @@ export function padding(n: number): string {
|
|
|
85
85
|
// Grapheme segmenter (shared instance)
|
|
86
86
|
const segmenter = new Intl.Segmenter(undefined, { granularity: "grapheme" });
|
|
87
87
|
|
|
88
|
+
const EXTENDED_PICTOGRAPHIC_REGEX = /\p{Extended_Pictographic}/u;
|
|
89
|
+
|
|
90
|
+
// Matches CSI (`\x1b[…`) and OSC (`\x1b]…` terminated by BEL/ST) escape
|
|
91
|
+
// sequences. Mirrors the standard ansi-regex coverage so visible-span
|
|
92
|
+
// segmentation lines up with the native ANSI scanner.
|
|
93
|
+
const ANSI_ESCAPE_REGEX =
|
|
94
|
+
/[\u001b\u009b][[\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\d/#&.:=?%@~_]+)*|[a-zA-Z\d]+(?:;[-a-zA-Z\d/#&.:=?%@~_]*)*)?\u0007)|(?:(?:\d{1,4}(?:;\d{0,4})*)?[\dA-PR-TZcf-nq-uy=><~]))/g;
|
|
95
|
+
|
|
96
|
+
function pictographicSpanWidth(span: string): number {
|
|
97
|
+
let width = 0;
|
|
98
|
+
for (const { segment } of segmenter.segment(span)) {
|
|
99
|
+
width += EXTENDED_PICTOGRAPHIC_REGEX.test(segment) ? 2 : nativeVisibleWidth(segment, getDefaultTabWidth());
|
|
100
|
+
}
|
|
101
|
+
return width;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Width fallback for strings that mix ANSI styling with ZWJ pictographic
|
|
105
|
+
// emoji. `Intl.Segmenter` would split an escape sequence into individual
|
|
106
|
+
// graphemes, so the native scanner (which only skips ANSI when handed the
|
|
107
|
+
// complete sequence) double-counts the printable SGR bytes. Excise the ANSI
|
|
108
|
+
// spans first — they contribute zero cells — and apply the pictographic
|
|
109
|
+
// grapheme override only to the visible spans, then sum.
|
|
110
|
+
function visibleWidthByGrapheme(str: string): number {
|
|
111
|
+
let width = 0;
|
|
112
|
+
let lastIndex = 0;
|
|
113
|
+
ANSI_ESCAPE_REGEX.lastIndex = 0;
|
|
114
|
+
for (let match = ANSI_ESCAPE_REGEX.exec(str); match !== null; match = ANSI_ESCAPE_REGEX.exec(str)) {
|
|
115
|
+
if (match.index > lastIndex) {
|
|
116
|
+
width += pictographicSpanWidth(str.slice(lastIndex, match.index));
|
|
117
|
+
}
|
|
118
|
+
lastIndex = ANSI_ESCAPE_REGEX.lastIndex;
|
|
119
|
+
}
|
|
120
|
+
if (lastIndex < str.length) {
|
|
121
|
+
width += lastIndex === 0 ? pictographicSpanWidth(str) : pictographicSpanWidth(str.slice(lastIndex));
|
|
122
|
+
}
|
|
123
|
+
return width;
|
|
124
|
+
}
|
|
125
|
+
|
|
88
126
|
/**
|
|
89
127
|
* Get the shared grapheme segmenter instance.
|
|
90
128
|
*/
|
|
@@ -104,7 +142,7 @@ export function visibleWidthRaw(str: string): number {
|
|
|
104
142
|
for (let i = 0; i < str.length; i++) {
|
|
105
143
|
const code = str.charCodeAt(i);
|
|
106
144
|
if (code < 0x20 || code > 0x7e) {
|
|
107
|
-
return nativeVisibleWidth(str, getDefaultTabWidth());
|
|
145
|
+
return str.includes("\u200d") ? visibleWidthByGrapheme(str) : nativeVisibleWidth(str, getDefaultTabWidth());
|
|
108
146
|
}
|
|
109
147
|
}
|
|
110
148
|
return str.length;
|