@xzzpig/pi-starline 0.2.0 → 0.3.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/README.md +7 -6
- package/extensions/starline/index.ts +34 -33
- package/extensions/starline/mouse/api-consumer.ts +31 -0
- package/extensions/starline/mouse/capabilities.ts +69 -115
- package/extensions/starline/mouse/index.ts +467 -536
- package/extensions/starline/mouse/tool-box.ts +112 -83
- package/extensions/starline/prototype-patch-registry.ts +8 -5
- package/package.json +10 -3
|
@@ -1,40 +1,56 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Toggling an expandable transcript component by clicking it.
|
|
3
3
|
*
|
|
4
4
|
* `ctrl+o` toggles every box in the transcript at once, which is a screenful
|
|
5
5
|
* for the one line of output you wanted. This resolves the component under a
|
|
6
|
-
* mouse
|
|
7
|
-
*
|
|
6
|
+
* mouse click and toggles just that one — from any row it renders, not only
|
|
7
|
+
* its hint row.
|
|
8
8
|
*
|
|
9
|
-
* ##
|
|
9
|
+
* ## The click, not the press
|
|
10
10
|
*
|
|
11
|
-
* The
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
11
|
+
* The caller (`mouse/index.ts`) never consumes the press: Pi's selection
|
|
12
|
+
* machinery anchors on it, so a drag that starts anywhere on a box — hint row
|
|
13
|
+
* included — still selects and copies text. The toggle happens when the
|
|
14
|
+
* button is released on the very cell it was pressed on with no motion in
|
|
15
|
+
* between. Pi's own release path treats such a plain click as a no-op (an
|
|
16
|
+
* empty selection copies nothing), so consuming the release costs nothing;
|
|
17
|
+
* every other release — a drag, a different cell, a link — goes to Pi
|
|
18
|
+
* untouched.
|
|
19
19
|
*
|
|
20
|
-
*
|
|
21
|
-
* description)` (`keybinding-hints.js`), which is `theme.fg("dim", keyText) +
|
|
22
|
-
* theme.fg("muted", " " + description)`, and every call site wraps it in
|
|
23
|
-
* parentheses:
|
|
20
|
+
* ## Which way to toggle
|
|
24
21
|
*
|
|
25
|
-
*
|
|
26
|
-
* `... 3 more lines (ctrl+o to expand)` when collapsed.
|
|
27
|
-
* - `core/tools/{bash,find,grep,ls,read,write}.js` — `... (N more lines,
|
|
28
|
-
* ctrl+o to expand)`.
|
|
29
|
-
* - `read.js:91`, `skill-invocation-message.js:42`,
|
|
30
|
-
* `branch-summary-message.js:39`, `compaction-summary-message.js:40` —
|
|
31
|
-
* `(ctrl+o to expand)` on the title line.
|
|
22
|
+
* Two sources, in order:
|
|
32
23
|
*
|
|
33
|
-
* The
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
24
|
+
* 1. **The hint row.** Pi renders it through `keyHint("app.tools.expand",
|
|
25
|
+
* description)` (`keybinding-hints.js`), which is `theme.fg("dim",
|
|
26
|
+
* keyText) + theme.fg("muted", " " + description)`, and every call site
|
|
27
|
+
* wraps it in parentheses: `bash-execution.js:140,143`,
|
|
28
|
+
* `core/tools/{bash,find,grep,ls,read,write}.js`,
|
|
29
|
+
* `read.js:91`, `skill-invocation-message.js:42`,
|
|
30
|
+
* `branch-summary-message.js:39`, `compaction-summary-message.js:40`.
|
|
31
|
+
* The description carries the *direction*: `to expand` when collapsed, `to
|
|
32
|
+
* collapse` when expanded.
|
|
33
|
+
* 2. **The component's own state field.** Every expandable component keeps
|
|
34
|
+
* its state in a runtime-public boolean — `expanded` (bash-execution,
|
|
35
|
+
* tool-execution, skill/branch/compaction summaries) or `_expanded`
|
|
36
|
+
* (custom entries and messages) — and every mutation path writes it,
|
|
37
|
+
* ctrl+o's global fan-out included. Reading it cannot desync the way a
|
|
38
|
+
* `WeakMap` of "what I last set" would (the row on screen and the field
|
|
39
|
+
* are both the component's own truth); `expandedStateOf` duck-types both
|
|
40
|
+
* spellings and declines to guess when neither is a boolean.
|
|
41
|
+
*
|
|
42
|
+
* The field is what lets a click *collapse* from any row. Most of Pi's tool
|
|
43
|
+
* renderers emit the hint only while collapsed (`remaining > 0` in
|
|
44
|
+
* `core/tools/{read,grep,ls,write,find}.js`; the `else` branch of the
|
|
45
|
+
* skill/branch/compaction summaries), so an expanded box used to have no
|
|
46
|
+
* clickable way back at all without `pi-toolbox`'s collapse anchor.
|
|
47
|
+
*
|
|
48
|
+
* ## A component that handles the mouse keeps its clicks
|
|
49
|
+
*
|
|
50
|
+
* If any component on the clicked row's path implements `onMouse`, resolution
|
|
51
|
+
* declines. The layout folds the whole transcript into one box, so
|
|
52
|
+
* `pi-mouse-events` dispatch can never deliver an event to such a component —
|
|
53
|
+
* bowing out here is the only way its `onMouse` can mean what it says.
|
|
38
54
|
*
|
|
39
55
|
* ## Why resolution goes through the component tree
|
|
40
56
|
*
|
|
@@ -50,56 +66,24 @@
|
|
|
50
66
|
* ## What this deliberately does not do
|
|
51
67
|
*
|
|
52
68
|
* Nothing here looks at how a component *renders* to decide what it is:
|
|
53
|
-
* `isExpandableComponent` asks whether `setExpanded` is callable, and the
|
|
54
|
-
* rule is scoped to that component's own rows. An ordinary message that
|
|
55
|
-
* the hint — an assistant explaining `ctrl+o`, a paste of these very
|
|
56
|
-
* a `Text` or a `Markdown` with no `setExpanded` anywhere in its
|
|
57
|
-
* resolves to nothing and the
|
|
58
|
-
*
|
|
59
|
-
* ## Two accepted limitations
|
|
60
|
-
*
|
|
61
|
-
* Both are known, both were weighed, and neither has a fix worth its cost.
|
|
62
|
-
* They are recorded here and in `docs/configuration.md` because a user meets
|
|
63
|
-
* them before they meet any of the reasoning above.
|
|
69
|
+
* `isExpandableComponent` asks whether `setExpanded` is callable, and the
|
|
70
|
+
* hint rule is scoped to that component's own rows. An ordinary message that
|
|
71
|
+
* quotes the hint — an assistant explaining `ctrl+o`, a paste of these very
|
|
72
|
+
* docs — is a `Text` or a `Markdown` with no `setExpanded` anywhere in its
|
|
73
|
+
* path, so it resolves to nothing and the click stays a selection.
|
|
64
74
|
*
|
|
65
|
-
*
|
|
66
|
-
* "the hint row exists in both states, so one rule covers both directions" is
|
|
67
|
-
* only true of `bash-execution` and of `tool-execution` results that still
|
|
68
|
-
* have a preview to hide. For everything else Pi renders the hint *only while
|
|
69
|
-
* collapsed*, so expanding it removes the very row that would close it again:
|
|
75
|
+
* ## Accepted limitation: a box's own output can read like its hint
|
|
70
76
|
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
* expanded `ToolExecutionComponent` — the same text `bash-execution` renders
|
|
82
|
-
* natively — and the hint rule above matches it exactly as it matches Pi's
|
|
83
|
-
* own rows, because it is the component's own rendered row. That closes
|
|
84
|
-
* `read`/`grep`/`ls`/`write`/`find` boxes with one click. The second group
|
|
85
|
-
* (skill, branch and compaction summaries) keeps the limitation: no anchor
|
|
86
|
-
* exists for them, `ctrl+o` (which closes everything) is the way back, and
|
|
87
|
-
* inventing a second target (the title line, the first output row) would
|
|
88
|
-
* mean guessing at a component's layout, which is the inference this module
|
|
89
|
-
* exists to avoid. Without `pi-toolbox` installed the first group keeps the
|
|
90
|
-
* limitation too — no anchor is rendered, and nothing here invents one.
|
|
91
|
-
*
|
|
92
|
-
* **2. A box's own output can read like its hint.** The component scoping
|
|
93
|
-
* above stops a *different* message from being clickable, but not the box's
|
|
94
|
-
* own body: a line inside an expandable component that literally contains
|
|
95
|
-
* `(ctrl+o to expand)` — `cat` of a file documenting the keybinding, a
|
|
96
|
-
* transcript of this very docstring — is indistinguishable from the real hint,
|
|
97
|
-
* because Pi renders the box's output and the box's hint through the same
|
|
98
|
-
* `Text` with the same structure and only different theme colours. Matching
|
|
99
|
-
* the colours instead would make the rule depend on the user's theme.
|
|
100
|
-
* Accepted: the worst case is that a click on a box opens the box it is
|
|
101
|
-
* already inside — no crash, no lost input, and the same click a row above or
|
|
102
|
-
* below behaves normally.
|
|
77
|
+
* The component scoping above stops a *different* message from being
|
|
78
|
+
* clickable, but not the box's own body: a line inside an expandable
|
|
79
|
+
* component that literally contains `(ctrl+o to expand)` — `cat` of a file
|
|
80
|
+
* documenting the keybinding, a transcript of this very docstring — matches
|
|
81
|
+
* the hint rule and its direction, because Pi renders the box's output and
|
|
82
|
+
* the box's hint through the same `Text` with the same structure and only
|
|
83
|
+
* different theme colours. Matching the colours instead would make the rule
|
|
84
|
+
* depend on the user's theme. Accepted: the worst case is that a click on a
|
|
85
|
+
* box toggles the box it is already inside — no crash, no lost input, and the
|
|
86
|
+
* same click a row above or below behaves normally.
|
|
103
87
|
*/
|
|
104
88
|
|
|
105
89
|
import { keyText } from "@earendil-works/pi-coding-agent";
|
|
@@ -164,6 +148,32 @@ export function expandHintAction(line: string, keyText: string): "expand" | "col
|
|
|
164
148
|
return match[1] === "collapse" ? "collapse" : "expand";
|
|
165
149
|
}
|
|
166
150
|
|
|
151
|
+
/**
|
|
152
|
+
* The component's current expansion, read off its own state field.
|
|
153
|
+
*
|
|
154
|
+
* Pi's expandable message components keep a runtime-public boolean —
|
|
155
|
+
* `expanded` on bash-execution, tool-execution and the skill/branch/compaction
|
|
156
|
+
* summaries, `_expanded` on custom entries and messages — and every mutation
|
|
157
|
+
* path writes it, ctrl+o's global fan-out included, so it is the component's
|
|
158
|
+
* own current truth rather than a cached copy. A component that exposes
|
|
159
|
+
* neither spelling gets `undefined`: the caller declines to guess.
|
|
160
|
+
*/
|
|
161
|
+
export function expandedStateOf(component: object): boolean | undefined {
|
|
162
|
+
const fields = component as { expanded?: unknown; _expanded?: unknown };
|
|
163
|
+
if (typeof fields.expanded === "boolean") return fields.expanded;
|
|
164
|
+
if (typeof fields._expanded === "boolean") return fields._expanded;
|
|
165
|
+
return undefined;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** Whether the component declares its own mouse handling (`onMouse`). */
|
|
169
|
+
function handlesMouse(component: unknown): boolean {
|
|
170
|
+
return (
|
|
171
|
+
typeof component === "object" &&
|
|
172
|
+
component !== null &&
|
|
173
|
+
typeof (component as { onMouse?: unknown }).onMouse === "function"
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
167
177
|
/**
|
|
168
178
|
* The keys spelled the way the hint on screen spells them.
|
|
169
179
|
*
|
|
@@ -242,14 +252,15 @@ function scrollViewAt(root: BoxLike, x: number, y: number): unknown {
|
|
|
242
252
|
}
|
|
243
253
|
|
|
244
254
|
/**
|
|
245
|
-
* The expandable component
|
|
246
|
-
* undefined when that cell is anything else.
|
|
255
|
+
* The expandable component under screen cell (`x`, `y`) and the expansion to
|
|
256
|
+
* set on it, or undefined when that cell is anything else.
|
|
247
257
|
*
|
|
248
|
-
*
|
|
249
|
-
*
|
|
258
|
+
* The whole component tree is built and thrown away on every call: it holds
|
|
259
|
+
* no invalidation and a running tool re-renders under it (a
|
|
250
260
|
* `BashExecutionComponent` carries a ticking `Loader`), so the answer is only
|
|
251
261
|
* true of the frame that is on screen at this instant. Callers act on it
|
|
252
|
-
* inside the same
|
|
262
|
+
* inside the same event or not at all — the click flow in `mouse/index.ts`
|
|
263
|
+
* resolves once at press (identity only) and once more at release (action).
|
|
253
264
|
*/
|
|
254
265
|
export function expandTargetAt(
|
|
255
266
|
lookup: ExpandLookup,
|
|
@@ -259,6 +270,9 @@ export function expandTargetAt(
|
|
|
259
270
|
// Renamed on the way out of `lookup` so it cannot be read as this module's
|
|
260
271
|
// imported `keyText` function.
|
|
261
272
|
const { root, keyText: keys } = lookup;
|
|
273
|
+
// The keybinding gates the whole feature, hint rule included: an unbound
|
|
274
|
+
// `app.tools.expand` is a user with the expand affordances off, and "" is
|
|
275
|
+
// read as "no hint rule at all" rather than as a wildcard.
|
|
262
276
|
if (!root || !keys) return undefined;
|
|
263
277
|
|
|
264
278
|
const scrollView = scrollViewAt(root, x, y);
|
|
@@ -276,15 +290,30 @@ export function expandTargetAt(
|
|
|
276
290
|
|
|
277
291
|
const tree = createComponentTree(origin.component, origin.rect.width, lines);
|
|
278
292
|
const path = tree.pathAt(row);
|
|
293
|
+
|
|
294
|
+
// A component anywhere on the path that handles the mouse itself owns this
|
|
295
|
+
// click. The layout folds the transcript into one box, so `pi-mouse-events`
|
|
296
|
+
// dispatch can never deliver an event to it — the only way its `onMouse`
|
|
297
|
+
// can mean what it says is for this module to bow out.
|
|
298
|
+
for (const span of path) {
|
|
299
|
+
if (handlesMouse(span.component)) return undefined;
|
|
300
|
+
}
|
|
301
|
+
|
|
279
302
|
for (let index = path.length - 1; index >= 0; index--) {
|
|
280
303
|
const span = path[index];
|
|
281
304
|
if (!isExpandableComponent(span.component)) continue;
|
|
305
|
+
const component = span.component as ExpandableComponent;
|
|
282
306
|
// The hint is matched against this component's own rendered row, not
|
|
283
307
|
// against the transcript, so nothing outside the box it would toggle can
|
|
284
308
|
// stand in for its hint.
|
|
285
309
|
const action = expandHintAction(span.lines[row - span.start] ?? "", keys);
|
|
286
|
-
if (
|
|
287
|
-
|
|
310
|
+
if (action) return { component, expanded: action === "expand" };
|
|
311
|
+
// No hint on this row — or none rendered in this state, which is most
|
|
312
|
+
// components once expanded. The state field is the direction now, and a
|
|
313
|
+
// component that exposes neither source is not guessed at.
|
|
314
|
+
const state = expandedStateOf(component);
|
|
315
|
+
if (state === undefined) return undefined;
|
|
316
|
+
return { component, expanded: !state };
|
|
288
317
|
}
|
|
289
318
|
return undefined;
|
|
290
319
|
}
|
|
@@ -11,14 +11,17 @@ export const STARLINE_PROTOTYPE_PATCH_REGISTRY = Symbol.for("pi-starline.prototy
|
|
|
11
11
|
*/
|
|
12
12
|
const LEGACY_PROTOTYPE_PATCH_REGISTRY = Symbol.for("pi-zentui.prototype-patch-registry");
|
|
13
13
|
|
|
14
|
+
/**
|
|
15
|
+
* The adapters Starline still patches itself. The four mouse adapters
|
|
16
|
+
* (`mouse-viewport-input`, `mouse-wheel`, `mouse-selection-event`,
|
|
17
|
+
* `mouse-copy`) retired with the mouse patches: Starline's mouse features
|
|
18
|
+
* register with the `pi-mouse-events` extension instead of wrapping
|
|
19
|
+
* `TuiAltScreen.prototype` — see `mouse/index.ts`.
|
|
20
|
+
*/
|
|
14
21
|
type PrototypePatchAdapter =
|
|
15
22
|
| "user-message-render"
|
|
16
23
|
| "user-message-invalidate"
|
|
17
|
-
| "selector-border-render"
|
|
18
|
-
| "mouse-viewport-input"
|
|
19
|
-
| "mouse-wheel"
|
|
20
|
-
| "mouse-selection-event"
|
|
21
|
-
| "mouse-copy";
|
|
24
|
+
| "selector-border-render";
|
|
22
25
|
|
|
23
26
|
type PrototypeMethodName = string;
|
|
24
27
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xzzpig/pi-starline",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "A Starship-inspired statusline and Opencode-style TUI for Pi.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -25,7 +25,13 @@
|
|
|
25
25
|
"peerDependencies": {
|
|
26
26
|
"@earendil-works/pi-ai": ">=0.84.4",
|
|
27
27
|
"@earendil-works/pi-coding-agent": ">=0.84.4",
|
|
28
|
-
"@earendil-works/pi-tui": ">=0.84.4"
|
|
28
|
+
"@earendil-works/pi-tui": ">=0.84.4",
|
|
29
|
+
"@xzzpig/pi-mouse-events": ">=0.1.0 <2"
|
|
30
|
+
},
|
|
31
|
+
"peerDependenciesMeta": {
|
|
32
|
+
"@xzzpig/pi-mouse-events": {
|
|
33
|
+
"optional": true
|
|
34
|
+
}
|
|
29
35
|
},
|
|
30
36
|
"pi": {
|
|
31
37
|
"extensions": [
|
|
@@ -43,7 +49,8 @@
|
|
|
43
49
|
"@earendil-works/pi-tui": "^0.84.4",
|
|
44
50
|
"@types/node": "^26.0.0",
|
|
45
51
|
"typescript": "^7.0.2",
|
|
46
|
-
"vitest": "^4.1.8"
|
|
52
|
+
"vitest": "^4.1.8",
|
|
53
|
+
"@xzzpig/pi-mouse-events": "0.1.0"
|
|
47
54
|
},
|
|
48
55
|
"scripts": {
|
|
49
56
|
"lint": "biome check .",
|