@xzzpig/pi-starline 0.4.1 → 0.5.1
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/FORK-CHANGELOG.md +9 -0
- package/README.md +24 -1
- package/dist/public.d.ts +5 -0
- package/extensions/starline/config.ts +17 -10
- package/extensions/starline/mouse/api-consumer.ts +2 -2
- package/extensions/starline/mouse/capabilities.ts +4 -4
- package/extensions/starline/mouse/explicit-expand-target.ts +89 -0
- package/extensions/starline/mouse/index.ts +5 -4
- package/extensions/starline/mouse/key-text.ts +64 -0
- package/extensions/starline/mouse/tool-box.ts +11 -66
- package/extensions/starline/settings-command.ts +8 -7
- package/extensions/starline/user-message.ts +71 -11
- package/package.json +15 -10
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Fork release notes
|
|
2
|
+
|
|
3
|
+
## 0.5.1 — Release candidate
|
|
4
|
+
|
|
5
|
+
- Recognize the optional `@xzzpig/transcript-expand-target.v1` contract for group summary clicks. Invalid or absent providers fall back to the existing component resolver; no new mandatory package dependency or default-setting change.
|
|
6
|
+
- Add `build:types` and `prepack` to include `dist/public.d.ts` alongside the extension sources.
|
|
7
|
+
- Keep historical upstream/fork release notes in `CHANGELOG.md`; this fork-only file records the new compatibility release without expanding upstream documentation edits.
|
|
8
|
+
|
|
9
|
+
Validated host: Pi 1.1.0. Native drag-selection acceptance and OpenSpec archival are release gates, not implied by a successful package build.
|
package/README.md
CHANGED
|
@@ -176,7 +176,7 @@ Settings live in `~/.pi/agent/starline.json`. The file is optional — anything
|
|
|
176
176
|
{
|
|
177
177
|
"footerStyle": "pill",
|
|
178
178
|
"colors": { "gitBranch": "bold purple" },
|
|
179
|
-
"mouse": { "
|
|
179
|
+
"mouse": { "clickToToggleExpandable": false }
|
|
180
180
|
}
|
|
181
181
|
```
|
|
182
182
|
|
|
@@ -227,3 +227,26 @@ Inspired by:
|
|
|
227
227
|
## License
|
|
228
228
|
|
|
229
229
|
MIT
|
|
230
|
+
|
|
231
|
+
## Compatibility: explicit expand targets (fork-only)
|
|
232
|
+
|
|
233
|
+
This fork's click-to-expand resolution (`mouse/tool-box.ts`) checks an
|
|
234
|
+
optional, versioned **explicit expand-target protocol** before its own
|
|
235
|
+
`setExpanded`/hint logic:
|
|
236
|
+
|
|
237
|
+
- Providers publish a resolver on a component instance under
|
|
238
|
+
`Symbol.for("@xzzpig/transcript-expand-target.v1")` with
|
|
239
|
+
`{ version: 1, resolveLocalTarget(localRow, width) }`. The consumer is
|
|
240
|
+
`mouse/explicit-expand-target.ts` (fork-only file).
|
|
241
|
+
- Today the only provider is
|
|
242
|
+
[`@xzzpig/pi-group-collapse`](../pi-group-collapse): its group summaries
|
|
243
|
+
resolve to the independent group object, so clicking a summary toggles
|
|
244
|
+
the group instead of the tool that carries it.
|
|
245
|
+
- Without a provider nothing changes: no published protocol, a wrong
|
|
246
|
+
version, a non-callable resolver, a thrown resolution, or an invalid
|
|
247
|
+
resolved shape all fall through to the existing resolution. Installing
|
|
248
|
+
the provider is never required, and load order in either direction
|
|
249
|
+
works (the resolver is read per click, not cached).
|
|
250
|
+
- Drag selection, double-click word selection, hyperlinks, wheel, and
|
|
251
|
+
overlay behavior are unaffected; the explicit target only answers for
|
|
252
|
+
the specific local rows it owns.
|
package/dist/public.d.ts
ADDED
|
@@ -109,8 +109,8 @@ export type MouseConfig = {
|
|
|
109
109
|
wheelRouting: boolean;
|
|
110
110
|
/** Show a "copied to clipboard" notice for a copy Starline itself performs. */
|
|
111
111
|
copyNotice: boolean;
|
|
112
|
-
/**
|
|
113
|
-
|
|
112
|
+
/** Click any collapsible box (a tool box, a custom message, an expandable text) to expand or collapse it. */
|
|
113
|
+
clickToToggleExpandable: boolean;
|
|
114
114
|
/** Copy transcript selections without the painted rails, rules and frames. */
|
|
115
115
|
transcriptCleanCopy: boolean;
|
|
116
116
|
};
|
|
@@ -119,7 +119,10 @@ const FIXED_EDITOR_KEY_MAP: Record<string, keyof MouseConfig> = {
|
|
|
119
119
|
enabled: "enabled",
|
|
120
120
|
mouseScroll: "wheelRouting",
|
|
121
121
|
copyNotice: "copyNotice",
|
|
122
|
-
|
|
122
|
+
// The 0.2.x-era name for click-to-toggle; Pi 0.86.0 owns the click itself
|
|
123
|
+
// for tool boxes, but the fork keeps the general collapsible-box toggle,
|
|
124
|
+
// so the old value is carried to the renamed key instead of being dropped.
|
|
125
|
+
clickToExpandTools: "clickToToggleExpandable",
|
|
123
126
|
};
|
|
124
127
|
|
|
125
128
|
/**
|
|
@@ -418,7 +421,7 @@ export const defaultConfig: PolishedTuiConfig = {
|
|
|
418
421
|
enabled: true,
|
|
419
422
|
wheelRouting: true,
|
|
420
423
|
copyNotice: true,
|
|
421
|
-
|
|
424
|
+
clickToToggleExpandable: true,
|
|
422
425
|
transcriptCleanCopy: true,
|
|
423
426
|
},
|
|
424
427
|
};
|
|
@@ -838,10 +841,14 @@ function normalizeMouseConfig(record: Record<string, unknown>): MouseConfig {
|
|
|
838
841
|
: defaultConfig.mouse.wheelRouting,
|
|
839
842
|
copyNotice:
|
|
840
843
|
typeof record.copyNotice === "boolean" ? record.copyNotice : defaultConfig.mouse.copyNotice,
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
844
|
+
clickToToggleExpandable:
|
|
845
|
+
// The new key wins outright; a config carrying the pre-rename
|
|
846
|
+
// mouse.clickToExpandTools value is honoured as a fallback.
|
|
847
|
+
typeof record.clickToToggleExpandable === "boolean"
|
|
848
|
+
? record.clickToToggleExpandable
|
|
849
|
+
: typeof record.clickToExpandTools === "boolean"
|
|
850
|
+
? record.clickToExpandTools
|
|
851
|
+
: defaultConfig.mouse.clickToToggleExpandable,
|
|
845
852
|
transcriptCleanCopy:
|
|
846
853
|
typeof record.transcriptCleanCopy === "boolean"
|
|
847
854
|
? record.transcriptCleanCopy
|
|
@@ -1306,8 +1313,8 @@ export function saveMousePatch(patch: Partial<MouseConfig>, path = configPath):
|
|
|
1306
1313
|
...(patch.enabled !== undefined ? { enabled: patch.enabled } : {}),
|
|
1307
1314
|
...(patch.wheelRouting !== undefined ? { wheelRouting: patch.wheelRouting } : {}),
|
|
1308
1315
|
...(patch.copyNotice !== undefined ? { copyNotice: patch.copyNotice } : {}),
|
|
1309
|
-
...(patch.
|
|
1310
|
-
? {
|
|
1316
|
+
...(patch.clickToToggleExpandable !== undefined
|
|
1317
|
+
? { clickToToggleExpandable: patch.clickToToggleExpandable }
|
|
1311
1318
|
: {}),
|
|
1312
1319
|
...(patch.transcriptCleanCopy !== undefined
|
|
1313
1320
|
? { transcriptCleanCopy: patch.transcriptCleanCopy }
|
|
@@ -22,7 +22,7 @@ import type { MouseEventsApi } from "@xzzpig/pi-mouse-events/api";
|
|
|
22
22
|
* MUST match `MOUSE_EVENTS_API_KEY` in `@xzzpig/pi-mouse-events/api` — the
|
|
23
23
|
* contract test above is what keeps that true.
|
|
24
24
|
*/
|
|
25
|
-
const MOUSE_EVENTS_API_KEY = Symbol.for("pi-mouse-events.api.
|
|
25
|
+
const MOUSE_EVENTS_API_KEY = Symbol.for("pi-mouse-events.api.v2");
|
|
26
26
|
|
|
27
27
|
export function getMouseEventsApi(): MouseEventsApi | undefined {
|
|
28
28
|
const candidate = (globalThis as Record<symbol, unknown>)[MOUSE_EVENTS_API_KEY] as
|
|
@@ -31,7 +31,7 @@ export function getMouseEventsApi(): MouseEventsApi | undefined {
|
|
|
31
31
|
// The symbol key pins the contract name, not its shape: reject an object
|
|
32
32
|
// from a different contract major so callers never dereference a
|
|
33
33
|
// mismatched surface (the key is only bumped together with the version).
|
|
34
|
-
if (!candidate || candidate.version !==
|
|
34
|
+
if (!candidate || candidate.version !== 2) {
|
|
35
35
|
return undefined;
|
|
36
36
|
}
|
|
37
37
|
return candidate;
|
|
@@ -29,7 +29,7 @@ export type MouseCapability = MouseReceiverCapabilities | "copySlot";
|
|
|
29
29
|
|
|
30
30
|
export type MouseFeature =
|
|
31
31
|
| "selectionHint"
|
|
32
|
-
| "
|
|
32
|
+
| "clickToToggleExpandable"
|
|
33
33
|
| "editorWheelScroll"
|
|
34
34
|
| "editorClickToCaret"
|
|
35
35
|
| "editorBufferCopy"
|
|
@@ -77,7 +77,7 @@ const REQUIREMENTS: Record<MouseFeature, readonly MouseCapability[]> = {
|
|
|
77
77
|
// whatever tool box happens to sit behind it — and `requestRender`, because
|
|
78
78
|
// consuming the press means Pi never reaches its own repaint for that
|
|
79
79
|
// event.
|
|
80
|
-
|
|
80
|
+
clickToToggleExpandable: ["hasOverlay", "requestRender"],
|
|
81
81
|
// Same shape as click-to-expand: the notch arrives through the handler
|
|
82
82
|
// slot, `hasOverlay` keeps a notch aimed at a dialog from scrolling the
|
|
83
83
|
// draft hidden behind it, and consuming the notch means `requestRender` is
|
|
@@ -113,7 +113,7 @@ const REQUIREMENTS: Record<MouseFeature, readonly MouseCapability[]> = {
|
|
|
113
113
|
],
|
|
114
114
|
};
|
|
115
115
|
|
|
116
|
-
function isCallable(receiver:
|
|
116
|
+
function isCallable<T extends object>(receiver: T, name: string): boolean {
|
|
117
117
|
try {
|
|
118
118
|
return typeof (receiver as Record<string, unknown>)[name] === "function";
|
|
119
119
|
} catch {
|
|
@@ -122,7 +122,7 @@ function isCallable(receiver: object, name: string): boolean {
|
|
|
122
122
|
}
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
-
export function probeCapabilities(receiver:
|
|
125
|
+
export function probeCapabilities<T extends object>(receiver: T): ReadonlySet<MouseCapability> {
|
|
126
126
|
const found = new Set<MouseCapability>();
|
|
127
127
|
for (const capability of CAPABILITIES) {
|
|
128
128
|
if (capability === "copySlot") continue;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fork-only consumer for the versioned explicit expand-target protocol
|
|
3
|
+
* (`@xzzpig/transcript-expand-target.v1`).
|
|
4
|
+
*
|
|
5
|
+
* Providers (pi-group-collapse today) publish a resolver on the component
|
|
6
|
+
* instance that draws a group summary. Starline checks that resolver for
|
|
7
|
+
* the component's *local* row before its own `setExpanded`/hint logic, so a
|
|
8
|
+
* click on a summary toggles the group object instead of the first tool
|
|
9
|
+
* inside it.
|
|
10
|
+
*
|
|
11
|
+
* The contract is defensive by requirement (see the mouse-api spec):
|
|
12
|
+
* no published protocol, a wrong version, a non-callable resolver, a
|
|
13
|
+
* thrown resolution, or an invalid resolved shape all fall back to the
|
|
14
|
+
* existing component path — installing the provider is never required and
|
|
15
|
+
* a broken provider cannot break clicks that used to work.
|
|
16
|
+
*
|
|
17
|
+
* This module deliberately depends on nothing: the symbol key is its only
|
|
18
|
+
* tie to the provider, so Starline keeps working with the provider absent,
|
|
19
|
+
* and any number of providers can publish the same key.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/** The shared protocol key; `Symbol.for` so every copy in the process agrees. */
|
|
23
|
+
export const EXPAND_TARGET_KEY = Symbol.for("@xzzpig/transcript-expand-target.v1");
|
|
24
|
+
|
|
25
|
+
/** The slice of the protocol Starline reads. */
|
|
26
|
+
type ExpandTargetProtocol = {
|
|
27
|
+
version: 1;
|
|
28
|
+
resolveLocalTarget: (localRow: number, width: number) => unknown;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/** The resolved target, validated before use. */
|
|
32
|
+
export type ExplicitExpandTarget = {
|
|
33
|
+
component: { setExpanded(expanded: boolean): void };
|
|
34
|
+
expanded: boolean;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Resolve an explicit expand target for a component's local row, or
|
|
39
|
+
* undefined when there is no provider, no hit, or anything invalid.
|
|
40
|
+
*/
|
|
41
|
+
export function explicitExpandTargetAt(
|
|
42
|
+
component: unknown,
|
|
43
|
+
localRow: number,
|
|
44
|
+
width: number,
|
|
45
|
+
): ExplicitExpandTarget | undefined {
|
|
46
|
+
if (typeof component !== "object" || component === null) return undefined;
|
|
47
|
+
let protocol: unknown;
|
|
48
|
+
try {
|
|
49
|
+
protocol = (component as Record<PropertyKey, unknown>)[EXPAND_TARGET_KEY];
|
|
50
|
+
} catch {
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
if (
|
|
54
|
+
typeof protocol !== "object" ||
|
|
55
|
+
protocol === null ||
|
|
56
|
+
(protocol as ExpandTargetProtocol).version !== 1 ||
|
|
57
|
+
typeof (protocol as ExpandTargetProtocol).resolveLocalTarget !== "function"
|
|
58
|
+
) {
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
let resolved: unknown;
|
|
62
|
+
try {
|
|
63
|
+
resolved = (protocol as ExpandTargetProtocol).resolveLocalTarget(localRow, width);
|
|
64
|
+
} catch {
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
if (
|
|
68
|
+
typeof resolved !== "object" ||
|
|
69
|
+
resolved === null ||
|
|
70
|
+
typeof (resolved as ExplicitExpandTarget).expanded !== "boolean"
|
|
71
|
+
) {
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
const target = resolved as {
|
|
75
|
+
component?: { setExpanded?: unknown };
|
|
76
|
+
expanded: boolean;
|
|
77
|
+
};
|
|
78
|
+
if (
|
|
79
|
+
typeof target.component !== "object" ||
|
|
80
|
+
target.component === null ||
|
|
81
|
+
typeof target.component.setExpanded !== "function"
|
|
82
|
+
) {
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
return {
|
|
86
|
+
component: target.component as { setExpanded(expanded: boolean): void },
|
|
87
|
+
expanded: target.expanded,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
* located and how the live editor is reached; the scroll itself is
|
|
22
22
|
* `editor-scroll.ts`.
|
|
23
23
|
*
|
|
24
|
-
* `
|
|
24
|
+
* `clickToToggleExpandable` → `addMouseHandler`
|
|
25
25
|
* - A left-button press anywhere on an expandable component records the
|
|
26
26
|
* candidate; when the button is released on the same cell with no motion in
|
|
27
27
|
* between — a plain click — that one component is toggled and the release
|
|
@@ -89,8 +89,9 @@ import { activeEditor, pointerOverEditor } from "./editor-mouse";
|
|
|
89
89
|
import { scrollEditorBy } from "./editor-scroll";
|
|
90
90
|
import { editorVisualRowCount } from "./editor-text-cursor";
|
|
91
91
|
import { type BoxLike, scrollContentLinesFor } from "./hit-test";
|
|
92
|
+
import { keyTextFor } from "./key-text";
|
|
92
93
|
import { externalEditorName, selectionHintText } from "./selection-state";
|
|
93
|
-
import { type ExpandTarget, expandKeyText, expandTargetAt
|
|
94
|
+
import { type ExpandTarget, expandKeyText, expandTargetAt } from "./tool-box";
|
|
94
95
|
import { cleanTranscriptRows } from "./transcript-copy";
|
|
95
96
|
|
|
96
97
|
/** Pi's interrupt chord — refused by the range-delete branch, never consumed. */
|
|
@@ -664,7 +665,7 @@ function expandClickTarget(
|
|
|
664
665
|
deps: InstallMouseDeps,
|
|
665
666
|
): ExpandTarget | undefined {
|
|
666
667
|
try {
|
|
667
|
-
if (!deps.getConfig().mouse.
|
|
668
|
+
if (!deps.getConfig().mouse.clickToToggleExpandable) return undefined;
|
|
668
669
|
// Pi resolves no scroll view while an overlay is up, so neither does this
|
|
669
670
|
// — a click on a dialog must not reach the transcript behind it.
|
|
670
671
|
if (receiver.hasOverlay?.()) return undefined;
|
|
@@ -806,7 +807,7 @@ export function installMouseFeaturesOn(
|
|
|
806
807
|
api.addMouseHandler(
|
|
807
808
|
({ event, tui }) => {
|
|
808
809
|
const receiver = bindReceiver(tui);
|
|
809
|
-
if (!receiver || !featureOn("
|
|
810
|
+
if (!receiver || !featureOn("clickToToggleExpandable")) {
|
|
810
811
|
expandPress = undefined;
|
|
811
812
|
return undefined;
|
|
812
813
|
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The keys spelled the way Pi's on-screen hints spell them.
|
|
3
|
+
*
|
|
4
|
+
* The primary source is `pi-coding-agent`'s own exported `keyText` — literally
|
|
5
|
+
* the function `keyHint` calls to build the hint text the UI shows, so there
|
|
6
|
+
* is no second formatting rule to drift (it is `keyText` that turns `alt`
|
|
7
|
+
* into `option` on macOS and joins alternatives with "/"), and no second
|
|
8
|
+
* keybinding registry to disagree.
|
|
9
|
+
*
|
|
10
|
+
* That last point is not theoretical. `getKeybindings()` is a *singleton* per
|
|
11
|
+
* copy of pi-tui, and a copy is per `node_modules` tree: in this repo
|
|
12
|
+
* `pi-coding-agent` nests its own pi-tui, so the registry Starline's direct
|
|
13
|
+
* import reaches is not the registry the hints are rendered from. Production
|
|
14
|
+
* resolves both to Pi's own copy, but reading the value through the same
|
|
15
|
+
* package that renders it makes the question moot instead of assumed.
|
|
16
|
+
*
|
|
17
|
+
* pi-tui's registry is kept as a fallback for the case where `keyText` yields
|
|
18
|
+
* nothing. Both routes return "" when the binding is unbound, and callers
|
|
19
|
+
* read "" as "show no key" rather than as a wildcard.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { keyText } from "@earendil-works/pi-coding-agent";
|
|
23
|
+
import { getKeybindings } from "@earendil-works/pi-tui";
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* `keyText(keybinding)` with the registry fallback, shared by every feature
|
|
27
|
+
* that quotes a key on screen. See the module header for why the primary
|
|
28
|
+
* route is pi-coding-agent's own function rather than this repo's registry.
|
|
29
|
+
*/
|
|
30
|
+
export function keyTextFor(keybinding: string): string {
|
|
31
|
+
try {
|
|
32
|
+
const rendered = keyText(keybinding as never);
|
|
33
|
+
if (typeof rendered === "string" && rendered.length > 0) return rendered;
|
|
34
|
+
} catch {
|
|
35
|
+
// A Pi build that has moved this function is one where quoting the key
|
|
36
|
+
// simply shows nothing; it is never a reason to break a mouse feature.
|
|
37
|
+
}
|
|
38
|
+
return fallbackKeyText(keybinding);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* `keyText` re-derived from pi-tui's registry: keys joined with "/", `alt`
|
|
43
|
+
* shown as `option` on macOS, matching `formatKeyText` in
|
|
44
|
+
* `keybinding-hints.js`.
|
|
45
|
+
*/
|
|
46
|
+
function fallbackKeyText(keybinding: string): string {
|
|
47
|
+
try {
|
|
48
|
+
const bound: unknown = getKeybindings().getKeys(keybinding as never);
|
|
49
|
+
if (!Array.isArray(bound) || bound.length === 0) return "";
|
|
50
|
+
const keys = bound.filter((key): key is string => typeof key === "string");
|
|
51
|
+
return keys
|
|
52
|
+
.map((key: string) =>
|
|
53
|
+
key
|
|
54
|
+
.split("+")
|
|
55
|
+
.map((part) =>
|
|
56
|
+
process.platform === "darwin" && part.toLowerCase() === "alt" ? "option" : part,
|
|
57
|
+
)
|
|
58
|
+
.join("+"),
|
|
59
|
+
)
|
|
60
|
+
.join("/");
|
|
61
|
+
} catch {
|
|
62
|
+
return "";
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -45,12 +45,11 @@
|
|
|
45
45
|
* skill/branch/compaction summaries), so an expanded box used to have no
|
|
46
46
|
* clickable way back at all without `pi-toolbox`'s collapse anchor.
|
|
47
47
|
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
* bowing out here is the only way its `onMouse` can mean what it says.
|
|
48
|
+
* Ownership is decided by resolution alone: a click is taken over iff it
|
|
49
|
+
* resolves to an expandable component (one with `setExpanded`). A component
|
|
50
|
+
* that handles the mouse itself — Pi's own tool boxes wrapped in a core
|
|
51
|
+
* `MouseRegion` included — is simply not expandable by this predicate, so it
|
|
52
|
+
* falls through to Pi's native handling instead of being fought over.
|
|
54
53
|
*
|
|
55
54
|
* ## Why resolution goes through the component tree
|
|
56
55
|
*
|
|
@@ -86,10 +85,11 @@
|
|
|
86
85
|
* same click a row above or below behaves normally.
|
|
87
86
|
*/
|
|
88
87
|
|
|
89
|
-
import {
|
|
90
|
-
import { getKeybindings, stripTerminalSequences } from "@earendil-works/pi-tui";
|
|
88
|
+
import { stripTerminalSequences } from "@earendil-works/pi-tui";
|
|
91
89
|
import { createComponentTree, isComponentLike, isExpandableComponent } from "./component-tree";
|
|
90
|
+
import { explicitExpandTargetAt } from "./explicit-expand-target";
|
|
92
91
|
import { type BoxLike, boxesAt, scrollContentLinesFor, scrollContentOrigin } from "./hit-test";
|
|
92
|
+
import { keyTextFor } from "./key-text";
|
|
93
93
|
|
|
94
94
|
/** The slice of a Pi message component this module calls. */
|
|
95
95
|
export type ExpandableComponent = { setExpanded(expanded: boolean): void };
|
|
@@ -165,15 +165,6 @@ export function expandedStateOf(component: object): boolean | undefined {
|
|
|
165
165
|
return undefined;
|
|
166
166
|
}
|
|
167
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
|
-
|
|
177
168
|
/**
|
|
178
169
|
* The keys spelled the way the hint on screen spells them.
|
|
179
170
|
*
|
|
@@ -200,47 +191,6 @@ export function expandKeyText(): string {
|
|
|
200
191
|
return keyTextFor(KEYBINDING);
|
|
201
192
|
}
|
|
202
193
|
|
|
203
|
-
/**
|
|
204
|
-
* `keyText(keybinding)` with the registry fallback, shared by every feature
|
|
205
|
-
* that quotes a key on screen. See `expandKeyText` for why the primary route
|
|
206
|
-
* is pi-coding-agent's own function rather than this repo's registry.
|
|
207
|
-
*/
|
|
208
|
-
export function keyTextFor(keybinding: string): string {
|
|
209
|
-
try {
|
|
210
|
-
const rendered = keyText(keybinding as never);
|
|
211
|
-
if (typeof rendered === "string" && rendered.length > 0) return rendered;
|
|
212
|
-
} catch {
|
|
213
|
-
// A Pi build that has moved this function is one where quoting the key
|
|
214
|
-
// simply shows nothing; it is never a reason to break a mouse feature.
|
|
215
|
-
}
|
|
216
|
-
return fallbackKeyText(keybinding);
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
/**
|
|
220
|
-
* `keyText` re-derived from pi-tui's registry: keys joined with "/", `alt`
|
|
221
|
-
* shown as `option` on macOS, matching `formatKeyText` in
|
|
222
|
-
* `keybinding-hints.js`.
|
|
223
|
-
*/
|
|
224
|
-
function fallbackKeyText(keybinding: string): string {
|
|
225
|
-
try {
|
|
226
|
-
const bound: unknown = getKeybindings().getKeys(keybinding as never);
|
|
227
|
-
if (!Array.isArray(bound) || bound.length === 0) return "";
|
|
228
|
-
const keys = bound.filter((key): key is string => typeof key === "string");
|
|
229
|
-
return keys
|
|
230
|
-
.map((key: string) =>
|
|
231
|
-
key
|
|
232
|
-
.split("+")
|
|
233
|
-
.map((part) =>
|
|
234
|
-
process.platform === "darwin" && part.toLowerCase() === "alt" ? "option" : part,
|
|
235
|
-
)
|
|
236
|
-
.join("+"),
|
|
237
|
-
)
|
|
238
|
-
.join("/");
|
|
239
|
-
} catch {
|
|
240
|
-
return "";
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
|
|
244
194
|
/** The innermost scroll view whose box contains the screen cell. */
|
|
245
195
|
function scrollViewAt(root: BoxLike, x: number, y: number): unknown {
|
|
246
196
|
let found: unknown;
|
|
@@ -291,16 +241,11 @@ export function expandTargetAt(
|
|
|
291
241
|
const tree = createComponentTree(origin.component, origin.rect.width, lines);
|
|
292
242
|
const path = tree.pathAt(row);
|
|
293
243
|
|
|
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
|
-
|
|
302
244
|
for (let index = path.length - 1; index >= 0; index--) {
|
|
303
245
|
const span = path[index];
|
|
246
|
+
// Fork seam: a versioned explicit target on this component's local row wins over the component's own expand state, so a group summary toggles its group instead of the tool carrying it. No provider, wrong version, or invalid shape falls through untouched.
|
|
247
|
+
const explicit = explicitExpandTargetAt(span.component, row - span.start, origin.rect.width);
|
|
248
|
+
if (explicit) return { component: explicit.component, expanded: explicit.expanded };
|
|
304
249
|
if (!isExpandableComponent(span.component)) continue;
|
|
305
250
|
const component = span.component as ExpandableComponent;
|
|
306
251
|
// The hint is matched against this component's own rendered row, not
|
|
@@ -469,11 +469,12 @@ function buildItems(
|
|
|
469
469
|
values: featureStateValues,
|
|
470
470
|
});
|
|
471
471
|
items.push({
|
|
472
|
-
id: "
|
|
473
|
-
label: "Click to
|
|
472
|
+
id: "mouseClickToToggleExpandable",
|
|
473
|
+
label: "Click to toggle expandable",
|
|
474
474
|
description:
|
|
475
|
-
"Click
|
|
476
|
-
|
|
475
|
+
"Click any collapsible component (tool boxes, bash boxes, skill/summary blocks, " +
|
|
476
|
+
"custom entries/messages) to expand or collapse just that one.",
|
|
477
|
+
currentValue: featureValue(config.mouse.clickToToggleExpandable),
|
|
477
478
|
values: featureStateValues,
|
|
478
479
|
});
|
|
479
480
|
}
|
|
@@ -831,11 +832,11 @@ export function registerStarlineSettingsCommand(pi: ExtensionAPI, deps: Settings
|
|
|
831
832
|
tui.requestRender();
|
|
832
833
|
return;
|
|
833
834
|
}
|
|
834
|
-
if (id === "
|
|
835
|
-
deps.setMouseConfig({
|
|
835
|
+
if (id === "mouseClickToToggleExpandable" && isFeatureState(newValue)) {
|
|
836
|
+
deps.setMouseConfig({ clickToToggleExpandable: newValue === "enabled" }, ctx);
|
|
836
837
|
settingsList.updateValue(id, newValue);
|
|
837
838
|
deps.requestRender();
|
|
838
|
-
ctx.ui.notify(`Click to
|
|
839
|
+
ctx.ui.notify(`Click to toggle expandable: ${newValue}`, "info");
|
|
839
840
|
tui.requestRender();
|
|
840
841
|
return;
|
|
841
842
|
}
|
|
@@ -30,6 +30,7 @@ type UserMessageRenderCache = {
|
|
|
30
30
|
width?: number;
|
|
31
31
|
theme?: Theme;
|
|
32
32
|
configKey?: string;
|
|
33
|
+
transformers?: readonly unknown[];
|
|
33
34
|
renderedLines?: string[];
|
|
34
35
|
};
|
|
35
36
|
|
|
@@ -69,6 +70,49 @@ function getCachedMarkdownText(instance: object): string | undefined {
|
|
|
69
70
|
return text;
|
|
70
71
|
}
|
|
71
72
|
|
|
73
|
+
type MarkdownTransformerFn = (
|
|
74
|
+
markdown: string,
|
|
75
|
+
context: {
|
|
76
|
+
messageType: "user" | "assistant" | "assistant-thinking";
|
|
77
|
+
isStreaming: boolean;
|
|
78
|
+
availableWidth: number;
|
|
79
|
+
},
|
|
80
|
+
) => unknown;
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* pi 官方管道:UserMessageComponent 把扩展注册的 markdownTransformers 通过
|
|
84
|
+
* Markdown 的 transform 选项跑一遍(createMarkdownTransform)。我们重写了 render,
|
|
85
|
+
* 必须自己把这条管道接上,否则任何 registerMarkdownTransformer 的扩展
|
|
86
|
+
* (如 pi-ide-context 的选区引用行)在 starline 样式下都会被旁路。
|
|
87
|
+
*/
|
|
88
|
+
function getMarkdownTransformersRef(instance: object): readonly unknown[] | undefined {
|
|
89
|
+
if (!isRecord(instance)) return undefined;
|
|
90
|
+
const t = instance.markdownTransformers;
|
|
91
|
+
return Array.isArray(t) ? t : undefined;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function applyMarkdownTransformers(
|
|
95
|
+
text: string,
|
|
96
|
+
availableWidth: number,
|
|
97
|
+
transformers: readonly unknown[],
|
|
98
|
+
): string {
|
|
99
|
+
let out = text;
|
|
100
|
+
for (const t of transformers) {
|
|
101
|
+
if (typeof t !== "function") continue;
|
|
102
|
+
try {
|
|
103
|
+
const r = (t as MarkdownTransformerFn)(out, {
|
|
104
|
+
messageType: "user",
|
|
105
|
+
isStreaming: false,
|
|
106
|
+
availableWidth,
|
|
107
|
+
});
|
|
108
|
+
if (typeof r === "string") out = r;
|
|
109
|
+
} catch {
|
|
110
|
+
// 与 pi 的 applyMarkdownTransformers 一致:单个 transformer 抛错不影响其他
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return out;
|
|
114
|
+
}
|
|
115
|
+
|
|
72
116
|
function getUserMessageConfigKey(config: PolishedTuiConfig): string {
|
|
73
117
|
return [
|
|
74
118
|
config.features.copyFriendly ? "copy" : "chrome",
|
|
@@ -161,12 +205,14 @@ function renderStarlineUserMessage(
|
|
|
161
205
|
if (width <= 0) return [""];
|
|
162
206
|
|
|
163
207
|
const configKey = getUserMessageConfigKey(config);
|
|
208
|
+
const transformersRef = getMarkdownTransformersRef(instance);
|
|
164
209
|
const cached = userMessageRenderCache.get(instance);
|
|
165
210
|
if (
|
|
166
211
|
cached?.hasMarkdownText &&
|
|
167
212
|
cached.width === width &&
|
|
168
213
|
cached.theme === theme &&
|
|
169
214
|
cached.configKey === configKey &&
|
|
215
|
+
cached.transformers === transformersRef &&
|
|
170
216
|
cached.renderedLines
|
|
171
217
|
) {
|
|
172
218
|
return cached.renderedLines;
|
|
@@ -174,17 +220,30 @@ function renderStarlineUserMessage(
|
|
|
174
220
|
|
|
175
221
|
const railWidth = visibleWidth(renderPromptBoxRail(theme, config));
|
|
176
222
|
const contentWidth = Math.max(1, width - railWidth);
|
|
177
|
-
const renderer = new Markdown(
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
223
|
+
const renderer = new Markdown(
|
|
224
|
+
text,
|
|
225
|
+
0,
|
|
226
|
+
0,
|
|
227
|
+
makeMarkdownTheme(theme),
|
|
228
|
+
{
|
|
229
|
+
color: (content) =>
|
|
230
|
+
theme && config.colors.userMessageText
|
|
231
|
+
? renderStyleForSource(
|
|
232
|
+
theme,
|
|
233
|
+
config.colorSources.userMessages,
|
|
234
|
+
config.colors.userMessageText,
|
|
235
|
+
content,
|
|
236
|
+
)
|
|
237
|
+
: themeFg(theme, "userMessageText", content),
|
|
238
|
+
},
|
|
239
|
+
// 接上官方 transformer 管道;Markdown 每次 render 都会按当前宽度重跑
|
|
240
|
+
{
|
|
241
|
+
transform: transformersRef?.length
|
|
242
|
+
? (markdown, availableWidth) =>
|
|
243
|
+
applyMarkdownTransformers(markdown, availableWidth, transformersRef)
|
|
244
|
+
: undefined,
|
|
245
|
+
},
|
|
246
|
+
);
|
|
188
247
|
const body = renderer.render(contentWidth);
|
|
189
248
|
const contentLines = body.length > 0 ? body : [""];
|
|
190
249
|
const border = theme
|
|
@@ -212,6 +271,7 @@ function renderStarlineUserMessage(
|
|
|
212
271
|
width,
|
|
213
272
|
theme,
|
|
214
273
|
configKey,
|
|
274
|
+
transformers: transformersRef,
|
|
215
275
|
renderedLines: lines,
|
|
216
276
|
});
|
|
217
277
|
return lines;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xzzpig/pi-starline",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"description": "A Starship-inspired statusline and Opencode-style TUI for Pi.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -20,13 +20,15 @@
|
|
|
20
20
|
"tui"
|
|
21
21
|
],
|
|
22
22
|
"files": [
|
|
23
|
-
"extensions"
|
|
23
|
+
"extensions",
|
|
24
|
+
"dist",
|
|
25
|
+
"FORK-CHANGELOG.md"
|
|
24
26
|
],
|
|
25
27
|
"peerDependencies": {
|
|
26
|
-
"@earendil-works/pi-ai": ">=0.
|
|
27
|
-
"@earendil-works/pi-coding-agent": ">=0.
|
|
28
|
-
"@earendil-works/pi-tui": ">=0.
|
|
29
|
-
"@xzzpig/pi-mouse-events": ">=0.
|
|
28
|
+
"@earendil-works/pi-ai": ">=1.0.0 <2",
|
|
29
|
+
"@earendil-works/pi-coding-agent": ">=1.0.0 <2",
|
|
30
|
+
"@earendil-works/pi-tui": ">=1.0.0 <2",
|
|
31
|
+
"@xzzpig/pi-mouse-events": ">=0.2.0 <3"
|
|
30
32
|
},
|
|
31
33
|
"pi": {
|
|
32
34
|
"extensions": [
|
|
@@ -39,13 +41,15 @@
|
|
|
39
41
|
},
|
|
40
42
|
"devDependencies": {
|
|
41
43
|
"@biomejs/biome": "^2.5.0",
|
|
42
|
-
"@earendil-works/pi-ai": "
|
|
43
|
-
"@earendil-works/pi-coding-agent": "
|
|
44
|
-
"@earendil-works/pi-tui": "
|
|
44
|
+
"@earendil-works/pi-ai": "1.1.0",
|
|
45
|
+
"@earendil-works/pi-coding-agent": "1.1.0",
|
|
46
|
+
"@earendil-works/pi-tui": "1.1.0",
|
|
45
47
|
"@types/node": "^26.0.0",
|
|
48
|
+
"rollup": "^4.62.2",
|
|
49
|
+
"rollup-plugin-dts": "^6.4.1",
|
|
46
50
|
"typescript": "^7.0.2",
|
|
47
51
|
"vitest": "^4.1.8",
|
|
48
|
-
"@xzzpig/pi-mouse-events": "0.
|
|
52
|
+
"@xzzpig/pi-mouse-events": "0.2.0"
|
|
49
53
|
},
|
|
50
54
|
"peerDependenciesMeta": {
|
|
51
55
|
"@xzzpig/pi-mouse-events": {
|
|
@@ -55,6 +59,7 @@
|
|
|
55
59
|
"scripts": {
|
|
56
60
|
"lint": "biome check .",
|
|
57
61
|
"typecheck": "tsc --noEmit",
|
|
62
|
+
"build:types": "rollup -c rollup.dts.config.mjs && biome format --write dist/public.d.ts",
|
|
58
63
|
"test": "vitest run",
|
|
59
64
|
"pi:dev": "${PI_BIN:-/opt/homebrew/bin/pi} --no-extensions -e ./extensions/starline/index.ts",
|
|
60
65
|
"pi:install-local": "${PI_BIN:-/opt/homebrew/bin/pi} install ./ -l",
|