@quandev104/pi-style 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 (69) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/LICENSE +21 -0
  3. package/README.md +193 -0
  4. package/dist/extensions/pi-style.js +7897 -0
  5. package/dist/extensions/pi-style.js.map +1 -0
  6. package/extension-src/pi-style/app/command-service.ts +244 -0
  7. package/extension-src/pi-style/app/commands.ts +12 -0
  8. package/extension-src/pi-style/app/config-storage.ts +98 -0
  9. package/extension-src/pi-style/app/doctor.ts +53 -0
  10. package/extension-src/pi-style/app/index.ts +322 -0
  11. package/extension-src/pi-style/app/providers.ts +268 -0
  12. package/extension-src/pi-style/app/render-scheduler.ts +48 -0
  13. package/extension-src/pi-style/app/runtime.ts +404 -0
  14. package/extension-src/pi-style/app/snapshot.ts +15 -0
  15. package/extension-src/pi-style/domain/capabilities.ts +34 -0
  16. package/extension-src/pi-style/domain/config-authorization.ts +20 -0
  17. package/extension-src/pi-style/domain/config-diagnostics.ts +20 -0
  18. package/extension-src/pi-style/domain/config-diff.ts +30 -0
  19. package/extension-src/pi-style/domain/config-migrations.ts +52 -0
  20. package/extension-src/pi-style/domain/config-normalization.ts +567 -0
  21. package/extension-src/pi-style/domain/config-presets.ts +51 -0
  22. package/extension-src/pi-style/domain/config-types.ts +115 -0
  23. package/extension-src/pi-style/domain/providers.ts +37 -0
  24. package/extension-src/pi-style/domain/status-presets.ts +60 -0
  25. package/extension-src/pi-style/domain/status-renderer.ts +142 -0
  26. package/extension-src/pi-style/domain/status.ts +430 -0
  27. package/extension-src/pi-style/domain/theme.ts +232 -0
  28. package/extension-src/pi-style/features/.gitkeep +0 -0
  29. package/extension-src/pi-style/features/editor/index.ts +304 -0
  30. package/extension-src/pi-style/features/messages/boxed-block.ts +74 -0
  31. package/extension-src/pi-style/features/messages/index.ts +145 -0
  32. package/extension-src/pi-style/features/messages/special-blocks.ts +281 -0
  33. package/extension-src/pi-style/features/startup/index.ts +564 -0
  34. package/extension-src/pi-style/features/startup/logo.ts +151 -0
  35. package/extension-src/pi-style/features/status-line/index.ts +319 -0
  36. package/extension-src/pi-style/features/tools/boxed/bash.ts +347 -0
  37. package/extension-src/pi-style/features/tools/boxed/edit.ts +113 -0
  38. package/extension-src/pi-style/features/tools/boxed/fallback.ts +67 -0
  39. package/extension-src/pi-style/features/tools/boxed/find.ts +65 -0
  40. package/extension-src/pi-style/features/tools/boxed/grep.ts +108 -0
  41. package/extension-src/pi-style/features/tools/boxed/index.ts +64 -0
  42. package/extension-src/pi-style/features/tools/boxed/ls.ts +65 -0
  43. package/extension-src/pi-style/features/tools/boxed/quick-edit.ts +190 -0
  44. package/extension-src/pi-style/features/tools/boxed/read.ts +204 -0
  45. package/extension-src/pi-style/features/tools/boxed/session-config.ts +45 -0
  46. package/extension-src/pi-style/features/tools/boxed/shared.ts +157 -0
  47. package/extension-src/pi-style/features/tools/boxed/write.ts +89 -0
  48. package/extension-src/pi-style/features/tools/index.ts +480 -0
  49. package/extension-src/pi-style/pi/commands.ts +17 -0
  50. package/extension-src/pi-style/pi/compatibility-coordinator.ts +195 -0
  51. package/extension-src/pi-style/pi/compatibility-probe.ts +645 -0
  52. package/extension-src/pi-style/pi/compatibility-registry.ts +329 -0
  53. package/extension-src/pi-style/pi/config-host.ts +52 -0
  54. package/extension-src/pi-style/pi/config-session.ts +105 -0
  55. package/extension-src/pi-style/pi/index.ts +77 -0
  56. package/extension-src/pi-style/pi/operational-state.ts +29 -0
  57. package/extension-src/pi-style/pi/session-coordinator.ts +187 -0
  58. package/extension-src/pi-style/pi/session-usage.ts +62 -0
  59. package/extension-src/pi-style/pi/startup-resources.ts +70 -0
  60. package/extension-src/pi-style/shared/.gitkeep +0 -0
  61. package/extension-src/pi-style/shared/ansi.ts +386 -0
  62. package/extension-src/pi-style/shared/box.ts +805 -0
  63. package/extension-src/pi-style/shared/disposable-store.ts +28 -0
  64. package/extension-src/pi-style/shared/elapsed.ts +88 -0
  65. package/extension-src/pi-style/shared/render-budget.ts +367 -0
  66. package/extension-src/pi-style/shared/split-diff.ts +692 -0
  67. package/extension-src/pi-style/shared/theme-extras.ts +292 -0
  68. package/package.json +68 -0
  69. package/themes/.gitkeep +0 -0
@@ -0,0 +1,480 @@
1
+ import { visibleWidth } from "@earendil-works/pi-tui";
2
+ import { renderBoxedToolCall, renderBoxedToolResult } from "./boxed/index.js";
3
+
4
+ /**
5
+ * Neutralize the native ToolExecutionComponent status background for boxed
6
+ * rendering: Pi's updateDisplay sets contentBox/selfRenderContainer bgFn to
7
+ * toolPendingBg/toolErrorBg/toolSuccessBg before invoking the renderers. The
8
+ * boxed renderers own their visual boundary (borders + ✓/✗ state marks), so the
9
+ * container fill is removed (no background slab).
10
+ * Runs on every boxed dispatch; updateDisplay re-applies the bgFn on the next
11
+ * pass and this wrapper re-neutralizes it.
12
+ */
13
+ function neutralizeToolContainerBackground(instance: object): void {
14
+ const host = instance as {
15
+ getRenderShell?(): string;
16
+ selfRenderContainer?: { setBgFn?(fn: (text: string) => string): void; paddingX?: number; paddingY?: number };
17
+ contentBox?: { setBgFn?(fn: (text: string) => string): void; paddingX?: number; paddingY?: number };
18
+ };
19
+ const container =
20
+ typeof host.getRenderShell === "function" && host.getRenderShell() === "self"
21
+ ? host.selfRenderContainer
22
+ : host.contentBox;
23
+ if (!container) return;
24
+ container.paddingX = 0;
25
+ container.paddingY = 0;
26
+ if (typeof container.setBgFn === "function") container.setBgFn((text) => text);
27
+ }
28
+
29
+ type RenderFunction = (this: object, ...args: unknown[]) => unknown;
30
+ type RendererSubtype = "tool-call-renderer" | "tool-result-renderer";
31
+ type RendererKind = "call" | "result";
32
+ type TransactionState = "never-written" | "installed-owned" | "owner-changed" | "rollback-restored" | "rollback-failed";
33
+ type ToolRenderContext = {
34
+ args: object;
35
+ toolCallId: string;
36
+ invalidate: () => void;
37
+ lastComponent: object | undefined;
38
+ state: object;
39
+ cwd: string;
40
+ executionStarted: boolean;
41
+ argsComplete: boolean;
42
+ isPartial: boolean;
43
+ expanded: boolean;
44
+ showImages: boolean;
45
+ isError: boolean;
46
+ };
47
+ type ToolDecorationSnapshot = Readonly<{
48
+ callMarker: string;
49
+ resultMarker: string;
50
+ /** marker = prefix lines; compact-box = boxed tool call/result rendering. */
51
+ style?: "marker" | "compact-box";
52
+ }>;
53
+ type DescriptorView = Readonly<{
54
+ kind: "data" | "accessor";
55
+ value?: unknown;
56
+ get?: unknown;
57
+ set?: unknown;
58
+ writable?: boolean;
59
+ enumerable: boolean;
60
+ configurable: boolean;
61
+ }>;
62
+ type FailureEntry = Readonly<{ reason: string; transaction: TransactionState }>;
63
+ export type ToolDiagnosticArchive = Readonly<{
64
+ owner: string;
65
+ generation: number;
66
+ reasons: Readonly<Record<string, number>>;
67
+ restored: number;
68
+ failed: number;
69
+ laterOwner: number;
70
+ failures: readonly FailureEntry[];
71
+ }>;
72
+ type CleanupOutcome = "restored" | "later-owner" | "retry";
73
+ type DecorationRecord = {
74
+ component: object;
75
+ nativeRender: RenderFunction;
76
+ wrappedRender: RenderFunction;
77
+ attemptedDescriptor: PropertyDescriptor;
78
+ originalOwnDescriptor?: PropertyDescriptor;
79
+ inheritedDescriptor?: PropertyDescriptor;
80
+ context: ToolRenderContext;
81
+ transaction: TransactionState;
82
+ lastFailureSignature?: string;
83
+ };
84
+ type DecorationState = {
85
+ snapshot: ToolDecorationSnapshot;
86
+ decorated: WeakMap<object, DecorationRecord>;
87
+ active: Set<DecorationRecord>;
88
+ diagnostics: Map<string, number>;
89
+ failures: FailureEntry[];
90
+ archive?: ToolDiagnosticArchive;
91
+ restored: number;
92
+ failed: number;
93
+ laterOwner: number;
94
+ generation: number;
95
+ };
96
+ const DEFAULT_SNAPSHOT: ToolDecorationSnapshot = Object.freeze({
97
+ callMarker: "[tool] ",
98
+ resultMarker: "[tool:result] ",
99
+ style: "marker",
100
+ });
101
+ let ownerGeneration = 0;
102
+ const piStyleWrappers = new WeakSet<RenderFunction>();
103
+ let toolTestHooks: { defineProperty?: typeof Reflect.defineProperty; deleteProperty?: typeof Reflect.deleteProperty } =
104
+ {};
105
+ export function __setToolDecorationTestHooks(hooks: typeof toolTestHooks): () => void {
106
+ const previous = toolTestHooks;
107
+ toolTestHooks = hooks;
108
+ return () => {
109
+ toolTestHooks = previous;
110
+ };
111
+ }
112
+
113
+ function note(state: DecorationState, reason: string, transaction: TransactionState = "never-written"): void {
114
+ state.diagnostics.set(reason, (state.diagnostics.get(reason) ?? 0) + 1);
115
+ if (state.failures.length < 32) state.failures.push(Object.freeze({ reason, transaction }));
116
+ }
117
+ function isObject(value: unknown): value is object {
118
+ return typeof value === "object" && value !== null;
119
+ }
120
+ function ownData(object: object, key: string): unknown {
121
+ const descriptor = Object.getOwnPropertyDescriptor(object, key);
122
+ return descriptor && "value" in descriptor ? descriptor.value : undefined;
123
+ }
124
+ function hasOwnData(object: object, key: string): boolean {
125
+ const descriptor = Object.getOwnPropertyDescriptor(object, key);
126
+ return descriptor !== undefined && "value" in descriptor;
127
+ }
128
+ function validContext(value: unknown): value is ToolRenderContext {
129
+ if (!isObject(value)) return false;
130
+ const booleans = ["isPartial", "expanded", "isError", "executionStarted", "argsComplete", "showImages"] as const;
131
+ return (
132
+ isObject(ownData(value, "args")) &&
133
+ typeof ownData(value, "toolCallId") === "string" &&
134
+ typeof ownData(value, "invalidate") === "function" &&
135
+ isObject(ownData(value, "state")) &&
136
+ typeof ownData(value, "cwd") === "string" &&
137
+ booleans.every((key) => typeof ownData(value, key) === "boolean") &&
138
+ hasOwnData(value, "lastComponent") &&
139
+ (ownData(value, "lastComponent") === undefined || isObject(ownData(value, "lastComponent")))
140
+ );
141
+ }
142
+ function validCallArgs(args: unknown[]): args is [object, object, ToolRenderContext] {
143
+ return args.length === 3 && isObject(args[0]) && isObject(args[1]) && validContext(args[2]);
144
+ }
145
+ function validResultArgs(args: unknown[]): args is [object, object, object, ToolRenderContext] {
146
+ const [result, options, theme, context] = args;
147
+ return (
148
+ isObject(result) &&
149
+ hasOwnData(result, "content") &&
150
+ Array.isArray(ownData(result, "content")) &&
151
+ hasOwnData(result, "details") &&
152
+ isObject(options) &&
153
+ typeof ownData(options, "expanded") === "boolean" &&
154
+ typeof ownData(options, "isPartial") === "boolean" &&
155
+ isObject(theme) &&
156
+ validContext(context)
157
+ );
158
+ }
159
+ function descriptorView(descriptor: PropertyDescriptor | undefined): DescriptorView | undefined {
160
+ if (!descriptor) return undefined;
161
+ if ("value" in descriptor)
162
+ return Object.freeze({
163
+ kind: "data",
164
+ value: descriptor.value,
165
+ writable: descriptor.writable === true,
166
+ enumerable: descriptor.enumerable === true,
167
+ configurable: descriptor.configurable === true,
168
+ });
169
+ return Object.freeze({
170
+ kind: "accessor",
171
+ get: descriptor.get,
172
+ set: descriptor.set,
173
+ enumerable: descriptor.enumerable === true,
174
+ configurable: descriptor.configurable === true,
175
+ });
176
+ }
177
+ function descriptorEqual(actual: PropertyDescriptor | undefined, expected: PropertyDescriptor | undefined): boolean {
178
+ const a = descriptorView(actual),
179
+ e = descriptorView(expected);
180
+ return (
181
+ a?.kind === e?.kind &&
182
+ a?.value === e?.value &&
183
+ a?.get === e?.get &&
184
+ a?.set === e?.set &&
185
+ a?.writable === e?.writable &&
186
+ a?.enumerable === e?.enumerable &&
187
+ a?.configurable === e?.configurable
188
+ );
189
+ }
190
+ function renderDescriptor(
191
+ component: object,
192
+ ): { own?: PropertyDescriptor; inherited?: PropertyDescriptor; native?: RenderFunction } | false {
193
+ const own = Object.getOwnPropertyDescriptor(component, "render");
194
+ if (own && !("value" in own && typeof own.value === "function" && own.writable === true && own.configurable === true))
195
+ return false;
196
+ let cursor = Object.getPrototypeOf(component);
197
+ let inherited: PropertyDescriptor | undefined;
198
+ while (cursor && !inherited) {
199
+ const descriptor = Object.getOwnPropertyDescriptor(cursor, "render");
200
+ if (descriptor) inherited = descriptor;
201
+ cursor = Object.getPrototypeOf(cursor);
202
+ }
203
+ if (!own && inherited && !("value" in inherited && typeof inherited.value === "function")) return false;
204
+ const native = own?.value ?? (inherited && "value" in inherited ? inherited.value : undefined);
205
+ return typeof native === "function"
206
+ ? { ...(own ? { own } : {}), ...(inherited ? { inherited } : {}), native }
207
+ : false;
208
+ }
209
+ function marker(state: DecorationState, context: ToolRenderContext, kind: RendererKind): string {
210
+ if (context.isError) return "[tool:error] ";
211
+ if (context.isPartial) return context.executionStarted ? "[tool:running] " : "[tool:pending] ";
212
+ return kind === "call" ? state.snapshot.callMarker : state.snapshot.resultMarker;
213
+ }
214
+ function decorateLines(state: DecorationState, value: unknown, width: unknown, prefix: string): unknown {
215
+ if (!Array.isArray(value) || !value.every((line) => typeof line === "string")) {
216
+ note(state, "malformed-render");
217
+ return value;
218
+ }
219
+ if (typeof width !== "number" || !Number.isFinite(width) || width < 0) {
220
+ note(state, "invalid-width");
221
+ return value;
222
+ }
223
+ const lines = value as string[],
224
+ prefixWidth = visibleWidth(prefix),
225
+ bodyWidth = width - prefixWidth;
226
+ if (width <= prefixWidth || !lines.every((line) => visibleWidth(line) <= bodyWidth)) {
227
+ note(state, "reduced-native-fallback");
228
+ return value;
229
+ }
230
+ const output =
231
+ lines.length === 0 ? [prefix.trimEnd()] : lines.map((line, index) => (index === 0 ? `${prefix}${line}` : line));
232
+ if (output.every((line) => visibleWidth(line) <= width)) return output;
233
+ note(state, "overwide-decoration");
234
+ return value;
235
+ }
236
+ function createRecord(
237
+ state: DecorationState,
238
+ component: object,
239
+ nativeRender: RenderFunction,
240
+ context: ToolRenderContext,
241
+ kind: RendererKind,
242
+ descriptors: { own?: PropertyDescriptor; inherited?: PropertyDescriptor },
243
+ ): DecorationRecord {
244
+ const attemptedDescriptor = descriptors.own
245
+ ? { ...descriptors.own, value: undefined as unknown as RenderFunction }
246
+ : { value: undefined as unknown as RenderFunction, writable: true, enumerable: false, configurable: true };
247
+ const record = {
248
+ component,
249
+ nativeRender,
250
+ wrappedRender: undefined as unknown as RenderFunction,
251
+ attemptedDescriptor,
252
+ ...(descriptors.own ? { originalOwnDescriptor: descriptors.own } : {}),
253
+ ...(descriptors.inherited ? { inheritedDescriptor: descriptors.inherited } : {}),
254
+ context,
255
+ transaction: "never-written" as TransactionState,
256
+ };
257
+ record.wrappedRender = function (this: object, ...renderArgs: unknown[]): unknown {
258
+ const width = renderArgs[0],
259
+ prefix = marker(state, record.context, kind);
260
+ const nativeArgs =
261
+ typeof width === "number" && width > visibleWidth(prefix)
262
+ ? [width - visibleWidth(prefix), ...renderArgs.slice(1)]
263
+ : renderArgs;
264
+ return decorateLines(state, Reflect.apply(record.nativeRender, this, nativeArgs), width, prefix);
265
+ };
266
+ piStyleWrappers.add(record.wrappedRender);
267
+ return record;
268
+ }
269
+ function installDecoration(state: DecorationState, record: DecorationRecord): void {
270
+ record.attemptedDescriptor.value = record.wrappedRender;
271
+ let wrote = false;
272
+ try {
273
+ wrote = Reflect.defineProperty(record.component, "render", record.attemptedDescriptor);
274
+ } catch (error) {
275
+ note(state, `render-install-threw: ${error instanceof Error ? error.message : String(error)}`, "never-written");
276
+ return;
277
+ }
278
+ if (!wrote) {
279
+ note(state, "render-install-rejected", "never-written");
280
+ return;
281
+ }
282
+ const current = Object.getOwnPropertyDescriptor(record.component, "render");
283
+ if (!descriptorEqual(current, record.attemptedDescriptor)) {
284
+ if (current?.value === record.wrappedRender) {
285
+ const restored = record.originalOwnDescriptor
286
+ ? (toolTestHooks.defineProperty ?? Reflect.defineProperty)(
287
+ record.component,
288
+ "render",
289
+ record.originalOwnDescriptor,
290
+ )
291
+ : (toolTestHooks.deleteProperty ?? Reflect.deleteProperty)(record.component, "render");
292
+ const after = Object.getOwnPropertyDescriptor(record.component, "render");
293
+ if (
294
+ restored &&
295
+ (record.originalOwnDescriptor ? descriptorEqual(after, record.originalOwnDescriptor) : after === undefined)
296
+ ) {
297
+ record.transaction = "rollback-restored";
298
+ note(state, "render-install-flags-mismatch-rolled-back", record.transaction);
299
+ return;
300
+ }
301
+ record.transaction = "rollback-failed";
302
+ state.decorated.set(record.component, record);
303
+ state.active.add(record);
304
+ note(state, "render-install-rollback-failed", record.transaction);
305
+ return;
306
+ }
307
+ record.transaction = "owner-changed";
308
+ note(state, "render-owner-changed-during-install", record.transaction);
309
+ return;
310
+ }
311
+ record.transaction = "installed-owned";
312
+ state.decorated.set(record.component, record);
313
+ state.active.add(record);
314
+ }
315
+ function restoreDecoration(state: DecorationState, record: DecorationRecord): CleanupOutcome {
316
+ const current = Object.getOwnPropertyDescriptor(record.component, "render");
317
+ if (!descriptorEqual(current, record.attemptedDescriptor)) {
318
+ record.transaction = "owner-changed";
319
+ state.laterOwner++;
320
+ note(state, "render-owner-changed", record.transaction);
321
+ return "later-owner";
322
+ }
323
+ const restored = record.originalOwnDescriptor
324
+ ? (toolTestHooks.defineProperty ?? Reflect.defineProperty)(record.component, "render", record.originalOwnDescriptor)
325
+ : (toolTestHooks.deleteProperty ?? Reflect.deleteProperty)(record.component, "render");
326
+ const after = Object.getOwnPropertyDescriptor(record.component, "render");
327
+ const ok = record.originalOwnDescriptor ? descriptorEqual(after, record.originalOwnDescriptor) : after === undefined;
328
+ if (!restored || !ok) {
329
+ record.transaction = "rollback-failed";
330
+ const signature = JSON.stringify(descriptorView(after));
331
+ if (record.lastFailureSignature !== signature) {
332
+ record.lastFailureSignature = signature;
333
+ note(state, "render-restore-failed", record.transaction);
334
+ }
335
+ return "retry";
336
+ }
337
+ record.transaction = "rollback-restored";
338
+ return "restored";
339
+ }
340
+ function finalize(state: DecorationState): ToolDiagnosticArchive {
341
+ if (state.archive) return state.archive;
342
+ const reasons = Object.freeze(Object.fromEntries(state.diagnostics));
343
+ state.archive = Object.freeze({
344
+ owner: `tool-owner-${state.generation}`,
345
+ generation: state.generation,
346
+ reasons,
347
+ restored: state.restored,
348
+ failed: state.failed,
349
+ laterOwner: state.laterOwner,
350
+ failures: Object.freeze(state.failures.slice(0, 32)),
351
+ });
352
+ return state.archive;
353
+ }
354
+ export function createToolDecorationOwner(snapshot: Partial<ToolDecorationSnapshot> = {}) {
355
+ const state: DecorationState = {
356
+ snapshot: Object.freeze({ ...DEFAULT_SNAPSHOT, ...snapshot }),
357
+ decorated: new WeakMap(),
358
+ active: new Set(),
359
+ diagnostics: new Map(),
360
+ failures: [],
361
+ restored: 0,
362
+ failed: 0,
363
+ laterOwner: 0,
364
+ generation: ++ownerGeneration,
365
+ };
366
+ const dispose = () => {
367
+ if (state.archive)
368
+ return {
369
+ restored: state.restored,
370
+ failed: state.failed,
371
+ diagnostics: new Map(state.diagnostics),
372
+ archive: state.archive,
373
+ };
374
+ for (const record of [...state.active]) {
375
+ const outcome = restoreDecoration(state, record);
376
+ if (outcome === "restored") {
377
+ state.restored++;
378
+ state.active.delete(record);
379
+ } else if (outcome === "later-owner") state.active.delete(record);
380
+ else state.failed++;
381
+ }
382
+ const archive = state.active.size === 0 ? finalize(state) : undefined;
383
+ return { restored: state.restored, failed: state.failed, diagnostics: new Map(state.diagnostics), archive };
384
+ };
385
+ return Object.freeze({
386
+ decorateToolRendererSelection(subtype: RendererSubtype, original: unknown, instance: object, args: unknown[]) {
387
+ if (typeof original !== "function") return undefined;
388
+ const renderer = Reflect.apply(original, instance, args);
389
+ if (state.snapshot.style === "compact-box") {
390
+ const toolName = (instance as { toolName?: unknown }).toolName;
391
+ // Tools without a native renderCall/renderResult (e.g. extension tools
392
+ // like TaskUpdate/TaskList) still get boxed presentation through a
393
+ // fallback renderer, mirroring the generic boxed fallback used for
394
+ // unknown tool names.
395
+ if (typeof renderer !== "function") {
396
+ neutralizeToolContainerBackground(instance);
397
+ if (subtype === "tool-call-renderer")
398
+ return (callArgs: unknown, theme: unknown, context: unknown) =>
399
+ renderBoxedToolCall(toolName, callArgs as Record<string, unknown>, theme as never, context as never);
400
+ return (result: unknown, options: unknown, theme: unknown, context: unknown) =>
401
+ renderBoxedToolResult(
402
+ toolName,
403
+ result as { content?: readonly unknown[]; details?: unknown },
404
+ options as { expanded: boolean; isPartial: boolean },
405
+ theme as never,
406
+ context as never,
407
+ );
408
+ }
409
+ return function (this: unknown, ...rendererArgs: unknown[]) {
410
+ const valid = subtype === "tool-call-renderer" ? validCallArgs(rendererArgs) : validResultArgs(rendererArgs);
411
+ if (!valid) {
412
+ note(state, `${subtype}-malformed-context`);
413
+ return Reflect.apply(renderer, this, rendererArgs);
414
+ }
415
+ const component =
416
+ subtype === "tool-call-renderer"
417
+ ? (() => {
418
+ const [args, theme, context] = rendererArgs as [object, object, ToolRenderContext];
419
+ return renderBoxedToolCall(
420
+ toolName,
421
+ args as Record<string, unknown>,
422
+ theme as never,
423
+ context as never,
424
+ );
425
+ })()
426
+ : (() => {
427
+ const [result, options, theme, context] = rendererArgs as [object, object, object, ToolRenderContext];
428
+ return renderBoxedToolResult(
429
+ toolName,
430
+ result as { content?: readonly unknown[]; details?: unknown },
431
+ options as { expanded: boolean; isPartial: boolean },
432
+ theme as never,
433
+ context as never,
434
+ );
435
+ })();
436
+ neutralizeToolContainerBackground(instance);
437
+ return component;
438
+ };
439
+ }
440
+ if (typeof renderer !== "function") return renderer;
441
+ return function (this: unknown, ...rendererArgs: unknown[]) {
442
+ const valid = subtype === "tool-call-renderer" ? validCallArgs(rendererArgs) : validResultArgs(rendererArgs);
443
+ if (!valid) {
444
+ note(state, `${subtype}-malformed-context`);
445
+ return Reflect.apply(renderer, this, rendererArgs);
446
+ }
447
+ const component = Reflect.apply(renderer, this, rendererArgs);
448
+ if (!isObject(component)) return component;
449
+ const descriptors = renderDescriptor(component);
450
+ if (descriptors !== false && descriptors.native && piStyleWrappers.has(descriptors.native)) {
451
+ note(state, `${subtype}-owner-conflict`);
452
+ return component;
453
+ }
454
+ if (!descriptors) {
455
+ note(state, `${subtype}-unsafe-render-descriptor`);
456
+ return component;
457
+ }
458
+ const record = state.decorated.get(component);
459
+ if (record) {
460
+ record.context = rendererArgs.at(-1) as ToolRenderContext;
461
+ return component;
462
+ }
463
+ const next = createRecord(
464
+ state,
465
+ component,
466
+ descriptors.native as RenderFunction,
467
+ rendererArgs.at(-1) as ToolRenderContext,
468
+ subtype === "tool-call-renderer" ? "call" : "result",
469
+ descriptors,
470
+ );
471
+ installDecoration(state, next);
472
+ return component;
473
+ };
474
+ },
475
+ getDiagnostics: () => new Map(state.diagnostics),
476
+ getFinalArchive: () => state.archive,
477
+ getActiveRecordCount: () => state.active.size,
478
+ dispose,
479
+ });
480
+ }
@@ -0,0 +1,17 @@
1
+ import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
2
+ import type { CommandApp } from "../app/command-service.js";
3
+ import { runCommand } from "../app/commands.js";
4
+ import { createPiConfigFilePort, defaultStoragePaths } from "./config-host.js";
5
+
6
+ export function registerPiStyleCommand(pi: ExtensionAPI, app: CommandApp): void {
7
+ pi.registerCommand("pi-style", {
8
+ description: "Configure pi-style for this session",
9
+ handler: async (args: string, ctx: ExtensionCommandContext) => {
10
+ const cwd = ctx.cwd ?? process.cwd();
11
+ await runCommand(args, { ui: ctx.ui, cwd, isProjectTrusted: ctx.isProjectTrusted }, app, {
12
+ port: createPiConfigFilePort(),
13
+ paths: defaultStoragePaths(cwd),
14
+ });
15
+ },
16
+ });
17
+ }
@@ -0,0 +1,195 @@
1
+ import { isTierCAuthorized } from "../domain/config-authorization.js";
2
+ import type { NormalizedPiStyleConfig } from "../domain/config-types.js";
3
+ import {
4
+ type CompatibilityCleanupResult,
5
+ type CompatibilityProbeReport,
6
+ detectPiVersion,
7
+ disposePiCompatibilityProbe,
8
+ probePiCompatibility,
9
+ } from "./compatibility-probe.js";
10
+
11
+ export interface CompatibilityCoordinator {
12
+ captureAuthorization(
13
+ coreFlag: boolean,
14
+ userFlag: boolean,
15
+ assistantFlag: boolean,
16
+ specialBlocksFlag: boolean,
17
+ toolsFlag: boolean,
18
+ asciiFlag: boolean,
19
+ ): void;
20
+ state(config: NormalizedPiStyleConfig): Readonly<Record<string, unknown>>;
21
+ install(
22
+ config: NormalizedPiStyleConfig,
23
+ tui: boolean,
24
+ productGate?: "omitted" | "allow" | "deny",
25
+ ): CompatibilityProbeReport | undefined;
26
+ dispose(): CompatibilityCleanupResult;
27
+ readonly report: CompatibilityProbeReport | undefined;
28
+ }
29
+
30
+ export function createCompatibilityCoordinator(dispose = disposePiCompatibilityProbe): CompatibilityCoordinator {
31
+ let report: CompatibilityProbeReport | undefined;
32
+ let cleanupPending = false;
33
+ let authorization:
34
+ | {
35
+ core: boolean;
36
+ user: boolean;
37
+ assistant: boolean;
38
+ specialBlocks: boolean;
39
+ tools: boolean;
40
+ ascii: boolean;
41
+ }
42
+ | undefined;
43
+ return {
44
+ get report() {
45
+ return report;
46
+ },
47
+ captureAuthorization(core, user, assistant, specialBlocks, tools, ascii) {
48
+ authorization = { core, user, assistant, specialBlocks, tools, ascii };
49
+ },
50
+ state(config) {
51
+ const version = detectPiVersion();
52
+ const messagesConfigured =
53
+ config.enabled &&
54
+ config.messages.enabled &&
55
+ (config.messages.userPrefix || config.messages.assistantPrefix || config.messages.specialBlocks);
56
+ const toolsConfigured = config.enabled && config.tools.enabled;
57
+ const surface = (
58
+ feature: "messages" | "tools",
59
+ configured: boolean,
60
+ surfaceAuthorized: boolean,
61
+ subtype?: string,
62
+ ) => {
63
+ const records =
64
+ report?.unsupported.filter((item) =>
65
+ feature === "messages" ? item.subtype.includes("message") : item.subtype.includes("tool"),
66
+ ) ?? [];
67
+ const failed = records.some((item) => /identity|failed|unsupported shape/i.test(item.reason));
68
+ const fallback = records.some((item) => /fallback|authorization|disabled/i.test(item.reason));
69
+ const authorized = Boolean(authorization?.core && surfaceAuthorized);
70
+ const installedRecord = report?.recordSnapshots.some(
71
+ (item) => item.feature === feature && !item.disposed && (subtype === undefined || item.subtype === subtype),
72
+ );
73
+ return {
74
+ configured,
75
+ authorized,
76
+ installed: Boolean(installedRecord && !failed && authorized && configured),
77
+ conflicted: records.some((item) => item.reason.includes("owner")),
78
+ failed,
79
+ cleanupPending,
80
+ nativeFallback: fallback,
81
+ ...(authorized ? {} : { awaitingAuthorization: true }),
82
+ };
83
+ };
84
+ return {
85
+ configured: messagesConfigured || toolsConfigured,
86
+ authorized: authorization?.core ?? false,
87
+ installed: report !== undefined,
88
+ conflicted: report?.unsupported.some((item) => item.reason.includes("identity")) ?? false,
89
+ failed: report?.unsupported.some((item) => item.reason.includes("failed")) ?? false,
90
+ cleanupPending,
91
+ nativeFallbacks: report?.unsupported.filter((item) => item.reason.includes("fallback")).length ?? 0,
92
+ piVersion: version.version ?? report?.piVersion ?? "unknown",
93
+ versionRange: report?.versionRange ?? ">=0.83.0 <0.84.0",
94
+ userMessage: surface(
95
+ "messages",
96
+ config.enabled && config.messages.enabled && config.messages.userPrefix,
97
+ Boolean(authorization?.user),
98
+ "native-user-message",
99
+ ),
100
+ assistantMessage: surface(
101
+ "messages",
102
+ config.enabled && config.messages.enabled && config.messages.assistantPrefix,
103
+ Boolean(authorization?.assistant),
104
+ "native-assistant-message",
105
+ ),
106
+ tools: surface("tools", config.enabled && config.tools.enabled, Boolean(authorization?.tools)),
107
+ specialBlocks: surface(
108
+ "messages",
109
+ config.enabled && config.messages.enabled && config.messages.specialBlocks,
110
+ Boolean(authorization?.core),
111
+ ),
112
+ };
113
+ },
114
+ install(config, tui, productGate = "omitted") {
115
+ const productDenied = productGate === "deny";
116
+ if (cleanupPending && !report) cleanupPending = false;
117
+ if (cleanupPending || !tui || !config.enabled || !authorization?.core || productDenied) return undefined;
118
+ const certifiedHost = detectPiVersion().version === "0.83.0";
119
+ const userEnabled =
120
+ authorization.user &&
121
+ isTierCAuthorized({
122
+ certifiedHost,
123
+ coreFlag: authorization.core,
124
+ surfaceFlag: true,
125
+ surface: "userMessage",
126
+ config,
127
+ });
128
+ const assistantEnabled =
129
+ authorization.assistant &&
130
+ isTierCAuthorized({
131
+ certifiedHost,
132
+ coreFlag: authorization.core,
133
+ surfaceFlag: true,
134
+ surface: "assistantMessage",
135
+ config,
136
+ });
137
+ const specialBlocksEnabled =
138
+ authorization.specialBlocks &&
139
+ isTierCAuthorized({
140
+ certifiedHost,
141
+ coreFlag: authorization.core,
142
+ surfaceFlag: true,
143
+ surface: "specialBlocks",
144
+ config,
145
+ });
146
+ const messagesEnabled = (userEnabled || assistantEnabled || specialBlocksEnabled) && config.messages.enabled;
147
+ const toolsEnabled =
148
+ authorization.tools &&
149
+ isTierCAuthorized({ certifiedHost, coreFlag: authorization.core, surfaceFlag: true, surface: "tools", config });
150
+ if (!messagesEnabled && !toolsEnabled) return undefined;
151
+ const detected = detectPiVersion();
152
+ report = probePiCompatibility(detected.version, {
153
+ config: {
154
+ ...config,
155
+ messages: {
156
+ ...config.messages,
157
+ enabled: messagesEnabled,
158
+ userPrefix: userEnabled,
159
+ assistantPrefix: assistantEnabled,
160
+ specialBlocks: messagesEnabled && config.messages.specialBlocks && specialBlocksEnabled,
161
+ },
162
+ tools: {
163
+ ...config.tools,
164
+ enabled: toolsEnabled,
165
+ },
166
+ },
167
+ messageSnapshot: {
168
+ userPrefix: authorization.ascii ? "[user] " : "❯ ",
169
+ assistantPrefix: authorization.ascii ? "[assistant] " : "│ ",
170
+ userEnabled,
171
+ assistantEnabled,
172
+ },
173
+ toolSnapshot: {
174
+ callMarker: authorization.ascii ? "[tool] " : "[tool] ",
175
+ resultMarker: authorization.ascii ? "[result] " : "[tool:result] ",
176
+ style: config.tools.style === "compact-box" ? "compact-box" : "marker",
177
+ },
178
+ });
179
+ return report;
180
+ },
181
+ dispose() {
182
+ if (!report) {
183
+ cleanupPending = false;
184
+ return { complete: true, retryablePrototypeRecords: 0, retryableToolRecords: 0 };
185
+ }
186
+ const result = dispose(report);
187
+ cleanupPending = !result.complete;
188
+ if (result.complete) {
189
+ report = undefined;
190
+ cleanupPending = false;
191
+ }
192
+ return result;
193
+ },
194
+ };
195
+ }