pi-compact-tools 0.14.0 → 0.14.2
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 +19 -2
- package/extensions/compact-tools-codex-rows.ts +1 -0
- package/extensions/compact-tools-custom.ts +36 -22
- package/extensions/compact-tools-grouping.ts +10 -2
- package/extensions/compact-tools-layout.ts +39 -0
- package/extensions/compact-tools-rendering.ts +51 -0
- package/extensions/compact-tools-runtime.ts +11 -2
- package/extensions/compact-tools-types.ts +8 -1
- package/extensions/compact-tools.ts +146 -91
- package/package.json +3 -3
- package/release-notes.json +22 -0
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@ pi install npm:pi-compact-tools
|
|
|
12
12
|
|
|
13
13
|
Restart Pi or run `/reload`, then run `/compact-tools` to choose a style. To try it for one session without installing: `pi -e npm:pi-compact-tools`.
|
|
14
14
|
|
|
15
|
-
Requires Node.js 22.19+ and Pi 0.85.1+. It changes only how tool calls are drawn, not what they run or what the model sees.
|
|
15
|
+
Requires Node.js 22.19+ and Pi 0.85.1+. **Pi 1.0.2 or later is recommended** for native renderer integration and compact HTML exports. It changes only how tool calls are drawn, not what they run or what the model sees.
|
|
16
16
|
|
|
17
17
|
## Styles
|
|
18
18
|
|
|
@@ -88,6 +88,23 @@ Optional, in `~/.pi/agent/compact-tools.json` or `<project>/.pi/compact-tools.js
|
|
|
88
88
|
- `auto_compact`: `true` starts a Compact row collapsed; `false` shows up to `previewLines` rows. Custom tool names work too.
|
|
89
89
|
- `custom_tools`: rows for tools from other packages. `exclude` keeps a tool's own look.
|
|
90
90
|
|
|
91
|
+
## UI-only behavior
|
|
92
|
+
|
|
93
|
+
This package never registers or activates agent tools and does not add model-facing
|
|
94
|
+
messages, tool schemas, or prompt text. Host execution settings (including shell
|
|
95
|
+
prefixes and image resizing) remain untouched. Renderer-bearing overrides of names
|
|
96
|
+
such as `read` or `bash` follow `custom_tools`, including its exclusions.
|
|
97
|
+
|
|
98
|
+
On Pi 1.0.2 and newer, the public renderer API also applies compact rendering to HTML
|
|
99
|
+
exports. Older hosts use a terminal-only compatibility patch; HTML exports keep
|
|
100
|
+
Pi's native renderers. On Pi 1.1 and newer, final durations use the host's recorded
|
|
101
|
+
`durationMs`, including restored sessions, and tool rows follow `outputPad` (0 or 1)
|
|
102
|
+
without changing their internal style indentation. Older hosts keep event-based
|
|
103
|
+
timing and the original margins; the minimum supported Pi version is unchanged.
|
|
104
|
+
When custom compact rendering is enabled, it intentionally takes precedence over
|
|
105
|
+
fallback renderers supplied by earlier `next() ?? mine` resolvers for unknown tools.
|
|
106
|
+
Disable `custom_tools` or exclude the tool to preserve those fallback renderers.
|
|
107
|
+
|
|
91
108
|
## Controls
|
|
92
109
|
|
|
93
110
|
- Click a row or group, or press `Ctrl+O`, to expand and collapse.
|
|
@@ -101,7 +118,7 @@ npm run check
|
|
|
101
118
|
pi -e .
|
|
102
119
|
```
|
|
103
120
|
|
|
104
|
-
Development dependencies are pinned to Pi 1.
|
|
121
|
+
Development dependencies are pinned to Pi 1.1.0; the peer minimum remains Pi 0.85.1. CI checks Pi 0.85.1, 0.99.1, 1.0.0, 1.0.2, and 1.1.0 on Node 22.19.0 and 24. Schema validation and deterministic test clocks are development-only dependencies, not runtime dependencies. Development scripts and tests are not included in the published package. `npm test` runs the rendering tests first and the resource/packaging checks separately so their subprocesses do not disturb animation tests.
|
|
105
122
|
|
|
106
123
|
For a local before/after performance comparison, run `npx tsx tests/benchmark.ts <clean-baseline-directory>` with the same host dependencies in both directories. Measurements are informational, not timing-sensitive test assertions.
|
|
107
124
|
|
|
@@ -145,6 +145,7 @@ function prefixedRows(lines: readonly Segment[][], first: string, rest: string,
|
|
|
145
145
|
}
|
|
146
146
|
|
|
147
147
|
function durationOf(state: RowState): string | undefined {
|
|
148
|
+
if (state.durationMs !== undefined) return formatCodexDuration(state.durationMs);
|
|
148
149
|
return state.startedAt !== undefined && state.endedAt !== undefined ? formatCodexDuration(state.endedAt - state.startedAt) : undefined;
|
|
149
150
|
}
|
|
150
151
|
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { ToolExecutionComponent, type ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { hookMethod } from "./compact-tools-hook.ts";
|
|
3
|
+
import { refreshRowShell } from "./compact-tools-rendering.ts";
|
|
2
4
|
|
|
3
5
|
// Symbol.for keys are shared by every evaluation of this module. The prototype is
|
|
4
6
|
// Pi's and outlives a /reload, so the patch is installed once and always asks the
|
|
5
7
|
// most recently loaded extension instance, through the shared slot, what to render.
|
|
6
|
-
const RESOLVER_KEY = Symbol.for("pi-compact-tools.custom.resolver");
|
|
7
|
-
const
|
|
8
|
+
const RESOLVER_KEY = Symbol.for("pi-compact-tools.custom.resolver.v2");
|
|
9
|
+
const LEGACY_RESOLVER_KEY = Symbol.for("pi-compact-tools.custom.resolver");
|
|
10
|
+
const PATCHED_KEY = Symbol.for("pi-compact-tools.custom.patched.v2");
|
|
8
11
|
const ROUTED_METHODS = ["getCallRenderer", "getResultRenderer", "getRenderShell", "hasRendererDefinition"] as const;
|
|
9
12
|
|
|
10
13
|
type AnyToolDefinition = ToolDefinition<any, any, any>;
|
|
@@ -26,14 +29,12 @@ type RoutedPrototype = Record<(typeof ROUTED_METHODS)[number], (...args: unknown
|
|
|
26
29
|
type ResolverSlot = typeof globalThis & { [RESOLVER_KEY]?: RowResolver };
|
|
27
30
|
|
|
28
31
|
export function setRowResolver(resolver: RowResolver | undefined): void {
|
|
32
|
+
// The v1 patch may survive an npm update plus /reload. Keep its slot empty.
|
|
33
|
+
(globalThis as unknown as Record<symbol, unknown>)[LEGACY_RESOLVER_KEY] = undefined;
|
|
29
34
|
(globalThis as ResolverSlot)[RESOLVER_KEY] = resolver;
|
|
30
35
|
}
|
|
31
36
|
|
|
32
|
-
/**
|
|
33
|
-
* Stop answering for rows, if `resolver` is still the one answering. The patch on
|
|
34
|
-
* Pi's rows outlives the extension, so a /reload that leaves it out must find no
|
|
35
|
-
* resolver, and Pi's own renderers draw every row made afterwards.
|
|
36
|
-
*/
|
|
37
|
+
/** Remove this instance's resolver when the UI extension is unloaded. */
|
|
37
38
|
export function clearRowResolver(resolver: RowResolver): void {
|
|
38
39
|
if ((globalThis as ResolverSlot)[RESOLVER_KEY] === resolver) setRowResolver(undefined);
|
|
39
40
|
}
|
|
@@ -48,28 +49,41 @@ export function patchToolRows(prototype: object | undefined): boolean {
|
|
|
48
49
|
const routed = prototype as RoutedPrototype;
|
|
49
50
|
if (!ROUTED_METHODS.every((method) => typeof routed[method] === "function")) return false;
|
|
50
51
|
if (Object.hasOwn(prototype, PATCHED_KEY)) return true;
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
52
|
+
hookMethod(prototype, "updateDisplay", "custom.refreshShell", (self, args, original) => {
|
|
53
|
+
refreshRowShell(self);
|
|
54
|
+
return original.apply(self, args);
|
|
55
|
+
});
|
|
56
|
+
// Cache within a configuration generation. session_start can apply trusted
|
|
57
|
+
// project settings after rows were constructed; a new resolver invalidates
|
|
58
|
+
// those provisional decisions, and updateDisplay reconciles the shell.
|
|
59
|
+
const decisions = new WeakMap<object, { resolver: RowResolver | undefined; renderers: RowRenderers | null }>();
|
|
60
|
+
const owners = new WeakMap<object, { call?: unknown; result?: unknown }>();
|
|
61
|
+
const changed = (row: object, kind: "call" | "result", renderer: unknown) => {
|
|
62
|
+
let owner = owners.get(row);
|
|
63
|
+
if (!owner) owners.set(row, owner = {});
|
|
64
|
+
if (owner[kind] !== renderer) {
|
|
65
|
+
(row as Record<string, unknown>)[kind === "call" ? "callRendererComponent" : "resultRendererComponent"] = undefined;
|
|
66
|
+
owner[kind] = renderer;
|
|
67
|
+
}
|
|
68
|
+
return renderer;
|
|
69
|
+
};
|
|
55
70
|
const resolve = (row: ToolRow): RowRenderers | undefined => {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
} catch {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
decisions.set(row, renderers);
|
|
71
|
+
const resolver = (globalThis as ResolverSlot)[RESOLVER_KEY];
|
|
72
|
+
let decision = decisions.get(row);
|
|
73
|
+
if (!decision || decision.resolver !== resolver) {
|
|
74
|
+
let renderers: RowRenderers | null;
|
|
75
|
+
try { renderers = resolver?.(row) ?? null; } catch { renderers = null; }
|
|
76
|
+
decision = { resolver, renderers };
|
|
77
|
+
decisions.set(row, decision);
|
|
64
78
|
}
|
|
65
|
-
return renderers ?? undefined;
|
|
79
|
+
return decision.renderers ?? undefined;
|
|
66
80
|
};
|
|
67
81
|
const original = Object.fromEntries(ROUTED_METHODS.map((method) => [method, routed[method]])) as RoutedPrototype;
|
|
68
82
|
routed.getCallRenderer = function (this: ToolRow, ...args) {
|
|
69
|
-
return resolve(this)?.renderCall ?? original.getCallRenderer.apply(this, args);
|
|
83
|
+
return changed(this, "call", resolve(this)?.renderCall ?? original.getCallRenderer.apply(this, args));
|
|
70
84
|
};
|
|
71
85
|
routed.getResultRenderer = function (this: ToolRow, ...args) {
|
|
72
|
-
return resolve(this)?.renderResult ?? original.getResultRenderer.apply(this, args);
|
|
86
|
+
return changed(this, "result", resolve(this)?.renderResult ?? original.getResultRenderer.apply(this, args));
|
|
73
87
|
};
|
|
74
88
|
routed.getRenderShell = function (this: ToolRow, ...args) {
|
|
75
89
|
return resolve(this) ? "self" : original.getRenderShell.apply(this, args);
|
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
import { formatDurationMs, type RowStatus } from "./compact-tools-core.ts";
|
|
27
27
|
import { hookMethod } from "./compact-tools-hook.ts";
|
|
28
28
|
import { getTextResult } from "./compact-tools-invocation.ts";
|
|
29
|
+
import { outputPadLines } from "./compact-tools-layout.ts";
|
|
29
30
|
import { chromePainter, paintIndicator } from "./compact-tools-palette.ts";
|
|
30
31
|
import type { ToolRuntime } from "./compact-tools-runtime.ts";
|
|
31
32
|
import { findChat, isKind, isSilent } from "./compact-tools-silent.ts";
|
|
@@ -52,6 +53,8 @@ export interface ToolRowLike {
|
|
|
52
53
|
result?: AgentToolResult<unknown> & { isError?: boolean };
|
|
53
54
|
isPartial: boolean;
|
|
54
55
|
expanded: boolean;
|
|
56
|
+
/** Present on Pi 1.1+ tool components, absent on legacy hosts. */
|
|
57
|
+
outputPad?: number;
|
|
55
58
|
render(width: number): string[];
|
|
56
59
|
handleMouse?(event: unknown): unknown;
|
|
57
60
|
invalidate(): void;
|
|
@@ -333,7 +336,10 @@ export class ToolGroupController {
|
|
|
333
336
|
this.states.set(key, state);
|
|
334
337
|
}
|
|
335
338
|
const groupState = state;
|
|
336
|
-
const draw = (group: readonly ToolRowLike[], width: number, expanded: boolean) =>
|
|
339
|
+
const draw = (group: readonly ToolRowLike[], width: number, expanded: boolean) => {
|
|
340
|
+
const pad = group[0]?.outputPad;
|
|
341
|
+
return outputPadLines(this.drawCached(group, width + (pad === 0 ? 1 : 0), expanded, groupState), width, pad);
|
|
342
|
+
};
|
|
337
343
|
if (this.runtime.config.style !== "codex") return new ToolGroupComponent(rows, members, state, draw, () => tui.requestRender());
|
|
338
344
|
// Codex opens a group to the whole of each call, and closes it from one "− Show less" beneath them.
|
|
339
345
|
const toggle = (expanded: boolean) => {
|
|
@@ -342,7 +348,9 @@ export class ToolGroupController {
|
|
|
342
348
|
};
|
|
343
349
|
const footer = (width: number) => {
|
|
344
350
|
const theme = this.paint?.theme ?? this.context?.ui.theme;
|
|
345
|
-
|
|
351
|
+
const pad = rows[0]?.outputPad;
|
|
352
|
+
const available = width + (pad === 0 ? 1 : 0);
|
|
353
|
+
return theme ? outputPadLines([truncateToWidth(theme.fg("dim", codexDisclosure(available, true)), Math.max(1, available), "…")], width, pad) : [];
|
|
346
354
|
};
|
|
347
355
|
const group = new ToolGroupComponent(rows, members, state, draw, toggle, footer);
|
|
348
356
|
// Read once the group has followed Ctrl+O, which it does as it is made. A call that joins an
|
|
@@ -15,6 +15,45 @@ import { highlightMarkdown } from "./compact-tools-markdown.ts";
|
|
|
15
15
|
import { paintChrome } from "./compact-tools-palette.ts";
|
|
16
16
|
import type { ToolArgs } from "./compact-tools-types.ts";
|
|
17
17
|
|
|
18
|
+
/**
|
|
19
|
+
* Styles include one outer margin column in their prefixes. Pi 1.1+ can request
|
|
20
|
+
* no outer padding; reclaim that column without changing tree/code indentation.
|
|
21
|
+
* Missing fields and outputPad=1 keep the legacy output byte-for-byte.
|
|
22
|
+
*/
|
|
23
|
+
export function outputPadLines(lines: string[], width: number, outputPad?: number): string[] {
|
|
24
|
+
if (outputPad !== 0) return lines;
|
|
25
|
+
return lines.map((line) => stripTerminalSequences(line).startsWith(" ")
|
|
26
|
+
? sliceByColumn(line, 1, Math.max(0, width), true)
|
|
27
|
+
: truncateToWidth(line, Math.max(0, width), "…"));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
class UnpaddedComponent implements Component {
|
|
31
|
+
private source?: string[];
|
|
32
|
+
private width?: number;
|
|
33
|
+
private lines: string[] = [];
|
|
34
|
+
|
|
35
|
+
constructor(private readonly inner: Component) {}
|
|
36
|
+
|
|
37
|
+
render(width: number): string[] {
|
|
38
|
+
const source = this.inner.render(width + 1);
|
|
39
|
+
if (source !== this.source || width !== this.width) {
|
|
40
|
+
this.source = source;
|
|
41
|
+
this.width = width;
|
|
42
|
+
this.lines = outputPadLines(source, width, 0);
|
|
43
|
+
}
|
|
44
|
+
return this.lines;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
invalidate(): void {
|
|
48
|
+
this.inner.invalidate();
|
|
49
|
+
this.source = undefined;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function withOutputPad(component: Component, outputPad?: number): Component {
|
|
54
|
+
return outputPad === 0 ? new UnpaddedComponent(component) : component;
|
|
55
|
+
}
|
|
56
|
+
|
|
18
57
|
export class CachedComponent implements Component {
|
|
19
58
|
private cachedWidth?: number;
|
|
20
59
|
private cachedLines?: string[];
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createBashToolDefinition, createEditToolDefinition, createFindToolDefinition,
|
|
3
|
+
createGrepToolDefinition, createLsToolDefinition, createPowerShellToolDefinition,
|
|
4
|
+
createReadToolDefinition, createWriteToolDefinition,
|
|
5
|
+
type ToolDefinition,
|
|
6
|
+
} from "@earendil-works/pi-coding-agent";
|
|
7
|
+
|
|
8
|
+
// Constructing definitions does not register/activate tools or run their execute
|
|
9
|
+
// functions. Only renderer fingerprints are retained, never model-facing fields.
|
|
10
|
+
const factories = {
|
|
11
|
+
read: createReadToolDefinition, write: createWriteToolDefinition, edit: createEditToolDefinition,
|
|
12
|
+
bash: createBashToolDefinition, powershell: createPowerShellToolDefinition,
|
|
13
|
+
grep: createGrepToolDefinition, find: createFindToolDefinition, ls: createLsToolDefinition,
|
|
14
|
+
};
|
|
15
|
+
const builtinRenderers = new Map<string, { call: string; result: string }>();
|
|
16
|
+
for (const [name, factory] of Object.entries(factories)) {
|
|
17
|
+
try {
|
|
18
|
+
const definition = factory(process.cwd());
|
|
19
|
+
builtinRenderers.set(name, { call: String(definition.renderCall), result: String(definition.renderResult) });
|
|
20
|
+
} catch {
|
|
21
|
+
// A missing/unsupported reference renderer must not prevent UI loading.
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Conservative fallback on legacy hosts without source metadata. */
|
|
26
|
+
export function hasAuthorRenderers(name: string, definition: ToolDefinition<any, any, any> | undefined): boolean {
|
|
27
|
+
if (!definition) return false;
|
|
28
|
+
const builtin = builtinRenderers.get(name);
|
|
29
|
+
return Boolean((definition.renderCall && String(definition.renderCall) !== builtin?.call)
|
|
30
|
+
|| (definition.renderResult && String(definition.renderResult) !== builtin?.result));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Change only a transcript row's renderer shell, never a tool registry entry. */
|
|
34
|
+
export function refreshRowShell(row: object): void {
|
|
35
|
+
const target = row as {
|
|
36
|
+
children?: unknown[]; selfRenderContainer?: unknown; contentBox?: unknown; contentTextRegion?: unknown;
|
|
37
|
+
hasRendererDefinition?(): boolean; getRenderShell?(): string;
|
|
38
|
+
callRendererComponent?: unknown; resultRendererComponent?: unknown;
|
|
39
|
+
};
|
|
40
|
+
if (!Array.isArray(target.children) || !target.selfRenderContainer || !target.contentBox || !target.contentTextRegion) return;
|
|
41
|
+
const wanted = target.hasRendererDefinition?.()
|
|
42
|
+
? target.getRenderShell?.() === "self" ? target.selfRenderContainer : target.contentBox
|
|
43
|
+
: target.contentTextRegion;
|
|
44
|
+
const shells: unknown[] = [target.selfRenderContainer, target.contentBox, target.contentTextRegion];
|
|
45
|
+
const index = target.children.findIndex((child) => shells.includes(child));
|
|
46
|
+
if (index >= 0 && target.children[index] !== wanted) {
|
|
47
|
+
target.children[index] = wanted;
|
|
48
|
+
target.callRendererComponent = undefined;
|
|
49
|
+
target.resultRendererComponent = undefined;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -20,7 +20,12 @@ const INDICATOR_INTERVAL_MS = 45;
|
|
|
20
20
|
const UNDRAWN_ROW_MS = 1_500;
|
|
21
21
|
const EXECUTION_TIMINGS_KEY = Symbol.for("pi.compact-tools.execution-timings");
|
|
22
22
|
|
|
23
|
-
type ExecutionTiming = { startedAt: number; endedAt?: number };
|
|
23
|
+
type ExecutionTiming = { startedAt: number; endedAt?: number; durationMs?: number };
|
|
24
|
+
|
|
25
|
+
/** Treat missing or malformed host durations like an older host; zero is valid. */
|
|
26
|
+
export function validDurationMs(value: unknown): value is number {
|
|
27
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0;
|
|
28
|
+
}
|
|
24
29
|
type Indicator = { invalidate: () => void; drawnAt: number };
|
|
25
30
|
type TimedExecute = (...args: any[]) => Promise<AgentToolResult<unknown>>;
|
|
26
31
|
type SharedState = typeof globalThis & {
|
|
@@ -186,6 +191,7 @@ export class ToolRuntime {
|
|
|
186
191
|
// Pi can rebuild the transcript mid-run, making new rows for old calls; a call
|
|
187
192
|
// keeps the run its first row was made in.
|
|
188
193
|
this.restoreTiming(state, ctx.toolCallId);
|
|
194
|
+
if (validDurationMs(ctx.durationMs)) state.durationMs = ctx.durationMs;
|
|
189
195
|
// A row is timed from when it runs, as Pi times its own: the model writing the
|
|
190
196
|
// arguments is not the tool's time. A restored row never ran in front of the
|
|
191
197
|
// reader, so it has no time to measure unless this process timed it before a
|
|
@@ -210,9 +216,10 @@ export class ToolRuntime {
|
|
|
210
216
|
this.trimTimings();
|
|
211
217
|
}
|
|
212
218
|
|
|
213
|
-
noteExecutionEnd(toolCallId: string): void {
|
|
219
|
+
noteExecutionEnd(toolCallId: string, durationMs?: number): void {
|
|
214
220
|
const timing = this.executionTimings.get(toolCallId) ?? { startedAt: Date.now() };
|
|
215
221
|
timing.endedAt ??= Date.now();
|
|
222
|
+
if (validDurationMs(durationMs)) timing.durationMs = durationMs;
|
|
216
223
|
this.executionTimings.set(toolCallId, timing);
|
|
217
224
|
this.trimTimings();
|
|
218
225
|
}
|
|
@@ -290,6 +297,7 @@ export class ToolRuntime {
|
|
|
290
297
|
if (!timing) return;
|
|
291
298
|
state.startedAt = timing.startedAt;
|
|
292
299
|
state.endedAt = timing.endedAt;
|
|
300
|
+
if (validDurationMs(timing.durationMs)) state.durationMs = timing.durationMs;
|
|
293
301
|
}
|
|
294
302
|
|
|
295
303
|
private persistTiming(state: RowState, toolCallId: string): void {
|
|
@@ -297,6 +305,7 @@ export class ToolRuntime {
|
|
|
297
305
|
const timing = this.executionTimings.get(toolCallId) ?? { startedAt: state.startedAt };
|
|
298
306
|
timing.startedAt = state.startedAt;
|
|
299
307
|
if (state.endedAt !== undefined) timing.endedAt = state.endedAt;
|
|
308
|
+
if (validDurationMs(state.durationMs)) timing.durationMs = state.durationMs;
|
|
300
309
|
this.executionTimings.set(toolCallId, timing);
|
|
301
310
|
this.trimTimings();
|
|
302
311
|
}
|
|
@@ -41,6 +41,8 @@ export interface RowState {
|
|
|
41
41
|
finished?: boolean;
|
|
42
42
|
startedAt?: number;
|
|
43
43
|
endedAt?: number;
|
|
44
|
+
/** Host-recorded final duration, when available (Pi 1.1+). */
|
|
45
|
+
durationMs?: number;
|
|
44
46
|
resultLineSummary?: string;
|
|
45
47
|
resultLineSummaryComputed?: boolean;
|
|
46
48
|
lastResultContent?: unknown;
|
|
@@ -51,6 +53,8 @@ export interface RowState {
|
|
|
51
53
|
lastResultError?: boolean;
|
|
52
54
|
lastResultConfigRevision?: object;
|
|
53
55
|
lastResultPalette?: object;
|
|
56
|
+
lastResultDurationMs?: number;
|
|
57
|
+
lastResultOutputPad?: number;
|
|
54
58
|
/** What a Codex row's head reads from its result: a command's exit code, an edit's counts. */
|
|
55
59
|
codex?: {
|
|
56
60
|
exitCode?: number;
|
|
@@ -61,7 +65,10 @@ export interface RowState {
|
|
|
61
65
|
}
|
|
62
66
|
|
|
63
67
|
type BaseRenderContext = Parameters<NonNullable<BuiltInDefinition["renderCall"]>>[2];
|
|
64
|
-
export type RenderContext<TArgs = ToolArgs> = Omit<BaseRenderContext, "args" | "state"> & {
|
|
68
|
+
export type RenderContext<TArgs = ToolArgs> = Omit<BaseRenderContext, "args" | "state" | "durationMs" | "outputPad"> & {
|
|
65
69
|
args: TArgs;
|
|
66
70
|
state: RowState;
|
|
71
|
+
/** Optional across hosts: older Pi versions do not provide these fields. */
|
|
72
|
+
durationMs?: number;
|
|
73
|
+
outputPad?: number;
|
|
67
74
|
};
|
|
@@ -7,21 +7,12 @@ import type {
|
|
|
7
7
|
ToolDefinition,
|
|
8
8
|
ToolRenderResultOptions,
|
|
9
9
|
} from "@earendil-works/pi-coding-agent";
|
|
10
|
-
import {
|
|
11
|
-
createBashToolDefinition,
|
|
12
|
-
createEditToolDefinition,
|
|
13
|
-
createFindToolDefinition,
|
|
14
|
-
createGrepToolDefinition,
|
|
15
|
-
createLsToolDefinition,
|
|
16
|
-
createPowerShellToolDefinition,
|
|
17
|
-
createReadToolDefinition,
|
|
18
|
-
createWriteToolDefinition,
|
|
19
|
-
ToolExecutionComponent,
|
|
20
|
-
} from "@earendil-works/pi-coding-agent";
|
|
10
|
+
import { ToolExecutionComponent } from "@earendil-works/pi-coding-agent";
|
|
21
11
|
import type { Component } from "@earendil-works/pi-tui";
|
|
22
12
|
import { classifyCallStatus, formatDurationMs, normalizeLineEndings, type RowStatus } from "./compact-tools-core.ts";
|
|
23
13
|
import { loadConfig } from "./compact-tools-config.ts";
|
|
24
14
|
import { paletteOf } from "./compact-tools-color.ts";
|
|
15
|
+
import { hasAuthorRenderers, refreshRowShell } from "./compact-tools-rendering.ts";
|
|
25
16
|
import { registerCompactToolsCommand } from "./compact-tools-command.ts";
|
|
26
17
|
import { clearRowResolver, installToolRowPatch, setRowResolver, type RowRenderers, type ToolRow } from "./compact-tools-custom.ts";
|
|
27
18
|
import {
|
|
@@ -47,6 +38,7 @@ import {
|
|
|
47
38
|
prefixedSentence,
|
|
48
39
|
prefixedText,
|
|
49
40
|
railComponent,
|
|
41
|
+
withOutputPad,
|
|
50
42
|
renderArguments,
|
|
51
43
|
renderCodeDiff,
|
|
52
44
|
renderCodeView,
|
|
@@ -68,7 +60,6 @@ import { ThinkingCycleController } from "./compact-tools-thinking.ts";
|
|
|
68
60
|
import { ViewportKeeper } from "./compact-tools-viewport.ts";
|
|
69
61
|
import {
|
|
70
62
|
SUPPORTED_TOOL_SET,
|
|
71
|
-
type BuiltInDefinition,
|
|
72
63
|
type CompactToolName,
|
|
73
64
|
type RenderContext,
|
|
74
65
|
type RowState,
|
|
@@ -80,7 +71,6 @@ const PREVIEW_DIFF_CONTEXT_LINES = 1;
|
|
|
80
71
|
|
|
81
72
|
const runtime = new ToolRuntime();
|
|
82
73
|
const RENDER_WIDGET_KEY = "compact-tools-render";
|
|
83
|
-
const registeredTools = new Set<CompactToolName>();
|
|
84
74
|
let registeredConfiguration: string | undefined;
|
|
85
75
|
/**
|
|
86
76
|
* Whether the last session found its directory trusted. A /reload rebuilds the
|
|
@@ -146,6 +136,7 @@ function liveIndicator(theme: Theme, ctx: RenderContext, state: RowState, glyph?
|
|
|
146
136
|
}
|
|
147
137
|
|
|
148
138
|
function formatDuration(state: RowState): string | undefined {
|
|
139
|
+
if (state.durationMs !== undefined) return formatDurationMs(state.durationMs);
|
|
149
140
|
if (state.startedAt === undefined) return undefined;
|
|
150
141
|
return formatDurationMs((state.endedAt ?? Date.now()) - state.startedAt);
|
|
151
142
|
}
|
|
@@ -165,7 +156,9 @@ function canReuseResult<TArgs>(
|
|
|
165
156
|
&& state.lastResultPreview === state.preview
|
|
166
157
|
&& state.lastResultError === ctx.isError
|
|
167
158
|
&& state.lastResultConfigRevision === state.configRevision
|
|
168
|
-
&& state.lastResultPalette === paletteOf(theme)
|
|
159
|
+
&& state.lastResultPalette === paletteOf(theme)
|
|
160
|
+
&& state.lastResultDurationMs === state.durationMs
|
|
161
|
+
&& state.lastResultOutputPad === ctx.outputPad;
|
|
169
162
|
}
|
|
170
163
|
|
|
171
164
|
function rememberResult(state: RowState, result: AgentToolResult<unknown>, options: ToolRenderResultOptions, isError: boolean, theme: Theme): void {
|
|
@@ -214,6 +207,10 @@ function hasNestedArguments(args: ToolArgs): boolean {
|
|
|
214
207
|
|
|
215
208
|
/** The call line: a status dot, the tool, and what it was asked, then any arguments the line leaves out. */
|
|
216
209
|
function renderRowCall(name: string, args: ToolArgs, theme: Theme, ctx: RenderContext): Component {
|
|
210
|
+
return withOutputPad(renderRowCallContent(name, args, theme, ctx), ctx.outputPad);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function renderRowCallContent(name: string, args: ToolArgs, theme: Theme, ctx: RenderContext): Component {
|
|
217
214
|
const state = runtime.syncRow(ctx);
|
|
218
215
|
runtime.syncExpansion(state, ctx.expanded, name);
|
|
219
216
|
const kind = rowKind(name);
|
|
@@ -292,7 +289,7 @@ function renderRowBody(
|
|
|
292
289
|
author: AuthorResultRenderer | undefined,
|
|
293
290
|
rail?: string,
|
|
294
291
|
): Component | undefined {
|
|
295
|
-
if (rowKind(name) !== "custom") return renderFileBody(name, args, result, output, expanded, theme, isError, rail);
|
|
292
|
+
if (!author && rowKind(name) !== "custom") return renderFileBody(name, args, result, output, expanded, theme, isError, rail);
|
|
296
293
|
if (author) {
|
|
297
294
|
try {
|
|
298
295
|
const component = author(expanded);
|
|
@@ -333,6 +330,7 @@ function claudeBody(
|
|
|
333
330
|
theme: Theme,
|
|
334
331
|
author: AuthorResultRenderer | undefined,
|
|
335
332
|
): Component | undefined {
|
|
333
|
+
if (author && expanded) return renderRowBody(name, args, result, output, true, theme, false, author, CLAUDE_INDENT);
|
|
336
334
|
const path = pathArgument(args);
|
|
337
335
|
if (name === "edit") {
|
|
338
336
|
return renderCodeDiff(getEditPatch(result), getEditDiff(result), path, theme,
|
|
@@ -416,13 +414,13 @@ function renderRowResult(
|
|
|
416
414
|
runtime.syncExpansion(state, ctx.expanded, name);
|
|
417
415
|
if (canReuseResult(state, result, options, theme, ctx)) return ctx.lastComponent;
|
|
418
416
|
const kind = rowKind(name);
|
|
419
|
-
const output = kind === "file" ? getFileOutput(name, ctx.args, result, ctx.isError) : getTextResult(result);
|
|
417
|
+
const output = kind === "file" && !author ? getFileOutput(name, ctx.args, result, ctx.isError) : getTextResult(result);
|
|
420
418
|
const hasEditDiff = name === "edit" && getEditDiff(result).length > 0;
|
|
421
419
|
runtime.setResultAvailable(state, name, hasEditDiff || output.length > 0 || author !== undefined);
|
|
422
420
|
let component: Component;
|
|
423
421
|
if (isCodex()) {
|
|
424
422
|
component = renderCodexResult({
|
|
425
|
-
name, kind, args: ctx.args, result, output, theme, state, isError: ctx.isError,
|
|
423
|
+
name, kind: author ? "custom" : kind, args: ctx.args, result, output, theme, state, isError: ctx.isError,
|
|
426
424
|
isPartial: options.isPartial, expanded: state.expanded === true, toolCallId: ctx.toolCallId, author,
|
|
427
425
|
});
|
|
428
426
|
} else if (isClaude()) {
|
|
@@ -446,7 +444,9 @@ function renderRowResult(
|
|
|
446
444
|
component = new WithinWidth(container);
|
|
447
445
|
}
|
|
448
446
|
rememberResult(state, result, options, ctx.isError, theme);
|
|
449
|
-
|
|
447
|
+
state.lastResultDurationMs = state.durationMs;
|
|
448
|
+
state.lastResultOutputPad = ctx.outputPad;
|
|
449
|
+
return withOutputPad(component, ctx.outputPad);
|
|
450
450
|
}
|
|
451
451
|
|
|
452
452
|
/**
|
|
@@ -454,20 +454,30 @@ function renderRowResult(
|
|
|
454
454
|
* author's result renderer still draws the expanded body; the row chrome, status,
|
|
455
455
|
* and collapse behavior match the built-ins.
|
|
456
456
|
*/
|
|
457
|
+
let authorStates = new WeakMap<object, { state: RowState; component?: Component }>();
|
|
457
458
|
function createCustomRenderers(name: string, author: ToolDefinition<any, any, any> | undefined): RowRenderers {
|
|
458
459
|
// Pi hands both renderers the row's own state object, which belongs to the tool's
|
|
459
460
|
// author. Compact bookkeeping is kept here so neither side overwrites the other.
|
|
460
|
-
const
|
|
461
|
-
|
|
462
|
-
|
|
461
|
+
const slot = (ctx: RenderContext<any>) => {
|
|
462
|
+
let value = authorStates.get(ctx.state);
|
|
463
|
+
if (!value) authorStates.set(ctx.state, value = { state: {} });
|
|
464
|
+
return value;
|
|
465
|
+
};
|
|
466
|
+
const own = (ctx: RenderContext<any>): RenderContext => ({ ...ctx, args: ctx.args ?? {}, state: slot(ctx).state });
|
|
463
467
|
return {
|
|
464
468
|
renderCall: (args, theme, ctx) => renderRowCall(name, (args ?? {}) as ToolArgs, theme, own(ctx)),
|
|
465
469
|
renderResult: (result, options, theme, ctx) => {
|
|
466
470
|
const authorRenderer = author?.renderResult;
|
|
467
471
|
const renderAuthorResult = authorRenderer
|
|
468
472
|
? (expanded: boolean) => {
|
|
469
|
-
|
|
470
|
-
|
|
473
|
+
const value = slot(ctx);
|
|
474
|
+
// Compact owns the outer margin and the result rail. An author
|
|
475
|
+
// renderer is embedded content, not another host-padded shell.
|
|
476
|
+
// Leave the field absent on legacy hosts.
|
|
477
|
+
value.component = authorRenderer(result, { ...options, expanded }, theme, {
|
|
478
|
+
...ctx, lastComponent: value.component, ...((ctx as { outputPad?: number }).outputPad === undefined ? {} : { outputPad: 0 }),
|
|
479
|
+
});
|
|
480
|
+
return value.component;
|
|
471
481
|
}
|
|
472
482
|
: undefined;
|
|
473
483
|
return renderRowResult(name, result, options, theme, own(ctx), renderAuthorResult);
|
|
@@ -475,63 +485,90 @@ function createCustomRenderers(name: string, author: ToolDefinition<any, any, an
|
|
|
475
485
|
};
|
|
476
486
|
}
|
|
477
487
|
|
|
488
|
+
// Renderer resolvers may run repeatedly for the same call, notably HTML export.
|
|
489
|
+
let builtinStates = new WeakMap<object, RowState>();
|
|
490
|
+
const compactRendererDefinitions = new WeakSet<object>();
|
|
491
|
+
function builtinContext(ctx: RenderContext): RenderContext {
|
|
492
|
+
let state = builtinStates.get(ctx.state);
|
|
493
|
+
if (!state) builtinStates.set(ctx.state, state = {});
|
|
494
|
+
return { ...ctx, state };
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/** UI-only routing: never register, activate, or replace model-facing tools. */
|
|
478
498
|
function resolveCustomRow(row: ToolRow): RowRenderers | undefined {
|
|
499
|
+
if (runtime.config.style === "off") return undefined;
|
|
479
500
|
const customTools = runtime.config.custom_tools;
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
501
|
+
if (SUPPORTED_TOOL_SET.has(row.toolName) && (!row.toolDefinition || compactRendererDefinitions.has(row.toolDefinition) || (row.toolDefinition.renderResult && compactRendererDefinitions.has(row.toolDefinition.renderResult)) || !hasAuthorRenderers(row.toolName, row.toolDefinition))) {
|
|
502
|
+
if (!runtime.config.tools.includes(row.toolName as CompactToolName)) return undefined;
|
|
503
|
+
const renderers: RowRenderers = {
|
|
504
|
+
renderCall: (args, theme, ctx) => renderRowCall(row.toolName, (args ?? {}) as ToolArgs, theme, builtinContext(ctx as RenderContext)),
|
|
505
|
+
renderResult: (result, options, theme, ctx) => renderRowResult(row.toolName, result, options, theme, builtinContext(ctx as RenderContext)),
|
|
506
|
+
};
|
|
507
|
+
compactRendererDefinitions.add(renderers);
|
|
508
|
+
compactRendererDefinitions.add(renderers.renderResult);
|
|
509
|
+
return renderers;
|
|
510
|
+
}
|
|
511
|
+
if (!customTools.enabled || customTools.exclude.includes(row.toolName)) return undefined;
|
|
483
512
|
return createCustomRenderers(row.toolName, row.toolDefinition);
|
|
484
513
|
}
|
|
485
514
|
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
write: createWriteToolDefinition,
|
|
489
|
-
edit: createEditToolDefinition,
|
|
490
|
-
bash: createBashToolDefinition,
|
|
491
|
-
powershell: createPowerShellToolDefinition,
|
|
492
|
-
grep: createGrepToolDefinition,
|
|
493
|
-
find: createFindToolDefinition,
|
|
494
|
-
ls: createLsToolDefinition,
|
|
515
|
+
type RendererAPI = ExtensionAPI & {
|
|
516
|
+
registerToolRenderer?: (resolver: (name: string, next: () => ToolDefinition<any, any, any> | undefined) => ToolDefinition<any, any, any> | undefined) => void;
|
|
495
517
|
};
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
518
|
+
let publicRendering = false;
|
|
519
|
+
let sessionReady = false;
|
|
520
|
+
let publicOwners = new WeakMap<object, { call?: unknown; result?: unknown }>();
|
|
521
|
+
function publicRowDefinition(name: string, base: ToolDefinition<any, any, any> | undefined): ToolDefinition<any, any, any> | undefined {
|
|
522
|
+
// Preserve next()'s result exactly once the session's configuration is known.
|
|
523
|
+
let config: unknown;
|
|
524
|
+
let decision: RowRenderers | undefined;
|
|
525
|
+
const decide = () => {
|
|
526
|
+
if (config !== runtime.config) {
|
|
527
|
+
config = runtime.config;
|
|
528
|
+
decision = resolveCustomRow({ toolName: name, toolDefinition: base });
|
|
529
|
+
}
|
|
530
|
+
return decision;
|
|
531
|
+
};
|
|
532
|
+
if (!decide() && (sessionReady || !base)) return base;
|
|
533
|
+
const context = (ctx: Parameters<RowRenderers["renderCall"]>[2], kind: "call" | "result", render: unknown): typeof ctx => {
|
|
534
|
+
let owner = publicOwners.get(ctx.state);
|
|
535
|
+
if (!owner) publicOwners.set(ctx.state, owner = {});
|
|
536
|
+
const changed = owner[kind] !== render;
|
|
537
|
+
owner[kind] = render;
|
|
538
|
+
return changed ? { ...ctx, lastComponent: undefined } : ctx;
|
|
539
|
+
};
|
|
540
|
+
const call: RowRenderers["renderCall"] = (args, theme, ctx) => {
|
|
541
|
+
const render = decide()?.renderCall ?? base?.renderCall;
|
|
542
|
+
return render!(args, theme, context(ctx, "call", render));
|
|
543
|
+
};
|
|
544
|
+
const result: RowRenderers["renderResult"] = (value, options, theme, ctx) => {
|
|
545
|
+
const render = decide()?.renderResult ?? base?.renderResult;
|
|
546
|
+
return render!(value, options, theme, context(ctx, "result", render));
|
|
547
|
+
};
|
|
548
|
+
return {
|
|
549
|
+
get renderShell() { return decide() ? "self" : base?.renderShell ?? "default"; },
|
|
550
|
+
get renderCall() { return decide()?.renderCall || base?.renderCall ? call : undefined; },
|
|
551
|
+
get renderResult() { return decide()?.renderResult || base?.renderResult ? result : undefined; },
|
|
552
|
+
} as ToolDefinition<any, any, any>;
|
|
553
|
+
}
|
|
554
|
+
let legacyResolver: ((row: ToolRow) => RowRenderers | undefined) | undefined;
|
|
555
|
+
let refreshTranscriptRows: (() => void) | undefined;
|
|
556
|
+
function invalidateRowDecisions(): void {
|
|
557
|
+
if (!publicRendering) {
|
|
558
|
+
legacyResolver = (row) => resolveCustomRow(row);
|
|
559
|
+
setRowResolver(legacyResolver);
|
|
524
560
|
}
|
|
561
|
+
refreshTranscriptRows?.();
|
|
525
562
|
}
|
|
526
563
|
|
|
527
|
-
function configure(
|
|
564
|
+
function configure(_pi: ExtensionAPI, cwd?: string, projectTrusted = false): void {
|
|
528
565
|
const resolvedCwd = cwd ?? process.cwd();
|
|
529
566
|
const config = loadConfig(resolvedCwd, projectTrusted);
|
|
530
567
|
const signature = JSON.stringify([resolvedCwd, config]);
|
|
531
568
|
if (signature === registeredConfiguration) return;
|
|
532
569
|
runtime.configure(config);
|
|
533
|
-
registerTools(pi, resolvedCwd);
|
|
534
570
|
registeredConfiguration = signature;
|
|
571
|
+
invalidateRowDecisions();
|
|
535
572
|
}
|
|
536
573
|
|
|
537
574
|
/**
|
|
@@ -547,12 +584,7 @@ function applyStyle(cwd: string, trusted: boolean): boolean {
|
|
|
547
584
|
if (JSON.stringify({ ...config, style: current.style }) !== JSON.stringify(current)) return false;
|
|
548
585
|
runtime.configure(config);
|
|
549
586
|
registeredConfiguration = JSON.stringify([cwd, config]);
|
|
550
|
-
|
|
551
|
-
for (const child of chat?.children ?? []) {
|
|
552
|
-
// Pi asks a row's renderers again whenever it redraws the row; this is that redraw.
|
|
553
|
-
if (isKind(child, ToolExecutionComponent)) (child as { updateDisplay?(): void }).updateDisplay?.();
|
|
554
|
-
}
|
|
555
|
-
boundTui?.requestRender();
|
|
587
|
+
invalidateRowDecisions();
|
|
556
588
|
return true;
|
|
557
589
|
}
|
|
558
590
|
|
|
@@ -576,32 +608,41 @@ function madeByTool(event: object): boolean {
|
|
|
576
608
|
return typeof (event as { parentToolCallId?: unknown }).parentToolCallId === "string";
|
|
577
609
|
}
|
|
578
610
|
|
|
579
|
-
/** Pi's TUI while one is bound: where a style switched in place finds the rows to draw again. */
|
|
580
|
-
let boundTui: { requestRender(): void } | undefined;
|
|
581
|
-
|
|
582
611
|
/** Hand the animation Pi's frame request, reached through an invisible widget as the extension API allows. */
|
|
583
612
|
function bindRenderer(ctx: ExtensionContext): void {
|
|
584
613
|
ctx.ui.setWidget(RENDER_WIDGET_KEY, (tui) => {
|
|
585
|
-
boundTui = tui;
|
|
586
614
|
runtime.bindRenderer(() => tui.requestRender());
|
|
615
|
+
refreshTranscriptRows = () => {
|
|
616
|
+
const visit = (parent: { children?: unknown[] }) => {
|
|
617
|
+
for (const child of parent.children ?? []) {
|
|
618
|
+
if (!child || typeof child !== "object") continue;
|
|
619
|
+
if (isKind(child, ToolExecutionComponent)) {
|
|
620
|
+
const row = child as ToolRow & { updateDisplay?(): void };
|
|
621
|
+
refreshRowShell(row);
|
|
622
|
+
row.updateDisplay?.();
|
|
623
|
+
} else visit(child as { children?: unknown[] });
|
|
624
|
+
}
|
|
625
|
+
};
|
|
626
|
+
const chat = findChat(tui);
|
|
627
|
+
if (chat && chat.children.length > 0) { visit(chat); tui.requestRender(); }
|
|
628
|
+
};
|
|
587
629
|
return { render: () => [], invalidate() {} };
|
|
588
630
|
}, { placement: "belowEditor" });
|
|
631
|
+
refreshTranscriptRows?.();
|
|
589
632
|
}
|
|
590
633
|
|
|
591
634
|
function unbindRenderer(ctx: ExtensionContext | undefined): void {
|
|
592
|
-
|
|
635
|
+
refreshTranscriptRows = undefined;
|
|
593
636
|
runtime.bindRenderer(undefined);
|
|
594
637
|
ctx?.ui.setWidget(RENDER_WIDGET_KEY, undefined, { placement: "belowEditor" });
|
|
595
638
|
}
|
|
596
639
|
|
|
597
640
|
export default function compactTools(pi: ExtensionAPI): void {
|
|
598
|
-
//
|
|
599
|
-
//
|
|
600
|
-
// an empty tool registry. The module itself survives those invocations, so the
|
|
601
|
-
// memo below has to be dropped here or configure() mistakes the previous
|
|
602
|
-
// instance's registration for this one and never installs the renderers.
|
|
641
|
+
// The cached factory is invoked again after session replacement and reload.
|
|
642
|
+
// Reset configuration memoization, but never change the host's tool registry.
|
|
603
643
|
registeredConfiguration = undefined;
|
|
604
|
-
|
|
644
|
+
sessionReady = false;
|
|
645
|
+
publicOwners = new WeakMap();
|
|
605
646
|
|
|
606
647
|
registerCompactToolsCommand(pi, () => runtime.config, applyStyle);
|
|
607
648
|
const thinkingCycle = new ThinkingCycleController(pi);
|
|
@@ -609,12 +650,22 @@ export default function compactTools(pi: ExtensionAPI): void {
|
|
|
609
650
|
const viewport = new ViewportKeeper();
|
|
610
651
|
const silent = new SilentModeController(pi, showReleaseNotice);
|
|
611
652
|
const groups = new ToolGroupController(runtime);
|
|
612
|
-
|
|
613
|
-
|
|
653
|
+
builtinStates = new WeakMap();
|
|
654
|
+
authorStates = new WeakMap();
|
|
655
|
+
const rendererAPI = pi as RendererAPI;
|
|
656
|
+
publicRendering = typeof rendererAPI.registerToolRenderer === "function";
|
|
657
|
+
const customRowsAvailable = publicRendering || installToolRowPatch();
|
|
658
|
+
if (publicRendering) {
|
|
659
|
+
setRowResolver(undefined);
|
|
660
|
+
rendererAPI.registerToolRenderer!((name, next) => {
|
|
661
|
+
const base = next();
|
|
662
|
+
return publicRowDefinition(name, base as ToolDefinition<any, any, any> | undefined);
|
|
663
|
+
});
|
|
664
|
+
} else invalidateRowDecisions();
|
|
614
665
|
let customRowsWarned = false;
|
|
615
666
|
let renderContext: ExtensionContext | undefined;
|
|
616
|
-
//
|
|
617
|
-
//
|
|
667
|
+
// Configure UI routing before Pi rebuilds transcript rows. Tool declarations
|
|
668
|
+
// and the active loadout must remain exactly as the host selected them.
|
|
618
669
|
const cwd = process.cwd();
|
|
619
670
|
const lastTrust = (globalThis as TrustSlot)[TRUST_KEY];
|
|
620
671
|
configure(pi, cwd, lastTrust?.cwd === realDirectory(cwd) && lastTrust.trusted);
|
|
@@ -625,11 +676,13 @@ export default function compactTools(pi: ExtensionAPI): void {
|
|
|
625
676
|
const trusted = ctx.isProjectTrusted();
|
|
626
677
|
(globalThis as TrustSlot)[TRUST_KEY] = { cwd: realDirectory(ctx.cwd), trusted };
|
|
627
678
|
configure(pi, ctx.cwd, trusted);
|
|
679
|
+
sessionReady = true;
|
|
628
680
|
unbindRenderer(renderContext);
|
|
629
681
|
renderContext = undefined;
|
|
682
|
+
// Even off must reconcile rows built before trusted config was available.
|
|
683
|
+
if (ctx.mode === "tui") bindRenderer(ctx);
|
|
630
684
|
if (ctx.mode === "tui" && runtime.config.style !== "off") {
|
|
631
685
|
renderContext = ctx;
|
|
632
|
-
bindRenderer(ctx);
|
|
633
686
|
// First, so its input listener sees relayout keys before the thinking controller consumes them.
|
|
634
687
|
viewport.bind(ctx);
|
|
635
688
|
progress.bind(ctx);
|
|
@@ -637,11 +690,12 @@ export default function compactTools(pi: ExtensionAPI): void {
|
|
|
637
690
|
silent.bind(ctx, runtime.config.mode, event.reason === "reload");
|
|
638
691
|
groups.bind(ctx);
|
|
639
692
|
if (!silent.isEnabled()) showReleaseNotice(ctx);
|
|
640
|
-
if (runtime.config.custom_tools.enabled && !customRowsAvailable && !customRowsWarned) {
|
|
693
|
+
if ((runtime.config.custom_tools.enabled || runtime.config.tools.length > 0) && !customRowsAvailable && !customRowsWarned) {
|
|
641
694
|
customRowsWarned = true;
|
|
642
|
-
ctx.ui.notify("Compact rendering
|
|
695
|
+
ctx.ui.notify("Compact tool rendering is unavailable in this version of Pi", "warning");
|
|
643
696
|
}
|
|
644
697
|
} else {
|
|
698
|
+
if (ctx.mode === "tui") unbindRenderer(ctx);
|
|
645
699
|
viewport.dispose();
|
|
646
700
|
progress.dispose();
|
|
647
701
|
thinkingCycle.dispose();
|
|
@@ -661,7 +715,7 @@ export default function compactTools(pi: ExtensionAPI): void {
|
|
|
661
715
|
});
|
|
662
716
|
pi.on("tool_execution_end", (event) => {
|
|
663
717
|
if (madeByTool(event)) return runtime.noteNestedCall(event.toolCallId, false);
|
|
664
|
-
runtime.noteExecutionEnd(event.toolCallId);
|
|
718
|
+
runtime.noteExecutionEnd(event.toolCallId, (event as { durationMs?: number }).durationMs);
|
|
665
719
|
});
|
|
666
720
|
pi.on("session_shutdown", (event) => {
|
|
667
721
|
thinkingCycle.dispose();
|
|
@@ -673,6 +727,7 @@ export default function compactTools(pi: ExtensionAPI): void {
|
|
|
673
727
|
renderContext = undefined;
|
|
674
728
|
runtime.reset(event.reason !== "reload");
|
|
675
729
|
// A reload that keeps the extension sets it again before Pi rebuilds the transcript.
|
|
676
|
-
clearRowResolver(
|
|
730
|
+
if (legacyResolver) clearRowResolver(legacyResolver);
|
|
731
|
+
refreshTranscriptRows = undefined;
|
|
677
732
|
});
|
|
678
733
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-compact-tools",
|
|
3
|
-
"version": "0.14.
|
|
3
|
+
"version": "0.14.2",
|
|
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.1.0",
|
|
55
|
+
"@earendil-works/pi-tui": "1.1.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",
|
package/release-notes.json
CHANGED
|
@@ -1,4 +1,26 @@
|
|
|
1
1
|
{
|
|
2
|
+
"0.14.2": {
|
|
3
|
+
"New": [
|
|
4
|
+
"Supports Pi 1.1 recorded execution durations and output padding when those fields are available"
|
|
5
|
+
],
|
|
6
|
+
"Fix": [
|
|
7
|
+
"Restored Compact and expanded Codex rows retain their recorded execution time, including zero-duration calls",
|
|
8
|
+
"Final host-recorded durations stay accurate when the system clock changes",
|
|
9
|
+
"Tool rows and folded groups honor outputPad without changing their internal style indentation",
|
|
10
|
+
"Embedded third-party renderers do not receive a duplicate outer margin",
|
|
11
|
+
"Preserves existing timing and margins on older Pi versions; the minimum supported version remains Pi 0.85.1"
|
|
12
|
+
],
|
|
13
|
+
"Deprecated": []
|
|
14
|
+
},
|
|
15
|
+
"0.14.1": {
|
|
16
|
+
"Fix": [
|
|
17
|
+
"Fixed unintended input-token overhead: compact rendering no longer registers or activates tools that Pi would otherwise leave inactive",
|
|
18
|
+
"Preserves Pi’s tool execution settings and other extensions’ tool definitions and renderers",
|
|
19
|
+
"Fixed tool rows not updating correctly after reloads or trusted-project style changes",
|
|
20
|
+
"Uses Pi’s native renderer API on Pi 1.0.2 and later, including compact HTML exports",
|
|
21
|
+
"Supports Pi 0.85.1 and later; Pi 1.0.2 or later is recommended. On earlier Pi versions, HTML exports use Pi’s default renderers"
|
|
22
|
+
]
|
|
23
|
+
},
|
|
2
24
|
"0.14.0": {
|
|
3
25
|
"New": [
|
|
4
26
|
"Codex style added.",
|