pi-better-subagents 0.1.15 → 0.1.17

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/completion.mjs CHANGED
@@ -16,16 +16,15 @@
16
16
  * - Announces the subagent finished
17
17
  * - Tells the model to call/use subagent_result id="<id>"
18
18
  * - Does NOT contain "--- result ---" or any result payload
19
- * - MAY include label, verdict, stat, and tools list
19
+ * - Includes label, verdict, stat, and lifecycle classification when available
20
+ * - Omits tool history; detailed execution evidence belongs in subagent_result
20
21
  *
21
22
  * @param p.id - The run id
22
23
  * @param p.label - Human-readable label (e.g., "reviewer (abc123)")
23
24
  * @param p.verdict - Status line (e.g., "✓ completed" or "✗ failed (exit 1)")
24
25
  * @param p.stat - Statistics line (e.g., "45s · 1.2k tok · $0.0034")
25
- * @param p.tools - Optional tools used (e.g., "read,bash,web_fetch")
26
26
  */
27
27
  export function formatCallbackTrigger(p) {
28
- const tools = p.tools ? ` ·${p.tools.replace(/\n/, " ")}` : "";
29
28
  const lifecycle = p.lifecycleClassification ? ` · lifecycle ${p.lifecycleClassification}` : "";
30
29
  const announcement = p.incomplete
31
30
  ? "ATTENTION: a background subagent exited unexpectedly before producing a coherent final result."
@@ -33,7 +32,7 @@ export function formatCallbackTrigger(p) {
33
32
  const instruction = p.incomplete
34
33
  ? `Inspect the diagnostic with subagent_result id="${p.id}" before deciding how to continue.`
35
34
  : `Ingest this signal and call subagent_result id="${p.id}" to retrieve the actual result, then use/present it as appropriate.`;
36
- return `${announcement}\nsubagent: ${p.label} · ${p.verdict} · ${p.stat}${tools}${lifecycle}\n\n${instruction}`;
35
+ return `${announcement}\nsubagent: ${p.label} · ${p.verdict} · ${p.stat}${lifecycle}\n\n${instruction}`;
37
36
  }
38
37
 
39
38
  /**
@@ -74,7 +73,6 @@ export function formatCallbackQuiet(p) {
74
73
  * @param p.label - Human-readable label
75
74
  * @param p.verdict - Status line (e.g. "✓ completed")
76
75
  * @param p.stat - Statistics line (e.g. "45s · 1.2k tok")
77
- * @param p.tools - Optional tools list
78
76
  * @param p.callback - Whether to trigger a turn (true) or be quiet (false)
79
77
  * @param p.resultText - The parsed final answer; MUST NOT appear in content
80
78
  */
@@ -86,7 +84,6 @@ export function buildCompletionDelivery(p) {
86
84
  label: p.label,
87
85
  verdict: p.verdict,
88
86
  stat: p.stat,
89
- tools: p.tools,
90
87
  incomplete: p.incomplete,
91
88
  lifecycleClassification: p.lifecycleClassification,
92
89
  }),
package/finalization.ts CHANGED
@@ -69,7 +69,6 @@ export function finalizeRun(
69
69
  const el = fmtElapsed(meta.endedAt - meta.startedAt);
70
70
  const spend = fmtSpend(r.usage);
71
71
  const stat = `${el}${spend ? ` · ${spend}` : ""}`;
72
- const tools = r.toolCalls.length ? r.toolCalls.join(", ") : undefined;
73
72
 
74
73
  // A finished run is no longer in the widget; redraw (and stop the ticker if
75
74
  // it was the last one).
@@ -94,7 +93,6 @@ export function finalizeRun(
94
93
  label,
95
94
  verdict,
96
95
  stat,
97
- tools,
98
96
  callback,
99
97
  incomplete: outcome.incomplete,
100
98
  lifecycleClassification: outcome.classification,
package/index.ts CHANGED
@@ -96,6 +96,7 @@ import {
96
96
  WIDGET_CLEAR,
97
97
  fmtElapsed,
98
98
  fmtSpend,
99
+ fmtTokens,
99
100
  shortModel,
100
101
  isSpendCacheFresh,
101
102
  resolveHealthLogExtraction,
@@ -587,6 +588,8 @@ export function setIdentityProbeForTests(probe: ProcessProbe | undefined): void
587
588
 
588
589
  let unregisterSubagentProvider: (() => void) | undefined;
589
590
  const TERMINAL_NAVIGATOR_RETENTION_MS = 30_000;
591
+ let mainAgentStartedAt: number | undefined;
592
+ const mainAgentTools = new Map<string, string>();
590
593
 
591
594
  /**
592
595
  * Observe health for a navigator row/detail (#69). Reuses the size/mtime-gated
@@ -743,6 +746,37 @@ function subagentWorkRows(now: number): BackgroundWorkRow[] {
743
746
  });
744
747
  }
745
748
 
749
+ function mainAgentWorkRow(now: number): BackgroundWorkRow {
750
+ let running = mainAgentStartedAt !== undefined;
751
+ try { running ||= uiCtx?.isIdle() === false; } catch { /* use event state */ }
752
+ const model = shortModel(uiCtx?.model?.id);
753
+ const effort = uiCtx?.thinkingLevel;
754
+ const tool = [...mainAgentTools.values()].at(-1);
755
+ let contextTokens: number | null | undefined;
756
+ try { contextTokens = uiCtx?.getContextUsage()?.tokens; } catch { contextTokens = undefined; }
757
+ const tokens = typeof contextTokens === "number" ? `${fmtTokens(contextTokens)} tok` : undefined;
758
+ const bits = [
759
+ effort ? `${model} ${effort}` : model,
760
+ tool ? `tool ${tool}` : undefined,
761
+ tokens,
762
+ ].filter((bit): bit is string => Boolean(bit));
763
+ return {
764
+ providerId: "subagents",
765
+ id: "main",
766
+ name: "main",
767
+ model,
768
+ effort,
769
+ tool,
770
+ tokens,
771
+ status: running ? "running" : "idle",
772
+ statusTone: running ? "running" : "muted",
773
+ kind: "main agent",
774
+ elapsed: running && mainAgentStartedAt !== undefined ? fmtElapsed(now - mainAgentStartedAt) : "idle",
775
+ primary: bits.join(" · "),
776
+ sortStartedAt: mainAgentStartedAt ?? now,
777
+ };
778
+ }
779
+
746
780
  function subagentWorkDetail(id: string, now: number, options?: { logTailLines?: number }): BackgroundWorkDetail | null {
747
781
  const detail = navigatorDetail(id, now);
748
782
  if (!detail) return null;
@@ -780,6 +814,7 @@ function ensureSubagentProvider(): void {
780
814
  label: "Subagents",
781
815
  priority: 10,
782
816
  visibleCount: () => navigatorRunningCount(),
817
+ parentRow: (now) => mainAgentWorkRow(now),
783
818
  listRows: (now) => subagentWorkRows(now),
784
819
  detail: (id, now, options) => subagentWorkDetail(id, now, options),
785
820
  armCloseLabel: (row) => row.status === "running" || row.status === "orphaned" ? "x again to stop" : "x again to dismiss",
@@ -1322,11 +1357,49 @@ export default function (pi: ExtensionAPI) {
1322
1357
  pi.registerTool(subagentStopTool(Type, { onStopped: renderWidget }));
1323
1358
 
1324
1359
  // ---- live-status lifecycle -----------------------------------------
1360
+ pi.on("agent_start", async (_event, ctx) => {
1361
+ uiCtx = ctx;
1362
+ mainAgentStartedAt = Date.now();
1363
+ mainAgentTools.clear();
1364
+ refreshBackgroundWorkNavigator(ctx);
1365
+ });
1366
+
1367
+ pi.on("tool_execution_start", async (event, ctx) => {
1368
+ mainAgentTools.set(event.toolCallId, event.toolName);
1369
+ refreshBackgroundWorkNavigator(ctx);
1370
+ });
1371
+
1372
+ pi.on("tool_execution_end", async (event, ctx) => {
1373
+ mainAgentTools.delete(event.toolCallId);
1374
+ refreshBackgroundWorkNavigator(ctx);
1375
+ });
1376
+
1377
+ pi.on("message_end", async (event, ctx) => {
1378
+ if (event.message.role === "assistant") refreshBackgroundWorkNavigator(ctx);
1379
+ });
1380
+
1381
+ pi.on("model_select", async (_event, ctx) => {
1382
+ refreshBackgroundWorkNavigator(ctx);
1383
+ });
1384
+
1385
+ pi.on("thinking_level_select", async (_event, ctx) => {
1386
+ refreshBackgroundWorkNavigator(ctx);
1387
+ });
1388
+
1389
+ pi.on("agent_settled", async (_event, ctx) => {
1390
+ mainAgentStartedAt = undefined;
1391
+ mainAgentTools.clear();
1392
+ refreshBackgroundWorkNavigator(ctx);
1393
+ });
1394
+
1325
1395
  // Capture a UI-bearing context and, if runs from a prior session are still
1326
1396
  // alive, resume the ticking widget. Deferred out of the factory per pi's
1327
1397
  // "no background resources at load" rule.
1328
1398
  pi.on("session_start", async (_event, ctx) => {
1329
1399
  uiCtx = ctx;
1400
+ try { mainAgentStartedAt = ctx.isIdle() ? undefined : Date.now(); }
1401
+ catch { mainAgentStartedAt = undefined; }
1402
+ mainAgentTools.clear();
1330
1403
  activeCallbackOrigin = callbackOriginFromContext(ctx);
1331
1404
  // Reload / session switch hardening (#48):
1332
1405
  // - Drop any leftover overlay timers/confirm state from a prior session
@@ -1359,6 +1432,8 @@ export default function (pi: ExtensionAPI) {
1359
1432
 
1360
1433
  pi.on("session_before_switch", () => {
1361
1434
  activeCallbackOrigin = undefined;
1435
+ mainAgentStartedAt = undefined;
1436
+ mainAgentTools.clear();
1362
1437
  disposeBackgroundWorkNavigator();
1363
1438
  });
1364
1439
 
@@ -1369,6 +1444,8 @@ export default function (pi: ExtensionAPI) {
1369
1444
  // cannot become live process groups with no coordinator.
1370
1445
  stopCurrentSessionSubagents(ctx);
1371
1446
  activeCallbackOrigin = undefined;
1447
+ mainAgentStartedAt = undefined;
1448
+ mainAgentTools.clear();
1372
1449
  stopTicker();
1373
1450
  stopHealthTicker();
1374
1451
  spendCache.clear();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-better-subagents",
3
- "version": "0.1.15",
3
+ "version": "0.1.17",
4
4
  "description": "Pi extension for detached, sandboxed subagent runs that keep the foreground session free.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -54,6 +54,7 @@ export type BackgroundWorkProvider = {
54
54
  label: string;
55
55
  priority: number;
56
56
  visibleCount(): number;
57
+ parentRow?(now: number): BackgroundWorkRow | null;
57
58
  listRows(now: number): BackgroundWorkRow[];
58
59
  detail(id: string, now: number, options?: { logTailLines?: number }): BackgroundWorkDetail | null;
59
60
  armCloseLabel(row: BackgroundWorkRow): string;
@@ -411,6 +412,16 @@ function buildMainListLines(
411
412
  const group = grouped.get(label)!;
412
413
  if (i > 0) lines.push("");
413
414
  lines.push(providerGroupLabel(label, fg));
415
+ const provider = state().providers.get(group[0]!.providerId);
416
+ let parentRow: BackgroundWorkRow | null = null;
417
+ try { parentRow = provider?.parentRow?.(Date.now()) ?? null; } catch { parentRow = null; }
418
+ if (parentRow) {
419
+ lines.push(formatMainListRow({
420
+ ...parentRow,
421
+ navigatorId: rowKey(parentRow.providerId, parentRow.id),
422
+ providerLabel: label,
423
+ }, false, fg, width));
424
+ }
414
425
  for (const row of group) {
415
426
  const selected = options.focused && row.navigatorId === options.selectedId;
416
427
  lines.push(formatMainListRow(row, selected === true, fg, width));
@@ -976,7 +987,10 @@ function buildDetailLines(
976
987
  } else {
977
988
  const evidenceLabel = /log/i.test(detail.evidence.label) ? `${detail.evidence.label} · latest ${tailRows} rows` : detail.evidence.label;
978
989
  lines.push(dim(section(evidenceLabel, width), fg));
979
- for (const raw of body.split(/\r?\n/)) lines.push(raw ? ` ${raw}` : " ");
990
+ const wrapped = /log/i.test(detail.evidence.label)
991
+ ? wrapLogText(body, width - 6)
992
+ : body.split(/\r?\n/);
993
+ for (const raw of wrapped) lines.push(raw ? ` ${raw}` : " ");
980
994
  }
981
995
  lines.push("");
982
996
  const footerLines = [dim(` ← back · ${actions}`, fg), dim(rule("", width), fg)];
@@ -1042,6 +1056,82 @@ function wrapEvidenceText(text: string, width: number): string[] {
1042
1056
  return rows.length ? rows : ["(no output yet)"];
1043
1057
  }
1044
1058
 
1059
+ /**
1060
+ * Wrap terminal log rows without flattening entries together. Continuations
1061
+ * retain the source indentation, and long paths/JSON tokens are hard-wrapped
1062
+ * so the final width guard never has to discard their suffix.
1063
+ */
1064
+ export function wrapLogText(text: string, width: number): string[] {
1065
+ const max = Math.max(8, Math.floor(width));
1066
+ const rows: string[] = [];
1067
+ for (const source of String(text ?? "").split(/\r?\n/)) {
1068
+ if (!source) {
1069
+ rows.push("");
1070
+ continue;
1071
+ }
1072
+ const sourceIndent = source.match(/^[ \t]*/)?.[0] ?? "";
1073
+ const indent = sourceIndent.replace(/\t/g, " ");
1074
+ let remaining = source.slice(sourceIndent.length);
1075
+ const contentWidth = Math.max(1, max - visibleWidth(indent));
1076
+ if (!remaining) {
1077
+ rows.push(indent);
1078
+ continue;
1079
+ }
1080
+ while (visibleWidth(remaining) > contentWidth) {
1081
+ let [head, tail] = splitVisiblePrefix(remaining, contentWidth);
1082
+ const minSoftBreak = Math.floor(contentWidth * 0.45);
1083
+ const delimiterBreak = lastDelimiterBreak(head);
1084
+ const whitespace = head.match(/\s+\S*$/)?.index;
1085
+ if (delimiterBreak > minSoftBreak) {
1086
+ tail = head.slice(delimiterBreak) + tail;
1087
+ head = head.slice(0, delimiterBreak);
1088
+ } else if (whitespace !== undefined && whitespace > minSoftBreak) {
1089
+ tail = head.slice(whitespace).trimStart() + tail;
1090
+ head = head.slice(0, whitespace).trimEnd();
1091
+ }
1092
+ rows.push(indent + head);
1093
+ remaining = tail.trimStart();
1094
+ }
1095
+ rows.push(indent + remaining);
1096
+ }
1097
+ return rows.length ? rows : [""];
1098
+ }
1099
+
1100
+ function lastDelimiterBreak(value: string): number {
1101
+ let last = -1;
1102
+ for (const match of value.matchAll(/[\/,}:]/g)) last = match.index + 1;
1103
+ return last;
1104
+ }
1105
+
1106
+ function splitVisiblePrefix(value: string, width: number): [string, string] {
1107
+ const max = Math.max(0, Math.floor(width));
1108
+ let visible = 0;
1109
+ let index = 0;
1110
+ while (index < value.length && visible < max) {
1111
+ if (value[index] === "\u001b" || value[index] === "\u009b") {
1112
+ const match = value.slice(index).match(ANSI_RE);
1113
+ if (match?.index === 0) {
1114
+ index += match[0].length;
1115
+ continue;
1116
+ }
1117
+ }
1118
+ if (value[index] === "<") {
1119
+ const close = value.indexOf(">", index);
1120
+ if (close !== -1) {
1121
+ const tag = value.slice(index, close + 1);
1122
+ if (/^<\/?[a-zA-Z][\w-]*>$/.test(tag) || tag === "</>") {
1123
+ index = close + 1;
1124
+ continue;
1125
+ }
1126
+ }
1127
+ }
1128
+ const codePoint = value.codePointAt(index)!;
1129
+ index += codePoint > 0xFFFF ? 2 : 1;
1130
+ visible += 1;
1131
+ }
1132
+ return [value.slice(0, index), value.slice(index)];
1133
+ }
1134
+
1045
1135
  function cycleLogTailRows(current: number): number {
1046
1136
  const idx = LOG_TAIL_ROW_CHOICES.findIndex((value) => value === current);
1047
1137
  return LOG_TAIL_ROW_CHOICES[(idx + 1) % LOG_TAIL_ROW_CHOICES.length];