pi-better-harness 0.14.0 → 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-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 +4 -4
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) {
|
|
@@ -121,8 +121,9 @@ repository's `.git/hooks` or `.git/config` (`core.hooksPath`, `core.fsmonitor`,
|
|
|
121
121
|
Write, those paths are also protected from removal and renaming.
|
|
122
122
|
|
|
123
123
|
Space applies every change immediately, including higher permission levels,
|
|
124
|
-
capabilities switched on, and a sandbox switched off.
|
|
125
|
-
|
|
124
|
+
capabilities switched on, and a sandbox switched off. `ctrl+s` saves the
|
|
125
|
+
defaults from any row on the first press, and the note names anything that
|
|
126
|
+
loosened. Enter on the Save row still saves. The focused
|
|
126
127
|
row has a full-width background and bold text; the selected Main/Subagents cell
|
|
127
128
|
also uses inverse styling. No model
|
|
128
129
|
tool can change these settings. Other rows toggle Off/On. Detail cells under an
|
|
@@ -9,7 +9,7 @@ export interface PermissionPageHandlers {
|
|
|
9
9
|
getConfig(): PermissionSettings;
|
|
10
10
|
change(settings: PermissionSettings): void | Promise<void>;
|
|
11
11
|
save(settings: PermissionSettings): void | Promise<void>;
|
|
12
|
-
/** What saving `settings` would loosen versus the saved defaults
|
|
12
|
+
/** What saving `settings` would loosen versus the saved defaults. Reported after the save. */
|
|
13
13
|
loosening?(settings: PermissionSettings): string[];
|
|
14
14
|
/** Trusted-tool candidates registered in the running Pi (builtins and harness tools excluded). */
|
|
15
15
|
discoverTools?(): readonly Pick<DiscoveredTool, "name" | "package">[];
|
|
@@ -128,7 +128,6 @@ export function createPermissionsPage(
|
|
|
128
128
|
let row = 0;
|
|
129
129
|
let column = 0;
|
|
130
130
|
let busy = false;
|
|
131
|
-
let pendingConfirmation: string | undefined;
|
|
132
131
|
|
|
133
132
|
function report(error: unknown): void {
|
|
134
133
|
message = errorText(error);
|
|
@@ -202,19 +201,10 @@ export function createPermissionsPage(
|
|
|
202
201
|
report(error);
|
|
203
202
|
return;
|
|
204
203
|
}
|
|
205
|
-
const key = JSON.stringify(settings);
|
|
206
|
-
if (loosened.length && pendingConfirmation !== key) {
|
|
207
|
-
pendingConfirmation = key;
|
|
208
|
-
message = `Looser defaults (${loosened.join("; ")}). Press Enter again to save.`;
|
|
209
|
-
isError = false;
|
|
210
|
-
requestRender();
|
|
211
|
-
return;
|
|
212
|
-
}
|
|
213
|
-
pendingConfirmation = undefined;
|
|
214
204
|
busy = true;
|
|
215
205
|
try {
|
|
216
206
|
await handlers.save(snapshot(settings));
|
|
217
|
-
message = "Defaults saved.";
|
|
207
|
+
message = loosened.length ? `Defaults saved. Looser: ${loosened.join("; ")}.` : "Defaults saved.";
|
|
218
208
|
isError = false;
|
|
219
209
|
requestRender();
|
|
220
210
|
} catch (error) {
|
|
@@ -238,7 +228,6 @@ export function createPermissionsPage(
|
|
|
238
228
|
return {
|
|
239
229
|
invalidate() {},
|
|
240
230
|
handleInput(data: string) {
|
|
241
|
-
if (!matchesKey(data, Key.enter)) pendingConfirmation = undefined;
|
|
242
231
|
if (matchesKey(data, Key.escape)) {
|
|
243
232
|
close();
|
|
244
233
|
} else if (matchesKey(data, Key.up)) {
|
|
@@ -257,6 +246,8 @@ export function createPermissionsPage(
|
|
|
257
246
|
requestRender();
|
|
258
247
|
} else if (matchesKey(data, Key.space)) {
|
|
259
248
|
void change();
|
|
249
|
+
} else if (matchesKey(data, "ctrl+s")) {
|
|
250
|
+
void save();
|
|
260
251
|
} else if (matchesKey(data, Key.enter) && row === saveRow()) {
|
|
261
252
|
void save();
|
|
262
253
|
} else if (matchesKey(data, Key.enter)) {
|
|
@@ -330,7 +321,7 @@ export function createPermissionsPage(
|
|
|
330
321
|
const contextual = tool ? (tool.kind === "guarded" ? GUARDED_HINT : TRUSTED_HINT)
|
|
331
322
|
: selected && settings ? cellHint(selected, settings[columns[column]!]) : undefined;
|
|
332
323
|
for (const hint of [
|
|
333
|
-
"↑↓ Select · ←→ Column/fold · Space Toggle
|
|
324
|
+
"↑↓ Select · ←→ Column/fold · Space Toggle · ctrl+s Save · Enter Fold · Esc Back",
|
|
334
325
|
"Changes apply to new launches. Background tasks follow their launcher.",
|
|
335
326
|
"Stored credentials: known files only; excludes OS vaults and environment tokens.",
|
|
336
327
|
"Trusted tools run outside the file rules.",
|
|
@@ -338,8 +329,7 @@ export function createPermissionsPage(
|
|
|
338
329
|
]) output.push(theme.fg("dim", truncateToWidth(hint, w, "")));
|
|
339
330
|
if (message) {
|
|
340
331
|
const clean = message.replace(/[\r\n]+/g, " ");
|
|
341
|
-
const
|
|
342
|
-
const messages = confirmation && visibleWidth(clean) > w ? [confirmation[1]!, confirmation[2]!] : [clean];
|
|
332
|
+
const messages = visibleWidth(clean) > w && clean.includes(". ") ? clean.split(/(?<=\. )/) : [clean];
|
|
343
333
|
for (const text of messages) output.push(theme.fg(isError ? "error" : "muted", truncateToWidth(text, w, "")));
|
|
344
334
|
}
|
|
345
335
|
return output;
|
|
@@ -38,7 +38,10 @@ Linux confinement requires a usable `bubblewrap` backend and Pi SDK 0.82.1 or ne
|
|
|
38
38
|
`config.json` sets `delegationMode` to `manual`, `adaptive` (default), or
|
|
39
39
|
`coordinator`. `/subagents` displays the active mode, and
|
|
40
40
|
`/subagents mode manual|adaptive|coordinator` changes it for the current
|
|
41
|
-
session without changing config.
|
|
41
|
+
session without changing config. `/subagents save` writes the current mode as
|
|
42
|
+
the next session's default. The navigator's `main` row opens the same control:
|
|
43
|
+
`m` cycles the session mode, `ctrl+s` saves that default, and `x`
|
|
44
|
+
does not stop it. Manual delegates only on explicit user or workflow request, even with a
|
|
42
45
|
plan. Adaptive delegates substantial independent work when useful. Coordinator
|
|
43
46
|
uses `agents_catalog` to discover current role descriptions and delegates every
|
|
44
47
|
nontrivial role-owned task, while the foreground coordinates, integrates, and
|
|
@@ -123,7 +123,7 @@ interface ParsedArgs {
|
|
|
123
123
|
|
|
124
124
|
export function registerAgentCommands(pi: Pick<ExtensionAPI, "registerCommand">, deps: AgentCommandDeps = {}): void {
|
|
125
125
|
pi.registerCommand("agents", {
|
|
126
|
-
description: "List, inspect, create, reload, or import
|
|
126
|
+
description: "[list|show|create|reload|import-codex] — List, inspect, create, reload, or import agent definitions",
|
|
127
127
|
getArgumentCompletions(prefix: string) {
|
|
128
128
|
const commands = ["list", "inspect", "show", "create", "reload", "import-codex", "help"];
|
|
129
129
|
const first = prefix.trim().split(/\s+/)[0] ?? "";
|
|
@@ -8,12 +8,12 @@
|
|
|
8
8
|
* `defaultTools` absent → the built-in SAFE_DEFAULT_TOOLS.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
11
|
+
import { existsSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
12
12
|
import { fileURLToPath } from "node:url";
|
|
13
13
|
import { dirname, join } from "node:path";
|
|
14
14
|
import { homedir } from "node:os";
|
|
15
15
|
import { SELF_SPEC } from "./extensions.ts";
|
|
16
|
-
import type
|
|
16
|
+
import { isDelegationMode, type DelegationMode } from "./delegation.ts";
|
|
17
17
|
import type { TimingSettings } from "./timing.ts";
|
|
18
18
|
|
|
19
19
|
export interface SubagentConfig extends TimingSettings {
|
|
@@ -80,24 +80,49 @@ export const SAFE_DEFAULT_TOOLS = "read, bash, edit, write, web_search, web_fetc
|
|
|
80
80
|
export const SAFE_CLEAN_TOOLS = "read, bash";
|
|
81
81
|
|
|
82
82
|
let cached: SubagentConfig | undefined;
|
|
83
|
+
let configPathForTests: string | undefined;
|
|
83
84
|
|
|
84
85
|
/** Test seam. Pass undefined to reload config.json on the next loadConfig call. */
|
|
85
86
|
export function setConfigForTests(next: SubagentConfig | undefined): void {
|
|
86
87
|
cached = next;
|
|
87
88
|
}
|
|
88
89
|
|
|
90
|
+
/** Test seam. Save and load use this file instead of the package config.json. */
|
|
91
|
+
export function setConfigPathForTests(path: string | undefined): void {
|
|
92
|
+
configPathForTests = path;
|
|
93
|
+
cached = undefined;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function configPath(): string {
|
|
97
|
+
return configPathForTests ?? join(dirname(fileURLToPath(import.meta.url)), "config.json");
|
|
98
|
+
}
|
|
99
|
+
|
|
89
100
|
/** Load config.json from the extension directory. Missing/invalid → {}. */
|
|
90
101
|
export function loadConfig(): SubagentConfig {
|
|
91
102
|
if (cached) return cached;
|
|
92
103
|
try {
|
|
93
|
-
|
|
94
|
-
cached = JSON.parse(readFileSync(join(dir, "config.json"), "utf-8")) as SubagentConfig;
|
|
104
|
+
cached = JSON.parse(readFileSync(configPath(), "utf-8")) as SubagentConfig;
|
|
95
105
|
} catch {
|
|
96
106
|
cached = {};
|
|
97
107
|
}
|
|
98
108
|
return cached;
|
|
99
109
|
}
|
|
100
110
|
|
|
111
|
+
/** Write delegationMode into config.json, preserving every other key. */
|
|
112
|
+
export function writeDelegationMode(mode: DelegationMode, path = configPath()): void {
|
|
113
|
+
if (!isDelegationMode(mode)) throw new Error(`Invalid delegation mode: ${String(mode)}`);
|
|
114
|
+
let current: SubagentConfig = {};
|
|
115
|
+
try {
|
|
116
|
+
const parsed = JSON.parse(readFileSync(path, "utf-8")) as unknown;
|
|
117
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) current = parsed as SubagentConfig;
|
|
118
|
+
} catch { /* missing file starts as an empty config */ }
|
|
119
|
+
current.delegationMode = mode;
|
|
120
|
+
const pending = `${path}.${process.pid}.tmp`;
|
|
121
|
+
writeFileSync(pending, `${JSON.stringify(current, null, 2)}\n`);
|
|
122
|
+
renameSync(pending, path);
|
|
123
|
+
if (path === configPath()) cached = current;
|
|
124
|
+
}
|
|
125
|
+
|
|
101
126
|
/** Normalize a comma/space tool list to pi's bare comma form: "a, b" → "a,b". */
|
|
102
127
|
export function normalizeTools(list: string): string {
|
|
103
128
|
return list.split(",").map((t) => t.trim()).filter(Boolean).join(",");
|
|
@@ -18,9 +18,12 @@ Set `delegationMode` in this package's `config.json` to `manual`, `adaptive`, or
|
|
|
18
18
|
`coordinator` (default `adaptive`). This controls foreground delegation guidance,
|
|
19
19
|
not child permissions or catalog definitions. `/subagents` reports the active
|
|
20
20
|
mode; `/subagents mode manual|adaptive|coordinator` overrides it for the current
|
|
21
|
-
session without editing config.
|
|
22
|
-
|
|
23
|
-
session
|
|
21
|
+
session without editing config. `/subagents save` writes the current mode into
|
|
22
|
+
`config.json` after confirmation when a UI is available, and clears the session
|
|
23
|
+
override so the next session starts there. On the navigator `main` sheet, `m`
|
|
24
|
+
cycles the session mode and `ctrl+s` saves that default.
|
|
25
|
+
Other arguments show usage. `/reload` retains a session override; new sessions
|
|
26
|
+
use config, while resuming a session restores that session's override.
|
|
24
27
|
|
|
25
28
|
- **Manual:** no proactive delegation, including in plan mode. A user request or
|
|
26
29
|
explicit workflow requirement may still delegate.
|
|
@@ -380,6 +383,7 @@ top.
|
|
|
380
383
|
foreground model).
|
|
381
384
|
- `delegationMode` — foreground policy (`manual`, `adaptive`, or `coordinator`;
|
|
382
385
|
default `adaptive`). `/subagents mode ...` changes only the current session.
|
|
386
|
+
`/subagents save` writes that mode as the config default.
|
|
383
387
|
- `maxConcurrent` — how many subagents may run at once (**default 4**). A spawn
|
|
384
388
|
past the cap is rejected until a running one finishes.
|
|
385
389
|
|
|
@@ -517,9 +521,11 @@ is unchanged in every mode.
|
|
|
517
521
|
- While the main-window list is focused, the title hint changes to
|
|
518
522
|
`Enter to view · x to stop`; the selected row is marked with `›`. Press
|
|
519
523
|
`↓` from the bottom row to return to the input line.
|
|
520
|
-
- The Subagents lane pins
|
|
521
|
-
|
|
522
|
-
|
|
524
|
+
- The Subagents lane pins a `main` row above child runs. It shows the
|
|
525
|
+
foreground model, effort, active tool, context tokens, elapsed time, and
|
|
526
|
+
delegation mode. Enter opens its detail sheet. `m` cycles the session mode.
|
|
527
|
+
`ctrl+s` saves that mode as the config default. `x` still stops
|
|
528
|
+
a child run; it never stops main.
|
|
523
529
|
- `↑` moves to the previous row when multiple running rows are shown. `↓`
|
|
524
530
|
moves toward the input line, returning to normal input from the bottom row.
|
|
525
531
|
- `Enter` opens the selected run's live detail view. `x` stops the selected
|
|
@@ -637,6 +643,10 @@ health model:
|
|
|
637
643
|
orphaned, lost, and degraded running runs; #65 orphaned/lost result bodies kept.
|
|
638
644
|
- **Passive live widget** — healthy/quiet unchanged; degraded (and orphaned) may
|
|
639
645
|
show a short suffix. Still `setWidget` only — never focusable.
|
|
646
|
+
- **Navigator** — lifecycle status and operational health facts remain visible;
|
|
647
|
+
child tool/model incident summaries and history are omitted from rows and detail
|
|
648
|
+
chrome. Tool labels show just their name (for example, `bash`). The original
|
|
649
|
+
transcript, including error results, and transcript-read diagnostics remain available.
|
|
640
650
|
- **`callback:false`** suppresses coordinator follow-up only; human `ui.notify` and
|
|
641
651
|
TUI/passive visibility remain.
|
|
642
652
|
|
|
@@ -162,6 +162,7 @@ export function formatNavigatorHealthFacts(obs) {
|
|
|
162
162
|
if (!obs || !isActionableHealth(obs)) return [];
|
|
163
163
|
const facts = [];
|
|
164
164
|
for (const f of surfaceableCompactFacts(obs)) {
|
|
165
|
+
if (obs.model?.state === "error" && f === obs.model.listWarning) continue;
|
|
165
166
|
if (f && f !== obs.status && !facts.includes(f)) facts.push(f);
|
|
166
167
|
}
|
|
167
168
|
if (obs.activity === "stale" && !facts.some((f) => /\bstale\b/i.test(f))) {
|
|
@@ -35,7 +35,7 @@ import {
|
|
|
35
35
|
import { spawnDetached, type SpawnResult } from "./spawn.ts";
|
|
36
36
|
import { parseRun, readRunTranscript, resetParseRunCursor, type Usage } from "./parse.ts";
|
|
37
37
|
import { finalizeRun as finalizeRunCore } from "./finalization.ts";
|
|
38
|
-
import { loadConfig, normalizeTools, resolveExtensionPath, selfDir, SAFE_DEFAULT_TOOLS, SAFE_CLEAN_TOOLS, DEFAULT_MAX_CONCURRENT } from "./config.ts";
|
|
38
|
+
import { loadConfig, writeDelegationMode, normalizeTools, resolveExtensionPath, selfDir, SAFE_DEFAULT_TOOLS, SAFE_CLEAN_TOOLS, DEFAULT_MAX_CONCURRENT } from "./config.ts";
|
|
39
39
|
import { STEER_FILE_ENV, readSteerReceipt } from "./child-steer.ts";
|
|
40
40
|
import {
|
|
41
41
|
decideTiming,
|
|
@@ -117,7 +117,7 @@ import {
|
|
|
117
117
|
import { buildHealthCallbackDelivery } from "./completion.ts";
|
|
118
118
|
import { cancelCallbackBatch, getCallbackBatcher } from "./shared-callback-batcher.ts";
|
|
119
119
|
import { completionCallbackFields, failureAttentionFields, healthCallbackFields } from "./callback-fields.ts";
|
|
120
|
-
import { collectRunFailures, failurePath,
|
|
120
|
+
import { collectRunFailures, failurePath, markFailureAttentionDelivered, pendingFailureAttention } from "./failures.ts";
|
|
121
121
|
import { failureAttentionHandled, observeFailures } from "./shared-failure-observations.ts";
|
|
122
122
|
import {
|
|
123
123
|
text,
|
|
@@ -165,6 +165,7 @@ const SUBAGENT_MODE_ACTIONS: readonly AutocompleteItem[] = [
|
|
|
165
165
|
{ value: "mode manual", label: "mode manual", description: "Delegate only when explicitly requested" },
|
|
166
166
|
{ value: "mode adaptive", label: "mode adaptive", description: "Delegate substantial independent work" },
|
|
167
167
|
{ value: "mode coordinator", label: "mode coordinator", description: "Delegate role-owned work by default" },
|
|
168
|
+
{ value: "save", label: "save", description: "Write the current mode as the next session's default" },
|
|
168
169
|
];
|
|
169
170
|
|
|
170
171
|
export function subagentsArgumentCompletions(argumentPrefix: string): AutocompleteItem[] | null {
|
|
@@ -1149,9 +1150,6 @@ function subagentWorkRows(now: number): BackgroundWorkRow[] {
|
|
|
1149
1150
|
if (row.model) bits.push(row.effort ? `${row.model} ${row.effort}` : row.model);
|
|
1150
1151
|
if (row.tool) bits.push(row.tool);
|
|
1151
1152
|
if (row.spend) bits.push(row.spend);
|
|
1152
|
-
const failure = failureView(row.id, metaById.get(row.id)?.cwd ?? "", row.status !== "running" && row.status !== "orphaned");
|
|
1153
|
-
// Only a failure that needs action replaces the row's columns; history stays in the detail view.
|
|
1154
|
-
const firstFailure = failure.actionable ? failure.text.split("\n")[0] || "" : "";
|
|
1155
1153
|
return {
|
|
1156
1154
|
providerId: "subagents",
|
|
1157
1155
|
id: row.id,
|
|
@@ -1164,9 +1162,8 @@ function subagentWorkRows(now: number): BackgroundWorkRow[] {
|
|
|
1164
1162
|
statusTone: statusTone(row.status),
|
|
1165
1163
|
kind: "subagent",
|
|
1166
1164
|
elapsed: row.elapsed,
|
|
1167
|
-
primary:
|
|
1168
|
-
|
|
1169
|
-
facts: [firstFailure, ...row.healthFacts].filter(Boolean).slice(0, 2),
|
|
1165
|
+
primary: bits.join(" · ") || "subagent run",
|
|
1166
|
+
facts: row.healthFacts,
|
|
1170
1167
|
sortStartedAt: metaById.get(row.id)?.startedAt ?? now,
|
|
1171
1168
|
expiresAt: (() => {
|
|
1172
1169
|
const meta = metaById.get(row.id);
|
|
@@ -1179,6 +1176,16 @@ function subagentWorkRows(now: number): BackgroundWorkRow[] {
|
|
|
1179
1176
|
});
|
|
1180
1177
|
}
|
|
1181
1178
|
|
|
1179
|
+
const DELEGATION_MODES = ["manual", "adaptive", "coordinator"] as const;
|
|
1180
|
+
const DELEGATION_BLURB: Record<DelegationMode, string> = {
|
|
1181
|
+
manual: "only when you or a workflow asks",
|
|
1182
|
+
adaptive: "substantial independent work",
|
|
1183
|
+
coordinator: "every role-owned task",
|
|
1184
|
+
};
|
|
1185
|
+
let readDelegation = (): { mode: DelegationMode; source: "session" | "config" } => ({ mode: "adaptive", source: "config" });
|
|
1186
|
+
let cycleDelegation = (): void => {};
|
|
1187
|
+
let saveDelegationDefault: (mode: DelegationMode) => { ok: boolean; message: string } = () => ({ ok: false, message: "Saving the delegation default is unavailable." });
|
|
1188
|
+
|
|
1182
1189
|
function mainAgentWorkRow(now: number): BackgroundWorkRow {
|
|
1183
1190
|
let running = mainAgentStartedAt !== undefined;
|
|
1184
1191
|
try { running ||= uiCtx?.isIdle() === false; } catch { /* use event state */ }
|
|
@@ -1188,10 +1195,12 @@ function mainAgentWorkRow(now: number): BackgroundWorkRow {
|
|
|
1188
1195
|
let contextTokens: number | null | undefined;
|
|
1189
1196
|
try { contextTokens = uiCtx?.getContextUsage()?.tokens; } catch { contextTokens = undefined; }
|
|
1190
1197
|
const tokens = typeof contextTokens === "number" ? `${fmtTokens(contextTokens)} tok` : undefined;
|
|
1198
|
+
const delegation = readDelegation();
|
|
1191
1199
|
const bits = [
|
|
1192
1200
|
effort ? `${model} ${effort}` : model,
|
|
1193
|
-
tool
|
|
1201
|
+
tool,
|
|
1194
1202
|
tokens,
|
|
1203
|
+
delegation.source === "session" ? `${delegation.mode} · session` : delegation.mode,
|
|
1195
1204
|
].filter((bit): bit is string => Boolean(bit));
|
|
1196
1205
|
return {
|
|
1197
1206
|
providerId: "subagents",
|
|
@@ -1210,15 +1219,43 @@ function mainAgentWorkRow(now: number): BackgroundWorkRow {
|
|
|
1210
1219
|
};
|
|
1211
1220
|
}
|
|
1212
1221
|
|
|
1222
|
+
function mainAgentWorkDetail(now: number): BackgroundWorkDetail {
|
|
1223
|
+
const row = mainAgentWorkRow(now);
|
|
1224
|
+
const delegation = readDelegation();
|
|
1225
|
+
return {
|
|
1226
|
+
providerId: "subagents",
|
|
1227
|
+
id: "main",
|
|
1228
|
+
title: "main",
|
|
1229
|
+
status: row.status,
|
|
1230
|
+
statusTone: row.statusTone,
|
|
1231
|
+
subtitle: row.primary,
|
|
1232
|
+
metadata: [
|
|
1233
|
+
{ label: "kind", value: "main agent" },
|
|
1234
|
+
{ label: "model", value: row.effort ? `${row.model} · effort ${row.effort}` : (row.model || "-") },
|
|
1235
|
+
{ label: "tool", value: row.tool || "-" },
|
|
1236
|
+
{ label: "context", value: row.tokens || "-" },
|
|
1237
|
+
{ label: "elapsed", value: row.elapsed },
|
|
1238
|
+
{ label: "mode", value: `${delegation.mode} · ${delegation.source}` },
|
|
1239
|
+
],
|
|
1240
|
+
evidence: {
|
|
1241
|
+
label: "delegation",
|
|
1242
|
+
text: [
|
|
1243
|
+
`${delegation.mode}: ${DELEGATION_BLURB[delegation.mode]}.`,
|
|
1244
|
+
"m cycles this session. ctrl+s saves that mode as the next session's default.",
|
|
1245
|
+
"x does not stop the main agent.",
|
|
1246
|
+
].filter(Boolean).join(" "),
|
|
1247
|
+
},
|
|
1248
|
+
footerActions: ["m mode", "ctrl+s save"],
|
|
1249
|
+
};
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1213
1252
|
function subagentWorkDetail(id: string, now: number, options?: { logTailLines?: number }): BackgroundWorkDetail | null {
|
|
1253
|
+
if (id === "main") return mainAgentWorkDetail(now);
|
|
1214
1254
|
const detail = navigatorDetail(id, now);
|
|
1215
1255
|
if (!detail) return null;
|
|
1216
1256
|
void options;
|
|
1217
1257
|
const transcript = readRunTranscript(id);
|
|
1218
|
-
const view = failureView(id, readMeta(id)?.cwd ?? "", detail.status !== "running" && detail.status !== "orphaned");
|
|
1219
|
-
const failure = view.text;
|
|
1220
1258
|
const metadata = [
|
|
1221
|
-
...(failure ? [{ label: "failure", value: failure.split("\n")[0]! }] : []),
|
|
1222
1259
|
{ label: "provider", value: "Subagents" },
|
|
1223
1260
|
{ label: "id", value: detail.id },
|
|
1224
1261
|
...(detail.role ? [{ label: "role", value: String(detail.role) }] : []),
|
|
@@ -1235,11 +1272,11 @@ function subagentWorkDetail(id: string, now: number, options?: { logTailLines?:
|
|
|
1235
1272
|
title: detail.name || detail.id,
|
|
1236
1273
|
status: detail.status,
|
|
1237
1274
|
statusTone: statusTone(detail.status),
|
|
1238
|
-
subtitle:
|
|
1275
|
+
subtitle: detail.currentTool || undefined,
|
|
1239
1276
|
metadata,
|
|
1240
|
-
evidence: { label: "transcript", text:
|
|
1277
|
+
evidence: { label: "transcript", text: detail.output || "(no transcript yet)" },
|
|
1241
1278
|
transcript: transcript.entries,
|
|
1242
|
-
transcriptDiagnostic:
|
|
1279
|
+
transcriptDiagnostic: transcript.diagnostic,
|
|
1243
1280
|
footerActions: [detail.status === "running" || detail.status === "orphaned" ? "x stop" : "x dismiss"],
|
|
1244
1281
|
};
|
|
1245
1282
|
}
|
|
@@ -1307,6 +1344,16 @@ function ensureSubagentProvider(): void {
|
|
|
1307
1344
|
parentRow: (now) => mainAgentWorkRow(now),
|
|
1308
1345
|
listRows: (now) => subagentWorkRows(now),
|
|
1309
1346
|
detail: (id, now, options) => subagentWorkDetail(id, now, options),
|
|
1347
|
+
handleDetailInput: (id, data) => {
|
|
1348
|
+
if (id !== "main") return false;
|
|
1349
|
+
if (data === "m") {
|
|
1350
|
+
cycleDelegation();
|
|
1351
|
+
return true;
|
|
1352
|
+
}
|
|
1353
|
+
if (data !== "ctrl+s") return false;
|
|
1354
|
+
saveDelegationDefault(readDelegation().mode);
|
|
1355
|
+
return true;
|
|
1356
|
+
},
|
|
1310
1357
|
armCloseLabel: (row) => row.status === "running" || row.status === "orphaned" ? "x again to stop" : "x again to dismiss",
|
|
1311
1358
|
close: (id) => {
|
|
1312
1359
|
const outcome = navigatorCloseRun(id) as { action: string; id: string; status?: string };
|
|
@@ -1450,6 +1497,26 @@ function catalogRoleSchema(purpose: string) {
|
|
|
1450
1497
|
export default function (pi: ExtensionAPI) {
|
|
1451
1498
|
let delegationOverride: DelegationMode | undefined;
|
|
1452
1499
|
const activeDelegationMode = (): DelegationMode => delegationOverride ?? normalizeDelegationMode(loadConfig().delegationMode);
|
|
1500
|
+
readDelegation = () => ({
|
|
1501
|
+
mode: activeDelegationMode(),
|
|
1502
|
+
source: delegationOverride ? "session" : "config",
|
|
1503
|
+
});
|
|
1504
|
+
cycleDelegation = () => {
|
|
1505
|
+
const current = activeDelegationMode();
|
|
1506
|
+
const next = DELEGATION_MODES[(DELEGATION_MODES.indexOf(current) + 1) % DELEGATION_MODES.length]!;
|
|
1507
|
+
delegationOverride = next;
|
|
1508
|
+
pi.appendEntry("pi-better-subagents-delegation", { version: 1, mode: next });
|
|
1509
|
+
};
|
|
1510
|
+
saveDelegationDefault = (mode) => {
|
|
1511
|
+
try {
|
|
1512
|
+
writeDelegationMode(mode);
|
|
1513
|
+
} catch (error) {
|
|
1514
|
+
return { ok: false, message: `Could not save the delegation default: ${error instanceof Error ? error.message : String(error)}` };
|
|
1515
|
+
}
|
|
1516
|
+
delegationOverride = undefined;
|
|
1517
|
+
pi.appendEntry("pi-better-subagents-delegation", { version: 1, mode: null });
|
|
1518
|
+
return { ok: true, message: `Delegation default saved: ${mode}.` };
|
|
1519
|
+
};
|
|
1453
1520
|
const restoreDelegationMode = (ctx: ExtensionContext): void => {
|
|
1454
1521
|
delegationOverride = undefined;
|
|
1455
1522
|
const branch = ctx.sessionManager?.getBranch?.();
|
|
@@ -1457,7 +1524,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
1457
1524
|
for (const entry of branch) {
|
|
1458
1525
|
if (entry.type !== "custom" || entry.customType !== "pi-better-subagents-delegation") continue;
|
|
1459
1526
|
const data = entry.data as { version?: unknown; mode?: unknown } | null;
|
|
1460
|
-
if (data?.version
|
|
1527
|
+
if (data?.version !== 1) continue;
|
|
1528
|
+
if (data.mode === null) delegationOverride = undefined;
|
|
1529
|
+
else if (isDelegationMode(data.mode)) delegationOverride = data.mode;
|
|
1461
1530
|
}
|
|
1462
1531
|
};
|
|
1463
1532
|
const unsubscribeDelegationRequest = pi.events?.on?.(DELEGATION_MODE_REQUEST, (data: unknown) => {
|
|
@@ -2166,7 +2235,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
2166
2235
|
if (typeof pi.registerCommand === "function") {
|
|
2167
2236
|
agentOperations.registerCommands(pi);
|
|
2168
2237
|
pi.registerCommand("subagents", {
|
|
2169
|
-
description: "
|
|
2238
|
+
description: "[mode manual|adaptive|coordinator|save] — Session mode, or save it as the next session's default",
|
|
2170
2239
|
getArgumentCompletions: subagentsArgumentCompletions,
|
|
2171
2240
|
async handler(args, ctx) {
|
|
2172
2241
|
const tokens = args.trim().split(/\s+/).filter(Boolean);
|
|
@@ -2175,12 +2244,20 @@ export default function (pi: ExtensionAPI) {
|
|
|
2175
2244
|
return;
|
|
2176
2245
|
}
|
|
2177
2246
|
const requested = tokens[1];
|
|
2178
|
-
if (tokens.length ===
|
|
2247
|
+
if (tokens.length === 1 && tokens[0] === "save") {
|
|
2248
|
+
const mode = activeDelegationMode();
|
|
2249
|
+
if (ctx.hasUI && !(await ctx.ui.confirm("Save delegation default?", `Future sessions will start in ${mode}.`))) {
|
|
2250
|
+
ctx.ui.notify("Delegation default unchanged.", "info");
|
|
2251
|
+
return;
|
|
2252
|
+
}
|
|
2253
|
+
const saved = saveDelegationDefault(mode);
|
|
2254
|
+
ctx.ui.notify(saved.message, saved.ok ? "info" : "warning");
|
|
2255
|
+
} else if (tokens.length === 2 && tokens[0] === "mode" && isDelegationMode(requested)) {
|
|
2179
2256
|
delegationOverride = requested;
|
|
2180
2257
|
pi.appendEntry("pi-better-subagents-delegation", { version: 1, mode: delegationOverride });
|
|
2181
2258
|
ctx.ui.notify(`Delegation mode: ${delegationOverride} (current session).`, "info");
|
|
2182
2259
|
} else {
|
|
2183
|
-
ctx.ui.notify("Usage: /subagents [mode manual|adaptive|coordinator]", "warning");
|
|
2260
|
+
ctx.ui.notify("Usage: /subagents [mode manual|adaptive|coordinator|save]", "warning");
|
|
2184
2261
|
}
|
|
2185
2262
|
},
|
|
2186
2263
|
});
|
|
@@ -543,7 +543,6 @@ export function buildDetailLines(detail, opts = {}) {
|
|
|
543
543
|
const bits = [];
|
|
544
544
|
if (c.last.reason) bits.push(c.last.reason);
|
|
545
545
|
if (c.last.aborted) bits.push("aborted");
|
|
546
|
-
if (c.last.errorMessage) bits.push(c.last.errorMessage);
|
|
547
546
|
if (bits.length) lines.push(` last ${bits.join(" · ")}`);
|
|
548
547
|
}
|
|
549
548
|
} else {
|
|
@@ -566,28 +565,17 @@ export function buildDetailLines(detail, opts = {}) {
|
|
|
566
565
|
lines.push(" idle");
|
|
567
566
|
}
|
|
568
567
|
|
|
569
|
-
// Model
|
|
568
|
+
// Model activity only; incident summaries stay in result tools and logs.
|
|
570
569
|
lines.push(sectionRule("model", width));
|
|
571
570
|
if (h?.model) {
|
|
572
571
|
const m = h.model;
|
|
573
|
-
lines.push(` state ${m.state}`);
|
|
574
|
-
if (m.listWarning) lines.push(` warning ${m.listWarning}`);
|
|
572
|
+
if (m.state !== "error") lines.push(` state ${m.state}`);
|
|
573
|
+
if (m.state !== "error" && m.listWarning) lines.push(` warning ${m.listWarning}`);
|
|
575
574
|
if (m.retry) {
|
|
576
575
|
const a = m.retry.attempt != null ? String(m.retry.attempt) : "?";
|
|
577
576
|
const max = m.retry.maxAttempts != null ? String(m.retry.maxAttempts) : "?";
|
|
578
577
|
lines.push(` retry ${a}/${max}`);
|
|
579
578
|
}
|
|
580
|
-
if (m.lastError) {
|
|
581
|
-
const age = m.lastError.at != null ? ` · ${fmtDetailTs(m.lastError.at, now)}` : "";
|
|
582
|
-
lines.push(` last error ${m.lastError.message}${age}`);
|
|
583
|
-
}
|
|
584
|
-
if (Array.isArray(m.errorHistory) && m.errorHistory.length > 0) {
|
|
585
|
-
const recent = m.errorHistory.slice(-3);
|
|
586
|
-
for (const e of recent) {
|
|
587
|
-
const age = e.at != null ? ` · ${fmtDetailTs(e.at, now)}` : "";
|
|
588
|
-
lines.push(` history ${e.message}${age}`);
|
|
589
|
-
}
|
|
590
|
-
}
|
|
591
579
|
if (m.longModelCall) {
|
|
592
580
|
const age = m.longModelCall.ageMs != null ? ` · ${fmtDetailAge(m.longModelCall.ageMs)}` : "";
|
|
593
581
|
lines.push(` long call${age}`);
|
|
@@ -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) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-better-harness",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.0",
|
|
4
4
|
"description": "Pi extension bundle for a write sandbox, subagents, background tasks, SSH, goals, and structured plans.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -52,12 +52,12 @@
|
|
|
52
52
|
"test": "node --test test/*.test.mjs"
|
|
53
53
|
},
|
|
54
54
|
"dependencies": {
|
|
55
|
-
"pi-better-background-tasks": "0.6.
|
|
55
|
+
"pi-better-background-tasks": "0.6.3",
|
|
56
56
|
"pi-better-goal": "0.6.0",
|
|
57
57
|
"pi-better-plan": "0.5.1",
|
|
58
|
-
"pi-better-sandbox": "0.8.
|
|
58
|
+
"pi-better-sandbox": "0.8.1",
|
|
59
59
|
"pi-better-ssh": "0.1.1",
|
|
60
|
-
"pi-better-subagents": "0.
|
|
60
|
+
"pi-better-subagents": "0.11.0",
|
|
61
61
|
"smol-toml": "1.9.0",
|
|
62
62
|
"yaml": "^2.9.1"
|
|
63
63
|
},
|