pi-one-ui 0.1.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.
Files changed (101) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +106 -0
  3. package/extensions/app/config/renderer.ts +452 -0
  4. package/extensions/app/config/shell.ts +2484 -0
  5. package/extensions/app/ownership.ts +18 -0
  6. package/extensions/app/panel.ts +533 -0
  7. package/extensions/app/presets.ts +36 -0
  8. package/extensions/features/agent-summary/core.ts +194 -0
  9. package/extensions/features/agent-summary/index.ts +37 -0
  10. package/extensions/features/compact-thinking.ts +1262 -0
  11. package/extensions/features/context.ts +857 -0
  12. package/extensions/features/reference/index.ts +643 -0
  13. package/extensions/features/reference/session.ts +278 -0
  14. package/extensions/features/reference/subagent.ts +218 -0
  15. package/extensions/features/shell/aliases.ts +28 -0
  16. package/extensions/features/shell/flush-docked-bash.ts +39 -0
  17. package/extensions/features/shell/startup-header.ts +357 -0
  18. package/extensions/features/shell/working-message.ts +226 -0
  19. package/extensions/index.ts +52 -0
  20. package/extensions/shell/accent-rail-editor.ts +112 -0
  21. package/extensions/shell/accent-rail-layout-patch.ts +530 -0
  22. package/extensions/shell/completion-menu.ts +186 -0
  23. package/extensions/shell/editor-metadata-format.ts +287 -0
  24. package/extensions/shell/editor-transfer.ts +73 -0
  25. package/extensions/shell/extension-status.ts +102 -0
  26. package/extensions/shell/footer-format.ts +388 -0
  27. package/extensions/shell/footer-layout.ts +144 -0
  28. package/extensions/shell/footer.ts +1035 -0
  29. package/extensions/shell/format.ts +577 -0
  30. package/extensions/shell/git.ts +434 -0
  31. package/extensions/shell/icons.ts +261 -0
  32. package/extensions/shell/index.ts +1659 -0
  33. package/extensions/shell/interaction-summary.ts +1194 -0
  34. package/extensions/shell/live-context.ts +82 -0
  35. package/extensions/shell/minimalist-editor.ts +548 -0
  36. package/extensions/shell/package-version.ts +675 -0
  37. package/extensions/shell/project-refresh.ts +130 -0
  38. package/extensions/shell/project-state.ts +60 -0
  39. package/extensions/shell/prototype-patch-registry.ts +183 -0
  40. package/extensions/shell/runtime.ts +915 -0
  41. package/extensions/shell/selector-border.ts +109 -0
  42. package/extensions/shell/session-lifecycle.ts +60 -0
  43. package/extensions/shell/settings-command.ts +2144 -0
  44. package/extensions/shell/settings-previews.ts +137 -0
  45. package/extensions/shell/state.ts +83 -0
  46. package/extensions/shell/style.ts +480 -0
  47. package/extensions/shell/telemetry.ts +90 -0
  48. package/extensions/shell/ui.ts +1326 -0
  49. package/extensions/shell/user-message-osc.ts +279 -0
  50. package/extensions/shell/user-message-styles.ts +266 -0
  51. package/extensions/shell/user-message.ts +226 -0
  52. package/extensions/shell/working-line-messages.ts +24 -0
  53. package/extensions/shell/working-line-spinners.ts +54 -0
  54. package/extensions/shell/working-line.ts +1603 -0
  55. package/extensions/tools/ansi-color.ts +44 -0
  56. package/extensions/tools/ansi-text.ts +52 -0
  57. package/extensions/tools/component-tree.ts +37 -0
  58. package/extensions/tools/format.ts +28 -0
  59. package/extensions/tools/fullscreen-detect.ts +11 -0
  60. package/extensions/tools/patch-keys.ts +158 -0
  61. package/extensions/tools/sgr-mouse.ts +59 -0
  62. package/extensions/tools/tool-loading-icon.ts +20 -0
  63. package/extensions/tools/tool-result-sanitize.ts +27 -0
  64. package/extensions/transcript/index.ts +65 -0
  65. package/extensions/transcript/renderer/compact-mode.ts +1443 -0
  66. package/extensions/transcript/renderer/default-mode.ts +735 -0
  67. package/extensions/transcript/renderer/index.ts +286 -0
  68. package/extensions/transcript/renderer/markdown-enhance.ts +325 -0
  69. package/extensions/transcript/renderer/mouse/hover.ts +175 -0
  70. package/extensions/transcript/renderer/mouse/interaction.ts +1016 -0
  71. package/extensions/transcript/renderer/mouse/layout.ts +207 -0
  72. package/extensions/transcript/renderer/mouse/packets.ts +96 -0
  73. package/extensions/transcript/renderer/mouse/scroll.ts +211 -0
  74. package/extensions/transcript/renderer/tool/diff/ATTRIBUTION.md +28 -0
  75. package/extensions/transcript/renderer/tool/diff/ansi-utils.ts +128 -0
  76. package/extensions/transcript/renderer/tool/diff/diff-component.ts +112 -0
  77. package/extensions/transcript/renderer/tool/diff/diff-edit-render.ts +255 -0
  78. package/extensions/transcript/renderer/tool/diff/diff-header.ts +184 -0
  79. package/extensions/transcript/renderer/tool/diff/diff-highlight.ts +131 -0
  80. package/extensions/transcript/renderer/tool/diff/diff-inline.ts +350 -0
  81. package/extensions/transcript/renderer/tool/diff/diff-layout.ts +870 -0
  82. package/extensions/transcript/renderer/tool/diff/diff-limits.ts +189 -0
  83. package/extensions/transcript/renderer/tool/diff/diff-palette.ts +436 -0
  84. package/extensions/transcript/renderer/tool/diff/diff-parse.ts +536 -0
  85. package/extensions/transcript/renderer/tool/diff/diff-presentation.ts +79 -0
  86. package/extensions/transcript/renderer/tool/diff/diff-renderer.ts +29 -0
  87. package/extensions/transcript/renderer/tool/diff/diff-text.ts +57 -0
  88. package/extensions/transcript/renderer/tool/diff/diff-write-render.ts +629 -0
  89. package/extensions/transcript/renderer/tool/diff/index.ts +147 -0
  90. package/extensions/transcript/renderer/tool/diff/line-width-safety.ts +100 -0
  91. package/extensions/transcript/renderer/tool/diff/shiki-highlight.ts +104 -0
  92. package/extensions/transcript/renderer/tool/diff/write-execution.ts +124 -0
  93. package/extensions/transcript/renderer/tool/grouping.ts +619 -0
  94. package/extensions/transcript/renderer/tool/message-display.ts +183 -0
  95. package/extensions/transcript/renderer/tool/names.ts +195 -0
  96. package/extensions/transcript/renderer/tool/result.ts +842 -0
  97. package/extensions/transcript/renderer/tool/show-more-hint.ts +42 -0
  98. package/extensions/transcript/renderer/transcript-refresh.ts +79 -0
  99. package/package.json +58 -0
  100. package/themes/cc-dark.json +91 -0
  101. package/themes/cc-light.json +91 -0
@@ -0,0 +1,112 @@
1
+ import type { Theme } from "@earendil-works/pi-coding-agent";
2
+ import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
3
+ import {
4
+ applyOwnedSurfaceBackground,
5
+ fillTerminalLine,
6
+ renderCompletionRows,
7
+ } from "./completion-menu.ts";
8
+ import type { ZentuiConfig } from "../app/config/shell.ts";
9
+ import { sanitizeEditorMetadataText } from "./editor-metadata-format.ts";
10
+ import {
11
+ renderStyleForSourceOrFallback,
12
+ type SourceStyleFallback,
13
+ } from "./style.ts";
14
+
15
+ export const ACCENT_RAIL_CHROME_WIDTH = 2;
16
+
17
+ const ACCENT_RAIL_FALLBACK: SourceStyleFallback = {
18
+ theme: "syntaxNumber",
19
+ terminal: "fg:215",
20
+ };
21
+
22
+ export type AccentRailViewport = {
23
+ above?: string;
24
+ below?: string;
25
+ };
26
+
27
+ export type AccentRailEditorFrameOptions = {
28
+ width: number;
29
+ editorLines: string[];
30
+ autocompleteLines?: string[];
31
+ viewport?: AccentRailViewport;
32
+ uiTheme: Theme;
33
+ config: ZentuiConfig;
34
+ };
35
+
36
+ function clampLines(lines: string[], width: number): string[] {
37
+ const maxWidth = Math.max(0, width);
38
+ return lines.map((line) => truncateToWidth(line, maxWidth, ""));
39
+ }
40
+
41
+ function selectedRail(config: ZentuiConfig): string {
42
+ const style = config.components.editor.styles["accent-rail"];
43
+ const fallback = config.icons.mode === "ascii" ? "|" : "▎";
44
+ const configured =
45
+ config.icons.mode === "ascii" ? style.asciiRail : style.rail;
46
+ const sanitized = sanitizeEditorMetadataText(configured);
47
+ const glyph = truncateToWidth(sanitized, 1, "");
48
+ return visibleWidth(glyph) === 1 ? glyph : fallback;
49
+ }
50
+
51
+ function viewportLabel(
52
+ direction: "above" | "below",
53
+ count: string | undefined,
54
+ ): string | undefined {
55
+ if (!count || !/^[1-9]\d*$/.test(count)) return undefined;
56
+ return `${direction === "above" ? "↑" : "↓"} ${count} more`;
57
+ }
58
+
59
+ /** Pure compact accent-rail composition shared by live rendering and settings previews. */
60
+ export function renderAccentRailEditorFrame({
61
+ width,
62
+ editorLines,
63
+ autocompleteLines = [],
64
+ viewport = {},
65
+ uiTheme,
66
+ config,
67
+ }: AccentRailEditorFrameOptions): string[] {
68
+ if (width < ACCENT_RAIL_CHROME_WIDTH + 1) {
69
+ return clampLines([...editorLines, ...autocompleteLines], width);
70
+ }
71
+
72
+ const contentWidth = width - ACCENT_RAIL_CHROME_WIDTH;
73
+ const rail = renderStyleForSourceOrFallback(
74
+ uiTheme,
75
+ config.components.editor.colorSource,
76
+ config.colors.editorRail,
77
+ ACCENT_RAIL_FALLBACK,
78
+ selectedRail(config),
79
+ );
80
+ const above = config.components.editor.viewportIndicators
81
+ ? viewportLabel("above", viewport.above)
82
+ : undefined;
83
+ const below = config.components.editor.viewportIndicators
84
+ ? viewportLabel("below", viewport.below)
85
+ : undefined;
86
+ const rows = [
87
+ ...(above ? [above] : []),
88
+ ...editorLines,
89
+ ...(below ? [below] : []),
90
+ ];
91
+ const transparent =
92
+ config.components.editor.styles["accent-rail"].transparent;
93
+ const surface = rows.map((line) => {
94
+ const railCell = transparent
95
+ ? rail
96
+ : applyOwnedSurfaceBackground(uiTheme, rail);
97
+ const bodyText = ` ${fillTerminalLine(line, contentWidth)}`;
98
+ const body = transparent
99
+ ? bodyText
100
+ : applyOwnedSurfaceBackground(uiTheme, bodyText);
101
+ return `${railCell}${body}`;
102
+ });
103
+ const autocompleteSurface = renderCompletionRows({
104
+ lines: autocompleteLines,
105
+ width,
106
+ theme: uiTheme,
107
+ selectedPrefix: `${rail} `,
108
+ ownedBackground: !transparent,
109
+ });
110
+
111
+ return clampLines([...surface, ...autocompleteSurface], width);
112
+ }
@@ -0,0 +1,530 @@
1
+ import { readFileSync, realpathSync, statSync } from "node:fs";
2
+ import { createRequire } from "node:module";
3
+ import { dirname, join, parse } from "node:path";
4
+ import { pathToFileURL } from "node:url";
5
+
6
+ export const ZENTUI_ACCENT_RAIL_LAYOUT_EDITOR = Symbol.for(
7
+ "pi-zentui.accent-rail-layout-editor",
8
+ );
9
+ const ZENTUI_ACCENT_RAIL_LAYOUT_REGISTRY = Symbol.for(
10
+ "pi-zentui.accent-rail-layout-registry",
11
+ );
12
+ const LAYOUT_NODE = Symbol.for("@earendil-works/pi-tui/layout-node");
13
+
14
+ const SUPPORTED_PI_TUI_VERSION = /^0\.84\.\d+$/;
15
+
16
+ type AccentRailEditorMarker = {
17
+ owner: symbol;
18
+ active: () => boolean;
19
+ };
20
+
21
+ type MarkedEditor = {
22
+ [ZENTUI_ACCENT_RAIL_LAYOUT_EDITOR]?: AccentRailEditorMarker;
23
+ };
24
+
25
+ type StackEntry = {
26
+ component?: unknown;
27
+ minSize?: number;
28
+ shrink?: number;
29
+ [key: PropertyKey]: unknown;
30
+ };
31
+
32
+ type LayoutNode = {
33
+ type?: unknown;
34
+ entries?: unknown;
35
+ [key: PropertyKey]: unknown;
36
+ };
37
+
38
+ type LayoutMethod = (this: unknown, ...args: unknown[]) => unknown;
39
+
40
+ type LayoutPatchRegistry = {
41
+ method: symbol;
42
+ predecessor: LayoutMethod;
43
+ predecessorDescriptor?: PropertyDescriptor;
44
+ wrapper: LayoutMethod;
45
+ owners: Map<symbol, number>;
46
+ active: boolean;
47
+ };
48
+
49
+ type PatchPrototype = Record<PropertyKey, unknown>;
50
+
51
+ type LayoutComponent = {
52
+ render: (...args: unknown[]) => unknown;
53
+ invalidate?: (...args: unknown[]) => unknown;
54
+ };
55
+
56
+ const forwardingComponents = new WeakMap<object, LayoutComponent>();
57
+
58
+ function forwardingLayoutComponent(
59
+ component: object,
60
+ ): LayoutComponent | undefined {
61
+ const cached = forwardingComponents.get(component);
62
+ if (cached) return cached;
63
+ const render = (component as Partial<LayoutComponent>).render;
64
+ if (typeof render !== "function") return undefined;
65
+ try {
66
+ const wrapper: LayoutComponent = {
67
+ render: (...args: unknown[]) => {
68
+ const rows = Reflect.apply(render, component, args);
69
+ return Array.isArray(rows) && rows.length === 1 ? ["", rows[0]] : rows;
70
+ },
71
+ invalidate: (...args: unknown[]) => {
72
+ const invalidate = (component as Partial<LayoutComponent>).invalidate;
73
+ return typeof invalidate === "function"
74
+ ? Reflect.apply(invalidate, component, args)
75
+ : undefined;
76
+ },
77
+ };
78
+ forwardingComponents.set(component, wrapper);
79
+ return wrapper;
80
+ } catch {
81
+ return undefined;
82
+ }
83
+ }
84
+
85
+ export type AccentRailLayoutPatchTarget = {
86
+ prototype: object;
87
+ version: string;
88
+ canonicalEntrypoint?: string;
89
+ resolvedModulePath?: string;
90
+ localModulePath?: string;
91
+ };
92
+
93
+ export type AccentRailLayoutPatchDiagnostic =
94
+ | "installed"
95
+ | "reused"
96
+ | "displaced"
97
+ | "unsupported-version"
98
+ | "unsupported-shape"
99
+ | "host-module-unavailable";
100
+
101
+ export type AccentRailLayoutPatchInstallation = {
102
+ cleanup: () => void;
103
+ diagnostic: AccentRailLayoutPatchDiagnostic;
104
+ version?: string;
105
+ };
106
+
107
+ export type AccentRailLayoutPatchRetention = "retained" | "stale" | "failed";
108
+
109
+ export async function retainAccentRailLayoutPatchInstallation(
110
+ install: () => Promise<AccentRailLayoutPatchInstallation>,
111
+ isCurrent: () => boolean,
112
+ retain: (installation: AccentRailLayoutPatchInstallation) => void,
113
+ ): Promise<AccentRailLayoutPatchRetention> {
114
+ let installation: AccentRailLayoutPatchInstallation;
115
+ try {
116
+ installation = await install();
117
+ } catch {
118
+ return "failed";
119
+ }
120
+ if (!isCurrent()) {
121
+ installation.cleanup();
122
+ return "stale";
123
+ }
124
+ try {
125
+ retain(installation);
126
+ } catch {
127
+ installation.cleanup();
128
+ return "failed";
129
+ }
130
+ return "retained";
131
+ }
132
+
133
+ function ownMarker(value: unknown): AccentRailEditorMarker | undefined {
134
+ if (!value || typeof value !== "object") return undefined;
135
+ if (!Object.hasOwn(value, ZENTUI_ACCENT_RAIL_LAYOUT_EDITOR)) {
136
+ return undefined;
137
+ }
138
+ const marker = (value as MarkedEditor)[ZENTUI_ACCENT_RAIL_LAYOUT_EDITOR];
139
+ return marker &&
140
+ typeof marker.owner === "symbol" &&
141
+ typeof marker.active === "function"
142
+ ? marker
143
+ : undefined;
144
+ }
145
+
146
+ function entryMarker(entry: StackEntry): AccentRailEditorMarker | undefined {
147
+ const component = entry.component;
148
+ if (!component || typeof component !== "object") return undefined;
149
+ if (!Object.hasOwn(component, "children")) return undefined;
150
+ const children = (component as { children?: unknown }).children;
151
+ if (!Array.isArray(children) || children.length !== 1) return undefined;
152
+ return ownMarker(children[0]);
153
+ }
154
+
155
+ export function adjustAccentRailLayoutNode(
156
+ node: unknown,
157
+ activeOwners: ReadonlySet<symbol>,
158
+ ): unknown {
159
+ if (!node || typeof node !== "object") return node;
160
+ const candidate = node as LayoutNode;
161
+ if (candidate.type !== "vstack" || !Array.isArray(candidate.entries))
162
+ return node;
163
+
164
+ const entries = candidate.entries as StackEntry[];
165
+ const matches: number[] = [];
166
+ for (let index = 0; index < entries.length; index += 1) {
167
+ const entry = entries[index];
168
+ if (entry?.minSize !== 3 || entry.shrink !== 1) continue;
169
+ const marker = entryMarker(entry);
170
+ if (!marker || !activeOwners.has(marker.owner)) continue;
171
+ let active = false;
172
+ try {
173
+ active = marker.active() === true;
174
+ } catch {
175
+ return node;
176
+ }
177
+ if (active) matches.push(index);
178
+ }
179
+ if (matches.length !== 1) return node;
180
+
181
+ const match = matches[0];
182
+ const matchedEntry = entries[match];
183
+ if (!matchedEntry?.component || typeof matchedEntry.component !== "object")
184
+ return node;
185
+ const component = forwardingLayoutComponent(matchedEntry.component);
186
+ if (!component) return node;
187
+ const nextEntries = entries.map((entry, index) =>
188
+ index === match ? { ...entry, component } : entry,
189
+ );
190
+ return { ...candidate, entries: nextEntries };
191
+ }
192
+
193
+ export function markAccentRailLayoutEditor(
194
+ editor: object,
195
+ owner: symbol,
196
+ active: () => boolean,
197
+ ): boolean {
198
+ try {
199
+ Object.defineProperty(editor, ZENTUI_ACCENT_RAIL_LAYOUT_EDITOR, {
200
+ value: { owner, active } satisfies AccentRailEditorMarker,
201
+ configurable: true,
202
+ enumerable: false,
203
+ writable: false,
204
+ });
205
+ return true;
206
+ } catch {
207
+ return false;
208
+ }
209
+ }
210
+
211
+ function registryOn(
212
+ prototype: PatchPrototype,
213
+ ): LayoutPatchRegistry | undefined {
214
+ const value = prototype[ZENTUI_ACCENT_RAIL_LAYOUT_REGISTRY];
215
+ if (!value || typeof value !== "object") return undefined;
216
+ const registry = value as Partial<LayoutPatchRegistry>;
217
+ return registry.owners instanceof Map &&
218
+ typeof registry.wrapper === "function"
219
+ ? (value as LayoutPatchRegistry)
220
+ : undefined;
221
+ }
222
+
223
+ function discoverLayoutMethod(prototype: PatchPrototype): symbol | undefined {
224
+ const symbols = new Set<PropertyKey>([
225
+ LAYOUT_NODE,
226
+ ...Object.getOwnPropertySymbols(prototype),
227
+ ]);
228
+ for (const key of symbols) {
229
+ const method = prototype[key];
230
+ if (typeof method !== "function") continue;
231
+ try {
232
+ const receiver = Object.create(prototype) as Record<PropertyKey, unknown>;
233
+ receiver.entries = [];
234
+ receiver.layoutType = "vstack";
235
+ receiver.gap = 0;
236
+ receiver.align = "stretch";
237
+ const node = Reflect.apply(method as LayoutMethod, receiver, []);
238
+ if (
239
+ node &&
240
+ typeof node === "object" &&
241
+ (node as LayoutNode).type === "vstack" &&
242
+ Array.isArray((node as LayoutNode).entries)
243
+ ) {
244
+ return key as symbol;
245
+ }
246
+ } catch {
247
+ // Probe candidates fail open.
248
+ }
249
+ }
250
+ return undefined;
251
+ }
252
+
253
+ function restorePredecessor(
254
+ prototype: PatchPrototype,
255
+ registry: LayoutPatchRegistry,
256
+ ): void {
257
+ if (prototype[registry.method] !== registry.wrapper) return;
258
+ if (registry.predecessorDescriptor) {
259
+ Object.defineProperty(
260
+ prototype,
261
+ registry.method,
262
+ registry.predecessorDescriptor,
263
+ );
264
+ } else {
265
+ delete prototype[registry.method];
266
+ }
267
+ }
268
+
269
+ function cleanupOwner(
270
+ prototype: PatchPrototype,
271
+ registry: LayoutPatchRegistry,
272
+ owner: symbol,
273
+ ): () => void {
274
+ let cleaned = false;
275
+ return () => {
276
+ if (cleaned) return;
277
+ cleaned = true;
278
+ const count = registry.owners.get(owner) ?? 0;
279
+ if (count <= 1) registry.owners.delete(owner);
280
+ else registry.owners.set(owner, count - 1);
281
+ if (registry.owners.size > 0) return;
282
+
283
+ // Deactivate before restoration. If a successor patch captured and later
284
+ // restores our wrapper, the ownerless wrapper remains an inert pass-through.
285
+ registry.active = false;
286
+ if (registryOn(prototype) !== registry) return;
287
+ restorePredecessor(prototype, registry);
288
+ if (prototype[ZENTUI_ACCENT_RAIL_LAYOUT_REGISTRY] === registry) {
289
+ delete prototype[ZENTUI_ACCENT_RAIL_LAYOUT_REGISTRY];
290
+ }
291
+ };
292
+ }
293
+
294
+ export function installAccentRailLayoutPatchOnTarget(
295
+ target: AccentRailLayoutPatchTarget,
296
+ owner: symbol,
297
+ ): AccentRailLayoutPatchInstallation {
298
+ if (!SUPPORTED_PI_TUI_VERSION.test(target.version)) {
299
+ return {
300
+ cleanup: () => {},
301
+ diagnostic: "unsupported-version",
302
+ version: target.version,
303
+ };
304
+ }
305
+ const prototype = target.prototype as PatchPrototype;
306
+ const existing = registryOn(prototype);
307
+ if (existing) {
308
+ if (existing.active && prototype[existing.method] === existing.wrapper) {
309
+ existing.owners.set(owner, (existing.owners.get(owner) ?? 0) + 1);
310
+ return {
311
+ cleanup: cleanupOwner(prototype, existing, owner),
312
+ diagnostic: "reused",
313
+ version: target.version,
314
+ };
315
+ }
316
+
317
+ // A later extension displaced the method. Never wrap that successor and
318
+ // never leave our old wrapper active if the successor restores it later.
319
+ existing.active = false;
320
+ if (prototype[existing.method] !== existing.wrapper) {
321
+ if (prototype[ZENTUI_ACCENT_RAIL_LAYOUT_REGISTRY] === existing) {
322
+ delete prototype[ZENTUI_ACCENT_RAIL_LAYOUT_REGISTRY];
323
+ }
324
+ return {
325
+ cleanup: () => {},
326
+ diagnostic: "displaced",
327
+ version: target.version,
328
+ };
329
+ }
330
+
331
+ // An inactive wrapper restored by a former successor can be safely reduced
332
+ // to its original predecessor before a fresh installation.
333
+ restorePredecessor(prototype, existing);
334
+ if (prototype[ZENTUI_ACCENT_RAIL_LAYOUT_REGISTRY] === existing) {
335
+ delete prototype[ZENTUI_ACCENT_RAIL_LAYOUT_REGISTRY];
336
+ }
337
+ }
338
+
339
+ const method = discoverLayoutMethod(prototype);
340
+ if (!method) {
341
+ return {
342
+ cleanup: () => {},
343
+ diagnostic: "unsupported-shape",
344
+ version: target.version,
345
+ };
346
+ }
347
+ const predecessorDescriptor = Object.getOwnPropertyDescriptor(
348
+ prototype,
349
+ method,
350
+ );
351
+ const predecessor = prototype[method];
352
+ if (
353
+ typeof predecessor !== "function" ||
354
+ predecessorDescriptor?.configurable === false
355
+ ) {
356
+ return {
357
+ cleanup: () => {},
358
+ diagnostic: "unsupported-shape",
359
+ version: target.version,
360
+ };
361
+ }
362
+
363
+ const registry: LayoutPatchRegistry = {
364
+ method,
365
+ predecessor: predecessor as LayoutMethod,
366
+ predecessorDescriptor,
367
+ wrapper: () => undefined,
368
+ owners: new Map([[owner, 1]]),
369
+ active: true,
370
+ };
371
+ registry.wrapper = function accentRailLayoutNodePatch(
372
+ this: unknown,
373
+ ...args: unknown[]
374
+ ) {
375
+ const node = Reflect.apply(registry.predecessor, this, args);
376
+ if (!registry.active || registry.owners.size === 0) return node;
377
+ try {
378
+ return adjustAccentRailLayoutNode(node, new Set(registry.owners.keys()));
379
+ } catch {
380
+ return node;
381
+ }
382
+ };
383
+ try {
384
+ Object.defineProperty(prototype, method, {
385
+ ...(predecessorDescriptor ?? {
386
+ configurable: true,
387
+ enumerable: false,
388
+ writable: true,
389
+ }),
390
+ value: registry.wrapper,
391
+ });
392
+ Object.defineProperty(prototype, ZENTUI_ACCENT_RAIL_LAYOUT_REGISTRY, {
393
+ value: registry,
394
+ configurable: true,
395
+ enumerable: false,
396
+ });
397
+ } catch {
398
+ registry.active = false;
399
+ restorePredecessor(prototype, registry);
400
+ return {
401
+ cleanup: () => {},
402
+ diagnostic: "unsupported-shape",
403
+ version: target.version,
404
+ };
405
+ }
406
+ return {
407
+ cleanup: cleanupOwner(prototype, registry, owner),
408
+ diagnostic: "installed",
409
+ version: target.version,
410
+ };
411
+ }
412
+
413
+ type PackageLocation = {
414
+ root: string;
415
+ version: string;
416
+ };
417
+
418
+ function packageLocationFromEntry(
419
+ entry: string,
420
+ expectedName: string,
421
+ ): PackageLocation | undefined {
422
+ const root = parse(entry).root;
423
+ let current = dirname(entry);
424
+ while (current !== root) {
425
+ try {
426
+ const packageJson = JSON.parse(
427
+ readFileSync(join(current, "package.json"), "utf8"),
428
+ ) as {
429
+ name?: unknown;
430
+ version?: unknown;
431
+ };
432
+ if (
433
+ packageJson.name === expectedName &&
434
+ typeof packageJson.version === "string"
435
+ ) {
436
+ return { root: current, version: packageJson.version };
437
+ }
438
+ } catch {
439
+ // Continue walking to the package root.
440
+ }
441
+ current = dirname(current);
442
+ }
443
+ return undefined;
444
+ }
445
+
446
+ function canonicalFile(path: string): string | undefined {
447
+ try {
448
+ const canonical = realpathSync(path);
449
+ return statSync(canonical).isFile() ? canonical : undefined;
450
+ } catch {
451
+ return undefined;
452
+ }
453
+ }
454
+
455
+ function localPiTuiPath(): string | undefined {
456
+ try {
457
+ return canonicalFile(
458
+ createRequire(import.meta.url).resolve("@earendil-works/pi-tui"),
459
+ );
460
+ } catch {
461
+ return undefined;
462
+ }
463
+ }
464
+
465
+ export async function discoverAccentRailLayoutPatchTargetFromEntrypoint(
466
+ entrypoint: string,
467
+ ): Promise<AccentRailLayoutPatchTarget | undefined> {
468
+ try {
469
+ const canonicalEntrypoint = canonicalFile(entrypoint);
470
+ if (!canonicalEntrypoint) return undefined;
471
+ if (
472
+ !packageLocationFromEntry(
473
+ canonicalEntrypoint,
474
+ "@earendil-works/pi-coding-agent",
475
+ )
476
+ ) {
477
+ return undefined;
478
+ }
479
+
480
+ const hostRequire = createRequire(canonicalEntrypoint);
481
+ const unresolved = hostRequire.resolve("@earendil-works/pi-tui");
482
+ const resolvedModulePath = canonicalFile(unresolved);
483
+ if (!resolvedModulePath) return undefined;
484
+ const packageLocation = packageLocationFromEntry(
485
+ resolvedModulePath,
486
+ "@earendil-works/pi-tui",
487
+ );
488
+ if (
489
+ !packageLocation ||
490
+ !SUPPORTED_PI_TUI_VERSION.test(packageLocation.version)
491
+ ) {
492
+ return undefined;
493
+ }
494
+
495
+ const module = (await import(pathToFileURL(resolvedModulePath).href)) as {
496
+ VStack?: { prototype?: object };
497
+ };
498
+ const prototype = module.VStack?.prototype;
499
+ if (!prototype || !discoverLayoutMethod(prototype as PatchPrototype))
500
+ return undefined;
501
+
502
+ return {
503
+ prototype,
504
+ version: packageLocation.version,
505
+ canonicalEntrypoint,
506
+ resolvedModulePath,
507
+ localModulePath: localPiTuiPath(),
508
+ };
509
+ } catch {
510
+ return undefined;
511
+ }
512
+ }
513
+
514
+ export async function discoverHostAccentRailLayoutPatchTarget(): Promise<
515
+ AccentRailLayoutPatchTarget | undefined
516
+ > {
517
+ const entrypoint = process.argv[1];
518
+ return entrypoint
519
+ ? discoverAccentRailLayoutPatchTargetFromEntrypoint(entrypoint)
520
+ : undefined;
521
+ }
522
+
523
+ export async function installHostAccentRailLayoutPatch(
524
+ owner: symbol,
525
+ ): Promise<AccentRailLayoutPatchInstallation> {
526
+ const target = await discoverHostAccentRailLayoutPatchTarget();
527
+ return target
528
+ ? installAccentRailLayoutPatchOnTarget(target, owner)
529
+ : { cleanup: () => {}, diagnostic: "host-module-unavailable" };
530
+ }