pi-better-harness 0.15.1 → 0.15.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 +5 -4
- package/extensions/minimal-output/hook.ts +9 -205
- package/extensions/minimal-output/index.ts +5 -12
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -63,11 +63,12 @@ need interactive confirmation. Full policy: [pi-better-sandbox](https://github.c
|
|
|
63
63
|
## Minimal Tool Output
|
|
64
64
|
|
|
65
65
|
The bundled harness also provides `/tool-output minimal`, `/tool-output normal`,
|
|
66
|
-
and `/tool-output` (toggle). Minimal mode
|
|
67
|
-
|
|
66
|
+
and `/tool-output` (toggle). Minimal mode hides tool blocks entirely, including
|
|
67
|
+
running calls, headers, results, images, summary rows, and tool spacers. Built-in,
|
|
68
68
|
extension, and MCP calls are included. Execution, sandboxing, and agent-facing
|
|
69
|
-
payloads are unchanged. Ctrl+O
|
|
70
|
-
|
|
69
|
+
payloads are unchanged. Ctrl+O reveals the original tool blocks; Ctrl+O again
|
|
70
|
+
hides them. `/tool-output normal` restores ordinary rendering. Error results
|
|
71
|
+
are also hidden in minimal mode and remain available when revealed.
|
|
71
72
|
|
|
72
73
|
Normal mode is the default. The preference is saved in the current session,
|
|
73
74
|
including resume/reload. This is a version-sensitive internal TUI adapter,
|
|
@@ -7,10 +7,7 @@ interface ToolComponent {
|
|
|
7
7
|
expanded: boolean;
|
|
8
8
|
showImages: boolean;
|
|
9
9
|
resultRendererComponent?: unknown;
|
|
10
|
-
|
|
11
|
-
toolCallId?: string;
|
|
12
|
-
result?: { isError?: boolean };
|
|
13
|
-
ui: { requestRender(): void; children?: ChatContainer[] };
|
|
10
|
+
ui: { requestRender(): void };
|
|
14
11
|
invalidate(): void;
|
|
15
12
|
setExpanded(expanded: boolean): void;
|
|
16
13
|
render(width: number): string[];
|
|
@@ -23,43 +20,21 @@ interface ToolPrototype {
|
|
|
23
20
|
getTextOutput: Method;
|
|
24
21
|
setExpanded: Method;
|
|
25
22
|
invalidate: Method;
|
|
23
|
+
render: ToolComponent["render"];
|
|
26
24
|
[HOOK]?: HookState;
|
|
27
25
|
}
|
|
28
|
-
interface ChatContainer {
|
|
29
|
-
children: unknown[];
|
|
30
|
-
render: (width: number) => string[];
|
|
31
|
-
mouseLayout?: { width: number; children: Array<{ component: unknown; height: number }> };
|
|
32
|
-
[CHAT]?: { original: (width: number) => string[]; wrapper: (width: number) => string[] };
|
|
33
|
-
}
|
|
34
|
-
interface ContainerPrototype {
|
|
35
|
-
addChild: (child: unknown, ...rest: unknown[]) => unknown;
|
|
36
|
-
render: (width: number) => string[];
|
|
37
|
-
[ADD]?: { original: ContainerPrototype["addChild"]; wrapper: ContainerPrototype["addChild"] };
|
|
38
|
-
}
|
|
39
26
|
interface HookState {
|
|
40
27
|
enabled: boolean;
|
|
41
28
|
owners: number;
|
|
42
|
-
foldsTurns: boolean;
|
|
43
|
-
closeRuns(): void;
|
|
44
29
|
redraw(collapse?: boolean): void;
|
|
45
30
|
restore(): void;
|
|
46
|
-
installContainer(prototype: ContainerPrototype): void;
|
|
47
31
|
}
|
|
48
32
|
export interface MinimalOutputHook {
|
|
49
33
|
setEnabled(enabled: boolean): void;
|
|
50
34
|
dispose(): void;
|
|
51
|
-
readonly foldsTurns: boolean;
|
|
52
|
-
}
|
|
53
|
-
interface FoldSummary {
|
|
54
|
-
render(width: number): string[];
|
|
55
|
-
invalidate(): void;
|
|
56
|
-
handleMouse(event: { type?: string; button?: string }): { handled: true } | undefined;
|
|
57
|
-
setTools(tools: ToolComponent[]): void;
|
|
58
35
|
}
|
|
59
36
|
|
|
60
37
|
const HOOK = Symbol.for("pi-better-harness.minimal-output-hook");
|
|
61
|
-
const CHAT = Symbol.for("pi-better-harness.minimal-output-chat");
|
|
62
|
-
const ADD = Symbol.for("pi-better-harness.minimal-output-add-child");
|
|
63
38
|
const emptyResult = () => ({ render: () => [] as string[], invalidate() {} });
|
|
64
39
|
|
|
65
40
|
function hostRequire(): NodeRequire {
|
|
@@ -96,25 +71,6 @@ export async function loadToolPrototype(): Promise<ToolPrototype> {
|
|
|
96
71
|
return module.ToolExecutionComponent.prototype;
|
|
97
72
|
}
|
|
98
73
|
|
|
99
|
-
/** Use the actual tool class's Container base, including in bundled hosts. */
|
|
100
|
-
export async function loadContainerPrototype(): Promise<ContainerPrototype> {
|
|
101
|
-
const prototype = Object.getPrototypeOf(await loadToolPrototype());
|
|
102
|
-
if (typeof prototype?.addChild !== "function" || typeof prototype?.render !== "function") {
|
|
103
|
-
throw new Error("Pi's chat container API is incompatible: missing Container.addChild. Turn folding is unavailable.");
|
|
104
|
-
}
|
|
105
|
-
return prototype;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
export async function loadMutedText(): Promise<(text: string) => string> {
|
|
109
|
-
const entry = sdkEntry(hostRequire());
|
|
110
|
-
// The bundled SDK does not export its live theme singleton.
|
|
111
|
-
if (bundledHostEntry(entry)) return (text) => text;
|
|
112
|
-
const theme = await import(pathToFileURL(join(dirname(entry), "modes/interactive/theme/theme.js")).href);
|
|
113
|
-
return (text) => {
|
|
114
|
-
try { return theme.theme.fg("muted", text); } catch { return text; }
|
|
115
|
-
};
|
|
116
|
-
}
|
|
117
|
-
|
|
118
74
|
// Pi's ESM-only export cannot be resolved with require.resolve on older SDKs.
|
|
119
75
|
function sdkEntry(require: ReturnType<typeof createRequire>): string {
|
|
120
76
|
for (const directory of require.resolve.paths("@earendil-works/pi-coding-agent") ?? []) {
|
|
@@ -128,38 +84,9 @@ function sdkEntry(require: ReturnType<typeof createRequire>): string {
|
|
|
128
84
|
throw new Error("Could not locate the running Pi SDK. Normal output remains enabled.");
|
|
129
85
|
}
|
|
130
86
|
|
|
131
|
-
function hasModelText(component: unknown): boolean {
|
|
132
|
-
const content = (component as { lastMessage?: { content?: unknown } } | null)?.lastMessage?.content;
|
|
133
|
-
if (!Array.isArray(content)) return false;
|
|
134
|
-
return content.some((block) => block?.type === "text" && typeof block.text === "string" && block.text.trim().length > 0);
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
function formatRun(tools: ToolComponent[], open: boolean): string {
|
|
138
|
-
const counts = new Map<string, number>();
|
|
139
|
-
let errors = 0;
|
|
140
|
-
for (const tool of tools) {
|
|
141
|
-
const name = tool.toolName || "tool";
|
|
142
|
-
counts.set(name, (counts.get(name) ?? 0) + 1);
|
|
143
|
-
if (tool.result?.isError) errors += 1;
|
|
144
|
-
}
|
|
145
|
-
const names = [...counts].map(([name, count]) => count > 1 ? `${name} ×${count}` : name);
|
|
146
|
-
const noun = tools.length === 1 ? "tool" : "tools";
|
|
147
|
-
const error = errors === 0 ? "" : errors === 1 ? " · 1 error" : ` · ${errors} errors`;
|
|
148
|
-
return `${open ? "▾" : "▸"} ${tools.length} ${noun}${error} · ${names.join(", ")}`;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
function clip(text: string, width: number): string {
|
|
152
|
-
if (width <= 0 || text.length <= width) return text;
|
|
153
|
-
return width === 1 ? "…" : `${text.slice(0, width - 1)}…`;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
87
|
/** Internal TUI adapter: no tools are replaced and no result content is changed. */
|
|
157
|
-
export function installMinimalOutputHook(
|
|
158
|
-
|
|
159
|
-
containerPrototype?: ContainerPrototype,
|
|
160
|
-
muted: (text: string) => string = (text) => text,
|
|
161
|
-
): MinimalOutputHook {
|
|
162
|
-
for (const name of ["updateDisplay", "getResultRenderer", "getTextOutput", "setExpanded", "invalidate"] as const) {
|
|
88
|
+
export function installMinimalOutputHook(prototype: ToolPrototype): MinimalOutputHook {
|
|
89
|
+
for (const name of ["updateDisplay", "getResultRenderer", "getTextOutput", "setExpanded", "invalidate", "render"] as const) {
|
|
163
90
|
if (typeof prototype[name] !== "function") {
|
|
164
91
|
throw new Error(`Pi's tool display API is incompatible: missing ${name}. Normal output remains enabled.`);
|
|
165
92
|
}
|
|
@@ -170,158 +97,35 @@ export function installMinimalOutputHook(
|
|
|
170
97
|
updateDisplay: prototype.updateDisplay,
|
|
171
98
|
getResultRenderer: prototype.getResultRenderer,
|
|
172
99
|
getTextOutput: prototype.getTextOutput,
|
|
100
|
+
render: prototype.render,
|
|
173
101
|
};
|
|
174
102
|
const seen = new WeakSet<ToolComponent>();
|
|
175
103
|
const components = new Set<WeakRef<ToolComponent>>();
|
|
176
|
-
const chats = new Set<WeakRef<ChatContainer>>();
|
|
177
|
-
const summaries = new WeakMap<ToolComponent, FoldSummary>();
|
|
178
|
-
const openedAt = new WeakMap<ToolComponent, number>();
|
|
179
|
-
let generation = 0;
|
|
180
|
-
let containerHooked = false;
|
|
181
|
-
let installedContainer: ContainerPrototype | undefined;
|
|
182
|
-
const isOpen = (tool: ToolComponent) => openedAt.get(tool) === generation;
|
|
183
|
-
const laterModelText = (children: unknown[], index: number) => {
|
|
184
|
-
for (let i = index; i < children.length; i++) {
|
|
185
|
-
if (hasModelText(children[i])) return true;
|
|
186
|
-
}
|
|
187
|
-
return false;
|
|
188
|
-
};
|
|
189
|
-
const attachFromTool = (tool: ToolComponent) => {
|
|
190
|
-
for (const child of tool.ui?.children ?? []) {
|
|
191
|
-
if (child?.children?.includes(tool)) wrapChat(child);
|
|
192
|
-
}
|
|
193
|
-
};
|
|
194
|
-
const wrapChat = (container: ChatContainer) => {
|
|
195
|
-
if (!container || seen.has(container as unknown as ToolComponent) || container[CHAT] || typeof container.render !== "function") return;
|
|
196
|
-
const original = container.render;
|
|
197
|
-
const wrapper = function (this: ChatContainer, width: number) {
|
|
198
|
-
if (!current.enabled || !Array.isArray(this.children) || !needsFold(this.children)) return original.call(this, width);
|
|
199
|
-
const lines: string[] = [];
|
|
200
|
-
const mouseChildren: Array<{ component: unknown; height: number }> = [];
|
|
201
|
-
const push = (component: { render(width: number): string[] }) => {
|
|
202
|
-
const childLines = component.render(width) ?? [];
|
|
203
|
-
mouseChildren.push({ component, height: childLines.length });
|
|
204
|
-
lines.push(...childLines);
|
|
205
|
-
};
|
|
206
|
-
let index = 0;
|
|
207
|
-
while (index < this.children.length) {
|
|
208
|
-
const child = this.children[index];
|
|
209
|
-
if (!seen.has(child as ToolComponent)) {
|
|
210
|
-
push(child as { render(width: number): string[] });
|
|
211
|
-
index += 1;
|
|
212
|
-
continue;
|
|
213
|
-
}
|
|
214
|
-
let end = index;
|
|
215
|
-
while (end < this.children.length && seen.has(this.children[end] as ToolComponent)) end += 1;
|
|
216
|
-
const tools = this.children.slice(index, end) as ToolComponent[];
|
|
217
|
-
const closed = laterModelText(this.children, end) && tools.every((tool) => !tool.expanded);
|
|
218
|
-
if (!closed) {
|
|
219
|
-
for (const tool of tools) push(tool);
|
|
220
|
-
} else {
|
|
221
|
-
const open = isOpen(tools[0]!);
|
|
222
|
-
let summary = summaries.get(tools[0]!);
|
|
223
|
-
if (!summary) {
|
|
224
|
-
summary = createSummary(tools[0]!);
|
|
225
|
-
summaries.set(tools[0]!, summary);
|
|
226
|
-
}
|
|
227
|
-
summary.setTools(tools);
|
|
228
|
-
push(summary);
|
|
229
|
-
if (open) for (const tool of tools) push(tool);
|
|
230
|
-
}
|
|
231
|
-
index = end;
|
|
232
|
-
}
|
|
233
|
-
this.mouseLayout = { width, children: mouseChildren };
|
|
234
|
-
return lines;
|
|
235
|
-
};
|
|
236
|
-
container[CHAT] = { original, wrapper };
|
|
237
|
-
container.render = wrapper;
|
|
238
|
-
chats.add(new WeakRef(container));
|
|
239
|
-
};
|
|
240
|
-
const createSummary = (anchor: ToolComponent): FoldSummary => {
|
|
241
|
-
let tools = [anchor];
|
|
242
|
-
return {
|
|
243
|
-
setTools(next) { tools = next; },
|
|
244
|
-
invalidate() {},
|
|
245
|
-
render(width) {
|
|
246
|
-
return ["", muted(clip(formatRun(tools, isOpen(anchor)), width))];
|
|
247
|
-
},
|
|
248
|
-
handleMouse(event) {
|
|
249
|
-
if (event.type !== "click" || (event.button && event.button !== "left")) return undefined;
|
|
250
|
-
if (isOpen(anchor)) openedAt.delete(anchor);
|
|
251
|
-
else openedAt.set(anchor, generation);
|
|
252
|
-
anchor.ui.requestRender();
|
|
253
|
-
return { handled: true };
|
|
254
|
-
},
|
|
255
|
-
};
|
|
256
|
-
};
|
|
257
|
-
const needsFold = (children: unknown[]) => {
|
|
258
|
-
let index = 0;
|
|
259
|
-
while (index < children.length) {
|
|
260
|
-
if (!seen.has(children[index] as ToolComponent)) { index += 1; continue; }
|
|
261
|
-
let end = index;
|
|
262
|
-
while (end < children.length && seen.has(children[end] as ToolComponent)) end += 1;
|
|
263
|
-
const tools = children.slice(index, end) as ToolComponent[];
|
|
264
|
-
if (laterModelText(children, end) && tools.every((tool) => !tool.expanded)) return true;
|
|
265
|
-
index = end;
|
|
266
|
-
}
|
|
267
|
-
return false;
|
|
268
|
-
};
|
|
269
104
|
state = {
|
|
270
105
|
enabled: false,
|
|
271
106
|
owners: 0,
|
|
272
|
-
foldsTurns: false,
|
|
273
|
-
closeRuns() { generation += 1; },
|
|
274
107
|
redraw(collapse = false) {
|
|
275
108
|
for (const reference of components) {
|
|
276
109
|
const component = reference.deref();
|
|
277
110
|
if (!component) { components.delete(reference); continue; }
|
|
278
111
|
if (collapse) component.setExpanded(false);
|
|
279
112
|
else component.invalidate();
|
|
280
|
-
attachFromTool(component);
|
|
281
113
|
component.ui.requestRender();
|
|
282
114
|
}
|
|
283
115
|
},
|
|
284
|
-
installContainer(next) {
|
|
285
|
-
if (containerHooked) return;
|
|
286
|
-
if (typeof next.addChild !== "function") {
|
|
287
|
-
throw new Error("Pi's chat container API is incompatible: missing Container.addChild. Turn folding is unavailable.");
|
|
288
|
-
}
|
|
289
|
-
const original = next[ADD]?.original ?? next.addChild;
|
|
290
|
-
const wrapper: ContainerPrototype["addChild"] = function (this: ChatContainer, child, ...rest) {
|
|
291
|
-
const result = original.call(this, child, ...rest);
|
|
292
|
-
if (seen.has(child as ToolComponent)) wrapChat(this);
|
|
293
|
-
return result;
|
|
294
|
-
};
|
|
295
|
-
next.addChild = wrapper;
|
|
296
|
-
next[ADD] = { original, wrapper };
|
|
297
|
-
containerHooked = true;
|
|
298
|
-
installedContainer = next;
|
|
299
|
-
state!.foldsTurns = true;
|
|
300
|
-
},
|
|
301
116
|
restore() {
|
|
302
117
|
for (const name of Object.keys(originals) as Array<keyof typeof originals>) {
|
|
303
118
|
// Do not remove another extension's subsequently installed wrapper.
|
|
304
|
-
if (prototype[name] === wrappers[name]) prototype[name]
|
|
119
|
+
if (prototype[name] === wrappers[name]) Object.assign(prototype, { [name]: originals[name] });
|
|
305
120
|
}
|
|
306
121
|
delete prototype[HOOK];
|
|
307
122
|
components.clear();
|
|
308
|
-
if (installedContainer && installedContainer[ADD]?.wrapper === installedContainer.addChild) {
|
|
309
|
-
installedContainer.addChild = installedContainer[ADD].original;
|
|
310
|
-
delete installedContainer[ADD];
|
|
311
|
-
}
|
|
312
|
-
for (const reference of chats) {
|
|
313
|
-
const container = reference.deref();
|
|
314
|
-
if (container?.[CHAT] && container.render === container[CHAT].wrapper) container.render = container[CHAT].original;
|
|
315
|
-
if (container) delete container[CHAT];
|
|
316
|
-
}
|
|
317
|
-
chats.clear();
|
|
318
123
|
},
|
|
319
124
|
};
|
|
320
125
|
const current = state;
|
|
321
126
|
const wrappers = {
|
|
322
127
|
updateDisplay(this: ToolComponent, ...args: unknown[]) {
|
|
323
128
|
if (!seen.has(this)) { seen.add(this); components.add(new WeakRef(this)); }
|
|
324
|
-
attachFromTool(this);
|
|
325
129
|
if (!current.enabled || this.expanded) return originals.updateDisplay.apply(this, args);
|
|
326
130
|
const showImages = this.showImages;
|
|
327
131
|
const lastRenderer = this.resultRendererComponent;
|
|
@@ -340,20 +144,20 @@ export function installMinimalOutputHook(
|
|
|
340
144
|
getTextOutput(this: ToolComponent, ...args: unknown[]) {
|
|
341
145
|
return current.enabled && !this.expanded ? "" : originals.getTextOutput.apply(this, args);
|
|
342
146
|
},
|
|
147
|
+
render(this: ToolComponent, width: number): string[] {
|
|
148
|
+
return current.enabled && !this.expanded ? [] : originals.render.call(this, width);
|
|
149
|
+
},
|
|
343
150
|
};
|
|
344
151
|
Object.assign(prototype, wrappers);
|
|
345
152
|
prototype[HOOK] = state;
|
|
346
153
|
}
|
|
347
154
|
const current = state;
|
|
348
|
-
if (containerPrototype) current.installContainer(containerPrototype);
|
|
349
155
|
current.owners += 1;
|
|
350
156
|
let disposed = false;
|
|
351
157
|
return {
|
|
352
|
-
foldsTurns: current.foldsTurns,
|
|
353
158
|
setEnabled(enabled) {
|
|
354
159
|
if (disposed) return;
|
|
355
160
|
current.enabled = enabled;
|
|
356
|
-
if (enabled) current.closeRuns();
|
|
357
161
|
current.redraw(enabled);
|
|
358
162
|
},
|
|
359
163
|
dispose() {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import { installMinimalOutputHook,
|
|
2
|
+
import { installMinimalOutputHook, loadToolPrototype, type MinimalOutputHook } from "./hook.ts";
|
|
3
3
|
|
|
4
4
|
const ENTRY = "pi-better-harness-tool-output";
|
|
5
5
|
|
|
@@ -24,12 +24,7 @@ export default function minimalOutputExtension(pi: ExtensionAPI): void {
|
|
|
24
24
|
}
|
|
25
25
|
if (hook) return true;
|
|
26
26
|
try {
|
|
27
|
-
|
|
28
|
-
loadToolPrototype(),
|
|
29
|
-
loadContainerPrototype().catch(() => undefined),
|
|
30
|
-
loadMutedText().catch(() => undefined),
|
|
31
|
-
]);
|
|
32
|
-
hook = installMinimalOutputHook(toolPrototype, containerPrototype, muted);
|
|
27
|
+
hook = installMinimalOutputHook(await loadToolPrototype());
|
|
33
28
|
return true;
|
|
34
29
|
} catch (error) {
|
|
35
30
|
ctx.ui.notify(`Minimal tool output is unavailable: ${error instanceof Error ? error.message : String(error)}`, "warning");
|
|
@@ -61,11 +56,11 @@ export default function minimalOutputExtension(pi: ExtensionAPI): void {
|
|
|
61
56
|
});
|
|
62
57
|
|
|
63
58
|
pi.registerCommand("tool-output", {
|
|
64
|
-
description: "[minimal|normal] —
|
|
59
|
+
description: "[minimal|normal] — Hide tool blocks entirely, or restore normal output. No argument toggles. Display only.",
|
|
65
60
|
getArgumentCompletions: (argumentPrefix) => {
|
|
66
61
|
const prefix = argumentPrefix.trimStart().toLowerCase();
|
|
67
62
|
const matches = [
|
|
68
|
-
{ value: "minimal", label: "minimal", description: "
|
|
63
|
+
{ value: "minimal", label: "minimal", description: "Hide all tool blocks, including running tools" },
|
|
69
64
|
{ value: "normal", label: "normal", description: "Restore ordinary tool output" },
|
|
70
65
|
].filter((option) => option.value.startsWith(prefix));
|
|
71
66
|
return matches.length > 0 ? matches : null;
|
|
@@ -83,9 +78,7 @@ export default function minimalOutputExtension(pi: ExtensionAPI): void {
|
|
|
83
78
|
pi.appendEntry(ENTRY, { version: 1, enabled });
|
|
84
79
|
status(ctx);
|
|
85
80
|
ctx.ui.notify(enabled
|
|
86
|
-
?
|
|
87
|
-
? "Minimal tool output on. Finished tool runs fold after the next model text. Ctrl+O reveals them; click a folded run when the terminal supports it."
|
|
88
|
-
: "Minimal tool output on, but turn folding is unavailable in this Pi version. Ctrl+O reveals results."
|
|
81
|
+
? "Minimal tool output on. Tool blocks are hidden. Ctrl+O reveals them."
|
|
89
82
|
: "Normal tool output restored.", "info");
|
|
90
83
|
},
|
|
91
84
|
});
|
package/package.json
CHANGED