@khalilgharbaoui/opencode-claude-code-plugin 0.56.0 → 0.57.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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" : "ok";
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,31 @@ 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
+ ok: "ok",
888
+ error: "error",
889
+ agent: "text",
890
+ description: "text",
891
+ muted: "muted",
892
+ effort: "effort",
893
+ "effort-faded": "effortFaded",
894
+ "bar-done": "barDone",
895
+ // Past every recorded turn: the warning colour, which the effort already takes.
896
+ "bar-over": "effort",
897
+ "bar-track": "barTrack",
898
+ fraction: "fraction",
899
+ estimate: "fraction",
900
+ "estimate-over": "effort",
901
+ tool: "tool",
902
+ cost: "cost",
903
+ tier: "tier",
904
+ reset: "reset"
905
+ };
431
906
  function attempt(fn, fallback) {
432
907
  try {
433
908
  return fn();
@@ -447,74 +922,131 @@ function mix(a, b, weight) {
447
922
  );
448
923
  }
449
924
  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
- }
925
+ return colors[ROLE_COLOR[role] ?? "muted"];
467
926
  }
927
+ var EMPTY_SOURCE = { messages: [], todos: [] };
928
+ var EMPTY_VIEW = { rows: [], lines: /* @__PURE__ */ new Map() };
468
929
  function createSidebar(host) {
469
930
  const [frame, setFrame] = createSignal(0);
470
931
  const [now, setNow] = createSignal(Date.now());
471
932
  const records = createSpawnRecordReader();
472
933
  let recordMap = attempt(() => records.read(), /* @__PURE__ */ new Map());
934
+ const history = createTurnHistoryReader();
935
+ let historyMap = attempt(() => history.read(), /* @__PURE__ */ new Map());
473
936
  const [recordVersion, setRecordVersion] = createSignal(0);
937
+ const watch = createStatusWatch();
938
+ const fills = createFillMemory();
474
939
  const usageReader = createAccountUsageReader();
475
940
  const [usage, setUsage] = createSignal(attempt(() => usageReader.read(), /* @__PURE__ */ new Map()));
941
+ const [known, setKnown] = createSignal(attempt(() => usageReader.known(), /* @__PURE__ */ new Map()));
476
942
  const [usageNow, setUsageNow] = createSignal(Date.now());
477
943
  const sectionsRunning = /* @__PURE__ */ new Set();
478
944
  const sectionsShowing = /* @__PURE__ */ new Set();
479
945
  const sectionsMounted = /* @__PURE__ */ new Set();
480
946
  let spinner;
481
947
  let refresh;
948
+ let refreshPeriod = 0;
949
+ const refreshTick = () => {
950
+ if (sectionsShowing.size > 0) {
951
+ const next = attempt(() => records.read(), recordMap);
952
+ const nextHistory = attempt(() => history.read(), historyMap);
953
+ if (next !== recordMap || nextHistory !== historyMap) {
954
+ recordMap = next;
955
+ historyMap = nextHistory;
956
+ setRecordVersion((value) => value + 1);
957
+ }
958
+ setNow(Date.now());
959
+ }
960
+ setUsage(attempt(() => usageReader.read(), usage()));
961
+ setKnown(attempt(() => usageReader.known(), known()));
962
+ const minute = Math.floor(Date.now() / 6e4) * 6e4;
963
+ if (minute !== usageNow()) setUsageNow(minute);
964
+ };
482
965
  const syncClocks = () => {
483
966
  if (sectionsRunning.size > 0 && !spinner) {
484
- spinner = setInterval(() => setFrame((value) => (value + 1) % SPINNER_FRAMES.length), SPINNER_INTERVAL_MS);
967
+ spinner = setInterval(() => setFrame((value) => (value + 1) % FRAME_WRAP), SPINNER_INTERVAL_MS);
485
968
  } else if (sectionsRunning.size === 0 && spinner) {
486
969
  clearInterval(spinner);
487
970
  spinner = void 0;
488
971
  }
489
972
  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);
973
+ const period = sectionsRunning.size > 0 ? PROGRESS_TICK_MS : REFRESH_INTERVAL_MS;
974
+ if (wanted && (!refresh || refreshPeriod !== period)) {
975
+ if (refresh) clearInterval(refresh);
976
+ refreshPeriod = period;
977
+ refresh = setInterval(refreshTick, period);
504
978
  } else if (!wanted && refresh) {
505
979
  clearInterval(refresh);
506
980
  refresh = void 0;
981
+ refreshPeriod = 0;
507
982
  }
508
983
  };
509
- const deriveRows = (parentID) => {
984
+ const progressOf = (sessionID) => attempt(() => host.progress(sessionID), EMPTY_SOURCE);
985
+ const estimateFor = (sessionID, measure, view, agentFallback) => {
986
+ if (view.state !== "running") return view;
987
+ const record = recordMap.get(sessionID);
988
+ if (!record?.model) return view;
989
+ const samples = historyMap.get(turnHistoryKey(record.agent ?? agentFallback, record.model));
990
+ if (!samples) return view;
991
+ const estimate = estimateTurn(samples, { elapsedMs: view.elapsedMs, chars: measure.chars, effort: record.effort });
992
+ if (!estimate) return view;
993
+ const turn = measure.startedAt ?? 0;
994
+ return { ...view, estimate: { ...estimate, fill: fills.hold(sessionID, turn, estimate.fill) } };
995
+ };
996
+ const deriveView = (sessionID) => {
510
997
  recordVersion();
511
- return deriveSubagentRows({
512
- ...host.gather(parentID),
998
+ now();
999
+ const clock = Date.now();
1000
+ const gathered = host.gather(sessionID);
1001
+ const rows = deriveSubagentRows({
1002
+ ...gathered,
513
1003
  record: (id) => recordMap.get(id),
514
- now: now()
1004
+ now: clock
1005
+ });
1006
+ const lines = /* @__PURE__ */ new Map();
1007
+ for (const row of rows) {
1008
+ if (row.state !== "running" || !row.sessionID) continue;
1009
+ const measure2 = measureProgress(progressOf(row.sessionID));
1010
+ const line = rowProgress(measure2, { started: row.started, now: clock });
1011
+ lines.set(row.sessionID, estimateFor(row.sessionID, measure2, line, row.agent));
1012
+ }
1013
+ const status = gathered.status(sessionID);
1014
+ const memory = watch.observe(sessionID, status, clock);
1015
+ const measure = measureProgress(progressOf(sessionID));
1016
+ const session = sessionProgress(measure, {
1017
+ status,
1018
+ busySince: memory.busySince,
1019
+ stoppedAt: gathered.stoppedAt(sessionID) ?? memory.stoppedAt,
1020
+ now: clock
515
1021
  });
1022
+ const progress = session ? estimateFor(sessionID, measure, session) : void 0;
1023
+ return { rows, lines, progress };
1024
+ };
1025
+ const styled = (segment) => {
1026
+ const span = createElement(segment.role === "fraction" ? "b" : "span");
1027
+ insertNode(span, createTextNode(segment.text));
1028
+ effect(() => setProp(span, "style", { fg: roleColor(host.colors(), segment.role) }));
1029
+ return span;
516
1030
  };
517
- const buildRow = (row) => {
1031
+ const buildProgressLine = (view, lead) => {
1032
+ const line = createElement("text");
1033
+ setProp(line, "wrapMode", "none");
1034
+ const layout = layoutProgressLine(view, lead, SIDEBAR_ROW_WIDTH);
1035
+ layout.lead.forEach((segment, index) => {
1036
+ if (index === 0 && lead === "glyph" && view.state === "running") {
1037
+ const span = createElement("span");
1038
+ insert(span, () => SPINNER_FRAMES[frame() % SPINNER_FRAMES.length]);
1039
+ effect(() => setProp(span, "style", { fg: roleColor(host.colors(), segment.role) }));
1040
+ insertNode(line, span);
1041
+ } else {
1042
+ insertNode(line, styled(segment));
1043
+ }
1044
+ });
1045
+ for (const segment of layout.bar) insertNode(line, styled(segment));
1046
+ for (const segment of layout.tail) insertNode(line, styled(segment));
1047
+ return line;
1048
+ };
1049
+ const buildRow = (row, progress) => {
518
1050
  const box = createElement("box");
519
1051
  const layout = layoutRow(row, SIDEBAR_ROW_WIDTH);
520
1052
  const first = createElement("text");
@@ -533,14 +1065,10 @@ function createSidebar(host) {
533
1065
  if (layout.second.length > 0) {
534
1066
  const second = createElement("text");
535
1067
  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
- }
1068
+ for (const segment of layout.second) insertNode(second, styled(segment));
542
1069
  insertNode(box, second);
543
1070
  }
1071
+ if (progress) insertNode(box, attempt(() => buildProgressLine(progress, "indent"), createElement("box")));
544
1072
  if (row.sessionID) {
545
1073
  const sessionID = row.sessionID;
546
1074
  setProp(box, "onMouseDown", () => attempt(() => host.navigate(sessionID), void 0));
@@ -557,7 +1085,16 @@ function createSidebar(host) {
557
1085
  }
558
1086
  return box;
559
1087
  };
560
- const buildSection = (list, expanded, setExpanded) => {
1088
+ const buildHeading = (label) => {
1089
+ const title = createElement("text");
1090
+ const bold = createElement("b");
1091
+ insertNode(bold, createTextNode(label));
1092
+ insertNode(title, bold);
1093
+ effect(() => setProp(title, "fg", host.colors().text));
1094
+ return title;
1095
+ };
1096
+ const buildSection = (view, expanded, setExpanded) => {
1097
+ const list = view.rows;
561
1098
  const section = createElement("box");
562
1099
  const header = createElement("box");
563
1100
  setProp(header, "flexDirection", "row");
@@ -569,62 +1106,65 @@ function createSidebar(host) {
569
1106
  effect(() => setProp(caret, "fg", host.colors().text));
570
1107
  insertNode(header, caret);
571
1108
  }
572
- const title = createElement("text");
573
- const bold = createElement("b");
574
- insertNode(bold, createTextNode("Subagents"));
575
- insertNode(title, bold);
1109
+ const title = buildHeading("Subagents");
576
1110
  const summary = createElement("span");
577
1111
  insert(summary, () => expanded() ? "" : ` (${collapsedSummary(list)})`);
578
1112
  effect(() => setProp(summary, "style", { fg: host.colors().muted }));
579
1113
  insertNode(title, summary);
580
- effect(() => setProp(title, "fg", host.colors().text));
581
1114
  insertNode(header, title);
582
1115
  insertNode(section, header);
583
1116
  const body = createElement("box");
584
- insert(body, () => expanded() || list.length <= 2 ? list.map(buildRow) : null);
1117
+ insert(
1118
+ body,
1119
+ () => expanded() || list.length <= 2 ? list.map((row) => buildRow(row, row.sessionID ? view.lines.get(row.sessionID) : void 0)) : null
1120
+ );
1121
+ insertNode(section, body);
1122
+ return section;
1123
+ };
1124
+ const buildProgress = (progress) => {
1125
+ const section = createElement("box");
1126
+ insertNode(section, buildHeading("Main Progress"));
1127
+ const body = createElement("box");
1128
+ insert(body, () => {
1129
+ const view = progress();
1130
+ return view ? attempt(() => buildProgressLine(view, "glyph"), null) : null;
1131
+ });
585
1132
  insertNode(section, body);
586
1133
  return section;
587
1134
  };
588
1135
  const buildAccounts = (lines) => {
589
1136
  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);
1137
+ insertNode(section, buildHeading("Accounts"));
596
1138
  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);
1139
+ const text3 = createElement("text");
1140
+ setProp(text3, "wrapMode", "none");
1141
+ for (const segment of line.segments) insertNode(text3, styled(segment));
1142
+ insertNode(section, text3);
606
1143
  }
607
1144
  return section;
608
1145
  };
609
1146
  const render = (sessionID) => {
610
1147
  const root = createElement("box");
1148
+ const progressBox = createElement("box");
611
1149
  const subagentsBox = createElement("box");
612
1150
  const accountsBox = createElement("box");
1151
+ insertNode(root, progressBox);
613
1152
  insertNode(root, subagentsBox);
614
1153
  insertNode(root, accountsBox);
615
1154
  const key = /* @__PURE__ */ Symbol("subagents-section");
616
1155
  sectionsMounted.add(key);
617
1156
  const [expanded, setExpanded] = createSignal(true);
618
- const rows = createMemo(() => {
1157
+ const view = createMemo(() => {
619
1158
  const parentID = sessionID();
620
- if (!parentID) return [];
621
- return attempt(() => deriveRows(parentID), []);
1159
+ if (!parentID) return EMPTY_VIEW;
1160
+ return attempt(() => deriveView(parentID), EMPTY_VIEW);
622
1161
  });
623
1162
  effect(() => {
624
- const list = rows();
625
- if (list.some((row) => row.state === "running")) sectionsRunning.add(key);
1163
+ const { rows, progress } = view();
1164
+ const running = rows.some((row) => row.state === "running") || progress?.state === "running";
1165
+ if (running) sectionsRunning.add(key);
626
1166
  else sectionsRunning.delete(key);
627
- if (list.length > 0) sectionsShowing.add(key);
1167
+ if (rows.length > 0 || progress) sectionsShowing.add(key);
628
1168
  else sectionsShowing.delete(key);
629
1169
  syncClocks();
630
1170
  });
@@ -634,13 +1174,18 @@ function createSidebar(host) {
634
1174
  sectionsMounted.delete(key);
635
1175
  syncClocks();
636
1176
  });
1177
+ const hasProgress = createMemo(() => view().progress !== void 0);
1178
+ insert(
1179
+ progressBox,
1180
+ () => hasProgress() ? attempt(() => buildProgress(() => view().progress), null) : null
1181
+ );
637
1182
  insert(subagentsBox, () => {
638
- const list = rows();
639
- if (list.length === 0) return null;
640
- return attempt(() => buildSection(list, expanded, setExpanded), null);
1183
+ const current = view();
1184
+ if (current.rows.length === 0) return null;
1185
+ return attempt(() => buildSection(current, expanded, setExpanded), null);
641
1186
  });
642
1187
  const accountLines = createMemo(
643
- () => attempt(() => accountUsageLines(usage(), usageNow()), [])
1188
+ () => attempt(() => accountUsageLines(usage(), usageNow(), SIDEBAR_ROW_WIDTH, known()), [])
644
1189
  );
645
1190
  insert(accountsBox, () => {
646
1191
  const lines = accountLines();
@@ -703,6 +1248,7 @@ function registerV1(api) {
703
1248
  lastStatus.set(id, type);
704
1249
  if (type === "idle" && previous && previous !== "idle") stoppedAt.set(id, Date.now());
705
1250
  if (type !== "idle") stoppedAt.delete(id);
1251
+ bump();
706
1252
  });
707
1253
  const fetchChildren = (parentID) => {
708
1254
  if (fetched.has(parentID)) return;
@@ -718,15 +1264,24 @@ function registerV1(api) {
718
1264
  const host = {
719
1265
  colors() {
720
1266
  const theme = api.theme.current;
1267
+ const muted = theme.textMuted;
721
1268
  return {
722
1269
  text: theme.text,
723
- muted: theme.textMuted,
1270
+ muted,
724
1271
  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
1272
+ done: mix(theme.success, muted, 0.45),
1273
+ ok: theme.success ?? theme.accent,
1274
+ error: mix(theme.error, muted, 0.35),
1275
+ effort: theme.warning ?? muted,
1276
+ effortFaded: mix(theme.warning ?? muted, muted, 0.45),
1277
+ hover: theme.backgroundElement,
1278
+ barDone: theme.success ?? theme.accent,
1279
+ barTrack: mix(muted, theme.backgroundPanel ?? theme.backgroundElement, 0.5),
1280
+ fraction: theme.text,
1281
+ tool: theme.info ?? theme.accent,
1282
+ cost: theme.secondary ?? theme.primary ?? muted,
1283
+ tier: theme.secondary ?? theme.primary ?? muted,
1284
+ reset: theme.info ?? theme.accent
730
1285
  };
731
1286
  },
732
1287
  gather(parentID) {
@@ -779,9 +1334,31 @@ function registerV1(api) {
779
1334
  if (type === "idle") return "idle";
780
1335
  return "unknown";
781
1336
  },
782
- stoppedAt: (id) => stoppedAt.get(id)
1337
+ stoppedAt: (id) => stoppedAt.get(id),
1338
+ cost: (id) => attempt(() => api.state.session.get(id)?.cost, void 0)
783
1339
  };
784
1340
  },
1341
+ progress(sessionID) {
1342
+ const messages = [];
1343
+ const turn = turnMessages(api.state.session.messages(sessionID) ?? [], (message) => message.role === "user");
1344
+ for (const message of turn) {
1345
+ const tools = [];
1346
+ const texts = [];
1347
+ for (const part of api.state.part(message.id) ?? []) {
1348
+ if (message.role === "assistant" && part.type === "tool") tools.push({ name: part.tool, status: part.state?.status });
1349
+ if (message.role === "user" && part.type === "text" && typeof part.text === "string") texts.push(part.text);
1350
+ }
1351
+ messages.push({
1352
+ role: message.role,
1353
+ created: message.time?.created,
1354
+ error: message.error !== void 0 && message.error !== null,
1355
+ tools,
1356
+ chars: message.role === "user" ? promptChars(texts) : void 0
1357
+ });
1358
+ }
1359
+ const todos = attempt(() => api.state.session.todo?.(sessionID), void 0) ?? [];
1360
+ return { messages, todos: todos.map((todo) => ({ status: todo.status })) };
1361
+ },
785
1362
  navigate(sessionID) {
786
1363
  api.route.navigate("session", { sessionID });
787
1364
  }
@@ -805,16 +1382,25 @@ function setupV2(context) {
805
1382
  colors() {
806
1383
  const theme = context.theme;
807
1384
  const muted = theme.text.muted;
808
- const accent = theme.hue.accent[context.themeMode === "light" ? "500" : "200"] ?? theme.text.base;
1385
+ const shade = context.themeMode === "light" ? "500" : "200";
1386
+ const accent = theme.hue.accent[shade] ?? theme.text.base;
809
1387
  return {
810
1388
  text: theme.text.base,
811
1389
  muted,
812
1390
  spinner: accent,
813
1391
  done: mix(theme.text.feedback.success.muted ?? theme.text.feedback.success.base, muted, 0.45),
1392
+ ok: theme.text.feedback.success.base ?? accent,
814
1393
  error: mix(theme.text.feedback.error.muted ?? theme.text.feedback.error.base, muted, 0.35),
815
1394
  effort: theme.text.feedback.warning?.base ?? muted,
816
1395
  effortFaded: mix(theme.text.feedback.warning?.base ?? muted, muted, 0.45),
817
- hover: theme.background.raised.base
1396
+ hover: theme.background.raised.base,
1397
+ barDone: theme.text.feedback.success.base ?? accent,
1398
+ barTrack: mix(muted, theme.background.base ?? theme.background.raised.base, 0.5),
1399
+ fraction: theme.text.base,
1400
+ tool: theme.text.feedback.info?.base ?? accent,
1401
+ cost: theme.hue.interactive?.[shade] ?? accent,
1402
+ tier: theme.hue.interactive?.[shade] ?? accent,
1403
+ reset: theme.text.feedback.info?.base ?? accent
818
1404
  };
819
1405
  },
820
1406
  gather(parentID) {
@@ -853,9 +1439,37 @@ function setupV2(context) {
853
1439
  if (status === "idle") return "idle";
854
1440
  return "unknown";
855
1441
  },
856
- stoppedAt: () => void 0
1442
+ stoppedAt: () => void 0,
1443
+ cost: (id) => attempt(() => context.data.session.cost?.(id), void 0)
857
1444
  };
858
1445
  },
1446
+ progress(sessionID) {
1447
+ const messages = [];
1448
+ const turn = turnMessages(
1449
+ context.data.session.message.list(sessionID) ?? [],
1450
+ (message) => message.type === "user" || message.type === "synthetic"
1451
+ );
1452
+ for (const message of turn) {
1453
+ const tools = [];
1454
+ const texts = [];
1455
+ for (const item of message.content ?? []) {
1456
+ if (message.type === "assistant" && item.type === "tool") tools.push({ name: item.name, status: item.state?.status });
1457
+ if (message.type === "user" && item.type === "text" && typeof item.text === "string") texts.push(item.text);
1458
+ }
1459
+ if (message.type === "synthetic" && message.text) texts.push(message.text);
1460
+ const starts = message.type === "user" || message.type === "synthetic";
1461
+ messages.push({
1462
+ // A synthetic message (a background child's end) prompts the parent
1463
+ // like a user message does: it starts a turn.
1464
+ role: message.type === "synthetic" ? "user" : message.type ?? "",
1465
+ created: message.time?.created,
1466
+ error: message.error !== void 0 && message.error !== null,
1467
+ tools,
1468
+ chars: starts ? promptChars(texts) : void 0
1469
+ });
1470
+ }
1471
+ return { messages, todos: [] };
1472
+ },
859
1473
  navigate(sessionID) {
860
1474
  context.ui.router.navigate({ type: "session", sessionID });
861
1475
  }