@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.
@@ -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": { "clickToExpandTools": false }
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.
@@ -0,0 +1,5 @@
1
+ import { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+
3
+ declare function export_default(pi: ExtensionAPI): void;
4
+
5
+ export { export_default as default };
@@ -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
- /** Clicking a tool box's `ctrl+o to expand` hint expands that one box. */
113
- clickToExpandTools: boolean;
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
- clickToExpandTools: "clickToExpandTools",
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
- clickToExpandTools: true,
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
- clickToExpandTools:
842
- typeof record.clickToExpandTools === "boolean"
843
- ? record.clickToExpandTools
844
- : defaultConfig.mouse.clickToExpandTools,
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.clickToExpandTools !== undefined
1310
- ? { clickToExpandTools: patch.clickToExpandTools }
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.v1");
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 !== 1) {
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
- | "clickToExpandTools"
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
- clickToExpandTools: ["hasOverlay", "requestRender"],
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: object, name: string): boolean {
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: object): ReadonlySet<MouseCapability> {
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
- * `clickToExpandTools` → `addMouseHandler`
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, keyTextFor } from "./tool-box";
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.clickToExpandTools) return undefined;
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("clickToExpandTools")) {
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
- * ## 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.
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 { keyText } from "@earendil-works/pi-coding-agent";
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: "mouseClickToExpandTools",
473
- label: "Click to expand tools",
472
+ id: "mouseClickToToggleExpandable",
473
+ label: "Click to toggle expandable",
474
474
  description:
475
- "Click a tool box's border or its expand hint to expand that one box. Needs wheel routing.",
476
- currentValue: featureValue(config.mouse.clickToExpandTools),
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 === "mouseClickToExpandTools" && isFeatureState(newValue)) {
835
- deps.setMouseConfig({ clickToExpandTools: newValue === "enabled" }, ctx);
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 expand tools: ${newValue}`, "info");
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(text, 0, 0, makeMarkdownTheme(theme), {
178
- color: (content) =>
179
- theme && config.colors.userMessageText
180
- ? renderStyleForSource(
181
- theme,
182
- config.colorSources.userMessages,
183
- config.colors.userMessageText,
184
- content,
185
- )
186
- : themeFg(theme, "userMessageText", content),
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.4.1",
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.84.4",
27
- "@earendil-works/pi-coding-agent": ">=0.84.4",
28
- "@earendil-works/pi-tui": ">=0.84.4",
29
- "@xzzpig/pi-mouse-events": ">=0.1.0 <2"
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": "^0.84.4",
43
- "@earendil-works/pi-coding-agent": "^0.84.4",
44
- "@earendil-works/pi-tui": "^0.84.4",
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.1.2"
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",