pi-better-subagents 0.1.16 → 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/index.ts +77 -0
- package/package.json +1 -1
- package/shared-navigator.ts +91 -1
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
package/shared-navigator.ts
CHANGED
|
@@ -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
|
-
|
|
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];
|