@khalilgharbaoui/opencode-claude-code-plugin 0.55.0 → 0.57.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -1
- package/dist/index.d.ts +121 -0
- package/dist/index.js +4002 -2594
- package/dist/index.js.map +1 -1
- package/dist/tui.js +883 -74
- package/dist/tui.js.map +1 -1
- package/package.json +1 -1
- package/skills/claude-code-plugin/SKILL.md +59 -3
package/dist/tui.js
CHANGED
|
@@ -8,13 +8,14 @@ var FINISHED_TTL_MS = 10 * 6e4;
|
|
|
8
8
|
var FINISHED_KEEP = 3;
|
|
9
9
|
var STARTING_GRACE_MS = 1e4;
|
|
10
10
|
var SIDEBAR_ROW_WIDTH = 36;
|
|
11
|
+
var ROW_INDENT = " ";
|
|
11
12
|
var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
12
13
|
var TITLE_SUFFIX = /\s*\(@([^)\s]+) subagent\)\s*$/;
|
|
13
14
|
function parseChildTitle(title) {
|
|
14
|
-
const
|
|
15
|
-
const match = TITLE_SUFFIX.exec(
|
|
16
|
-
if (!match) return { description:
|
|
17
|
-
return { description:
|
|
15
|
+
const text3 = (title ?? "").trim();
|
|
16
|
+
const match = TITLE_SUFFIX.exec(text3);
|
|
17
|
+
if (!match) return { description: text3 };
|
|
18
|
+
return { description: text3.slice(0, match.index).trim(), agent: match[1] };
|
|
18
19
|
}
|
|
19
20
|
function text(value) {
|
|
20
21
|
return typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
@@ -81,6 +82,12 @@ function subagentLabel(record, part) {
|
|
|
81
82
|
const account = record?.model ? text(record.account) : accountFromProvider(askedProvider);
|
|
82
83
|
return [model, effort, account].filter(Boolean).join(" \xB7 ");
|
|
83
84
|
}
|
|
85
|
+
function formatCost(value) {
|
|
86
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) return void 0;
|
|
87
|
+
if (value < 5e-3) return "<$0.01";
|
|
88
|
+
const digits = [2, 1, 0].map((places) => value.toFixed(places)).find((text3) => text3.length <= 4);
|
|
89
|
+
return `$${digits ?? value.toFixed(0)}`;
|
|
90
|
+
}
|
|
84
91
|
function truncate(value, width) {
|
|
85
92
|
const chars = Array.from(value.replace(/\s+/g, " ").trim());
|
|
86
93
|
if (width <= 0) return "";
|
|
@@ -157,6 +164,7 @@ function rowFor(input, child, parsed, part, completion) {
|
|
|
157
164
|
if (state !== "running" && finished === void 0) {
|
|
158
165
|
finished = input.stoppedAt(child.id) ?? child.updated ?? started;
|
|
159
166
|
}
|
|
167
|
+
const cost = input.cost?.(child.id);
|
|
160
168
|
return {
|
|
161
169
|
sessionID: child.id,
|
|
162
170
|
agent: parsed.agent ?? (part ? partAgent(part) : void 0) ?? text(child.agent) ?? "subagent",
|
|
@@ -165,6 +173,7 @@ function rowFor(input, child, parsed, part, completion) {
|
|
|
165
173
|
state,
|
|
166
174
|
label: subagentLabel(input.record(child.id), part),
|
|
167
175
|
effort: text(input.record(child.id)?.effort),
|
|
176
|
+
...cost !== void 0 ? { cost } : {},
|
|
168
177
|
started,
|
|
169
178
|
finished
|
|
170
179
|
};
|
|
@@ -193,15 +202,29 @@ function layoutRow(row, width = SIDEBAR_ROW_WIDTH, frame = 0) {
|
|
|
193
202
|
first.push({ text: marker, role: "muted" });
|
|
194
203
|
}
|
|
195
204
|
const second = [];
|
|
196
|
-
|
|
197
|
-
|
|
205
|
+
const content = width - ROW_INDENT.length;
|
|
206
|
+
const pieces = row.label ? truncate(row.label, content).split(" \xB7 ") : [];
|
|
207
|
+
const labelWidth = pieces.reduce((sum, piece) => sum + Array.from(piece).length, 0) + 3 * Math.max(0, pieces.length - 1);
|
|
208
|
+
const cost = formatCost(row.cost);
|
|
209
|
+
const costWidth = cost ? Array.from(cost).length : 0;
|
|
210
|
+
let joiner;
|
|
211
|
+
if (cost) {
|
|
212
|
+
if (pieces.length === 0) joiner = costWidth <= content ? "" : void 0;
|
|
213
|
+
else if (labelWidth + 3 + costWidth <= content) joiner = " \xB7 ";
|
|
214
|
+
else if (labelWidth + 1 + costWidth <= content) joiner = " ";
|
|
215
|
+
}
|
|
216
|
+
if (pieces.length > 0 || joiner !== void 0) {
|
|
198
217
|
const effortAt = row.effort ? pieces.indexOf(row.effort) : -1;
|
|
199
|
-
second.push({ text:
|
|
218
|
+
second.push({ text: ROW_INDENT, role: "muted" });
|
|
200
219
|
pieces.forEach((piece, index) => {
|
|
201
220
|
if (index > 0) second.push({ text: " \xB7 ", role: "muted" });
|
|
202
221
|
const role = index !== effortAt ? "muted" : finished ? "effort-faded" : "effort";
|
|
203
222
|
second.push({ text: piece, role });
|
|
204
223
|
});
|
|
224
|
+
if (cost && joiner !== void 0) {
|
|
225
|
+
if (joiner) second.push({ text: joiner, role: "muted" });
|
|
226
|
+
second.push({ text: cost, role: "cost" });
|
|
227
|
+
}
|
|
205
228
|
}
|
|
206
229
|
return { first, second };
|
|
207
230
|
}
|
|
@@ -210,6 +233,199 @@ function collapsedSummary(rows) {
|
|
|
210
233
|
return running > 0 ? `${running} running` : `${rows.length} done`;
|
|
211
234
|
}
|
|
212
235
|
|
|
236
|
+
// src/progress-line.ts
|
|
237
|
+
var BAR_WIDTH = 10;
|
|
238
|
+
var PROGRESS_LINGER_MS = 15e3;
|
|
239
|
+
var BAR_DONE = "\u25B0";
|
|
240
|
+
var BAR_TODO = "\u25B1";
|
|
241
|
+
function text2(value) {
|
|
242
|
+
return typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
243
|
+
}
|
|
244
|
+
function cells(value) {
|
|
245
|
+
return Array.from(value).length;
|
|
246
|
+
}
|
|
247
|
+
function turnMessages(messages, isUser) {
|
|
248
|
+
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
249
|
+
if (isUser(messages[index])) return index === 0 ? messages : messages.slice(index);
|
|
250
|
+
}
|
|
251
|
+
return messages;
|
|
252
|
+
}
|
|
253
|
+
function measureProgress(source) {
|
|
254
|
+
const messages = source.messages;
|
|
255
|
+
let turnStart = -1;
|
|
256
|
+
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
257
|
+
if (messages[index].role === "user") {
|
|
258
|
+
turnStart = index;
|
|
259
|
+
break;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
let calls = 0;
|
|
263
|
+
let tool;
|
|
264
|
+
let failed = false;
|
|
265
|
+
for (let index = turnStart + 1; index < messages.length; index += 1) {
|
|
266
|
+
const message = messages[index];
|
|
267
|
+
if (message.role !== "assistant") continue;
|
|
268
|
+
failed = message.error === true;
|
|
269
|
+
for (const call of message.tools) {
|
|
270
|
+
calls += 1;
|
|
271
|
+
const running = call.status === "running" || call.status === "pending";
|
|
272
|
+
tool = running ? text2(call.name) : void 0;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
let total = 0;
|
|
276
|
+
let done = 0;
|
|
277
|
+
for (const todo of source.todos) {
|
|
278
|
+
if (todo.status === "cancelled") continue;
|
|
279
|
+
total += 1;
|
|
280
|
+
if (todo.status === "completed") done += 1;
|
|
281
|
+
}
|
|
282
|
+
return {
|
|
283
|
+
plan: total > 0 ? { done, total } : void 0,
|
|
284
|
+
startedAt: turnStart >= 0 ? messages[turnStart].created : void 0,
|
|
285
|
+
chars: turnStart >= 0 ? messages[turnStart].chars : void 0,
|
|
286
|
+
calls,
|
|
287
|
+
tool,
|
|
288
|
+
failed
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
function sessionProgress(measure, input) {
|
|
292
|
+
const startedAt = measure.startedAt ?? input.busySince;
|
|
293
|
+
if (input.status === "busy") {
|
|
294
|
+
return {
|
|
295
|
+
state: "running",
|
|
296
|
+
plan: measure.plan,
|
|
297
|
+
elapsedMs: Math.max(0, input.now - (startedAt ?? input.now)),
|
|
298
|
+
calls: measure.calls,
|
|
299
|
+
tool: measure.tool
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
if (input.stoppedAt === void 0) return void 0;
|
|
303
|
+
if (startedAt !== void 0 && input.stoppedAt < startedAt) return void 0;
|
|
304
|
+
if (input.now - input.stoppedAt > PROGRESS_LINGER_MS) return void 0;
|
|
305
|
+
return {
|
|
306
|
+
state: measure.failed ? "error" : "done",
|
|
307
|
+
plan: measure.plan,
|
|
308
|
+
elapsedMs: Math.max(0, input.stoppedAt - (startedAt ?? input.stoppedAt)),
|
|
309
|
+
calls: measure.calls,
|
|
310
|
+
tool: void 0
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
function rowProgress(measure, input) {
|
|
314
|
+
const startedAt = measure.startedAt ?? input.started;
|
|
315
|
+
return {
|
|
316
|
+
state: "running",
|
|
317
|
+
plan: measure.plan,
|
|
318
|
+
elapsedMs: Math.max(0, input.now - startedAt),
|
|
319
|
+
calls: measure.calls,
|
|
320
|
+
tool: measure.tool
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
var STATUS_WATCH_CAP = 256;
|
|
324
|
+
function createStatusWatch() {
|
|
325
|
+
const seen = /* @__PURE__ */ new Map();
|
|
326
|
+
return {
|
|
327
|
+
observe(sessionID, status, now) {
|
|
328
|
+
let entry = seen.get(sessionID);
|
|
329
|
+
if (!entry) {
|
|
330
|
+
entry = { status: "unknown" };
|
|
331
|
+
if (seen.size >= STATUS_WATCH_CAP) {
|
|
332
|
+
const oldest = seen.keys().next().value;
|
|
333
|
+
if (oldest !== void 0) seen.delete(oldest);
|
|
334
|
+
}
|
|
335
|
+
seen.set(sessionID, entry);
|
|
336
|
+
}
|
|
337
|
+
if (status === "busy" && entry.status !== "busy") {
|
|
338
|
+
entry.busySince = now;
|
|
339
|
+
entry.stoppedAt = void 0;
|
|
340
|
+
} else if (status === "idle" && entry.status === "busy") {
|
|
341
|
+
entry.stoppedAt = now;
|
|
342
|
+
}
|
|
343
|
+
if (status !== "unknown") entry.status = status;
|
|
344
|
+
return { busySince: entry.busySince, stoppedAt: entry.stoppedAt };
|
|
345
|
+
}
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
function createFillMemory() {
|
|
349
|
+
const held = /* @__PURE__ */ new Map();
|
|
350
|
+
return {
|
|
351
|
+
hold(sessionID, turn, fill) {
|
|
352
|
+
const entry = held.get(sessionID);
|
|
353
|
+
if (entry && entry.turn === turn) {
|
|
354
|
+
entry.fill = Math.max(entry.fill, fill);
|
|
355
|
+
return entry.fill;
|
|
356
|
+
}
|
|
357
|
+
if (!entry && held.size >= STATUS_WATCH_CAP) {
|
|
358
|
+
const oldest = held.keys().next().value;
|
|
359
|
+
if (oldest !== void 0) held.delete(oldest);
|
|
360
|
+
}
|
|
361
|
+
held.set(sessionID, { turn, fill });
|
|
362
|
+
return fill;
|
|
363
|
+
}
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
function filledCells(plan, width = BAR_WIDTH) {
|
|
367
|
+
if (plan.total <= 0 || plan.done <= 0) return 0;
|
|
368
|
+
if (plan.done >= plan.total) return width;
|
|
369
|
+
return Math.min(width - 1, Math.max(1, Math.round(plan.done / plan.total * width)));
|
|
370
|
+
}
|
|
371
|
+
function estimateCells(estimate, width = BAR_WIDTH) {
|
|
372
|
+
if (estimate.over) return width;
|
|
373
|
+
return Math.min(width - 1, Math.max(0, Math.round(estimate.fill * width)));
|
|
374
|
+
}
|
|
375
|
+
function formatElapsed(ms) {
|
|
376
|
+
const seconds = Math.max(0, Math.floor(ms / 1e3));
|
|
377
|
+
if (seconds < 60) return `${seconds}s`;
|
|
378
|
+
const minutes = Math.floor(seconds / 60);
|
|
379
|
+
const pad2 = (value) => String(value).padStart(2, "0");
|
|
380
|
+
if (minutes < 60) return `${minutes}m${pad2(seconds % 60)}s`;
|
|
381
|
+
return `${Math.floor(minutes / 60)}h${pad2(minutes % 60)}m`;
|
|
382
|
+
}
|
|
383
|
+
function layoutProgressLine(view, lead, width = SIDEBAR_ROW_WIDTH, frame = 0) {
|
|
384
|
+
const ended = view.state !== "running";
|
|
385
|
+
const endedRole = view.state === "error" ? "error" : "done";
|
|
386
|
+
const leadSegments = lead === "indent" ? [{ text: ROW_INDENT, role: "muted" }] : [
|
|
387
|
+
view.state === "running" ? { text: SPINNER_FRAMES[frame % SPINNER_FRAMES.length], role: "spinner" } : { text: view.state === "done" ? "\u2713" : "\u2717", role: endedRole },
|
|
388
|
+
{ text: " ", role: "muted" }
|
|
389
|
+
];
|
|
390
|
+
const estimate = !ended && !view.plan ? view.estimate : void 0;
|
|
391
|
+
const filled = ended ? BAR_WIDTH : view.plan ? filledCells(view.plan) : estimate ? estimateCells(estimate) : void 0;
|
|
392
|
+
const litRole = ended ? endedRole : estimate?.over ? "bar-over" : "bar-done";
|
|
393
|
+
const bar = [];
|
|
394
|
+
if (filled !== void 0) {
|
|
395
|
+
for (let cell = 0; cell < BAR_WIDTH; cell += 1) {
|
|
396
|
+
bar.push(cell < filled ? { text: BAR_DONE, role: litRole } : { text: BAR_TODO, role: "bar-track" });
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
const required = [];
|
|
400
|
+
if (view.plan) {
|
|
401
|
+
required.push({ text: `${view.plan.done}/${view.plan.total}`, role: ended ? "muted" : "fraction" });
|
|
402
|
+
required.push({ text: formatElapsed(view.elapsedMs), role: "muted" });
|
|
403
|
+
} else if (estimate) {
|
|
404
|
+
required.push({
|
|
405
|
+
text: `${formatElapsed(view.elapsedMs)}/~${formatElapsed(estimate.totalMs)}`,
|
|
406
|
+
role: estimate.over ? "estimate-over" : "estimate"
|
|
407
|
+
});
|
|
408
|
+
} else {
|
|
409
|
+
required.push({ text: formatElapsed(view.elapsedMs), role: "muted" });
|
|
410
|
+
}
|
|
411
|
+
const calls = !view.plan && !estimate && view.calls > 0 ? { text: `${view.calls} call${view.calls === 1 ? "" : "s"}`, role: "muted" } : void 0;
|
|
412
|
+
const tool = !ended && view.tool ? { text: view.tool, role: "tool" } : void 0;
|
|
413
|
+
const budget = width - leadSegments.reduce((sum, segment) => sum + cells(segment.text), 0) - (bar.length > 0 ? BAR_WIDTH + 2 : 0);
|
|
414
|
+
const widthOf = (pieces2) => pieces2.reduce((sum, piece) => sum + cells(piece.text), 0) + 3 * Math.max(0, pieces2.length - 1);
|
|
415
|
+
let pieces = [...required, ...calls ? [calls] : [], ...tool ? [tool] : []];
|
|
416
|
+
if (widthOf(pieces) > budget && calls) pieces = [...required, ...tool ? [tool] : []];
|
|
417
|
+
if (widthOf(pieces) > budget && tool) {
|
|
418
|
+
const room = budget - widthOf(required) - 3;
|
|
419
|
+
pieces = room >= 2 ? [...required, { text: truncate(tool.text, room), role: "tool" }] : required;
|
|
420
|
+
}
|
|
421
|
+
const tail = bar.length > 0 ? [{ text: " ", role: "muted" }] : [];
|
|
422
|
+
pieces.forEach((piece, index) => {
|
|
423
|
+
if (index > 0) tail.push({ text: " \xB7 ", role: "muted" });
|
|
424
|
+
tail.push(piece);
|
|
425
|
+
});
|
|
426
|
+
return { lead: leadSegments, bar, tail };
|
|
427
|
+
}
|
|
428
|
+
|
|
213
429
|
// src/spawn-record-store.ts
|
|
214
430
|
import { mkdirSync, readFileSync, renameSync, statSync, writeFileSync } from "fs";
|
|
215
431
|
import { homedir } from "os";
|
|
@@ -226,7 +442,7 @@ function isRecord(value) {
|
|
|
226
442
|
if (!record || typeof record !== "object" || Array.isArray(record)) return false;
|
|
227
443
|
if (typeof record.at !== "number" || !Number.isFinite(record.at)) return false;
|
|
228
444
|
const ok = (field) => field === void 0 || typeof field === "string";
|
|
229
|
-
if (!ok(record.model) || !ok(record.effort) || !ok(record.account)) return false;
|
|
445
|
+
if (!ok(record.model) || !ok(record.effort) || !ok(record.account) || !ok(record.agent)) return false;
|
|
230
446
|
return !!record.model;
|
|
231
447
|
}
|
|
232
448
|
function readEntries(file) {
|
|
@@ -242,6 +458,7 @@ function readEntries(file) {
|
|
|
242
458
|
model: record.model,
|
|
243
459
|
...record.effort ? { effort: record.effort } : {},
|
|
244
460
|
...record.account ? { account: record.account } : {},
|
|
461
|
+
...record.agent ? { agent: record.agent } : {},
|
|
245
462
|
at: record.at
|
|
246
463
|
});
|
|
247
464
|
}
|
|
@@ -271,12 +488,420 @@ function createSpawnRecordReader(file = spawnRecordStorePath) {
|
|
|
271
488
|
};
|
|
272
489
|
}
|
|
273
490
|
|
|
491
|
+
// src/turn-history.ts
|
|
492
|
+
import { mkdirSync as mkdirSync2, readFileSync as readFileSync2, renameSync as renameSync2, statSync as statSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
493
|
+
import { homedir as homedir2 } from "os";
|
|
494
|
+
import { join as join2 } from "path";
|
|
495
|
+
var MAX_TURN_SAMPLES = 48;
|
|
496
|
+
var TURN_SAMPLE_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
497
|
+
var MIN_TURN_SAMPLES = 3;
|
|
498
|
+
var CHARS_BANDWIDTH = Math.log(4);
|
|
499
|
+
var OTHER_EFFORT_WEIGHT = 0.5;
|
|
500
|
+
var CHARS_FLOOR = 50;
|
|
501
|
+
var pathOverride2 = null;
|
|
502
|
+
function turnHistoryStorePath() {
|
|
503
|
+
if (pathOverride2) return pathOverride2;
|
|
504
|
+
const stateRoot = process.env.XDG_STATE_HOME || join2(homedir2(), ".local", "state");
|
|
505
|
+
return join2(stateRoot, "opencode-claude-code-plugin", "turn-history.json");
|
|
506
|
+
}
|
|
507
|
+
function turnHistoryKey(agent, model) {
|
|
508
|
+
return `${agent || "main"}/${model}`;
|
|
509
|
+
}
|
|
510
|
+
var INJECTED_BLOCKS = [
|
|
511
|
+
/<system-reminder>[\s\S]*?<\/system-reminder>/g,
|
|
512
|
+
/<dcp-system-reminder>[\s\S]*?<\/dcp-system-reminder>/g,
|
|
513
|
+
/<dcp-message-id>[\s\S]*?<\/dcp-message-id>/g
|
|
514
|
+
];
|
|
515
|
+
function promptChars(texts) {
|
|
516
|
+
if (texts.length === 0) return void 0;
|
|
517
|
+
let total = 0;
|
|
518
|
+
for (const text3 of texts) {
|
|
519
|
+
let stripped = text3;
|
|
520
|
+
for (const block of INJECTED_BLOCKS) stripped = stripped.replace(block, "");
|
|
521
|
+
total += stripped.trim().length;
|
|
522
|
+
}
|
|
523
|
+
return total;
|
|
524
|
+
}
|
|
525
|
+
function isSample(value) {
|
|
526
|
+
const sample = value;
|
|
527
|
+
if (!sample || typeof sample !== "object" || Array.isArray(sample)) return false;
|
|
528
|
+
if (typeof sample.ms !== "number" || !Number.isFinite(sample.ms) || sample.ms < 0) return false;
|
|
529
|
+
if (typeof sample.at !== "number" || !Number.isFinite(sample.at)) return false;
|
|
530
|
+
if (sample.chars !== void 0 && (typeof sample.chars !== "number" || !Number.isFinite(sample.chars) || sample.chars < 0)) {
|
|
531
|
+
return false;
|
|
532
|
+
}
|
|
533
|
+
if (sample.effort !== void 0 && typeof sample.effort !== "string") return false;
|
|
534
|
+
return true;
|
|
535
|
+
}
|
|
536
|
+
function cleanSample(sample) {
|
|
537
|
+
return {
|
|
538
|
+
ms: Math.round(sample.ms),
|
|
539
|
+
...sample.chars !== void 0 ? { chars: Math.round(sample.chars) } : {},
|
|
540
|
+
...sample.effort ? { effort: sample.effort } : {},
|
|
541
|
+
at: sample.at
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
function readEntries2(file, now) {
|
|
545
|
+
const out = /* @__PURE__ */ new Map();
|
|
546
|
+
try {
|
|
547
|
+
const parsed = JSON.parse(readFileSync2(file, "utf8"));
|
|
548
|
+
const entries = parsed && typeof parsed === "object" ? parsed.turns : null;
|
|
549
|
+
if (!entries || typeof entries !== "object") return out;
|
|
550
|
+
const cutoff = now - TURN_SAMPLE_MAX_AGE_MS;
|
|
551
|
+
for (const [key, value] of Object.entries(entries)) {
|
|
552
|
+
if (!key || !Array.isArray(value)) continue;
|
|
553
|
+
const samples = value.filter(isSample).filter((sample) => sample.at >= cutoff).sort((a, b) => a.at - b.at).slice(-MAX_TURN_SAMPLES).map(cleanSample);
|
|
554
|
+
if (samples.length > 0) out.set(key, samples);
|
|
555
|
+
}
|
|
556
|
+
} catch {
|
|
557
|
+
}
|
|
558
|
+
return out;
|
|
559
|
+
}
|
|
560
|
+
function createTurnHistoryReader(file = turnHistoryStorePath) {
|
|
561
|
+
let identity = "";
|
|
562
|
+
let cached = /* @__PURE__ */ new Map();
|
|
563
|
+
return {
|
|
564
|
+
read(now = Date.now()) {
|
|
565
|
+
const path = file();
|
|
566
|
+
let next = "";
|
|
567
|
+
try {
|
|
568
|
+
const stats = statSync2(path);
|
|
569
|
+
next = `${stats.mtimeMs}:${stats.size}:${stats.ino}`;
|
|
570
|
+
} catch {
|
|
571
|
+
next = "missing";
|
|
572
|
+
}
|
|
573
|
+
if (next !== identity) {
|
|
574
|
+
identity = next;
|
|
575
|
+
cached = next === "missing" ? /* @__PURE__ */ new Map() : readEntries2(path, now);
|
|
576
|
+
}
|
|
577
|
+
return cached;
|
|
578
|
+
}
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
function weightedMedian(values, weights) {
|
|
582
|
+
const order = values.map((_, index) => index).sort((a, b) => values[a] - values[b]);
|
|
583
|
+
const total = order.reduce((sum, index) => sum + weights[index], 0);
|
|
584
|
+
let cumulative = 0;
|
|
585
|
+
for (const index of order) {
|
|
586
|
+
cumulative += weights[index];
|
|
587
|
+
if (cumulative >= total / 2) return values[index];
|
|
588
|
+
}
|
|
589
|
+
return values[order[order.length - 1]];
|
|
590
|
+
}
|
|
591
|
+
function charsWeight(a, b) {
|
|
592
|
+
if (a === void 0 || b === void 0) return 1;
|
|
593
|
+
const distance = Math.log((a + CHARS_FLOOR) / (b + CHARS_FLOOR)) / CHARS_BANDWIDTH;
|
|
594
|
+
return Math.exp(-0.5 * distance * distance);
|
|
595
|
+
}
|
|
596
|
+
function effortWeight(a, b) {
|
|
597
|
+
return a === void 0 || b === void 0 || a === b ? 1 : OTHER_EFFORT_WEIGHT;
|
|
598
|
+
}
|
|
599
|
+
function estimateTurn(samples, input) {
|
|
600
|
+
const usable = samples.filter((sample) => Number.isFinite(sample.ms) && sample.ms >= 0);
|
|
601
|
+
if (usable.length < MIN_TURN_SAMPLES) return void 0;
|
|
602
|
+
const elapsed = Math.max(0, input.elapsedMs);
|
|
603
|
+
const weights = usable.map(
|
|
604
|
+
(sample) => charsWeight(input.chars, sample.chars) * effortWeight(input.effort, sample.effort)
|
|
605
|
+
);
|
|
606
|
+
const survivors = usable.map((sample, index) => ({ ms: sample.ms, weight: weights[index] })).filter((sample) => sample.ms > elapsed);
|
|
607
|
+
if (survivors.length === 0) {
|
|
608
|
+
const usual = weightedMedian(usable.map((sample) => sample.ms), weights);
|
|
609
|
+
return { totalMs: usual, fill: 1, over: true };
|
|
610
|
+
}
|
|
611
|
+
const residual = weightedMedian(
|
|
612
|
+
survivors.map((sample) => sample.ms - elapsed),
|
|
613
|
+
survivors.map((sample) => sample.weight)
|
|
614
|
+
);
|
|
615
|
+
const totalMs = elapsed + residual;
|
|
616
|
+
return { totalMs, fill: totalMs > 0 ? elapsed / totalMs : 0, over: false };
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// src/account-usage.ts
|
|
620
|
+
import { mkdirSync as mkdirSync3, readFileSync as readFileSync3, renameSync as renameSync3, statSync as statSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
621
|
+
import { homedir as homedir3 } from "os";
|
|
622
|
+
import { join as join3 } from "path";
|
|
623
|
+
var UNTIMED_READING_TTL_MS = 5 * 60 * 60 * 1e3;
|
|
624
|
+
var USAGE_READING_MAX_AGE_MS = 8 * 24 * 60 * 60 * 1e3;
|
|
625
|
+
var MAX_USAGE_ACCOUNTS = 32;
|
|
626
|
+
var NAME = /^[a-z0-9_-]{1,64}$/;
|
|
627
|
+
var STATUS = /^[a-z_]{1,32}$/;
|
|
628
|
+
var TIER = /^[A-Za-z0-9][A-Za-z0-9 ]{0,23}$/;
|
|
629
|
+
var MAX_RESETS_TEXT = 80;
|
|
630
|
+
var pathOverride3 = null;
|
|
631
|
+
function accountUsageStorePath() {
|
|
632
|
+
if (pathOverride3) return pathOverride3;
|
|
633
|
+
const stateRoot = process.env.XDG_STATE_HOME || join3(homedir3(), ".local", "state");
|
|
634
|
+
return join3(stateRoot, "opencode-claude-code-plugin", "account-usage.json");
|
|
635
|
+
}
|
|
636
|
+
function finite(value) {
|
|
637
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
638
|
+
}
|
|
639
|
+
function validEntry(value) {
|
|
640
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
641
|
+
const entry = value;
|
|
642
|
+
if (!finite(entry.observedAt)) return null;
|
|
643
|
+
if (typeof entry.status !== "string" || !STATUS.test(entry.status)) return null;
|
|
644
|
+
if (entry.utilization !== null && !finite(entry.utilization)) return null;
|
|
645
|
+
if (entry.resetsAt !== void 0 && !finite(entry.resetsAt)) return null;
|
|
646
|
+
if (entry.resetsText !== void 0 && (typeof entry.resetsText !== "string" || entry.resetsText.length > MAX_RESETS_TEXT)) {
|
|
647
|
+
return null;
|
|
648
|
+
}
|
|
649
|
+
const noted = Array.isArray(entry.noted) ? [...new Set(entry.noted.filter((t) => finite(t) && t > 0 && t <= 100))].sort(
|
|
650
|
+
(a, b) => a - b
|
|
651
|
+
) : [];
|
|
652
|
+
return {
|
|
653
|
+
utilization: entry.utilization,
|
|
654
|
+
...entry.resetsAt === void 0 ? {} : { resetsAt: entry.resetsAt },
|
|
655
|
+
...entry.resetsText === void 0 ? {} : { resetsText: entry.resetsText },
|
|
656
|
+
status: entry.status,
|
|
657
|
+
observedAt: entry.observedAt,
|
|
658
|
+
noted
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
var emptyStore = () => ({ usage: /* @__PURE__ */ new Map(), known: /* @__PURE__ */ new Map() });
|
|
662
|
+
function readKnown(parsed) {
|
|
663
|
+
const out = /* @__PURE__ */ new Map();
|
|
664
|
+
const known = parsed.known;
|
|
665
|
+
if (!known || typeof known !== "object" || Array.isArray(known)) return out;
|
|
666
|
+
for (const [account, raw] of Object.entries(known)) {
|
|
667
|
+
if (out.size >= MAX_USAGE_ACCOUNTS) break;
|
|
668
|
+
if (!NAME.test(account) || !raw || typeof raw !== "object" || Array.isArray(raw)) continue;
|
|
669
|
+
const record = raw;
|
|
670
|
+
if (!finite(record.at)) continue;
|
|
671
|
+
const tier = typeof record.tier === "string" && TIER.test(record.tier) ? record.tier : void 0;
|
|
672
|
+
out.set(account, { ...tier ? { tier } : {}, at: record.at });
|
|
673
|
+
}
|
|
674
|
+
return out;
|
|
675
|
+
}
|
|
676
|
+
function parseStore(file, now) {
|
|
677
|
+
const out = emptyStore();
|
|
678
|
+
try {
|
|
679
|
+
const parsed = JSON.parse(readFileSync3(file, "utf8"));
|
|
680
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return out;
|
|
681
|
+
out.known = readKnown(parsed);
|
|
682
|
+
const accounts = parsed.accounts;
|
|
683
|
+
if (!accounts || typeof accounts !== "object" || Array.isArray(accounts)) return out;
|
|
684
|
+
const cutoff = now - USAGE_READING_MAX_AGE_MS;
|
|
685
|
+
for (const [account, windows] of Object.entries(accounts)) {
|
|
686
|
+
if (!NAME.test(account) || !windows || typeof windows !== "object") continue;
|
|
687
|
+
const kept = /* @__PURE__ */ new Map();
|
|
688
|
+
for (const [window, raw] of Object.entries(windows)) {
|
|
689
|
+
if (!NAME.test(window)) continue;
|
|
690
|
+
const entry = validEntry(raw);
|
|
691
|
+
if (entry && entry.observedAt >= cutoff) kept.set(window, entry);
|
|
692
|
+
}
|
|
693
|
+
if (kept.size > 0) out.usage.set(account, kept);
|
|
694
|
+
}
|
|
695
|
+
} catch {
|
|
696
|
+
}
|
|
697
|
+
return out;
|
|
698
|
+
}
|
|
699
|
+
function isReadingCurrent(entry, now = Date.now()) {
|
|
700
|
+
if (entry.resetsAt !== void 0) return entry.resetsAt > now;
|
|
701
|
+
return now - entry.observedAt < UNTIMED_READING_TTL_MS;
|
|
702
|
+
}
|
|
703
|
+
function usagePercent(utilization) {
|
|
704
|
+
return Math.round(utilization * 1e3) / 10;
|
|
705
|
+
}
|
|
706
|
+
function createAccountUsageReader(file = accountUsageStorePath) {
|
|
707
|
+
let identity = "";
|
|
708
|
+
let store = emptyStore();
|
|
709
|
+
const refresh = () => {
|
|
710
|
+
const path = file();
|
|
711
|
+
let next = "";
|
|
712
|
+
try {
|
|
713
|
+
const stats = statSync3(path);
|
|
714
|
+
next = `${stats.mtimeMs}:${stats.size}:${stats.ino}`;
|
|
715
|
+
} catch {
|
|
716
|
+
next = "missing";
|
|
717
|
+
}
|
|
718
|
+
if (next !== identity) {
|
|
719
|
+
identity = next;
|
|
720
|
+
store = next === "missing" ? emptyStore() : parseStore(path, Date.now());
|
|
721
|
+
}
|
|
722
|
+
};
|
|
723
|
+
return {
|
|
724
|
+
read() {
|
|
725
|
+
refresh();
|
|
726
|
+
return store.usage;
|
|
727
|
+
},
|
|
728
|
+
/** The accounts registered at startup, with their tiers: the same map while the file is unchanged. */
|
|
729
|
+
known() {
|
|
730
|
+
refresh();
|
|
731
|
+
return store.known;
|
|
732
|
+
}
|
|
733
|
+
};
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
// src/account-sidebar.ts
|
|
737
|
+
var WARNING_PERCENT = 75;
|
|
738
|
+
var NO_READING_TEXT = "no reading yet";
|
|
739
|
+
var RESET_WORD = "resets ";
|
|
740
|
+
var SEPARATOR = " \xB7 ";
|
|
741
|
+
var SHORT_WINDOWS = {
|
|
742
|
+
five_hour: "5h",
|
|
743
|
+
seven_day: "7d",
|
|
744
|
+
seven_day_opus: "7d opus",
|
|
745
|
+
seven_day_sonnet: "7d sonnet",
|
|
746
|
+
seven_day_overage_included: "7d extra",
|
|
747
|
+
overage: "extra"
|
|
748
|
+
};
|
|
749
|
+
function shortWindowName(window) {
|
|
750
|
+
if (window === "unknown") return "";
|
|
751
|
+
return SHORT_WINDOWS[window] ?? window.replace(/_/g, " ");
|
|
752
|
+
}
|
|
753
|
+
var pad = (value) => String(value).padStart(2, "0");
|
|
754
|
+
function sameDay(a, b) {
|
|
755
|
+
return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
|
|
756
|
+
}
|
|
757
|
+
function formatResetClock(resetsAt, now, year = true) {
|
|
758
|
+
const when = new Date(resetsAt);
|
|
759
|
+
const clock = `${pad(when.getHours())}:${pad(when.getMinutes())}`;
|
|
760
|
+
if (sameDay(when, new Date(now))) return clock;
|
|
761
|
+
const date = `${pad(when.getMonth() + 1)}-${pad(when.getDate())} ${clock}`;
|
|
762
|
+
return year ? `${when.getFullYear()}-${date}` : date;
|
|
763
|
+
}
|
|
764
|
+
function resetSpellings(entry, now) {
|
|
765
|
+
if (entry.resetsAt !== void 0) {
|
|
766
|
+
const full = formatResetClock(entry.resetsAt, now);
|
|
767
|
+
if (sameDay(new Date(entry.resetsAt), new Date(now))) return [`${RESET_WORD}${full}`];
|
|
768
|
+
return [`${RESET_WORD}${full}`, full, formatResetClock(entry.resetsAt, now, false)];
|
|
769
|
+
}
|
|
770
|
+
if (entry.resetsText) return [`${RESET_WORD}${entry.resetsText}`, entry.resetsText];
|
|
771
|
+
return [];
|
|
772
|
+
}
|
|
773
|
+
function rank(entry) {
|
|
774
|
+
if (entry.status === "rejected") return 1e3;
|
|
775
|
+
if (entry.utilization === null) return -1;
|
|
776
|
+
return usagePercent(entry.utilization);
|
|
777
|
+
}
|
|
778
|
+
function pressingWindow(windows, now) {
|
|
779
|
+
let best;
|
|
780
|
+
for (const [window, entry] of windows) {
|
|
781
|
+
if (!isReadingCurrent(entry, now)) continue;
|
|
782
|
+
if (!best || rank(entry) > rank(best[1])) best = [window, entry];
|
|
783
|
+
}
|
|
784
|
+
return best;
|
|
785
|
+
}
|
|
786
|
+
var cells2 = (value) => Array.from(value).length;
|
|
787
|
+
var segmentsCells = (segments) => segments.reduce((sum, segment) => sum + cells2(segment.text), 0);
|
|
788
|
+
function levelRole(state) {
|
|
789
|
+
return state === "limited" ? "error" : state === "warning" ? "effort" : "muted";
|
|
790
|
+
}
|
|
791
|
+
function fitSegments(segments, budget) {
|
|
792
|
+
const out = [];
|
|
793
|
+
let used = 0;
|
|
794
|
+
for (const segment of segments) {
|
|
795
|
+
const length = cells2(segment.text);
|
|
796
|
+
if (used + length <= budget) {
|
|
797
|
+
out.push(segment);
|
|
798
|
+
used += length;
|
|
799
|
+
continue;
|
|
800
|
+
}
|
|
801
|
+
if (segment.text !== SEPARATOR) {
|
|
802
|
+
const cut = truncate(segment.text, budget - used);
|
|
803
|
+
if (cut) out.push({ ...segment, text: cut });
|
|
804
|
+
}
|
|
805
|
+
break;
|
|
806
|
+
}
|
|
807
|
+
return out;
|
|
808
|
+
}
|
|
809
|
+
function tailSegments(draft, spelling, withTier) {
|
|
810
|
+
if (draft.state === "none") {
|
|
811
|
+
const out2 = [];
|
|
812
|
+
if (withTier && draft.tier) out2.push({ text: SEPARATOR, role: "muted" }, { text: draft.tier, role: "tier" });
|
|
813
|
+
out2.push({ text: SEPARATOR, role: "muted" }, { text: NO_READING_TEXT, role: "muted" });
|
|
814
|
+
return out2;
|
|
815
|
+
}
|
|
816
|
+
const out = [
|
|
817
|
+
{ text: SEPARATOR, role: "muted" },
|
|
818
|
+
{ text: draft.level, role: levelRole(draft.state) }
|
|
819
|
+
];
|
|
820
|
+
const reset = draft.spellings[Math.min(spelling, draft.spellings.length - 1)];
|
|
821
|
+
if (reset) {
|
|
822
|
+
out.push({ text: SEPARATOR, role: "muted" });
|
|
823
|
+
if (reset.startsWith(RESET_WORD)) {
|
|
824
|
+
out.push({ text: RESET_WORD, role: "muted" }, { text: reset.slice(RESET_WORD.length), role: "reset" });
|
|
825
|
+
} else {
|
|
826
|
+
out.push({ text: reset, role: "reset" });
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
return out;
|
|
830
|
+
}
|
|
831
|
+
function accountUsageLines(usage, now, width = SIDEBAR_ROW_WIDTH, known = /* @__PURE__ */ new Map()) {
|
|
832
|
+
const drafts = [];
|
|
833
|
+
const names = [.../* @__PURE__ */ new Set([...usage.keys(), ...known.keys()])].sort();
|
|
834
|
+
for (const account of names) {
|
|
835
|
+
const name = truncate(account, Math.max(1, Math.floor(width / 2)));
|
|
836
|
+
const windows = usage.get(account);
|
|
837
|
+
const pressing = windows ? pressingWindow(windows, now) : void 0;
|
|
838
|
+
if (!pressing) {
|
|
839
|
+
if (!known.has(account)) continue;
|
|
840
|
+
drafts.push({
|
|
841
|
+
account,
|
|
842
|
+
window: "",
|
|
843
|
+
state: "none",
|
|
844
|
+
name,
|
|
845
|
+
level: "",
|
|
846
|
+
tier: known.get(account)?.tier,
|
|
847
|
+
spellings: []
|
|
848
|
+
});
|
|
849
|
+
continue;
|
|
850
|
+
}
|
|
851
|
+
const [window, entry] = pressing;
|
|
852
|
+
const state = entry.status === "rejected" ? "limited" : entry.utilization !== null && usagePercent(entry.utilization) >= WARNING_PERCENT ? "warning" : "ok";
|
|
853
|
+
const short = shortWindowName(window);
|
|
854
|
+
const level = state === "limited" ? "limited" : entry.utilization === null ? `${short ? `${short} ` : ""}ok` : `${short ? `${short} ` : ""}${Math.round(usagePercent(entry.utilization))}%`;
|
|
855
|
+
drafts.push({ account, window, state, name, level, spellings: resetSpellings(entry, now) });
|
|
856
|
+
}
|
|
857
|
+
const budget = (draft) => Math.max(0, width - cells2(draft.name));
|
|
858
|
+
const spelt = drafts.filter((draft) => draft.state !== "none");
|
|
859
|
+
const last = Math.max(0, ...spelt.map((draft) => draft.spellings.length - 1));
|
|
860
|
+
let spelling = 0;
|
|
861
|
+
while (spelling < last && spelt.some((draft) => segmentsCells(tailSegments(draft, spelling, true)) > budget(draft))) {
|
|
862
|
+
spelling += 1;
|
|
863
|
+
}
|
|
864
|
+
return drafts.map((draft) => {
|
|
865
|
+
const full = tailSegments(draft, spelling, true);
|
|
866
|
+
const tail = segmentsCells(full) <= budget(draft) ? full : tailSegments(draft, spelling, false);
|
|
867
|
+
return {
|
|
868
|
+
account: draft.account,
|
|
869
|
+
window: draft.window,
|
|
870
|
+
state: draft.state,
|
|
871
|
+
segments: [{ text: draft.name, role: "agent" }, ...fitSegments(tail, budget(draft))]
|
|
872
|
+
};
|
|
873
|
+
});
|
|
874
|
+
}
|
|
875
|
+
|
|
274
876
|
// src/tui.ts
|
|
275
877
|
var SUBAGENTS_TUI_PLUGIN_ID = "opencode-claude-code-plugin.subagents";
|
|
276
878
|
var SECTION_ORDER = 900;
|
|
277
879
|
var SPINNER_INTERVAL_MS = 100;
|
|
278
880
|
var REFRESH_INTERVAL_MS = 1500;
|
|
881
|
+
var PROGRESS_TICK_MS = 1e3;
|
|
882
|
+
var FRAME_WRAP = SPINNER_FRAMES.length * 600;
|
|
279
883
|
var TASK_TOOLS = /* @__PURE__ */ new Set(["task", "subagent"]);
|
|
884
|
+
var ROLE_COLOR = {
|
|
885
|
+
spinner: "spinner",
|
|
886
|
+
done: "done",
|
|
887
|
+
error: "error",
|
|
888
|
+
agent: "text",
|
|
889
|
+
description: "text",
|
|
890
|
+
muted: "muted",
|
|
891
|
+
effort: "effort",
|
|
892
|
+
"effort-faded": "effortFaded",
|
|
893
|
+
"bar-done": "barDone",
|
|
894
|
+
// Past every recorded turn: the warning colour, which the effort already takes.
|
|
895
|
+
"bar-over": "effort",
|
|
896
|
+
"bar-track": "barTrack",
|
|
897
|
+
fraction: "fraction",
|
|
898
|
+
estimate: "fraction",
|
|
899
|
+
"estimate-over": "effort",
|
|
900
|
+
tool: "tool",
|
|
901
|
+
cost: "cost",
|
|
902
|
+
tier: "tier",
|
|
903
|
+
reset: "reset"
|
|
904
|
+
};
|
|
280
905
|
function attempt(fn, fallback) {
|
|
281
906
|
try {
|
|
282
907
|
return fn();
|
|
@@ -296,64 +921,131 @@ function mix(a, b, weight) {
|
|
|
296
921
|
);
|
|
297
922
|
}
|
|
298
923
|
function roleColor(colors, role) {
|
|
299
|
-
|
|
300
|
-
case "spinner":
|
|
301
|
-
return colors.spinner;
|
|
302
|
-
case "done":
|
|
303
|
-
return colors.done;
|
|
304
|
-
case "error":
|
|
305
|
-
return colors.error;
|
|
306
|
-
case "effort":
|
|
307
|
-
return colors.effort;
|
|
308
|
-
case "effort-faded":
|
|
309
|
-
return colors.effortFaded;
|
|
310
|
-
case "agent":
|
|
311
|
-
case "description":
|
|
312
|
-
return colors.text;
|
|
313
|
-
default:
|
|
314
|
-
return colors.muted;
|
|
315
|
-
}
|
|
924
|
+
return colors[ROLE_COLOR[role] ?? "muted"];
|
|
316
925
|
}
|
|
926
|
+
var EMPTY_SOURCE = { messages: [], todos: [] };
|
|
927
|
+
var EMPTY_VIEW = { rows: [], lines: /* @__PURE__ */ new Map() };
|
|
317
928
|
function createSidebar(host) {
|
|
318
929
|
const [frame, setFrame] = createSignal(0);
|
|
319
930
|
const [now, setNow] = createSignal(Date.now());
|
|
320
931
|
const records = createSpawnRecordReader();
|
|
321
932
|
let recordMap = attempt(() => records.read(), /* @__PURE__ */ new Map());
|
|
933
|
+
const history = createTurnHistoryReader();
|
|
934
|
+
let historyMap = attempt(() => history.read(), /* @__PURE__ */ new Map());
|
|
322
935
|
const [recordVersion, setRecordVersion] = createSignal(0);
|
|
936
|
+
const watch = createStatusWatch();
|
|
937
|
+
const fills = createFillMemory();
|
|
938
|
+
const usageReader = createAccountUsageReader();
|
|
939
|
+
const [usage, setUsage] = createSignal(attempt(() => usageReader.read(), /* @__PURE__ */ new Map()));
|
|
940
|
+
const [known, setKnown] = createSignal(attempt(() => usageReader.known(), /* @__PURE__ */ new Map()));
|
|
941
|
+
const [usageNow, setUsageNow] = createSignal(Date.now());
|
|
323
942
|
const sectionsRunning = /* @__PURE__ */ new Set();
|
|
324
943
|
const sectionsShowing = /* @__PURE__ */ new Set();
|
|
944
|
+
const sectionsMounted = /* @__PURE__ */ new Set();
|
|
325
945
|
let spinner;
|
|
326
946
|
let refresh;
|
|
947
|
+
let refreshPeriod = 0;
|
|
948
|
+
const refreshTick = () => {
|
|
949
|
+
if (sectionsShowing.size > 0) {
|
|
950
|
+
const next = attempt(() => records.read(), recordMap);
|
|
951
|
+
const nextHistory = attempt(() => history.read(), historyMap);
|
|
952
|
+
if (next !== recordMap || nextHistory !== historyMap) {
|
|
953
|
+
recordMap = next;
|
|
954
|
+
historyMap = nextHistory;
|
|
955
|
+
setRecordVersion((value) => value + 1);
|
|
956
|
+
}
|
|
957
|
+
setNow(Date.now());
|
|
958
|
+
}
|
|
959
|
+
setUsage(attempt(() => usageReader.read(), usage()));
|
|
960
|
+
setKnown(attempt(() => usageReader.known(), known()));
|
|
961
|
+
const minute = Math.floor(Date.now() / 6e4) * 6e4;
|
|
962
|
+
if (minute !== usageNow()) setUsageNow(minute);
|
|
963
|
+
};
|
|
327
964
|
const syncClocks = () => {
|
|
328
965
|
if (sectionsRunning.size > 0 && !spinner) {
|
|
329
|
-
spinner = setInterval(() => setFrame((value) => (value + 1) %
|
|
966
|
+
spinner = setInterval(() => setFrame((value) => (value + 1) % FRAME_WRAP), SPINNER_INTERVAL_MS);
|
|
330
967
|
} else if (sectionsRunning.size === 0 && spinner) {
|
|
331
968
|
clearInterval(spinner);
|
|
332
969
|
spinner = void 0;
|
|
333
970
|
}
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
setNow(Date.now());
|
|
342
|
-
}, REFRESH_INTERVAL_MS);
|
|
343
|
-
} else if (sectionsShowing.size === 0 && refresh) {
|
|
971
|
+
const wanted = sectionsShowing.size > 0 || sectionsMounted.size > 0;
|
|
972
|
+
const period = sectionsRunning.size > 0 ? PROGRESS_TICK_MS : REFRESH_INTERVAL_MS;
|
|
973
|
+
if (wanted && (!refresh || refreshPeriod !== period)) {
|
|
974
|
+
if (refresh) clearInterval(refresh);
|
|
975
|
+
refreshPeriod = period;
|
|
976
|
+
refresh = setInterval(refreshTick, period);
|
|
977
|
+
} else if (!wanted && refresh) {
|
|
344
978
|
clearInterval(refresh);
|
|
345
979
|
refresh = void 0;
|
|
980
|
+
refreshPeriod = 0;
|
|
346
981
|
}
|
|
347
982
|
};
|
|
348
|
-
const
|
|
983
|
+
const progressOf = (sessionID) => attempt(() => host.progress(sessionID), EMPTY_SOURCE);
|
|
984
|
+
const estimateFor = (sessionID, measure, view, agentFallback) => {
|
|
985
|
+
if (view.state !== "running") return view;
|
|
986
|
+
const record = recordMap.get(sessionID);
|
|
987
|
+
if (!record?.model) return view;
|
|
988
|
+
const samples = historyMap.get(turnHistoryKey(record.agent ?? agentFallback, record.model));
|
|
989
|
+
if (!samples) return view;
|
|
990
|
+
const estimate = estimateTurn(samples, { elapsedMs: view.elapsedMs, chars: measure.chars, effort: record.effort });
|
|
991
|
+
if (!estimate) return view;
|
|
992
|
+
const turn = measure.startedAt ?? 0;
|
|
993
|
+
return { ...view, estimate: { ...estimate, fill: fills.hold(sessionID, turn, estimate.fill) } };
|
|
994
|
+
};
|
|
995
|
+
const deriveView = (sessionID) => {
|
|
349
996
|
recordVersion();
|
|
350
|
-
|
|
351
|
-
|
|
997
|
+
now();
|
|
998
|
+
const clock = Date.now();
|
|
999
|
+
const gathered = host.gather(sessionID);
|
|
1000
|
+
const rows = deriveSubagentRows({
|
|
1001
|
+
...gathered,
|
|
352
1002
|
record: (id) => recordMap.get(id),
|
|
353
|
-
now:
|
|
1003
|
+
now: clock
|
|
1004
|
+
});
|
|
1005
|
+
const lines = /* @__PURE__ */ new Map();
|
|
1006
|
+
for (const row of rows) {
|
|
1007
|
+
if (row.state !== "running" || !row.sessionID) continue;
|
|
1008
|
+
const measure2 = measureProgress(progressOf(row.sessionID));
|
|
1009
|
+
const line = rowProgress(measure2, { started: row.started, now: clock });
|
|
1010
|
+
lines.set(row.sessionID, estimateFor(row.sessionID, measure2, line, row.agent));
|
|
1011
|
+
}
|
|
1012
|
+
const status = gathered.status(sessionID);
|
|
1013
|
+
const memory = watch.observe(sessionID, status, clock);
|
|
1014
|
+
const measure = measureProgress(progressOf(sessionID));
|
|
1015
|
+
const session = sessionProgress(measure, {
|
|
1016
|
+
status,
|
|
1017
|
+
busySince: memory.busySince,
|
|
1018
|
+
stoppedAt: gathered.stoppedAt(sessionID) ?? memory.stoppedAt,
|
|
1019
|
+
now: clock
|
|
1020
|
+
});
|
|
1021
|
+
const progress = session ? estimateFor(sessionID, measure, session) : void 0;
|
|
1022
|
+
return { rows, lines, progress };
|
|
1023
|
+
};
|
|
1024
|
+
const styled = (segment) => {
|
|
1025
|
+
const span = createElement(segment.role === "fraction" ? "b" : "span");
|
|
1026
|
+
insertNode(span, createTextNode(segment.text));
|
|
1027
|
+
effect(() => setProp(span, "style", { fg: roleColor(host.colors(), segment.role) }));
|
|
1028
|
+
return span;
|
|
1029
|
+
};
|
|
1030
|
+
const buildProgressLine = (view, lead) => {
|
|
1031
|
+
const line = createElement("text");
|
|
1032
|
+
setProp(line, "wrapMode", "none");
|
|
1033
|
+
const layout = layoutProgressLine(view, lead, SIDEBAR_ROW_WIDTH);
|
|
1034
|
+
layout.lead.forEach((segment, index) => {
|
|
1035
|
+
if (index === 0 && lead === "glyph" && view.state === "running") {
|
|
1036
|
+
const span = createElement("span");
|
|
1037
|
+
insert(span, () => SPINNER_FRAMES[frame() % SPINNER_FRAMES.length]);
|
|
1038
|
+
effect(() => setProp(span, "style", { fg: roleColor(host.colors(), segment.role) }));
|
|
1039
|
+
insertNode(line, span);
|
|
1040
|
+
} else {
|
|
1041
|
+
insertNode(line, styled(segment));
|
|
1042
|
+
}
|
|
354
1043
|
});
|
|
1044
|
+
for (const segment of layout.bar) insertNode(line, styled(segment));
|
|
1045
|
+
for (const segment of layout.tail) insertNode(line, styled(segment));
|
|
1046
|
+
return line;
|
|
355
1047
|
};
|
|
356
|
-
const buildRow = (row) => {
|
|
1048
|
+
const buildRow = (row, progress) => {
|
|
357
1049
|
const box = createElement("box");
|
|
358
1050
|
const layout = layoutRow(row, SIDEBAR_ROW_WIDTH);
|
|
359
1051
|
const first = createElement("text");
|
|
@@ -372,14 +1064,10 @@ function createSidebar(host) {
|
|
|
372
1064
|
if (layout.second.length > 0) {
|
|
373
1065
|
const second = createElement("text");
|
|
374
1066
|
setProp(second, "wrapMode", "none");
|
|
375
|
-
for (const segment of layout.second)
|
|
376
|
-
const span = createElement("span");
|
|
377
|
-
insertNode(span, createTextNode(segment.text));
|
|
378
|
-
effect(() => setProp(span, "style", { fg: roleColor(host.colors(), segment.role) }));
|
|
379
|
-
insertNode(second, span);
|
|
380
|
-
}
|
|
1067
|
+
for (const segment of layout.second) insertNode(second, styled(segment));
|
|
381
1068
|
insertNode(box, second);
|
|
382
1069
|
}
|
|
1070
|
+
if (progress) insertNode(box, attempt(() => buildProgressLine(progress, "indent"), createElement("box")));
|
|
383
1071
|
if (row.sessionID) {
|
|
384
1072
|
const sessionID = row.sessionID;
|
|
385
1073
|
setProp(box, "onMouseDown", () => attempt(() => host.navigate(sessionID), void 0));
|
|
@@ -396,7 +1084,16 @@ function createSidebar(host) {
|
|
|
396
1084
|
}
|
|
397
1085
|
return box;
|
|
398
1086
|
};
|
|
399
|
-
const
|
|
1087
|
+
const buildHeading = (label) => {
|
|
1088
|
+
const title = createElement("text");
|
|
1089
|
+
const bold = createElement("b");
|
|
1090
|
+
insertNode(bold, createTextNode(label));
|
|
1091
|
+
insertNode(title, bold);
|
|
1092
|
+
effect(() => setProp(title, "fg", host.colors().text));
|
|
1093
|
+
return title;
|
|
1094
|
+
};
|
|
1095
|
+
const buildSection = (view, expanded, setExpanded) => {
|
|
1096
|
+
const list = view.rows;
|
|
400
1097
|
const section = createElement("box");
|
|
401
1098
|
const header = createElement("box");
|
|
402
1099
|
setProp(header, "flexDirection", "row");
|
|
@@ -408,54 +1105,99 @@ function createSidebar(host) {
|
|
|
408
1105
|
effect(() => setProp(caret, "fg", host.colors().text));
|
|
409
1106
|
insertNode(header, caret);
|
|
410
1107
|
}
|
|
411
|
-
const title =
|
|
412
|
-
const bold = createElement("b");
|
|
413
|
-
insertNode(bold, createTextNode("Subagents"));
|
|
414
|
-
insertNode(title, bold);
|
|
1108
|
+
const title = buildHeading("Subagents");
|
|
415
1109
|
const summary = createElement("span");
|
|
416
1110
|
insert(summary, () => expanded() ? "" : ` (${collapsedSummary(list)})`);
|
|
417
1111
|
effect(() => setProp(summary, "style", { fg: host.colors().muted }));
|
|
418
1112
|
insertNode(title, summary);
|
|
419
|
-
effect(() => setProp(title, "fg", host.colors().text));
|
|
420
1113
|
insertNode(header, title);
|
|
421
1114
|
insertNode(section, header);
|
|
422
1115
|
const body = createElement("box");
|
|
423
|
-
insert(
|
|
1116
|
+
insert(
|
|
1117
|
+
body,
|
|
1118
|
+
() => expanded() || list.length <= 2 ? list.map((row) => buildRow(row, row.sessionID ? view.lines.get(row.sessionID) : void 0)) : null
|
|
1119
|
+
);
|
|
1120
|
+
insertNode(section, body);
|
|
1121
|
+
return section;
|
|
1122
|
+
};
|
|
1123
|
+
const buildProgress = (progress) => {
|
|
1124
|
+
const section = createElement("box");
|
|
1125
|
+
insertNode(section, buildHeading("Main Progress"));
|
|
1126
|
+
const body = createElement("box");
|
|
1127
|
+
insert(body, () => {
|
|
1128
|
+
const view = progress();
|
|
1129
|
+
return view ? attempt(() => buildProgressLine(view, "glyph"), null) : null;
|
|
1130
|
+
});
|
|
424
1131
|
insertNode(section, body);
|
|
425
1132
|
return section;
|
|
426
1133
|
};
|
|
1134
|
+
const buildAccounts = (lines) => {
|
|
1135
|
+
const section = createElement("box");
|
|
1136
|
+
insertNode(section, buildHeading("Accounts"));
|
|
1137
|
+
for (const line of lines) {
|
|
1138
|
+
const text3 = createElement("text");
|
|
1139
|
+
setProp(text3, "wrapMode", "none");
|
|
1140
|
+
for (const segment of line.segments) insertNode(text3, styled(segment));
|
|
1141
|
+
insertNode(section, text3);
|
|
1142
|
+
}
|
|
1143
|
+
return section;
|
|
1144
|
+
};
|
|
427
1145
|
const render = (sessionID) => {
|
|
428
1146
|
const root = createElement("box");
|
|
1147
|
+
const progressBox = createElement("box");
|
|
1148
|
+
const subagentsBox = createElement("box");
|
|
1149
|
+
const accountsBox = createElement("box");
|
|
1150
|
+
insertNode(root, progressBox);
|
|
1151
|
+
insertNode(root, subagentsBox);
|
|
1152
|
+
insertNode(root, accountsBox);
|
|
429
1153
|
const key = /* @__PURE__ */ Symbol("subagents-section");
|
|
1154
|
+
sectionsMounted.add(key);
|
|
430
1155
|
const [expanded, setExpanded] = createSignal(true);
|
|
431
|
-
const
|
|
1156
|
+
const view = createMemo(() => {
|
|
432
1157
|
const parentID = sessionID();
|
|
433
|
-
if (!parentID) return
|
|
434
|
-
return attempt(() =>
|
|
1158
|
+
if (!parentID) return EMPTY_VIEW;
|
|
1159
|
+
return attempt(() => deriveView(parentID), EMPTY_VIEW);
|
|
435
1160
|
});
|
|
436
1161
|
effect(() => {
|
|
437
|
-
const
|
|
438
|
-
|
|
1162
|
+
const { rows, progress } = view();
|
|
1163
|
+
const running = rows.some((row) => row.state === "running") || progress?.state === "running";
|
|
1164
|
+
if (running) sectionsRunning.add(key);
|
|
439
1165
|
else sectionsRunning.delete(key);
|
|
440
|
-
if (
|
|
1166
|
+
if (rows.length > 0 || progress) sectionsShowing.add(key);
|
|
441
1167
|
else sectionsShowing.delete(key);
|
|
442
1168
|
syncClocks();
|
|
443
1169
|
});
|
|
444
1170
|
onCleanup(() => {
|
|
445
1171
|
sectionsRunning.delete(key);
|
|
446
1172
|
sectionsShowing.delete(key);
|
|
1173
|
+
sectionsMounted.delete(key);
|
|
447
1174
|
syncClocks();
|
|
448
1175
|
});
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
1176
|
+
const hasProgress = createMemo(() => view().progress !== void 0);
|
|
1177
|
+
insert(
|
|
1178
|
+
progressBox,
|
|
1179
|
+
() => hasProgress() ? attempt(() => buildProgress(() => view().progress), null) : null
|
|
1180
|
+
);
|
|
1181
|
+
insert(subagentsBox, () => {
|
|
1182
|
+
const current = view();
|
|
1183
|
+
if (current.rows.length === 0) return null;
|
|
1184
|
+
return attempt(() => buildSection(current, expanded, setExpanded), null);
|
|
453
1185
|
});
|
|
1186
|
+
const accountLines = createMemo(
|
|
1187
|
+
() => attempt(() => accountUsageLines(usage(), usageNow(), SIDEBAR_ROW_WIDTH, known()), [])
|
|
1188
|
+
);
|
|
1189
|
+
insert(accountsBox, () => {
|
|
1190
|
+
const lines = accountLines();
|
|
1191
|
+
if (lines.length === 0) return null;
|
|
1192
|
+
return attempt(() => buildAccounts(lines), null);
|
|
1193
|
+
});
|
|
1194
|
+
syncClocks();
|
|
454
1195
|
return root;
|
|
455
1196
|
};
|
|
456
1197
|
const dispose = () => {
|
|
457
1198
|
sectionsRunning.clear();
|
|
458
1199
|
sectionsShowing.clear();
|
|
1200
|
+
sectionsMounted.clear();
|
|
459
1201
|
syncClocks();
|
|
460
1202
|
};
|
|
461
1203
|
return { render, dispose };
|
|
@@ -505,6 +1247,7 @@ function registerV1(api) {
|
|
|
505
1247
|
lastStatus.set(id, type);
|
|
506
1248
|
if (type === "idle" && previous && previous !== "idle") stoppedAt.set(id, Date.now());
|
|
507
1249
|
if (type !== "idle") stoppedAt.delete(id);
|
|
1250
|
+
bump();
|
|
508
1251
|
});
|
|
509
1252
|
const fetchChildren = (parentID) => {
|
|
510
1253
|
if (fetched.has(parentID)) return;
|
|
@@ -520,15 +1263,23 @@ function registerV1(api) {
|
|
|
520
1263
|
const host = {
|
|
521
1264
|
colors() {
|
|
522
1265
|
const theme = api.theme.current;
|
|
1266
|
+
const muted = theme.textMuted;
|
|
523
1267
|
return {
|
|
524
1268
|
text: theme.text,
|
|
525
|
-
muted
|
|
1269
|
+
muted,
|
|
526
1270
|
spinner: theme.accent,
|
|
527
|
-
done: mix(theme.success,
|
|
528
|
-
error: mix(theme.error,
|
|
529
|
-
effort: theme.warning ??
|
|
530
|
-
effortFaded: mix(theme.warning ??
|
|
531
|
-
hover: theme.backgroundElement
|
|
1271
|
+
done: mix(theme.success, muted, 0.45),
|
|
1272
|
+
error: mix(theme.error, muted, 0.35),
|
|
1273
|
+
effort: theme.warning ?? muted,
|
|
1274
|
+
effortFaded: mix(theme.warning ?? muted, muted, 0.45),
|
|
1275
|
+
hover: theme.backgroundElement,
|
|
1276
|
+
barDone: theme.success ?? theme.accent,
|
|
1277
|
+
barTrack: mix(muted, theme.backgroundPanel ?? theme.backgroundElement, 0.5),
|
|
1278
|
+
fraction: theme.text,
|
|
1279
|
+
tool: theme.info ?? theme.accent,
|
|
1280
|
+
cost: theme.secondary ?? theme.primary ?? muted,
|
|
1281
|
+
tier: theme.secondary ?? theme.primary ?? muted,
|
|
1282
|
+
reset: theme.info ?? theme.accent
|
|
532
1283
|
};
|
|
533
1284
|
},
|
|
534
1285
|
gather(parentID) {
|
|
@@ -581,9 +1332,31 @@ function registerV1(api) {
|
|
|
581
1332
|
if (type === "idle") return "idle";
|
|
582
1333
|
return "unknown";
|
|
583
1334
|
},
|
|
584
|
-
stoppedAt: (id) => stoppedAt.get(id)
|
|
1335
|
+
stoppedAt: (id) => stoppedAt.get(id),
|
|
1336
|
+
cost: (id) => attempt(() => api.state.session.get(id)?.cost, void 0)
|
|
585
1337
|
};
|
|
586
1338
|
},
|
|
1339
|
+
progress(sessionID) {
|
|
1340
|
+
const messages = [];
|
|
1341
|
+
const turn = turnMessages(api.state.session.messages(sessionID) ?? [], (message) => message.role === "user");
|
|
1342
|
+
for (const message of turn) {
|
|
1343
|
+
const tools = [];
|
|
1344
|
+
const texts = [];
|
|
1345
|
+
for (const part of api.state.part(message.id) ?? []) {
|
|
1346
|
+
if (message.role === "assistant" && part.type === "tool") tools.push({ name: part.tool, status: part.state?.status });
|
|
1347
|
+
if (message.role === "user" && part.type === "text" && typeof part.text === "string") texts.push(part.text);
|
|
1348
|
+
}
|
|
1349
|
+
messages.push({
|
|
1350
|
+
role: message.role,
|
|
1351
|
+
created: message.time?.created,
|
|
1352
|
+
error: message.error !== void 0 && message.error !== null,
|
|
1353
|
+
tools,
|
|
1354
|
+
chars: message.role === "user" ? promptChars(texts) : void 0
|
|
1355
|
+
});
|
|
1356
|
+
}
|
|
1357
|
+
const todos = attempt(() => api.state.session.todo?.(sessionID), void 0) ?? [];
|
|
1358
|
+
return { messages, todos: todos.map((todo) => ({ status: todo.status })) };
|
|
1359
|
+
},
|
|
587
1360
|
navigate(sessionID) {
|
|
588
1361
|
api.route.navigate("session", { sessionID });
|
|
589
1362
|
}
|
|
@@ -607,7 +1380,8 @@ function setupV2(context) {
|
|
|
607
1380
|
colors() {
|
|
608
1381
|
const theme = context.theme;
|
|
609
1382
|
const muted = theme.text.muted;
|
|
610
|
-
const
|
|
1383
|
+
const shade = context.themeMode === "light" ? "500" : "200";
|
|
1384
|
+
const accent = theme.hue.accent[shade] ?? theme.text.base;
|
|
611
1385
|
return {
|
|
612
1386
|
text: theme.text.base,
|
|
613
1387
|
muted,
|
|
@@ -616,7 +1390,14 @@ function setupV2(context) {
|
|
|
616
1390
|
error: mix(theme.text.feedback.error.muted ?? theme.text.feedback.error.base, muted, 0.35),
|
|
617
1391
|
effort: theme.text.feedback.warning?.base ?? muted,
|
|
618
1392
|
effortFaded: mix(theme.text.feedback.warning?.base ?? muted, muted, 0.45),
|
|
619
|
-
hover: theme.background.raised.base
|
|
1393
|
+
hover: theme.background.raised.base,
|
|
1394
|
+
barDone: theme.text.feedback.success.base ?? accent,
|
|
1395
|
+
barTrack: mix(muted, theme.background.base ?? theme.background.raised.base, 0.5),
|
|
1396
|
+
fraction: theme.text.base,
|
|
1397
|
+
tool: theme.text.feedback.info?.base ?? accent,
|
|
1398
|
+
cost: theme.hue.interactive?.[shade] ?? accent,
|
|
1399
|
+
tier: theme.hue.interactive?.[shade] ?? accent,
|
|
1400
|
+
reset: theme.text.feedback.info?.base ?? accent
|
|
620
1401
|
};
|
|
621
1402
|
},
|
|
622
1403
|
gather(parentID) {
|
|
@@ -655,9 +1436,37 @@ function setupV2(context) {
|
|
|
655
1436
|
if (status === "idle") return "idle";
|
|
656
1437
|
return "unknown";
|
|
657
1438
|
},
|
|
658
|
-
stoppedAt: () => void 0
|
|
1439
|
+
stoppedAt: () => void 0,
|
|
1440
|
+
cost: (id) => attempt(() => context.data.session.cost?.(id), void 0)
|
|
659
1441
|
};
|
|
660
1442
|
},
|
|
1443
|
+
progress(sessionID) {
|
|
1444
|
+
const messages = [];
|
|
1445
|
+
const turn = turnMessages(
|
|
1446
|
+
context.data.session.message.list(sessionID) ?? [],
|
|
1447
|
+
(message) => message.type === "user" || message.type === "synthetic"
|
|
1448
|
+
);
|
|
1449
|
+
for (const message of turn) {
|
|
1450
|
+
const tools = [];
|
|
1451
|
+
const texts = [];
|
|
1452
|
+
for (const item of message.content ?? []) {
|
|
1453
|
+
if (message.type === "assistant" && item.type === "tool") tools.push({ name: item.name, status: item.state?.status });
|
|
1454
|
+
if (message.type === "user" && item.type === "text" && typeof item.text === "string") texts.push(item.text);
|
|
1455
|
+
}
|
|
1456
|
+
if (message.type === "synthetic" && message.text) texts.push(message.text);
|
|
1457
|
+
const starts = message.type === "user" || message.type === "synthetic";
|
|
1458
|
+
messages.push({
|
|
1459
|
+
// A synthetic message (a background child's end) prompts the parent
|
|
1460
|
+
// like a user message does: it starts a turn.
|
|
1461
|
+
role: message.type === "synthetic" ? "user" : message.type ?? "",
|
|
1462
|
+
created: message.time?.created,
|
|
1463
|
+
error: message.error !== void 0 && message.error !== null,
|
|
1464
|
+
tools,
|
|
1465
|
+
chars: starts ? promptChars(texts) : void 0
|
|
1466
|
+
});
|
|
1467
|
+
}
|
|
1468
|
+
return { messages, todos: [] };
|
|
1469
|
+
},
|
|
661
1470
|
navigate(sessionID) {
|
|
662
1471
|
context.ui.router.navigate({ type: "session", sessionID });
|
|
663
1472
|
}
|