pi-better-harness 0.13.1 → 0.15.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 +5 -5
- package/extensions/minimal-output/hook.ts +215 -8
- package/extensions/minimal-output/index.ts +12 -5
- package/node_modules/pi-better-background-tasks/package.json +1 -1
- package/node_modules/pi-better-background-tasks/src/shared-navigator.ts +22 -17
- package/node_modules/pi-better-goal/README.md +1 -1
- package/node_modules/pi-better-goal/package.json +1 -1
- package/node_modules/pi-better-goal/src/index.ts +99 -45
- package/node_modules/pi-better-sandbox/README.md +3 -2
- package/node_modules/pi-better-sandbox/package.json +1 -1
- package/node_modules/pi-better-sandbox/permissions-page.ts +6 -16
- package/node_modules/pi-better-subagents/README.md +4 -1
- package/node_modules/pi-better-subagents/agent-commands.ts +1 -1
- package/node_modules/pi-better-subagents/config.ts +29 -4
- package/node_modules/pi-better-subagents/docs/usage.md +16 -6
- package/node_modules/pi-better-subagents/health-surface.mjs +1 -0
- package/node_modules/pi-better-subagents/index.ts +96 -19
- package/node_modules/pi-better-subagents/navigator.mjs +3 -15
- package/node_modules/pi-better-subagents/package.json +1 -1
- package/node_modules/pi-better-subagents/shared-navigator.ts +22 -17
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -63,11 +63,11 @@ 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
|
-
|
|
68
|
-
|
|
69
|
-
Ctrl+O
|
|
70
|
-
also
|
|
66
|
+
and `/tool-output` (toggle). Minimal mode leaves the current tool run visible,
|
|
67
|
+
then folds every finished run between model texts into one line. Built-in,
|
|
68
|
+
extension, and MCP calls are included. Execution, sandboxing, and agent-facing
|
|
69
|
+
payloads are unchanged. Ctrl+O unfolds runs and reveals results. Clickable Pi
|
|
70
|
+
builds can also open one folded run by clicking its summary.
|
|
71
71
|
|
|
72
72
|
Normal mode is the default. The preference is saved in the current session,
|
|
73
73
|
including resume/reload. This is a version-sensitive internal TUI adapter,
|
|
@@ -7,9 +7,13 @@ interface ToolComponent {
|
|
|
7
7
|
expanded: boolean;
|
|
8
8
|
showImages: boolean;
|
|
9
9
|
resultRendererComponent?: unknown;
|
|
10
|
-
|
|
10
|
+
toolName?: string;
|
|
11
|
+
toolCallId?: string;
|
|
12
|
+
result?: { isError?: boolean };
|
|
13
|
+
ui: { requestRender(): void; children?: ChatContainer[] };
|
|
11
14
|
invalidate(): void;
|
|
12
15
|
setExpanded(expanded: boolean): void;
|
|
16
|
+
render(width: number): string[];
|
|
13
17
|
}
|
|
14
18
|
|
|
15
19
|
type Method = (this: ToolComponent, ...args: unknown[]) => unknown;
|
|
@@ -21,36 +25,83 @@ interface ToolPrototype {
|
|
|
21
25
|
invalidate: Method;
|
|
22
26
|
[HOOK]?: HookState;
|
|
23
27
|
}
|
|
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
|
+
}
|
|
24
39
|
interface HookState {
|
|
25
40
|
enabled: boolean;
|
|
26
41
|
owners: number;
|
|
42
|
+
foldsTurns: boolean;
|
|
43
|
+
closeRuns(): void;
|
|
27
44
|
redraw(collapse?: boolean): void;
|
|
28
45
|
restore(): void;
|
|
46
|
+
installContainer(prototype: ContainerPrototype): void;
|
|
29
47
|
}
|
|
30
48
|
export interface MinimalOutputHook {
|
|
31
49
|
setEnabled(enabled: boolean): void;
|
|
32
50
|
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;
|
|
33
58
|
}
|
|
34
59
|
|
|
35
60
|
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");
|
|
36
63
|
const emptyResult = () => ({ render: () => [] as string[], invalidate() {} });
|
|
37
64
|
|
|
38
|
-
|
|
39
|
-
export async function loadToolPrototype(): Promise<ToolPrototype> {
|
|
65
|
+
function hostRequire(): NodeRequire {
|
|
40
66
|
let require = createRequire(import.meta.url);
|
|
41
67
|
if (process.argv[1]) {
|
|
42
68
|
try {
|
|
43
|
-
const
|
|
44
|
-
sdkEntry(
|
|
45
|
-
require =
|
|
69
|
+
const candidate = createRequire(realpathSync(process.argv[1]));
|
|
70
|
+
sdkEntry(candidate);
|
|
71
|
+
require = candidate;
|
|
46
72
|
} catch { /* SDK embedding or a test runner: use the extension's SDK. */ }
|
|
47
73
|
}
|
|
48
|
-
|
|
74
|
+
return require;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Resolve the running host's SDK, not a second SDK bundled beside the extension. */
|
|
78
|
+
export async function loadToolPrototype(): Promise<ToolPrototype> {
|
|
79
|
+
const entry = sdkEntry(hostRequire());
|
|
49
80
|
const path = join(dirname(entry), "modes/interactive/components/tool-execution.js");
|
|
50
81
|
const module = await import(pathToFileURL(path).href);
|
|
51
82
|
return module.ToolExecutionComponent.prototype;
|
|
52
83
|
}
|
|
53
84
|
|
|
85
|
+
/** Chat rows are Container children. Patch that host class, not a second copy. */
|
|
86
|
+
export async function loadContainerPrototype(): Promise<ContainerPrototype> {
|
|
87
|
+
const entry = sdkEntry(hostRequire());
|
|
88
|
+
const tuiPath = createRequire(entry).resolve("@earendil-works/pi-tui");
|
|
89
|
+
const tui = await import(pathToFileURL(tuiPath).href);
|
|
90
|
+
const prototype = tui.Container?.prototype;
|
|
91
|
+
if (typeof prototype?.addChild !== "function" || typeof prototype?.render !== "function") {
|
|
92
|
+
throw new Error("Pi's chat container API is incompatible: missing Container.addChild. Turn folding is unavailable.");
|
|
93
|
+
}
|
|
94
|
+
return prototype;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export async function loadMutedText(): Promise<(text: string) => string> {
|
|
98
|
+
const entry = sdkEntry(hostRequire());
|
|
99
|
+
const theme = await import(pathToFileURL(join(dirname(entry), "modes/interactive/theme/theme.js")).href);
|
|
100
|
+
return (text) => {
|
|
101
|
+
try { return theme.theme.fg("muted", text); } catch { return text; }
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
54
105
|
// Pi's ESM-only export cannot be resolved with require.resolve on older SDKs.
|
|
55
106
|
function sdkEntry(require: ReturnType<typeof createRequire>): string {
|
|
56
107
|
for (const directory of require.resolve.paths("@earendil-works/pi-coding-agent") ?? []) {
|
|
@@ -64,8 +115,37 @@ function sdkEntry(require: ReturnType<typeof createRequire>): string {
|
|
|
64
115
|
throw new Error("Could not locate the running Pi SDK. Normal output remains enabled.");
|
|
65
116
|
}
|
|
66
117
|
|
|
118
|
+
function hasModelText(component: unknown): boolean {
|
|
119
|
+
const content = (component as { lastMessage?: { content?: unknown } } | null)?.lastMessage?.content;
|
|
120
|
+
if (!Array.isArray(content)) return false;
|
|
121
|
+
return content.some((block) => block?.type === "text" && typeof block.text === "string" && block.text.trim().length > 0);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function formatRun(tools: ToolComponent[], open: boolean): string {
|
|
125
|
+
const counts = new Map<string, number>();
|
|
126
|
+
let errors = 0;
|
|
127
|
+
for (const tool of tools) {
|
|
128
|
+
const name = tool.toolName || "tool";
|
|
129
|
+
counts.set(name, (counts.get(name) ?? 0) + 1);
|
|
130
|
+
if (tool.result?.isError) errors += 1;
|
|
131
|
+
}
|
|
132
|
+
const names = [...counts].map(([name, count]) => count > 1 ? `${name} ×${count}` : name);
|
|
133
|
+
const noun = tools.length === 1 ? "tool" : "tools";
|
|
134
|
+
const error = errors === 0 ? "" : errors === 1 ? " · 1 error" : ` · ${errors} errors`;
|
|
135
|
+
return `${open ? "▾" : "▸"} ${tools.length} ${noun}${error} · ${names.join(", ")}`;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function clip(text: string, width: number): string {
|
|
139
|
+
if (width <= 0 || text.length <= width) return text;
|
|
140
|
+
return width === 1 ? "…" : `${text.slice(0, width - 1)}…`;
|
|
141
|
+
}
|
|
142
|
+
|
|
67
143
|
/** Internal TUI adapter: no tools are replaced and no result content is changed. */
|
|
68
|
-
export function installMinimalOutputHook(
|
|
144
|
+
export function installMinimalOutputHook(
|
|
145
|
+
prototype: ToolPrototype,
|
|
146
|
+
containerPrototype?: ContainerPrototype,
|
|
147
|
+
muted: (text: string) => string = (text) => text,
|
|
148
|
+
): MinimalOutputHook {
|
|
69
149
|
for (const name of ["updateDisplay", "getResultRenderer", "getTextOutput", "setExpanded", "invalidate"] as const) {
|
|
70
150
|
if (typeof prototype[name] !== "function") {
|
|
71
151
|
throw new Error(`Pi's tool display API is incompatible: missing ${name}. Normal output remains enabled.`);
|
|
@@ -80,18 +160,131 @@ export function installMinimalOutputHook(prototype: ToolPrototype): MinimalOutpu
|
|
|
80
160
|
};
|
|
81
161
|
const seen = new WeakSet<ToolComponent>();
|
|
82
162
|
const components = new Set<WeakRef<ToolComponent>>();
|
|
163
|
+
const chats = new Set<WeakRef<ChatContainer>>();
|
|
164
|
+
const summaries = new WeakMap<ToolComponent, FoldSummary>();
|
|
165
|
+
const openedAt = new WeakMap<ToolComponent, number>();
|
|
166
|
+
let generation = 0;
|
|
167
|
+
let containerHooked = false;
|
|
168
|
+
let installedContainer: ContainerPrototype | undefined;
|
|
169
|
+
const isOpen = (tool: ToolComponent) => openedAt.get(tool) === generation;
|
|
170
|
+
const laterModelText = (children: unknown[], index: number) => {
|
|
171
|
+
for (let i = index; i < children.length; i++) {
|
|
172
|
+
if (hasModelText(children[i])) return true;
|
|
173
|
+
}
|
|
174
|
+
return false;
|
|
175
|
+
};
|
|
176
|
+
const attachFromTool = (tool: ToolComponent) => {
|
|
177
|
+
for (const child of tool.ui?.children ?? []) {
|
|
178
|
+
if (child?.children?.includes(tool)) wrapChat(child);
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
const wrapChat = (container: ChatContainer) => {
|
|
182
|
+
if (!container || seen.has(container as unknown as ToolComponent) || container[CHAT] || typeof container.render !== "function") return;
|
|
183
|
+
const original = container.render;
|
|
184
|
+
const wrapper = function (this: ChatContainer, width: number) {
|
|
185
|
+
if (!current.enabled || !Array.isArray(this.children) || !needsFold(this.children)) return original.call(this, width);
|
|
186
|
+
const lines: string[] = [];
|
|
187
|
+
const mouseChildren: Array<{ component: unknown; height: number }> = [];
|
|
188
|
+
const push = (component: { render(width: number): string[] }) => {
|
|
189
|
+
const childLines = component.render(width) ?? [];
|
|
190
|
+
mouseChildren.push({ component, height: childLines.length });
|
|
191
|
+
lines.push(...childLines);
|
|
192
|
+
};
|
|
193
|
+
let index = 0;
|
|
194
|
+
while (index < this.children.length) {
|
|
195
|
+
const child = this.children[index];
|
|
196
|
+
if (!seen.has(child as ToolComponent)) {
|
|
197
|
+
push(child as { render(width: number): string[] });
|
|
198
|
+
index += 1;
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
let end = index;
|
|
202
|
+
while (end < this.children.length && seen.has(this.children[end] as ToolComponent)) end += 1;
|
|
203
|
+
const tools = this.children.slice(index, end) as ToolComponent[];
|
|
204
|
+
const closed = laterModelText(this.children, end) && tools.every((tool) => !tool.expanded);
|
|
205
|
+
if (!closed) {
|
|
206
|
+
for (const tool of tools) push(tool);
|
|
207
|
+
} else {
|
|
208
|
+
const open = isOpen(tools[0]!);
|
|
209
|
+
let summary = summaries.get(tools[0]!);
|
|
210
|
+
if (!summary) {
|
|
211
|
+
summary = createSummary(tools[0]!);
|
|
212
|
+
summaries.set(tools[0]!, summary);
|
|
213
|
+
}
|
|
214
|
+
summary.setTools(tools);
|
|
215
|
+
push(summary);
|
|
216
|
+
if (open) for (const tool of tools) push(tool);
|
|
217
|
+
}
|
|
218
|
+
index = end;
|
|
219
|
+
}
|
|
220
|
+
this.mouseLayout = { width, children: mouseChildren };
|
|
221
|
+
return lines;
|
|
222
|
+
};
|
|
223
|
+
container[CHAT] = { original, wrapper };
|
|
224
|
+
container.render = wrapper;
|
|
225
|
+
chats.add(new WeakRef(container));
|
|
226
|
+
};
|
|
227
|
+
const createSummary = (anchor: ToolComponent): FoldSummary => {
|
|
228
|
+
let tools = [anchor];
|
|
229
|
+
return {
|
|
230
|
+
setTools(next) { tools = next; },
|
|
231
|
+
invalidate() {},
|
|
232
|
+
render(width) {
|
|
233
|
+
return ["", muted(clip(formatRun(tools, isOpen(anchor)), width))];
|
|
234
|
+
},
|
|
235
|
+
handleMouse(event) {
|
|
236
|
+
if (event.type !== "click" || (event.button && event.button !== "left")) return undefined;
|
|
237
|
+
if (isOpen(anchor)) openedAt.delete(anchor);
|
|
238
|
+
else openedAt.set(anchor, generation);
|
|
239
|
+
anchor.ui.requestRender();
|
|
240
|
+
return { handled: true };
|
|
241
|
+
},
|
|
242
|
+
};
|
|
243
|
+
};
|
|
244
|
+
const needsFold = (children: unknown[]) => {
|
|
245
|
+
let index = 0;
|
|
246
|
+
while (index < children.length) {
|
|
247
|
+
if (!seen.has(children[index] as ToolComponent)) { index += 1; continue; }
|
|
248
|
+
let end = index;
|
|
249
|
+
while (end < children.length && seen.has(children[end] as ToolComponent)) end += 1;
|
|
250
|
+
const tools = children.slice(index, end) as ToolComponent[];
|
|
251
|
+
if (laterModelText(children, end) && tools.every((tool) => !tool.expanded)) return true;
|
|
252
|
+
index = end;
|
|
253
|
+
}
|
|
254
|
+
return false;
|
|
255
|
+
};
|
|
83
256
|
state = {
|
|
84
257
|
enabled: false,
|
|
85
258
|
owners: 0,
|
|
259
|
+
foldsTurns: false,
|
|
260
|
+
closeRuns() { generation += 1; },
|
|
86
261
|
redraw(collapse = false) {
|
|
87
262
|
for (const reference of components) {
|
|
88
263
|
const component = reference.deref();
|
|
89
264
|
if (!component) { components.delete(reference); continue; }
|
|
90
265
|
if (collapse) component.setExpanded(false);
|
|
91
266
|
else component.invalidate();
|
|
267
|
+
attachFromTool(component);
|
|
92
268
|
component.ui.requestRender();
|
|
93
269
|
}
|
|
94
270
|
},
|
|
271
|
+
installContainer(next) {
|
|
272
|
+
if (containerHooked) return;
|
|
273
|
+
if (typeof next.addChild !== "function") {
|
|
274
|
+
throw new Error("Pi's chat container API is incompatible: missing Container.addChild. Turn folding is unavailable.");
|
|
275
|
+
}
|
|
276
|
+
const original = next[ADD]?.original ?? next.addChild;
|
|
277
|
+
const wrapper: ContainerPrototype["addChild"] = function (this: ChatContainer, child, ...rest) {
|
|
278
|
+
const result = original.call(this, child, ...rest);
|
|
279
|
+
if (seen.has(child as ToolComponent)) wrapChat(this);
|
|
280
|
+
return result;
|
|
281
|
+
};
|
|
282
|
+
next.addChild = wrapper;
|
|
283
|
+
next[ADD] = { original, wrapper };
|
|
284
|
+
containerHooked = true;
|
|
285
|
+
installedContainer = next;
|
|
286
|
+
state!.foldsTurns = true;
|
|
287
|
+
},
|
|
95
288
|
restore() {
|
|
96
289
|
for (const name of Object.keys(originals) as Array<keyof typeof originals>) {
|
|
97
290
|
// Do not remove another extension's subsequently installed wrapper.
|
|
@@ -99,12 +292,23 @@ export function installMinimalOutputHook(prototype: ToolPrototype): MinimalOutpu
|
|
|
99
292
|
}
|
|
100
293
|
delete prototype[HOOK];
|
|
101
294
|
components.clear();
|
|
295
|
+
if (installedContainer && installedContainer[ADD]?.wrapper === installedContainer.addChild) {
|
|
296
|
+
installedContainer.addChild = installedContainer[ADD].original;
|
|
297
|
+
delete installedContainer[ADD];
|
|
298
|
+
}
|
|
299
|
+
for (const reference of chats) {
|
|
300
|
+
const container = reference.deref();
|
|
301
|
+
if (container?.[CHAT] && container.render === container[CHAT].wrapper) container.render = container[CHAT].original;
|
|
302
|
+
if (container) delete container[CHAT];
|
|
303
|
+
}
|
|
304
|
+
chats.clear();
|
|
102
305
|
},
|
|
103
306
|
};
|
|
104
307
|
const current = state;
|
|
105
308
|
const wrappers = {
|
|
106
309
|
updateDisplay(this: ToolComponent, ...args: unknown[]) {
|
|
107
310
|
if (!seen.has(this)) { seen.add(this); components.add(new WeakRef(this)); }
|
|
311
|
+
attachFromTool(this);
|
|
108
312
|
if (!current.enabled || this.expanded) return originals.updateDisplay.apply(this, args);
|
|
109
313
|
const showImages = this.showImages;
|
|
110
314
|
const lastRenderer = this.resultRendererComponent;
|
|
@@ -128,12 +332,15 @@ export function installMinimalOutputHook(prototype: ToolPrototype): MinimalOutpu
|
|
|
128
332
|
prototype[HOOK] = state;
|
|
129
333
|
}
|
|
130
334
|
const current = state;
|
|
335
|
+
if (containerPrototype) current.installContainer(containerPrototype);
|
|
131
336
|
current.owners += 1;
|
|
132
337
|
let disposed = false;
|
|
133
338
|
return {
|
|
339
|
+
foldsTurns: current.foldsTurns,
|
|
134
340
|
setEnabled(enabled) {
|
|
135
341
|
if (disposed) return;
|
|
136
342
|
current.enabled = enabled;
|
|
343
|
+
if (enabled) current.closeRuns();
|
|
137
344
|
current.redraw(enabled);
|
|
138
345
|
},
|
|
139
346
|
dispose() {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import { installMinimalOutputHook, loadToolPrototype, type MinimalOutputHook } from "./hook.ts";
|
|
2
|
+
import { installMinimalOutputHook, loadContainerPrototype, loadMutedText, loadToolPrototype, type MinimalOutputHook } from "./hook.ts";
|
|
3
3
|
|
|
4
4
|
const ENTRY = "pi-better-harness-tool-output";
|
|
5
5
|
|
|
@@ -24,7 +24,12 @@ export default function minimalOutputExtension(pi: ExtensionAPI): void {
|
|
|
24
24
|
}
|
|
25
25
|
if (hook) return true;
|
|
26
26
|
try {
|
|
27
|
-
|
|
27
|
+
const [toolPrototype, containerPrototype, muted] = await Promise.all([
|
|
28
|
+
loadToolPrototype(),
|
|
29
|
+
loadContainerPrototype().catch(() => undefined),
|
|
30
|
+
loadMutedText().catch(() => undefined),
|
|
31
|
+
]);
|
|
32
|
+
hook = installMinimalOutputHook(toolPrototype, containerPrototype, muted);
|
|
28
33
|
return true;
|
|
29
34
|
} catch (error) {
|
|
30
35
|
ctx.ui.notify(`Minimal tool output is unavailable: ${error instanceof Error ? error.message : String(error)}`, "warning");
|
|
@@ -56,11 +61,11 @@ export default function minimalOutputExtension(pi: ExtensionAPI): void {
|
|
|
56
61
|
});
|
|
57
62
|
|
|
58
63
|
pi.registerCommand("tool-output", {
|
|
59
|
-
description: "
|
|
64
|
+
description: "[minimal|normal] — Fold finished tool runs, or restore normal output. No argument toggles. Display only.",
|
|
60
65
|
getArgumentCompletions: (argumentPrefix) => {
|
|
61
66
|
const prefix = argumentPrefix.trimStart().toLowerCase();
|
|
62
67
|
const matches = [
|
|
63
|
-
{ value: "minimal", label: "minimal", description: "
|
|
68
|
+
{ value: "minimal", label: "minimal", description: "Fold finished tool runs after the next model text" },
|
|
64
69
|
{ value: "normal", label: "normal", description: "Restore ordinary tool output" },
|
|
65
70
|
].filter((option) => option.value.startsWith(prefix));
|
|
66
71
|
return matches.length > 0 ? matches : null;
|
|
@@ -78,7 +83,9 @@ export default function minimalOutputExtension(pi: ExtensionAPI): void {
|
|
|
78
83
|
pi.appendEntry(ENTRY, { version: 1, enabled });
|
|
79
84
|
status(ctx);
|
|
80
85
|
ctx.ui.notify(enabled
|
|
81
|
-
?
|
|
86
|
+
? hook!.foldsTurns
|
|
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."
|
|
82
89
|
: "Normal tool output restored.", "info");
|
|
83
90
|
},
|
|
84
91
|
});
|
|
@@ -66,6 +66,8 @@ export type BackgroundWorkProvider = {
|
|
|
66
66
|
detail(id: string, now: number, options?: { logTailLines?: number }): BackgroundWorkDetail | null;
|
|
67
67
|
armCloseLabel(row: BackgroundWorkRow): string;
|
|
68
68
|
close(id: string): BackgroundWorkCloseOutcome;
|
|
69
|
+
/** Return true when this detail handled the key. The navigator refreshes the sheet. */
|
|
70
|
+
handleDetailInput?(id: string, data: string): boolean;
|
|
69
71
|
onVisibleChanged?(notify: () => void): () => void;
|
|
70
72
|
};
|
|
71
73
|
|
|
@@ -559,7 +561,7 @@ function rowSummary(row: InternalRow): string {
|
|
|
559
561
|
const parts: string[] = [];
|
|
560
562
|
if (row.statusTone !== "running" && row.kind !== "main agent") parts.push(singleLine(row.status));
|
|
561
563
|
if (row.model) parts.push(row.effort ? `${singleLine(row.model)} ${singleLine(row.effort)}` : singleLine(row.model));
|
|
562
|
-
if (row.tool) parts.push(
|
|
564
|
+
if (row.tool) parts.push(singleLine(row.tool));
|
|
563
565
|
if (row.tokens) parts.push(singleLine(row.tokens));
|
|
564
566
|
else if (row.primary) parts.push(singleLine(row.primary));
|
|
565
567
|
if (parts.length) return parts.join(" · ");
|
|
@@ -703,12 +705,7 @@ function handleMainListInput(data: string, deps: HostDeps): boolean {
|
|
|
703
705
|
return true;
|
|
704
706
|
}
|
|
705
707
|
if (deps.matchKey(data, "enter")) {
|
|
706
|
-
|
|
707
|
-
if (selected?.parentRow) {
|
|
708
|
-
unfocusMainList();
|
|
709
|
-
dismissOverlay();
|
|
710
|
-
}
|
|
711
|
-
else openNavigator();
|
|
708
|
+
openNavigator();
|
|
712
709
|
return true;
|
|
713
710
|
}
|
|
714
711
|
if (data === "x" || data === "X" || deps.matchKey(data, "x") || deps.matchKey(data, "X")) {
|
|
@@ -851,7 +848,8 @@ function createOverlayComponent(
|
|
|
851
848
|
let logTailRows: number = DEFAULT_LOG_TAIL_ROWS;
|
|
852
849
|
const expandedSections = new Set<string>();
|
|
853
850
|
let detailId: string = initialDetailId;
|
|
854
|
-
|
|
851
|
+
const initialRow = initialRows.find((row) => row.navigatorId === detailId);
|
|
852
|
+
let detail: BackgroundWorkDetail | null = detailFor(detailId, Date.now(), { logTailLines: logTailRows }) ?? (initialRow ? fallbackDetail(initialRow) : null);
|
|
855
853
|
applyDefaultExpandedSections(detail, expandedSections);
|
|
856
854
|
{
|
|
857
855
|
const idx = overlayState.rows.findIndex((row) => row.navigatorId === detailId);
|
|
@@ -891,8 +889,7 @@ function createOverlayComponent(
|
|
|
891
889
|
}
|
|
892
890
|
|
|
893
891
|
function activateSelectedRow(): void {
|
|
894
|
-
|
|
895
|
-
else openDetail();
|
|
892
|
+
openDetail();
|
|
896
893
|
}
|
|
897
894
|
|
|
898
895
|
function clearCloseArm(): void {
|
|
@@ -922,10 +919,6 @@ function createOverlayComponent(
|
|
|
922
919
|
function openDetail(): void {
|
|
923
920
|
const row = selectedRow();
|
|
924
921
|
if (!row) return;
|
|
925
|
-
if (row.parentRow) {
|
|
926
|
-
close();
|
|
927
|
-
return;
|
|
928
|
-
}
|
|
929
922
|
clearCloseArm();
|
|
930
923
|
detailId = row.navigatorId;
|
|
931
924
|
expandedSections.clear();
|
|
@@ -1073,6 +1066,15 @@ function createOverlayComponent(
|
|
|
1073
1066
|
requestRender();
|
|
1074
1067
|
}
|
|
1075
1068
|
}
|
|
1069
|
+
else if (data === "m" || data === "M" || data === "\x13" || deps.matchKey(data, "ctrl+s")) {
|
|
1070
|
+
const row = selectedRow();
|
|
1071
|
+
const provider = row ? state().providers.get(row.providerId) : undefined;
|
|
1072
|
+
const key = data === "m" || data === "M" ? "m" : "ctrl+s";
|
|
1073
|
+
if (!row || !provider?.handleDetailInput?.(row.id, key)) return;
|
|
1074
|
+
detail = detailFor(detailId, Date.now(), { logTailLines: logTailRows }) ?? detail;
|
|
1075
|
+
refreshMainListWidget();
|
|
1076
|
+
requestRender();
|
|
1077
|
+
}
|
|
1076
1078
|
else if (data === "l" || data === "L") {
|
|
1077
1079
|
logTailRows = cycleLogTailRows(logTailRows);
|
|
1078
1080
|
if (detailId) detail = detailFor(detailId, Date.now(), { logTailLines: logTailRows }) ?? detail;
|
|
@@ -1091,8 +1093,7 @@ function createOverlayComponent(
|
|
|
1091
1093
|
if (idx < 0) return;
|
|
1092
1094
|
overlayState.selected = idx;
|
|
1093
1095
|
state().mainListSelectedId = navigatorId;
|
|
1094
|
-
|
|
1095
|
-
else openDetail();
|
|
1096
|
+
openDetail();
|
|
1096
1097
|
},
|
|
1097
1098
|
/** Close the overlay (done), e.g. when the navigator is disposed. */
|
|
1098
1099
|
dismiss() { close(); },
|
|
@@ -1150,7 +1151,11 @@ function buildTranscriptDetailLines(
|
|
|
1150
1151
|
...detail.metadata.map((item) => ` ${item.label.padEnd(8, " ").slice(0, 8)} ${item.value}`),
|
|
1151
1152
|
];
|
|
1152
1153
|
const transcriptHeader = ["", dim(section(`transcript · latest ${tailRows} rows`, width), fg)];
|
|
1153
|
-
if (detail.transcriptDiagnostic)
|
|
1154
|
+
if (detail.transcriptDiagnostic) {
|
|
1155
|
+
for (const diagnosticRow of detail.transcriptDiagnostic.split(/\r\n|\r|\n/)) {
|
|
1156
|
+
transcriptHeader.push(` ${dim(diagnosticRow, fg)}`);
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1154
1159
|
// Metadata always renders; the transcript tail takes only the rows left over (the tail size is a cap, not a guarantee).
|
|
1155
1160
|
lines.push(...optionalMetadata);
|
|
1156
1161
|
if (options.maxRows !== undefined) {
|
|
@@ -17,7 +17,7 @@ Use `pi-better-goal` when a Pi session should keep an explicit objective visible
|
|
|
17
17
|
- Background work that finishes while an `ask_user_question` is pending is handed to the agent right after the answer.
|
|
18
18
|
- A compact goal widget that does not replace Pi's footer.
|
|
19
19
|
- Background activity tracking for subagents and other registered providers.
|
|
20
|
-
- A progress-aware follow-up loop
|
|
20
|
+
- A progress-aware follow-up loop with ten identical no-progress retries and linear backoff (30s through 300s by default), followed by a hold recoverable with `/goal resume`. Delays saturate at Node's timer limit, and observed background drains reset progress before callback turns can cancel the wake. Pi's network retry policy remains independent.
|
|
21
21
|
- An observable-progress stall state for active goals.
|
|
22
22
|
|
|
23
23
|
## Skill-Owned Workflows
|