pi-better-subagents 0.10.0 → 0.11.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 +4 -1
- package/agent-commands.ts +1 -1
- package/config.ts +29 -4
- package/docs/usage.md +16 -6
- package/health-surface.mjs +1 -0
- package/index.ts +96 -19
- package/navigator.mjs +3 -15
- package/package.json +1 -1
- package/shared-navigator.ts +22 -17
package/README.md
CHANGED
|
@@ -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
|
package/agent-commands.ts
CHANGED
|
@@ -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] ?? "";
|
package/config.ts
CHANGED
|
@@ -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(",");
|
package/docs/usage.md
CHANGED
|
@@ -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
|
|
package/health-surface.mjs
CHANGED
|
@@ -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))) {
|
package/index.ts
CHANGED
|
@@ -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
|
});
|
package/navigator.mjs
CHANGED
|
@@ -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}`);
|
package/package.json
CHANGED
package/shared-navigator.ts
CHANGED
|
@@ -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) {
|