@xzzpig/pi-starline 0.2.0 → 0.4.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
 
@@ -42,6 +42,7 @@ const extensionStatusPlacementValues: ExtensionStatusPlacement[] = [
42
42
  "left",
43
43
  "middle",
44
44
  "right",
45
+ "editor",
45
46
  ];
46
47
  const extensionStatusColorModeValues: ExtensionStatusColorMode[] = ["themed", "original"];
47
48
  const contextStyleValues: ContextStyle[] = ["text", "gauge", "text+gauge"];
@@ -11,6 +11,7 @@ import {
11
11
  import type { PolishedTuiConfig } from "./config";
12
12
  import { applyEditorCursorStyleToLines } from "./editor-cursor";
13
13
  import { renderEditorMetadataFormat } from "./editor-metadata-format";
14
+ import { collectExtensionStatusSegments } from "./extension-status";
14
15
  import { activeSelectionHintText, externalEditorHintText } from "./mouse";
15
16
  import { composeHints } from "./mouse/hint";
16
17
  import { pasteExpandHintText } from "./paste-collapse";
@@ -85,6 +86,8 @@ type PolishedFrameOptions = {
85
86
  modelMeta: EditorMeta;
86
87
  thinkingLevel: string | undefined;
87
88
  rightStatus?: string;
89
+ /** Third-party statuses placed on the editor row (`placement: "editor"`). */
90
+ editorStatus?: string;
88
91
  splitBaseFrame?: (lines: string[]) => PolishedFrameSplit | undefined;
89
92
  };
90
93
 
@@ -132,7 +135,8 @@ function getEditorChromeWidths(config: PolishedTuiConfig, uiTheme: Theme, reset:
132
135
  }
133
136
 
134
137
  /**
135
- * The right side of the metadata row: vim mode, the paste-expand hint, or both.
138
+ * The right side of the metadata row: vim mode, third-party statuses placed
139
+ * there, and the paste/selection hints.
136
140
  *
137
141
  * The hint used to live on the editor's bottom border, drawn there by the fixed
138
142
  * editor's compositor. Pi 0.84 supersedes the fixed editor, so the hint needs a
@@ -140,8 +144,15 @@ function getEditorChromeWidths(config: PolishedTuiConfig, uiTheme: Theme, reset:
140
144
  * `isHorizontalBorder` requires an unbroken rule to find the frame again. The
141
145
  * metadata row is always rendered, even when `editorMetadataFormat` is blank,
142
146
  * so the hint shows up whatever the user has done to that template.
147
+ *
148
+ * Hints come last: `composeMetadataLine` truncates this string from the right
149
+ * when it cannot fit, and the hints are the transient, actionable part.
143
150
  */
144
- function composeRightStatus(vimStatus: string | undefined, uiTheme: Theme): string | undefined {
151
+ function composeRightStatus(
152
+ vimStatus: string | undefined,
153
+ editorStatus: string | undefined,
154
+ uiTheme: Theme,
155
+ ): string | undefined {
145
156
  // A live selection hint already spells the external editor out ("N
146
157
  // characters selected, ctrl+x to copy ⋅ ctrl+g to edit in $EDITOR"), so it
147
158
  // takes precedence; the always-on hint only speaks when nothing else does.
@@ -149,9 +160,31 @@ function composeRightStatus(vimStatus: string | undefined, uiTheme: Theme): stri
149
160
  pasteExpandHintText(),
150
161
  activeSelectionHintText() ?? externalEditorHintText(),
151
162
  );
152
- if (!hint) return vimStatus;
153
- const styled = safeThemeFg(uiTheme, "muted", hint);
154
- return vimStatus ? `${vimStatus} ⋅ ${styled}` : styled;
163
+ const styled = hint ? safeThemeFg(uiTheme, "muted", hint) : undefined;
164
+ const parts = [vimStatus, editorStatus, styled].filter((part): part is string => Boolean(part));
165
+ return parts.length > 0 ? parts.join(" ⋅ ") : undefined;
166
+ }
167
+
168
+ /**
169
+ * Statuses configured with `placement: "editor"`, joined for the metadata row.
170
+ *
171
+ * The row is one muted line like the hints beside it, so the per-status colours
172
+ * and icons (footer features) are not applied here; `colorMode: "original"`
173
+ * text keeps whatever the extension already styled it with.
174
+ */
175
+ function editorStatusText(
176
+ config: PolishedTuiConfig,
177
+ uiTheme: Theme,
178
+ statuses: ReadonlyMap<string, string> | undefined,
179
+ ): string | undefined {
180
+ if (!statuses || statuses.size === 0) return undefined;
181
+ const segments = collectExtensionStatusSegments(statuses, config).editor;
182
+ if (segments.length === 0) return undefined;
183
+ return segments
184
+ .map((segment) =>
185
+ segment.colorMode === "original" ? segment.text : safeThemeFg(uiTheme, "muted", segment.text),
186
+ )
187
+ .join(" ⋅ ");
155
188
  }
156
189
 
157
190
  function composeMetadataLine(left: string, right: string | undefined, width: number): string {
@@ -280,6 +313,7 @@ function renderPolishedFrame({
280
313
  modelMeta,
281
314
  thinkingLevel,
282
315
  rightStatus,
316
+ editorStatus,
283
317
  splitBaseFrame,
284
318
  }: PolishedFrameOptions): string[] {
285
319
  if (width <= 2) return clampRenderedLines(baseRendered, width);
@@ -330,7 +364,7 @@ function renderPolishedFrame({
330
364
  uiTheme,
331
365
  config,
332
366
  );
333
- const status = composeRightStatus(rightStatus, uiTheme);
367
+ const status = composeRightStatus(rightStatus, editorStatus, uiTheme);
334
368
  const copyFriendlyMeta = composeMetadataLine(meta, status, Math.max(0, width - 1));
335
369
  const railedMeta = composeMetadataLine(meta, status, innerWidth);
336
370
 
@@ -381,6 +415,7 @@ export class PolishedEditor extends CustomEditor {
381
415
  private readonly getThinkingLevel: () => string | undefined;
382
416
  private readonly getConfig: () => PolishedTuiConfig;
383
417
  private readonly uiTheme: Theme;
418
+ private readonly getExtensionStatuses: (() => ReadonlyMap<string, string>) | undefined;
384
419
 
385
420
  constructor(
386
421
  tui: TUI,
@@ -390,6 +425,7 @@ export class PolishedEditor extends CustomEditor {
390
425
  getConfig: () => PolishedTuiConfig,
391
426
  getModelMeta: () => EditorMeta,
392
427
  getThinkingLevel: () => string | undefined,
428
+ getExtensionStatuses?: () => ReadonlyMap<string, string>,
393
429
  ) {
394
430
  super(tui, theme, keybindings, { paddingX: 0 });
395
431
  this.borderColor = (text: string) => safeThemeFg(uiTheme, "border", text);
@@ -397,6 +433,7 @@ export class PolishedEditor extends CustomEditor {
397
433
  this.getConfig = getConfig;
398
434
  this.getModelMeta = getModelMeta;
399
435
  this.getThinkingLevel = getThinkingLevel;
436
+ this.getExtensionStatuses = getExtensionStatuses;
400
437
  }
401
438
 
402
439
  render(width: number): string[] {
@@ -412,11 +449,14 @@ export class PolishedEditor extends CustomEditor {
412
449
  const result = renderPolishedFrame({
413
450
  width,
414
451
  baseRendered: rendered,
452
+ // SAFETY: this PolishedEditor is constructed with the internals the
453
+ // renderer needs; the widening crosses an interface boundary only.
415
454
  autocompleteSource: this as unknown as AutocompleteEditorInternals,
416
455
  uiTheme: this.uiTheme,
417
456
  config,
418
457
  modelMeta,
419
458
  thinkingLevel: this.getThinkingLevel(),
459
+ editorStatus: editorStatusText(config, this.uiTheme, this.getExtensionStatuses?.()),
420
460
  });
421
461
  return result;
422
462
  }
@@ -427,6 +467,8 @@ export class PolishedEditor extends CustomEditor {
427
467
  }
428
468
  // Also resolvable under the pre-rename key, so a pi-zentui wrapper reading a
429
469
  // pi-starline-built base still finds the splitter.
470
+ // SAFETY: LEGACY_SPLIT_POLISHED_FRAME is our own Symbol key; the cast only
471
+ // widens the receiver to assign a symbol-keyed method on the prototype.
430
472
  (PolishedEditor.prototype as unknown as Record<PropertyKey, unknown>)[LEGACY_SPLIT_POLISHED_FRAME] =
431
473
  PolishedEditor.prototype[SPLIT_POLISHED_FRAME];
432
474
 
@@ -437,6 +479,7 @@ export class WrappedPolishedEditor implements EditorComponent {
437
479
  private readonly getConfig: () => PolishedTuiConfig,
438
480
  private readonly getModelMeta: () => EditorMeta,
439
481
  private readonly getThinkingLevel: () => string | undefined,
482
+ private readonly getExtensionStatuses?: () => ReadonlyMap<string, string>,
440
483
  ) {}
441
484
 
442
485
  get focused(): boolean {
@@ -534,6 +577,7 @@ export class WrappedPolishedEditor implements EditorComponent {
534
577
  modelMeta,
535
578
  thinkingLevel: this.getThinkingLevel(),
536
579
  rightStatus: vimStatus,
580
+ editorStatus: editorStatusText(config, this.uiTheme, this.getExtensionStatuses?.()),
537
581
  splitBaseFrame: (
538
582
  this.base[SPLIT_POLISHED_FRAME] ?? this.base[LEGACY_SPLIT_POLISHED_FRAME]
539
583
  )?.bind(this.base),
@@ -588,10 +632,17 @@ export class WrappedPolishedEditor implements EditorComponent {
588
632
  return this.base.getLines?.() ?? this.base.getText().split("\n");
589
633
  }
590
634
 
635
+ // Deliberate pass-through of the base editor's duck-typed contract: Pi
636
+ // returns a {line, col} cursor object, so narrowing here would assert a
637
+ // type this wrapper cannot guarantee.
638
+ // pi-lens-ignore: no-unknown-returns
591
639
  getCursor(): unknown {
592
640
  return this.base.getCursor?.();
593
641
  }
594
642
 
643
+ // Deliberate pass-through of the base editor's duck-typed contract; callers
644
+ // narrow the value with their own runtime checks.
645
+ // pi-lens-ignore: no-unknown-returns
595
646
  getMode(): unknown {
596
647
  return this.base.getMode?.();
597
648
  }
@@ -606,6 +657,8 @@ export class WrappedPolishedEditor implements EditorComponent {
606
657
  }
607
658
  // Also resolvable under the pre-rename key, so a pi-zentui wrapper reading a
608
659
  // pi-starline-built base still finds the splitter.
660
+ // SAFETY: LEGACY_SPLIT_POLISHED_FRAME is our own Symbol key; the cast only
661
+ // widens the receiver to assign a symbol-keyed method on the prototype.
609
662
  (WrappedPolishedEditor.prototype as unknown as Record<PropertyKey, unknown>)[
610
663
  LEGACY_SPLIT_POLISHED_FRAME
611
664
  ] = WrappedPolishedEditor.prototype[SPLIT_POLISHED_FRAME];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xzzpig/pi-starline",
3
- "version": "0.2.0",
3
+ "version": "0.4.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,8 @@
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"
29
30
  },
30
31
  "pi": {
31
32
  "extensions": [
@@ -43,7 +44,13 @@
43
44
  "@earendil-works/pi-tui": "^0.84.4",
44
45
  "@types/node": "^26.0.0",
45
46
  "typescript": "^7.0.2",
46
- "vitest": "^4.1.8"
47
+ "vitest": "^4.1.8",
48
+ "@xzzpig/pi-mouse-events": "0.1.1"
49
+ },
50
+ "peerDependenciesMeta": {
51
+ "@xzzpig/pi-mouse-events": {
52
+ "optional": true
53
+ }
47
54
  },
48
55
  "scripts": {
49
56
  "lint": "biome check .",