@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/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 text2 = (title ?? "").trim();
15
- const match = TITLE_SUFFIX.exec(text2);
16
- if (!match) return { description: text2 };
17
- return { description: text2.slice(0, match.index).trim(), agent: match[1] };
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
- if (row.label) {
197
- const pieces = truncate(row.label, width - 2).split(" \xB7 ");
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: " ", role: "muted" });
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
- switch (role) {
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) % SPINNER_FRAMES.length), SPINNER_INTERVAL_MS);
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
- if (sectionsShowing.size > 0 && !refresh) {
335
- refresh = setInterval(() => {
336
- const next = attempt(() => records.read(), recordMap);
337
- if (next !== recordMap) {
338
- recordMap = next;
339
- setRecordVersion((value) => value + 1);
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 deriveRows = (parentID) => {
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
- return deriveSubagentRows({
351
- ...host.gather(parentID),
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: 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 buildSection = (list, expanded, setExpanded) => {
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 = createElement("text");
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(body, () => expanded() || list.length <= 2 ? list.map(buildRow) : null);
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 rows = createMemo(() => {
1156
+ const view = createMemo(() => {
432
1157
  const parentID = sessionID();
433
- if (!parentID) return [];
434
- return attempt(() => deriveRows(parentID), []);
1158
+ if (!parentID) return EMPTY_VIEW;
1159
+ return attempt(() => deriveView(parentID), EMPTY_VIEW);
435
1160
  });
436
1161
  effect(() => {
437
- const list = rows();
438
- if (list.some((row) => row.state === "running")) sectionsRunning.add(key);
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 (list.length > 0) sectionsShowing.add(key);
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
- insert(root, () => {
450
- const list = rows();
451
- if (list.length === 0) return null;
452
- return attempt(() => buildSection(list, expanded, setExpanded), null);
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: theme.textMuted,
1269
+ muted,
526
1270
  spinner: theme.accent,
527
- done: mix(theme.success, theme.textMuted, 0.45),
528
- error: mix(theme.error, theme.textMuted, 0.35),
529
- effort: theme.warning ?? theme.textMuted,
530
- effortFaded: mix(theme.warning ?? theme.textMuted, theme.textMuted, 0.45),
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 accent = theme.hue.accent[context.themeMode === "light" ? "500" : "200"] ?? theme.text.base;
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
  }