@mageyuki/opencode-subagent-statusline 0.0.0-stage → 0.7.1
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/LICENSE +21 -0
- package/README.md +265 -2
- package/assets/opencode_full.webp +0 -0
- package/assets/opencode_sidebar.webp +0 -0
- package/assets/subagents_monitor_banner.webp +0 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +1505 -0
- package/dist/metafile-esm.json +1 -0
- package/dist/tui-v1.d.ts +130 -0
- package/dist/tui-v1.js +4380 -0
- package/dist/tui-v2.d.ts +5 -0
- package/dist/tui-v2.js +2922 -0
- package/dist/tui.d.ts +7 -0
- package/dist/tui.js +15 -0
- package/package.json +105 -3
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
|
+
};
|