@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.
@@ -1,40 +1,56 @@
1
1
  /**
2
- * Expanding one tool box by clicking it.
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 press and, when the press landed on that component's *hint row*,
7
- * toggles just it.
6
+ * mouse click and toggles just that one — from any row it renders, not only
7
+ * its hint row.
8
8
  *
9
- * ## Why the hint row is the only target
9
+ * ## The click, not the press
10
10
  *
11
- * The plan for this feature started as "click the box's border". Pi 0.84's own
12
- * tool boxes have no border: there is not a single box-drawing character
13
- * anywhere in `pi-coding-agent`'s tool rendering, and a collapsed box is a
14
- * plain `Container` of preview lines with an inline hint. The `╭─╮ │ ╰─╯` in a
15
- * real session comes from `pi-toolbox`, a separate extension that patches
16
- * `ToolExecutionComponent.prototype.render`. So a border is conditional on
17
- * what else is installed and cannot be the target — and inferring one from the
18
- * rendered rows is exactly what frame-free selection was cut for.
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
- * The hint row can be. Pi renders it through `keyHint("app.tools.expand",
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
- * - `bash-execution.js:140,143` — `(ctrl+o to collapse)` when expanded,
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 description carries the *direction*: `to expand` when collapsed, `to
34
- * collapse` when expanded. That is what this module toggles on, rather than
35
- * tracking expansion itself — a `WeakMap` of "what I last set" would desync
36
- * the first time `ctrl+o` toggled everything behind its back, whereas the row
37
- * on screen is by definition current.
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 hint
54
- * rule is scoped to that component's own rows. An ordinary message that quotes
55
- * the hint — an assistant explaining `ctrl+o`, a paste of these very docs — is
56
- * a `Text` or a `Markdown` with no `setExpanded` anywhere in its path, so it
57
- * resolves to nothing and the press starts a selection as usual.
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
- * **1. Clicking cannot always collapse what it expanded.** The premise that
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
- * - `core/tools/{read,grep,ls,write,find}.js` append the hint inside `if
72
- * (remaining > 0)`, and expanding sets `maxLines` to `lines.length`, so
73
- * `remaining` becomes 0 and the row is not emitted at all (`read.js:113-118`
74
- * is the clearest instance).
75
- * - `skill-invocation-message.js:42`, `branch-summary-message.js:39` and
76
- * `compaction-summary-message.js:40` build the hint in their `else` branch,
77
- * i.e. only when `this.expanded` is false.
78
- *
79
- * For the first group there is a way back: current `pi-toolbox` appends a
80
- * `(ctrl+o to collapse)` anchor row as the last line inside the frame of an
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 whose hint row is at screen cell (`x`, `y`), or
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
- * Resolution is done end to end here and thrown away: the component tree it
249
- * builds holds no invalidation and a running tool re-renders under it (a
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 press or not at all.
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 (!action) return undefined;
287
- return { component: span.component as ExpandableComponent, expanded: action === "expand" };
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.2.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 .",