pi-compact-tools 0.16.1 → 0.16.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +48 -84
- package/extensions/compact-tools-activity.ts +9 -10
- package/extensions/compact-tools-clock.ts +37 -20
- package/extensions/compact-tools-command.ts +13 -16
- package/extensions/compact-tools-grouping.ts +2 -1
- package/extensions/compact-tools-invocation.ts +6 -0
- package/extensions/compact-tools-progress.ts +17 -18
- package/extensions/compact-tools-rendering.ts +36 -1
- package/extensions/compact-tools-runtime.ts +9 -50
- package/extensions/compact-tools-silent.ts +11 -8
- package/extensions/compact-tools-thinking.ts +12 -4
- package/extensions/compact-tools-transcript.ts +93 -0
- package/extensions/compact-tools-viewport.ts +5 -3
- package/extensions/compact-tools.ts +93 -81
- package/package.json +3 -3
- package/release-notes.json +42 -0
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import type { AgentToolResult } from "@earendil-works/pi-coding-agent";
|
|
2
1
|
import { DEFAULT_CONFIG } from "./compact-tools-config.ts";
|
|
2
|
+
import { onTick } from "./compact-tools-clock.ts";
|
|
3
3
|
import { isSilent } from "./compact-tools-silent.ts";
|
|
4
4
|
import {
|
|
5
5
|
SUPPORTED_TOOL_SET,
|
|
6
|
-
type BuiltInDefinition,
|
|
7
6
|
type CompactToolName,
|
|
8
7
|
type CompactToolsConfig,
|
|
9
8
|
type RenderContext,
|
|
@@ -27,7 +26,6 @@ export function validDurationMs(value: unknown): value is number {
|
|
|
27
26
|
return typeof value === "number" && Number.isFinite(value) && value >= 0;
|
|
28
27
|
}
|
|
29
28
|
type Indicator = { invalidate: () => void; drawnAt: number };
|
|
30
|
-
type TimedExecute = (...args: any[]) => Promise<AgentToolResult<unknown>>;
|
|
31
29
|
type SharedState = typeof globalThis & {
|
|
32
30
|
[EXECUTION_TIMINGS_KEY]?: Map<string, ExecutionTiming>;
|
|
33
31
|
};
|
|
@@ -38,7 +36,7 @@ export class ToolRuntime {
|
|
|
38
36
|
private readonly executionTimings: Map<string, ExecutionTiming>;
|
|
39
37
|
private readonly indicators = new Map<string, Indicator>();
|
|
40
38
|
private indicatorFrame = 0;
|
|
41
|
-
private
|
|
39
|
+
private stopIndicatorTicks: (() => void) | undefined;
|
|
42
40
|
/** Pi's frame request, once bound to its terminal; rows then repaint without being rebuilt. */
|
|
43
41
|
private requestRender: (() => void) | undefined;
|
|
44
42
|
/** Whether the agent is working; undefined until the host says, which counts as working. */
|
|
@@ -49,12 +47,6 @@ export class ToolRuntime {
|
|
|
49
47
|
* more than one run's calls.
|
|
50
48
|
*/
|
|
51
49
|
private readonly currentCalls = new Set<string>();
|
|
52
|
-
/**
|
|
53
|
-
* Calls a tool is making through `ctx.executeTool()`, as codemode scripts do. They
|
|
54
|
-
* never get a row of their own, so no execution timing is kept for them.
|
|
55
|
-
* The independent nested-summary observer keeps only their outcome counts.
|
|
56
|
-
*/
|
|
57
|
-
private readonly nestedCalls = new Set<string>();
|
|
58
50
|
|
|
59
51
|
constructor(private readonly clock: () => number = () => performance.now()) {
|
|
60
52
|
const shared = globalThis as SharedState;
|
|
@@ -111,7 +103,6 @@ export class ToolRuntime {
|
|
|
111
103
|
setBusy(busy: boolean): void {
|
|
112
104
|
if (busy && this.busy !== true) {
|
|
113
105
|
this.currentCalls.clear();
|
|
114
|
-
this.nestedCalls.clear();
|
|
115
106
|
}
|
|
116
107
|
this.busy = busy;
|
|
117
108
|
if (busy) return;
|
|
@@ -129,12 +120,6 @@ export class ToolRuntime {
|
|
|
129
120
|
this.currentCalls.add(toolCallId);
|
|
130
121
|
}
|
|
131
122
|
|
|
132
|
-
/** A call another tool made began (`running`) or finished; Pi says so with `parentToolCallId`. */
|
|
133
|
-
noteNestedCall(toolCallId: string, running: boolean): void {
|
|
134
|
-
if (running) this.nestedCalls.add(toolCallId);
|
|
135
|
-
else this.nestedCalls.delete(toolCallId);
|
|
136
|
-
}
|
|
137
|
-
|
|
138
123
|
/**
|
|
139
124
|
* Whether a call can be running: only one Pi announced in the run going on. A
|
|
140
125
|
* row restored from the session, one left without a result when its run ended,
|
|
@@ -158,10 +143,7 @@ export class ToolRuntime {
|
|
|
158
143
|
} else {
|
|
159
144
|
this.indicators.set(toolCallId, { invalidate, drawnAt: this.clock() });
|
|
160
145
|
}
|
|
161
|
-
|
|
162
|
-
this.indicatorTimer = setInterval(() => this.tick(), INDICATOR_INTERVAL_MS);
|
|
163
|
-
this.indicatorTimer.unref?.();
|
|
164
|
-
}
|
|
146
|
+
this.stopIndicatorTicks ??= onTick(() => this.tick(), INDICATOR_INTERVAL_MS);
|
|
165
147
|
return this.indicatorFrame;
|
|
166
148
|
}
|
|
167
149
|
|
|
@@ -211,12 +193,7 @@ export class ToolRuntime {
|
|
|
211
193
|
return state;
|
|
212
194
|
}
|
|
213
195
|
|
|
214
|
-
/**
|
|
215
|
-
* Record when a tool this extension does not wrap starts and ends. Pi reports
|
|
216
|
-
* both to extensions before it redraws the row, and the row draws its status
|
|
217
|
-
* dot before its result, so without this a finished custom tool kept the
|
|
218
|
-
* running dot. Wrapped built-ins already have timings, which are kept.
|
|
219
|
-
*/
|
|
196
|
+
/** Record top-level execution events without wrapping model-facing tools. */
|
|
220
197
|
noteExecutionStart(toolCallId: string): void {
|
|
221
198
|
if (!this.executionTimings.has(toolCallId)) this.executionTimings.set(toolCallId, { startedAt: Date.now() });
|
|
222
199
|
this.trimTimings();
|
|
@@ -230,24 +207,6 @@ export class ToolRuntime {
|
|
|
230
207
|
this.trimTimings();
|
|
231
208
|
}
|
|
232
209
|
|
|
233
|
-
createTimedExecute(definition: BuiltInDefinition): TimedExecute {
|
|
234
|
-
const execute = definition.execute as TimedExecute;
|
|
235
|
-
return async (...args: any[]) => {
|
|
236
|
-
const toolCallId = typeof args[0] === "string" && !this.nestedCalls.has(args[0]) ? args[0] : undefined;
|
|
237
|
-
const startedAt = (toolCallId ? this.executionTimings.get(toolCallId)?.startedAt : undefined) ?? Date.now();
|
|
238
|
-
if (toolCallId && !this.executionTimings.has(toolCallId)) {
|
|
239
|
-
this.executionTimings.set(toolCallId, { startedAt });
|
|
240
|
-
this.trimTimings();
|
|
241
|
-
}
|
|
242
|
-
try {
|
|
243
|
-
return await execute.apply(definition, args);
|
|
244
|
-
} finally {
|
|
245
|
-
const timing = toolCallId ? this.executionTimings.get(toolCallId) : undefined;
|
|
246
|
-
if (timing) timing.endedAt = Date.now();
|
|
247
|
-
}
|
|
248
|
-
};
|
|
249
|
-
}
|
|
250
|
-
|
|
251
210
|
private tick(): void {
|
|
252
211
|
this.indicatorFrame++;
|
|
253
212
|
const requestRender = this.requestRender;
|
|
@@ -269,15 +228,15 @@ export class ToolRuntime {
|
|
|
269
228
|
|
|
270
229
|
private removeIndicator(toolCallId: string): void {
|
|
271
230
|
this.indicators.delete(toolCallId);
|
|
272
|
-
if (this.indicators.size > 0
|
|
273
|
-
|
|
274
|
-
this.
|
|
231
|
+
if (this.indicators.size > 0) return;
|
|
232
|
+
this.stopIndicatorTicks?.();
|
|
233
|
+
this.stopIndicatorTicks = undefined;
|
|
275
234
|
this.indicatorFrame = 0;
|
|
276
235
|
}
|
|
277
236
|
|
|
278
237
|
private stopIndicators(): void {
|
|
279
|
-
|
|
280
|
-
this.
|
|
238
|
+
this.stopIndicatorTicks?.();
|
|
239
|
+
this.stopIndicatorTicks = undefined;
|
|
281
240
|
this.indicators.clear();
|
|
282
241
|
this.indicatorFrame = 0;
|
|
283
242
|
}
|
|
@@ -311,9 +311,9 @@ export class SilentModeController {
|
|
|
311
311
|
return sharedState().enabled;
|
|
312
312
|
}
|
|
313
313
|
|
|
314
|
-
bind(ctx: ExtensionContext, mode: DisplayMode, reload: boolean): void {
|
|
314
|
+
bind(ctx: ExtensionContext, mode: DisplayMode, reload: boolean, resume = false): void {
|
|
315
315
|
this.context = ctx;
|
|
316
|
-
this.activity.bind(ctx);
|
|
316
|
+
this.activity.bind(ctx, resume);
|
|
317
317
|
// An invisible widget is the extension API's way to reach Pi's TUI, and
|
|
318
318
|
// through it the chat container whose notices silent mode hides.
|
|
319
319
|
ctx.ui.setWidget(WIDGET_KEY, (tui) => {
|
|
@@ -326,13 +326,16 @@ export class SilentModeController {
|
|
|
326
326
|
}
|
|
327
327
|
|
|
328
328
|
dispose(): void {
|
|
329
|
-
this.
|
|
330
|
-
// A patched Pi prototype outlives this extension instance. Disable hiding
|
|
331
|
-
// while no instance is bound, but keep the choice for a subsequent /reload.
|
|
332
|
-
sharedState().active = false;
|
|
333
|
-
this.context?.ui.setStatus(STATUS_KEY, undefined);
|
|
334
|
-
this.context?.ui.setWidget(WIDGET_KEY, undefined, { placement: "belowEditor" });
|
|
329
|
+
const context = this.context;
|
|
335
330
|
this.context = undefined;
|
|
331
|
+
// Detach state before calling host UI: an exception must not reactivate
|
|
332
|
+
// silent hiding or its animation while the extension is off.
|
|
333
|
+
sharedState().active = false;
|
|
334
|
+
try { this.activity.dispose(); }
|
|
335
|
+
finally {
|
|
336
|
+
try { context?.ui.setStatus(STATUS_KEY, undefined); }
|
|
337
|
+
finally { context?.ui.setWidget(WIDGET_KEY, undefined, { placement: "belowEditor" }); }
|
|
338
|
+
}
|
|
336
339
|
}
|
|
337
340
|
|
|
338
341
|
private apply(enabled: boolean): boolean {
|
|
@@ -201,15 +201,18 @@ function getInitialThinkingPhase(): ThinkingPhase {
|
|
|
201
201
|
}
|
|
202
202
|
|
|
203
203
|
export class ThinkingCycleController {
|
|
204
|
+
private context: ExtensionContext | undefined;
|
|
204
205
|
private phase: ThinkingPhase = "summary";
|
|
205
206
|
private unsubscribe: (() => void) | undefined;
|
|
206
207
|
private theme: Theme | undefined;
|
|
207
208
|
private hasToggleableThinking = false;
|
|
208
209
|
private refreshThinking: (() => void) | undefined;
|
|
209
210
|
|
|
210
|
-
constructor(pi: ExtensionAPI) {
|
|
211
|
+
constructor(pi: ExtensionAPI, enabled: () => boolean = () => true) {
|
|
211
212
|
pi.registerMarkdownTransformer((markdown, context) => {
|
|
212
|
-
|
|
213
|
+
// Off also applies before session_start, during transcript reconstruction
|
|
214
|
+
// and HTML exports. Do not summarize or restyle native thinking there.
|
|
215
|
+
if (!enabled() || context.messageType !== "assistant-thinking") return markdown;
|
|
213
216
|
const sections = splitThinkingSections(markdown);
|
|
214
217
|
this.hasToggleableThinking ||= sectionsHaveDetail(sections);
|
|
215
218
|
const theme = this.theme;
|
|
@@ -241,6 +244,7 @@ export class ThinkingCycleController {
|
|
|
241
244
|
|
|
242
245
|
bind(ctx: ExtensionContext): void {
|
|
243
246
|
this.unsubscribe?.();
|
|
247
|
+
this.context = ctx;
|
|
244
248
|
this.theme = ctx.ui.theme;
|
|
245
249
|
this.hasToggleableThinking = false;
|
|
246
250
|
// Match Pi's persisted host visibility after startup or /reload. When the
|
|
@@ -277,11 +281,15 @@ export class ThinkingCycleController {
|
|
|
277
281
|
});
|
|
278
282
|
}
|
|
279
283
|
|
|
280
|
-
dispose(): void {
|
|
281
|
-
this.
|
|
284
|
+
dispose(restoreNative = false): void {
|
|
285
|
+
const context = this.context;
|
|
286
|
+
const unsubscribe = this.unsubscribe;
|
|
287
|
+
this.context = undefined;
|
|
282
288
|
this.unsubscribe = undefined;
|
|
283
289
|
this.refreshThinking = undefined;
|
|
284
290
|
this.hasToggleableThinking = false;
|
|
285
291
|
this.theme = undefined;
|
|
292
|
+
try { unsubscribe?.(); }
|
|
293
|
+
finally { if (restoreNative) context?.ui.setHiddenThinkingLabel(); }
|
|
286
294
|
}
|
|
287
295
|
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { ToolExecutionComponent, type ExtensionContext, type ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { TUI } from "@earendil-works/pi-tui";
|
|
3
|
+
import type { RowRenderers, ToolRow } from "./compact-tools-custom.ts";
|
|
4
|
+
import { refreshRowShell, syncNativeToolTimer } from "./compact-tools-rendering.ts";
|
|
5
|
+
import type { ToolRuntime } from "./compact-tools-runtime.ts";
|
|
6
|
+
import { findChat, isKind } from "./compact-tools-silent.ts";
|
|
7
|
+
|
|
8
|
+
type Definition = ToolDefinition<any, any, any> | undefined;
|
|
9
|
+
export interface UIFailure { operation: string; error: unknown }
|
|
10
|
+
|
|
11
|
+
/** Attempt every independent cleanup/row action; the caller reports failures once. */
|
|
12
|
+
export function attemptUI(failures: UIFailure[], operation: string, action: () => void): void {
|
|
13
|
+
try { action(); } catch (error) { failures.push({ operation, error }); }
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface TranscriptOptions {
|
|
17
|
+
runtime: ToolRuntime;
|
|
18
|
+
publicRendering: boolean;
|
|
19
|
+
resolve(row: ToolRow): RowRenderers | undefined;
|
|
20
|
+
definition(name: string, base: Definition): Definition;
|
|
21
|
+
unwrap(definition: Definition): Definition;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const WIDGET_KEY = "compact-tools-render";
|
|
25
|
+
const emptyWidget = () => ({ render: () => [], invalidate() {} });
|
|
26
|
+
|
|
27
|
+
/** Own the TUI probe and the private host-row reconciliation boundary together. */
|
|
28
|
+
export function createTranscriptRenderer(options: TranscriptOptions) {
|
|
29
|
+
let context: ExtensionContext | undefined;
|
|
30
|
+
let refresh: ((failures: UIFailure[]) => void) | undefined;
|
|
31
|
+
|
|
32
|
+
const reconcile = (tui: TUI, failures: UIFailure[]) => {
|
|
33
|
+
const visit = (parent: { children?: unknown[] }) => {
|
|
34
|
+
for (const child of parent.children ?? []) {
|
|
35
|
+
if (!child || typeof child !== "object") continue;
|
|
36
|
+
attemptUI(failures, "tool row", () => {
|
|
37
|
+
if (!isKind(child, ToolExecutionComponent)) { visit(child); return; }
|
|
38
|
+
const row = child as ToolRow & { toolCallId: string; updateDisplay?(): void; callRendererComponent?: unknown; resultRendererComponent?: unknown };
|
|
39
|
+
const base = options.unwrap(row.toolDefinition);
|
|
40
|
+
syncNativeToolTimer(row, row.toolName, base, !options.resolve({ toolName: row.toolName, toolDefinition: base }), options.runtime.timing(row.toolCallId));
|
|
41
|
+
if (options.publicRendering) {
|
|
42
|
+
row.toolDefinition = options.definition(row.toolName, base);
|
|
43
|
+
// Components must not cross native/compact renderer ownership,
|
|
44
|
+
// even when both definitions request the same shell.
|
|
45
|
+
row.callRendererComponent = undefined;
|
|
46
|
+
row.resultRendererComponent = undefined;
|
|
47
|
+
}
|
|
48
|
+
refreshRowShell(row);
|
|
49
|
+
row.updateDisplay?.();
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
attemptUI(failures, "transcript", () => {
|
|
54
|
+
const chat = findChat(tui);
|
|
55
|
+
if (chat && chat.children.length > 0) {
|
|
56
|
+
visit(chat);
|
|
57
|
+
attemptUI(failures, "repaint", () => tui.requestRender());
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
bind(ctx: ExtensionContext, failures: UIFailure[]): void {
|
|
64
|
+
context = ctx;
|
|
65
|
+
attemptUI(failures, "renderer probe", () => ctx.ui.setWidget(WIDGET_KEY, (tui) => {
|
|
66
|
+
options.runtime.bindRenderer(() => tui.requestRender());
|
|
67
|
+
refresh = (errors) => reconcile(tui, errors);
|
|
68
|
+
return emptyWidget();
|
|
69
|
+
}, { placement: "belowEditor" }));
|
|
70
|
+
refresh?.(failures);
|
|
71
|
+
},
|
|
72
|
+
refresh(failures: UIFailure[]): void { refresh?.(failures); },
|
|
73
|
+
detach(): void {
|
|
74
|
+
const previous = context;
|
|
75
|
+
context = undefined;
|
|
76
|
+
refresh = undefined;
|
|
77
|
+
options.runtime.bindRenderer(undefined);
|
|
78
|
+
previous?.ui.setWidget(WIDGET_KEY, undefined, { placement: "belowEditor" });
|
|
79
|
+
},
|
|
80
|
+
/** Reconcile provisional startup rows without binding runtime animation. */
|
|
81
|
+
reconcileOnce(ctx: ExtensionContext, failures: UIFailure[]): void {
|
|
82
|
+
if (refresh) { refresh(failures); return; }
|
|
83
|
+
try {
|
|
84
|
+
attemptUI(failures, "startup renderer probe", () => ctx.ui.setWidget(WIDGET_KEY, (tui) => {
|
|
85
|
+
reconcile(tui, failures);
|
|
86
|
+
return emptyWidget();
|
|
87
|
+
}, { placement: "belowEditor" }));
|
|
88
|
+
} finally {
|
|
89
|
+
attemptUI(failures, "startup renderer cleanup", () => ctx.ui.setWidget(WIDGET_KEY, undefined, { placement: "belowEditor" }));
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
}
|
|
@@ -221,12 +221,14 @@ export class ViewportKeeper {
|
|
|
221
221
|
}
|
|
222
222
|
|
|
223
223
|
dispose(): void {
|
|
224
|
-
this.
|
|
224
|
+
const context = this.context;
|
|
225
|
+
const unsubscribe = this.unsubscribe;
|
|
226
|
+
this.context = undefined;
|
|
225
227
|
this.unsubscribe = undefined;
|
|
226
228
|
const state = sharedState();
|
|
227
229
|
state.pending = undefined;
|
|
228
230
|
state.tui = undefined;
|
|
229
|
-
|
|
230
|
-
|
|
231
|
+
try { unsubscribe?.(); }
|
|
232
|
+
finally { context?.ui.setWidget(WIDGET_KEY, undefined, { placement: "belowEditor" }); }
|
|
231
233
|
}
|
|
232
234
|
}
|
|
@@ -7,12 +7,11 @@ import type {
|
|
|
7
7
|
ToolDefinition,
|
|
8
8
|
ToolRenderResultOptions,
|
|
9
9
|
} from "@earendil-works/pi-coding-agent";
|
|
10
|
-
import { ToolExecutionComponent } from "@earendil-works/pi-coding-agent";
|
|
11
10
|
import type { Component } from "@earendil-works/pi-tui";
|
|
12
11
|
import { classifyCallStatus, formatDurationMs, normalizeLineEndings, type RowStatus } from "./compact-tools-core.ts";
|
|
13
12
|
import { loadConfig } from "./compact-tools-config.ts";
|
|
14
13
|
import { paletteOf } from "./compact-tools-color.ts";
|
|
15
|
-
import { hasAuthorRenderers,
|
|
14
|
+
import { hasAuthorRenderers, loadNativeTimerRenderers, syncNativeToolTimer } from "./compact-tools-rendering.ts";
|
|
16
15
|
import { registerCompactToolsCommand } from "./compact-tools-command.ts";
|
|
17
16
|
import { clearRowResolver, installToolRowPatch, setRowResolver, type RowRenderers, type ToolRow } from "./compact-tools-custom.ts";
|
|
18
17
|
import {
|
|
@@ -23,6 +22,7 @@ import {
|
|
|
23
22
|
getEditDiff,
|
|
24
23
|
getEditPatch,
|
|
25
24
|
getFileOutput,
|
|
25
|
+
readStartLine,
|
|
26
26
|
getTextResult,
|
|
27
27
|
isReadTextResult,
|
|
28
28
|
splitReadFooter,
|
|
@@ -56,7 +56,8 @@ import { chromePainter, completionPainter, paintIndicator } from "./compact-tool
|
|
|
56
56
|
import { ProgressController } from "./compact-tools-progress.ts";
|
|
57
57
|
import { showReleaseNotice } from "./compact-tools-release.ts";
|
|
58
58
|
import { ToolRuntime } from "./compact-tools-runtime.ts";
|
|
59
|
-
import {
|
|
59
|
+
import { SilentModeController } from "./compact-tools-silent.ts";
|
|
60
|
+
import { attemptUI, createTranscriptRenderer, type UIFailure } from "./compact-tools-transcript.ts";
|
|
60
61
|
import { ThinkingCycleController } from "./compact-tools-thinking.ts";
|
|
61
62
|
import { ViewportKeeper } from "./compact-tools-viewport.ts";
|
|
62
63
|
import {
|
|
@@ -72,7 +73,6 @@ const PREVIEW_DIFF_CONTEXT_LINES = 1;
|
|
|
72
73
|
|
|
73
74
|
const runtime = new ToolRuntime();
|
|
74
75
|
const nestedSummaries = new NestedCallSummaries();
|
|
75
|
-
const RENDER_WIDGET_KEY = "compact-tools-render";
|
|
76
76
|
let registeredConfiguration: string | undefined;
|
|
77
77
|
/**
|
|
78
78
|
* Whether the last session found its directory trusted. A /reload rebuilds the
|
|
@@ -278,7 +278,7 @@ function renderFileBody(
|
|
|
278
278
|
}
|
|
279
279
|
if (name === "read" && isReadTextResult(result)) {
|
|
280
280
|
const { body, footer } = splitReadFooter(output);
|
|
281
|
-
const startLine =
|
|
281
|
+
const startLine = readStartLine(args.offset);
|
|
282
282
|
return renderCodeView(body, path, theme, { startLine, footer, rail }) ?? fallback();
|
|
283
283
|
}
|
|
284
284
|
if (name === "write") return renderCodeView(output, path, theme, { rail }) ?? fallback();
|
|
@@ -509,9 +509,15 @@ function createCustomRenderers(name: string, author: ToolDefinition<any, any, an
|
|
|
509
509
|
// Compact owns the outer margin and the result rail. An author
|
|
510
510
|
// renderer is embedded content, not another host-padded shell.
|
|
511
511
|
// Leave the field absent on legacy hosts.
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
512
|
+
try {
|
|
513
|
+
value.component = authorRenderer(result, { ...options, expanded }, theme, {
|
|
514
|
+
...ctx, lastComponent: value.component, ...((ctx as { outputPad?: number }).outputPad === undefined ? {} : { outputPad: 0 }),
|
|
515
|
+
});
|
|
516
|
+
} finally {
|
|
517
|
+
// Compact owns animation; a delegated native codemode preview
|
|
518
|
+
// must not leave a second clock when later collapsed.
|
|
519
|
+
syncNativeToolTimer({ rendererState: ctx.state }, name, author, false);
|
|
520
|
+
}
|
|
515
521
|
return value.component;
|
|
516
522
|
}
|
|
517
523
|
: undefined;
|
|
@@ -552,6 +558,7 @@ type RendererAPI = ExtensionAPI & {
|
|
|
552
558
|
};
|
|
553
559
|
let publicRendering = false;
|
|
554
560
|
let sessionReady = false;
|
|
561
|
+
let publicBases = new WeakMap<object, ToolDefinition<any, any, any> | undefined>();
|
|
555
562
|
let publicOwners = new WeakMap<object, { call?: unknown; result?: unknown }>();
|
|
556
563
|
function publicRowDefinition(name: string, base: ToolDefinition<any, any, any> | undefined): ToolDefinition<any, any, any> | undefined {
|
|
557
564
|
// Preserve next()'s result exactly once the session's configuration is known.
|
|
@@ -580,46 +587,47 @@ function publicRowDefinition(name: string, base: ToolDefinition<any, any, any> |
|
|
|
580
587
|
const render = decide()?.renderResult ?? base?.renderResult;
|
|
581
588
|
return render!(value, options, theme, context(ctx, "result", render));
|
|
582
589
|
};
|
|
583
|
-
|
|
590
|
+
const definition = {
|
|
584
591
|
get renderShell() { return decide() ? "self" : base?.renderShell ?? "default"; },
|
|
585
592
|
get renderCall() { return decide()?.renderCall || base?.renderCall ? call : undefined; },
|
|
586
593
|
get renderResult() { return decide()?.renderResult || base?.renderResult ? result : undefined; },
|
|
587
594
|
} as ToolDefinition<any, any, any>;
|
|
595
|
+
publicBases.set(definition, base);
|
|
596
|
+
return definition;
|
|
588
597
|
}
|
|
589
598
|
let legacyResolver: ((row: ToolRow) => RowRenderers | undefined) | undefined;
|
|
590
|
-
let refreshTranscriptRows: (() => void) | undefined;
|
|
591
|
-
function invalidateRowDecisions(): void {
|
|
599
|
+
let refreshTranscriptRows: ((failures: UIFailure[]) => void) | undefined;
|
|
600
|
+
function invalidateRowDecisions(failures: UIFailure[]): void {
|
|
592
601
|
if (!publicRendering) {
|
|
593
602
|
legacyResolver = (row) => resolveCustomRow(row);
|
|
594
603
|
setRowResolver(legacyResolver);
|
|
595
604
|
}
|
|
596
|
-
refreshTranscriptRows?.();
|
|
605
|
+
refreshTranscriptRows?.(failures);
|
|
597
606
|
}
|
|
598
607
|
|
|
599
|
-
function configure(_pi: ExtensionAPI, cwd?: string, projectTrusted = false): void {
|
|
608
|
+
function configure(_pi: ExtensionAPI, cwd?: string, projectTrusted = false, failures: UIFailure[] = []): void {
|
|
600
609
|
const resolvedCwd = cwd ?? process.cwd();
|
|
601
610
|
const config = loadConfig(resolvedCwd, projectTrusted);
|
|
602
611
|
const signature = JSON.stringify([resolvedCwd, config]);
|
|
603
612
|
if (signature === registeredConfiguration) return;
|
|
604
613
|
runtime.configure(config);
|
|
605
614
|
registeredConfiguration = signature;
|
|
606
|
-
invalidateRowDecisions();
|
|
615
|
+
invalidateRowDecisions(failures);
|
|
607
616
|
}
|
|
608
617
|
|
|
609
618
|
/**
|
|
610
619
|
* Apply a style saved by /compact-tools without reloading Pi. The compact, Claude, and
|
|
611
620
|
* Codex styles share every tool registration and read the style as they draw, so the
|
|
612
621
|
* configuration is swapped and each tool row on screen is drawn again, a running one
|
|
613
|
-
* included.
|
|
622
|
+
* included. Only non-style configuration changes require a reload.
|
|
614
623
|
*/
|
|
615
|
-
function applyStyle(cwd: string, trusted: boolean): boolean {
|
|
624
|
+
function applyStyle(cwd: string, trusted: boolean, failures: UIFailure[]): boolean {
|
|
616
625
|
const config = loadConfig(cwd, trusted);
|
|
617
626
|
const current = runtime.config;
|
|
618
|
-
if (config.style === "off" || current.style === "off") return false;
|
|
619
627
|
if (JSON.stringify({ ...config, style: current.style }) !== JSON.stringify(current)) return false;
|
|
620
628
|
runtime.configure(config);
|
|
621
629
|
registeredConfiguration = JSON.stringify([cwd, config]);
|
|
622
|
-
invalidateRowDecisions();
|
|
630
|
+
invalidateRowDecisions(failures);
|
|
623
631
|
return true;
|
|
624
632
|
}
|
|
625
633
|
|
|
@@ -643,44 +651,15 @@ function madeByTool(event: object): boolean {
|
|
|
643
651
|
return typeof (event as { parentToolCallId?: unknown }).parentToolCallId === "string";
|
|
644
652
|
}
|
|
645
653
|
|
|
646
|
-
/** Hand the animation Pi's frame request, reached through an invisible widget as the extension API allows. */
|
|
647
|
-
function bindRenderer(ctx: ExtensionContext): void {
|
|
648
|
-
ctx.ui.setWidget(RENDER_WIDGET_KEY, (tui) => {
|
|
649
|
-
runtime.bindRenderer(() => tui.requestRender());
|
|
650
|
-
refreshTranscriptRows = () => {
|
|
651
|
-
const visit = (parent: { children?: unknown[] }) => {
|
|
652
|
-
for (const child of parent.children ?? []) {
|
|
653
|
-
if (!child || typeof child !== "object") continue;
|
|
654
|
-
if (isKind(child, ToolExecutionComponent)) {
|
|
655
|
-
const row = child as ToolRow & { updateDisplay?(): void };
|
|
656
|
-
refreshRowShell(row);
|
|
657
|
-
row.updateDisplay?.();
|
|
658
|
-
} else visit(child as { children?: unknown[] });
|
|
659
|
-
}
|
|
660
|
-
};
|
|
661
|
-
const chat = findChat(tui);
|
|
662
|
-
if (chat && chat.children.length > 0) { visit(chat); tui.requestRender(); }
|
|
663
|
-
};
|
|
664
|
-
return { render: () => [], invalidate() {} };
|
|
665
|
-
}, { placement: "belowEditor" });
|
|
666
|
-
refreshTranscriptRows?.();
|
|
667
|
-
}
|
|
668
|
-
|
|
669
|
-
function unbindRenderer(ctx: ExtensionContext | undefined): void {
|
|
670
|
-
refreshTranscriptRows = undefined;
|
|
671
|
-
runtime.bindRenderer(undefined);
|
|
672
|
-
ctx?.ui.setWidget(RENDER_WIDGET_KEY, undefined, { placement: "belowEditor" });
|
|
673
|
-
}
|
|
674
|
-
|
|
675
654
|
export default function compactTools(pi: ExtensionAPI): void {
|
|
676
655
|
// The cached factory is invoked again after session replacement and reload.
|
|
677
656
|
// Reset configuration memoization, but never change the host's tool registry.
|
|
678
657
|
registeredConfiguration = undefined;
|
|
658
|
+
refreshTranscriptRows = undefined;
|
|
679
659
|
sessionReady = false;
|
|
680
660
|
publicOwners = new WeakMap();
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
const thinkingCycle = new ThinkingCycleController(pi);
|
|
661
|
+
publicBases = new WeakMap();
|
|
662
|
+
const thinkingCycle = new ThinkingCycleController(pi, () => runtime.config.style !== "off");
|
|
684
663
|
const progress = new ProgressController(pi);
|
|
685
664
|
const viewport = new ViewportKeeper();
|
|
686
665
|
const silent = new SilentModeController(pi, showReleaseNotice);
|
|
@@ -696,15 +675,66 @@ export default function compactTools(pi: ExtensionAPI): void {
|
|
|
696
675
|
const base = next();
|
|
697
676
|
return publicRowDefinition(name, base as ToolDefinition<any, any, any> | undefined);
|
|
698
677
|
});
|
|
699
|
-
} else invalidateRowDecisions();
|
|
678
|
+
} else invalidateRowDecisions([]);
|
|
679
|
+
const transcript = createTranscriptRenderer({
|
|
680
|
+
runtime, publicRendering, resolve: resolveCustomRow, definition: publicRowDefinition,
|
|
681
|
+
unwrap: (current) => current && publicBases.has(current) ? publicBases.get(current) : current,
|
|
682
|
+
});
|
|
683
|
+
refreshTranscriptRows = transcript.refresh;
|
|
700
684
|
let customRowsWarned = false;
|
|
701
|
-
let
|
|
685
|
+
let sessionContext: ExtensionContext | undefined;
|
|
686
|
+
const reportFailures = (ctx: ExtensionContext | undefined, failures: UIFailure[]) => {
|
|
687
|
+
if (ctx && failures.length > 0) {
|
|
688
|
+
const operations = [...new Set(failures.map(failure => failure.operation))].join(", ");
|
|
689
|
+
const message = `Compact tools: UI refresh/cleanup failed (${operations}). The selected style is saved; affected UI may need /reload.`;
|
|
690
|
+
try { ctx.ui.notify(message, "warning"); }
|
|
691
|
+
catch (error) { console.warn(message, "UI notification also failed:", error); }
|
|
692
|
+
}
|
|
693
|
+
};
|
|
694
|
+
const enableUI = (ctx: ExtensionContext, reload: boolean, resume: boolean, failures: UIFailure[]) => {
|
|
695
|
+
// Observe relayout keys before the thinking controller consumes them.
|
|
696
|
+
attemptUI(failures, "viewport bind", () => viewport.bind(ctx));
|
|
697
|
+
attemptUI(failures, "progress bind", () => progress.bind(ctx, resume));
|
|
698
|
+
attemptUI(failures, "thinking bind", () => thinkingCycle.bind(ctx));
|
|
699
|
+
attemptUI(failures, "silent bind", () => silent.bind(ctx, runtime.config.mode, reload, resume));
|
|
700
|
+
attemptUI(failures, "groups bind", () => groups.bind(ctx));
|
|
701
|
+
transcript.bind(ctx, failures);
|
|
702
|
+
};
|
|
703
|
+
// One ownership/cleanup inventory for live off, session replacement and shutdown.
|
|
704
|
+
// Each action is independent: a host failure must not skip later cleanup steps.
|
|
705
|
+
const detachUI = (options: { preservePhase?: boolean; restoreNative?: boolean; clearTimings?: boolean }, failures: UIFailure[]) => {
|
|
706
|
+
attemptUI(failures, "thinking cleanup", () => thinkingCycle.dispose(options.restoreNative));
|
|
707
|
+
attemptUI(failures, "progress cleanup", () => progress.dispose(options.preservePhase));
|
|
708
|
+
attemptUI(failures, "silent cleanup", () => silent.dispose());
|
|
709
|
+
attemptUI(failures, "groups cleanup", () => groups.dispose());
|
|
710
|
+
attemptUI(failures, "viewport cleanup", () => viewport.dispose());
|
|
711
|
+
attemptUI(failures, "animation cleanup", () => runtime.reset(options.clearTimings === true));
|
|
712
|
+
attemptUI(failures, "renderer cleanup", () => transcript.detach());
|
|
713
|
+
};
|
|
714
|
+
registerCompactToolsCommand(pi, () => runtime.config, (cwd, trusted, ctx) => {
|
|
715
|
+
const failures: UIFailure[] = [];
|
|
716
|
+
const wasOff = runtime.config.style === "off";
|
|
717
|
+
if (!applyStyle(cwd, trusted, failures)) return false;
|
|
718
|
+
const uiContext = sessionContext ?? ctx;
|
|
719
|
+
if (uiContext.mode === "tui") {
|
|
720
|
+
if (runtime.config.style === "off") detachUI({ preservePhase: true, restoreNative: true }, failures);
|
|
721
|
+
else if (wasOff) enableUI(uiContext, true, true, failures);
|
|
722
|
+
// Off->off has no attached thinking context to restore the native label.
|
|
723
|
+
if (wasOff && runtime.config.style === "off") attemptUI(failures, "native thinking", () => uiContext.ui.setHiddenThinkingLabel());
|
|
724
|
+
}
|
|
725
|
+
reportFailures(uiContext, failures);
|
|
726
|
+
return true;
|
|
727
|
+
});
|
|
702
728
|
// Configure UI routing before Pi rebuilds transcript rows. Tool declarations
|
|
703
729
|
// and the active loadout must remain exactly as the host selected them.
|
|
704
730
|
const cwd = process.cwd();
|
|
705
731
|
const lastTrust = (globalThis as TrustSlot)[TRUST_KEY];
|
|
706
732
|
configure(pi, cwd, lastTrust?.cwd === realDirectory(cwd) && lastTrust.trusted);
|
|
733
|
+
pi.on("session_start", () => loadNativeTimerRenderers());
|
|
707
734
|
pi.on("session_start", (event, ctx) => {
|
|
735
|
+
const failures: UIFailure[] = [];
|
|
736
|
+
detachUI({ restoreNative: true }, failures);
|
|
737
|
+
sessionContext = ctx;
|
|
708
738
|
if (event.reason !== "reload") {
|
|
709
739
|
runtime.clearTimings();
|
|
710
740
|
nestedSummaries.clear();
|
|
@@ -715,33 +745,21 @@ export default function compactTools(pi: ExtensionAPI): void {
|
|
|
715
745
|
if (typeof ctx.isIdle === "function") runtime.setBusy(!ctx.isIdle());
|
|
716
746
|
const trusted = ctx.isProjectTrusted();
|
|
717
747
|
(globalThis as TrustSlot)[TRUST_KEY] = { cwd: realDirectory(ctx.cwd), trusted };
|
|
718
|
-
configure(pi, ctx.cwd, trusted);
|
|
748
|
+
configure(pi, ctx.cwd, trusted, failures);
|
|
719
749
|
sessionReady = true;
|
|
720
|
-
unbindRenderer(renderContext);
|
|
721
|
-
renderContext = undefined;
|
|
722
|
-
// Even off must reconcile rows built before trusted config was available.
|
|
723
|
-
if (ctx.mode === "tui") bindRenderer(ctx);
|
|
724
750
|
if (ctx.mode === "tui" && runtime.config.style !== "off") {
|
|
725
|
-
|
|
726
|
-
// First, so its input listener sees relayout keys before the thinking controller consumes them.
|
|
727
|
-
viewport.bind(ctx);
|
|
728
|
-
progress.bind(ctx);
|
|
729
|
-
thinkingCycle.bind(ctx);
|
|
730
|
-
silent.bind(ctx, runtime.config.mode, event.reason === "reload");
|
|
731
|
-
groups.bind(ctx);
|
|
751
|
+
enableUI(ctx, event.reason === "reload", false, failures);
|
|
732
752
|
if (!silent.isEnabled()) showReleaseNotice(ctx);
|
|
733
753
|
if ((runtime.config.custom_tools.enabled || runtime.config.tools.length > 0) && !customRowsAvailable && !customRowsWarned) {
|
|
734
754
|
customRowsWarned = true;
|
|
735
755
|
ctx.ui.notify("Compact tool rendering is unavailable in this version of Pi", "warning");
|
|
736
756
|
}
|
|
737
|
-
} else {
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
thinkingCycle.dispose();
|
|
742
|
-
silent.dispose();
|
|
743
|
-
groups.dispose();
|
|
757
|
+
} else if (ctx.mode === "tui") {
|
|
758
|
+
// Startup-off also reconciles rows built using provisional/trusted config,
|
|
759
|
+
// but never attaches animation or the compact controllers.
|
|
760
|
+
transcript.reconcileOnce(ctx, failures);
|
|
744
761
|
}
|
|
762
|
+
reportFailures(ctx, failures);
|
|
745
763
|
});
|
|
746
764
|
// /tree changes the branch without session_start. Fill sibling-branch records
|
|
747
765
|
// before Pi rebuilds its transcript, while retaining richer same-process deltas.
|
|
@@ -761,7 +779,6 @@ export default function compactTools(pi: ExtensionAPI): void {
|
|
|
761
779
|
});
|
|
762
780
|
pi.on("tool_execution_start", (event) => {
|
|
763
781
|
if (madeByTool(event)) {
|
|
764
|
-
runtime.noteNestedCall(event.toolCallId, true);
|
|
765
782
|
nestedSummaries.note(event, false);
|
|
766
783
|
return;
|
|
767
784
|
}
|
|
@@ -771,7 +788,6 @@ export default function compactTools(pi: ExtensionAPI): void {
|
|
|
771
788
|
});
|
|
772
789
|
pi.on("tool_execution_end", (event) => {
|
|
773
790
|
if (madeByTool(event)) {
|
|
774
|
-
runtime.noteNestedCall(event.toolCallId, false);
|
|
775
791
|
if (nestedSummaries.note(event, true)) runtime.repaint();
|
|
776
792
|
return;
|
|
777
793
|
}
|
|
@@ -779,14 +795,10 @@ export default function compactTools(pi: ExtensionAPI): void {
|
|
|
779
795
|
runtime.noteExecutionEnd(event.toolCallId, (event as { durationMs?: number }).durationMs);
|
|
780
796
|
});
|
|
781
797
|
pi.on("session_shutdown", (event) => {
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
viewport.dispose();
|
|
787
|
-
unbindRenderer(renderContext);
|
|
788
|
-
renderContext = undefined;
|
|
789
|
-
runtime.reset(event.reason !== "reload");
|
|
798
|
+
const failures: UIFailure[] = [];
|
|
799
|
+
detachUI({ clearTimings: event.reason !== "reload" }, failures);
|
|
800
|
+
reportFailures(sessionContext, failures);
|
|
801
|
+
sessionContext = undefined;
|
|
790
802
|
if (event.reason !== "reload") nestedSummaries.clear();
|
|
791
803
|
// A reload that keeps the extension sets it again before Pi rebuilds the transcript.
|
|
792
804
|
if (legacyResolver) clearRowResolver(legacyResolver);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-compact-tools",
|
|
3
|
-
"version": "0.16.
|
|
3
|
+
"version": "0.16.5",
|
|
4
4
|
"description": "Tool rows for Pi in three styles: Claude, Codex, and Compact. Switch with /compact-tools; includes silent mode and a GitHub Dark theme.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -51,8 +51,8 @@
|
|
|
51
51
|
"@earendil-works/pi-tui": ">=0.85.1"
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|
|
54
|
-
"@earendil-works/pi-coding-agent": "1.
|
|
55
|
-
"@earendil-works/pi-tui": "1.
|
|
54
|
+
"@earendil-works/pi-coding-agent": "1.3.0",
|
|
55
|
+
"@earendil-works/pi-tui": "1.3.0",
|
|
56
56
|
"@sinonjs/fake-timers": "15.0.0",
|
|
57
57
|
"@types/node": "^24.0.0",
|
|
58
58
|
"@types/sinonjs__fake-timers": "15.0.1",
|