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 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. Manual delegates only on explicit user or workflow request, even with a
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 Codex agent definitions",
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 { DelegationMode } from "./delegation.ts";
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
- const dir = dirname(fileURLToPath(import.meta.url));
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. Other arguments show usage. `/reload` retains
22
- this override; new sessions use config, while resuming a session restores that
23
- session's override.
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 an informational `main` row above child runs. It
521
- shows the foreground model, effort, active tool, context tokens, and active
522
- elapsed time. It is not selectable and can never become an `x` stop target.
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))) {
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, failureView, markFailureAttentionDelivered, pendingFailureAttention, prependFailureSummary } from "./failures.ts";
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: firstFailure || bits.join(" · ") || "subagent run",
1168
- secondary: firstFailure ? bits.join(" · ") : undefined,
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 ? `tool ${tool}` : undefined,
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: (view.actionable && failure.split("\n")[0]) || (detail.currentTool ? `current tool ${detail.currentTool}` : undefined),
1275
+ subtitle: detail.currentTool || undefined,
1239
1276
  metadata,
1240
- evidence: { label: "transcript", text: prependFailureSummary(detail.output || "(no transcript yet)", failure) },
1277
+ evidence: { label: "transcript", text: detail.output || "(no transcript yet)" },
1241
1278
  transcript: transcript.entries,
1242
- transcriptDiagnostic: failure ? prependFailureSummary(transcript.diagnostic ?? "", failure) : transcript.diagnostic,
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 === 1 && isDelegationMode(data.mode)) delegationOverride = data.mode;
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: "Show or change the current-session delegation mode",
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 === 2 && tokens[0] === "mode" && isDelegationMode(requested)) {
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 call / error, separate from tool state.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-better-subagents",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "description": "Pi extension for detached, sandboxed subagent runs that keep the foreground session free.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -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(`tool ${singleLine(row.tool)}`);
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
- const selected = selectedMainListRow();
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
- let detail: BackgroundWorkDetail | null = detailFor(detailId, Date.now(), { logTailLines: logTailRows }) ?? null;
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
- if (selectedRow()?.parentRow) close();
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
- if (overlayState.rows[idx]!.parentRow) close();
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) transcriptHeader.push(` ${dim(detail.transcriptDiagnostic, fg)}`);
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) {