@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
@@ -23,7 +23,6 @@ import { formatUserTaskHandoff } from "../domains/user-tasks/handoff.js";
23
23
  import type { UserTasksStore } from "../domains/user-tasks/store.js";
24
24
  import type { TUI } from "../engine/tui.js";
25
25
  import { type OpenContextOverlayOptions, openContextOverlay } from "./context-overlay.js";
26
- import { openCostOverlay } from "./cost-overlay.js";
27
26
  import {
28
27
  type createDispatchBoardView,
29
28
  isDispatchBoardRowCancellable,
@@ -43,6 +42,7 @@ import { openFleetRunApprovalOverlay } from "./overlays/fleet-run-approval.js";
43
42
  import { openSideQuestionOverlay } from "./overlays/side-question.js";
44
43
  import type { ContextClearCommandOptions } from "./slash-commands.js";
45
44
  import { openTasksOverlay } from "./tasks-overlay.js";
45
+ import { openUsageOverlay } from "./usage-overlay.js";
46
46
  import { type ArtifactProviderDeps, createDefaultArtifactProviders } from "./view/artifacts.js";
47
47
  import { openViewOverlay } from "./view/view-overlay.js";
48
48
 
@@ -51,6 +51,8 @@ export interface OverlayGeneralOpenersDeps {
51
51
  tui: TUI;
52
52
  transitions: OverlayTransitions;
53
53
  observability: ObservabilityContract;
54
+ getQuotaSnapshots?: () => ReadonlyArray<import("../domains/quota/types.js").UsageSnapshot>;
55
+ getDispatchRows?: () => ReadonlyArray<import("./dispatch-board.js").DispatchBoardRow>;
54
56
  getSessionId?: () => string | null;
55
57
  getContextLedger: () => ContextLedger;
56
58
  contextChat: NonNullable<OpenContextOverlayOptions["chat"]>;
@@ -80,7 +82,7 @@ export interface OverlayGeneralOpenersDeps {
80
82
  startDispatchBoardTicker: () => void;
81
83
  closeOverlay: () => void;
82
84
  showOverlayFrame?: typeof showClioOverlayFrame;
83
- openCostOverlay?: typeof openCostOverlay;
85
+ openUsageOverlay?: typeof openUsageOverlay;
84
86
  openContextOverlay?: typeof openContextOverlay;
85
87
  openContextResetOverlay?: typeof openContextResetOverlay;
86
88
  openTasksOverlay?: typeof openTasksOverlay;
@@ -121,7 +123,7 @@ export interface OverlayGeneralOpenersDeps {
121
123
  }
122
124
 
123
125
  export interface OverlayGeneralOpeners {
124
- openCost(): void;
126
+ openUsage(): void;
125
127
  openContextView(): void;
126
128
  openContextReset(): void;
127
129
  toggleFooter(): void;
@@ -136,7 +138,7 @@ export interface OverlayGeneralOpeners {
136
138
  }
137
139
 
138
140
  export function createOverlayGeneralOpeners(deps: OverlayGeneralOpenersDeps): OverlayGeneralOpeners {
139
- const openCostOverlayFactory = deps.openCostOverlay ?? openCostOverlay;
141
+ const openUsageOverlayFactory = deps.openUsageOverlay ?? openUsageOverlay;
140
142
  const openContextOverlayFactory = deps.openContextOverlay ?? openContextOverlay;
141
143
  const openContextResetOverlayFactory = deps.openContextResetOverlay ?? openContextResetOverlay;
142
144
  const openTasksOverlayFactory = deps.openTasksOverlay ?? openTasksOverlay;
@@ -146,11 +148,13 @@ export function createOverlayGeneralOpeners(deps: OverlayGeneralOpenersDeps): Ov
146
148
  const showOverlayFrameFactory = deps.showOverlayFrame ?? showClioOverlayFrame;
147
149
  const openSideQuestionOverlayFactory = deps.openSideQuestionOverlay ?? openSideQuestionOverlay;
148
150
 
149
- const openCost = (): void => {
151
+ const openUsage = (): void => {
150
152
  if (deps.transitions.state !== "closed") return;
151
- deps.transitions.state = "cost";
152
- deps.transitions.handle = openCostOverlayFactory(deps.tui, deps.observability, {
153
+ deps.transitions.state = "usage";
154
+ deps.transitions.handle = openUsageOverlayFactory(deps.tui, deps.observability, {
153
155
  sessionId: deps.getSessionId?.() ?? null,
156
+ ...(deps.getQuotaSnapshots ? { getQuotaSnapshots: deps.getQuotaSnapshots } : {}),
157
+ ...(deps.getDispatchRows ? { getDispatchRows: deps.getDispatchRows } : {}),
154
158
  ...(deps.readSessionEntries
155
159
  ? {
156
160
  getSessionEntries: () => deps.readSessionEntries?.() ?? [],
@@ -525,7 +529,7 @@ export function createOverlayGeneralOpeners(deps: OverlayGeneralOpenersDeps): Ov
525
529
  };
526
530
 
527
531
  return {
528
- openCost,
532
+ openUsage,
529
533
  openContextView,
530
534
  openContextReset,
531
535
  toggleFooter,
@@ -7,7 +7,7 @@ export type OverlayState =
7
7
  | "permission-confirm"
8
8
  | "dispatch-board"
9
9
  | "auth"
10
- | "cost"
10
+ | "usage"
11
11
  | "context-view"
12
12
  | "context-reset"
13
13
  | "tasks"
@@ -213,8 +213,8 @@ function routeAuthOverlayKey(data: string, deps: CloseOverlayKeyDeps): boolean {
213
213
  return false;
214
214
  }
215
215
 
216
- /** Pure overlay key router for the /cost overlay. Esc closes; everything else is swallowed. */
217
- function routeCostOverlayKey(data: string, deps: CloseOverlayKeyDeps): boolean {
216
+ /** Esc closes an inspection overlay; callers decide how to route its remaining keys. */
217
+ function routeReadOnlyOverlayKey(data: string, deps: CloseOverlayKeyDeps): boolean {
218
218
  if (isEscapeKey(data)) {
219
219
  deps.closeOverlay();
220
220
  return true;
@@ -262,19 +262,16 @@ export function routeOverlayKey(
262
262
  return true;
263
263
  }
264
264
  if (overlayState === "auth") return routeAuthOverlayKey(data, deps);
265
- if (overlayState === "cost") {
266
- routeCostOverlayKey(data, deps);
267
- return true;
268
- }
265
+ if (overlayState === "usage") return routeReadOnlyOverlayKey(data, deps);
269
266
  if (overlayState === "context-view") {
270
- routeCostOverlayKey(data, deps);
267
+ routeReadOnlyOverlayKey(data, deps);
271
268
  return true;
272
269
  }
273
270
  // Esc is the only key the side-question overlay answers: it aborts a round
274
271
  // that is still streaming and closes one that has settled. Everything else is
275
272
  // swallowed so a keystroke cannot reach the composer behind it.
276
273
  if (overlayState === "side-question") {
277
- routeCostOverlayKey(data, deps);
274
+ routeReadOnlyOverlayKey(data, deps);
278
275
  return true;
279
276
  }
280
277
  // The handoff review overlay owns Enter, `e`, the arrows, and Esc itself, so
@@ -75,6 +75,8 @@ function interopProposalsFor(
75
75
 
76
76
  export interface OverlayLifecycleRuntimeDeps {
77
77
  app: OverlayLifecycleApplicationDeps;
78
+ getQuotaSnapshots?: () => ReadonlyArray<import("../domains/quota/types.js").UsageSnapshot>;
79
+ getDispatchRows?: () => ReadonlyArray<import("./dispatch-board.js").DispatchBoardRow>;
78
80
  tui: import("../engine/tui.js").TUI;
79
81
  footer: import("./footer/dashboard.js").FooterDashboardPanel;
80
82
  interactiveTickers: import("./interactive-tickers.js").InteractiveTickers;
@@ -117,7 +119,7 @@ export interface OverlayLifecycleRuntimeDeps {
117
119
  openTreeOverlay?: typeof import("./overlays/tree-selector.js").openTreeOverlay;
118
120
  openMessagePickerOverlay?: typeof import("./overlays/message-picker.js").openMessagePickerOverlay;
119
121
  openCwdFallbackOverlay?: typeof import("./overlays/cwd-fallback.js").openCwdFallbackOverlay;
120
- openCostOverlay?: typeof import("./cost-overlay.js").openCostOverlay;
122
+ openUsageOverlay?: typeof import("./usage-overlay.js").openUsageOverlay;
121
123
  openContextOverlay?: typeof import("./context-overlay.js").openContextOverlay;
122
124
  openContextResetOverlay?: typeof import("./overlays/context-reset.js").openContextResetOverlay;
123
125
  openTasksOverlay?: typeof import("./tasks-overlay.js").openTasksOverlay;
@@ -140,7 +142,7 @@ export interface OverlayLifecycleController {
140
142
  isAskUserWaiting(): boolean;
141
143
  resetAskUserCancellation(): void;
142
144
  refreshSettingsOverlay(): void;
143
- openCostOverlayState(): void;
145
+ openUsageOverlayState(): void;
144
146
  openContextViewOverlayState(): void;
145
147
  openContextResetOverlayState(): void;
146
148
  toggleFooterDashboardState(): void;
@@ -207,7 +209,7 @@ export function createOverlayLifecycle(deps: OverlayLifecycleRuntimeDeps): Overl
207
209
  openTreeOverlay: openTreeOverlayFactory,
208
210
  openMessagePickerOverlay: openMessagePickerOverlayFactory,
209
211
  openCwdFallbackOverlay: openCwdFallbackOverlayFactory,
210
- openCostOverlay: openCostOverlayFactory,
212
+ openUsageOverlay: openUsageOverlayFactory,
211
213
  openContextOverlay: openContextOverlayFactory,
212
214
  openContextResetOverlay: openContextResetOverlayFactory,
213
215
  openTasksOverlay: openTasksOverlayFactory,
@@ -417,6 +419,8 @@ export function createOverlayLifecycle(deps: OverlayLifecycleRuntimeDeps): Overl
417
419
  tui,
418
420
  transitions: overlayTransitions,
419
421
  observability: deps.app.observability,
422
+ ...(deps.getQuotaSnapshots ? { getQuotaSnapshots: deps.getQuotaSnapshots } : {}),
423
+ ...(deps.getDispatchRows ? { getDispatchRows: deps.getDispatchRows } : {}),
420
424
  ...(deps.app.getSessionId ? { getSessionId: deps.app.getSessionId } : {}),
421
425
  getContextLedger: () => deps.app.chat.contextLedger(),
422
426
  contextChat: deps.app.chat,
@@ -445,7 +449,7 @@ export function createOverlayLifecycle(deps: OverlayLifecycleRuntimeDeps): Overl
445
449
  startDispatchBoardTicker: () => interactiveTickers.startDispatchBoardTicker(),
446
450
  closeOverlay,
447
451
  showOverlayFrame,
448
- ...(openCostOverlayFactory ? { openCostOverlay: openCostOverlayFactory } : {}),
452
+ ...(openUsageOverlayFactory ? { openUsageOverlay: openUsageOverlayFactory } : {}),
449
453
  ...(openContextOverlayFactory ? { openContextOverlay: openContextOverlayFactory } : {}),
450
454
  ...(openContextResetOverlayFactory ? { openContextResetOverlay: openContextResetOverlayFactory } : {}),
451
455
  ...(openTasksOverlayFactory ? { openTasksOverlay: openTasksOverlayFactory } : {}),
@@ -462,7 +466,7 @@ export function createOverlayLifecycle(deps: OverlayLifecycleRuntimeDeps): Overl
462
466
  const openResumeOverlayState = overlaySessions.openResume;
463
467
  const openTreeOverlayState = overlaySessions.openTree;
464
468
  const openMessagePickerOverlayState = overlaySessions.openMessagePicker;
465
- const openCostOverlayState = overlayGeneralOpeners.openCost;
469
+ const openUsageOverlayState = overlayGeneralOpeners.openUsage;
466
470
  const openContextViewOverlayState = overlayGeneralOpeners.openContextView;
467
471
  const openContextResetOverlayState = overlayGeneralOpeners.openContextReset;
468
472
  const toggleFooterDashboardState = overlayGeneralOpeners.toggleFooter;
@@ -484,7 +488,7 @@ export function createOverlayLifecycle(deps: OverlayLifecycleRuntimeDeps): Overl
484
488
  isAskUserWaiting: overlayAskUser.isWaiting,
485
489
  resetAskUserCancellation: overlayAskUser.resetCancellation,
486
490
  refreshSettingsOverlay: overlayModelSelectors.refreshSettingsOverlay,
487
- openCostOverlayState,
491
+ openUsageOverlayState,
488
492
  openContextViewOverlayState,
489
493
  openContextResetOverlayState,
490
494
  toggleFooterDashboardState,
@@ -92,7 +92,7 @@ export interface OverlaySessionLifecycle {
92
92
 
93
93
  /**
94
94
  * The target and model a session started under, so reseeded calls land in the
95
- * same `/cost` bucket the live path writes to. Without it a single target read
95
+ * same `/usage` bucket the live path writes to. Without it a single target read
96
96
  * as two providers, one under the target id and one under the runtime name.
97
97
  */
98
98
  function sessionUsageDefaults(session: SessionContract): { target?: string | null; model?: string | null } {
@@ -120,7 +120,7 @@ function restorePriorSessionOrReopen(
120
120
 
121
121
  export function createOverlaySessionLifecycle(deps: OverlaySessionLifecycleDeps): OverlaySessionLifecycle {
122
122
  /**
123
- * Point every running number the footer and `/cost` show at one branch. The
123
+ * Point every running number the footer and `/usage` show at one branch. The
124
124
  * session total and the last-turn line are read off the same lineage in the
125
125
  * same call, so a switch cannot move one and leave the other behind.
126
126
  */
@@ -244,7 +244,7 @@ export function createOverlaySessionLifecycle(deps: OverlaySessionLifecycleDeps)
244
244
  const replayMessages = buildModelReplayAgentMessagesFromTurns(turns, { uptoTurnId: turnId });
245
245
  deps.chat.resetForSession(turnId, replayMessages);
246
246
  // The same branch the transcript above was just scoped to. Without the
247
- // leaf, /cost, the footer Σ, and the last-turn line kept reporting the
247
+ // leaf, /usage, the footer Σ, and the last-turn line kept reporting the
248
248
  // abandoned turns.
249
249
  rescopeToBranch(session, turns, turnId);
250
250
  } catch (error) {
@@ -0,0 +1,229 @@
1
+ import { costAggregateForAmount, formatCostAggregate } from "../domains/observability/index.js";
2
+ import {
3
+ foldDuplicateAccounts,
4
+ formatPct,
5
+ primaryWindow,
6
+ type QuotaSeverity,
7
+ severityForPct,
8
+ windowSeverity,
9
+ } from "../domains/quota/presentation.js";
10
+ import type { UsageSnapshot } from "../domains/quota/types.js";
11
+ import { sanitizeCallTargetText } from "../domains/safety/call-target.js";
12
+ import { redactSecretString } from "../domains/safety/redaction.js";
13
+ import { truncateToWidth, visibleWidth, wrapTextWithAnsi } from "../engine/tui.js";
14
+ import type { DispatchBoardRow } from "./dispatch-board.js";
15
+ import { formatFooterTokens } from "./footer-panel.js";
16
+ import { type ClioToken, clioTheme, formatCompactMs } from "./theme/index.js";
17
+
18
+ const clean = (text: string) => sanitizeCallTargetText(redactSecretString(text));
19
+ const tone = (severity: QuotaSeverity): ClioToken =>
20
+ severity === "critical" ? "error" : severity === "normal" ? "accent" : "warning";
21
+
22
+ /** Filled cells always mean consumed usage, on every surface. */
23
+ export function quotaMeter(usedPct: number, cells: number, severity: QuotaSeverity = severityForPct(usedPct)): string {
24
+ const count = Math.max(1, Math.floor(cells));
25
+ const used = Math.max(0, Math.min(100, Number.isFinite(usedPct) ? usedPct : 0));
26
+ const filled = Math.round((used / 100) * count);
27
+ const theme = clioTheme();
28
+ return theme.fg(tone(severity), "━".repeat(filled)) + theme.fg("frame", "─".repeat(count - filled));
29
+ }
30
+
31
+ export function quotaResetLabel(
32
+ reset: string | null,
33
+ now = Date.now(),
34
+ timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone,
35
+ ): string {
36
+ const timestamp = reset === null ? NaN : Date.parse(reset);
37
+ if (!Number.isFinite(timestamp)) return "Reset time not reported";
38
+ const absolute = new Intl.DateTimeFormat("en-US", {
39
+ month: "short",
40
+ day: "numeric",
41
+ hour: "numeric",
42
+ minute: "2-digit",
43
+ timeZone,
44
+ }).format(timestamp);
45
+ const minutes = Math.max(0, Math.ceil((timestamp - now) / 60_000));
46
+ const duration =
47
+ minutes >= 1440
48
+ ? `${Math.floor(minutes / 1440)}d ${Math.floor((minutes % 1440) / 60)}h`
49
+ : minutes >= 60
50
+ ? `${Math.floor(minutes / 60)}h ${minutes % 60}m`
51
+ : `${minutes}m`;
52
+ return minutes === 0
53
+ ? `Reset time passed · awaiting provider update (${absolute} · ${timeZone})`
54
+ : `Resets in ${duration} · ${absolute} (${timeZone})`;
55
+ }
56
+
57
+ /** Account-wide facts only: never infer a worker's share from changing percentages. */
58
+ export function renderQuotaAccounts(
59
+ snapshots: ReadonlyArray<UsageSnapshot>,
60
+ width: number,
61
+ options: { compact?: boolean; now?: number; timeZone?: string } = {},
62
+ ): string[] {
63
+ const theme = clioTheme();
64
+ const now = options.now ?? Date.now();
65
+ const accounts = foldDuplicateAccounts(snapshots).filter((account) => account.status !== "no_credentials");
66
+ if (!accounts.length) return [theme.fg("dim", "Subscriptions · loading")];
67
+ const out: string[] = [];
68
+ for (const account of accounts) {
69
+ const title = clean(`${account.displayName}${account.plan ? ` (${account.plan})` : ""}`);
70
+ const age =
71
+ account.fetchedAt === null ? null : Math.max(0, Math.floor((now - Date.parse(account.fetchedAt)) / 60_000));
72
+ const freshness = account.stale
73
+ ? "STALE · last good reading"
74
+ : age === null || !Number.isFinite(age)
75
+ ? ""
76
+ : age === 0
77
+ ? "updated just now"
78
+ : `updated ${age}m ago`;
79
+ if (options.compact) {
80
+ const window = primaryWindow(account);
81
+ const ordered =
82
+ window && ["warning", "critical"].includes(windowSeverity(window))
83
+ ? [window, ...account.windows.filter((item) => item !== window)]
84
+ : account.windows;
85
+ const windows = ordered
86
+ .map(
87
+ (item) =>
88
+ `${item.scope && item.scope !== item.label ? `${item.scope} ` : ""}${item.label} ${formatPct(item.usedPct)} used`,
89
+ )
90
+ .join(" · ");
91
+ const detail =
92
+ account.status !== "ok"
93
+ ? (account.message ?? account.status)
94
+ : account.providerId === "local"
95
+ ? "$0.00 · no subscription window consumed"
96
+ : windows || account.credits?.display || "no windows reported";
97
+ const meter = window && width >= 65 ? `${quotaMeter(window.usedPct, 8, windowSeverity(window))} ` : "";
98
+ out.push(
99
+ truncateToWidth(
100
+ `${meter}${theme.fg(account.status !== "ok" || account.stale ? "warning" : "muted", `${account.stale ? "STALE · " : ""}${title}`)} ${clean(detail)}`,
101
+ width,
102
+ "…",
103
+ true,
104
+ ),
105
+ );
106
+ continue;
107
+ }
108
+ if (out.length) out.push("");
109
+ out.push(theme.style("accent", title, { bold: true }));
110
+ if (freshness) out.push(theme.fg(account.stale ? "warning" : "dim", freshness));
111
+ if (account.status !== "ok")
112
+ out.push(theme.fg("warning", clean(`${account.status} · ${account.message ?? "usage unavailable"}`)));
113
+ let scope: string | undefined;
114
+ for (const window of account.windows) {
115
+ if (window.scope && scope !== window.scope) {
116
+ scope = window.scope;
117
+ out.push(theme.style("muted", clean(scope), { bold: true }));
118
+ }
119
+ const label = `${window.key.startsWith("weekly_scoped.") ? "Weekly" : window.label} · ${formatPct(window.usedPct)} used · ${formatPct(Math.max(0, 100 - window.usedPct))} remaining${window.active ? " · active limit" : ""}`;
120
+ out.push(clean(label));
121
+ out.push(quotaMeter(window.usedPct, Math.max(1, Math.min(48, width)), windowSeverity(window)));
122
+ out.push(theme.fg("dim", quotaResetLabel(window.resetsAt, now, options.timeZone)));
123
+ }
124
+ if (account.credits) {
125
+ out.push(
126
+ clean(
127
+ `${account.providerId === "local" ? "Local inference" : "Usage credits"} · ${account.credits.display}${account.credits.usedPct === null ? "" : ` · ${formatPct(account.credits.usedPct)} used`}`,
128
+ ),
129
+ );
130
+ if (account.credits.usedPct !== null) out.push(quotaMeter(account.credits.usedPct, Math.min(48, width)));
131
+ }
132
+ if (account.status === "ok" && account.message)
133
+ out.push(theme.fg(account.stale ? "warning" : "dim", clean(account.message)));
134
+ if (account.status === "ok" && !account.windows.length && !account.credits && !account.message)
135
+ out.push(theme.fg("dim", "Connected · no usage windows reported"));
136
+ if (account.retryAfterSeconds != null) out.push(theme.fg("warning", `Retry after ${account.retryAfterSeconds}s`));
137
+ }
138
+ return out.flatMap((line) => (visibleWidth(line) > width ? wrapTextWithAnsi(line, Math.max(1, width)) : [line]));
139
+ }
140
+
141
+ /** The route must share the credential owner read by the quota adapter. */
142
+ function routeAccount(
143
+ route: Pick<DispatchBoardRow, "runtimeId" | "node">,
144
+ snapshots: ReadonlyArray<UsageSnapshot>,
145
+ ): UsageSnapshot | undefined {
146
+ if (route.node && route.node !== "local") return undefined;
147
+ const provider = (
148
+ {
149
+ "claude-code": "claude-code",
150
+ "claude-sdk": "claude-code",
151
+ "anthropic-max": "anthropic-max",
152
+ "antigravity-code": "antigravity",
153
+ } as Record<string, string>
154
+ )[route.runtimeId];
155
+ // Clio's openai-codex OAuth storage is independent of Codex CLI auth.json.
156
+ return snapshots.find((item) => item.providerId === provider);
157
+ }
158
+
159
+ /** A weekly badge only when both the account and model group are known. */
160
+ export function routeWeeklyQuota(
161
+ route: Pick<DispatchBoardRow, "runtimeId" | "wireModelId" | "node">,
162
+ snapshots: ReadonlyArray<UsageSnapshot>,
163
+ ): { label: string; account: string; severity: QuotaSeverity } | null {
164
+ const account = routeAccount(route, snapshots);
165
+ if (account?.status !== "ok") return null;
166
+ const weekly = account.windows.filter((window) => window.key === "weekly" || window.key.endsWith(".weekly"));
167
+ const model = route.wireModelId.toLowerCase();
168
+ const matches =
169
+ account.providerId === "antigravity"
170
+ ? weekly.filter((window) => {
171
+ const scope = window.scope?.toLowerCase() ?? "";
172
+ if (model.includes("gemini")) return scope.includes("gemini");
173
+ if (/claude|opus|sonnet|haiku/.test(model)) return scope.includes("claude");
174
+ if (model.includes("gpt")) return scope.includes("gpt");
175
+ return false;
176
+ })
177
+ : weekly.filter((window) => !window.scope);
178
+ if (matches.length !== 1) return null;
179
+ const window = matches[0];
180
+ if (!window) return null;
181
+ return {
182
+ label: `${account.stale ? "STALE · " : ""}weekly ${formatPct(Math.max(0, Math.min(100, 100 - window.usedPct)))} left`,
183
+ account: account.displayName,
184
+ severity: windowSeverity(window),
185
+ };
186
+ }
187
+
188
+ export function workerQuotaLabel(
189
+ row: Pick<DispatchBoardRow, "runtimeId" | "node">,
190
+ snapshots: ReadonlyArray<UsageSnapshot>,
191
+ ): string {
192
+ // Remote workers and generic transports may use different credentials. Only
193
+ // known local credential owners can be associated with this process's reads.
194
+ if (row.node && row.node !== "local") return "Account quota unavailable for remote credentials";
195
+ const account = routeAccount(row, snapshots);
196
+ if (!account) return "Account quota not linked to this runtime";
197
+ if (account.status !== "ok") return `${account.displayName} · ${account.message ?? account.status}`;
198
+ return `Shared ${account.displayName}${account.stale ? " · STALE" : ""} · ${account.windows.map((window) => `${window.scope && window.scope !== window.label ? `${window.scope} ` : ""}${window.label} ${formatPct(Math.max(0, 100 - window.usedPct))} left`).join(" · ") || "no windows reported"}`;
199
+ }
200
+
201
+ export function renderWorkerUsage(
202
+ rows: ReadonlyArray<DispatchBoardRow>,
203
+ snapshots: ReadonlyArray<UsageSnapshot>,
204
+ width: number,
205
+ ): string[] {
206
+ const theme = clioTheme();
207
+ const out = [theme.fg("dim", "Active and recent workers · recorded tokens and cost · account limits are shared.")];
208
+ if (!rows.length) out.push("", "No worker invocations recorded.");
209
+ for (const row of rows) {
210
+ const cost = formatCostAggregate(costAggregateForAmount(row.costUsd, row.costProvenance)) ?? "cost not reported";
211
+ out.push(
212
+ "",
213
+ theme.style("agent", clean(`${row.agentId} · ${row.status} · ${formatCompactMs(row.elapsedMs)}`), { bold: true }),
214
+ );
215
+ out.push(clean(`${row.targetId} / ${row.wireModelId}${row.node ? ` · ${row.node}` : ""}`));
216
+ out.push(
217
+ row.tokenCount > 0 || row.inputTokens > 0 || row.outputTokens > 0 || row.progress?.inputTokens !== undefined
218
+ ? `${formatFooterTokens(row.tokenCount)} recorded tokens · input ${formatFooterTokens(row.inputTokens)} · output ${formatFooterTokens(row.outputTokens)} · ${cost}`
219
+ : `Token usage not reported · ${cost}`,
220
+ );
221
+ if (row.lastContextTokens !== undefined)
222
+ out.push(
223
+ `Context ${formatFooterTokens(row.lastContextTokens)}${row.contextWindow ? ` / ${formatFooterTokens(row.contextWindow)}` : ""}`,
224
+ );
225
+ out.push(theme.fg("muted", clean(workerQuotaLabel(row, snapshots))));
226
+ out.push(theme.fg("dim", clean(`/view dispatch:${row.runId}`)));
227
+ }
228
+ return out.flatMap((line) => wrapTextWithAnsi(line, Math.max(1, width)));
229
+ }
@@ -4,7 +4,7 @@
4
4
  * The line is a live accumulator: it describes whatever turn this process last
5
5
  * ran. current.jsonl is append-only, so after a `/tree` switch that turn can sit
6
6
  * on a branch the reader just left, and the line kept describing it while the Σ
7
- * total and `/cost` beside it had already been rescoped by the usage reseed. One
7
+ * total and `/usage` beside it had already been rescoped by the usage reseed. One
8
8
  * footer, two branches, no way to tell which line belonged to which.
9
9
  *
10
10
  * The fold reads the same lineage the reseed does, through
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Rebuild the running usage totals from a session's own ledger.
3
3
  *
4
- * The `/cost` overlay and the footer both render one process-lifetime
4
+ * The `/usage` overlay and the footer both render one process-lifetime
5
5
  * accumulator, and only `startNewSession` ever reset it. Resuming did not, so a
6
6
  * resumed session showed the *previous* session's numbers under the resumed
7
7
  * session's id: one process, two sessions, byte-identical totals relabelled.
@@ -56,7 +56,7 @@ export interface SessionUsageSink {
56
56
  * `activeLeafTurnId` scopes the fold to one branch, through the same
57
57
  * `filterEntriesToActivePath` the transcript replays through. current.jsonl is
58
58
  * append-only, so after a `/tree` switch the abandoned sibling turns are still
59
- * in the file: the transcript stopped showing them and `/cost` and the footer Σ
59
+ * in the file: the transcript stopped showing them and `/usage` and the footer Σ
60
60
  * kept counting them, which put a session total on screen for turns the reader
61
61
  * had just been told were not on this branch. Omitted, the fold follows the
62
62
  * newest message's ancestry, the same fallback an offline read of the transcript
@@ -100,7 +100,7 @@ function positive(value: unknown): number {
100
100
  }
101
101
 
102
102
  /**
103
- * Normalize one provider usage object into the shape `/cost` and the
103
+ * Normalize one provider usage object into the shape `/usage` and the
104
104
  * out-of-turn usage store take. Shared with the session pre-warm, which is
105
105
  * another call billed beside the session rather than inside it.
106
106
  */
@@ -144,7 +144,7 @@ function textFromMessage(message: unknown): string {
144
144
 
145
145
  /**
146
146
  * Run the side-question round. Resolves with the answer, the provider usage
147
- * (which is still real money and is reported to `/cost`), and whether the
147
+ * (which is still real money and is reported to `/usage`), and whether the
148
148
  * operator aborted it. Provider failures reject; the caller renders the reason
149
149
  * in the overlay.
150
150
  */
@@ -183,7 +183,7 @@ type SlashCommandVariant =
183
183
  | { kind: "fleet-run-usage"; reason?: string }
184
184
  | { kind: "fleet" }
185
185
  | { kind: "agents"; connect?: boolean }
186
- | { kind: "cost" }
186
+ | { kind: "usage" }
187
187
  | { kind: "context-view" }
188
188
  | { kind: "tasks" }
189
189
  | { kind: "decisions" }
@@ -706,7 +706,7 @@ export interface SlashCommandContext {
706
706
  }>;
707
707
  exportShareArchive?: (outPath: string) => { fileCount: number; path: string };
708
708
  importShareArchive?: (path: string, options: { dryRun?: boolean; force?: boolean }) => ShareImportPlan;
709
- openCost: () => void;
709
+ openUsage: () => void;
710
710
  /**
711
711
  * `/doctor [deep]`: the CLI doctor's findings, rendered as one notice.
712
712
  * `deep` adds the live tool probe on the session's targets and the
@@ -1686,14 +1686,14 @@ export const BUILTIN_SLASH_COMMANDS: ReadonlyArray<BuiltinSlashCommand> = [
1686
1686
  },
1687
1687
  },
1688
1688
  {
1689
- name: "cost",
1690
- description: "Show session token and cost totals",
1689
+ name: "usage",
1690
+ description: "Show subscription quota, credits, and session token and cost totals",
1691
1691
  group: "Inspect",
1692
- kinds: ["cost"],
1692
+ kinds: ["usage"],
1693
1693
  args: {},
1694
- fromArgs: fromArgsOrUsage("cost", { kind: "cost" }),
1694
+ fromArgs: fromArgsOrUsage("usage", { kind: "usage" }),
1695
1695
  handle(_command, ctx) {
1696
- ctx.openCost();
1696
+ ctx.openUsage();
1697
1697
  },
1698
1698
  },
1699
1699
  {
@@ -2648,7 +2648,7 @@ const COMMAND_ORDER = [
2648
2648
  "memory",
2649
2649
  "view",
2650
2650
  "panes",
2651
- "cost",
2651
+ "usage",
2652
2652
  "doctor",
2653
2653
  "decisions",
2654
2654
  "library",
@@ -1083,7 +1083,7 @@ export function createTurnContext(deps: TurnContextDeps): TurnContext {
1083
1083
 
1084
1084
  // The checkpoint identifies the selected summary route; older results
1085
1085
  // fall back to the active chat route. The ledger carries usage for reseed; this is the
1086
- // live sink, so `/cost` and the footer move the moment /context compact
1086
+ // live sink, so `/usage` and the footer move the moment /context compact
1087
1087
  // returns instead of staying byte-identical to before it ran.
1088
1088
  recordCompactionUsage(agentRuntime, result);
1089
1089
 
@@ -72,7 +72,7 @@ export interface TurnPrewarmDeps {
72
72
  ) => Promise<{ ok: true; runtime: AgentRuntime; apiKey: string | undefined } | { ok: false; reason: string }>;
73
73
  /** Freeze the session tool surface onto the agent, exactly as a submit does. */
74
74
  applySessionTools: (runtime: AgentRuntime) => void;
75
- /** Report the round's provider usage under its own `/cost` label. */
75
+ /** Report the round's provider usage under its own `/usage` label. */
76
76
  recordUsage: (runtime: AgentRuntime, usage: Usage | null) => void;
77
77
  /**
78
78
  * Claim one in-flight request on the runtime's endpoint for the duration of