@khalilgharbaoui/opencode-claude-code-plugin 0.56.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,20 +488,150 @@ function createSpawnRecordReader(file = spawnRecordStorePath) {
271
488
  };
272
489
  }
273
490
 
274
- // src/account-usage.ts
491
+ // src/turn-history.ts
275
492
  import { mkdirSync as mkdirSync2, readFileSync as readFileSync2, renameSync as renameSync2, statSync as statSync2, writeFileSync as writeFileSync2 } from "fs";
276
493
  import { homedir as homedir2 } from "os";
277
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";
278
623
  var UNTIMED_READING_TTL_MS = 5 * 60 * 60 * 1e3;
279
624
  var USAGE_READING_MAX_AGE_MS = 8 * 24 * 60 * 60 * 1e3;
625
+ var MAX_USAGE_ACCOUNTS = 32;
280
626
  var NAME = /^[a-z0-9_-]{1,64}$/;
281
627
  var STATUS = /^[a-z_]{1,32}$/;
628
+ var TIER = /^[A-Za-z0-9][A-Za-z0-9 ]{0,23}$/;
282
629
  var MAX_RESETS_TEXT = 80;
283
- var pathOverride2 = null;
630
+ var pathOverride3 = null;
284
631
  function accountUsageStorePath() {
285
- if (pathOverride2) return pathOverride2;
286
- const stateRoot = process.env.XDG_STATE_HOME || join2(homedir2(), ".local", "state");
287
- return join2(stateRoot, "opencode-claude-code-plugin", "account-usage.json");
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");
288
635
  }
289
636
  function finite(value) {
290
637
  return typeof value === "number" && Number.isFinite(value);
@@ -311,11 +658,28 @@ function validEntry(value) {
311
658
  noted
312
659
  };
313
660
  }
314
- function readStore(file, now) {
661
+ var emptyStore = () => ({ usage: /* @__PURE__ */ new Map(), known: /* @__PURE__ */ new Map() });
662
+ function readKnown(parsed) {
315
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();
316
678
  try {
317
- const parsed = JSON.parse(readFileSync2(file, "utf8"));
318
- const accounts = parsed && typeof parsed === "object" ? parsed.accounts : null;
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;
319
683
  if (!accounts || typeof accounts !== "object" || Array.isArray(accounts)) return out;
320
684
  const cutoff = now - USAGE_READING_MAX_AGE_MS;
321
685
  for (const [account, windows] of Object.entries(accounts)) {
@@ -326,7 +690,7 @@ function readStore(file, now) {
326
690
  const entry = validEntry(raw);
327
691
  if (entry && entry.observedAt >= cutoff) kept.set(window, entry);
328
692
  }
329
- if (kept.size > 0) out.set(account, kept);
693
+ if (kept.size > 0) out.usage.set(account, kept);
330
694
  }
331
695
  } catch {
332
696
  }
@@ -341,28 +705,39 @@ function usagePercent(utilization) {
341
705
  }
342
706
  function createAccountUsageReader(file = accountUsageStorePath) {
343
707
  let identity = "";
344
- let usage = /* @__PURE__ */ new Map();
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
+ };
345
723
  return {
346
724
  read() {
347
- const path = file();
348
- let next = "";
349
- try {
350
- const stats = statSync2(path);
351
- next = `${stats.mtimeMs}:${stats.size}:${stats.ino}`;
352
- } catch {
353
- next = "missing";
354
- }
355
- if (next !== identity) {
356
- identity = next;
357
- usage = next === "missing" ? /* @__PURE__ */ new Map() : readStore(path, Date.now());
358
- }
359
- return usage;
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;
360
732
  }
361
733
  };
362
734
  }
363
735
 
364
736
  // src/account-sidebar.ts
365
737
  var WARNING_PERCENT = 75;
738
+ var NO_READING_TEXT = "no reading yet";
739
+ var RESET_WORD = "resets ";
740
+ var SEPARATOR = " \xB7 ";
366
741
  var SHORT_WINDOWS = {
367
742
  five_hour: "5h",
368
743
  seven_day: "7d",
@@ -375,14 +750,25 @@ function shortWindowName(window) {
375
750
  if (window === "unknown") return "";
376
751
  return SHORT_WINDOWS[window] ?? window.replace(/_/g, " ");
377
752
  }
378
- var MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
379
- function formatResetClock(resetsAt, now) {
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) {
380
758
  const when = new Date(resetsAt);
381
- const today = new Date(now);
382
- const pad = (value) => String(value).padStart(2, "0");
383
759
  const clock = `${pad(when.getHours())}:${pad(when.getMinutes())}`;
384
- const sameDay = when.getFullYear() === today.getFullYear() && when.getMonth() === today.getMonth() && when.getDate() === today.getDate();
385
- return sameDay ? clock : `${MONTHS[when.getMonth()]} ${when.getDate()} ${clock}`;
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 [];
386
772
  }
387
773
  function rank(entry) {
388
774
  if (entry.status === "rejected") return 1e3;
@@ -397,29 +783,94 @@ function pressingWindow(windows, now) {
397
783
  }
398
784
  return best;
399
785
  }
400
- function accountUsageLines(usage, now, width = SIDEBAR_ROW_WIDTH) {
401
- const lines = [];
402
- for (const account of [...usage.keys()].sort()) {
403
- const pressing = pressingWindow(usage.get(account), now);
404
- if (!pressing) continue;
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
+ }
405
851
  const [window, entry] = pressing;
406
852
  const state = entry.status === "rejected" ? "limited" : entry.utilization !== null && usagePercent(entry.utilization) >= WARNING_PERCENT ? "warning" : "ok";
407
853
  const short = shortWindowName(window);
408
854
  const level = state === "limited" ? "limited" : entry.utilization === null ? `${short ? `${short} ` : ""}ok` : `${short ? `${short} ` : ""}${Math.round(usagePercent(entry.utilization))}%`;
409
- const resets = entry.resetsAt !== void 0 ? ` \xB7 resets ${formatResetClock(entry.resetsAt, now)}` : entry.resetsText ? ` \xB7 resets ${entry.resetsText}` : "";
410
- const name = truncate(account, Math.max(1, Math.floor(width / 2)));
411
- const rest = ` ${truncate(`\xB7 ${level}${resets}`, Math.max(0, width - Array.from(name).length - 1))}`;
412
- lines.push({
413
- account,
414
- window,
415
- state,
416
- segments: [
417
- { text: name, role: "agent" },
418
- { text: rest, role: state === "limited" ? "error" : state === "warning" ? "effort" : "muted" }
419
- ]
420
- });
855
+ drafts.push({ account, window, state, name, level, spellings: resetSpellings(entry, now) });
421
856
  }
422
- return lines;
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
+ });
423
874
  }
424
875
 
425
876
  // src/tui.ts
@@ -427,7 +878,30 @@ var SUBAGENTS_TUI_PLUGIN_ID = "opencode-claude-code-plugin.subagents";
427
878
  var SECTION_ORDER = 900;
428
879
  var SPINNER_INTERVAL_MS = 100;
429
880
  var REFRESH_INTERVAL_MS = 1500;
881
+ var PROGRESS_TICK_MS = 1e3;
882
+ var FRAME_WRAP = SPINNER_FRAMES.length * 600;
430
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
+ };
431
905
  function attempt(fn, fallback) {
432
906
  try {
433
907
  return fn();
@@ -447,74 +921,131 @@ function mix(a, b, weight) {
447
921
  );
448
922
  }
449
923
  function roleColor(colors, role) {
450
- switch (role) {
451
- case "spinner":
452
- return colors.spinner;
453
- case "done":
454
- return colors.done;
455
- case "error":
456
- return colors.error;
457
- case "effort":
458
- return colors.effort;
459
- case "effort-faded":
460
- return colors.effortFaded;
461
- case "agent":
462
- case "description":
463
- return colors.text;
464
- default:
465
- return colors.muted;
466
- }
924
+ return colors[ROLE_COLOR[role] ?? "muted"];
467
925
  }
926
+ var EMPTY_SOURCE = { messages: [], todos: [] };
927
+ var EMPTY_VIEW = { rows: [], lines: /* @__PURE__ */ new Map() };
468
928
  function createSidebar(host) {
469
929
  const [frame, setFrame] = createSignal(0);
470
930
  const [now, setNow] = createSignal(Date.now());
471
931
  const records = createSpawnRecordReader();
472
932
  let recordMap = attempt(() => records.read(), /* @__PURE__ */ new Map());
933
+ const history = createTurnHistoryReader();
934
+ let historyMap = attempt(() => history.read(), /* @__PURE__ */ new Map());
473
935
  const [recordVersion, setRecordVersion] = createSignal(0);
936
+ const watch = createStatusWatch();
937
+ const fills = createFillMemory();
474
938
  const usageReader = createAccountUsageReader();
475
939
  const [usage, setUsage] = createSignal(attempt(() => usageReader.read(), /* @__PURE__ */ new Map()));
940
+ const [known, setKnown] = createSignal(attempt(() => usageReader.known(), /* @__PURE__ */ new Map()));
476
941
  const [usageNow, setUsageNow] = createSignal(Date.now());
477
942
  const sectionsRunning = /* @__PURE__ */ new Set();
478
943
  const sectionsShowing = /* @__PURE__ */ new Set();
479
944
  const sectionsMounted = /* @__PURE__ */ new Set();
480
945
  let spinner;
481
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
+ };
482
964
  const syncClocks = () => {
483
965
  if (sectionsRunning.size > 0 && !spinner) {
484
- spinner = setInterval(() => setFrame((value) => (value + 1) % SPINNER_FRAMES.length), SPINNER_INTERVAL_MS);
966
+ spinner = setInterval(() => setFrame((value) => (value + 1) % FRAME_WRAP), SPINNER_INTERVAL_MS);
485
967
  } else if (sectionsRunning.size === 0 && spinner) {
486
968
  clearInterval(spinner);
487
969
  spinner = void 0;
488
970
  }
489
971
  const wanted = sectionsShowing.size > 0 || sectionsMounted.size > 0;
490
- if (wanted && !refresh) {
491
- refresh = setInterval(() => {
492
- if (sectionsShowing.size > 0) {
493
- const next = attempt(() => records.read(), recordMap);
494
- if (next !== recordMap) {
495
- recordMap = next;
496
- setRecordVersion((value) => value + 1);
497
- }
498
- setNow(Date.now());
499
- }
500
- setUsage(attempt(() => usageReader.read(), usage()));
501
- const minute = Math.floor(Date.now() / 6e4) * 6e4;
502
- if (minute !== usageNow()) setUsageNow(minute);
503
- }, REFRESH_INTERVAL_MS);
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);
504
977
  } else if (!wanted && refresh) {
505
978
  clearInterval(refresh);
506
979
  refresh = void 0;
980
+ refreshPeriod = 0;
507
981
  }
508
982
  };
509
- 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) => {
510
996
  recordVersion();
511
- return deriveSubagentRows({
512
- ...host.gather(parentID),
997
+ now();
998
+ const clock = Date.now();
999
+ const gathered = host.gather(sessionID);
1000
+ const rows = deriveSubagentRows({
1001
+ ...gathered,
513
1002
  record: (id) => recordMap.get(id),
514
- 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
515
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;
516
1029
  };
517
- const buildRow = (row) => {
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
+ }
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;
1047
+ };
1048
+ const buildRow = (row, progress) => {
518
1049
  const box = createElement("box");
519
1050
  const layout = layoutRow(row, SIDEBAR_ROW_WIDTH);
520
1051
  const first = createElement("text");
@@ -533,14 +1064,10 @@ function createSidebar(host) {
533
1064
  if (layout.second.length > 0) {
534
1065
  const second = createElement("text");
535
1066
  setProp(second, "wrapMode", "none");
536
- for (const segment of layout.second) {
537
- const span = createElement("span");
538
- insertNode(span, createTextNode(segment.text));
539
- effect(() => setProp(span, "style", { fg: roleColor(host.colors(), segment.role) }));
540
- insertNode(second, span);
541
- }
1067
+ for (const segment of layout.second) insertNode(second, styled(segment));
542
1068
  insertNode(box, second);
543
1069
  }
1070
+ if (progress) insertNode(box, attempt(() => buildProgressLine(progress, "indent"), createElement("box")));
544
1071
  if (row.sessionID) {
545
1072
  const sessionID = row.sessionID;
546
1073
  setProp(box, "onMouseDown", () => attempt(() => host.navigate(sessionID), void 0));
@@ -557,7 +1084,16 @@ function createSidebar(host) {
557
1084
  }
558
1085
  return box;
559
1086
  };
560
- 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;
561
1097
  const section = createElement("box");
562
1098
  const header = createElement("box");
563
1099
  setProp(header, "flexDirection", "row");
@@ -569,62 +1105,65 @@ function createSidebar(host) {
569
1105
  effect(() => setProp(caret, "fg", host.colors().text));
570
1106
  insertNode(header, caret);
571
1107
  }
572
- const title = createElement("text");
573
- const bold = createElement("b");
574
- insertNode(bold, createTextNode("Subagents"));
575
- insertNode(title, bold);
1108
+ const title = buildHeading("Subagents");
576
1109
  const summary = createElement("span");
577
1110
  insert(summary, () => expanded() ? "" : ` (${collapsedSummary(list)})`);
578
1111
  effect(() => setProp(summary, "style", { fg: host.colors().muted }));
579
1112
  insertNode(title, summary);
580
- effect(() => setProp(title, "fg", host.colors().text));
581
1113
  insertNode(header, title);
582
1114
  insertNode(section, header);
583
1115
  const body = createElement("box");
584
- 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
+ });
585
1131
  insertNode(section, body);
586
1132
  return section;
587
1133
  };
588
1134
  const buildAccounts = (lines) => {
589
1135
  const section = createElement("box");
590
- const title = createElement("text");
591
- const bold = createElement("b");
592
- insertNode(bold, createTextNode("Accounts"));
593
- insertNode(title, bold);
594
- effect(() => setProp(title, "fg", host.colors().text));
595
- insertNode(section, title);
1136
+ insertNode(section, buildHeading("Accounts"));
596
1137
  for (const line of lines) {
597
- const text2 = createElement("text");
598
- setProp(text2, "wrapMode", "none");
599
- for (const segment of line.segments) {
600
- const span = createElement("span");
601
- insertNode(span, createTextNode(segment.text));
602
- effect(() => setProp(span, "style", { fg: roleColor(host.colors(), segment.role) }));
603
- insertNode(text2, span);
604
- }
605
- insertNode(section, text2);
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);
606
1142
  }
607
1143
  return section;
608
1144
  };
609
1145
  const render = (sessionID) => {
610
1146
  const root = createElement("box");
1147
+ const progressBox = createElement("box");
611
1148
  const subagentsBox = createElement("box");
612
1149
  const accountsBox = createElement("box");
1150
+ insertNode(root, progressBox);
613
1151
  insertNode(root, subagentsBox);
614
1152
  insertNode(root, accountsBox);
615
1153
  const key = /* @__PURE__ */ Symbol("subagents-section");
616
1154
  sectionsMounted.add(key);
617
1155
  const [expanded, setExpanded] = createSignal(true);
618
- const rows = createMemo(() => {
1156
+ const view = createMemo(() => {
619
1157
  const parentID = sessionID();
620
- if (!parentID) return [];
621
- return attempt(() => deriveRows(parentID), []);
1158
+ if (!parentID) return EMPTY_VIEW;
1159
+ return attempt(() => deriveView(parentID), EMPTY_VIEW);
622
1160
  });
623
1161
  effect(() => {
624
- const list = rows();
625
- 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);
626
1165
  else sectionsRunning.delete(key);
627
- if (list.length > 0) sectionsShowing.add(key);
1166
+ if (rows.length > 0 || progress) sectionsShowing.add(key);
628
1167
  else sectionsShowing.delete(key);
629
1168
  syncClocks();
630
1169
  });
@@ -634,13 +1173,18 @@ function createSidebar(host) {
634
1173
  sectionsMounted.delete(key);
635
1174
  syncClocks();
636
1175
  });
1176
+ const hasProgress = createMemo(() => view().progress !== void 0);
1177
+ insert(
1178
+ progressBox,
1179
+ () => hasProgress() ? attempt(() => buildProgress(() => view().progress), null) : null
1180
+ );
637
1181
  insert(subagentsBox, () => {
638
- const list = rows();
639
- if (list.length === 0) return null;
640
- return attempt(() => buildSection(list, expanded, setExpanded), null);
1182
+ const current = view();
1183
+ if (current.rows.length === 0) return null;
1184
+ return attempt(() => buildSection(current, expanded, setExpanded), null);
641
1185
  });
642
1186
  const accountLines = createMemo(
643
- () => attempt(() => accountUsageLines(usage(), usageNow()), [])
1187
+ () => attempt(() => accountUsageLines(usage(), usageNow(), SIDEBAR_ROW_WIDTH, known()), [])
644
1188
  );
645
1189
  insert(accountsBox, () => {
646
1190
  const lines = accountLines();
@@ -703,6 +1247,7 @@ function registerV1(api) {
703
1247
  lastStatus.set(id, type);
704
1248
  if (type === "idle" && previous && previous !== "idle") stoppedAt.set(id, Date.now());
705
1249
  if (type !== "idle") stoppedAt.delete(id);
1250
+ bump();
706
1251
  });
707
1252
  const fetchChildren = (parentID) => {
708
1253
  if (fetched.has(parentID)) return;
@@ -718,15 +1263,23 @@ function registerV1(api) {
718
1263
  const host = {
719
1264
  colors() {
720
1265
  const theme = api.theme.current;
1266
+ const muted = theme.textMuted;
721
1267
  return {
722
1268
  text: theme.text,
723
- muted: theme.textMuted,
1269
+ muted,
724
1270
  spinner: theme.accent,
725
- done: mix(theme.success, theme.textMuted, 0.45),
726
- error: mix(theme.error, theme.textMuted, 0.35),
727
- effort: theme.warning ?? theme.textMuted,
728
- effortFaded: mix(theme.warning ?? theme.textMuted, theme.textMuted, 0.45),
729
- 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
730
1283
  };
731
1284
  },
732
1285
  gather(parentID) {
@@ -779,9 +1332,31 @@ function registerV1(api) {
779
1332
  if (type === "idle") return "idle";
780
1333
  return "unknown";
781
1334
  },
782
- stoppedAt: (id) => stoppedAt.get(id)
1335
+ stoppedAt: (id) => stoppedAt.get(id),
1336
+ cost: (id) => attempt(() => api.state.session.get(id)?.cost, void 0)
783
1337
  };
784
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
+ },
785
1360
  navigate(sessionID) {
786
1361
  api.route.navigate("session", { sessionID });
787
1362
  }
@@ -805,7 +1380,8 @@ function setupV2(context) {
805
1380
  colors() {
806
1381
  const theme = context.theme;
807
1382
  const muted = theme.text.muted;
808
- 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;
809
1385
  return {
810
1386
  text: theme.text.base,
811
1387
  muted,
@@ -814,7 +1390,14 @@ function setupV2(context) {
814
1390
  error: mix(theme.text.feedback.error.muted ?? theme.text.feedback.error.base, muted, 0.35),
815
1391
  effort: theme.text.feedback.warning?.base ?? muted,
816
1392
  effortFaded: mix(theme.text.feedback.warning?.base ?? muted, muted, 0.45),
817
- 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
818
1401
  };
819
1402
  },
820
1403
  gather(parentID) {
@@ -853,9 +1436,37 @@ function setupV2(context) {
853
1436
  if (status === "idle") return "idle";
854
1437
  return "unknown";
855
1438
  },
856
- stoppedAt: () => void 0
1439
+ stoppedAt: () => void 0,
1440
+ cost: (id) => attempt(() => context.data.session.cost?.(id), void 0)
857
1441
  };
858
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
+ },
859
1470
  navigate(sessionID) {
860
1471
  context.ui.router.navigate({ type: "session", sessionID });
861
1472
  }