@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.
- package/README.md +9 -7
- package/extensions/starline/config.ts +20 -3
- package/extensions/starline/extension-status.ts +3 -0
- package/extensions/starline/index.ts +35 -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/extensions/starline/settings-command.ts +1 -0
- package/extensions/starline/ui.ts +59 -6
- 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
|
|
|
@@ -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,
|
|
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(
|
|
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
|
-
|
|
153
|
-
const
|
|
154
|
-
return
|
|
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.
|
|
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 .",
|