@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.
- package/CHANGELOG.md +11 -0
- package/README.md +12 -9
- package/dist/{acp-KS7ARGV6.js → acp-X3N7JDGS.js} +2 -2
- package/dist/{agents-YOGJ2YEJ.js → agents-UYGSMAWR.js} +3 -3
- package/dist/assets/codewiki.json +1 -1
- package/dist/{chunk-EOJPDNUP.js → chunk-3OODBJ45.js} +1 -1
- package/dist/{chunk-R7FGRZNF.js → chunk-4D7QM7S5.js} +2 -2
- package/dist/{chunk-X4CEFOSG.js → chunk-6B76U65H.js} +4 -4
- package/dist/{chunk-LTWH2ACR.js → chunk-7FHSA7VB.js} +5 -5
- package/dist/{chunk-GRHOXDKB.js → chunk-7KI4ZSEB.js} +2 -2
- package/dist/{chunk-LYJJ6546.js → chunk-7VNLPEUG.js} +6 -6
- package/dist/{chunk-D65E25JG.js → chunk-A7G7OUND.js} +2 -2
- package/dist/{chunk-3JLTRPN7.js → chunk-AKQ3N3L4.js} +2 -2
- package/dist/{chunk-AVSR2HKI.js → chunk-BIQLF3NZ.js} +3 -3
- package/dist/{chunk-D3KWFOUT.js → chunk-DGMY2DWE.js} +2 -2
- package/dist/{chunk-IN7DGBVS.js → chunk-FBXTHPNZ.js} +2 -2
- package/dist/{chunk-EX57VVWW.js → chunk-GGEEG7CD.js} +2 -2
- package/dist/{chunk-V35KZDUZ.js → chunk-GW2KVAVI.js} +2 -2
- package/dist/{chunk-SJGS3GDI.js → chunk-KIAUZGKF.js} +2 -2
- package/dist/{chunk-CBAQPTDU.js → chunk-LFYA3HYH.js} +2 -2
- package/dist/{chunk-4VDHLCF5.js → chunk-M5FPIZQW.js} +2 -2
- package/dist/{chunk-R2UUNSNQ.js → chunk-O3ZAF7UY.js} +18 -5
- package/dist/{chunk-QNUQ7K7D.js → chunk-OXWRE2QJ.js} +2 -2
- package/dist/{chunk-55RMDDFM.js → chunk-PGQG7ZMW.js} +2 -2
- package/dist/{chunk-HQVN7G4F.js → chunk-QM7HFPQQ.js} +4 -4
- package/dist/{chunk-ZR5YA6ZF.js → chunk-QTYRMBW6.js} +2 -2
- package/dist/{chunk-3LT34CAM.js → chunk-SAL2SBP4.js} +8 -8
- package/dist/{chunk-OJXOA6YU.js → chunk-TL6LNERH.js} +2 -2
- package/dist/{chunk-U6YTUVOX.js → chunk-VMTCZ2GR.js} +2 -2
- package/dist/{chunk-OQ2EOAHA.js → chunk-VWKR2NN5.js} +5 -5
- package/dist/{chunk-QDUUOZ3S.js → chunk-WAISUJNQ.js} +2 -2
- package/dist/{chunk-XDHUDE5K.js → chunk-XOH3SD7H.js} +3 -3
- package/dist/{chunk-62UK7AGW.js → chunk-ZNTGRDFA.js} +281 -14
- package/dist/cli/index.js +17 -17
- package/dist/{clio-K2PBVCEM.js → clio-Z3JTEL2I.js} +2 -2
- package/dist/{config-Q5J7UBAB.js → config-QYIRZHI7.js} +8 -8
- package/dist/{config-graph-P3555574.js → config-graph-TS3SGGQW.js} +8 -8
- package/dist/{configure-KIJ32ZO6.js → configure-OX7KVJTQ.js} +2 -2
- package/dist/{context-LFBQOSXT.js → context-2BNF6HTP.js} +12 -12
- package/dist/{context-WBH3KSVR.js → context-GTMUGAR4.js} +7 -7
- package/dist/{context-clear-YOHMQ6SQ.js → context-clear-FVJQBBL7.js} +7 -7
- package/dist/{context-working-set-MRAEVDNV.js → context-working-set-MOE72SD6.js} +2 -2
- package/dist/{detail-RHJOTGN4.js → detail-QZM3QAAU.js} +8 -8
- package/dist/{dispatch-runner-NC7PYNIO.js → dispatch-runner-2KSQO5FU.js} +8 -8
- package/dist/{doctor-S5KZ2GTN.js → doctor-2U6YR5NH.js} +3 -3
- package/dist/{eval-3D6M36N7.js → eval-ZKK77JWZ.js} +3 -3
- package/dist/{evidence-6IYN52ET.js → evidence-FMP7KZZ2.js} +7 -7
- package/dist/{evidence-JVECPI3F.js → evidence-PJTOE7JA.js} +7 -7
- package/dist/{evidence-XNVPPLVK.js → evidence-POSEQW4Q.js} +9 -9
- package/dist/{evolve-TLPUDJDM.js → evolve-JT6PAMZE.js} +7 -7
- package/dist/{fleet-WKVBF5SZ.js → fleet-4XR2ZEOF.js} +16 -16
- package/dist/{fleet-6RJWIOEO.js → fleet-6JJKNIMV.js} +8 -8
- package/dist/{fleet-inspect-236FZPRB.js → fleet-inspect-SWJMFVDP.js} +9 -9
- package/dist/{fleet-preflight-NZQHABWO.js → fleet-preflight-KSIHWQAU.js} +4 -4
- package/dist/{fleet-verify-E3RO6A5W.js → fleet-verify-QM6XU2KL.js} +7 -7
- package/dist/{fleet-view-KMZQY7YJ.js → fleet-view-RLKFHFCM.js} +8 -8
- package/dist/gui/reads-worker.js +4 -4
- package/dist/{init-DV5B6HQG.js → init-VBUPHABR.js} +11 -11
- package/dist/{interactive-OXGYZYAS.js → interactive-JCVBPVYT.js} +1597 -503
- package/dist/{inventory-OEGIL6PY.js → inventory-54G7FSVY.js} +8 -8
- package/dist/{mcp-2GSPYQ54.js → mcp-EFCWUPUR.js} +3 -3
- package/dist/{memory-J3STUUIK.js → memory-5BPQSSOP.js} +7 -7
- package/dist/{models-Y3GMFJ2V.js → models-R6HILX6X.js} +3 -3
- package/dist/{monitor-LHNH577J.js → monitor-HVRKSFEQ.js} +10 -10
- package/dist/{orchestrator-DTAPXULA.js → orchestrator-OR7V4YWK.js} +26 -26
- package/dist/{preload-OJBMAIXF.js → preload-KLXFOX2F.js} +7 -7
- package/dist/{run-H64MEVTA.js → run-UIDRUYAY.js} +16 -16
- package/dist/{skills-eval-CQ23MYQK.js → skills-eval-JDAXOPWC.js} +7 -7
- package/dist/{slash-commands-ROG3KIFJ.js → slash-commands-MNKQ5QP3.js} +4 -4
- package/dist/{startup-background-KDWCQMWT.js → startup-background-PJMWTKVO.js} +7 -7
- package/dist/{system-ZDW44ZGX.js → system-IVVZRKGI.js} +2 -2
- package/dist/{targets-AJWA6E7Z.js → targets-RFRUMCQ5.js} +4 -4
- package/dist/{terminal-lease-YFUSDPAD.js → terminal-lease-V2N2BIKQ.js} +2 -2
- package/dist/{usage-XSILDHKF.js → usage-LA33SG3R.js} +9 -9
- package/dist/{wiki-generate-RP22WP5K.js → wiki-generate-HL73TICA.js} +10 -10
- package/dist/worker/entry.js +4 -4
- package/docs/architecture/architecture.md +1 -0
- package/docs/architecture/context-engine.md +2 -2
- package/docs/architecture/observability.md +6 -4
- package/docs/architecture/prompt-envelope-and-tools.md +1 -1
- package/docs/architecture/session-lifecycle.md +1 -1
- package/docs/architecture/tui-design.md +12 -9
- package/docs/gui/parity/02-slash-and-surfaces.md +1 -1
- package/docs/guide/commands-and-modes.md +91 -5
- package/docs/guide/configuration-and-targets.md +2 -2
- package/docs/guide/proactive-memory.md +3 -3
- package/package.json +1 -1
- package/src/cli/usage.ts +2 -2
- package/src/domains/gateway/mcp/client.ts +1 -1
- package/src/domains/observability/background-memory-usage.ts +1 -1
- package/src/domains/observability/cost.ts +3 -3
- package/src/domains/observability/extension.ts +1 -1
- package/src/domains/observability/metrics.ts +1 -1
- package/src/domains/quota/anthropic-max-provider.ts +134 -0
- package/src/domains/quota/anthropic-usage.ts +220 -0
- package/src/domains/quota/antigravity-provider.ts +339 -0
- package/src/domains/quota/cache.ts +87 -0
- package/src/domains/quota/claude-code-provider.ts +169 -0
- package/src/domains/quota/codex-provider.ts +237 -0
- package/src/domains/quota/presentation.ts +240 -0
- package/src/domains/quota/registry.ts +23 -0
- package/src/domains/quota/service.ts +93 -0
- package/src/domains/quota/summary-feed.ts +86 -0
- package/src/domains/quota/types.ts +84 -0
- package/src/domains/session/usage.ts +2 -2
- package/src/engine/acp/commands.ts +1 -1
- package/src/entry/orchestrator.ts +2 -2
- package/src/interactive/chat-loop-messages.ts +2 -2
- package/src/interactive/chat-loop.ts +3 -3
- package/src/interactive/dispatch-board.ts +21 -3
- package/src/interactive/footer/dashboard.ts +13 -0
- package/src/interactive/footer/key-hints.ts +2 -2
- package/src/interactive/footer/pages.ts +55 -20
- package/src/interactive/footer-panel.ts +1 -1
- package/src/interactive/interactive-application.ts +5 -2
- package/src/interactive/interactive-input-runtime.ts +2 -2
- package/src/interactive/interactive-presentation.ts +15 -0
- package/src/interactive/interactive-slash-runtime.ts +2 -2
- package/src/interactive/interactive-tickers.ts +7 -1
- package/src/interactive/overlay-general-openers.ts +12 -8
- package/src/interactive/overlay-key-routing.ts +6 -9
- package/src/interactive/overlay-lifecycle.ts +10 -6
- package/src/interactive/overlay-session-lifecycle.ts +3 -3
- package/src/interactive/quota-view.ts +229 -0
- package/src/interactive/session-last-turn.ts +1 -1
- package/src/interactive/session-usage-reseed.ts +2 -2
- package/src/interactive/side-question.ts +2 -2
- package/src/interactive/slash-commands.ts +8 -8
- package/src/interactive/turn-context.ts +1 -1
- package/src/interactive/turn-prewarm.ts +1 -1
- package/src/interactive/{cost-overlay.ts → usage-overlay.ts} +121 -33
- 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 `/
|
|
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 `/
|
|
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
|
-
|
|
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 `/
|
|
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 `/
|
|
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 `/
|
|
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 `/
|
|
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 `/
|
|
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 `/
|
|
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 `/
|
|
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(
|
|
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
|
-
|
|
18
|
-
|
|
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(
|
|
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
|
-
/**
|
|
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
|
-
|
|
266
|
-
const
|
|
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
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
const
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
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" ? "/
|
|
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
|
-
: "/
|
|
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 `/
|
|
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
|
-
|
|
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
|
-
|
|
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", "
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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();
|