pi-better-harness 0.22.0 → 0.24.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 +18 -6
- package/extensions/minimal-output/hook.ts +213 -12
- package/extensions/minimal-output/index.ts +11 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -68,14 +68,26 @@ The bundled harness provides **Tool output: Normal / Minimal** in
|
|
|
68
68
|
`/harness-settings`. `/tool-output minimal`, `/tool-output normal`, and
|
|
69
69
|
`/tool-output` (toggle) remain compatibility shortcuts to the same preference.
|
|
70
70
|
Minimal mode folds each tool call into a single-line header, including running
|
|
71
|
-
calls.
|
|
72
|
-
|
|
73
|
-
|
|
71
|
+
calls. Each row has a single tool-specific icon: accent-colored while running,
|
|
72
|
+
muted when completed, and error-colored when failed. Tool names stay muted;
|
|
73
|
+
inline command/path arguments use the theme's distinct dim tone. Running and
|
|
74
|
+
failed calls retain text labels so state never depends on color alone. Activity
|
|
75
|
+
stays quieter than conversation text. Tool rows are indented two columns past
|
|
76
|
+
the assistant text padding. Long
|
|
77
|
+
headers are truncated to the terminal width; result bodies,
|
|
74
78
|
images, boxes, and tool spacers are hidden. Built-in, extension, and MCP calls
|
|
75
79
|
are included. Execution, sandboxing, and agent-facing payloads are unchanged.
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
80
|
+
When a foreground run ends, each consecutive block of tool calls folds further
|
|
81
|
+
into one disclosure row showing the call count and any failures. Only the failure
|
|
82
|
+
count uses the error color; the disclosure and total stay muted. Assistant text
|
|
83
|
+
stays visible, and restored history uses the same folded view. In newer Pi
|
|
84
|
+
fullscreen mode, click a block disclosure to reveal its call rows, then click a
|
|
85
|
+
call to expand its original details. Hover highlights compact call and disclosure
|
|
86
|
+
rows using the selection background (reverse video for themes without one), and
|
|
87
|
+
tool icons sit centered in a three-column gutter. Click the disclosure again to refold the
|
|
88
|
+
block; native expanded call/result clicks collapse individual details. Regular
|
|
89
|
+
terminals keep mouse input for terminal selection and scrollback; Ctrl+O expands
|
|
90
|
+
all original tool details in both modes and folds them again on the next toggle.
|
|
79
91
|
`/tool-output normal` restores ordinary rendering. Error result bodies are also
|
|
80
92
|
hidden in minimal mode and remain available when expanded.
|
|
81
93
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { truncateToWidth, type Component } from "@earendil-works/pi-tui";
|
|
1
|
+
import { truncateToWidth, visibleWidth, type Component } from "@earendil-works/pi-tui";
|
|
2
2
|
import { readFileSync, realpathSync } from "node:fs";
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
4
|
import { dirname, join } from "node:path";
|
|
@@ -6,8 +6,14 @@ import { pathToFileURL } from "node:url";
|
|
|
6
6
|
import { stripVTControlCharacters } from "node:util";
|
|
7
7
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
8
8
|
|
|
9
|
+
interface ToolUI {
|
|
10
|
+
requestRender(): void;
|
|
11
|
+
children?: TranscriptComponent[];
|
|
12
|
+
handleViewportInput?(data: string): unknown;
|
|
13
|
+
}
|
|
9
14
|
interface ToolComponent {
|
|
10
15
|
toolName: string;
|
|
16
|
+
toolCallId: string;
|
|
11
17
|
args: unknown;
|
|
12
18
|
callRendererComponent?: Component;
|
|
13
19
|
expanded: boolean;
|
|
@@ -15,14 +21,21 @@ interface ToolComponent {
|
|
|
15
21
|
result?: { isError?: boolean };
|
|
16
22
|
showImages: boolean;
|
|
17
23
|
resultRendererComponent?: unknown;
|
|
18
|
-
ui:
|
|
24
|
+
ui: ToolUI;
|
|
19
25
|
invalidate(): void;
|
|
20
26
|
setExpanded(expanded: boolean): void;
|
|
21
27
|
render(width: number): string[];
|
|
22
28
|
}
|
|
23
29
|
|
|
30
|
+
interface TranscriptComponent extends Component {
|
|
31
|
+
children?: TranscriptComponent[];
|
|
32
|
+
outputPad?: number;
|
|
33
|
+
lastMessage?: { content: { type: string; text?: string; thinking?: string }[]; stopReason?: string };
|
|
34
|
+
}
|
|
35
|
+
interface ToolGroup { members: ToolComponent[]; open: boolean; expanded: number }
|
|
36
|
+
|
|
24
37
|
type Method = (this: ToolComponent, ...args: unknown[]) => unknown;
|
|
25
|
-
interface ToolMouseEvent { type: string; button: string; y: number; shift?: boolean; alt?: boolean; ctrl?: boolean }
|
|
38
|
+
interface ToolMouseEvent { type: string; button: string; x: number; y: number; screenX: number; screenY: number; width: number; shift?: boolean; alt?: boolean; ctrl?: boolean }
|
|
26
39
|
type MouseHandler = (this: ToolComponent, event: ToolMouseEvent) => unknown;
|
|
27
40
|
interface ToolPrototype {
|
|
28
41
|
handleMouse?: MouseHandler;
|
|
@@ -39,16 +52,30 @@ interface HookState {
|
|
|
39
52
|
owners: number;
|
|
40
53
|
redraw(collapse?: boolean): void;
|
|
41
54
|
restore(): void;
|
|
55
|
+
completeRun(): void;
|
|
56
|
+
restoreCompletedCalls(ids: Iterable<string>): void;
|
|
57
|
+
handleTerminalInput(data: string): void;
|
|
42
58
|
}
|
|
43
59
|
export interface MinimalOutputHook {
|
|
44
60
|
setEnabled(enabled: boolean): void;
|
|
61
|
+
completeRun(): void;
|
|
62
|
+
restoreCompletedCalls(ids: Iterable<string>): void;
|
|
45
63
|
dispose(): void;
|
|
46
64
|
}
|
|
47
65
|
|
|
48
66
|
const HOOK = Symbol.for("pi-better-harness.minimal-output-hook");
|
|
49
67
|
const emptyResult = () => ({ render: () => [] as string[], invalidate() {} });
|
|
68
|
+
const TOOL_ICONS: Record<string, string> = {
|
|
69
|
+
bash: "\u2318",
|
|
70
|
+
read: "\u25a4",
|
|
71
|
+
write: "\u2710",
|
|
72
|
+
edit: "\u270e",
|
|
73
|
+
grep: "\u2315",
|
|
74
|
+
find: "\u2316",
|
|
75
|
+
ls: "\u2261",
|
|
76
|
+
};
|
|
50
77
|
|
|
51
|
-
function compactCall(component: ToolComponent, width: number, theme?: Theme): string[] {
|
|
78
|
+
function compactCall(component: ToolComponent, width: number, theme?: Theme, indent = 3): string[] {
|
|
52
79
|
if (width <= 0) return [];
|
|
53
80
|
// Render the call alone at a wider width so terminal wrapping becomes truncation.
|
|
54
81
|
const header = component.callRendererComponent?.render(Math.max(4096, width + 1))
|
|
@@ -67,13 +94,35 @@ function compactCall(component: ToolComponent, width: number, theme?: Theme): st
|
|
|
67
94
|
: text === component.toolName ? "" : text;
|
|
68
95
|
const label = component.toolName === "bash" ? "Shell"
|
|
69
96
|
: component.toolName.replace(/_/g, " ").replace(/^./, letter => letter.toUpperCase());
|
|
70
|
-
const marker = failed ? "!" : running ? "~" : ">";
|
|
71
97
|
const suffix = failed ? " (failed)" : running ? " (running)" : "";
|
|
72
|
-
const
|
|
98
|
+
const icon = TOOL_ICONS[component.toolName] ?? "\u25c7";
|
|
99
|
+
const title = ` ${icon} ${label}`;
|
|
73
100
|
const styled = theme
|
|
74
|
-
?
|
|
101
|
+
? ` ${theme.fg(failed ? "error" : running ? "accent" : "muted", icon)} ${theme.fg("muted", label)}${detail ? ` ${theme.fg("dim", detail)}` : ""}${theme.fg(failed ? "error" : "dim", suffix)}`
|
|
75
102
|
: `${title}${detail ? ` ${detail}` : ""}${suffix}`;
|
|
76
|
-
return [truncateToWidth(
|
|
103
|
+
return [truncateToWidth(`${" ".repeat(Math.min(indent, Math.max(0, width - 1)))}${styled}`, width)];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function toolParent(component: ToolComponent): TranscriptComponent | undefined {
|
|
107
|
+
const visit = (children: TranscriptComponent[]): TranscriptComponent | undefined => {
|
|
108
|
+
for (const child of children) {
|
|
109
|
+
if (child.children?.includes(component)) return child;
|
|
110
|
+
if (child.children) {
|
|
111
|
+
const found = visit(child.children);
|
|
112
|
+
if (found) return found;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return undefined;
|
|
116
|
+
};
|
|
117
|
+
return visit(component.ui.children ?? []);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function paddingAnchor(component: ToolComponent, parent?: TranscriptComponent): TranscriptComponent | undefined {
|
|
121
|
+
const siblings = parent?.children ?? [];
|
|
122
|
+
for (let index = siblings.indexOf(component) - 1; index >= 0; index--) {
|
|
123
|
+
if (typeof siblings[index].outputPad === "number") return siblings[index];
|
|
124
|
+
}
|
|
125
|
+
return undefined;
|
|
77
126
|
}
|
|
78
127
|
|
|
79
128
|
function hostRequire(): NodeRequire {
|
|
@@ -136,15 +185,93 @@ export function installMinimalOutputHook(prototype: ToolPrototype, getTheme?: ()
|
|
|
136
185
|
updateDisplay: prototype.updateDisplay,
|
|
137
186
|
getResultRenderer: prototype.getResultRenderer,
|
|
138
187
|
getTextOutput: prototype.getTextOutput,
|
|
188
|
+
setExpanded: prototype.setExpanded,
|
|
139
189
|
render: prototype.render,
|
|
140
190
|
...(typeof prototype.handleMouse === "function" ? { handleMouse: prototype.handleMouse } : {}),
|
|
141
191
|
};
|
|
192
|
+
let mouseHandling = false;
|
|
193
|
+
const inputWrappers = new Map<ToolUI, { original: NonNullable<ToolUI["handleViewportInput"]>; wrapper: NonNullable<ToolUI["handleViewportInput"]> }>();
|
|
194
|
+
let hovered: { component: ToolComponent; row: number; left: number; top: number; width: number } | undefined;
|
|
195
|
+
const clearHover = () => {
|
|
196
|
+
if (!hovered) return;
|
|
197
|
+
const component = hovered.component;
|
|
198
|
+
hovered = undefined;
|
|
199
|
+
component.ui.requestRender();
|
|
200
|
+
};
|
|
201
|
+
const highlight = (component: ToolComponent, lines: string[], width: number): string[] => {
|
|
202
|
+
if (hovered?.component !== component) return lines;
|
|
203
|
+
const theme = getTheme?.();
|
|
204
|
+
if (width !== hovered.width || hovered.row >= lines.length) { clearHover(); return lines; }
|
|
205
|
+
if (theme) {
|
|
206
|
+
const row = lines[hovered.row] + " ".repeat(Math.max(0, width - visibleWidth(lines[hovered.row])));
|
|
207
|
+
const shaded = theme.bg("selectedBg", row);
|
|
208
|
+
lines[hovered.row] = shaded.startsWith("\x1b[49m") ? theme.inverse(row) : shaded;
|
|
209
|
+
}
|
|
210
|
+
return lines;
|
|
211
|
+
};
|
|
142
212
|
const seen = new WeakSet<ToolComponent>();
|
|
143
213
|
const components = new Set<WeakRef<ToolComponent>>();
|
|
214
|
+
let completed = new Map<string, object>();
|
|
215
|
+
let groups = new WeakMap<ToolComponent, ToolGroup>();
|
|
216
|
+
let layouts = new WeakMap<ToolComponent, { parent: TranscriptComponent; anchor?: TranscriptComponent }>();
|
|
217
|
+
const layoutFor = (component: ToolComponent) => {
|
|
218
|
+
const cached = layouts.get(component);
|
|
219
|
+
if (cached) return cached;
|
|
220
|
+
const parent = toolParent(component);
|
|
221
|
+
if (!parent) return undefined;
|
|
222
|
+
const layout = { parent, anchor: paddingAnchor(component, parent) };
|
|
223
|
+
layouts.set(component, layout);
|
|
224
|
+
return layout;
|
|
225
|
+
};
|
|
226
|
+
const groupFor = (component: ToolComponent, parent?: TranscriptComponent): ToolGroup | undefined => {
|
|
227
|
+
const cached = groups.get(component);
|
|
228
|
+
if (cached) return cached;
|
|
229
|
+
if (!completed.has(component.toolCallId) || component.isPartial || !parent?.children) return undefined;
|
|
230
|
+
let members: ToolComponent[] = [];
|
|
231
|
+
const save = () => {
|
|
232
|
+
if (!members.length) return;
|
|
233
|
+
const group = { members, open: false, expanded: members.filter(member => member.expanded).length };
|
|
234
|
+
for (const member of members) groups.set(member, group);
|
|
235
|
+
members = [];
|
|
236
|
+
};
|
|
237
|
+
for (const child of parent.children) {
|
|
238
|
+
const tool = child as ToolComponent;
|
|
239
|
+
if (seen.has(tool) && completed.has(tool.toolCallId) && !tool.isPartial && !groups.has(tool)) {
|
|
240
|
+
if (members.length && completed.get(members[0].toolCallId) !== completed.get(tool.toolCallId)) save();
|
|
241
|
+
members.push(tool);
|
|
242
|
+
} else if (typeof child.outputPad === "number" && child.lastMessage
|
|
243
|
+
&& !["length", "error", "aborted"].includes(child.lastMessage.stopReason ?? "")
|
|
244
|
+
&& !child.lastMessage.content.some(content => content.text?.trim() || content.thinking?.trim())) {
|
|
245
|
+
// Tool-only assistant messages occupy no transcript rows and do not split a visual block.
|
|
246
|
+
continue;
|
|
247
|
+
} else save();
|
|
248
|
+
}
|
|
249
|
+
save();
|
|
250
|
+
return groups.get(component);
|
|
251
|
+
};
|
|
144
252
|
state = {
|
|
145
253
|
enabled: false,
|
|
146
254
|
owners: 0,
|
|
255
|
+
completeRun() {
|
|
256
|
+
const run = {};
|
|
257
|
+
for (const reference of components) {
|
|
258
|
+
const component = reference.deref();
|
|
259
|
+
if (component?.result && !component.isPartial && !completed.has(component.toolCallId)) {
|
|
260
|
+
completed.set(component.toolCallId, run);
|
|
261
|
+
if (this.enabled) component.setExpanded(false);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
this.redraw();
|
|
265
|
+
},
|
|
266
|
+
restoreCompletedCalls(ids) {
|
|
267
|
+
const history = {};
|
|
268
|
+
completed = new Map(Array.from(ids, id => [id, history]));
|
|
269
|
+
groups = new WeakMap();
|
|
270
|
+
layouts = new WeakMap();
|
|
271
|
+
this.redraw();
|
|
272
|
+
},
|
|
147
273
|
redraw(collapse = false) {
|
|
274
|
+
clearHover();
|
|
148
275
|
for (const reference of components) {
|
|
149
276
|
const component = reference.deref();
|
|
150
277
|
if (!component) { components.delete(reference); continue; }
|
|
@@ -154,7 +281,12 @@ export function installMinimalOutputHook(prototype: ToolPrototype, getTheme?: ()
|
|
|
154
281
|
}
|
|
155
282
|
},
|
|
156
283
|
restore() {
|
|
157
|
-
|
|
284
|
+
clearHover();
|
|
285
|
+
for (const [ui, { original, wrapper }] of inputWrappers) {
|
|
286
|
+
if (ui.handleViewportInput === wrapper) ui.handleViewportInput = original;
|
|
287
|
+
}
|
|
288
|
+
inputWrappers.clear();
|
|
289
|
+
for (const name of ["updateDisplay", "getResultRenderer", "getTextOutput", "setExpanded", "render"] as const) {
|
|
158
290
|
// Do not remove another extension's subsequently installed wrapper.
|
|
159
291
|
if (prototype[name] === wrappers[name]) Object.assign(prototype, { [name]: originals[name] });
|
|
160
292
|
}
|
|
@@ -162,10 +294,30 @@ export function installMinimalOutputHook(prototype: ToolPrototype, getTheme?: ()
|
|
|
162
294
|
delete prototype[HOOK];
|
|
163
295
|
components.clear();
|
|
164
296
|
},
|
|
297
|
+
handleTerminalInput(data) {
|
|
298
|
+
if (!hovered) return;
|
|
299
|
+
// Pi routes moves only to the new target; observe input to clear a row on exit.
|
|
300
|
+
const motion = /^\x1b\[<35;(\d+);(\d+)M$/.exec(data);
|
|
301
|
+
if (motion) {
|
|
302
|
+
const x = Number(motion[1]) - 1;
|
|
303
|
+
const y = Number(motion[2]) - 1;
|
|
304
|
+
if (y === hovered.top && x >= hovered.left && x < hovered.left + hovered.width) return;
|
|
305
|
+
}
|
|
306
|
+
clearHover();
|
|
307
|
+
},
|
|
165
308
|
};
|
|
166
309
|
const current = state;
|
|
167
310
|
const wrappers = {
|
|
168
311
|
updateDisplay(this: ToolComponent, ...args: unknown[]) {
|
|
312
|
+
if (typeof this.ui.handleViewportInput === "function" && !inputWrappers.has(this.ui)) {
|
|
313
|
+
const original = this.ui.handleViewportInput;
|
|
314
|
+
const wrapper = function(this: ToolUI, data: string) {
|
|
315
|
+
current.handleTerminalInput(data);
|
|
316
|
+
return original.call(this, data);
|
|
317
|
+
};
|
|
318
|
+
inputWrappers.set(this.ui, { original, wrapper });
|
|
319
|
+
this.ui.handleViewportInput = wrapper;
|
|
320
|
+
}
|
|
169
321
|
if (!seen.has(this)) { seen.add(this); components.add(new WeakRef(this)); }
|
|
170
322
|
if (!current.enabled || this.expanded) return originals.updateDisplay.apply(this, args);
|
|
171
323
|
const showImages = this.showImages;
|
|
@@ -179,6 +331,15 @@ export function installMinimalOutputHook(prototype: ToolPrototype, getTheme?: ()
|
|
|
179
331
|
this.resultRendererComponent = lastRenderer;
|
|
180
332
|
}
|
|
181
333
|
},
|
|
334
|
+
setExpanded(this: ToolComponent, ...args: unknown[]) {
|
|
335
|
+
clearHover();
|
|
336
|
+
const group = groups.get(this);
|
|
337
|
+
const wasExpanded = this.expanded;
|
|
338
|
+
if (args[0] === false && !mouseHandling && group) group.open = false;
|
|
339
|
+
const result = originals.setExpanded.apply(this, args);
|
|
340
|
+
if (group) group.expanded += Number(this.expanded) - Number(wasExpanded);
|
|
341
|
+
return result;
|
|
342
|
+
},
|
|
182
343
|
getResultRenderer(this: ToolComponent, ...args: unknown[]) {
|
|
183
344
|
return current.enabled && !this.expanded ? emptyResult : originals.getResultRenderer.apply(this, args);
|
|
184
345
|
},
|
|
@@ -186,17 +347,55 @@ export function installMinimalOutputHook(prototype: ToolPrototype, getTheme?: ()
|
|
|
186
347
|
return current.enabled && !this.expanded ? "" : originals.getTextOutput.apply(this, args);
|
|
187
348
|
},
|
|
188
349
|
render(this: ToolComponent, width: number): string[] {
|
|
189
|
-
|
|
350
|
+
if (!current.enabled || this.expanded) return originals.render.call(this, width);
|
|
351
|
+
const layout = layoutFor(this);
|
|
352
|
+
const padding = layout?.anchor?.outputPad ?? 1;
|
|
353
|
+
const indent = Number.isFinite(padding) ? Math.max(0, Math.floor(padding)) + 2 : 3;
|
|
354
|
+
const group = groupFor(this, layout?.parent);
|
|
355
|
+
if (!group) return highlight(this, compactCall(this, width, getTheme?.(), indent), width);
|
|
356
|
+
const open = group.open || group.expanded > 0;
|
|
357
|
+
const first = group.members[0] === this;
|
|
358
|
+
const lines: string[] = [];
|
|
359
|
+
if (first && width > 0) {
|
|
360
|
+
const failed = group.members.filter(member => member.result?.isError).length;
|
|
361
|
+
const text = `${open ? "\u25be" : "\u25b8"} ${group.members.length} tool call${group.members.length === 1 ? "" : "s"}`;
|
|
362
|
+
const failure = failed ? `${failed} failed` : "";
|
|
363
|
+
const theme = getTheme?.();
|
|
364
|
+
const styled = theme
|
|
365
|
+
? `${theme.fg("muted", text)}${failed ? `${theme.fg("dim", " \u00b7 ")}${theme.fg("error", failure)}` : ""}`
|
|
366
|
+
: `${text}${failed ? ` \u00b7 ${failure}` : ""}`;
|
|
367
|
+
lines.push(truncateToWidth(`${" ".repeat(Math.min(indent, Math.max(0, width - 1)))}${styled}`, width));
|
|
368
|
+
}
|
|
369
|
+
if (open) lines.push(...compactCall(this, width, getTheme?.(), indent + 2));
|
|
370
|
+
return highlight(this, lines, width);
|
|
190
371
|
},
|
|
191
372
|
};
|
|
192
373
|
const mouseWrapper: MouseHandler = function(event) {
|
|
193
374
|
if (current.enabled && !this.expanded) {
|
|
194
|
-
if (event.type
|
|
375
|
+
if (event.type === "move" && event.button === "none") {
|
|
376
|
+
const lines = this.render(event.width);
|
|
377
|
+
if (event.y < 0 || event.y >= lines.length) { clearHover(); return undefined; }
|
|
378
|
+
if (hovered?.component === this && hovered.row === event.y) return { handled: true, render: false };
|
|
379
|
+
clearHover();
|
|
380
|
+
hovered = { component: this, row: event.y, left: event.screenX - event.x, top: event.screenY, width: event.width };
|
|
381
|
+
return { handled: true, render: true };
|
|
382
|
+
}
|
|
383
|
+
if (event.type !== "click" || event.button !== "left" || event.shift || event.alt || event.ctrl) return undefined;
|
|
384
|
+
const group = groupFor(this, layoutFor(this)?.parent);
|
|
385
|
+
if (group?.members[0] === this && event.y === 0) {
|
|
386
|
+
group.open = !(group.open || group.expanded > 0);
|
|
387
|
+
if (!group.open) for (const member of group.members) member.setExpanded(false);
|
|
388
|
+
this.ui.requestRender();
|
|
389
|
+
return { handled: true, render: true };
|
|
390
|
+
}
|
|
391
|
+
if (event.y !== (group?.members[0] === this ? 1 : 0)) return undefined;
|
|
195
392
|
this.setExpanded(true);
|
|
196
393
|
this.ui.requestRender();
|
|
197
394
|
return { handled: true, render: true };
|
|
198
395
|
}
|
|
199
|
-
|
|
396
|
+
mouseHandling = true;
|
|
397
|
+
try { return originals.handleMouse?.call(this, event); }
|
|
398
|
+
finally { mouseHandling = false; }
|
|
200
399
|
};
|
|
201
400
|
if (originals.handleMouse) prototype.handleMouse = mouseWrapper;
|
|
202
401
|
Object.assign(prototype, wrappers);
|
|
@@ -206,6 +405,8 @@ export function installMinimalOutputHook(prototype: ToolPrototype, getTheme?: ()
|
|
|
206
405
|
current.owners += 1;
|
|
207
406
|
let disposed = false;
|
|
208
407
|
return {
|
|
408
|
+
completeRun() { if (!disposed) current.completeRun(); },
|
|
409
|
+
restoreCompletedCalls(ids) { if (!disposed) current.restoreCompletedCalls(ids); },
|
|
209
410
|
setEnabled(enabled) {
|
|
210
411
|
if (disposed) return;
|
|
211
412
|
current.enabled = enabled;
|
|
@@ -7,6 +7,7 @@ const ENTRY = "pi-better-harness-tool-output";
|
|
|
7
7
|
export default function minimalOutputExtension(pi: ExtensionAPI): void {
|
|
8
8
|
let hook: MinimalOutputHook | undefined;
|
|
9
9
|
let enabled = false;
|
|
10
|
+
let running = false;
|
|
10
11
|
|
|
11
12
|
function status(ctx: ExtensionContext): void {
|
|
12
13
|
ctx.ui.setStatus(ENTRY, enabled ? "tools: minimal" : undefined);
|
|
@@ -44,6 +45,8 @@ export default function minimalOutputExtension(pi: ExtensionAPI): void {
|
|
|
44
45
|
}
|
|
45
46
|
// Observe transcript components from startup, so the toggle also redraws existing rows.
|
|
46
47
|
if (await ensureHook(ctx)) {
|
|
48
|
+
hook!.restoreCompletedCalls(ctx.sessionManager.getBranch().flatMap(entry =>
|
|
49
|
+
entry.type === "message" && entry.message.role === "toolResult" ? [entry.message.toolCallId] : []));
|
|
47
50
|
if (enabled) collapseTools(ctx);
|
|
48
51
|
hook!.setEnabled(enabled);
|
|
49
52
|
} else enabled = false;
|
|
@@ -59,6 +62,7 @@ export default function minimalOutputExtension(pi: ExtensionAPI): void {
|
|
|
59
62
|
enabled = data.enabled;
|
|
60
63
|
if (enabled) collapseTools(ctx);
|
|
61
64
|
hook!.setEnabled(enabled);
|
|
65
|
+
if (enabled && !running) hook!.completeRun();
|
|
62
66
|
status(ctx);
|
|
63
67
|
}
|
|
64
68
|
|
|
@@ -73,6 +77,12 @@ export default function minimalOutputExtension(pi: ExtensionAPI): void {
|
|
|
73
77
|
register();
|
|
74
78
|
pi.on("session_start", async (_event, ctx) => { subscribe(); await restore(ctx); register(); });
|
|
75
79
|
pi.on("session_tree", async (_event, ctx) => { await restore(ctx); });
|
|
80
|
+
pi.on("agent_start", () => { running = true; });
|
|
81
|
+
pi.on("agent_settled", (_event, ctx) => {
|
|
82
|
+
running = false;
|
|
83
|
+
if (enabled) ctx.ui.setToolsExpanded(false);
|
|
84
|
+
hook?.completeRun();
|
|
85
|
+
});
|
|
76
86
|
|
|
77
87
|
pi.on("session_shutdown", () => {
|
|
78
88
|
stopSettings?.();
|
|
@@ -80,6 +90,7 @@ export default function minimalOutputExtension(pi: ExtensionAPI): void {
|
|
|
80
90
|
hook?.dispose();
|
|
81
91
|
hook = undefined;
|
|
82
92
|
enabled = false;
|
|
93
|
+
running = false;
|
|
83
94
|
});
|
|
84
95
|
|
|
85
96
|
pi.registerCommand("tool-output", {
|
package/package.json
CHANGED