pi-compact-tools 0.11.1 → 0.12.0
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 +3 -3
- package/extensions/compact-tools-activity.ts +15 -10
- package/extensions/compact-tools-claude.ts +22 -4
- package/extensions/compact-tools-grouping.ts +102 -20
- package/extensions/compact-tools-layout.ts +58 -2
- package/extensions/compact-tools-palette.ts +1 -1
- package/extensions/compact-tools-runtime.ts +112 -22
- package/extensions/compact-tools-types.ts +2 -0
- package/extensions/compact-tools.ts +77 -18
- package/package.json +1 -1
- package/release-notes.json +10 -0
package/README.md
CHANGED
|
@@ -18,7 +18,7 @@ Pick a style. Silent mode works on top of either.
|
|
|
18
18
|
|
|
19
19
|
| | Setting | Shows | How |
|
|
20
20
|
|---|---|---|---|
|
|
21
|
-
| **Compact** | Style | every tool call,
|
|
21
|
+
| **Compact** | Style | every tool call as its own compact, expandable row | `"style": "compact"` (default) |
|
|
22
22
|
| **Claude** | Style | reads and searches folded into one line, like Claude Code | `"style": "claude"` |
|
|
23
23
|
| **Silent** | Mode | only your prompts and the answers | `Ctrl+'` or `/silent` |
|
|
24
24
|
|
|
@@ -26,9 +26,9 @@ Set `"style"` in the config below, then `/reload`. `"style": "off"` restores Pi'
|
|
|
26
26
|
|
|
27
27
|
### Style: Compact
|
|
28
28
|
|
|
29
|
-

|
|
30
30
|
|
|
31
|
-
-
|
|
31
|
+
- Each call: what it ran, then its status, duration, and line count.
|
|
32
32
|
- Edits show a highlighted diff with `(+3 -1)`; reads and writes show numbered code.
|
|
33
33
|
|
|
34
34
|
### Style: Claude
|
|
@@ -3,7 +3,7 @@ import type { TUI } from "@earendil-works/pi-tui";
|
|
|
3
3
|
import { onTick } from "./compact-tools-clock.ts";
|
|
4
4
|
import { colorizeRgb, interpolateRgb, type Rgb } from "./compact-tools-color.ts";
|
|
5
5
|
import { hookMethod } from "./compact-tools-hook.ts";
|
|
6
|
-
import { activityGlow, type ColorRamp, type ThinkingLevel } from "./compact-tools-palette.ts";
|
|
6
|
+
import { activityGlow, thinkingLevelColor, type ColorRamp, type ThinkingLevel } from "./compact-tools-palette.ts";
|
|
7
7
|
|
|
8
8
|
const WIDGET_KEY = "compact-tools-silent-activity-render";
|
|
9
9
|
const ACTIVITY_STATE = Symbol.for("pi-compact-tools.silent.activity.state");
|
|
@@ -109,17 +109,22 @@ export function frameChanges(frame: number): boolean {
|
|
|
109
109
|
return FRAME_CHANGED[frameIndex(frame)]!;
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
-
|
|
113
|
-
|
|
112
|
+
/**
|
|
113
|
+
* Resolving a ramp walks the theme's colors, so each level keeps its last one.
|
|
114
|
+
* It is kept against the colors it is made from, the level's and the text's, not
|
|
115
|
+
* against the theme: Pi's theme is one object whose palette /theme swaps.
|
|
116
|
+
*/
|
|
117
|
+
const glowCache = new Map<ThinkingLevel, { colors: string; glow: ColorRamp | undefined }>();
|
|
114
118
|
|
|
115
119
|
function cachedGlow(theme: Theme, level: ThinkingLevel): ColorRamp | undefined {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
120
|
+
const colors = typeof theme.getFgAnsi === "function"
|
|
121
|
+
? theme.getFgAnsi(thinkingLevelColor(level)) + theme.getFgAnsi("text")
|
|
122
|
+
: "";
|
|
123
|
+
const cached = glowCache.get(level);
|
|
124
|
+
if (cached && cached.colors === colors) return cached.glow;
|
|
125
|
+
const glow = activityGlow(theme, level);
|
|
126
|
+
glowCache.set(level, { colors, glow });
|
|
127
|
+
return glow;
|
|
123
128
|
}
|
|
124
129
|
|
|
125
130
|
function toneColor(glow: ColorRamp, intensity: number): Rgb {
|
|
@@ -51,10 +51,28 @@ function commandOf(args: ToolArgs): string {
|
|
|
51
51
|
return typeof args.command === "string" ? args.command : "";
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
/** Claude Code shortens a collapsed command only past two lines or 160 characters. */
|
|
55
|
+
export const CLAUDE_COMMAND_LINES = 2;
|
|
56
|
+
export const CLAUDE_COMMAND_CHARS = 160;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* A command as Claude Code titles it: whole when opened, and collapsed cut only
|
|
60
|
+
* past its line and character limits, with an ellipsis. Within them it is never
|
|
61
|
+
* cut to fit the row; the title wraps instead.
|
|
62
|
+
*/
|
|
63
|
+
export function claudeCommand(command: string, expanded: boolean): string {
|
|
64
|
+
const text = normalizeLineEndings(stripTerminalSequences(command)).trim();
|
|
65
|
+
if (expanded) return text;
|
|
66
|
+
const lines = text.split("\n");
|
|
67
|
+
let shown = lines.length > CLAUDE_COMMAND_LINES ? lines.slice(0, CLAUDE_COMMAND_LINES).join("\n") : text;
|
|
68
|
+
if (shown.length > CLAUDE_COMMAND_CHARS) shown = shown.slice(0, CLAUDE_COMMAND_CHARS);
|
|
69
|
+
return shown === text ? text : `${shown.trim()}…`;
|
|
70
|
+
}
|
|
71
|
+
|
|
54
72
|
/** What goes between the parentheses: `Bash(npm test)`, `Search(pattern: "x", path: "src")`. */
|
|
55
|
-
export function claudeArgument(name: string, args: ToolArgs): string {
|
|
73
|
+
export function claudeArgument(name: string, args: ToolArgs, expanded = false): string {
|
|
56
74
|
const path = pathOf(args);
|
|
57
|
-
if (name === "bash" || name === "powershell") return
|
|
75
|
+
if (name === "bash" || name === "powershell") return claudeCommand(commandOf(args), expanded);
|
|
58
76
|
if (name === "grep" || name === "find") {
|
|
59
77
|
const pattern = typeof args.pattern === "string" ? oneLine(args.pattern) : "";
|
|
60
78
|
// Arguments stream in; until the pattern arrives there is nothing to name.
|
|
@@ -85,8 +103,8 @@ export function formatArguments(args: ToolArgs): string {
|
|
|
85
103
|
return parts.length > 0 ? parts.join(", ") : summarizeCustomArguments(args);
|
|
86
104
|
}
|
|
87
105
|
|
|
88
|
-
export function claudeTitle(name: string, args: ToolArgs): { label: string; argument: string } {
|
|
89
|
-
return { label: claudeLabel(name), argument: claudeArgument(name, args) };
|
|
106
|
+
export function claudeTitle(name: string, args: ToolArgs, expanded = false): { label: string; argument: string } {
|
|
107
|
+
return { label: claudeLabel(name), argument: claudeArgument(name, args, expanded) };
|
|
90
108
|
}
|
|
91
109
|
|
|
92
110
|
// Claude Code's own lists: a command made only of these only looks around, so it
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
ToolExecutionComponent,
|
|
4
4
|
type AgentToolResult,
|
|
5
5
|
type ExtensionContext,
|
|
6
|
+
type Theme,
|
|
6
7
|
} from "@earendil-works/pi-coding-agent";
|
|
7
8
|
import { Container, Spacer, Text, truncateToWidth, type Component } from "@earendil-works/pi-tui";
|
|
8
9
|
import { claudeFailure, lookHint, lookKind, pathOf, type LookKind } from "./compact-tools-claude.ts";
|
|
@@ -39,8 +40,18 @@ export interface ToolRowLike {
|
|
|
39
40
|
invalidate(): void;
|
|
40
41
|
}
|
|
41
42
|
|
|
42
|
-
|
|
43
|
-
|
|
43
|
+
/** Whether a call without its result can still get one; the row itself answers the same way. */
|
|
44
|
+
export type CanRun = (row: ToolRowLike) => boolean;
|
|
45
|
+
|
|
46
|
+
const ALWAYS_RUNS: CanRun = () => true;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* A call with no result yet runs, unless it can no longer get one: restored from
|
|
50
|
+
* the session without it, or left without it when its run ended. Then it waits,
|
|
51
|
+
* as its row shows.
|
|
52
|
+
*/
|
|
53
|
+
export function memberStatus(row: ToolRowLike, canRun: CanRun = ALWAYS_RUNS): RowStatus {
|
|
54
|
+
if (!row.result || row.isPartial) return canRun(row) ? "running" : "pending";
|
|
44
55
|
return row.result.isError ? "error" : "success";
|
|
45
56
|
}
|
|
46
57
|
|
|
@@ -60,9 +71,9 @@ function kindOf(row: ToolRowLike): LookKind | undefined {
|
|
|
60
71
|
return kind;
|
|
61
72
|
}
|
|
62
73
|
|
|
63
|
-
function lastRunning(rows: readonly ToolRowLike[]): ToolRowLike | undefined {
|
|
74
|
+
function lastRunning(rows: readonly ToolRowLike[], canRun: CanRun): ToolRowLike | undefined {
|
|
64
75
|
for (let index = rows.length - 1; index >= 0; index--) {
|
|
65
|
-
if (memberStatus(rows[index]
|
|
76
|
+
if (memberStatus(rows[index]!, canRun) === "running") return rows[index];
|
|
66
77
|
}
|
|
67
78
|
return undefined;
|
|
68
79
|
}
|
|
@@ -89,9 +100,13 @@ export function countGroup(rows: readonly ToolRowLike[]): Record<LookKind, numbe
|
|
|
89
100
|
* "Searched for 2 patterns, read 3 files". While any call in the group still runs,
|
|
90
101
|
* every verb reads in progress. `bold` marks the counts, as Claude Code does.
|
|
91
102
|
*/
|
|
92
|
-
export function summarizeGroup(
|
|
103
|
+
export function summarizeGroup(
|
|
104
|
+
rows: readonly ToolRowLike[],
|
|
105
|
+
bold: (text: string) => string = (text) => text,
|
|
106
|
+
canRun: CanRun = ALWAYS_RUNS,
|
|
107
|
+
): string {
|
|
93
108
|
const counts = countGroup(rows);
|
|
94
|
-
const running = lastRunning(rows) !== undefined;
|
|
109
|
+
const running = lastRunning(rows, canRun) !== undefined;
|
|
95
110
|
const text = PHRASES.filter((phrase) => counts[phrase.kind] > 0)
|
|
96
111
|
.map((phrase) => {
|
|
97
112
|
const count = counts[phrase.kind];
|
|
@@ -102,8 +117,8 @@ export function summarizeGroup(rows: readonly ToolRowLike[], bold: (text: string
|
|
|
102
117
|
}
|
|
103
118
|
|
|
104
119
|
/** While a call runs, the line under its group names what it looks at. */
|
|
105
|
-
export function groupHint(rows: readonly ToolRowLike[]): string | undefined {
|
|
106
|
-
const running = lastRunning(rows);
|
|
120
|
+
export function groupHint(rows: readonly ToolRowLike[], canRun: CanRun = ALWAYS_RUNS): string | undefined {
|
|
121
|
+
const running = lastRunning(rows, canRun);
|
|
107
122
|
if (!running) return undefined;
|
|
108
123
|
return lookHint(running.toolName, argsOf(running)) || undefined;
|
|
109
124
|
}
|
|
@@ -114,7 +129,33 @@ export function groupFailures(rows: readonly ToolRowLike[]): string[] {
|
|
|
114
129
|
.map((row) => claudeFailure(row.toolName, getTextResult(row.result!)).headline);
|
|
115
130
|
}
|
|
116
131
|
|
|
117
|
-
|
|
132
|
+
/**
|
|
133
|
+
* A finished group's lines, and everything they were drawn from. Only a group with
|
|
134
|
+
* no running row is kept, so every row in it has its final result.
|
|
135
|
+
*/
|
|
136
|
+
type HeaderCache = {
|
|
137
|
+
paint: string;
|
|
138
|
+
width: number;
|
|
139
|
+
expanded: boolean;
|
|
140
|
+
rows: readonly ToolRowLike[];
|
|
141
|
+
args: unknown[];
|
|
142
|
+
results: unknown[];
|
|
143
|
+
lines: string[];
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
type GroupState = { expanded: boolean; lastHostExpanded: boolean; header?: HeaderCache };
|
|
147
|
+
|
|
148
|
+
/** The colors a group line is drawn in, resolved once per frame rather than once per group. */
|
|
149
|
+
type Paint = { theme: Theme; chrome: (text: string) => string; key: string };
|
|
150
|
+
|
|
151
|
+
/** Theme colors a group line uses; a theme that changes any of them draws the lines anew. */
|
|
152
|
+
const PAINT_COLORS = ["toolTitle", "error", "success", "muted", "dim", "text", "borderMuted"] as const;
|
|
153
|
+
|
|
154
|
+
function headerStill(cache: HeaderCache, rows: readonly ToolRowLike[], paint: string, width: number, expanded: boolean): boolean {
|
|
155
|
+
if (cache.paint !== paint || cache.width !== width || cache.expanded !== expanded || cache.rows.length !== rows.length) return false;
|
|
156
|
+
return rows.every((row, index) => row === cache.rows[index] && row.args === cache.args[index]
|
|
157
|
+
&& row.result === cache.results[index]);
|
|
158
|
+
}
|
|
118
159
|
|
|
119
160
|
/** A run of tool rows drawn as one summary line, which a click or Ctrl+O opens into the rows themselves. */
|
|
120
161
|
export class ToolGroupComponent extends Container {
|
|
@@ -212,6 +253,10 @@ export function groupChildren(
|
|
|
212
253
|
export class ToolGroupController {
|
|
213
254
|
private context: ExtensionContext | undefined;
|
|
214
255
|
private readonly states = new WeakMap<object, GroupState>();
|
|
256
|
+
/** This frame's colors, set as the chat starts drawing. */
|
|
257
|
+
private paint: Paint | undefined;
|
|
258
|
+
/** Asks the runtime, which knows which run each call belongs to, as the call's own row does. */
|
|
259
|
+
private readonly canRun: CanRun = (row) => this.runtime.canRun(row.toolCallId);
|
|
215
260
|
|
|
216
261
|
constructor(private readonly runtime: ToolRuntime) {}
|
|
217
262
|
|
|
@@ -225,11 +270,13 @@ export class ToolGroupController {
|
|
|
225
270
|
hookMethod(chat, "render", "claude.groups", (self, args, original) => {
|
|
226
271
|
if (!this.context || isSilent() || this.runtime.config.style !== "claude") return original.apply(self, args);
|
|
227
272
|
const children = self.children as unknown[];
|
|
273
|
+
this.paint = this.resolvePaint();
|
|
228
274
|
self.children = groupChildren(children, (rows, members) => this.makeGroup(rows, members, tui));
|
|
229
275
|
try {
|
|
230
276
|
return original.apply(self, args);
|
|
231
277
|
} finally {
|
|
232
278
|
self.children = children;
|
|
279
|
+
this.paint = undefined;
|
|
233
280
|
}
|
|
234
281
|
});
|
|
235
282
|
}
|
|
@@ -249,8 +296,40 @@ export class ToolGroupController {
|
|
|
249
296
|
state = { expanded: key.expanded, lastHostExpanded: key.expanded };
|
|
250
297
|
this.states.set(key, state);
|
|
251
298
|
}
|
|
252
|
-
|
|
253
|
-
|
|
299
|
+
const groupState = state;
|
|
300
|
+
return new ToolGroupComponent(rows, members, state,
|
|
301
|
+
(group, width, expanded) => this.drawCached(group, width, expanded, groupState), () => tui.requestRender());
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
private resolvePaint(): Paint | undefined {
|
|
305
|
+
const theme = this.context?.ui.theme;
|
|
306
|
+
if (!theme) return undefined;
|
|
307
|
+
const chrome = chromePainter(theme);
|
|
308
|
+
const key = PAINT_COLORS.map((color) => theme.fg(color, "x")).join("") + theme.bold("x") + chrome("x");
|
|
309
|
+
return { theme, chrome, key };
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* A finished group draws the same lines until one of its rows, the width, the
|
|
314
|
+
* expansion, or the theme changes, so those lines are kept. A running group's
|
|
315
|
+
* line pulses and counts time, so it is drawn every frame.
|
|
316
|
+
*/
|
|
317
|
+
private drawCached(rows: readonly ToolRowLike[], width: number, expanded: boolean, state: GroupState): string[] {
|
|
318
|
+
const paint = this.paint ?? this.resolvePaint();
|
|
319
|
+
if (!paint) return [];
|
|
320
|
+
if (lastRunning(rows, this.canRun)) return this.draw(rows, width, expanded, paint);
|
|
321
|
+
if (state.header && headerStill(state.header, rows, paint.key, width, expanded)) return state.header.lines;
|
|
322
|
+
const lines = this.draw(rows, width, expanded, paint);
|
|
323
|
+
state.header = {
|
|
324
|
+
paint: paint.key,
|
|
325
|
+
width,
|
|
326
|
+
expanded,
|
|
327
|
+
rows: [...rows],
|
|
328
|
+
args: rows.map((row) => row.args),
|
|
329
|
+
results: rows.map((row) => row.result),
|
|
330
|
+
lines,
|
|
331
|
+
};
|
|
332
|
+
return lines;
|
|
254
333
|
}
|
|
255
334
|
|
|
256
335
|
/**
|
|
@@ -258,23 +337,26 @@ export class ToolGroupController {
|
|
|
258
337
|
* the title style. While it runs the dot pulses and the line below names what it
|
|
259
338
|
* looks at.
|
|
260
339
|
*/
|
|
261
|
-
private draw(rows: readonly ToolRowLike[], width: number, expanded: boolean): string[] {
|
|
262
|
-
const theme =
|
|
263
|
-
if (!theme) return [];
|
|
264
|
-
const chrome = chromePainter(theme);
|
|
340
|
+
private draw(rows: readonly ToolRowLike[], width: number, expanded: boolean, paint: Paint): string[] {
|
|
341
|
+
const { theme, chrome } = paint;
|
|
265
342
|
const fit = (line: string) => truncateToWidth(line, Math.max(1, width), "…");
|
|
266
|
-
const running = lastRunning(rows);
|
|
267
|
-
const
|
|
268
|
-
|
|
269
|
-
|
|
343
|
+
const running = lastRunning(rows, this.canRun);
|
|
344
|
+
const statuses = rows.map((row) => memberStatus(row, this.canRun));
|
|
345
|
+
// A call that waits for a result it will never get leaves the group waiting too.
|
|
346
|
+
const status: RowStatus = running ? "running" : statuses.includes("error") ? "error"
|
|
347
|
+
: statuses.includes("pending") ? "pending" : "success";
|
|
348
|
+
const summary = summarizeGroup(rows, undefined, this.canRun);
|
|
349
|
+
let title = `${paintIndicator(theme, status, this.runtime.frame)} ${theme.fg("toolTitle", theme.bold(summary))}`;
|
|
270
350
|
if (running) {
|
|
351
|
+
// The folded row is not drawn, so the line that pulses for it keeps the clock going.
|
|
352
|
+
this.runtime.keepAnimating(running.toolCallId);
|
|
271
353
|
const started = this.runtime.timing(running.toolCallId)?.startedAt;
|
|
272
354
|
const elapsed = started === undefined ? 0 : Date.now() - started;
|
|
273
355
|
title += `${elapsed >= 2000 ? chrome(` · ${formatDurationMs(elapsed)}`) : ""}…`;
|
|
274
356
|
}
|
|
275
357
|
if (!expanded) title += chrome(` ${EXPAND_HINT}`);
|
|
276
358
|
const lines = ["", fit(` ${title}`)];
|
|
277
|
-
const hintText = groupHint(rows);
|
|
359
|
+
const hintText = groupHint(rows, this.canRun);
|
|
278
360
|
if (hintText) lines.push(fit(chrome(` └ ${hintText}`)));
|
|
279
361
|
if (!expanded) for (const failure of groupFailures(rows)) lines.push(fit(chrome(" └ ") + theme.fg("error", failure)));
|
|
280
362
|
return lines;
|
|
@@ -39,13 +39,24 @@ class CachedComponent implements Component {
|
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
/**
|
|
42
|
+
/**
|
|
43
|
+
* A Container that does not re-copy all child lines on every fullscreen scroll
|
|
44
|
+
* frame. Its lines are kept for a width, and for whatever else `cacheKey()`
|
|
45
|
+
* says they depend on, until a child is added, removed, or invalidated.
|
|
46
|
+
*/
|
|
43
47
|
export class CachedContainer extends Container {
|
|
44
48
|
private cachedWidth?: number;
|
|
49
|
+
private cachedKey?: unknown;
|
|
45
50
|
private cachedLines?: string[];
|
|
46
51
|
|
|
52
|
+
/** What besides the width the lines depend on, read as each frame is drawn. */
|
|
53
|
+
protected cacheKey(): unknown {
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
|
|
47
57
|
private clearRenderCache(): void {
|
|
48
58
|
this.cachedWidth = undefined;
|
|
59
|
+
this.cachedKey = undefined;
|
|
49
60
|
this.cachedLines = undefined;
|
|
50
61
|
}
|
|
51
62
|
|
|
@@ -65,8 +76,10 @@ export class CachedContainer extends Container {
|
|
|
65
76
|
}
|
|
66
77
|
|
|
67
78
|
override render(width: number): string[] {
|
|
68
|
-
|
|
79
|
+
const key = this.cacheKey();
|
|
80
|
+
if (this.cachedWidth !== width || this.cachedKey !== key || !this.cachedLines) {
|
|
69
81
|
this.cachedWidth = width;
|
|
82
|
+
this.cachedKey = key;
|
|
70
83
|
this.cachedLines = super.render(width);
|
|
71
84
|
}
|
|
72
85
|
return this.cachedLines;
|
|
@@ -78,6 +91,49 @@ export class CachedContainer extends Container {
|
|
|
78
91
|
}
|
|
79
92
|
}
|
|
80
93
|
|
|
94
|
+
/**
|
|
95
|
+
* A call line whose status dot is painted when the row is drawn rather than when
|
|
96
|
+
* it was built. The dot follows the row's state and the animation frame without
|
|
97
|
+
* Pi rebuilding the row, and a line is laid out once for each dot it has shown.
|
|
98
|
+
*/
|
|
99
|
+
export class LiveCallContainer extends CachedContainer {
|
|
100
|
+
private readonly titles = new Map<string, Component>();
|
|
101
|
+
private indicator = "";
|
|
102
|
+
private readonly titleLine: Component = {
|
|
103
|
+
render: (width) => this.title().render(width),
|
|
104
|
+
invalidate: () => this.titles.clear(),
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
constructor(
|
|
108
|
+
private readonly paintIndicator: () => string,
|
|
109
|
+
private readonly renderTitle: (indicator: string) => Component,
|
|
110
|
+
) {
|
|
111
|
+
super();
|
|
112
|
+
super.addChild(this.titleLine);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Clearing removes what was added beneath the call line; the line itself stays. */
|
|
116
|
+
override clear(): void {
|
|
117
|
+
super.clear();
|
|
118
|
+
super.addChild(this.titleLine);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** The dot, painted now; the title drawn beneath it follows. */
|
|
122
|
+
protected override cacheKey(): string {
|
|
123
|
+
this.indicator = this.paintIndicator();
|
|
124
|
+
return this.indicator;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
private title(): Component {
|
|
128
|
+
let title = this.titles.get(this.indicator);
|
|
129
|
+
if (!title) {
|
|
130
|
+
title = this.renderTitle(this.indicator);
|
|
131
|
+
this.titles.set(this.indicator, title);
|
|
132
|
+
}
|
|
133
|
+
return title;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
81
137
|
export function prefixedText(text: string, firstPrefix: string, continuationPrefix = firstPrefix): Component {
|
|
82
138
|
const prefixWidth = Math.max(visibleWidth(firstPrefix), visibleWidth(continuationPrefix));
|
|
83
139
|
const normalized = text.replace(/\t/g, " ");
|
|
@@ -66,7 +66,7 @@ const THINKING_LEVEL_COLOR = {
|
|
|
66
66
|
} as const satisfies Record<ThinkingLevel, ThemeForeground>;
|
|
67
67
|
|
|
68
68
|
/** The theme color a thinking level is painted with. */
|
|
69
|
-
function thinkingLevelColor(level: ThinkingLevel): ThemeForeground {
|
|
69
|
+
export function thinkingLevelColor(level: ThinkingLevel): ThemeForeground {
|
|
70
70
|
return THINKING_LEVEL_COLOR[level] ?? "thinkingMedium";
|
|
71
71
|
}
|
|
72
72
|
|
|
@@ -12,9 +12,16 @@ import {
|
|
|
12
12
|
|
|
13
13
|
const MAX_TRACKED_ROWS = 2_000;
|
|
14
14
|
const INDICATOR_INTERVAL_MS = 45;
|
|
15
|
+
/**
|
|
16
|
+
* How long a running row may go undrawn before it stops asking for frames. Pi draws
|
|
17
|
+
* every row it shows on every frame, so only a row that left the transcript, or
|
|
18
|
+
* never got its result, goes this long; it asks again the next time it is drawn.
|
|
19
|
+
*/
|
|
20
|
+
const UNDRAWN_ROW_MS = 1_500;
|
|
15
21
|
const EXECUTION_TIMINGS_KEY = Symbol.for("pi.compact-tools.execution-timings");
|
|
16
22
|
|
|
17
23
|
type ExecutionTiming = { startedAt: number; endedAt?: number };
|
|
24
|
+
type Indicator = { invalidate: () => void; drawnAt: number };
|
|
18
25
|
type TimedExecute = (...args: any[]) => Promise<AgentToolResult<unknown>>;
|
|
19
26
|
type SharedState = typeof globalThis & {
|
|
20
27
|
[EXECUTION_TIMINGS_KEY]?: Map<string, ExecutionTiming>;
|
|
@@ -24,11 +31,21 @@ export class ToolRuntime {
|
|
|
24
31
|
private configValue = DEFAULT_CONFIG;
|
|
25
32
|
private configRevision: object = {};
|
|
26
33
|
private readonly executionTimings: Map<string, ExecutionTiming>;
|
|
27
|
-
private readonly
|
|
34
|
+
private readonly indicators = new Map<string, Indicator>();
|
|
28
35
|
private indicatorFrame = 0;
|
|
29
36
|
private indicatorTimer: ReturnType<typeof setInterval> | undefined;
|
|
37
|
+
/** Pi's frame request, once bound to its terminal; rows then repaint without being rebuilt. */
|
|
38
|
+
private requestRender: (() => void) | undefined;
|
|
39
|
+
/** Whether the agent is working; undefined until the host says, which counts as working. */
|
|
40
|
+
private busy: boolean | undefined;
|
|
41
|
+
/**
|
|
42
|
+
* The calls Pi announced in the run going on. Only these can be running, so
|
|
43
|
+
* nothing about earlier calls has to be remembered, and the set never holds
|
|
44
|
+
* more than one run's calls.
|
|
45
|
+
*/
|
|
46
|
+
private readonly currentCalls = new Set<string>();
|
|
30
47
|
|
|
31
|
-
constructor() {
|
|
48
|
+
constructor(private readonly clock: () => number = () => performance.now()) {
|
|
32
49
|
const shared = globalThis as SharedState;
|
|
33
50
|
this.executionTimings = shared[EXECUTION_TIMINGS_KEY] ?? new Map<string, ExecutionTiming>();
|
|
34
51
|
shared[EXECUTION_TIMINGS_KEY] = this.executionTimings;
|
|
@@ -62,24 +79,76 @@ export class ToolRuntime {
|
|
|
62
79
|
if (clearTimings) this.clearTimings();
|
|
63
80
|
}
|
|
64
81
|
|
|
82
|
+
/**
|
|
83
|
+
* Bind the animation to Pi's frame requests. A running dot is painted when its
|
|
84
|
+
* row is drawn, so a frame only has to be asked for, once, however many rows
|
|
85
|
+
* run. Unbound, each running row is rebuilt to repaint, as Pi does on its own.
|
|
86
|
+
*/
|
|
87
|
+
bindRenderer(requestRender: (() => void) | undefined): void {
|
|
88
|
+
this.requestRender = requestRender;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* The agent started or stopped working. Once it stops nothing can still be
|
|
93
|
+
* running, so a row that never got its result stops pulsing and waits instead.
|
|
94
|
+
*/
|
|
95
|
+
setBusy(busy: boolean): void {
|
|
96
|
+
if (busy && this.busy !== true) this.currentCalls.clear();
|
|
97
|
+
this.busy = busy;
|
|
98
|
+
if (busy) return;
|
|
99
|
+
const animating = this.indicators.size > 0;
|
|
100
|
+
this.stopIndicators();
|
|
101
|
+
if (animating) this.requestRender?.();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Pi announced a call of the run going on: its arguments began to stream, or it
|
|
106
|
+
* began to run. Pi tells extensions before it makes the call's row. The set is
|
|
107
|
+
* emptied as each run starts, so it holds one run's calls at most.
|
|
108
|
+
*/
|
|
109
|
+
noteCall(toolCallId: string): void {
|
|
110
|
+
this.currentCalls.add(toolCallId);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Whether a call can be running: only one Pi announced in the run going on. A
|
|
115
|
+
* row restored from the session, one left without a result when its run ended,
|
|
116
|
+
* and one Pi made again for an old call when it rebuilt the transcript were
|
|
117
|
+
* never announced in this run; whatever they did is over. Until the host says
|
|
118
|
+
* whether the agent works, every call can run, as before.
|
|
119
|
+
*/
|
|
120
|
+
canRun(toolCallId: string): boolean {
|
|
121
|
+
return this.busy === undefined || (this.busy && this.currentCalls.has(toolCallId));
|
|
122
|
+
}
|
|
123
|
+
|
|
65
124
|
syncIndicator(toolCallId: string, running: boolean, invalidate: () => void): number {
|
|
66
125
|
if (!running) {
|
|
67
126
|
this.removeIndicator(toolCallId);
|
|
68
127
|
return 0;
|
|
69
128
|
}
|
|
70
|
-
this.
|
|
129
|
+
const indicator = this.indicators.get(toolCallId);
|
|
130
|
+
if (indicator) {
|
|
131
|
+
indicator.invalidate = invalidate;
|
|
132
|
+
indicator.drawnAt = this.clock();
|
|
133
|
+
} else {
|
|
134
|
+
this.indicators.set(toolCallId, { invalidate, drawnAt: this.clock() });
|
|
135
|
+
}
|
|
71
136
|
if (!this.indicatorTimer) {
|
|
72
|
-
this.indicatorTimer = setInterval(() =>
|
|
73
|
-
this.indicatorFrame++;
|
|
74
|
-
// Silent mode hides tool rows; repainting the screen for them would be pure cost.
|
|
75
|
-
if (isSilent()) return;
|
|
76
|
-
for (const requestRender of this.indicatorInvalidators.values()) requestRender();
|
|
77
|
-
}, INDICATOR_INTERVAL_MS);
|
|
137
|
+
this.indicatorTimer = setInterval(() => this.tick(), INDICATOR_INTERVAL_MS);
|
|
78
138
|
this.indicatorTimer.unref?.();
|
|
79
139
|
}
|
|
80
140
|
return this.indicatorFrame;
|
|
81
141
|
}
|
|
82
142
|
|
|
143
|
+
/**
|
|
144
|
+
* A running row that is out of sight but still counts, such as one folded into
|
|
145
|
+
* a group whose line pulses for it, keeps the animation going.
|
|
146
|
+
*/
|
|
147
|
+
keepAnimating(toolCallId: string): void {
|
|
148
|
+
const indicator = this.indicators.get(toolCallId);
|
|
149
|
+
if (indicator) indicator.drawnAt = this.clock();
|
|
150
|
+
}
|
|
151
|
+
|
|
83
152
|
syncExpansion(state: RowState, hostExpanded: boolean, name: string): boolean {
|
|
84
153
|
const initialized = this.initializeExpansion(state, name, hostExpanded);
|
|
85
154
|
if (!initialized && state.lastHostExpanded !== hostExpanded) {
|
|
@@ -98,15 +167,18 @@ export class ToolRuntime {
|
|
|
98
167
|
state.hasResult = available;
|
|
99
168
|
}
|
|
100
169
|
|
|
101
|
-
syncRow(
|
|
102
|
-
ctx: RenderContext,
|
|
103
|
-
running = ctx.executionStarted && ctx.state.endedAt === undefined,
|
|
104
|
-
finished = false,
|
|
105
|
-
): RowState {
|
|
170
|
+
syncRow(ctx: RenderContext, finished = false): RowState {
|
|
106
171
|
const state = ctx.state;
|
|
107
|
-
|
|
108
|
-
|
|
172
|
+
// Pi can rebuild the transcript mid-run, making new rows for old calls; a call
|
|
173
|
+
// keeps the run its first row was made in.
|
|
174
|
+
this.restoreTiming(state, ctx.toolCallId);
|
|
175
|
+
// A row is timed from when it runs, as Pi times its own: the model writing the
|
|
176
|
+
// arguments is not the tool's time. A restored row never ran in front of the
|
|
177
|
+
// reader, so it has no time to measure unless this process timed it before a
|
|
178
|
+
// /reload, and a call that never ran has none either.
|
|
179
|
+
const started = this.canRun(ctx.toolCallId) && ctx.executionStarted;
|
|
109
180
|
if (started && state.startedAt === undefined) state.startedAt = Date.now();
|
|
181
|
+
if (finished) state.finished = true;
|
|
110
182
|
if (finished && state.startedAt !== undefined && state.endedAt === undefined) state.endedAt = Date.now();
|
|
111
183
|
if (finished) this.removeIndicator(ctx.toolCallId);
|
|
112
184
|
this.persistTiming(state, ctx.toolCallId);
|
|
@@ -146,9 +218,28 @@ export class ToolRuntime {
|
|
|
146
218
|
};
|
|
147
219
|
}
|
|
148
220
|
|
|
221
|
+
private tick(): void {
|
|
222
|
+
this.indicatorFrame++;
|
|
223
|
+
const requestRender = this.requestRender;
|
|
224
|
+
if (requestRender) {
|
|
225
|
+
const oldest = this.clock() - UNDRAWN_ROW_MS;
|
|
226
|
+
for (const [toolCallId, indicator] of this.indicators) {
|
|
227
|
+
if (indicator.drawnAt < oldest) this.removeIndicator(toolCallId);
|
|
228
|
+
}
|
|
229
|
+
if (this.indicators.size === 0) return;
|
|
230
|
+
}
|
|
231
|
+
// Silent mode hides tool rows; repainting the screen for them would be pure cost.
|
|
232
|
+
if (isSilent()) return;
|
|
233
|
+
if (requestRender) {
|
|
234
|
+
requestRender();
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
for (const { invalidate } of this.indicators.values()) invalidate();
|
|
238
|
+
}
|
|
239
|
+
|
|
149
240
|
private removeIndicator(toolCallId: string): void {
|
|
150
|
-
this.
|
|
151
|
-
if (this.
|
|
241
|
+
this.indicators.delete(toolCallId);
|
|
242
|
+
if (this.indicators.size > 0 || !this.indicatorTimer) return;
|
|
152
243
|
clearInterval(this.indicatorTimer);
|
|
153
244
|
this.indicatorTimer = undefined;
|
|
154
245
|
this.indicatorFrame = 0;
|
|
@@ -157,7 +248,7 @@ export class ToolRuntime {
|
|
|
157
248
|
private stopIndicators(): void {
|
|
158
249
|
if (this.indicatorTimer) clearInterval(this.indicatorTimer);
|
|
159
250
|
this.indicatorTimer = undefined;
|
|
160
|
-
this.
|
|
251
|
+
this.indicators.clear();
|
|
161
252
|
this.indicatorFrame = 0;
|
|
162
253
|
}
|
|
163
254
|
|
|
@@ -177,12 +268,11 @@ export class ToolRuntime {
|
|
|
177
268
|
return true;
|
|
178
269
|
}
|
|
179
270
|
|
|
180
|
-
private restoreTiming(state: RowState, toolCallId: string):
|
|
271
|
+
private restoreTiming(state: RowState, toolCallId: string): void {
|
|
181
272
|
const timing = this.executionTimings.get(toolCallId);
|
|
182
|
-
if (!timing) return
|
|
273
|
+
if (!timing) return;
|
|
183
274
|
state.startedAt = timing.startedAt;
|
|
184
275
|
state.endedAt = timing.endedAt;
|
|
185
|
-
return timing.endedAt !== undefined;
|
|
186
276
|
}
|
|
187
277
|
|
|
188
278
|
private persistTiming(state: RowState, toolCallId: string): void {
|
|
@@ -37,6 +37,8 @@ export interface RowState {
|
|
|
37
37
|
hasResult?: boolean;
|
|
38
38
|
lastHostExpanded?: boolean;
|
|
39
39
|
configRevision?: object;
|
|
40
|
+
/** The row has its final result, whether or not it was timed. */
|
|
41
|
+
finished?: boolean;
|
|
40
42
|
startedAt?: number;
|
|
41
43
|
endedAt?: number;
|
|
42
44
|
resultLineSummary?: string;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
AgentToolResult,
|
|
3
3
|
ExtensionAPI,
|
|
4
|
+
ExtensionContext,
|
|
4
5
|
Theme,
|
|
5
6
|
ToolDefinition,
|
|
6
7
|
ToolRenderResultOptions,
|
|
@@ -37,6 +38,7 @@ import {
|
|
|
37
38
|
import {
|
|
38
39
|
CachedContainer,
|
|
39
40
|
claudeRows,
|
|
41
|
+
LiveCallContainer,
|
|
40
42
|
limitComponentLines,
|
|
41
43
|
prefixedLines,
|
|
42
44
|
prefixedText,
|
|
@@ -72,6 +74,7 @@ const PREVIEW_DIFF_CONTEXT_LINES = 1;
|
|
|
72
74
|
const CLAUDE_INDENT = " ";
|
|
73
75
|
|
|
74
76
|
const runtime = new ToolRuntime();
|
|
77
|
+
const RENDER_WIDGET_KEY = "compact-tools-render";
|
|
75
78
|
const registeredTools = new Set<CompactToolName>();
|
|
76
79
|
let registeredConfiguration: string | undefined;
|
|
77
80
|
|
|
@@ -95,13 +98,25 @@ function callStatus(ctx: RenderContext, state: RowState): RowStatus {
|
|
|
95
98
|
// write/edit arguments can stream for much longer than their eventual filesystem
|
|
96
99
|
// operation. Treat that active tool-call phase as running so every built-in row
|
|
97
100
|
// animates consistently instead of waiting for execute() to begin.
|
|
98
|
-
const active = ctx.executionStarted || !ctx.argsComplete;
|
|
99
|
-
return classifyCallStatus(ctx.isError, active, state.endedAt !== undefined);
|
|
101
|
+
const active = runtime.canRun(ctx.toolCallId) && (ctx.executionStarted || !ctx.argsComplete);
|
|
102
|
+
return classifyCallStatus(ctx.isError, active, state.finished === true || state.endedAt !== undefined);
|
|
100
103
|
}
|
|
101
104
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
+
/**
|
|
106
|
+
* The status dot, painted each time the row is drawn. Pi draws a call before its
|
|
107
|
+
* result, so a dot painted with the call would miss the result that arrived with
|
|
108
|
+
* it; reading the row's state at draw time sees it. A settled dot never changes.
|
|
109
|
+
*/
|
|
110
|
+
function liveIndicator(theme: Theme, ctx: RenderContext, state: RowState): () => string {
|
|
111
|
+
let settled: string | undefined;
|
|
112
|
+
return () => {
|
|
113
|
+
if (settled !== undefined) return settled;
|
|
114
|
+
const status = callStatus(ctx, state);
|
|
115
|
+
const indicator = paintIndicator(theme, status,
|
|
116
|
+
runtime.syncIndicator(ctx.toolCallId, status === "running", () => ctx.invalidate()));
|
|
117
|
+
if (status === "success" || status === "error") settled = indicator;
|
|
118
|
+
return indicator;
|
|
119
|
+
};
|
|
105
120
|
}
|
|
106
121
|
|
|
107
122
|
function formatDuration(state: RowState): string | undefined {
|
|
@@ -170,22 +185,24 @@ function hasNestedArguments(args: ToolArgs): boolean {
|
|
|
170
185
|
|
|
171
186
|
/** The call line: a status dot, the tool, and what it was asked, then any arguments the line leaves out. */
|
|
172
187
|
function renderRowCall(name: string, args: ToolArgs, theme: Theme, ctx: RenderContext): Component {
|
|
173
|
-
const state = runtime.syncRow(ctx
|
|
188
|
+
const state = runtime.syncRow(ctx);
|
|
174
189
|
runtime.syncExpansion(state, ctx.expanded, name);
|
|
175
190
|
const kind = rowKind(name);
|
|
176
|
-
const
|
|
191
|
+
const indicator = liveIndicator(theme, ctx, state);
|
|
192
|
+
// Painted once now as well, so a row that starts running starts the animation clock.
|
|
193
|
+
indicator();
|
|
177
194
|
if (isClaude()) {
|
|
178
195
|
// Claude Code's call line: `⦁ Bash(npm test)`, `⦁ Update(src/app.ts)`.
|
|
179
|
-
const { label, argument } = claudeTitle(name, args);
|
|
180
|
-
const title =
|
|
181
|
-
+ (argument ? theme.fg("toolOutput",
|
|
182
|
-
//
|
|
183
|
-
container
|
|
196
|
+
const { label, argument } = claudeTitle(name, args, state.expanded === true);
|
|
197
|
+
const title = ` ${theme.fg("toolTitle", theme.bold(label))}`
|
|
198
|
+
+ (argument ? styleMultiline(`(${argument})`, (line) => theme.fg("toolOutput", line)) : "");
|
|
199
|
+
// The title wraps rather than being cut to the row; only a very long command is shortened.
|
|
200
|
+
const container = new LiveCallContainer(indicator, (dot) => renderToolCall(dot + title, undefined, theme));
|
|
184
201
|
// The title already lists plain arguments; only nested ones need the full view.
|
|
185
202
|
if (kind === "custom" && state.expanded && hasNestedArguments(args)) container.addChild(renderArguments(args, theme));
|
|
186
203
|
return container;
|
|
187
204
|
}
|
|
188
|
-
const title =
|
|
205
|
+
const title = ` ${theme.fg("toolTitle", theme.bold(name))}`;
|
|
189
206
|
let details: string | undefined;
|
|
190
207
|
if (kind === "shell") {
|
|
191
208
|
const command = normalizeLineEndings(typeof args.command === "string" ? args.command : "");
|
|
@@ -196,7 +213,7 @@ function renderRowCall(name: string, args: ToolArgs, theme: Theme, ctx: RenderCo
|
|
|
196
213
|
const summary = kind === "file" ? getCallDetails(name, args) : summarizeCustomArguments(args);
|
|
197
214
|
details = summary ? styleMultiline(summary, (line) => theme.fg("toolOutput", line)) : undefined;
|
|
198
215
|
}
|
|
199
|
-
container
|
|
216
|
+
const container = new LiveCallContainer(indicator, (dot) => renderToolCall(dot + title, details, theme));
|
|
200
217
|
const extra = kind === "file" ? getArgumentDetails(name, args) : kind === "custom" && state.expanded ? args : {};
|
|
201
218
|
if (Object.keys(extra).length > 0) container.addChild(renderArguments(extra, theme));
|
|
202
219
|
return container;
|
|
@@ -313,8 +330,8 @@ function renderClaudeResult(
|
|
|
313
330
|
): Component {
|
|
314
331
|
const chrome = chromePainter(theme);
|
|
315
332
|
const container = new CachedContainer();
|
|
316
|
-
// One line under the call
|
|
317
|
-
const line = (text: string) => prefixedLines(text, chrome(" └ "), CLAUDE_INDENT,
|
|
333
|
+
// One line under the call, wrapped rather than cut to the row.
|
|
334
|
+
const line = (text: string) => prefixedLines(text, chrome(" └ "), CLAUDE_INDENT, false);
|
|
318
335
|
if (options.isPartial) {
|
|
319
336
|
container.addChild(line(chrome("Running…")));
|
|
320
337
|
return container;
|
|
@@ -357,7 +374,7 @@ function renderRowResult(
|
|
|
357
374
|
ctx: RenderContext,
|
|
358
375
|
author?: AuthorResultRenderer,
|
|
359
376
|
): Component {
|
|
360
|
-
const state = runtime.syncRow(ctx,
|
|
377
|
+
const state = runtime.syncRow(ctx, !options.isPartial);
|
|
361
378
|
runtime.syncExpansion(state, ctx.expanded, name);
|
|
362
379
|
if (canReuseResult(state, result, options, ctx)) return ctx.lastComponent;
|
|
363
380
|
const kind = rowKind(name);
|
|
@@ -474,6 +491,31 @@ function configure(pi: ExtensionAPI, cwd?: string, projectTrusted = false): void
|
|
|
474
491
|
registeredConfiguration = signature;
|
|
475
492
|
}
|
|
476
493
|
|
|
494
|
+
/** Note the calls an assistant message makes, as Pi reads them to make their rows. */
|
|
495
|
+
function noteCalls(message: unknown): void {
|
|
496
|
+
// Only a run's own updates announce calls; the check is cheap enough for every update.
|
|
497
|
+
const content = (message as { role?: string; content?: unknown }).role === "assistant"
|
|
498
|
+
? (message as { content?: unknown }).content
|
|
499
|
+
: undefined;
|
|
500
|
+
if (!Array.isArray(content)) return;
|
|
501
|
+
for (const block of content) {
|
|
502
|
+
if (block?.type === "toolCall" && typeof block.id === "string") runtime.noteCall(block.id);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/** Hand the animation Pi's frame request, reached through an invisible widget as the extension API allows. */
|
|
507
|
+
function bindRenderer(ctx: ExtensionContext): void {
|
|
508
|
+
ctx.ui.setWidget(RENDER_WIDGET_KEY, (tui) => {
|
|
509
|
+
runtime.bindRenderer(() => tui.requestRender());
|
|
510
|
+
return { render: () => [], invalidate() {} };
|
|
511
|
+
}, { placement: "belowEditor" });
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
function unbindRenderer(ctx: ExtensionContext | undefined): void {
|
|
515
|
+
runtime.bindRenderer(undefined);
|
|
516
|
+
ctx?.ui.setWidget(RENDER_WIDGET_KEY, undefined, { placement: "belowEditor" });
|
|
517
|
+
}
|
|
518
|
+
|
|
477
519
|
export default function compactTools(pi: ExtensionAPI): void {
|
|
478
520
|
// Pi caches this factory and re-invokes it with a fresh `pi` for every session
|
|
479
521
|
// replacement (/resume, /new, /fork) as well as /reload, each time starting from
|
|
@@ -491,13 +533,20 @@ export default function compactTools(pi: ExtensionAPI): void {
|
|
|
491
533
|
const customRowsAvailable = installToolRowPatch();
|
|
492
534
|
setRowResolver(resolveCustomRow);
|
|
493
535
|
let customRowsWarned = false;
|
|
536
|
+
let renderContext: ExtensionContext | undefined;
|
|
494
537
|
// Register once while the extension runtime is being built. In particular, this
|
|
495
538
|
// makes the overrides available before Pi restores the active tool set on /reload.
|
|
496
539
|
configure(pi, process.cwd());
|
|
497
540
|
pi.on("session_start", (event, ctx) => {
|
|
498
541
|
if (event.reason !== "reload") runtime.clearTimings();
|
|
542
|
+
// Rows restored from the session are drawn next; while idle none of them is running.
|
|
543
|
+
if (typeof ctx.isIdle === "function") runtime.setBusy(!ctx.isIdle());
|
|
499
544
|
configure(pi, ctx.cwd, ctx.isProjectTrusted());
|
|
545
|
+
unbindRenderer(renderContext);
|
|
546
|
+
renderContext = undefined;
|
|
500
547
|
if (ctx.mode === "tui" && runtime.config.style !== "off") {
|
|
548
|
+
renderContext = ctx;
|
|
549
|
+
bindRenderer(ctx);
|
|
501
550
|
// First, so its input listener sees relayout keys before the thinking controller consumes them.
|
|
502
551
|
viewport.bind(ctx);
|
|
503
552
|
progress.bind(ctx);
|
|
@@ -517,7 +566,15 @@ export default function compactTools(pi: ExtensionAPI): void {
|
|
|
517
566
|
groups.dispose();
|
|
518
567
|
}
|
|
519
568
|
});
|
|
520
|
-
pi.on("
|
|
569
|
+
pi.on("agent_start", () => runtime.setBusy(true));
|
|
570
|
+
pi.on("agent_end", () => runtime.setBusy(false));
|
|
571
|
+
// The calls of the run going on, read from each update as Pi reads them to make
|
|
572
|
+
// their rows, and told to extensions before Pi makes them.
|
|
573
|
+
pi.on("message_update", (event) => noteCalls(event.message));
|
|
574
|
+
pi.on("tool_execution_start", (event) => {
|
|
575
|
+
runtime.noteCall(event.toolCallId);
|
|
576
|
+
runtime.noteExecutionStart(event.toolCallId);
|
|
577
|
+
});
|
|
521
578
|
pi.on("tool_execution_end", (event) => runtime.noteExecutionEnd(event.toolCallId));
|
|
522
579
|
pi.on("session_shutdown", (event) => {
|
|
523
580
|
thinkingCycle.dispose();
|
|
@@ -525,6 +582,8 @@ export default function compactTools(pi: ExtensionAPI): void {
|
|
|
525
582
|
silent.dispose();
|
|
526
583
|
groups.dispose();
|
|
527
584
|
viewport.dispose();
|
|
585
|
+
unbindRenderer(renderContext);
|
|
586
|
+
renderContext = undefined;
|
|
528
587
|
runtime.reset(event.reason !== "reload");
|
|
529
588
|
});
|
|
530
589
|
}
|
package/package.json
CHANGED
package/release-notes.json
CHANGED
|
@@ -1,4 +1,14 @@
|
|
|
1
1
|
{
|
|
2
|
+
"0.12.0": [
|
|
3
|
+
"Running rows animate without being rebuilt: one repaint per frame, however many tools run",
|
|
4
|
+
"A tool's time counts from when it runs, not from when the model began writing its call",
|
|
5
|
+
"Rows restored from a session show no made-up time, and a call left without a result waits instead of pulsing on",
|
|
6
|
+
"Claude style: groups draw about ten times faster in long sessions, and wait along with their calls",
|
|
7
|
+
"Silent mode's activity line follows a theme switch"
|
|
8
|
+
],
|
|
9
|
+
"0.11.2": [
|
|
10
|
+
"Claude style no longer cuts commands to one row; long ones wrap, and only past two lines or 160 characters are shortened"
|
|
11
|
+
],
|
|
2
12
|
"0.11.1": [],
|
|
3
13
|
"0.11.0": [
|
|
4
14
|
"Claude style: set \"style\": \"claude\" to see tool calls the way Claude Code shows them",
|