@mageyuki/opencode-subagent-statusline 0.0.0-stage → 0.7.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/dist/index.js ADDED
@@ -0,0 +1,1505 @@
1
+ // src/reconcile.ts
2
+ var DEFAULT_STALE_RUNNING_THRESHOLD_MS = 10 * 60 * 6e4;
3
+ var RUNNING_SESSION_STATUS_VALUES = /* @__PURE__ */ new Set([
4
+ "busy",
5
+ "running",
6
+ "pending",
7
+ "queued",
8
+ "in_progress",
9
+ "working",
10
+ "compacting",
11
+ "retry"
12
+ ]);
13
+ var DONE_SESSION_STATUS_VALUES = /* @__PURE__ */ new Set([
14
+ "idle",
15
+ "done",
16
+ "completed",
17
+ "complete",
18
+ "success",
19
+ "succeeded"
20
+ ]);
21
+ var ERROR_SESSION_STATUS_VALUES = /* @__PURE__ */ new Set([
22
+ "error",
23
+ "failed",
24
+ "failure",
25
+ "cancelled",
26
+ "canceled",
27
+ "aborted"
28
+ ]);
29
+ function deriveOpenCodeSessionStatus(value) {
30
+ if (hasStructuredErrorEvidence(value)) {
31
+ return "error";
32
+ }
33
+ const values = collectOpenCodeSessionStatusValues(value);
34
+ if (values.some((status) => ERROR_SESSION_STATUS_VALUES.has(status))) {
35
+ return "error";
36
+ }
37
+ if (values.some((status) => RUNNING_SESSION_STATUS_VALUES.has(status))) {
38
+ return "running";
39
+ }
40
+ if (values.some((status) => DONE_SESSION_STATUS_VALUES.has(status))) {
41
+ return "done";
42
+ }
43
+ return void 0;
44
+ }
45
+ function hasStructuredErrorEvidence(value, depth = 0) {
46
+ if (depth > 4) return false;
47
+ const record = asRecord(value);
48
+ if (!record) return false;
49
+ if (record.error) return true;
50
+ for (const nested of Object.values(record)) {
51
+ if (Array.isArray(nested)) {
52
+ if (nested.some((item) => hasStructuredErrorEvidence(item, depth + 1))) {
53
+ return true;
54
+ }
55
+ continue;
56
+ }
57
+ if (hasStructuredErrorEvidence(nested, depth + 1)) return true;
58
+ }
59
+ return false;
60
+ }
61
+ function asRecord(value) {
62
+ return value && typeof value === "object" ? value : void 0;
63
+ }
64
+ function collectOpenCodeSessionStatusValues(value) {
65
+ if (typeof value === "string") {
66
+ const normalized = normalizeStatusValue(value);
67
+ return normalized ? [normalized] : [];
68
+ }
69
+ const record = asRecord(value);
70
+ if (!record) return [];
71
+ const values = [
72
+ normalizeStatusValue(record.type),
73
+ normalizeStatusValue(record.status),
74
+ normalizeStatusValue(record.state),
75
+ normalizeStatusValue(record.phase),
76
+ normalizeStatusValue(record.result)
77
+ ].filter((status) => Boolean(status));
78
+ if (record.error) values.push("error");
79
+ if (record.busy === true || record.running === true) values.push("busy");
80
+ return values;
81
+ }
82
+ function normalizeStatusValue(value) {
83
+ if (typeof value !== "string") return void 0;
84
+ const normalized = value.trim().toLowerCase();
85
+ return normalized.length > 0 ? normalized : void 0;
86
+ }
87
+
88
+ // src/state.ts
89
+ import { randomUUID } from "crypto";
90
+ import { mkdir, readFile, rename, rm, writeFile } from "fs/promises";
91
+ import { basename, dirname, join } from "path";
92
+ import os from "os";
93
+
94
+ // src/internal-policy.ts
95
+ var TERMINAL_CHILD_TTL_MS = 3 * 24 * 60 * 60 * 1e3;
96
+ var MAX_TERMINAL_CHILDREN = 1500;
97
+ var MAX_SYNC_LOG_READ_BYTES = 1024 * 1024;
98
+
99
+ // src/subagent-classification.ts
100
+ function isRealSessionID(value) {
101
+ return typeof value === "string" && value.startsWith("ses_");
102
+ }
103
+ function isTrustedTargetSessionID(value) {
104
+ return isRealSessionID(value);
105
+ }
106
+ function trustedTargetSessionID(item) {
107
+ return isTrustedTargetSessionID(item.targetSessionID) ? item.targetSessionID : void 0;
108
+ }
109
+ function isRealExecution(item) {
110
+ return item.source === "session" || isRealSessionID(item.id);
111
+ }
112
+ function realExecutionID(item) {
113
+ return trustedTargetSessionID(item) ?? item.id;
114
+ }
115
+ function classifySubagentWorkItem(item) {
116
+ if (isRealExecution(item)) {
117
+ const executionID = realExecutionID(item);
118
+ return {
119
+ kind: "real-execution",
120
+ executionID,
121
+ targetSessionID: executionID
122
+ };
123
+ }
124
+ const targetSessionID = trustedTargetSessionID(item);
125
+ if (targetSessionID) {
126
+ return {
127
+ kind: "execution-proxy",
128
+ executionID: targetSessionID,
129
+ targetSessionID
130
+ };
131
+ }
132
+ return { kind: "invocation-wrapper" };
133
+ }
134
+ function uniqueExecutionID(candidates) {
135
+ const executionIDs = new Set(candidates.map((item) => realExecutionID(item)));
136
+ return executionIDs.size === 1 ? [...executionIDs][0] : void 0;
137
+ }
138
+ function realExecutions(items) {
139
+ return items.filter((item) => classifySubagentWorkItem(item).kind === "real-execution");
140
+ }
141
+ function resolveTrustedTargetExecutionID(item, realItems) {
142
+ const targetSessionID = trustedTargetSessionID(item);
143
+ if (!targetSessionID) return void 0;
144
+ return realExecutions(realItems).some(
145
+ (realItem) => realExecutionID(realItem) === targetSessionID
146
+ ) ? targetSessionID : void 0;
147
+ }
148
+ function resolveSharedMessageExecutionID(item, realItems) {
149
+ if (!item.messageID) return void 0;
150
+ return uniqueExecutionID(
151
+ realExecutions(realItems).filter(
152
+ (realItem) => realItem.parentID === item.parentID && realItem.messageID === item.messageID
153
+ )
154
+ );
155
+ }
156
+ function resolveUniqueSameParentExecutionID(item, realItems) {
157
+ return uniqueExecutionID(
158
+ realExecutions(realItems).filter(
159
+ (realItem) => realItem.parentID === item.parentID
160
+ )
161
+ );
162
+ }
163
+ function resolveCorrelatedExecutionID(item, realItems) {
164
+ if (trustedTargetSessionID(item)) {
165
+ return resolveTrustedTargetExecutionID(item, realItems);
166
+ }
167
+ return resolveSharedMessageExecutionID(item, realItems) ?? resolveUniqueSameParentExecutionID(item, realItems);
168
+ }
169
+ function correlateSubagentWorkItems(items) {
170
+ const realItems = realExecutions(items);
171
+ const executions = /* @__PURE__ */ new Map();
172
+ for (const item of realItems) {
173
+ const executionID = realExecutionID(item);
174
+ if (!executions.has(executionID)) {
175
+ executions.set(executionID, { executionID, real: item, proxies: [] });
176
+ }
177
+ }
178
+ for (const item of items) {
179
+ if (classifySubagentWorkItem(item).kind === "real-execution") continue;
180
+ const executionID = resolveCorrelatedExecutionID(item, realItems);
181
+ if (!executionID) continue;
182
+ executions.get(executionID)?.proxies.push(item);
183
+ }
184
+ return [...executions.values()];
185
+ }
186
+ function mergeProxyMetadataWithRealExecution(real, proxy) {
187
+ const executionID = realExecutionID(real);
188
+ return {
189
+ ...real,
190
+ title: proxy.title ?? real.title,
191
+ summary: proxy.summary ?? real.summary,
192
+ agentName: proxy.agentName ?? real.agentName,
193
+ messageID: real.messageID ?? proxy.messageID,
194
+ id: real.id,
195
+ parentID: real.parentID,
196
+ source: "session",
197
+ targetSessionID: real.targetSessionID ?? executionID,
198
+ status: real.status,
199
+ color: real.color,
200
+ startedAt: real.startedAt,
201
+ updatedAt: real.updatedAt,
202
+ endedAt: real.endedAt,
203
+ elapsedMs: real.elapsedMs,
204
+ tokens: real.tokens,
205
+ model: real.model
206
+ };
207
+ }
208
+
209
+ // src/state.ts
210
+ function statusColor(status) {
211
+ if (status === "done") return "green";
212
+ if (status === "error") return "red";
213
+ return "yellow";
214
+ }
215
+ function safeTimestamp(input, fallback) {
216
+ if (typeof input !== "string") return fallback;
217
+ return Number.isNaN(Date.parse(input)) ? fallback : input;
218
+ }
219
+ function toFiniteNumber(value) {
220
+ if (typeof value === "number" && Number.isFinite(value)) {
221
+ return value;
222
+ }
223
+ if (typeof value === "string" && value.trim().length > 0) {
224
+ const parsed = Number(value);
225
+ return Number.isFinite(parsed) ? parsed : void 0;
226
+ }
227
+ return void 0;
228
+ }
229
+ function toNonNegativeInteger(value) {
230
+ const parsed = toFiniteNumber(value);
231
+ if (parsed === void 0) return void 0;
232
+ return Math.max(0, Math.floor(parsed));
233
+ }
234
+ function sanitizeCountedChildIDs(input) {
235
+ if (!input || typeof input !== "object") return {};
236
+ const counted = {};
237
+ for (const [id, value] of Object.entries(input)) {
238
+ if (!id) continue;
239
+ if (value === true) {
240
+ counted[id] = true;
241
+ }
242
+ }
243
+ return counted;
244
+ }
245
+ function normalizeExecutionCounters(state) {
246
+ state.countedChildIDs = sanitizeCountedChildIDs(state.countedChildIDs);
247
+ const countedTotal = Object.keys(state.countedChildIDs).length;
248
+ state.totalExecuted = Math.max(
249
+ toNonNegativeInteger(state.totalExecuted) ?? 0,
250
+ countedTotal
251
+ );
252
+ }
253
+ function resolveExecutionCountIdentity(child) {
254
+ const classification = classifySubagentWorkItem(child);
255
+ return classification.kind === "real-execution" ? classification.executionID : void 0;
256
+ }
257
+ function countRetainedSubagentStatuses(input) {
258
+ const children = Array.isArray(input.children) ? input.children : Object.values(input.children);
259
+ const scopedChildren = input.parentSessionID ? children.filter((child) => child.parentID === input.parentSessionID) : children;
260
+ const counts = { running: 0, done: 0, error: 0 };
261
+ for (const { real } of correlateSubagentWorkItems(scopedChildren)) {
262
+ counts[real.status] += 1;
263
+ }
264
+ return counts;
265
+ }
266
+ function reconcileCountedExecutionsWithChildren(state) {
267
+ const executionIDs = correlateSubagentWorkItems(
268
+ Object.values(state.children)
269
+ ).map((execution) => execution.executionID);
270
+ state.countedChildIDs = Object.fromEntries(
271
+ executionIDs.map((id) => [id, true])
272
+ );
273
+ state.totalExecuted = executionIDs.length;
274
+ }
275
+ function countChildExecution(state, child) {
276
+ normalizeExecutionCounters(state);
277
+ const countIdentity = resolveExecutionCountIdentity(child);
278
+ if (!countIdentity) return false;
279
+ if (state.countedChildIDs[countIdentity]) return false;
280
+ const previousTotal = Math.max(
281
+ toNonNegativeInteger(state.totalExecuted) ?? 0,
282
+ Object.keys(state.countedChildIDs).length
283
+ );
284
+ state.countedChildIDs[countIdentity] = true;
285
+ state.totalExecuted = previousTotal + 1;
286
+ return true;
287
+ }
288
+ function sanitizeTokens(input) {
289
+ if (!input || typeof input !== "object") return void 0;
290
+ const raw = input;
291
+ const tokens = {
292
+ input: toFiniteNumber(raw.input),
293
+ output: toFiniteNumber(raw.output),
294
+ total: toFiniteNumber(raw.total),
295
+ contextPercent: toFiniteNumber(raw.contextPercent)
296
+ };
297
+ if (tokens.input === void 0 && tokens.output === void 0 && tokens.total === void 0 && tokens.contextPercent === void 0) {
298
+ return void 0;
299
+ }
300
+ return tokens;
301
+ }
302
+ function sanitizeModel(input) {
303
+ if (!input || typeof input !== "object") return void 0;
304
+ const raw = input;
305
+ const providerID = typeof raw.providerID === "string" ? raw.providerID.trim() : "";
306
+ const modelID = typeof raw.modelID === "string" ? raw.modelID.trim() : "";
307
+ const variant = typeof raw.variant === "string" ? raw.variant.trim() : "";
308
+ if (!providerID || !modelID) return void 0;
309
+ return { providerID, modelID, ...variant ? { variant } : {} };
310
+ }
311
+ function sanitizeTargetSessionID(value, fallback) {
312
+ if (typeof value === "string" && value.startsWith("ses_")) {
313
+ return value;
314
+ }
315
+ if (typeof fallback === "string" && fallback.startsWith("ses_")) {
316
+ return fallback;
317
+ }
318
+ return void 0;
319
+ }
320
+ function mergeTokens(existing, incoming) {
321
+ if (!existing && !incoming) return void 0;
322
+ return {
323
+ input: incoming?.input ?? existing?.input,
324
+ output: incoming?.output ?? existing?.output,
325
+ total: incoming?.total ?? existing?.total,
326
+ contextPercent: incoming?.contextPercent ?? existing?.contextPercent
327
+ };
328
+ }
329
+ function sameTokens(left, right) {
330
+ return JSON.stringify(left) === JSON.stringify(right);
331
+ }
332
+ function normalizeComparableText(value) {
333
+ return value.replace(/\s+/g, " ").trim().toLowerCase();
334
+ }
335
+ function sanitizeSummary(value, title) {
336
+ if (typeof value !== "string") return void 0;
337
+ const summary = value.replace(/\s+/g, " ").trim();
338
+ if (!summary) return void 0;
339
+ if (normalizeComparableText(summary) === normalizeComparableText(title)) {
340
+ return void 0;
341
+ }
342
+ return summary;
343
+ }
344
+ function sanitizeAgentName(value) {
345
+ if (typeof value !== "string") return void 0;
346
+ const agentName = value.replace(/^\((.*)\)$/, "$1").replace(/\s+/g, " ").trim();
347
+ return agentName || void 0;
348
+ }
349
+ function resolveElapsedMs(child, nowMs) {
350
+ const startedMs = Date.parse(child.startedAt);
351
+ if (Number.isNaN(startedMs)) return 0;
352
+ const endSource = child.endedAt ?? child.updatedAt;
353
+ const endMs = child.endedAt ? Date.parse(endSource) : nowMs;
354
+ if (Number.isNaN(endMs)) return 0;
355
+ return Math.max(0, endMs - startedMs);
356
+ }
357
+ function terminalReferenceMs(child) {
358
+ const parsed = Date.parse(
359
+ child.endedAt ?? child.updatedAt ?? child.startedAt
360
+ );
361
+ return Number.isNaN(parsed) ? 0 : parsed;
362
+ }
363
+ function pruneTerminalChildren(state, now = /* @__PURE__ */ new Date()) {
364
+ const nowMs = now.getTime();
365
+ const terminalChildren = [];
366
+ let pruned = 0;
367
+ for (const child of Object.values(state.children)) {
368
+ if (child.status === "running") continue;
369
+ const referenceMs = terminalReferenceMs(child);
370
+ if (nowMs - referenceMs > TERMINAL_CHILD_TTL_MS) {
371
+ delete state.children[child.id];
372
+ pruned += 1;
373
+ continue;
374
+ }
375
+ terminalChildren.push({ id: child.id, referenceMs });
376
+ }
377
+ if (terminalChildren.length <= MAX_TERMINAL_CHILDREN) {
378
+ return pruned;
379
+ }
380
+ terminalChildren.sort(
381
+ (a, b) => b.referenceMs - a.referenceMs || a.id.localeCompare(b.id)
382
+ );
383
+ for (const child of terminalChildren.slice(MAX_TERMINAL_CHILDREN)) {
384
+ delete state.children[child.id];
385
+ pruned += 1;
386
+ }
387
+ return pruned;
388
+ }
389
+ function refreshDerivedFields(state, now = /* @__PURE__ */ new Date()) {
390
+ const nowISO = now.toISOString();
391
+ const nowMs = now.getTime();
392
+ normalizeExecutionCounters(state);
393
+ for (const [id, child] of Object.entries(state.children)) {
394
+ const startedAt = safeTimestamp(child.startedAt, nowISO);
395
+ const updatedAt = safeTimestamp(child.updatedAt, nowISO);
396
+ const endedAt = child.endedAt ? safeTimestamp(child.endedAt, updatedAt) : void 0;
397
+ const status = child.status === "done" || child.status === "error" || child.status === "running" ? child.status : "running";
398
+ const targetSessionID = sanitizeTargetSessionID(
399
+ child.targetSessionID,
400
+ id.startsWith("ses_") ? id : void 0
401
+ );
402
+ state.children[id] = {
403
+ ...child,
404
+ startedAt,
405
+ updatedAt,
406
+ endedAt,
407
+ status,
408
+ targetSessionID,
409
+ color: statusColor(status),
410
+ tokens: sanitizeTokens(child.tokens),
411
+ model: sanitizeModel(child.model),
412
+ elapsedMs: resolveElapsedMs(
413
+ {
414
+ ...child,
415
+ startedAt,
416
+ updatedAt,
417
+ endedAt,
418
+ status,
419
+ color: statusColor(status)
420
+ },
421
+ nowMs
422
+ )
423
+ };
424
+ }
425
+ reconcileCountedExecutionsWithChildren(state);
426
+ state.updatedAt = safeTimestamp(state.updatedAt, nowISO);
427
+ if (pruneTerminalChildren(state, now) > 0) {
428
+ reconcileCountedExecutionsWithChildren(state);
429
+ state.updatedAt = nowISO;
430
+ }
431
+ }
432
+ var STATUS_DIRNAME = "opencode-subagent-statusline";
433
+ var STATUS_FILENAME = "state.json";
434
+ var STATUS_DIR_MODE = 448;
435
+ var STATUS_FILE_MODE = 384;
436
+ function sanitizeInstanceName(input) {
437
+ return input.replace(/[^A-Za-z0-9._-]/g, "_");
438
+ }
439
+ function resolveDefaultInstanceName() {
440
+ const fromEnv = process.env.OPENCODE_SUBAGENT_STATUSLINE_INSTANCE;
441
+ if (typeof fromEnv === "string" && fromEnv.trim().length > 0) {
442
+ const safe = sanitizeInstanceName(fromEnv);
443
+ if (safe.length > 0) {
444
+ return safe;
445
+ }
446
+ }
447
+ return `pid-${process.pid}`;
448
+ }
449
+ function shouldPreserveStateOnStartup() {
450
+ return process.env.OPENCODE_SUBAGENT_STATUSLINE_PRESERVE_STATE === "1";
451
+ }
452
+ function createEmptyState() {
453
+ return {
454
+ children: {},
455
+ countedChildIDs: {},
456
+ totalExecuted: 0,
457
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
458
+ };
459
+ }
460
+ function resolveStatePath() {
461
+ const fromEnv = process.env.OPENCODE_SUBAGENT_STATUSLINE_STATE;
462
+ if (typeof fromEnv === "string" && fromEnv.trim().length > 0) {
463
+ return fromEnv;
464
+ }
465
+ const runtimeDir = process.env.XDG_RUNTIME_DIR ?? os.tmpdir();
466
+ const instance = resolveDefaultInstanceName();
467
+ return join(runtimeDir, STATUS_DIRNAME, instance, STATUS_FILENAME);
468
+ }
469
+ function resolveTextPath(statePath) {
470
+ return join(dirname(statePath), "status.txt");
471
+ }
472
+ async function loadState(statePath) {
473
+ try {
474
+ const raw = await readFile(statePath, "utf8");
475
+ const parsed = JSON.parse(raw);
476
+ if (!parsed || typeof parsed !== "object") {
477
+ return createEmptyState();
478
+ }
479
+ const children = parsed.children && typeof parsed.children === "object" ? parsed.children : {};
480
+ const countedChildIDs = sanitizeCountedChildIDs(parsed.countedChildIDs);
481
+ const state = {
482
+ children,
483
+ countedChildIDs,
484
+ totalExecuted: Math.max(
485
+ toNonNegativeInteger(parsed.totalExecuted) ?? 0,
486
+ Object.keys(countedChildIDs).length
487
+ ),
488
+ updatedAt: typeof parsed.updatedAt === "string" ? parsed.updatedAt : (/* @__PURE__ */ new Date()).toISOString()
489
+ };
490
+ for (const [id, child] of Object.entries(children)) {
491
+ const candidate = child;
492
+ if (typeof candidate.title !== "string" || typeof candidate.parentID !== "string") {
493
+ continue;
494
+ }
495
+ const targetSessionID = sanitizeTargetSessionID(
496
+ candidate.targetSessionID,
497
+ id.startsWith("ses_") ? id : void 0
498
+ );
499
+ const countIdentity = resolveExecutionCountIdentity({
500
+ id,
501
+ title: candidate.title,
502
+ parentID: candidate.parentID,
503
+ messageID: candidate.messageID,
504
+ source: candidate.source,
505
+ targetSessionID
506
+ });
507
+ if (countIdentity) {
508
+ state.countedChildIDs[countIdentity] = true;
509
+ }
510
+ }
511
+ reconcileCountedExecutionsWithChildren(state);
512
+ refreshDerivedFields(state);
513
+ return state;
514
+ } catch {
515
+ return createEmptyState();
516
+ }
517
+ }
518
+ async function writeLocalStatusFile(path, contents, options) {
519
+ if (options?.shouldCommit?.() === false) return;
520
+ const directory = dirname(path);
521
+ await mkdir(directory, { recursive: true, mode: STATUS_DIR_MODE });
522
+ const tempPath = join(
523
+ directory,
524
+ `.${basename(path)}.${process.pid}.${Date.now()}.${randomUUID()}.tmp`
525
+ );
526
+ try {
527
+ await writeFile(tempPath, contents, {
528
+ encoding: "utf8",
529
+ mode: STATUS_FILE_MODE
530
+ });
531
+ if (options?.shouldCommit?.() === false) {
532
+ await rm(tempPath, { force: true });
533
+ return;
534
+ }
535
+ await rename(tempPath, path);
536
+ } catch (error) {
537
+ await rm(tempPath, { force: true }).catch(() => void 0);
538
+ throw error;
539
+ }
540
+ }
541
+ async function saveStatusText(textPath, contents, options) {
542
+ await writeLocalStatusFile(textPath, contents, options);
543
+ }
544
+ async function saveState(statePath, state, options) {
545
+ if (options?.shouldCommit?.() === false) return;
546
+ refreshDerivedFields(state);
547
+ await writeLocalStatusFile(statePath, JSON.stringify(state, null, 2), options);
548
+ }
549
+ function upsertRunningChild(state, input) {
550
+ const now = (/* @__PURE__ */ new Date()).toISOString();
551
+ const observedUpdatedAt = safeTimestamp(input.updatedAt, now);
552
+ const observedStartedAt = safeTimestamp(input.startedAt, observedUpdatedAt);
553
+ const existing = state.children[input.id];
554
+ const targetSessionID = sanitizeTargetSessionID(
555
+ input.targetSessionID ?? existing?.targetSessionID,
556
+ input.id.startsWith("ses_") ? input.id : void 0
557
+ );
558
+ const source = input.source ?? existing?.source ?? "session";
559
+ const counted = existing ? false : countChildExecution(state, {
560
+ id: input.id,
561
+ title: input.title,
562
+ parentID: input.parentID,
563
+ messageID: input.messageID,
564
+ source,
565
+ targetSessionID
566
+ });
567
+ const shouldKeepCompletedTiming = existing?.status === "done" || existing?.status === "error";
568
+ const next = {
569
+ id: input.id,
570
+ title: input.title,
571
+ summary: sanitizeSummary(input.summary, input.title) ?? sanitizeSummary(existing?.summary, input.title),
572
+ agentName: sanitizeAgentName(input.agentName) ?? existing?.agentName,
573
+ parentID: input.parentID,
574
+ messageID: input.messageID ?? existing?.messageID,
575
+ source,
576
+ toolName: input.toolName ?? existing?.toolName,
577
+ targetSessionID,
578
+ status: shouldKeepCompletedTiming ? existing.status : "running",
579
+ color: statusColor(shouldKeepCompletedTiming ? existing.status : "running"),
580
+ startedAt: existing?.startedAt ?? observedStartedAt,
581
+ updatedAt: observedUpdatedAt,
582
+ endedAt: shouldKeepCompletedTiming ? existing.endedAt : void 0,
583
+ elapsedMs: existing?.elapsedMs,
584
+ tokens: existing?.tokens,
585
+ model: existing?.model
586
+ };
587
+ if (existing && next.title === existing.title && next.summary === existing.summary && next.agentName === existing.agentName && next.parentID === existing.parentID && next.messageID === existing.messageID && next.source === existing.source && next.toolName === existing.toolName && next.targetSessionID === existing.targetSessionID && next.status === existing.status && next.color === existing.color && next.startedAt === existing.startedAt && next.endedAt === existing.endedAt && sameTokens(next.tokens, existing.tokens) && JSON.stringify(next.model) === JSON.stringify(existing.model)) {
588
+ return counted;
589
+ }
590
+ state.children[input.id] = next;
591
+ state.updatedAt = observedUpdatedAt;
592
+ return true;
593
+ }
594
+ function markChildStatus(state, childID, status, endedAt) {
595
+ const now = (/* @__PURE__ */ new Date()).toISOString();
596
+ let changed = false;
597
+ let stateUpdatedAt = state.updatedAt;
598
+ for (const child of Object.values(state.children)) {
599
+ if (child.id !== childID && child.targetSessionID !== childID) continue;
600
+ const observedEndedAt = endedAt ? safeTimestamp(endedAt, now) : child.endedAt ?? now;
601
+ if (child.status === status && child.color === statusColor(status) && child.updatedAt === observedEndedAt && child.endedAt === observedEndedAt) {
602
+ continue;
603
+ }
604
+ const nextChild = {
605
+ ...child,
606
+ status,
607
+ color: statusColor(status),
608
+ updatedAt: observedEndedAt,
609
+ endedAt: observedEndedAt
610
+ };
611
+ state.children[child.id] = {
612
+ ...nextChild,
613
+ elapsedMs: resolveElapsedMs(nextChild, Date.now())
614
+ };
615
+ stateUpdatedAt = observedEndedAt;
616
+ changed = true;
617
+ }
618
+ if (changed) {
619
+ state.updatedAt = stateUpdatedAt;
620
+ }
621
+ return changed;
622
+ }
623
+ function upsertChildDetails(state, childID, input) {
624
+ const existing = state.children[childID];
625
+ if (!existing) return false;
626
+ const nextTitle = typeof input.title === "string" && input.title.trim().length > 0 ? input.title : existing.title;
627
+ const nextSummary = sanitizeSummary(input.summary, nextTitle) ?? sanitizeSummary(existing.summary, nextTitle);
628
+ const nextAgentName = sanitizeAgentName(input.agentName) ?? existing.agentName;
629
+ const mergedTokens = mergeTokens(existing.tokens, input.tokens);
630
+ const nextTargetSessionID = sanitizeTargetSessionID(
631
+ input.targetSessionID ?? existing.targetSessionID,
632
+ existing.id.startsWith("ses_") ? existing.id : void 0
633
+ );
634
+ const detailsChanged = nextTitle !== existing.title || nextSummary !== existing.summary || nextAgentName !== existing.agentName || !sameTokens(mergedTokens, existing.tokens) || nextTargetSessionID !== existing.targetSessionID;
635
+ if (!detailsChanged) return false;
636
+ const now = (/* @__PURE__ */ new Date()).toISOString();
637
+ const observedUpdatedAt = safeTimestamp(input.updatedAt, now);
638
+ const next = {
639
+ ...existing,
640
+ title: nextTitle,
641
+ summary: nextSummary,
642
+ agentName: nextAgentName,
643
+ tokens: mergedTokens,
644
+ targetSessionID: nextTargetSessionID,
645
+ updatedAt: observedUpdatedAt
646
+ };
647
+ state.children[childID] = next;
648
+ state.updatedAt = observedUpdatedAt;
649
+ return true;
650
+ }
651
+ function setChildModel(state, sessionID, model, updatedAt) {
652
+ const matches = Object.values(state.children).filter(
653
+ (child) => child.id === sessionID || child.targetSessionID === sessionID
654
+ );
655
+ if (matches.length === 0) return false;
656
+ let changed = false;
657
+ const observedUpdatedAt = safeTimestamp(updatedAt, (/* @__PURE__ */ new Date()).toISOString());
658
+ for (const child of matches) {
659
+ const sanitized = sanitizeModel(model);
660
+ if (JSON.stringify(child.model) === JSON.stringify(sanitized)) continue;
661
+ state.children[child.id] = { ...child, model: sanitized };
662
+ changed = true;
663
+ }
664
+ if (changed) state.updatedAt = observedUpdatedAt;
665
+ return changed;
666
+ }
667
+
668
+ // src/events.ts
669
+ function asString(value) {
670
+ return typeof value === "string" && value.length > 0 ? value : void 0;
671
+ }
672
+ function normalizeMessage(value) {
673
+ const message = isRecord(value) ? value : void 0;
674
+ if (!message) return void 0;
675
+ return isRecord(message.info) ? message.info : message;
676
+ }
677
+ function messageActivityMs(message) {
678
+ const time = isRecord(message.time) ? message.time : void 0;
679
+ const value = time?.completed ?? time?.updated ?? time?.created;
680
+ if (typeof value === "number" && Number.isFinite(value)) return value;
681
+ if (typeof value === "string") {
682
+ const parsed = Date.parse(value);
683
+ return Number.isNaN(parsed) ? 0 : parsed;
684
+ }
685
+ return 0;
686
+ }
687
+ function extractLatestAssistantModel(input) {
688
+ const values = Array.isArray(input) ? input : [input];
689
+ const assistants = values.map((value, index) => ({ message: normalizeMessage(value), index })).filter(
690
+ (entry) => entry.message?.role === "assistant"
691
+ ).sort(
692
+ (left, right) => messageActivityMs(left.message) - messageActivityMs(right.message) || left.index - right.index
693
+ );
694
+ const latest = assistants.at(-1)?.message;
695
+ if (!latest) return void 0;
696
+ const sessionID = asString(latest.sessionID);
697
+ if (!sessionID) return void 0;
698
+ const providerID = asString(latest.providerID)?.trim();
699
+ const modelID = asString(latest.modelID)?.trim();
700
+ const variant = asString(latest.variant)?.trim();
701
+ const model = providerID && modelID ? { providerID, modelID, ...variant ? { variant } : {} } : void 0;
702
+ const activity = messageActivityMs(latest);
703
+ return {
704
+ sessionID,
705
+ model,
706
+ updatedAt: activity > 0 ? new Date(activity).toISOString() : void 0
707
+ };
708
+ }
709
+ function conciseText(value) {
710
+ if (typeof value !== "string") return void 0;
711
+ const text = value.replace(/\s+/g, " ").trim();
712
+ if (!text) return void 0;
713
+ return text.length > 180 ? `${text.slice(0, 179)}\u2026` : text;
714
+ }
715
+ function sameDisplayText(a, b) {
716
+ if (!a || !b) return false;
717
+ return a.replace(/\s+/g, " ").trim().toLowerCase() === b.replace(/\s+/g, " ").trim().toLowerCase();
718
+ }
719
+ function firstDistinctSummary(candidates, title) {
720
+ for (const candidate of candidates) {
721
+ const summary = conciseText(candidate);
722
+ if (summary && !sameDisplayText(summary, title)) return summary;
723
+ }
724
+ return void 0;
725
+ }
726
+ function isTechnicalDelegationTitle(value) {
727
+ if (!value) return false;
728
+ return /^delegation:\s+/i.test(value.trim());
729
+ }
730
+ function promptTitle(value) {
731
+ const text = conciseText(value);
732
+ if (!text) return void 0;
733
+ const sentence = text.match(/^(.+?[.!?])\s/)?.[1]?.trim();
734
+ const title = sentence && sentence.length <= 100 ? sentence : text;
735
+ return title.length > 100 ? `${title.slice(0, 99)}\u2026` : title;
736
+ }
737
+ function firstUsefulTitle(candidates) {
738
+ for (const candidate of candidates) {
739
+ const title = promptTitle(candidate);
740
+ if (title && !isTechnicalDelegationTitle(title)) return title;
741
+ }
742
+ return void 0;
743
+ }
744
+ function extractCreatedChild(event) {
745
+ const info = event.properties?.info;
746
+ const parentID = asString(info?.parentID);
747
+ if (!parentID) return null;
748
+ const id = asString(info?.id) ?? asString(event.properties?.id);
749
+ if (!id) return null;
750
+ const title = asString(info?.title) ?? "subagent";
751
+ const agentName = asString(info?.agent) ?? asString(info?.subagent_type);
752
+ const startedAt = extractEventTimestamp(event, [
753
+ "started",
754
+ "start",
755
+ "created",
756
+ "updated"
757
+ ]);
758
+ const updatedAt = extractEventTimestamp(event, ["updated", "created", "started", "start"]) ?? startedAt;
759
+ return { id, title, agentName, parentID, startedAt, updatedAt };
760
+ }
761
+ function extractSessionID(event) {
762
+ return asString(event.properties?.sessionID) ?? asString(event.properties?.sessionId) ?? asString(event.properties?.info?.sessionID) ?? asString(event.properties?.info?.sessionId) ?? asString(event.sessionID) ?? asString(event.sessionId) ?? asString(event.properties?.info?.id) ?? asString(event.properties?.id);
763
+ }
764
+ function isRecord(value) {
765
+ return !!value && typeof value === "object";
766
+ }
767
+ function isSessionID(value) {
768
+ return typeof value === "string" && value.startsWith("ses_");
769
+ }
770
+ function collectSessionIDs(input, target, depth = 0) {
771
+ if (depth > 4 || !input) return;
772
+ if (isSessionID(input)) {
773
+ target.add(input);
774
+ return;
775
+ }
776
+ if (!isRecord(input) && !Array.isArray(input)) return;
777
+ if (Array.isArray(input)) {
778
+ for (const value of input) {
779
+ collectSessionIDs(value, target, depth + 1);
780
+ }
781
+ return;
782
+ }
783
+ for (const [key, value] of Object.entries(input)) {
784
+ if (!key.toLowerCase().includes("session")) continue;
785
+ collectSessionIDs(value, target, depth + 1);
786
+ }
787
+ }
788
+ function resolveSyntheticTargetSessionID(state, synthetic, explicitCandidates = []) {
789
+ const candidates = new Set(explicitCandidates.filter(isSessionID));
790
+ const byMessage = Object.values(state.children).filter(
791
+ (child) => child.id.startsWith("ses_") && child.parentID === synthetic.parentID && child.messageID && synthetic.messageID && child.messageID === synthetic.messageID
792
+ );
793
+ if (byMessage.length === 1) {
794
+ candidates.add(byMessage[0].id);
795
+ }
796
+ const byParent = Object.values(state.children).filter(
797
+ (child) => child.id.startsWith("ses_") && child.parentID === synthetic.parentID
798
+ );
799
+ if (byParent.length === 1) {
800
+ candidates.add(byParent[0].id);
801
+ }
802
+ if (candidates.size !== 1) return void 0;
803
+ return [...candidates][0];
804
+ }
805
+ function extractPartTargetSessionCandidates(event) {
806
+ const part = isRecord(event.properties?.part) ? event.properties.part : void 0;
807
+ if (!part) return [];
808
+ const candidates = /* @__PURE__ */ new Set();
809
+ collectSessionIDs(part, candidates);
810
+ const parentSessionID = asString(part.sessionID) ?? extractSessionID(event);
811
+ if (parentSessionID) candidates.delete(parentSessionID);
812
+ return [...candidates];
813
+ }
814
+ function parseTaskSessionIDFromOutput(value, parentSessionID) {
815
+ if (typeof value !== "string") return void 0;
816
+ const matches = [...value.matchAll(/\b(?:task_id\s*:\s*)?(ses_[a-zA-Z0-9_-]+)\b/gi)];
817
+ const candidates = new Set(matches.map((match) => match[1]));
818
+ if (parentSessionID) candidates.delete(parentSessionID);
819
+ return candidates.size === 1 ? [...candidates][0] : void 0;
820
+ }
821
+ function backfillSyntheticTargetsForSession(state, session) {
822
+ const targetlessSynthetic = Object.values(state.children).filter(
823
+ (child) => (child.source === "tool" || child.source === "subtask") && !child.targetSessionID && child.parentID === session.parentID
824
+ );
825
+ const messageMatches = session.messageID ? targetlessSynthetic.filter(
826
+ (child) => child.messageID === session.messageID
827
+ ) : [];
828
+ const existingSessionSiblings = Object.values(state.children).filter(
829
+ (child) => child.id !== session.id && (child.source === "session" || child.id.startsWith("ses_")) && child.parentID === session.parentID
830
+ );
831
+ const candidates = messageMatches.length > 0 ? messageMatches : targetlessSynthetic;
832
+ if (candidates.length !== 1) return false;
833
+ if (messageMatches.length === 0 && existingSessionSiblings.length > 0) {
834
+ return false;
835
+ }
836
+ const synthetic = candidates[0];
837
+ const targetSessionID = resolveSyntheticTargetSessionID(
838
+ state,
839
+ {
840
+ id: synthetic.id,
841
+ parentID: synthetic.parentID,
842
+ messageID: synthetic.messageID
843
+ },
844
+ [session.id]
845
+ );
846
+ if (targetSessionID !== session.id) return false;
847
+ return upsertChildDetails(state, synthetic.id, {
848
+ targetSessionID,
849
+ updatedAt: session.updatedAt
850
+ });
851
+ }
852
+ function extractTaskToolEvidence(event) {
853
+ const part = event.properties?.part;
854
+ if (!isRecord(part) || part.type !== "tool") return null;
855
+ if (asString(part.tool) !== "task") return null;
856
+ const state = isRecord(part.state) ? part.state : void 0;
857
+ if (!state) return null;
858
+ const rawStatus = asString(state.status);
859
+ const status = rawStatus === "completed" ? "done" : rawStatus === "error" ? "error" : "running";
860
+ const metadata = isRecord(state.metadata) ? state.metadata : void 0;
861
+ const targetFromMetadata = asString(metadata?.sessionId);
862
+ const parentSessionID = asString(part.sessionID) ?? extractSessionID(event);
863
+ const targetFromOutput = parseTaskSessionIDFromOutput(
864
+ state.output,
865
+ parentSessionID
866
+ );
867
+ const targetCandidates = extractPartTargetSessionCandidates(event);
868
+ const targetSessionID = targetFromMetadata ?? targetFromOutput ?? (targetCandidates.length === 1 ? targetCandidates[0] : void 0);
869
+ const endedAt = status === "done" || status === "error" ? extractEventTimestamp(event, ["completed", "end", "ended", "updated"]) : void 0;
870
+ return {
871
+ status,
872
+ targetSessionID,
873
+ endedAt
874
+ };
875
+ }
876
+ function mapTaskToolToSubtaskID(state, task) {
877
+ const runningSubtasks = Object.values(state.children).filter(
878
+ (child) => child.source === "subtask" && child.status === "running" && child.parentID === task.parentID
879
+ );
880
+ const primaryCandidates = runningSubtasks.filter(
881
+ (child) => child.messageID === task.messageID
882
+ );
883
+ const legacyCandidates = task.parentMessageID ? runningSubtasks.filter(
884
+ (child) => child.messageID === task.parentMessageID
885
+ ) : [];
886
+ const candidates = primaryCandidates.length > 0 ? primaryCandidates : legacyCandidates;
887
+ if (candidates.length === 0) return void 0;
888
+ if (task.targetSessionID) {
889
+ const byTarget = candidates.filter(
890
+ (child) => child.targetSessionID === task.targetSessionID
891
+ );
892
+ if (byTarget.length === 1) return byTarget[0].id;
893
+ }
894
+ const byTitle = candidates.filter(
895
+ (child) => sameDisplayText(child.title, task.title)
896
+ );
897
+ if (byTitle.length === 1) return byTitle[0].id;
898
+ const bySummary = candidates.filter(
899
+ (child) => sameDisplayText(child.summary, task.summary)
900
+ );
901
+ if (bySummary.length === 1) return bySummary[0].id;
902
+ const byAgent = task.agentName ? candidates.filter(
903
+ (child) => sameDisplayText(child.agentName, task.agentName)
904
+ ) : [];
905
+ if (byAgent.length === 1) return byAgent[0].id;
906
+ if (candidates.length === 1) return candidates[0].id;
907
+ return void 0;
908
+ }
909
+ function extractParentMessageID(event) {
910
+ return asString(event.properties?.info?.parentID) ?? asString(event.properties?.parentID) ?? asString(event.parentID);
911
+ }
912
+ function toIsoTimestamp(value) {
913
+ if (typeof value === "string") {
914
+ if (value.trim().length === 0) return void 0;
915
+ const parsed = Date.parse(value);
916
+ if (Number.isNaN(parsed)) return void 0;
917
+ return new Date(parsed).toISOString();
918
+ }
919
+ if (typeof value === "number" && Number.isFinite(value)) {
920
+ if (value <= 0) return void 0;
921
+ const millis = value < 1e10 ? value * 1e3 : value;
922
+ const parsed = new Date(millis);
923
+ return Number.isNaN(parsed.getTime()) ? void 0 : parsed.toISOString();
924
+ }
925
+ return void 0;
926
+ }
927
+ function extractEventTimestamp(event, keys) {
928
+ const part = isRecord(event.properties?.part) ? event.properties?.part : void 0;
929
+ const state = isRecord(part?.state) ? part?.state : void 0;
930
+ const sources = [
931
+ isRecord(event.properties?.info?.time) ? event.properties?.info?.time : void 0,
932
+ isRecord(part?.time) ? part?.time : void 0,
933
+ isRecord(part?.timestamps) ? part?.timestamps : void 0,
934
+ isRecord(state?.time) ? state?.time : void 0,
935
+ isRecord(state?.timestamps) ? state?.timestamps : void 0,
936
+ state,
937
+ part
938
+ ];
939
+ for (const source of sources) {
940
+ if (!source) continue;
941
+ for (const key of keys) {
942
+ const candidate = toIsoTimestamp(source[key]);
943
+ if (candidate) return candidate;
944
+ }
945
+ }
946
+ return void 0;
947
+ }
948
+ function extractSubtaskChild(event) {
949
+ const part = event.properties?.part;
950
+ if (!isRecord(part) || part.type !== "subtask") return null;
951
+ const partID = asString(part.id);
952
+ const parentID = asString(part.sessionID) ?? extractSessionID(event);
953
+ const messageID = asString(part.messageID);
954
+ if (!partID || !parentID || !messageID) return null;
955
+ const description = asString(part.description);
956
+ const command = asString(part.command);
957
+ const agent = asString(part.agent);
958
+ const title = description || command || agent || "subtask";
959
+ const state = isRecord(part.state) ? part.state : void 0;
960
+ const input = isRecord(state?.input) ? state.input : void 0;
961
+ const summary = firstDistinctSummary(
962
+ [input?.prompt, input?.description, part.description, state?.description],
963
+ title
964
+ );
965
+ const startedAt = extractEventTimestamp(event, [
966
+ "started",
967
+ "start",
968
+ "created",
969
+ "updated"
970
+ ]);
971
+ const updatedAt = extractEventTimestamp(event, ["updated", "created", "started", "start"]) ?? startedAt;
972
+ const targetCandidates = extractPartTargetSessionCandidates(event);
973
+ const targetSessionID = targetCandidates.length === 1 ? targetCandidates[0] : void 0;
974
+ return {
975
+ id: `subtask:${partID}`,
976
+ title,
977
+ summary,
978
+ agentName: agent,
979
+ parentID,
980
+ messageID,
981
+ targetSessionID,
982
+ startedAt,
983
+ updatedAt
984
+ };
985
+ }
986
+ function extractToolChild(event) {
987
+ const part = event.properties?.part;
988
+ if (!isRecord(part) || part.type !== "tool") return null;
989
+ const tool = asString(part.tool);
990
+ if (tool !== "delegate" && tool !== "task") return null;
991
+ const partID = asString(part.id);
992
+ const parentID = asString(part.sessionID) ?? extractSessionID(event);
993
+ const messageID = asString(part.messageID);
994
+ const state = isRecord(part.state) ? part.state : void 0;
995
+ if (!partID || !parentID || !messageID || !state) return null;
996
+ const taskEvidence = extractTaskToolEvidence(event);
997
+ const rawStatus = asString(state.status);
998
+ const status = taskEvidence?.status ?? (rawStatus === "completed" ? "done" : rawStatus === "error" ? "error" : "running");
999
+ const input = isRecord(state.input) ? state.input : {};
1000
+ const description = asString(input.description);
1001
+ const subagentType = asString(input.subagent_type);
1002
+ const rawTitle = asString(state.title);
1003
+ const title = (isTechnicalDelegationTitle(rawTitle) ? void 0 : rawTitle) || description || firstUsefulTitle([input.prompt, part.description, state.description]) || subagentType || tool;
1004
+ const summary = firstDistinctSummary(
1005
+ [input.prompt, input.description, part.description, state.description],
1006
+ title
1007
+ );
1008
+ const startedAt = extractEventTimestamp(event, [
1009
+ "started",
1010
+ "start",
1011
+ "created",
1012
+ "updated"
1013
+ ]);
1014
+ const updatedAt = extractEventTimestamp(event, [
1015
+ "updated",
1016
+ "completed",
1017
+ "created",
1018
+ "started",
1019
+ "start"
1020
+ ]) ?? startedAt;
1021
+ const endedAt = status === "done" || status === "error" ? extractEventTimestamp(event, ["completed", "end", "ended", "updated"]) : void 0;
1022
+ const targetCandidates = extractPartTargetSessionCandidates(event);
1023
+ const targetSessionID = taskEvidence?.targetSessionID ?? (targetCandidates.length === 1 ? targetCandidates[0] : void 0);
1024
+ return {
1025
+ id: `tool:${partID}`,
1026
+ title,
1027
+ summary,
1028
+ agentName: subagentType,
1029
+ parentID,
1030
+ messageID,
1031
+ toolName: tool,
1032
+ targetSessionID,
1033
+ status,
1034
+ startedAt,
1035
+ updatedAt,
1036
+ endedAt
1037
+ };
1038
+ }
1039
+ function extractCompletedAssistantMessage(event) {
1040
+ const info = event.properties?.info;
1041
+ if (!isRecord(info)) return null;
1042
+ if (info.role !== "assistant") return null;
1043
+ const time = info.time;
1044
+ if (!isRecord(time) || typeof time.completed !== "number") return null;
1045
+ const sessionID = asString(info.sessionID) ?? extractSessionID(event);
1046
+ const messageID = asString(info.id);
1047
+ if (!sessionID || !messageID) return null;
1048
+ return { sessionID, messageID };
1049
+ }
1050
+ function extractDetailTargetIDs(event) {
1051
+ const ids = /* @__PURE__ */ new Set();
1052
+ const part = event.properties?.part;
1053
+ if (isRecord(part)) {
1054
+ const partID = asString(part.id);
1055
+ if (part.type === "subtask" && partID) {
1056
+ ids.add(`subtask:${partID}`);
1057
+ }
1058
+ if (part.type === "tool") {
1059
+ const tool = asString(part.tool);
1060
+ if ((tool === "delegate" || tool === "task") && partID) {
1061
+ ids.add(`tool:${partID}`);
1062
+ }
1063
+ }
1064
+ }
1065
+ const sessionID = extractSessionID(event);
1066
+ if (sessionID) ids.add(sessionID);
1067
+ return [...ids];
1068
+ }
1069
+ function normalizePercent(value) {
1070
+ if (value > 0 && value <= 1) {
1071
+ return value * 100;
1072
+ }
1073
+ return value;
1074
+ }
1075
+ function extractChildDetails(event) {
1076
+ const details = {};
1077
+ details.updatedAt = extractEventTimestamp(event, [
1078
+ "updated",
1079
+ "completed",
1080
+ "created",
1081
+ "started",
1082
+ "start"
1083
+ ]);
1084
+ const titleCandidates = [
1085
+ event.properties?.info?.title,
1086
+ event.properties?.title,
1087
+ event.properties?.info?.name,
1088
+ event.properties?.name,
1089
+ event.title,
1090
+ event.name
1091
+ ];
1092
+ for (const candidate of titleCandidates) {
1093
+ const title = asString(candidate);
1094
+ if (title) {
1095
+ details.title = title;
1096
+ break;
1097
+ }
1098
+ }
1099
+ const part = isRecord(event.properties?.part) ? event.properties.part : void 0;
1100
+ const partState = isRecord(part?.state) ? part.state : void 0;
1101
+ const partInput = isRecord(partState?.input) ? partState.input : void 0;
1102
+ details.agentName = asString(partInput?.subagent_type) ?? asString(partInput?.agent) ?? asString(part?.agent) ?? asString(event.properties?.info?.agent) ?? asString(event.properties?.info?.subagent_type);
1103
+ details.summary = firstDistinctSummary(
1104
+ [
1105
+ partInput?.prompt,
1106
+ partInput?.description,
1107
+ part?.description,
1108
+ partState?.description
1109
+ ],
1110
+ details.title
1111
+ );
1112
+ if (isTechnicalDelegationTitle(details.title)) {
1113
+ const replacementTitle = asString(partInput?.description) ?? firstUsefulTitle([
1114
+ partInput?.prompt,
1115
+ part?.description,
1116
+ partState?.description
1117
+ ]);
1118
+ if (replacementTitle) {
1119
+ details.title = replacementTitle;
1120
+ }
1121
+ }
1122
+ const tokenHints = {};
1123
+ const visited = /* @__PURE__ */ new Set();
1124
+ const walk = (node, depth) => {
1125
+ if (!isRecord(node) || depth > 6) return;
1126
+ if (visited.has(node)) return;
1127
+ visited.add(node);
1128
+ for (const [rawKey, rawValue] of Object.entries(node)) {
1129
+ const key = rawKey.toLowerCase();
1130
+ const asNumber = typeof rawValue === "number" ? rawValue : typeof rawValue === "string" && rawValue.trim().length > 0 ? Number(rawValue) : void 0;
1131
+ if (typeof asNumber === "number" && Number.isFinite(asNumber)) {
1132
+ if (key.includes("context") && key.includes("percent")) {
1133
+ tokenHints.contextPercent = normalizePercent(asNumber);
1134
+ } else if (key.includes("context") && key.includes("usage")) {
1135
+ tokenHints.contextPercent = normalizePercent(asNumber);
1136
+ } else if ((key.includes("input") || key.includes("prompt")) && key.includes("token")) {
1137
+ tokenHints.input = asNumber;
1138
+ } else if ((key.includes("output") || key.includes("completion")) && key.includes("token")) {
1139
+ tokenHints.output = asNumber;
1140
+ } else if (key.includes("total") && key.includes("token")) {
1141
+ tokenHints.total = asNumber;
1142
+ } else if (key === "tokens" || key === "token") {
1143
+ tokenHints.total = asNumber;
1144
+ }
1145
+ }
1146
+ if (isRecord(rawValue)) {
1147
+ walk(rawValue, depth + 1);
1148
+ }
1149
+ }
1150
+ };
1151
+ walk(event, 0);
1152
+ if (tokenHints.input !== void 0 || tokenHints.output !== void 0 || tokenHints.total !== void 0 || tokenHints.contextPercent !== void 0) {
1153
+ details.tokens = tokenHints;
1154
+ }
1155
+ return details;
1156
+ }
1157
+ function applySubagentEvent(state, event) {
1158
+ const e = event ?? {};
1159
+ const type = asString(e.type);
1160
+ if (!type) return false;
1161
+ if (type === "session.created" || type === "session.updated") {
1162
+ const child = extractCreatedChild(e);
1163
+ if (child) {
1164
+ const details = extractChildDetails(e);
1165
+ let changed2 = upsertRunningChild(state, {
1166
+ ...child,
1167
+ source: "session",
1168
+ targetSessionID: child.id
1169
+ });
1170
+ changed2 = upsertChildDetails(state, child.id, details) || changed2;
1171
+ changed2 = backfillSyntheticTargetsForSession(state, {
1172
+ id: child.id,
1173
+ parentID: child.parentID,
1174
+ updatedAt: child.updatedAt
1175
+ }) || changed2;
1176
+ const sessionStatusFromUpdate = type === "session.updated" ? hasStructuredErrorEvidence(e.properties ?? e) ? "error" : deriveOpenCodeSessionStatus(
1177
+ e.properties?.status ?? e.properties?.state ?? e.properties?.info?.status ?? e.status ?? e.state
1178
+ ) : void 0;
1179
+ if (sessionStatusFromUpdate === "done" || sessionStatusFromUpdate === "error") {
1180
+ const endedAt = extractEventTimestamp(e, [
1181
+ "completed",
1182
+ "end",
1183
+ "ended",
1184
+ "updated"
1185
+ ]);
1186
+ changed2 = markChildStatus(state, child.id, sessionStatusFromUpdate, endedAt) || changed2;
1187
+ }
1188
+ return changed2;
1189
+ }
1190
+ return false;
1191
+ }
1192
+ if (type === "session.idle") {
1193
+ const childID = extractSessionID(e);
1194
+ if (!childID) return false;
1195
+ const endedAt = extractEventTimestamp(e, [
1196
+ "completed",
1197
+ "end",
1198
+ "ended",
1199
+ "updated"
1200
+ ]);
1201
+ const details = extractChildDetails(e);
1202
+ const status = deriveOpenCodeSessionStatus(e.properties ?? e) === "error" || hasStructuredErrorEvidence(e.properties ?? e) ? "error" : "done";
1203
+ let changed2 = markChildStatus(state, childID, status, endedAt);
1204
+ changed2 = upsertChildDetails(state, childID, details) || changed2;
1205
+ return changed2;
1206
+ }
1207
+ if (type === "session.error") {
1208
+ const childID = extractSessionID(e);
1209
+ if (!childID) return false;
1210
+ const endedAt = extractEventTimestamp(e, [
1211
+ "completed",
1212
+ "end",
1213
+ "ended",
1214
+ "updated"
1215
+ ]);
1216
+ const details = extractChildDetails(e);
1217
+ let changed2 = markChildStatus(state, childID, "error", endedAt);
1218
+ changed2 = upsertChildDetails(state, childID, details) || changed2;
1219
+ return changed2;
1220
+ }
1221
+ if (type === "session.status") {
1222
+ const childID = extractSessionID(e);
1223
+ if (!childID) return false;
1224
+ const status = hasStructuredErrorEvidence(e.properties ?? e) ? "error" : deriveOpenCodeSessionStatus(
1225
+ e.properties?.status ?? e.properties?.state ?? e.properties?.info?.status ?? e.status ?? e.state ?? e.properties
1226
+ );
1227
+ if (!status) return false;
1228
+ const endedAt = status === "done" || status === "error" ? extractEventTimestamp(e, ["completed", "end", "ended", "updated"]) : void 0;
1229
+ const details = extractChildDetails(e);
1230
+ let changed2 = status === "running" ? false : markChildStatus(state, childID, status, endedAt);
1231
+ changed2 = upsertChildDetails(state, childID, details) || changed2;
1232
+ return changed2;
1233
+ }
1234
+ let changed = false;
1235
+ if (type === "message.part.updated") {
1236
+ const subtask = extractSubtaskChild(e);
1237
+ if (subtask) {
1238
+ const targetSessionID = resolveSyntheticTargetSessionID(
1239
+ state,
1240
+ {
1241
+ id: subtask.id,
1242
+ parentID: subtask.parentID,
1243
+ messageID: subtask.messageID
1244
+ },
1245
+ subtask.targetSessionID ? [subtask.targetSessionID] : []
1246
+ );
1247
+ changed = upsertRunningChild(state, {
1248
+ ...subtask,
1249
+ source: "subtask",
1250
+ targetSessionID,
1251
+ startedAt: subtask.startedAt,
1252
+ updatedAt: subtask.updatedAt
1253
+ }) || changed;
1254
+ }
1255
+ const tool = extractToolChild(e);
1256
+ if (tool) {
1257
+ const targetSessionID = resolveSyntheticTargetSessionID(
1258
+ state,
1259
+ {
1260
+ id: tool.id,
1261
+ parentID: tool.parentID,
1262
+ messageID: tool.messageID
1263
+ },
1264
+ tool.targetSessionID ? [tool.targetSessionID] : []
1265
+ );
1266
+ const childChanged = upsertRunningChild(state, {
1267
+ ...tool,
1268
+ source: "tool",
1269
+ targetSessionID,
1270
+ startedAt: tool.startedAt,
1271
+ updatedAt: tool.updatedAt
1272
+ });
1273
+ changed = childChanged || changed;
1274
+ if (tool.status === "done" || tool.status === "error") {
1275
+ changed = markChildStatus(
1276
+ state,
1277
+ tool.id,
1278
+ tool.status,
1279
+ tool.endedAt ?? tool.updatedAt
1280
+ ) || changed;
1281
+ if (asString(
1282
+ e.properties?.part?.tool
1283
+ ) === "task") {
1284
+ const subtaskID = mapTaskToolToSubtaskID(state, {
1285
+ parentID: tool.parentID,
1286
+ messageID: tool.messageID,
1287
+ parentMessageID: extractParentMessageID(e),
1288
+ title: tool.title,
1289
+ summary: tool.summary,
1290
+ agentName: tool.agentName,
1291
+ targetSessionID
1292
+ });
1293
+ if (subtaskID) {
1294
+ if (targetSessionID) {
1295
+ changed = upsertChildDetails(state, subtaskID, {
1296
+ targetSessionID,
1297
+ updatedAt: tool.updatedAt
1298
+ }) || changed;
1299
+ }
1300
+ changed = markChildStatus(
1301
+ state,
1302
+ subtaskID,
1303
+ tool.status,
1304
+ tool.endedAt ?? tool.updatedAt
1305
+ ) || changed;
1306
+ }
1307
+ }
1308
+ }
1309
+ }
1310
+ }
1311
+ if (type === "message.updated") {
1312
+ const assistantModel = extractLatestAssistantModel(e.properties?.info ?? e);
1313
+ if (assistantModel) {
1314
+ changed = setChildModel(
1315
+ state,
1316
+ assistantModel.sessionID,
1317
+ assistantModel.model,
1318
+ assistantModel.updatedAt
1319
+ ) || changed;
1320
+ }
1321
+ const completed = extractCompletedAssistantMessage(e);
1322
+ if (completed) {
1323
+ for (const child of Object.values(state.children)) {
1324
+ if (child.source === "subtask" && child.status === "running" && child.parentID === completed.sessionID && child.messageID === completed.messageID) {
1325
+ changed = markChildStatus(state, child.id, "done") || changed;
1326
+ }
1327
+ }
1328
+ }
1329
+ }
1330
+ if (type === "message.updated" || type === "message.part.updated") {
1331
+ const details = extractChildDetails(e);
1332
+ for (const childID of extractDetailTargetIDs(e)) {
1333
+ if (state.children[childID]) {
1334
+ changed = upsertChildDetails(state, childID, details) || changed;
1335
+ }
1336
+ }
1337
+ }
1338
+ return changed;
1339
+ }
1340
+
1341
+ // src/render.ts
1342
+ var ansi = {
1343
+ reset: "\x1B[0m",
1344
+ gray: "\x1B[90m",
1345
+ green: "\x1B[32m",
1346
+ yellow: "\x1B[33m",
1347
+ red: "\x1B[31m"
1348
+ };
1349
+ function colorsEnabled() {
1350
+ if (process.env.NO_COLOR) return false;
1351
+ const fromEnv = process.env.OPENCODE_SUBAGENT_STATUSLINE_COLOR;
1352
+ if (fromEnv === "0") return false;
1353
+ return true;
1354
+ }
1355
+ function paint(text, color, enabled) {
1356
+ if (!enabled) return text;
1357
+ return `${color}${text}${ansi.reset}`;
1358
+ }
1359
+ function formatDuration(elapsedMs) {
1360
+ const totalSeconds = Math.max(0, Math.floor((elapsedMs ?? 0) / 1e3));
1361
+ const hours = Math.floor(totalSeconds / 3600);
1362
+ const minutes = Math.floor(totalSeconds % 3600 / 60);
1363
+ const seconds = totalSeconds % 60;
1364
+ if (hours > 0) {
1365
+ return `${String(hours).padStart(2, "0")}:${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}`;
1366
+ }
1367
+ return `${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}`;
1368
+ }
1369
+ function formatNumber(value) {
1370
+ return Math.max(0, Math.round(value)).toLocaleString("en-US");
1371
+ }
1372
+ function resolveTokenTotal(child) {
1373
+ const total = child.tokens?.total;
1374
+ if (typeof total === "number" && Number.isFinite(total)) {
1375
+ return total;
1376
+ }
1377
+ const inTokens = child.tokens?.input;
1378
+ const outTokens = child.tokens?.output;
1379
+ if (typeof inTokens === "number" || typeof outTokens === "number") {
1380
+ return (inTokens ?? 0) + (outTokens ?? 0);
1381
+ }
1382
+ return void 0;
1383
+ }
1384
+ function formatPercentUsed(percent) {
1385
+ const rounded = Math.round(percent * 10) / 10;
1386
+ if (Math.abs(rounded - Math.round(rounded)) < 0.05) {
1387
+ return `${Math.round(rounded)}% used`;
1388
+ }
1389
+ return `${rounded.toFixed(1)}% used`;
1390
+ }
1391
+ function formatTokenCount(total) {
1392
+ const label = total === 1 ? "token" : "tokens";
1393
+ return `${formatNumber(total)} ${label}`;
1394
+ }
1395
+ function formatContextDetails(child) {
1396
+ const total = resolveTokenTotal(child);
1397
+ const percent = child.tokens?.contextPercent;
1398
+ const hasPercent = typeof percent === "number" && Number.isFinite(percent);
1399
+ const hasTotal = typeof total === "number" && Number.isFinite(total);
1400
+ if (hasTotal && hasPercent) {
1401
+ return `${formatTokenCount(total)} \xB7 ${formatPercentUsed(percent)}`;
1402
+ }
1403
+ if (hasTotal) {
1404
+ return formatTokenCount(total);
1405
+ }
1406
+ if (hasPercent) {
1407
+ return formatPercentUsed(percent);
1408
+ }
1409
+ return void 0;
1410
+ }
1411
+ function formatContext(child) {
1412
+ const details = formatContextDetails(child);
1413
+ if (!details) return "";
1414
+ return `ctx ${details}`;
1415
+ }
1416
+ function childColor(child) {
1417
+ if (child.color === "green") return ansi.green;
1418
+ if (child.color === "red") return ansi.red;
1419
+ return ansi.yellow;
1420
+ }
1421
+ function byPriority(a, b) {
1422
+ const startedDiff = b.startedAt.localeCompare(a.startedAt);
1423
+ if (startedDiff !== 0) return startedDiff;
1424
+ return a.id.localeCompare(b.id);
1425
+ }
1426
+ var RECENT_TERMINAL_VISIBLE_MS = 10 * 60 * 1e3;
1427
+ function collapseSubagentWorkItems(children) {
1428
+ return correlateSubagentWorkItems(children).map(
1429
+ ({ real, proxies }) => proxies.reduce(
1430
+ (current, proxy) => mergeProxyMetadataWithRealExecution(current, proxy),
1431
+ real
1432
+ )
1433
+ );
1434
+ }
1435
+ function isTerminalWorkItem(child) {
1436
+ return child.status === "done" || child.status === "error";
1437
+ }
1438
+ function isVisibleWorkItem(child, nowMs = Date.now()) {
1439
+ if (!isTerminalWorkItem(child)) return true;
1440
+ const endedMs = Date.parse(child.endedAt ?? child.updatedAt);
1441
+ if (Number.isNaN(endedMs)) return false;
1442
+ return nowMs - endedMs <= RECENT_TERMINAL_VISIBLE_MS;
1443
+ }
1444
+ function visibleSubagentWorkItems(children, nowMs = Date.now(), options = {}) {
1445
+ const collapsed = collapseSubagentWorkItems(children);
1446
+ if (options.showCompletedHistory) return collapsed;
1447
+ const visible = collapsed.filter((child) => isVisibleWorkItem(child, nowMs));
1448
+ const hasRunning = visible.some((child) => child.status === "running");
1449
+ const activeMessageIDs = new Set(
1450
+ visible.filter((child) => child.status === "running" && child.messageID).map((child) => child.messageID)
1451
+ );
1452
+ if (!hasRunning) return visible;
1453
+ return visible.filter((child) => {
1454
+ if (child.status === "running") return true;
1455
+ if (!child.messageID) return false;
1456
+ return activeMessageIDs.has(child.messageID);
1457
+ });
1458
+ }
1459
+ function renderStatusLine(state) {
1460
+ const children = visibleSubagentWorkItems(Object.values(state.children)).sort(
1461
+ byPriority
1462
+ );
1463
+ const counts = countRetainedSubagentStatuses({ children: state.children });
1464
+ const totalExecuted = formatNumber(state.totalExecuted ?? 0);
1465
+ const colorOn = colorsEnabled();
1466
+ const aggregate = `\u21B3 ${counts.running} running \xB7 ${counts.done} done \xB7 ${counts.error} error \xB7 \u03A3 ${totalExecuted} total`;
1467
+ if (children.length === 0) return aggregate;
1468
+ const details = children.map((child) => {
1469
+ const context = formatContext(child);
1470
+ const label = [child.title, formatDuration(child.elapsedMs), context].filter((part) => part.length > 0).join(" ");
1471
+ return paint(label, childColor(child), colorOn);
1472
+ }).join(paint(" \xB7 ", ansi.gray, colorOn));
1473
+ return `${aggregate} \xB7 ${details}`;
1474
+ }
1475
+
1476
+ // src/index.ts
1477
+ var SubagentStatusline = async () => {
1478
+ const statePath = resolveStatePath();
1479
+ const textPath = resolveTextPath(statePath);
1480
+ if (!shouldPreserveStateOnStartup()) {
1481
+ try {
1482
+ const emptyState = createEmptyState();
1483
+ await saveState(statePath, emptyState);
1484
+ await saveStatusText(textPath, renderStatusLine(emptyState));
1485
+ } catch {
1486
+ }
1487
+ }
1488
+ return {
1489
+ event: async ({ event }) => {
1490
+ try {
1491
+ const state = await loadState(statePath);
1492
+ const changed = applySubagentEvent(state, event);
1493
+ if (changed) {
1494
+ await saveState(statePath, state);
1495
+ const line = renderStatusLine(state);
1496
+ await saveStatusText(textPath, line);
1497
+ }
1498
+ } catch {
1499
+ }
1500
+ }
1501
+ };
1502
+ };
1503
+ export {
1504
+ SubagentStatusline
1505
+ };