@oh-my-pi/pi-tui 18.1.11 → 18.1.13
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 +8 -0
- package/dist/types/autocomplete.d.ts +7 -0
- package/dist/types/mouse.d.ts +6 -1
- package/package.json +3 -3
- package/src/autocomplete.ts +14 -2
- package/src/components/editor.ts +9 -4
- package/src/mouse.ts +7 -2
- package/src/terminal-capabilities.ts +62 -3
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [18.1.12] - 2026-09-06
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Fixed notifications never arriving in a Herdr pane. Herdr multiplexes panes like tmux but swallows bare OSC 9 / OSC 99 and has no passthrough envelope, so a backgrounded pane got no signal at all; delivery now goes through `herdr notification show` (a waiting question or an error rings `request`, a settled turn rings `done`), and the in-band write stays as the fallback when the pane id or the `herdr` binary is missing.
|
|
10
|
+
- Avoid inserting a trailing space when auto-completing directory paths with `@`, and keep autocomplete open when accepting a directory with Tab or Enter.
|
|
11
|
+
- Horizontal wheel reports (the sideways drift of a two-finger trackpad scroll) no longer decode as a vertical wheel direction, so fullscreen selectors such as `/copy` and the rewind picker stop jumping up and back down at the end of a scroll gesture.
|
|
12
|
+
|
|
5
13
|
## [18.1.9] - 2026-09-04
|
|
6
14
|
|
|
7
15
|
### Added
|
|
@@ -6,6 +6,13 @@
|
|
|
6
6
|
*/
|
|
7
7
|
export declare function findLeadingSlashCommandStart(text: string): number | null;
|
|
8
8
|
export declare function findTrailingSlashCommandStart(text: string): number | null;
|
|
9
|
+
/**
|
|
10
|
+
* Whether an autocomplete value represents a directory: trailing slash or
|
|
11
|
+
* backslash, optionally followed by a closing quote for quoted paths.
|
|
12
|
+
* Shared by the provider suffix logic and the editor chain-on-accept
|
|
13
|
+
* behavior so Tab and Enter acceptance stay in sync.
|
|
14
|
+
*/
|
|
15
|
+
export declare function isDirectoryCompletionValue(value: string): boolean;
|
|
9
16
|
export interface AutocompleteItem {
|
|
10
17
|
value: string;
|
|
11
18
|
label: string;
|
package/dist/types/mouse.d.ts
CHANGED
|
@@ -17,7 +17,12 @@ export interface SgrMouseEvent {
|
|
|
17
17
|
row: number;
|
|
18
18
|
/** True for a release report (`m` suffix). */
|
|
19
19
|
release: boolean;
|
|
20
|
-
/**
|
|
20
|
+
/**
|
|
21
|
+
* Vertical wheel direction: -1 up, 1 down, null when not a vertical wheel
|
|
22
|
+
* event. Horizontal wheel reports (buttons 66/67, the sideways drift of a
|
|
23
|
+
* two-finger trackpad scroll) are not a direction: treating them as one
|
|
24
|
+
* scrolled the selectors up and back down at the end of a gesture.
|
|
25
|
+
*/
|
|
21
26
|
wheel: -1 | 1 | null;
|
|
22
27
|
/** True when the pointer moved (hover or drag) rather than clicked. */
|
|
23
28
|
motion: boolean;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-tui",
|
|
4
|
-
"version": "18.1.
|
|
4
|
+
"version": "18.1.13",
|
|
5
5
|
"description": "Terminal User Interface library with differential rendering for efficient text-based applications",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": "Stencil Labs, Inc.",
|
|
@@ -37,8 +37,8 @@
|
|
|
37
37
|
"fmt": "oxfmt --no-error-on-unmatched-pattern 'src/**/*.{ts,tsx}' '{test,bench,examples,scripts}/**/*.ts' '*.ts'"
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@oh-my-pi/pi-natives": "18.1.
|
|
41
|
-
"@oh-my-pi/pi-utils": "18.1.
|
|
40
|
+
"@oh-my-pi/pi-natives": "18.1.13",
|
|
41
|
+
"@oh-my-pi/pi-utils": "18.1.13"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"kitty-vt-wasm": "^0.2.0"
|
package/src/autocomplete.ts
CHANGED
|
@@ -111,6 +111,16 @@ function parsePathPrefix(prefix: string): { rawPrefix: string; isAtPrefix: boole
|
|
|
111
111
|
return { rawPrefix: prefix, isAtPrefix: false, isQuotedPrefix: false };
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
+
/**
|
|
115
|
+
* Whether an autocomplete value represents a directory: trailing slash or
|
|
116
|
+
* backslash, optionally followed by a closing quote for quoted paths.
|
|
117
|
+
* Shared by the provider suffix logic and the editor chain-on-accept
|
|
118
|
+
* behavior so Tab and Enter acceptance stay in sync.
|
|
119
|
+
*/
|
|
120
|
+
export function isDirectoryCompletionValue(value: string): boolean {
|
|
121
|
+
return /[\\/]["']?$/.test(value);
|
|
122
|
+
}
|
|
123
|
+
|
|
114
124
|
function buildCompletionValue(
|
|
115
125
|
path: string,
|
|
116
126
|
options: { isDirectory: boolean; isAtPrefix: boolean; isQuotedPrefix: boolean },
|
|
@@ -738,14 +748,16 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
|
|
|
738
748
|
beforePrefix = currentLine.slice(0, cursorCol - liveAtPrefix.length);
|
|
739
749
|
}
|
|
740
750
|
// This is a file attachment completion
|
|
741
|
-
const
|
|
751
|
+
const isDirectory = isDirectoryCompletionValue(item.value);
|
|
752
|
+
const suffix = isDirectory ? "" : " ";
|
|
753
|
+
const newLine = `${beforePrefix + item.value}${suffix}${afterCursor}`;
|
|
742
754
|
const newLines = [...lines];
|
|
743
755
|
newLines[cursorLine] = newLine;
|
|
744
756
|
|
|
745
757
|
return {
|
|
746
758
|
lines: newLines,
|
|
747
759
|
cursorLine,
|
|
748
|
-
cursorCol: beforePrefix.length + item.value.length +
|
|
760
|
+
cursorCol: beforePrefix.length + item.value.length + suffix.length,
|
|
749
761
|
};
|
|
750
762
|
}
|
|
751
763
|
|
package/src/components/editor.ts
CHANGED
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
type AutocompleteProvider,
|
|
5
5
|
findLeadingSlashCommandStart,
|
|
6
6
|
findTrailingSlashCommandStart,
|
|
7
|
+
isDirectoryCompletionValue,
|
|
7
8
|
midPromptSkillTokenMatches,
|
|
8
9
|
SKILL_NAMESPACE,
|
|
9
10
|
} from "../autocomplete";
|
|
@@ -1467,7 +1468,8 @@ export class Editor implements Component, Focusable {
|
|
|
1467
1468
|
return;
|
|
1468
1469
|
}
|
|
1469
1470
|
if (selected && this.#autocompleteProvider) {
|
|
1470
|
-
const
|
|
1471
|
+
const shouldChainAutocomplete =
|
|
1472
|
+
this.#isSlashCommandNameAutocompleteSelection() || isDirectoryCompletionValue(selected.value);
|
|
1471
1473
|
const result = this.#autocompleteProvider.applyCompletion(
|
|
1472
1474
|
this.#state.lines,
|
|
1473
1475
|
this.#state.cursorLine,
|
|
@@ -1489,8 +1491,8 @@ export class Editor implements Component, Focusable {
|
|
|
1489
1491
|
|
|
1490
1492
|
result.onApplied?.();
|
|
1491
1493
|
|
|
1492
|
-
if (
|
|
1493
|
-
void this.#tryTriggerAutocomplete();
|
|
1494
|
+
if (shouldChainAutocomplete) {
|
|
1495
|
+
queueMicrotask(() => void this.#tryTriggerAutocomplete());
|
|
1494
1496
|
}
|
|
1495
1497
|
}
|
|
1496
1498
|
return;
|
|
@@ -1543,6 +1545,7 @@ export class Editor implements Component, Focusable {
|
|
|
1543
1545
|
} else {
|
|
1544
1546
|
if (selected && this.#autocompleteProvider) {
|
|
1545
1547
|
const shouldChainSlashCommandAutocomplete = this.#isSlashCommandNameAutocompleteSelection();
|
|
1548
|
+
const shouldChainDirectoryCompletion = isDirectoryCompletionValue(selected.value);
|
|
1546
1549
|
const result = this.#autocompleteProvider.applyCompletion(
|
|
1547
1550
|
this.#state.lines,
|
|
1548
1551
|
this.#state.cursorLine,
|
|
@@ -1563,7 +1566,9 @@ export class Editor implements Component, Focusable {
|
|
|
1563
1566
|
}
|
|
1564
1567
|
|
|
1565
1568
|
result.onApplied?.();
|
|
1566
|
-
if (
|
|
1569
|
+
if (shouldChainDirectoryCompletion) {
|
|
1570
|
+
queueMicrotask(() => void this.#tryTriggerAutocomplete());
|
|
1571
|
+
} else if (shouldChainSlashCommandAutocomplete && this.#isCompletedSlashCommandAtCursor()) {
|
|
1567
1572
|
void this.#tryTriggerAutocomplete();
|
|
1568
1573
|
}
|
|
1569
1574
|
}
|
package/src/mouse.ts
CHANGED
|
@@ -18,7 +18,12 @@ export interface SgrMouseEvent {
|
|
|
18
18
|
row: number;
|
|
19
19
|
/** True for a release report (`m` suffix). */
|
|
20
20
|
release: boolean;
|
|
21
|
-
/**
|
|
21
|
+
/**
|
|
22
|
+
* Vertical wheel direction: -1 up, 1 down, null when not a vertical wheel
|
|
23
|
+
* event. Horizontal wheel reports (buttons 66/67, the sideways drift of a
|
|
24
|
+
* two-finger trackpad scroll) are not a direction: treating them as one
|
|
25
|
+
* scrolled the selectors up and back down at the end of a gesture.
|
|
26
|
+
*/
|
|
22
27
|
wheel: -1 | 1 | null;
|
|
23
28
|
/** True when the pointer moved (hover or drag) rather than clicked. */
|
|
24
29
|
motion: boolean;
|
|
@@ -38,7 +43,7 @@ export function parseSgrMouse(data: string): SgrMouseEvent | null {
|
|
|
38
43
|
const col = Number(match[2]) - 1;
|
|
39
44
|
const row = Number(match[3]) - 1;
|
|
40
45
|
const release = match[4] === "m";
|
|
41
|
-
const wheel = button & 64 ? ((button & 1 ? 1 : -1) as 1 | -1) : null;
|
|
46
|
+
const wheel = button & 64 && !(button & 2) ? ((button & 1 ? 1 : -1) as 1 | -1) : null;
|
|
42
47
|
const motion = (button & 32) !== 0 && wheel === null;
|
|
43
48
|
const leftClick = !release && wheel === null && !motion && (button & 3) === 0;
|
|
44
49
|
return { button, col, row, release, wheel, motion, leftClick };
|
|
@@ -43,6 +43,12 @@ export type TerminalId =
|
|
|
43
43
|
const CMUX_NOTIFICATION_TITLE = "Oh My Pi";
|
|
44
44
|
const CMUX_SURFACE_ID_PATTERN = /^[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}$/iu;
|
|
45
45
|
|
|
46
|
+
/** Title and body for an out-of-band multiplexer notification (cmux, Herdr). */
|
|
47
|
+
function notificationTitleAndBody(message: string | TerminalNotification): { title: string; body: string } {
|
|
48
|
+
if (typeof message === "string") return { title: CMUX_NOTIFICATION_TITLE, body: message };
|
|
49
|
+
return { title: message.title?.trim() || CMUX_NOTIFICATION_TITLE, body: message.body ?? "" };
|
|
50
|
+
}
|
|
51
|
+
|
|
46
52
|
/**
|
|
47
53
|
* Route a notification through cmux when the process belongs to a concrete
|
|
48
54
|
* surface. Workspace/socket state alone is not enough: only the injected
|
|
@@ -54,9 +60,7 @@ function sendCmuxNotification(message: string | TerminalNotification, env: NodeJ
|
|
|
54
60
|
const surfaceId = env.CMUX_SURFACE_ID?.trim();
|
|
55
61
|
if (!surfaceId || !CMUX_SURFACE_ID_PATTERN.test(surfaceId)) return false;
|
|
56
62
|
|
|
57
|
-
const title =
|
|
58
|
-
typeof message === "string" ? CMUX_NOTIFICATION_TITLE : message.title?.trim() || CMUX_NOTIFICATION_TITLE;
|
|
59
|
-
const body = typeof message === "string" ? message : (message.body ?? "");
|
|
63
|
+
const { title, body } = notificationTitleAndBody(message);
|
|
60
64
|
try {
|
|
61
65
|
const child = Bun.spawn({
|
|
62
66
|
cmd: ["cmux", "notify", "--surface", surfaceId, "--title", title, "--body", body],
|
|
@@ -72,6 +76,56 @@ function sendCmuxNotification(message: string | TerminalNotification, env: NodeJ
|
|
|
72
76
|
return true;
|
|
73
77
|
}
|
|
74
78
|
|
|
79
|
+
const HERDR_PANE_ID_PATTERN = /^[0-9A-Za-z:_-]{1,64}$/u;
|
|
80
|
+
/**
|
|
81
|
+
* `herdr notification show` takes the title as its first positional and reads
|
|
82
|
+
* exactly these three values there as a help request; it has no `--`
|
|
83
|
+
* terminator. Any other text, including one starting with `-`, is a title.
|
|
84
|
+
*/
|
|
85
|
+
const HERDR_USAGE_TOKENS = new Set(["help", "--help", "-h"]);
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Route a notification through Herdr when the process runs inside one of its
|
|
89
|
+
* panes. Herdr multiplexes panes like tmux but swallows bare OSC 9 / OSC 99 and
|
|
90
|
+
* has no DCS passthrough envelope, and its bell relay does not flag a
|
|
91
|
+
* backgrounded tab — so without this branch a backgrounded pane gets no signal
|
|
92
|
+
* at all that the agent finished or is waiting for input.
|
|
93
|
+
*
|
|
94
|
+
* `sound` maps the notification kind onto what Herdr offers: a question waiting
|
|
95
|
+
* on the user and a turn that stopped with an error both need the human and
|
|
96
|
+
* ring `request`, a settled turn rings `done`, anything else stays
|
|
97
|
+
* silent. Returns whether Herdr owns delivery, so every existing terminal
|
|
98
|
+
* fallback is preserved when the pane id is absent or the binary is missing.
|
|
99
|
+
*/
|
|
100
|
+
function sendHerdrNotification(message: string | TerminalNotification, env: NodeJS.ProcessEnv = Bun.env): boolean {
|
|
101
|
+
// Pane-only detection, like `isInsideHerdr`: an env-sanitizing launcher can
|
|
102
|
+
// drop HERDR_ENV and keep the pane identity, and that pane can still be
|
|
103
|
+
// backgrounded. The pane id itself is what the CLI needs, so it stays required.
|
|
104
|
+
if (!isInsideHerdr(env)) return false;
|
|
105
|
+
const paneId = env.HERDR_PANE_ID?.trim();
|
|
106
|
+
if (!paneId || !HERDR_PANE_ID_PATTERN.test(paneId)) return false;
|
|
107
|
+
|
|
108
|
+
const parsed = notificationTitleAndBody(message);
|
|
109
|
+
const title = HERDR_USAGE_TOKENS.has(parsed.title) ? CMUX_NOTIFICATION_TITLE : parsed.title;
|
|
110
|
+
const body = parsed.body;
|
|
111
|
+
const kinds = typeof message === "string" ? [] : [message.type ?? []].flat();
|
|
112
|
+
const sound =
|
|
113
|
+
kinds.includes("ask") || kinds.includes("error") ? "request" : kinds.includes("completion") ? "done" : "none";
|
|
114
|
+
try {
|
|
115
|
+
const child = Bun.spawn({
|
|
116
|
+
cmd: ["herdr", "notification", "show", title, "--body", body, "--sound", sound],
|
|
117
|
+
stdin: "ignore",
|
|
118
|
+
stdout: "ignore",
|
|
119
|
+
stderr: "ignore",
|
|
120
|
+
});
|
|
121
|
+
child.unref();
|
|
122
|
+
} catch {
|
|
123
|
+
// A missing herdr binary leaves delivery to the existing terminal fallback.
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
128
|
+
|
|
75
129
|
function hasNeedleBefore(line: string, needle: string, limit: number): boolean {
|
|
76
130
|
const index = line.indexOf(needle);
|
|
77
131
|
return index !== -1 && index + needle.length <= limit;
|
|
@@ -159,6 +213,11 @@ export class TerminalInfo {
|
|
|
159
213
|
|
|
160
214
|
sendNotification(message: string | TerminalNotification): void {
|
|
161
215
|
if (isNotificationSuppressed() || isTerminalHeadless()) return;
|
|
216
|
+
// Innermost surface first. A Herdr pane launched inside a cmux surface
|
|
217
|
+
// inherits both `HERDR_PANE_ID` and the outer `CMUX_SURFACE_ID`; routing to
|
|
218
|
+
// cmux there would flag the containing surface and leave the backgrounded
|
|
219
|
+
// Herdr pane — the one actually waiting — without a sound or a marker.
|
|
220
|
+
if (sendHerdrNotification(message)) return;
|
|
162
221
|
if (sendCmuxNotification(message)) return;
|
|
163
222
|
const formatted = this.formatNotification(message);
|
|
164
223
|
// Under tmux, terminals whose notify protocol is OSC 9 / OSC 99 would
|