@khalilgharbaoui/opencode-claude-code-plugin 0.54.2 → 0.56.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
@@ -271,6 +271,157 @@ function createSpawnRecordReader(file = spawnRecordStorePath) {
271
271
  };
272
272
  }
273
273
 
274
+ // src/account-usage.ts
275
+ import { mkdirSync as mkdirSync2, readFileSync as readFileSync2, renameSync as renameSync2, statSync as statSync2, writeFileSync as writeFileSync2 } from "fs";
276
+ import { homedir as homedir2 } from "os";
277
+ import { join as join2 } from "path";
278
+ var UNTIMED_READING_TTL_MS = 5 * 60 * 60 * 1e3;
279
+ var USAGE_READING_MAX_AGE_MS = 8 * 24 * 60 * 60 * 1e3;
280
+ var NAME = /^[a-z0-9_-]{1,64}$/;
281
+ var STATUS = /^[a-z_]{1,32}$/;
282
+ var MAX_RESETS_TEXT = 80;
283
+ var pathOverride2 = null;
284
+ 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");
288
+ }
289
+ function finite(value) {
290
+ return typeof value === "number" && Number.isFinite(value);
291
+ }
292
+ function validEntry(value) {
293
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
294
+ const entry = value;
295
+ if (!finite(entry.observedAt)) return null;
296
+ if (typeof entry.status !== "string" || !STATUS.test(entry.status)) return null;
297
+ if (entry.utilization !== null && !finite(entry.utilization)) return null;
298
+ if (entry.resetsAt !== void 0 && !finite(entry.resetsAt)) return null;
299
+ if (entry.resetsText !== void 0 && (typeof entry.resetsText !== "string" || entry.resetsText.length > MAX_RESETS_TEXT)) {
300
+ return null;
301
+ }
302
+ const noted = Array.isArray(entry.noted) ? [...new Set(entry.noted.filter((t) => finite(t) && t > 0 && t <= 100))].sort(
303
+ (a, b) => a - b
304
+ ) : [];
305
+ return {
306
+ utilization: entry.utilization,
307
+ ...entry.resetsAt === void 0 ? {} : { resetsAt: entry.resetsAt },
308
+ ...entry.resetsText === void 0 ? {} : { resetsText: entry.resetsText },
309
+ status: entry.status,
310
+ observedAt: entry.observedAt,
311
+ noted
312
+ };
313
+ }
314
+ function readStore(file, now) {
315
+ const out = /* @__PURE__ */ new Map();
316
+ try {
317
+ const parsed = JSON.parse(readFileSync2(file, "utf8"));
318
+ const accounts = parsed && typeof parsed === "object" ? parsed.accounts : null;
319
+ if (!accounts || typeof accounts !== "object" || Array.isArray(accounts)) return out;
320
+ const cutoff = now - USAGE_READING_MAX_AGE_MS;
321
+ for (const [account, windows] of Object.entries(accounts)) {
322
+ if (!NAME.test(account) || !windows || typeof windows !== "object") continue;
323
+ const kept = /* @__PURE__ */ new Map();
324
+ for (const [window, raw] of Object.entries(windows)) {
325
+ if (!NAME.test(window)) continue;
326
+ const entry = validEntry(raw);
327
+ if (entry && entry.observedAt >= cutoff) kept.set(window, entry);
328
+ }
329
+ if (kept.size > 0) out.set(account, kept);
330
+ }
331
+ } catch {
332
+ }
333
+ return out;
334
+ }
335
+ function isReadingCurrent(entry, now = Date.now()) {
336
+ if (entry.resetsAt !== void 0) return entry.resetsAt > now;
337
+ return now - entry.observedAt < UNTIMED_READING_TTL_MS;
338
+ }
339
+ function usagePercent(utilization) {
340
+ return Math.round(utilization * 1e3) / 10;
341
+ }
342
+ function createAccountUsageReader(file = accountUsageStorePath) {
343
+ let identity = "";
344
+ let usage = /* @__PURE__ */ new Map();
345
+ return {
346
+ 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;
360
+ }
361
+ };
362
+ }
363
+
364
+ // src/account-sidebar.ts
365
+ var WARNING_PERCENT = 75;
366
+ var SHORT_WINDOWS = {
367
+ five_hour: "5h",
368
+ seven_day: "7d",
369
+ seven_day_opus: "7d opus",
370
+ seven_day_sonnet: "7d sonnet",
371
+ seven_day_overage_included: "7d extra",
372
+ overage: "extra"
373
+ };
374
+ function shortWindowName(window) {
375
+ if (window === "unknown") return "";
376
+ return SHORT_WINDOWS[window] ?? window.replace(/_/g, " ");
377
+ }
378
+ var MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
379
+ function formatResetClock(resetsAt, now) {
380
+ const when = new Date(resetsAt);
381
+ const today = new Date(now);
382
+ const pad = (value) => String(value).padStart(2, "0");
383
+ 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}`;
386
+ }
387
+ function rank(entry) {
388
+ if (entry.status === "rejected") return 1e3;
389
+ if (entry.utilization === null) return -1;
390
+ return usagePercent(entry.utilization);
391
+ }
392
+ function pressingWindow(windows, now) {
393
+ let best;
394
+ for (const [window, entry] of windows) {
395
+ if (!isReadingCurrent(entry, now)) continue;
396
+ if (!best || rank(entry) > rank(best[1])) best = [window, entry];
397
+ }
398
+ return best;
399
+ }
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;
405
+ const [window, entry] = pressing;
406
+ const state = entry.status === "rejected" ? "limited" : entry.utilization !== null && usagePercent(entry.utilization) >= WARNING_PERCENT ? "warning" : "ok";
407
+ const short = shortWindowName(window);
408
+ 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
+ });
421
+ }
422
+ return lines;
423
+ }
424
+
274
425
  // src/tui.ts
275
426
  var SUBAGENTS_TUI_PLUGIN_ID = "opencode-claude-code-plugin.subagents";
276
427
  var SECTION_ORDER = 900;
@@ -320,8 +471,12 @@ function createSidebar(host) {
320
471
  const records = createSpawnRecordReader();
321
472
  let recordMap = attempt(() => records.read(), /* @__PURE__ */ new Map());
322
473
  const [recordVersion, setRecordVersion] = createSignal(0);
474
+ const usageReader = createAccountUsageReader();
475
+ const [usage, setUsage] = createSignal(attempt(() => usageReader.read(), /* @__PURE__ */ new Map()));
476
+ const [usageNow, setUsageNow] = createSignal(Date.now());
323
477
  const sectionsRunning = /* @__PURE__ */ new Set();
324
478
  const sectionsShowing = /* @__PURE__ */ new Set();
479
+ const sectionsMounted = /* @__PURE__ */ new Set();
325
480
  let spinner;
326
481
  let refresh;
327
482
  const syncClocks = () => {
@@ -331,16 +486,22 @@ function createSidebar(host) {
331
486
  clearInterval(spinner);
332
487
  spinner = void 0;
333
488
  }
334
- if (sectionsShowing.size > 0 && !refresh) {
489
+ const wanted = sectionsShowing.size > 0 || sectionsMounted.size > 0;
490
+ if (wanted && !refresh) {
335
491
  refresh = setInterval(() => {
336
- const next = attempt(() => records.read(), recordMap);
337
- if (next !== recordMap) {
338
- recordMap = next;
339
- setRecordVersion((value) => value + 1);
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());
340
499
  }
341
- setNow(Date.now());
500
+ setUsage(attempt(() => usageReader.read(), usage()));
501
+ const minute = Math.floor(Date.now() / 6e4) * 6e4;
502
+ if (minute !== usageNow()) setUsageNow(minute);
342
503
  }, REFRESH_INTERVAL_MS);
343
- } else if (sectionsShowing.size === 0 && refresh) {
504
+ } else if (!wanted && refresh) {
344
505
  clearInterval(refresh);
345
506
  refresh = void 0;
346
507
  }
@@ -424,9 +585,35 @@ function createSidebar(host) {
424
585
  insertNode(section, body);
425
586
  return section;
426
587
  };
588
+ const buildAccounts = (lines) => {
589
+ 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);
596
+ 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);
606
+ }
607
+ return section;
608
+ };
427
609
  const render = (sessionID) => {
428
610
  const root = createElement("box");
611
+ const subagentsBox = createElement("box");
612
+ const accountsBox = createElement("box");
613
+ insertNode(root, subagentsBox);
614
+ insertNode(root, accountsBox);
429
615
  const key = /* @__PURE__ */ Symbol("subagents-section");
616
+ sectionsMounted.add(key);
430
617
  const [expanded, setExpanded] = createSignal(true);
431
618
  const rows = createMemo(() => {
432
619
  const parentID = sessionID();
@@ -444,18 +631,29 @@ function createSidebar(host) {
444
631
  onCleanup(() => {
445
632
  sectionsRunning.delete(key);
446
633
  sectionsShowing.delete(key);
634
+ sectionsMounted.delete(key);
447
635
  syncClocks();
448
636
  });
449
- insert(root, () => {
637
+ insert(subagentsBox, () => {
450
638
  const list = rows();
451
639
  if (list.length === 0) return null;
452
640
  return attempt(() => buildSection(list, expanded, setExpanded), null);
453
641
  });
642
+ const accountLines = createMemo(
643
+ () => attempt(() => accountUsageLines(usage(), usageNow()), [])
644
+ );
645
+ insert(accountsBox, () => {
646
+ const lines = accountLines();
647
+ if (lines.length === 0) return null;
648
+ return attempt(() => buildAccounts(lines), null);
649
+ });
650
+ syncClocks();
454
651
  return root;
455
652
  };
456
653
  const dispose = () => {
457
654
  sectionsRunning.clear();
458
655
  sectionsShowing.clear();
656
+ sectionsMounted.clear();
459
657
  syncClocks();
460
658
  };
461
659
  return { render, dispose };