@iowarp/clio-coder 0.5.0 → 0.5.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.
Files changed (132) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/README.md +12 -9
  3. package/dist/{acp-KS7ARGV6.js → acp-X3N7JDGS.js} +2 -2
  4. package/dist/{agents-YOGJ2YEJ.js → agents-UYGSMAWR.js} +3 -3
  5. package/dist/assets/codewiki.json +1 -1
  6. package/dist/{chunk-EOJPDNUP.js → chunk-3OODBJ45.js} +1 -1
  7. package/dist/{chunk-R7FGRZNF.js → chunk-4D7QM7S5.js} +2 -2
  8. package/dist/{chunk-X4CEFOSG.js → chunk-6B76U65H.js} +4 -4
  9. package/dist/{chunk-LTWH2ACR.js → chunk-7FHSA7VB.js} +5 -5
  10. package/dist/{chunk-GRHOXDKB.js → chunk-7KI4ZSEB.js} +2 -2
  11. package/dist/{chunk-LYJJ6546.js → chunk-7VNLPEUG.js} +6 -6
  12. package/dist/{chunk-D65E25JG.js → chunk-A7G7OUND.js} +2 -2
  13. package/dist/{chunk-3JLTRPN7.js → chunk-AKQ3N3L4.js} +2 -2
  14. package/dist/{chunk-AVSR2HKI.js → chunk-BIQLF3NZ.js} +3 -3
  15. package/dist/{chunk-D3KWFOUT.js → chunk-DGMY2DWE.js} +2 -2
  16. package/dist/{chunk-IN7DGBVS.js → chunk-FBXTHPNZ.js} +2 -2
  17. package/dist/{chunk-EX57VVWW.js → chunk-GGEEG7CD.js} +2 -2
  18. package/dist/{chunk-V35KZDUZ.js → chunk-GW2KVAVI.js} +2 -2
  19. package/dist/{chunk-SJGS3GDI.js → chunk-KIAUZGKF.js} +2 -2
  20. package/dist/{chunk-CBAQPTDU.js → chunk-LFYA3HYH.js} +2 -2
  21. package/dist/{chunk-4VDHLCF5.js → chunk-M5FPIZQW.js} +2 -2
  22. package/dist/{chunk-R2UUNSNQ.js → chunk-O3ZAF7UY.js} +18 -5
  23. package/dist/{chunk-QNUQ7K7D.js → chunk-OXWRE2QJ.js} +2 -2
  24. package/dist/{chunk-55RMDDFM.js → chunk-PGQG7ZMW.js} +2 -2
  25. package/dist/{chunk-HQVN7G4F.js → chunk-QM7HFPQQ.js} +4 -4
  26. package/dist/{chunk-ZR5YA6ZF.js → chunk-QTYRMBW6.js} +2 -2
  27. package/dist/{chunk-3LT34CAM.js → chunk-SAL2SBP4.js} +8 -8
  28. package/dist/{chunk-OJXOA6YU.js → chunk-TL6LNERH.js} +2 -2
  29. package/dist/{chunk-U6YTUVOX.js → chunk-VMTCZ2GR.js} +2 -2
  30. package/dist/{chunk-OQ2EOAHA.js → chunk-VWKR2NN5.js} +5 -5
  31. package/dist/{chunk-QDUUOZ3S.js → chunk-WAISUJNQ.js} +2 -2
  32. package/dist/{chunk-XDHUDE5K.js → chunk-XOH3SD7H.js} +3 -3
  33. package/dist/{chunk-62UK7AGW.js → chunk-ZNTGRDFA.js} +281 -14
  34. package/dist/cli/index.js +17 -17
  35. package/dist/{clio-K2PBVCEM.js → clio-Z3JTEL2I.js} +2 -2
  36. package/dist/{config-Q5J7UBAB.js → config-QYIRZHI7.js} +8 -8
  37. package/dist/{config-graph-P3555574.js → config-graph-TS3SGGQW.js} +8 -8
  38. package/dist/{configure-KIJ32ZO6.js → configure-OX7KVJTQ.js} +2 -2
  39. package/dist/{context-LFBQOSXT.js → context-2BNF6HTP.js} +12 -12
  40. package/dist/{context-WBH3KSVR.js → context-GTMUGAR4.js} +7 -7
  41. package/dist/{context-clear-YOHMQ6SQ.js → context-clear-FVJQBBL7.js} +7 -7
  42. package/dist/{context-working-set-MRAEVDNV.js → context-working-set-MOE72SD6.js} +2 -2
  43. package/dist/{detail-RHJOTGN4.js → detail-QZM3QAAU.js} +8 -8
  44. package/dist/{dispatch-runner-NC7PYNIO.js → dispatch-runner-2KSQO5FU.js} +8 -8
  45. package/dist/{doctor-S5KZ2GTN.js → doctor-2U6YR5NH.js} +3 -3
  46. package/dist/{eval-3D6M36N7.js → eval-ZKK77JWZ.js} +3 -3
  47. package/dist/{evidence-6IYN52ET.js → evidence-FMP7KZZ2.js} +7 -7
  48. package/dist/{evidence-JVECPI3F.js → evidence-PJTOE7JA.js} +7 -7
  49. package/dist/{evidence-XNVPPLVK.js → evidence-POSEQW4Q.js} +9 -9
  50. package/dist/{evolve-TLPUDJDM.js → evolve-JT6PAMZE.js} +7 -7
  51. package/dist/{fleet-WKVBF5SZ.js → fleet-4XR2ZEOF.js} +16 -16
  52. package/dist/{fleet-6RJWIOEO.js → fleet-6JJKNIMV.js} +8 -8
  53. package/dist/{fleet-inspect-236FZPRB.js → fleet-inspect-SWJMFVDP.js} +9 -9
  54. package/dist/{fleet-preflight-NZQHABWO.js → fleet-preflight-KSIHWQAU.js} +4 -4
  55. package/dist/{fleet-verify-E3RO6A5W.js → fleet-verify-QM6XU2KL.js} +7 -7
  56. package/dist/{fleet-view-KMZQY7YJ.js → fleet-view-RLKFHFCM.js} +8 -8
  57. package/dist/gui/reads-worker.js +4 -4
  58. package/dist/{init-DV5B6HQG.js → init-VBUPHABR.js} +11 -11
  59. package/dist/{interactive-OXGYZYAS.js → interactive-JCVBPVYT.js} +1597 -503
  60. package/dist/{inventory-OEGIL6PY.js → inventory-54G7FSVY.js} +8 -8
  61. package/dist/{mcp-2GSPYQ54.js → mcp-EFCWUPUR.js} +3 -3
  62. package/dist/{memory-J3STUUIK.js → memory-5BPQSSOP.js} +7 -7
  63. package/dist/{models-Y3GMFJ2V.js → models-R6HILX6X.js} +3 -3
  64. package/dist/{monitor-LHNH577J.js → monitor-HVRKSFEQ.js} +10 -10
  65. package/dist/{orchestrator-DTAPXULA.js → orchestrator-OR7V4YWK.js} +26 -26
  66. package/dist/{preload-OJBMAIXF.js → preload-KLXFOX2F.js} +7 -7
  67. package/dist/{run-H64MEVTA.js → run-UIDRUYAY.js} +16 -16
  68. package/dist/{skills-eval-CQ23MYQK.js → skills-eval-JDAXOPWC.js} +7 -7
  69. package/dist/{slash-commands-ROG3KIFJ.js → slash-commands-MNKQ5QP3.js} +4 -4
  70. package/dist/{startup-background-KDWCQMWT.js → startup-background-PJMWTKVO.js} +7 -7
  71. package/dist/{system-ZDW44ZGX.js → system-IVVZRKGI.js} +2 -2
  72. package/dist/{targets-AJWA6E7Z.js → targets-RFRUMCQ5.js} +4 -4
  73. package/dist/{terminal-lease-YFUSDPAD.js → terminal-lease-V2N2BIKQ.js} +2 -2
  74. package/dist/{usage-XSILDHKF.js → usage-LA33SG3R.js} +9 -9
  75. package/dist/{wiki-generate-RP22WP5K.js → wiki-generate-HL73TICA.js} +10 -10
  76. package/dist/worker/entry.js +4 -4
  77. package/docs/architecture/architecture.md +1 -0
  78. package/docs/architecture/context-engine.md +2 -2
  79. package/docs/architecture/observability.md +6 -4
  80. package/docs/architecture/prompt-envelope-and-tools.md +1 -1
  81. package/docs/architecture/session-lifecycle.md +1 -1
  82. package/docs/architecture/tui-design.md +12 -9
  83. package/docs/gui/parity/02-slash-and-surfaces.md +1 -1
  84. package/docs/guide/commands-and-modes.md +91 -5
  85. package/docs/guide/configuration-and-targets.md +2 -2
  86. package/docs/guide/proactive-memory.md +3 -3
  87. package/package.json +1 -1
  88. package/src/cli/usage.ts +2 -2
  89. package/src/domains/gateway/mcp/client.ts +1 -1
  90. package/src/domains/observability/background-memory-usage.ts +1 -1
  91. package/src/domains/observability/cost.ts +3 -3
  92. package/src/domains/observability/extension.ts +1 -1
  93. package/src/domains/observability/metrics.ts +1 -1
  94. package/src/domains/quota/anthropic-max-provider.ts +134 -0
  95. package/src/domains/quota/anthropic-usage.ts +220 -0
  96. package/src/domains/quota/antigravity-provider.ts +339 -0
  97. package/src/domains/quota/cache.ts +87 -0
  98. package/src/domains/quota/claude-code-provider.ts +169 -0
  99. package/src/domains/quota/codex-provider.ts +237 -0
  100. package/src/domains/quota/presentation.ts +240 -0
  101. package/src/domains/quota/registry.ts +23 -0
  102. package/src/domains/quota/service.ts +93 -0
  103. package/src/domains/quota/summary-feed.ts +86 -0
  104. package/src/domains/quota/types.ts +84 -0
  105. package/src/domains/session/usage.ts +2 -2
  106. package/src/engine/acp/commands.ts +1 -1
  107. package/src/entry/orchestrator.ts +2 -2
  108. package/src/interactive/chat-loop-messages.ts +2 -2
  109. package/src/interactive/chat-loop.ts +3 -3
  110. package/src/interactive/dispatch-board.ts +21 -3
  111. package/src/interactive/footer/dashboard.ts +13 -0
  112. package/src/interactive/footer/key-hints.ts +2 -2
  113. package/src/interactive/footer/pages.ts +55 -20
  114. package/src/interactive/footer-panel.ts +1 -1
  115. package/src/interactive/interactive-application.ts +5 -2
  116. package/src/interactive/interactive-input-runtime.ts +2 -2
  117. package/src/interactive/interactive-presentation.ts +15 -0
  118. package/src/interactive/interactive-slash-runtime.ts +2 -2
  119. package/src/interactive/interactive-tickers.ts +7 -1
  120. package/src/interactive/overlay-general-openers.ts +12 -8
  121. package/src/interactive/overlay-key-routing.ts +6 -9
  122. package/src/interactive/overlay-lifecycle.ts +10 -6
  123. package/src/interactive/overlay-session-lifecycle.ts +3 -3
  124. package/src/interactive/quota-view.ts +229 -0
  125. package/src/interactive/session-last-turn.ts +1 -1
  126. package/src/interactive/session-usage-reseed.ts +2 -2
  127. package/src/interactive/side-question.ts +2 -2
  128. package/src/interactive/slash-commands.ts +8 -8
  129. package/src/interactive/turn-context.ts +1 -1
  130. package/src/interactive/turn-prewarm.ts +1 -1
  131. package/src/interactive/{cost-overlay.ts → usage-overlay.ts} +121 -33
  132. package/src/interactive/welcome-dashboard.ts +26 -3
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Read-only data model for provider subscription quota.
3
+ *
4
+ * The shapes mirror QuotaBubble's `providers/base.py` (`ProviderStatus`,
5
+ * `UsageWindow`, `UsageSnapshot`) so that adapters written against either
6
+ * project describe the same usage facts. Timestamps are ISO 8601 strings
7
+ * rather than `Date` objects so a snapshot stays structurally cloneable
8
+ * across the worker and transport boundaries.
9
+ */
10
+
11
+ /** Outcome of one usage read, including the reasons a read produced no data. */
12
+ export type QuotaStatus = "ok" | "no_credentials" | "expired" | "error" | "loading";
13
+
14
+ /** One rate-limit window reported by a provider. */
15
+ export interface UsageWindow {
16
+ /** Stable identity for ordering and lookup, for example "session" or "weekly". */
17
+ key: string;
18
+ /** Human label such as "5h" or "Weekly". */
19
+ label: string;
20
+ /** Canonical percentage consumed, 0 to 100. Adapters convert remaining fractions here. */
21
+ usedPct: number;
22
+ /** ISO 8601 instant the window resets, or null when the provider omitted it. */
23
+ resetsAt: string | null;
24
+ /** Compact label for a narrow surface such as a footer indicator. */
25
+ short?: string;
26
+ /** Model or product scope when the window covers less than the whole plan. */
27
+ scope?: string;
28
+ /** Provider-supplied severity word; Clio does not invent one. */
29
+ severity?: string;
30
+ /** True when the provider marks this window as the currently binding limit. */
31
+ active?: boolean;
32
+ }
33
+
34
+ /** Prepaid credit balance for providers that bill credits instead of windows. */
35
+ export interface UsageCredits {
36
+ display: string;
37
+ usedPct: number | null;
38
+ }
39
+
40
+ /** Everything one provider reported about quota at a single point in time. */
41
+ export interface UsageSnapshot {
42
+ providerId: string;
43
+ displayName: string;
44
+ status: QuotaStatus;
45
+ windows: UsageWindow[];
46
+ credits?: UsageCredits | null;
47
+ plan?: string | null;
48
+ message?: string | null;
49
+ /** Seconds to wait, captured from a 429 `Retry-After` header. */
50
+ retryAfterSeconds?: number | null;
51
+ /** True when the snapshot is a cached last-good value rather than a fresh read. */
52
+ stale?: boolean;
53
+ /** ISO 8601 instant of the read that produced this snapshot. */
54
+ fetchedAt: string | null;
55
+ }
56
+
57
+ /**
58
+ * One runtime's quota adapter. Implementations stay pure data plumbing:
59
+ * no UI, no dispatch admission, no credential writes.
60
+ */
61
+ export interface QuotaProvider {
62
+ id: string;
63
+ displayName: string;
64
+ /** True when usable credentials are present, without spending a network call. */
65
+ detect(): Promise<boolean>;
66
+ /** Read current usage, reporting failure as a snapshot status rather than throwing. */
67
+ fetch(): Promise<UsageSnapshot>;
68
+ }
69
+
70
+ /** Parse a numeric `Retry-After` value into non-negative seconds. */
71
+ export function parseRetryAfterSeconds(value: string | null | undefined): number | null {
72
+ if (!value) return null;
73
+ const seconds = Number.parseFloat(value.trim());
74
+ if (!Number.isFinite(seconds)) return null;
75
+ return Math.max(0, seconds);
76
+ }
77
+
78
+ /** Turn a raw plan identifier such as "max_5x" into a display plan such as "Max 5X". */
79
+ export function formatPlan(value: string | null | undefined): string | null {
80
+ if (!value) return null;
81
+ const words = value.replace(/_/g, " ").trim();
82
+ if (!words) return null;
83
+ return words.replace(/\S+/g, (word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase());
84
+ }
@@ -5,7 +5,7 @@
5
5
  * for each call.
6
6
  *
7
7
  * This lives in the session domain because the ledger is the only durable
8
- * record of what a session spent, and three surfaces read it: the `/cost`
8
+ * record of what a session spent, and three surfaces read it: the `/usage`
9
9
  * overlay and footer reseed from it on every session change, and `clio-coder usage
10
10
  * report` folds it across sessions. It was written for the overlay first and
11
11
  * sat under src/interactive; a headless report reaching into the TUI surface's
@@ -94,7 +94,7 @@ export function ledgerUsageCalls(
94
94
  // A compaction summarizes history through a real model call, billed like
95
95
  // any other. Its usage rides on the compactionSummary entry rather than on
96
96
  // an assistant message, because the summary is context machinery and never
97
- // enters the conversation; folding it here is what puts it on `/cost` and
97
+ // enters the conversation; folding it here is what puts it on `/usage` and
98
98
  // in `clio-coder usage report`.
99
99
  if (entry?.kind === "compactionSummary") {
100
100
  const usage = entry.usage;
@@ -590,7 +590,7 @@ function headlessContext(
590
590
  listAgents: () => unreachable("listAgents"),
591
591
  listDelegationAgents: () => unreachable("listDelegationAgents"),
592
592
  shutdown: () => unreachable("shutdown"),
593
- openCost: () => unreachable("openCost"),
593
+ openUsage: () => unreachable("openUsage"),
594
594
  openSideQuestion: () => unreachable("openSideQuestion"),
595
595
  startHandoff: () => unreachable("startHandoff"),
596
596
  openContextView: () => unreachable("openContextView"),
@@ -925,7 +925,7 @@ async function runCompactionFlow(
925
925
  isSplitTurn: result.isSplitTurn,
926
926
  tokensAfter: estimateTokensAfterCompaction(entries, result),
927
927
  // The summarization call is a real model call. Persisting its provider
928
- // usage on the entry is what puts it in front of `/cost` and `clio-coder usage
928
+ // usage on the entry is what puts it in front of `/usage` and `clio-coder usage
929
929
  // report`, which folded the ledger and so counted every call but this one.
930
930
  ...(result.usage !== undefined ? { usage: result.usage } : {}),
931
931
  };
@@ -1495,7 +1495,7 @@ export async function bootOrchestrator(options: BootOptions = {}): Promise<BootR
1495
1495
  // check and the cost row read the same resolution the step itself used.
1496
1496
  /**
1497
1497
  * Account for one background memory step exactly as a `/btw` side question is
1498
- * accounted for: the in-process cost tracker under its own label so `/cost`
1498
+ * accounted for: the in-process cost tracker under its own label so `/usage`
1499
1499
  * shows it while the session lives, and one durable out-of-turn row so
1500
1500
  * `clio-coder usage report` can still see it afterwards.
1501
1501
  *
@@ -379,7 +379,7 @@ export interface RunUsageSummary {
379
379
  * messages. Earlier versions of this function walked the list from the tail
380
380
  * and returned the first match, which silently dropped every intermediate
381
381
  * API call from the cost tally. Summing instead matches what the provider
382
- * actually billed and keeps the `/cost` overlay and footer counters
382
+ * actually billed and keeps the `/usage` overlay and footer counters
383
383
  * aligned across tool-heavy runs.
384
384
  */
385
385
  export function sumRunUsage(messages: ReadonlyArray<AgentMessage>): RunUsageSummary {
@@ -539,7 +539,7 @@ export function assistantSessionPayload(
539
539
  *
540
540
  * `estimated: true` is the provenance marker: no surface may report these as
541
541
  * provider-reported numbers. The usage fold and the context estimator both skip
542
- * aborted turns already, so this never reaches `/cost` or the window math; it is
542
+ * aborted turns already, so this never reaches `/usage` or the window math; it is
543
543
  * the record's own honesty about what the cancelled call cost.
544
544
  *
545
545
  * Returns null when the provider did report usage (there is nothing to fill in)
@@ -362,7 +362,7 @@ export interface ChatLoop {
362
362
  *
363
363
  * Nothing this produces reaches the session ledger, the transcript, the
364
364
  * context ledger, or the footer token counters; the message history is read,
365
- * never mutated. The round's provider usage is still reported to `/cost`,
365
+ * never mutated. The round's provider usage is still reported to `/usage`,
366
366
  * labeled as a side question, because money was spent. Refused outright
367
367
  * while a turn is in flight rather than queued.
368
368
  */
@@ -967,12 +967,12 @@ export function createChatLoop(deps: CreateChatLoopDeps): ChatLoop {
967
967
  deps.recordOutOfTurnUsageRow ?? ((row: OutOfTurnUsageRow): void => appendOutOfTurnUsageRow(clioStateDir(), row));
968
968
 
969
969
  /**
970
- * Report an out-of-turn round's provider usage. Money was spent, so `/cost`
970
+ * Report an out-of-turn round's provider usage. Money was spent, so `/usage`
971
971
  * says so under its own label; turn persistence, the working-set ledger,
972
972
  * compaction inputs, and the footer counters never see it.
973
973
  *
974
974
  * The same call is also appended to the out-of-turn usage store under the
975
- * state dir. That store exists because `/cost` only knows what this process
975
+ * state dir. That store exists because `/usage` only knows what this process
976
976
  * spent: the round appends nothing to the session JSONL by design, so an
977
977
  * archive reader such as `clio-coder usage report` had no record of the
978
978
  * spend at all once the process exited. The session ledger stays untouched.
@@ -23,12 +23,14 @@ import {
23
23
  } from "../domains/observability/index.js";
24
24
  import type { WorkerAction, WorkerProgressSnapshot } from "../domains/observability/worker-progress.js";
25
25
  import { type CostProvenance, foregroundStreamUsage } from "../domains/providers/index.js";
26
+ import type { UsageSnapshot } from "../domains/quota/types.js";
26
27
  import { sanitizeCallTargetText } from "../domains/safety/call-target.js";
27
28
  import { type Component, truncateToWidth, visibleWidth, wrapTextWithAnsi } from "../engine/tui.js";
28
29
  import { formatWorkerContextMeter } from "./context-meter.js";
29
30
  import { COUNCIL_SYNTHESIS_LABEL } from "./council.js";
30
31
  import { type CouncilGroupView, type CouncilMemberView, councilGroupBody, councilIslandLines } from "./council-grid.js";
31
32
  import { formatFooterTokens } from "./footer-panel.js";
33
+ import { routeWeeklyQuota } from "./quota-view.js";
32
34
  import { presentWorkerContractAnswer, safeWorkerAnswerText } from "./renderers/worker-answer.js";
33
35
  import {
34
36
  type ClioTheme,
@@ -780,8 +782,9 @@ function renderDispatchCard(
780
782
  return frame(theme, cardTitle, bodyLines, width, { rightMeta: elapsed });
781
783
  }
782
784
 
783
- function renderTaskIslandRow(row: DispatchBoardRow, width: number): string[] {
785
+ function renderTaskIslandRow(row: DispatchBoardRow, width: number, quota: ReadonlyArray<UsageSnapshot>): string[] {
784
786
  const theme = clioTheme();
787
+ const weekly = routeWeeklyQuota(row, quota);
785
788
  const agentLabel = agentDisplayLabel(row);
786
789
  const elapsed = formatCompactMs(row.elapsedMs);
787
790
  const cost = formatCostAggregate(costAggregateForAmount(row.costUsd, row.costProvenance)) ?? COST_NOT_MEASURED;
@@ -854,6 +857,17 @@ function renderTaskIslandRow(row: DispatchBoardRow, width: number): string[] {
854
857
  ...quality.map((line) => padAnsi(theme.fg("muted", line), width)),
855
858
  ...task.map((line) => padAnsi(line, width)),
856
859
  padAnsi(telemetry, width),
860
+ ...(weekly
861
+ ? [
862
+ padAnsi(
863
+ theme.fg(
864
+ weekly.severity === "critical" ? "error" : weekly.severity === "normal" ? "dim" : "warning",
865
+ ` Shared account · ${weekly.label}`,
866
+ ),
867
+ width,
868
+ ),
869
+ ]
870
+ : []),
857
871
  ...wrapTextWithAnsi(theme.fg("dim", ` /view dispatch:${row.runId}`), width),
858
872
  ];
859
873
  }
@@ -1110,7 +1124,11 @@ export function createDispatchBoardView(
1110
1124
  };
1111
1125
  }
1112
1126
 
1113
- export function formatTaskIslandLines(rows: ReadonlyArray<DispatchBoardRow>, maxRows = 4): string[] {
1127
+ export function formatTaskIslandLines(
1128
+ rows: ReadonlyArray<DispatchBoardRow>,
1129
+ maxRows = 4,
1130
+ quota: ReadonlyArray<UsageSnapshot> = [],
1131
+ ): string[] {
1114
1132
  // Councils are folded before the row cap, so a five-member council costs the
1115
1133
  // island one card and never crowds out the runs beside it.
1116
1134
  const items = dispatchBoardItems(rows.filter((row) => !isHelperRun(row)));
@@ -1131,7 +1149,7 @@ export function formatTaskIslandLines(rows: ReadonlyArray<DispatchBoardRow>, max
1131
1149
  body.push(
1132
1150
  ...(item.kind === "council"
1133
1151
  ? councilIslandLines(clioTheme(), item.group, TASK_ISLAND_WIDTH)
1134
- : renderTaskIslandRow(item.row, TASK_ISLAND_WIDTH)),
1152
+ : renderTaskIslandRow(item.row, TASK_ISLAND_WIDTH, quota)),
1135
1153
  );
1136
1154
  }
1137
1155
  const hidden = items.length - visibleItems.length;
@@ -14,6 +14,7 @@ import {
14
14
  resolveModelCapabilities,
15
15
  resolveModelRuntimeCapabilitiesForProviders,
16
16
  } from "../../domains/providers/index.js";
17
+ import type { UsageSnapshot } from "../../domains/quota/types.js";
17
18
  import type { LocalCapacity } from "../../domains/scheduling/local-capacity.js";
18
19
  import type { ContextUsageSnapshot } from "../../domains/session/context-accounting.js";
19
20
  import type { ContextLedger } from "../../domains/session/context-ledger.js";
@@ -88,6 +89,7 @@ export type { ToolTallySnapshot } from "./widgets.js";
88
89
  export type FooterDashboardMode = "compact" | "expanded";
89
90
 
90
91
  export interface FooterDashboardDeps {
92
+ getQuotaSnapshots?: () => ReadonlyArray<UsageSnapshot>;
91
93
  getConnections?: () => { mcp: string[]; plugins: string[] };
92
94
  getExtensionStatus?: () => ReadonlyArray<string>;
93
95
  providers: ProvidersContract;
@@ -127,6 +129,8 @@ export interface FooterDashboardDeps {
127
129
  }
128
130
 
129
131
  export interface FooterDashboardRenderState {
132
+ quota?: ReadonlyArray<UsageSnapshot>;
133
+ quotaRoute?: Pick<DispatchBoardRow, "runtimeId" | "wireModelId" | "node">;
130
134
  demoHint?: string | null;
131
135
  resources?: LocalMachineMetrics | null;
132
136
  connections?: { mcp: string[]; plugins: string[] };
@@ -413,6 +417,15 @@ export function buildFooterDashboard(deps: FooterDashboardDeps): FooterDashboard
413
417
 
414
418
  return {
415
419
  resources: machine.snapshot(),
420
+ quota: deps.getQuotaSnapshots?.() ?? [],
421
+ ...(current?.runtime && wireModelId
422
+ ? {
423
+ quotaRoute: {
424
+ runtimeId: current.runtime.id,
425
+ wireModelId,
426
+ },
427
+ }
428
+ : {}),
416
429
  ...(deps.getConnections ? { connections: deps.getConnections() } : {}),
417
430
  ...(settings ? { costCeilingUsd: settings.safety.limits.sessionCostUsd } : {}),
418
431
 
@@ -14,8 +14,8 @@ export function footerKeyHint(now: number, narrow = false): string | null {
14
14
  }),
15
15
  ];
16
16
  const count = narrow ? 1 : 2;
17
- const page = Math.floor(now / 12_000) % (Math.ceil(entries.length / count) + 1);
18
- if (page === Math.ceil(entries.length / count)) return null;
17
+ if (!entries.length) return null;
18
+ const page = Math.floor(now / 12_000) % Math.ceil(entries.length / count);
19
19
  const start = page * count;
20
20
  return entries.slice(start, start + count).join(" · ");
21
21
  }
@@ -1,4 +1,5 @@
1
1
  import { costAggregateForAmount, formatCostAggregate } from "../../domains/observability/index.js";
2
+ import type { UsageSnapshot } from "../../domains/quota/types.js";
2
3
  /** Expanded footer pages: dense inspection, separate from transcript output style. */
3
4
  import { sanitizeCallTargetText } from "../../domains/safety/call-target.js";
4
5
  import { redactSecretString } from "../../domains/safety/redaction.js";
@@ -6,6 +7,7 @@ import { getKeybindings, truncateToWidth, visibleWidth, wrapTextWithAnsi } from
6
7
  import { contextCategorySwatch, renderContextMeterBar, renderContextMeterGrid } from "../context-meter.js";
7
8
  import { type DispatchBoardRow, dispatchStatusPresentation, renderDispatchActivity } from "../dispatch-board.js";
8
9
  import { formatFooterTokens } from "../footer-panel.js";
10
+ import { renderQuotaAccounts, routeWeeklyQuota } from "../quota-view.js";
9
11
  import { previewRows } from "../renderers/preview.js";
10
12
  import { clioTheme, formatCompactMs, rule } from "../theme/index.js";
11
13
  import { fitIdentityLabel } from "../theme/labels.js";
@@ -18,7 +20,12 @@ export type DashboardPage = (typeof DASHBOARD_PAGES)[number];
18
20
  const ACTIVE_AGENT_STATUSES = new Set(["running", "enqueued", "cancelling", "retrying", "stale"]);
19
21
  const clean = (value: string) => sanitizeCallTargetText(redactSecretString(value));
20
22
 
21
- function agentCard(row: DispatchBoardRow, width: number, compact = false): string[] {
23
+ function agentCard(
24
+ row: DispatchBoardRow,
25
+ width: number,
26
+ compact = false,
27
+ quota: ReadonlyArray<UsageSnapshot> = [],
28
+ ): string[] {
22
29
  const theme = clioTheme();
23
30
  const presentation = dispatchStatusPresentation(row.status, { compact: false });
24
31
  const name = clean(row.agentId).replace(
@@ -34,6 +41,9 @@ function agentCard(row: DispatchBoardRow, width: number, compact = false): strin
34
41
  const field = (label: string, value: string) =>
35
42
  wrapTextWithAnsi(`${theme.fg("dim", `${label} `)}${clean(value)}`, width);
36
43
  lines.push(...previewRows(field("Route", `${row.targetId}/${row.wireModelId}`), 2, width));
44
+ const weekly = routeWeeklyQuota(row, quota);
45
+ if (weekly)
46
+ lines.push(...previewRows(field("Account", `Shared ${weekly.account} · ${weekly.label}`), compact ? 1 : 2, width));
37
47
  if (row.taskSummary)
38
48
  lines.push(...(compact ? previewRows(field("Task", row.taskSummary), 2, width) : field("Task", row.taskSummary)));
39
49
  const activity = renderDispatchActivity(row, width);
@@ -107,8 +117,8 @@ function activityPage(state: FooterDashboardRenderState, width: number, budget:
107
117
  if (first && second) {
108
118
  cards.push(
109
119
  ...zipColumns(
110
- agentCard(first, col, true),
111
- agentCard(second, width - col - 5, true),
120
+ agentCard(first, col, true, state.quota),
121
+ agentCard(second, width - col - 5, true, state.quota),
112
122
  col,
113
123
  width - col - 5,
114
124
  ` ${theme.fg("frame", "│")} `,
@@ -119,7 +129,7 @@ function activityPage(state: FooterDashboardRenderState, width: number, budget:
119
129
  }
120
130
  } else
121
131
  for (const row of active) {
122
- const card = agentCard(row, width, active.length > 1 || cardBudget < 18);
132
+ const card = agentCard(row, width, active.length > 1 || cardBudget < 18, state.quota);
123
133
  if (cards.length + card.length + 2 > cardBudget && shown > 0) break;
124
134
  cards.push(...card, "");
125
135
  shown++;
@@ -183,6 +193,7 @@ function contextPage(state: FooterDashboardRenderState, width: number): string[]
183
193
  width,
184
194
  ),
185
195
  ];
196
+ out.push(theme.fg("dim", "Context occupancy is per request; subscription limits are account-wide · /usage"));
186
197
  const gridWidth = Math.max(12, Math.min(64, width >= 76 ? Math.floor(width * 0.38) : width));
187
198
  const gridHeight = width >= 76 ? 8 : 3;
188
199
  const grid = renderContextMeterGrid(ledger, gridWidth, gridHeight, theme);
@@ -248,7 +259,7 @@ function contextPage(state: FooterDashboardRenderState, width: number): string[]
248
259
  return out;
249
260
  }
250
261
 
251
- /** Two-line ambient strip. Notices borrow workspace space until dismissed/expired. */
262
+ /** Model and context above; persistent workspace and a rotating hint area below. */
252
263
  export function renderCompactDashboard(state: FooterDashboardRenderState, width: number): string[] {
253
264
  const theme = clioTheme();
254
265
  const w = Math.max(1, width);
@@ -262,8 +273,21 @@ export function renderCompactDashboard(state: FooterDashboardRenderState, width:
262
273
  const usage = `${formatFooterTokens(ledger?.usedTokens ?? state.context.used ?? 0)} / ${(ledger?.contextWindow ?? state.context.contextWindow) ? formatFooterTokens(ledger?.contextWindow ?? state.context.contextWindow ?? 0) : "?"}`;
263
274
  const context = `${ledger ? renderContextMeterBar(ledger, w >= 100 ? 14 : 8, theme) : ""} ${usage}`;
264
275
  const rightWidth = Math.min(Math.floor(w * 0.48), visibleWidth(context));
265
- const identityRoom = Math.max(8, w - rightWidth - visibleWidth(thinking) - visibleWidth(activity) - 10);
266
- const left = `${activity} · ${theme.fg("muted", fitIdentityLabel(identity, identityRoom))} · ${thinking}`;
276
+
277
+ const weekly = state.quotaRoute ? routeWeeklyQuota(state.quotaRoute, state.quota ?? []) : null;
278
+ const leftRoom = Math.max(1, w - rightWidth - 3);
279
+ const activityLabel = fit(activity, Math.min(visibleWidth(activity), Math.max(5, Math.floor(leftRoom / 3))));
280
+ const badge =
281
+ weekly && leftRoom - visibleWidth(activityLabel) - visibleWidth(weekly.label) >= 16
282
+ ? theme.fg(
283
+ weekly.severity === "critical" ? "error" : weekly.severity === "normal" ? "muted" : "warning",
284
+ weekly.label,
285
+ )
286
+ : "";
287
+ const baseRoom = leftRoom - visibleWidth(activityLabel) - (badge ? visibleWidth(badge) + 3 : 0) - 5;
288
+ const showThinking = baseRoom - visibleWidth(thinking) - 3 >= 12;
289
+ const identityRoom = Math.max(1, baseRoom - (showThinking ? visibleWidth(thinking) + 3 : 0));
290
+ const left = `${activityLabel} · ${theme.fg("muted", fitIdentityLabel(identity, identityRoom))}${badge ? ` · ${badge}` : ""}${showThinking ? ` · ${thinking}` : ""}`;
267
291
  const pair = (l: string, r: string, rw: number) => `${fit(l, w - rw - 3)} ${fit(r, rw)}`;
268
292
  const notice = [...state.notices]
269
293
  .filter((n) => n.expiresAt === null || n.expiresAt > state.now)
@@ -283,21 +307,31 @@ export function renderCompactDashboard(state: FooterDashboardRenderState, width:
283
307
  )
284
308
  : state.demoHint
285
309
  ? `${theme.fg("accent", "Tip")} ${theme.fg("muted", clean(state.demoHint))}`
286
- : theme.fg(
287
- "dim",
288
- footerKeyHint(state.now, w < 80) ??
289
- `${clean(state.workspace.cwd)} · ${clean(state.workspace.branch ?? "no Git branch")}${state.workspace.dirty ? " *" : ""}`,
290
- );
291
- const hint = theme.fg("muted", `${state.session.throughput ? `${state.session.throughput} · ` : ""}${key}`);
292
- return [
293
- fit(pair(left, context, rightWidth)),
294
- fit(pair(foot, hint, Math.min(Math.floor(w * 0.4), visibleWidth(hint)))),
295
- ];
310
+ : theme.fg("dim", footerKeyHint(state.now, w < 120) ?? `${key} Dashboard`);
311
+ const hintWidth = Math.min(Math.floor(w * 0.48), visibleWidth(foot));
312
+ const workspaceWidth = Math.max(1, w - hintWidth - 3);
313
+ const git = `${clean(state.workspace.branch ?? "no Git branch")}${state.workspace.dirty ? " *" : ""}`;
314
+ const gitWidth = Math.min(visibleWidth(git), Math.max(4, Math.floor(workspaceWidth * 0.45)));
315
+ const cwdWidth = Math.max(1, workspaceWidth - gitWidth - 3);
316
+ const workspace = theme.fg(
317
+ "dim",
318
+ `${fitIdentityLabel(clean(state.workspace.cwd), cwdWidth)} · ${fitIdentityLabel(git, gitWidth)}`,
319
+ );
320
+ return [fit(pair(left, context, rightWidth)), fit(pair(workspace, foot, hintWidth))];
296
321
  }
297
322
 
298
323
  function statusPage(state: FooterDashboardRenderState, width: number): string[] {
299
324
  const theme = clioTheme();
300
325
 
326
+ const quotaRows = [
327
+ theme.style(
328
+ "accent",
329
+ `SESSION · ${formatFooterTokens(state.sessionTokens?.totalTokens ?? 0)} recorded tokens · ${formatCostAggregate(state.sessionCost) ?? "cost not yet priced"}`,
330
+ { bold: true },
331
+ ),
332
+ ...renderQuotaAccounts(state.quota ?? [], width, { compact: true, now: state.now }),
333
+ ];
334
+
301
335
  const active = state.dispatchRows.filter((row) => ACTIVE_AGENT_STATUSES.has(row.status));
302
336
  const completed = state.dispatchRows.filter((row) => row.status === "completed").length;
303
337
  const failed = state.dispatchRows.filter((row) => ["failed", "dead", "aborted"].includes(row.status)).length;
@@ -334,7 +368,6 @@ function statusPage(state: FooterDashboardRenderState, width: number): string[]
334
368
  ["Target", state.session.target ?? "not selected"],
335
369
  ["Tracked cost", formatCostAggregate(state.sessionCost) ?? "not yet priced"],
336
370
  ["Clio ceiling", state.costCeilingUsd === undefined ? "unknown" : `$${state.costCeilingUsd} · tracked pricing only`],
337
- ["Provider quota", "not reported by provider"],
338
371
  ["MCP connected", names(state.connections?.mcp)],
339
372
  ["Plugins active", names(state.connections?.plugins)],
340
373
  [
@@ -404,6 +437,7 @@ function statusPage(state: FooterDashboardRenderState, width: number): string[]
404
437
  ];
405
438
  if (width < 76)
406
439
  return [
440
+ ...quotaRows,
407
441
  ...section("COST & CONNECTIONS", left, width),
408
442
  ...section("LOCAL MACHINE", right, width),
409
443
  theme.fg("accent", "CONTEXT ENGINE"),
@@ -414,6 +448,7 @@ function statusPage(state: FooterDashboardRenderState, width: number): string[]
414
448
  ];
415
449
  const col = Math.floor((width - 5) / 2);
416
450
  return [
451
+ ...quotaRows,
417
452
  ...zipColumns(
418
453
  section("COST & CONNECTIONS", left, col),
419
454
  section("LOCAL MACHINE", right, width - col - 5),
@@ -457,7 +492,7 @@ export function renderDashboardPage(
457
492
  const hint = truncateToWidth(
458
493
  theme.fg(
459
494
  "muted",
460
- `${cycleKey || "Dashboard"} → ${next} · ${page === "Status" ? "/cost · /mcp · /library" : "composer stays active"}`,
495
+ `${cycleKey || "Dashboard"} → ${next} · ${page === "Status" ? "/usage · /mcp · /library" : "composer stays active"}`,
461
496
  ),
462
497
  safeWidth,
463
498
  "…",
@@ -475,7 +510,7 @@ export function renderDashboardPage(
475
510
  ? `${getKeybindings().getKeys("clio-coder.dispatchBoard.toggle").join("/") || "Fleet Runs"} · /view`
476
511
  : page === "Context"
477
512
  ? "/context"
478
- : "/cost · /context";
513
+ : "/usage · /context";
479
514
  content = [
480
515
  ...content.slice(0, available - 1),
481
516
  truncateToWidth(theme.fg("dim", `More detail: ${detail}`), safeWidth, "…", true),
@@ -30,7 +30,7 @@ export function formatFooterTokens(n: number): string {
30
30
  * Build the token-counter footer segment. Returns `null` when no usage has
31
31
  * landed yet so the footer stays uncluttered at session start. Cache-read
32
32
  * tokens are omitted here to keep the line scannable; reasoning tokens are
33
- * shown only when the provider exposes them. The `/cost` overlay exposes the
33
+ * shown only when the provider exposes them. The `/usage` overlay exposes the
34
34
  * full breakdown.
35
35
  */
36
36
  export function tokensSegment(usage: UsageBreakdown | null | undefined): string | null {
@@ -840,7 +840,7 @@ export async function createInteractiveApplication(deps: InteractiveDeps): Promi
840
840
  expandSubmit: (text) => expandInteractiveSubmitAsync(text, deps.resources),
841
841
  openAskUser: (questions, options) => openAskUserOverlayState(questions, options),
842
842
  openSkillsHub: (request) => openSkillsHubState(request),
843
- openCost: () => openCostOverlayState(),
843
+ openUsage: () => openUsageOverlayState(),
844
844
  openSideQuestion: (question) => openSideQuestionOverlayState(question),
845
845
  startHandoff: (goal) => startHandoffState(goal),
846
846
  startFleetRun: (name, vars) => startFleetRunState(name, vars),
@@ -901,6 +901,7 @@ export async function createInteractiveApplication(deps: InteractiveDeps): Promi
901
901
  editor.onSubmit = editorSubmit.submitEditorText;
902
902
 
903
903
  const interactiveTickers = createInteractiveTickers({
904
+ getQuotaSnapshots: presentation.getQuotaSnapshots,
904
905
  tui,
905
906
  dispatchBoardStore,
906
907
  contextActivityStore,
@@ -937,6 +938,8 @@ export async function createInteractiveApplication(deps: InteractiveDeps): Promi
937
938
  resetTranscript();
938
939
  };
939
940
  overlayLifecycle = createOverlayLifecycle({
941
+ getQuotaSnapshots: presentation.getQuotaSnapshots,
942
+ getDispatchRows: () => dispatchBoardStore.rows(),
940
943
  app: deps,
941
944
  tui,
942
945
  footer,
@@ -972,7 +975,7 @@ export async function createInteractiveApplication(deps: InteractiveDeps): Promi
972
975
  const {
973
976
  closeOverlay,
974
977
  openAskUserOverlayState,
975
- openCostOverlayState,
978
+ openUsageOverlayState,
976
979
  openSideQuestionOverlayState,
977
980
  startHandoffState,
978
981
  startFleetRunState,
@@ -326,7 +326,7 @@ export function createInteractiveInputRuntime(deps: InteractiveInputRuntimeDeps)
326
326
  },
327
327
  cancelFocusedOwner: () => {
328
328
  const state = deps.overlay.getState();
329
- if (["permission-confirm", "cost", "context-view", "side-question", "dispatch-board", "auth"].includes(state))
329
+ if (["permission-confirm", "usage", "context-view", "side-question", "dispatch-board", "auth"].includes(state))
330
330
  deps.overlay.closeOverlay();
331
331
  else if (focused()?.handleInput) {
332
332
  focused()?.handleInput?.("\x1b");
@@ -531,7 +531,7 @@ function overlayScopeLabel(state: OverlayState): string {
531
531
  "permission-confirm": "Permission",
532
532
  "dispatch-board": "Workers",
533
533
  auth: "Authentication",
534
- cost: "Usage",
534
+ usage: "Usage",
535
535
  "context-view": "Context",
536
536
  "context-reset": "Context reset",
537
537
  tasks: "Tasks",
@@ -13,6 +13,8 @@ import type {
13
13
  TokenThroughputSnapshot,
14
14
  } from "../domains/observability/index.js";
15
15
  import { type ProvidersContract, resolveModelRuntimeCapabilitiesForProviders } from "../domains/providers/index.js";
16
+ import { createQuotaSummaryFeed } from "../domains/quota/summary-feed.js";
17
+ import type { UsageSnapshot } from "../domains/quota/types.js";
16
18
  import type { ResourcesContract } from "../domains/resources/index.js";
17
19
  import type { LocalCapacity } from "../domains/scheduling/local-capacity.js";
18
20
  import { ceilChars, contentChars } from "../domains/session/context-accounting.js";
@@ -160,6 +162,7 @@ export interface InteractivePresentation {
160
162
  changeOutputStyle(mutation: () => void): void;
161
163
  setLocalBashRunning(running: boolean): void;
162
164
  getObservabilitySnapshot(): ObservabilitySnapshot;
165
+ getQuotaSnapshots(): ReadonlyArray<UsageSnapshot>;
163
166
  /** Fold one raw chat event into the ephemeral throughput shown only while this turn is active. */
164
167
  recordChatEvent(event: ChatLoopEvent): void;
165
168
  recordToolStart(toolCallId: string, toolName: string): void;
@@ -234,6 +237,12 @@ export function createInteractivePresentation(deps: InteractivePresentationDeps)
234
237
  return key === undefined || key.length === 0 ? null : formatKeyLabel(key, "");
235
238
  };
236
239
 
240
+ const quotaSummary = createQuotaSummaryFeed({
241
+ onUpdate: () => {
242
+ footer?.refresh();
243
+ requestRender();
244
+ },
245
+ });
237
246
  const banner = factories.createBanner({
238
247
  providers: deps.providers,
239
248
  ...(deps.agents ? { getAgentCount: () => deps.agents?.listSpecs().filter(isUserVisibleAgent).length ?? 0 } : {}),
@@ -248,6 +257,9 @@ export function createInteractivePresentation(deps: InteractivePresentationDeps)
248
257
  return key ? formatKeyLabel(key, "") : null;
249
258
  },
250
259
  onFactsRefreshed: requestRender,
260
+ // Subscription usage is read lazily off the render path and cached, so the
261
+ // banner reads a string and never awaits a provider.
262
+ getQuotaSummary: () => quotaSummary.peek(),
251
263
  ...(deps.getSettings ? { getSettings: deps.getSettings } : {}),
252
264
  });
253
265
  // A boot-time `--continue`/`--session` resume opens onto an existing
@@ -391,6 +403,7 @@ export function createInteractivePresentation(deps: InteractivePresentationDeps)
391
403
  getTokenThroughput: () =>
392
404
  liveThroughput === null ? observabilitySnapshot.session.latestThroughput : currentLiveThroughput(),
393
405
  getSessionCost: () => observabilitySnapshot.session.cost,
406
+ getQuotaSnapshots: () => quotaSummary.peekSnapshots(),
394
407
  getContextUsage: () => deps.chat.contextUsage(),
395
408
  getContextLedger: () => deps.chat.contextLedger(),
396
409
  getDispatchRows: () => dispatchBoardStore.rows(),
@@ -622,6 +635,7 @@ export function createInteractivePresentation(deps: InteractivePresentationDeps)
622
635
  beforeStatusDisposed = true;
623
636
  // Before the subscriptions go: a project-context refresh already in flight
624
637
  // must not land afterwards and ask a torn-down presentation for a frame.
638
+ quotaSummary.dispose();
625
639
  banner.dispose();
626
640
  footer.dispose();
627
641
  unsubscribeObservability();
@@ -664,6 +678,7 @@ export function createInteractivePresentation(deps: InteractivePresentationDeps)
664
678
  changeOutputStyle: (mutation) =>
665
679
  chatRenderer.mutate(() => preserveTranscriptScroll(transcriptView, deps.terminal.columns, mutation), "output-style"),
666
680
  getObservabilitySnapshot: () => observabilitySnapshot,
681
+ getQuotaSnapshots: () => quotaSummary.peekSnapshots(),
667
682
  recordChatEvent,
668
683
  recordToolStart: (toolCallId, toolName) => {
669
684
  footerActiveTools.add(toolCallId);
@@ -127,7 +127,7 @@ export interface InteractiveSlashRuntimeDeps {
127
127
  expandSubmit: (text: string) => Promise<InteractiveSlashSubmitExpansion>;
128
128
  openAskUser: AskUserHandler;
129
129
  openSkillsHub: (request?: LibraryBrowseRequest) => void;
130
- openCost: () => void;
130
+ openUsage: () => void;
131
131
  /** Run one `/btw` side question in its own overlay. */
132
132
  openSideQuestion: (question: string) => void;
133
133
  /** Run one `/handoff <goal>`: extract, review, and seed a successor session. */
@@ -461,7 +461,7 @@ export function createInteractiveSlashRuntime(deps: InteractiveSlashRuntimeDeps)
461
461
  ? deps.share.planImport(resolve(archivePath), importOptions)
462
462
  : deps.share.importArchive(resolve(archivePath), importOptions);
463
463
  },
464
- openCost: deps.openCost,
464
+ openUsage: deps.openUsage,
465
465
  openSideQuestion: deps.openSideQuestion,
466
466
  // `/oracle` is refused, never queued, while a turn streams: the digest it
467
467
  // packs describes the record as it stands.
@@ -1,3 +1,4 @@
1
+ import type { UsageSnapshot } from "../domains/quota/types.js";
1
2
  import { type TaskBoardSnapshot, type TaskBoardStore, taskBoardCounts } from "../domains/session/task-board.js";
2
3
  import { Text, type TUI, visibleWidth, wrapTextWithAnsi } from "../engine/tui.js";
3
4
  import {
@@ -28,6 +29,7 @@ export interface InteractiveTickersDeps {
28
29
  dispatchBoardStore: InteractiveDispatchStore;
29
30
  contextActivityStore: InteractiveContextActivityStore;
30
31
  getOverlayState: () => string;
32
+ getQuotaSnapshots?: () => ReadonlyArray<UsageSnapshot>;
31
33
  isFooterExpanded: () => boolean;
32
34
  /** Bound to TaskBoardStore.cachedSnapshot; repaint must never fold the session ledger. */
33
35
  getTaskBoard?: TaskBoardStore["cachedSnapshot"];
@@ -126,7 +128,11 @@ export function createInteractiveTickers(deps: InteractiveTickersDeps): Interact
126
128
  taskIslandHidden = hidden;
127
129
  if (rows.length > 0)
128
130
  taskIsland.setText(
129
- formatTaskIslandLines(rows, Math.max(1, Math.min(4, Math.floor((taskIslandHeight - 8) / 9)))).join("\n"),
131
+ formatTaskIslandLines(
132
+ rows,
133
+ Math.max(1, Math.min(4, Math.floor((taskIslandHeight - 8) / 10))),
134
+ deps.getQuotaSnapshots?.() ?? [],
135
+ ).join("\n"),
130
136
  );
131
137
  else if (board) taskIsland.setText(formatTaskBoardIslandLines(board).join("\n"));
132
138
  taskIsland.invalidate();