@linxiraos/pi-tui 1.2.1 → 1.3.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 +245 -224
- package/README.md +36 -0
- package/THIRD-PARTY-NOTICES.txt +55 -5
- package/dist/types/app-keybindings.d.ts +18 -8
- package/dist/types/apps/git/diff-pane.d.ts +5 -0
- package/dist/types/apps/git/git-tui.d.ts +16 -2
- package/dist/types/apps/git/state.d.ts +11 -0
- package/dist/types/autocomplete.d.ts +2 -0
- package/dist/types/charts/chart-svg.d.ts +7 -13
- package/dist/types/chat/advisor-message.d.ts +4 -1
- package/dist/types/chat/assistant-message.d.ts +9 -4
- package/dist/types/chat/figure-markdown.d.ts +7 -2
- package/dist/types/chat/messages.d.ts +3 -1
- package/dist/types/chat/read-tool-group.d.ts +2 -2
- package/dist/types/chat/table-chart.d.ts +11 -15
- package/dist/types/chat/tool-execution.d.ts +32 -20
- package/dist/types/chat/user-message.d.ts +3 -2
- package/dist/types/chrome/countdown-timer.d.ts +3 -2
- package/dist/types/chrome/diff.d.ts +2 -3
- package/dist/types/chrome/status-notice.d.ts +2 -0
- package/dist/types/chrome/transcript-container.d.ts +2 -0
- package/dist/types/components/image.d.ts +2 -0
- package/dist/types/components/markdown.d.ts +49 -3
- package/dist/types/components/text.d.ts +2 -0
- package/dist/types/debug-server.d.ts +2 -0
- package/dist/types/keybinding-matchers.d.ts +6 -0
- package/dist/types/multiplexers/tmux.d.ts +1 -0
- package/dist/types/multiplexers/types.d.ts +6 -0
- package/dist/types/native/backend.d.ts +5 -4
- package/dist/types/native/blobs.d.ts +24 -12
- package/dist/types/overlays/agent-hub-projection.d.ts +10 -5
- package/dist/types/overlays/annotation-overlay.d.ts +19 -0
- package/dist/types/overlays/approval-dialog.d.ts +79 -0
- package/dist/types/overlays/ask-dialog.d.ts +11 -6
- package/dist/types/overlays/btw-history-panel.d.ts +2 -6
- package/dist/types/overlays/hook-editor.d.ts +7 -2
- package/dist/types/overlays/hook-input.d.ts +12 -4
- package/dist/types/overlays/job-tree.d.ts +50 -0
- package/dist/types/overlays/jobs-panel.d.ts +25 -11
- package/dist/types/overlays/login-dialog.d.ts +8 -3
- package/dist/types/overlays/mcp-add-wizard.d.ts +9 -1
- package/dist/types/overlays/model-browser.d.ts +6 -1
- package/dist/types/overlays/model-picker.d.ts +2 -0
- package/dist/types/overlays/pause-screen.d.ts +5 -0
- package/dist/types/overlays/plan-review-overlay.d.ts +4 -0
- package/dist/types/overlays/report-panel.d.ts +5 -1
- package/dist/types/overlays/reset-usage-selector.d.ts +7 -5
- package/dist/types/overlays/session-account-selector.d.ts +4 -0
- package/dist/types/overlays/session-info-overlay.d.ts +7 -3
- package/dist/types/overlays/session-selector.d.ts +3 -1
- package/dist/types/overlays/usage-dashboard.d.ts +6 -6
- package/dist/types/prompt/composer.d.ts +23 -20
- package/dist/types/prompt/custom-editor.d.ts +20 -10
- package/dist/types/prompt/image-references.d.ts +22 -0
- package/dist/types/prompt/internal-url-autocomplete.d.ts +1 -0
- package/dist/types/prompt/macos-spelling.d.ts +2 -1
- package/dist/types/prompt/prose-gate.d.ts +3 -0
- package/dist/types/prompt/queued-messages.d.ts +1 -1
- package/dist/types/prompt/typing-word.d.ts +25 -0
- package/dist/types/prompt/welcome.d.ts +26 -6
- package/dist/types/render/hanging-wrap.d.ts +7 -0
- package/dist/types/render/hyperlink.d.ts +2 -0
- package/dist/types/render/render-utils.d.ts +9 -1
- package/dist/types/render/utils.d.ts +1 -1
- package/dist/types/setup/scenes/splash.d.ts +6 -5
- package/dist/types/status-line/context-usage.d.ts +4 -1
- package/dist/types/status-line/host.d.ts +6 -4
- package/dist/types/status-line/startup.d.ts +6 -6
- package/dist/types/status-line/types.d.ts +3 -0
- package/dist/types/stdin-buffer.d.ts +7 -0
- package/dist/types/terminal-capabilities.d.ts +16 -1
- package/dist/types/terminal-multiplexer.d.ts +7 -0
- package/dist/types/terminal.d.ts +8 -4
- package/dist/types/theme/symbols.d.ts +1 -1
- package/dist/types/theme/theme.d.ts +11 -0
- package/dist/types/thinking.d.ts +5 -0
- package/dist/types/tmux.d.ts +39 -4
- package/dist/types/tools/ast-edit.d.ts +2 -2
- package/dist/types/tools/ast-grep.d.ts +2 -2
- package/dist/types/tools/bash.d.ts +6 -1
- package/dist/types/tools/edit.d.ts +2 -0
- package/dist/types/tools/eval.d.ts +53 -4
- package/dist/types/tools/find.d.ts +14 -4
- package/dist/types/tools/glob.d.ts +3 -3
- package/dist/types/tools/grep.d.ts +2 -2
- package/dist/types/tools/grouped-file-output.d.ts +9 -4
- package/dist/types/tools/native-view.d.ts +1 -1
- package/dist/types/tools/read.d.ts +32 -10
- package/dist/types/tools/renderer.d.ts +2 -0
- package/dist/types/tools/task.d.ts +31 -0
- package/dist/types/tools/web-search-types.d.ts +1 -0
- package/dist/types/tools/write.d.ts +2 -0
- package/dist/types/tui.d.ts +20 -3
- package/package.json +10 -10
- package/src/app-keybindings.d.ts +18 -8
- package/src/app-keybindings.ts +20 -8
- package/src/apps/autoresearch-dashboard.ts +444 -27
- package/src/apps/debug/raw-sse.ts +2 -2
- package/src/apps/git/diff-pane.ts +39 -41
- package/src/apps/git/git-tui.ts +80 -14
- package/src/apps/git/sidebar.ts +10 -1
- package/src/apps/git/state.ts +12 -0
- package/src/autocomplete.d.ts +2 -0
- package/src/autocomplete.ts +21 -19
- package/src/bracketed-paste.ts +9 -2
- package/src/charts/chart-svg.d.ts +7 -13
- package/src/charts/chart-svg.ts +19 -22
- package/src/charts/table-data.ts +17 -6
- package/src/chat/advisor-message.d.ts +4 -1
- package/src/chat/advisor-message.ts +77 -24
- package/src/chat/assistant-message.d.ts +9 -4
- package/src/chat/assistant-message.ts +127 -31
- package/src/chat/bash-execution.ts +7 -2
- package/src/chat/figure-markdown.d.ts +7 -2
- package/src/chat/figure-markdown.ts +10 -3
- package/src/chat/image-loading.ts +9 -6
- package/src/chat/messages.d.ts +3 -1
- package/src/chat/messages.ts +4 -1
- package/src/chat/read-tool-group.d.ts +2 -2
- package/src/chat/read-tool-group.ts +97 -42
- package/src/chat/svg-figure.ts +3 -1
- package/src/chat/svg-source.ts +6 -3
- package/src/chat/table-chart.d.ts +11 -15
- package/src/chat/table-chart.ts +24 -57
- package/src/chat/tool-execution.d.ts +32 -20
- package/src/chat/tool-execution.ts +103 -36
- package/src/chat/user-message.d.ts +3 -2
- package/src/chat/user-message.ts +54 -34
- package/src/chrome/countdown-timer.ts +3 -3
- package/src/chrome/diff.d.ts +2 -3
- package/src/chrome/diff.ts +4 -9
- package/src/chrome/status-notice.ts +5 -0
- package/src/chrome/transcript-container.d.ts +2 -0
- package/src/chrome/transcript-container.ts +212 -28
- package/src/components/editor.ts +12 -12
- package/src/components/image.d.ts +2 -0
- package/src/components/image.ts +54 -20
- package/src/components/markdown.d.ts +49 -3
- package/src/components/markdown.ts +482 -12
- package/src/components/settings-list.ts +2 -4
- package/src/components/text.d.ts +2 -0
- package/src/components/text.ts +11 -1
- package/src/debug-server.d.ts +2 -0
- package/src/debug-server.ts +51 -69
- package/src/hotkeys-markdown.ts +2 -0
- package/src/keybinding-matchers.d.ts +6 -0
- package/src/keybinding-matchers.ts +19 -0
- package/src/multiplexers/tmux.d.ts +1 -0
- package/src/multiplexers/tmux.ts +3 -0
- package/src/multiplexers/types.d.ts +6 -0
- package/src/multiplexers/types.ts +6 -0
- package/src/native/backend.d.ts +5 -4
- package/src/native/backend.ts +13 -5
- package/src/native/blobs.d.ts +24 -12
- package/src/native/blobs.ts +56 -7
- package/src/native/reconcile.ts +43 -21
- package/src/overlays/advisor-config.ts +259 -8
- package/src/overlays/agent-hub-projection.d.ts +10 -5
- package/src/overlays/agent-hub-projection.ts +63 -12
- package/src/overlays/annotation-overlay.ts +422 -168
- package/src/overlays/approval-dialog.ts +389 -0
- package/src/overlays/ask-dialog.ts +244 -105
- package/src/overlays/btw-history-panel.ts +208 -147
- package/src/overlays/extensions/extension-list.ts +4 -6
- package/src/overlays/hook-editor.ts +90 -32
- package/src/overlays/hook-input.ts +105 -19
- package/src/overlays/job-tree.ts +177 -0
- package/src/overlays/jobs-panel.ts +222 -142
- package/src/overlays/login-dialog.ts +201 -91
- package/src/overlays/mcp-add-wizard.ts +562 -279
- package/src/overlays/model-browser.ts +32 -7
- package/src/overlays/model-hub.ts +4 -10
- package/src/overlays/model-picker.ts +58 -21
- package/src/overlays/pause-screen.ts +81 -23
- package/src/overlays/plan-review-overlay.ts +16 -0
- package/src/overlays/plugin-selector.ts +17 -1
- package/src/overlays/queue-mode-selector.ts +10 -0
- package/src/overlays/report-panel.ts +6 -2
- package/src/overlays/reset-usage-selector.ts +130 -21
- package/src/overlays/session-account-selector.ts +36 -0
- package/src/overlays/session-info-overlay.ts +199 -76
- package/src/overlays/session-selector.ts +15 -10
- package/src/overlays/settings-selector.ts +3 -0
- package/src/overlays/show-images-selector.ts +1 -0
- package/src/overlays/snapcompact-shape-preview.ts +13 -6
- package/src/overlays/theme-selector.ts +6 -1
- package/src/overlays/thinking-selector.ts +8 -4
- package/src/overlays/tree-selector.ts +117 -2
- package/src/overlays/usage-dashboard.ts +498 -144
- package/src/prompt/composer-cache.ts +4 -1
- package/src/prompt/composer.ts +200 -74
- package/src/prompt/custom-editor.ts +168 -65
- package/src/prompt/image-references.d.ts +22 -0
- package/src/prompt/image-references.ts +79 -2
- package/src/prompt/internal-url-autocomplete.ts +1 -0
- package/src/prompt/macos-spelling.ts +28 -5
- package/src/prompt/prose-gate.ts +3 -0
- package/src/prompt/queued-messages.ts +9 -2
- package/src/prompt/tips.txt +1 -2
- package/src/prompt/typing-word.ts +59 -0
- package/src/prompt/welcome.ts +97 -45
- package/src/render/code-cell.ts +18 -5
- package/src/render/hanging-wrap.d.ts +7 -0
- package/src/render/hanging-wrap.ts +58 -0
- package/src/render/hyperlink.d.ts +2 -0
- package/src/render/hyperlink.ts +5 -0
- package/src/render/render-utils.d.ts +9 -1
- package/src/render/render-utils.ts +16 -2
- package/src/render/utils.d.ts +1 -1
- package/src/render/utils.ts +1 -1
- package/src/render/width-aware-text.ts +6 -5
- package/src/setup/scenes/splash.ts +22 -8
- package/src/status-line/component.ts +79 -11
- package/src/status-line/context-usage.ts +176 -73
- package/src/status-line/footer.ts +18 -35
- package/src/status-line/host.ts +6 -1
- package/src/status-line/segments.ts +16 -5
- package/src/status-line/startup.ts +6 -6
- package/src/status-line/types.ts +1 -0
- package/src/stdin-buffer.d.ts +7 -0
- package/src/stdin-buffer.ts +300 -62
- package/src/terminal-capabilities.d.ts +16 -1
- package/src/terminal-capabilities.ts +66 -28
- package/src/terminal-multiplexer.d.ts +7 -0
- package/src/terminal-multiplexer.ts +14 -0
- package/src/terminal.d.ts +8 -4
- package/src/terminal.ts +190 -63
- package/src/theme/symbols.d.ts +1 -1
- package/src/theme/symbols.ts +1 -0
- package/src/theme/theme-class.ts +1 -0
- package/src/theme/theme.d.ts +11 -0
- package/src/theme/theme.ts +46 -14
- package/src/thinking.d.ts +5 -0
- package/src/thinking.ts +9 -1
- package/src/tmux.d.ts +39 -4
- package/src/tmux.ts +110 -16
- package/src/tools/ask.ts +20 -9
- package/src/tools/ast-edit.d.ts +2 -2
- package/src/tools/ast-edit.ts +30 -7
- package/src/tools/ast-grep.d.ts +2 -2
- package/src/tools/ast-grep.ts +39 -35
- package/src/tools/bash.d.ts +6 -1
- package/src/tools/bash.ts +15 -9
- package/src/tools/debug.ts +2 -1
- package/src/tools/default-renderer.ts +3 -2
- package/src/tools/edit.d.ts +2 -0
- package/src/tools/edit.ts +100 -21
- package/src/tools/eval.d.ts +53 -4
- package/src/tools/eval.ts +173 -137
- package/src/tools/find.d.ts +14 -4
- package/src/tools/find.ts +71 -18
- package/src/tools/glob.d.ts +3 -3
- package/src/tools/glob.ts +51 -17
- package/src/tools/grep.d.ts +2 -2
- package/src/tools/grep.ts +99 -22
- package/src/tools/grouped-file-output.d.ts +9 -4
- package/src/tools/grouped-file-output.ts +21 -6
- package/src/tools/lsp.ts +30 -18
- package/src/tools/mcp.ts +2 -2
- package/src/tools/native-view.d.ts +1 -1
- package/src/tools/native-view.ts +2 -2
- package/src/tools/read.d.ts +32 -10
- package/src/tools/read.ts +89 -43
- package/src/tools/renderer.d.ts +2 -0
- package/src/tools/renderer.ts +2 -0
- package/src/tools/task.d.ts +31 -0
- package/src/tools/task.ts +40 -7
- package/src/tools/todo.ts +3 -2
- package/src/tools/wait.ts +16 -30
- package/src/tools/web-search-types.d.ts +1 -0
- package/src/tools/web-search-types.ts +1 -0
- package/src/tools/web-search.ts +2 -1
- package/src/tools/write.d.ts +2 -0
- package/src/tools/write.ts +36 -12
- package/src/tools/yield.ts +2 -2
- package/src/tui.d.ts +20 -3
- package/src/tui.ts +160 -43
- package/src/utils.ts +5 -3
- package/dist/types/tools/eval-format/code-mode-declarations.d.ts +0 -5
- package/src/tools/eval-format/code-mode-declarations.ts +0 -61
|
@@ -378,7 +378,7 @@ export function buildHeatmapLayout(points: DailyActivityPoint[], weeks: number,
|
|
|
378
378
|
const NATIVE_HEATMAP_WEEKS = 53;
|
|
379
379
|
/** Span tokens for heatmap intensity levels 1..4 (the `table` fallback). */
|
|
380
380
|
const HEAT_LEVEL_TOKENS = ["dim", "accent dim", "accent", "accent strong"];
|
|
381
|
-
/** Heatmap row labels, Monday first like
|
|
381
|
+
/** Heatmap row labels, Monday first like zeta's grid: M/W/F only. */
|
|
382
382
|
const HEATMAP_ROW_LABELS = ["M", "", "W", "", "F", "", ""];
|
|
383
383
|
const WEEKDAY_NAMES = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
|
|
384
384
|
|
|
@@ -390,7 +390,7 @@ function statusTone(status: UsageLimit["status"]): TspTone {
|
|
|
390
390
|
return "muted";
|
|
391
391
|
}
|
|
392
392
|
|
|
393
|
-
/** Status dot
|
|
393
|
+
/** Status dot before a provider's name, toned by the card's aggregate status. */
|
|
394
394
|
function statusDot(status: UsageLimit["status"]): NativeNode {
|
|
395
395
|
const token =
|
|
396
396
|
status === "exhausted" ? "error" : status === "warning" ? "warning" : status === "ok" ? "success" : "dim";
|
|
@@ -402,7 +402,7 @@ function statusDot(status: UsageLimit["status"]): NativeNode {
|
|
|
402
402
|
: status === "ok"
|
|
403
403
|
? "OK"
|
|
404
404
|
: "Unknown";
|
|
405
|
-
return text([span("●", token)], { title: label, aria: label });
|
|
405
|
+
return text([span("●", token)], { role: "zeta.usage.dot", title: label, aria: label });
|
|
406
406
|
}
|
|
407
407
|
|
|
408
408
|
/**
|
|
@@ -415,11 +415,6 @@ function usageMeter(fraction: number, status: UsageLimit["status"], meter: boole
|
|
|
415
415
|
return meter ? node("meter", { value, style: "bar", size: "md", tone }) : node("progress", { value, tone, grow: 1 });
|
|
416
416
|
}
|
|
417
417
|
|
|
418
|
-
/** `62% left` for a used fraction (overage reads as 0%). */
|
|
419
|
-
function leftText(fraction: number): string {
|
|
420
|
-
return `${Math.max(0, Math.round((1 - fraction) * 100))}% left`;
|
|
421
|
-
}
|
|
422
|
-
|
|
423
418
|
/** `Mon 28 Sep` for a local date. */
|
|
424
419
|
function shortDate(date: Date): string {
|
|
425
420
|
return `${WEEKDAY_NAMES[date.getDay()]} ${date.getDate()} ${MONTH_NAMES[date.getMonth()]}`;
|
|
@@ -455,24 +450,61 @@ function mutedText(content: string, wrap = false): NativeNode {
|
|
|
455
450
|
}
|
|
456
451
|
|
|
457
452
|
/**
|
|
458
|
-
* A quiet
|
|
459
|
-
*
|
|
453
|
+
* A quiet row under the provider rows naming the providers that get none
|
|
454
|
+
* (`Untouched Kimi Code, Zai`): the label sits in the provider column, the
|
|
455
|
+
* names in the quota column; `title` says why on hover.
|
|
460
456
|
*/
|
|
461
457
|
function footnote(key: string, label: string, cards: readonly ProviderCard[], title?: string): NativeNode {
|
|
462
458
|
const names = cards.map(card => card.name).join(", ");
|
|
463
459
|
return node(
|
|
464
|
-
"
|
|
465
|
-
{
|
|
466
|
-
|
|
467
|
-
wrap: "word",
|
|
468
|
-
role: "zeta.usage.footnote",
|
|
469
|
-
...(title ? { title: sanitizeDisplayLine(title) } : {}),
|
|
470
|
-
},
|
|
471
|
-
undefined,
|
|
460
|
+
"row",
|
|
461
|
+
{ role: "zeta.usage.footnote", align: "baseline", ...(title ? { title: sanitizeDisplayLine(title) } : {}) },
|
|
462
|
+
[text([span(label, "dim")], { role: "zeta.usage.footnote.k" }), text([span(names)], { wrap: "word" })],
|
|
472
463
|
key,
|
|
473
464
|
);
|
|
474
465
|
}
|
|
475
466
|
|
|
467
|
+
/** A section's title over its rows (`Quotas`, `Activity`) with quiet facts at its right end. */
|
|
468
|
+
function sectionHead(title: string, meta: readonly NativeChild[]): NativeNode {
|
|
469
|
+
return node(
|
|
470
|
+
"row",
|
|
471
|
+
{ role: "zeta.usage.section", gap: "sm", align: "center" },
|
|
472
|
+
[text([span(title)], { role: "zeta.usage.section.title" }), node("spacer", { grow: 1 }), ...meta],
|
|
473
|
+
"head",
|
|
474
|
+
);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* One headline figure of the summary strip: a small label, the figure (with an
|
|
479
|
+
* optional chart beside it), and a line of context under it.
|
|
480
|
+
*/
|
|
481
|
+
function statTile(
|
|
482
|
+
key: string,
|
|
483
|
+
label: string,
|
|
484
|
+
value: TspText,
|
|
485
|
+
sub: TspText,
|
|
486
|
+
options: { chart?: NativeNode; title?: string } = {},
|
|
487
|
+
): NativeNode {
|
|
488
|
+
const figure: NativeChild[] = [text(value, { role: "zeta.usage.stat.value", truncate: "end" })];
|
|
489
|
+
if (options.chart) figure.push(options.chart);
|
|
490
|
+
return node(
|
|
491
|
+
"col",
|
|
492
|
+
{ role: "zeta.usage.stat", gap: "xs", ...(options.title ? { title: options.title } : {}) },
|
|
493
|
+
[
|
|
494
|
+
text([span(label)], { role: "zeta.usage.stat.label", truncate: "end" }),
|
|
495
|
+
node("row", { gap: "md", align: "end" }, figure, "figure"),
|
|
496
|
+
text(sub, { role: "zeta.usage.stat.sub", truncate: "end" }),
|
|
497
|
+
],
|
|
498
|
+
key,
|
|
499
|
+
);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/** `62%` in mono, toned like the window's status, then a quiet `left`. */
|
|
503
|
+
function leftSpans(fraction: number, status: UsageLimit["status"]): TspSpan[] {
|
|
504
|
+
const token = status === "exhausted" ? "mono error" : status === "warning" ? "mono warning" : "mono";
|
|
505
|
+
return [span(`${Math.max(0, Math.round((1 - fraction) * 100))}%`, token), span(" left", "dim")];
|
|
506
|
+
}
|
|
507
|
+
|
|
476
508
|
/** Stable, human account name for a report in the detail view. */
|
|
477
509
|
function reportAccountLabel(report: UsageReport, limit: UsageLimit | undefined, index: number): string {
|
|
478
510
|
const metadata = report.metadata;
|
|
@@ -499,6 +531,81 @@ function detailWindowLabel(label: string, limit: UsageLimit): string | undefined
|
|
|
499
531
|
const WHOLE_DOLLARS = new Intl.NumberFormat("en-US", { maximumFractionDigits: 0 });
|
|
500
532
|
const COMPACT_COUNT = new Intl.NumberFormat("en-US", { notation: "compact", maximumFractionDigits: 1 });
|
|
501
533
|
|
|
534
|
+
/** `$1,234`, `$759`, `$84.20`, `$0.13`: cents only below $100. */
|
|
535
|
+
function formatCost(cost: number): string {
|
|
536
|
+
if (cost <= 0) return "$0";
|
|
537
|
+
return cost >= 100 ? `$${WHOLE_DOLLARS.format(cost)}` : `$${cost.toFixed(2)}`;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
function requestsText(requests: number): string {
|
|
541
|
+
return `${COMPACT_COUNT.format(requests)} request${requests === 1 ? "" : "s"}`;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/** Days the spend spark and the "Last 30 days" figure cover. */
|
|
545
|
+
const MONTH_DAYS = 30;
|
|
546
|
+
|
|
547
|
+
interface ActivityDay {
|
|
548
|
+
date: Date;
|
|
549
|
+
cost: number;
|
|
550
|
+
requests: number;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/** Headline figures of the activity history behind the summary strip and the calendar's foot. */
|
|
554
|
+
interface ActivityFigures {
|
|
555
|
+
/** Whether any day in range carries a price (else figures fall back to request counts). */
|
|
556
|
+
priced: boolean;
|
|
557
|
+
/** The last {@link MONTH_DAYS} days, oldest first. */
|
|
558
|
+
month: ActivityDay[];
|
|
559
|
+
monthCost: number;
|
|
560
|
+
monthRequests: number;
|
|
561
|
+
busiest?: ActivityDay;
|
|
562
|
+
activeDays: number;
|
|
563
|
+
longestStreak: number;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/** Derive the headline figures from the daily points over the heatmap's range. */
|
|
567
|
+
function activityFigures(
|
|
568
|
+
points: readonly DailyActivityPoint[],
|
|
569
|
+
layout: HeatmapLayout,
|
|
570
|
+
today = new Date(),
|
|
571
|
+
): ActivityFigures {
|
|
572
|
+
const byDay = new Map(points.map(point => [point.day, point]));
|
|
573
|
+
const today0 = new Date(today.getFullYear(), today.getMonth(), today.getDate());
|
|
574
|
+
const priced = layout.totalCost > 0;
|
|
575
|
+
const weight = (day: ActivityDay): number => (priced ? day.cost : day.requests);
|
|
576
|
+
const month: ActivityDay[] = [];
|
|
577
|
+
for (let back = MONTH_DAYS - 1; back >= 0; back--) {
|
|
578
|
+
const date = addDays(today0, -back);
|
|
579
|
+
const point = byDay.get(localIso(date));
|
|
580
|
+
month.push({ date, cost: point?.cost ?? 0, requests: point?.requests ?? 0 });
|
|
581
|
+
}
|
|
582
|
+
let busiest: ActivityDay | undefined;
|
|
583
|
+
let activeDays = 0;
|
|
584
|
+
let streak = 0;
|
|
585
|
+
let longestStreak = 0;
|
|
586
|
+
for (let date = layout.start; date <= today0; date = addDays(date, 1)) {
|
|
587
|
+
const point = byDay.get(localIso(date));
|
|
588
|
+
if (!point || (point.cost <= 0 && point.requests <= 0)) {
|
|
589
|
+
streak = 0;
|
|
590
|
+
continue;
|
|
591
|
+
}
|
|
592
|
+
activeDays++;
|
|
593
|
+
streak++;
|
|
594
|
+
longestStreak = Math.max(longestStreak, streak);
|
|
595
|
+
const day = { date, cost: point.cost, requests: point.requests };
|
|
596
|
+
if (!busiest || weight(day) > weight(busiest)) busiest = day;
|
|
597
|
+
}
|
|
598
|
+
return {
|
|
599
|
+
priced,
|
|
600
|
+
month,
|
|
601
|
+
monthCost: month.reduce((sum, day) => sum + day.cost, 0),
|
|
602
|
+
monthRequests: month.reduce((sum, day) => sum + day.requests, 0),
|
|
603
|
+
busiest,
|
|
604
|
+
activeDays,
|
|
605
|
+
longestStreak,
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
|
|
502
609
|
/** `$1,234 · 5.6K requests` totals for the activity summary. */
|
|
503
610
|
function formatActivityTotals(layout: HeatmapLayout): string {
|
|
504
611
|
const cost =
|
|
@@ -962,12 +1069,12 @@ export class UsageDashboardComponent implements Component {
|
|
|
962
1069
|
// ---------------------------------------------------------------------------
|
|
963
1070
|
|
|
964
1071
|
/**
|
|
965
|
-
* The sheet body (the terminal's `lg` overlay is the frame): a
|
|
966
|
-
* (
|
|
967
|
-
*
|
|
968
|
-
* per-provider detail tables, then Close like the other
|
|
969
|
-
* `meter`/`chart` fall back to `progress`/`table` on
|
|
970
|
-
* them. Rebuilt only when its inputs change.
|
|
1072
|
+
* The sheet body (the terminal's `lg` overlay is the frame): a toolbar
|
|
1073
|
+
* (Overview/Details, freshness, Refresh), then either the overview — a
|
|
1074
|
+
* strip of headline figures, the quota rows per provider, the activity
|
|
1075
|
+
* calendar — or the per-provider detail tables, then Close like the other
|
|
1076
|
+
* report sheets. `meter`/`chart` fall back to `progress`/`table` on
|
|
1077
|
+
* terminals without them. Rebuilt only when its inputs change.
|
|
971
1078
|
*/
|
|
972
1079
|
describe(cx: DescribeContext): NativeNode {
|
|
973
1080
|
const meter = cx.supports("meter");
|
|
@@ -979,9 +1086,14 @@ export class UsageDashboardComponent implements Component {
|
|
|
979
1086
|
const body = this.#view === "detail" ? this.#describeDetail() : this.#describeOverview(meter, chart);
|
|
980
1087
|
// Esc leaves Details for Overview first, so only Overview's Close wears its keycap.
|
|
981
1088
|
const close = actionButton("Close", "close", this.#view === "overview" ? { keys: "escape" } : {});
|
|
982
|
-
const root = col(
|
|
983
|
-
|
|
984
|
-
|
|
1089
|
+
const root = col(
|
|
1090
|
+
[
|
|
1091
|
+
this.#describeHead(),
|
|
1092
|
+
node("col", { role: "zeta.usage.body", gap: "lg" }, body, this.#view),
|
|
1093
|
+
actionBar([null, close]),
|
|
1094
|
+
],
|
|
1095
|
+
{ gap: "lg" },
|
|
1096
|
+
);
|
|
985
1097
|
this.#nativeCache = { revision: this.#revision, meter, chart, node: root };
|
|
986
1098
|
return root;
|
|
987
1099
|
}
|
|
@@ -1000,27 +1112,27 @@ export class UsageDashboardComponent implements Component {
|
|
|
1000
1112
|
}
|
|
1001
1113
|
}
|
|
1002
1114
|
|
|
1115
|
+
/** The toolbar: the two views as a segmented control, then how fresh the reports are and Refresh. */
|
|
1003
1116
|
#describeHead(): NativeNode {
|
|
1004
1117
|
const checked: NativeChild[] = [];
|
|
1005
1118
|
const latestFetchedAt = Math.max(0, ...this.#reports.map(report => report.fetchedAt ?? 0));
|
|
1006
1119
|
if (this.#refreshing) {
|
|
1007
|
-
checked.push(node("spinner", { label: [span("Refreshing…", "dim")] }));
|
|
1120
|
+
checked.push(node("spinner", { style: "dots", label: [span("Refreshing…", "dim")] }));
|
|
1008
1121
|
} else if (this.#refreshError) {
|
|
1009
1122
|
checked.push(
|
|
1010
1123
|
text([span(`Refresh failed: ${sanitizeDisplayLine(this.#refreshError)}`, "warning")], {
|
|
1011
1124
|
truncate: "end",
|
|
1125
|
+
title: sanitizeDisplayLine(this.#refreshError),
|
|
1012
1126
|
}),
|
|
1013
1127
|
);
|
|
1014
1128
|
} else if (latestFetchedAt) {
|
|
1015
1129
|
checked.push(
|
|
1016
|
-
text([span("
|
|
1130
|
+
text([span("Updated", "dim")]),
|
|
1017
1131
|
elapsed(Date.now() - latestFetchedAt),
|
|
1018
1132
|
text([span("ago", "dim")]),
|
|
1019
1133
|
);
|
|
1020
1134
|
}
|
|
1021
1135
|
const children: NativeChild[] = [
|
|
1022
|
-
node("row", { gap: "xs", align: "baseline", shrink: 1 }, checked, "checked"),
|
|
1023
|
-
node("spacer", { grow: 1 }),
|
|
1024
1136
|
node(
|
|
1025
1137
|
"tabs",
|
|
1026
1138
|
{
|
|
@@ -1033,88 +1145,244 @@ export class UsageDashboardComponent implements Component {
|
|
|
1033
1145
|
undefined,
|
|
1034
1146
|
"tabs",
|
|
1035
1147
|
),
|
|
1148
|
+
node("spacer", { grow: 1 }),
|
|
1149
|
+
node("row", { gap: "xs", align: "baseline", shrink: 1 }, checked, "checked"),
|
|
1036
1150
|
];
|
|
1037
1151
|
if (this.#options.refresh) children.push(actionButton("Refresh", "refresh", { keys: "r" }));
|
|
1038
|
-
return node("row", { role: "zeta.usage.head", gap: "
|
|
1152
|
+
return node("row", { role: "zeta.usage.head", gap: "md", align: "center" }, children, "head");
|
|
1039
1153
|
}
|
|
1040
1154
|
|
|
1155
|
+
/**
|
|
1156
|
+
* The overview: headline figures, then a row per provider with something
|
|
1157
|
+
* to show (the rest named in footnote rows), then the activity calendar.
|
|
1158
|
+
* With nothing used anywhere it is one empty state instead.
|
|
1159
|
+
*/
|
|
1041
1160
|
#describeOverview(meter: boolean, chart: boolean): NativeChild[] {
|
|
1042
|
-
|
|
1161
|
+
// Only providers with something to show get a row: the rest are named
|
|
1162
|
+
// in footnotes, whether untouched, unreadable or quota-less.
|
|
1163
|
+
const framed: ProviderCard[] = [];
|
|
1164
|
+
const untouched: ProviderCard[] = [];
|
|
1165
|
+
const unreported: ProviderCard[] = [];
|
|
1166
|
+
const unmetered: ProviderCard[] = [];
|
|
1167
|
+
for (const entry of this.#cards) {
|
|
1168
|
+
const shown =
|
|
1169
|
+
entry.windows.length > 0 || entry.resetCredits !== undefined || entry.daybreakAccounts !== undefined;
|
|
1170
|
+
if (!shown) (entry.unavailableAccounts.length > 0 ? unreported : unmetered).push(entry);
|
|
1171
|
+
else if (entry.idle) untouched.push(entry);
|
|
1172
|
+
else framed.push(entry);
|
|
1173
|
+
}
|
|
1174
|
+
const notes: NativeNode[] = [];
|
|
1175
|
+
if (untouched.length > 0) notes.push(footnote("idle", "Untouched", untouched));
|
|
1176
|
+
if (unreported.length > 0) {
|
|
1177
|
+
const accounts = unreported.flatMap(entry =>
|
|
1178
|
+
entry.unavailableAccounts.map(account => `${entry.name} (${account})`),
|
|
1179
|
+
);
|
|
1180
|
+
notes.push(
|
|
1181
|
+
footnote(
|
|
1182
|
+
"unreported",
|
|
1183
|
+
"No usage data",
|
|
1184
|
+
unreported,
|
|
1185
|
+
`No usage report came back for ${accounts.join(", ")}: the sign-in expired, the request failed, or the plan has no quotas`,
|
|
1186
|
+
),
|
|
1187
|
+
);
|
|
1188
|
+
}
|
|
1189
|
+
if (unmetered.length > 0) {
|
|
1190
|
+
const why = unmetered.flatMap(entry => entry.notes.map(note => `${entry.name}: ${note}`));
|
|
1191
|
+
notes.push(
|
|
1192
|
+
footnote(
|
|
1193
|
+
"unmetered",
|
|
1194
|
+
"No quotas reported",
|
|
1195
|
+
unmetered,
|
|
1196
|
+
why.length > 0 ? why.join(" • ") : "These providers report no quota windows to track",
|
|
1197
|
+
),
|
|
1198
|
+
);
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
const points = this.#activity;
|
|
1202
|
+
const layout = points ? buildHeatmapLayout(points, NATIVE_HEATMAP_WEEKS) : undefined;
|
|
1203
|
+
const figures = points && layout ? activityFigures(points, layout) : undefined;
|
|
1204
|
+
const quiet = layout !== undefined && layout.totalCost <= 0 && layout.totalRequests <= 0;
|
|
1205
|
+
if (framed.length === 0 && quiet && !this.#activityError) {
|
|
1206
|
+
const children: NativeChild[] = [
|
|
1207
|
+
node("icon", { name: "gauge" }, undefined, "icon"),
|
|
1208
|
+
text([span("No usage yet")], { role: "zeta.usage.empty.title" }),
|
|
1209
|
+
text(
|
|
1210
|
+
[
|
|
1211
|
+
span(
|
|
1212
|
+
"Quotas show here once a signed-in provider reports usage, and the calendar fills in as you work.",
|
|
1213
|
+
"muted",
|
|
1214
|
+
),
|
|
1215
|
+
],
|
|
1216
|
+
{ role: "zeta.usage.empty.body", wrap: "word" },
|
|
1217
|
+
),
|
|
1218
|
+
];
|
|
1219
|
+
if (notes.length > 0) children.push(node("col", { gap: "xs" }, notes, "footnotes"));
|
|
1220
|
+
return [node("col", { role: "zeta.usage.empty", gap: "sm", align: "center" }, children, "empty")];
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
const quotas: NativeChild[] = [sectionHead("Quotas", this.#quotaMeta(framed))];
|
|
1224
|
+
for (const entry of framed) quotas.push(this.#describeProvider(entry, meter));
|
|
1043
1225
|
if (this.#cards.length === 0) {
|
|
1044
|
-
|
|
1045
|
-
|
|
1226
|
+
quotas.push(text([span("No usage data available.", "muted")], { role: "zeta.usage.none", wrap: "word" }));
|
|
1227
|
+
} else if (framed.length === 0) {
|
|
1228
|
+
quotas.push(
|
|
1229
|
+
text([span("Nothing used on any quota yet.", "muted")], { role: "zeta.usage.none", wrap: "word" }),
|
|
1046
1230
|
);
|
|
1047
|
-
}
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
);
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
const why = unmetered.flatMap(entry => entry.notes.map(note => `${entry.name}: ${note}`));
|
|
1088
|
-
notes.push(
|
|
1089
|
-
footnote(
|
|
1090
|
-
"unmetered",
|
|
1091
|
-
"No quotas reported",
|
|
1092
|
-
unmetered,
|
|
1093
|
-
why.length > 0 ? why.join(" • ") : "These providers report no quota windows to track",
|
|
1094
|
-
),
|
|
1095
|
-
);
|
|
1231
|
+
}
|
|
1232
|
+
quotas.push(...notes);
|
|
1233
|
+
return [
|
|
1234
|
+
this.#describeStats(framed, layout, figures, chart),
|
|
1235
|
+
node("col", { role: "zeta.usage.quotas" }, quotas, "quotas"),
|
|
1236
|
+
this.#describeActivity(chart, layout, figures),
|
|
1237
|
+
];
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
/** `3 providers · 10 accounts` over the quota rows. */
|
|
1241
|
+
#quotaMeta(framed: readonly ProviderCard[]): NativeChild[] {
|
|
1242
|
+
if (framed.length === 0) return [];
|
|
1243
|
+
const accounts = framed.reduce((sum, entry) => sum + entry.accounts, 0);
|
|
1244
|
+
const parts = [`${framed.length} provider${framed.length === 1 ? "" : "s"}`];
|
|
1245
|
+
if (accounts > framed.length) parts.push(`${accounts} accounts`);
|
|
1246
|
+
return [
|
|
1247
|
+
text([span(parts.join(" · "), "dim")], {
|
|
1248
|
+
role: "zeta.usage.section.meta",
|
|
1249
|
+
title: "Each quota shows the mean across a provider's accounts; it resets with the fullest one",
|
|
1250
|
+
}),
|
|
1251
|
+
];
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
/**
|
|
1255
|
+
* The summary strip: the tightest quota (what stops work first), the
|
|
1256
|
+
* year's spend, the last 30 days with a spend spark, and the busiest day.
|
|
1257
|
+
*/
|
|
1258
|
+
#describeStats(
|
|
1259
|
+
framed: readonly ProviderCard[],
|
|
1260
|
+
layout: HeatmapLayout | undefined,
|
|
1261
|
+
figures: ActivityFigures | undefined,
|
|
1262
|
+
chart: boolean,
|
|
1263
|
+
): NativeNode {
|
|
1264
|
+
const tiles: NativeNode[] = [];
|
|
1265
|
+
let tightest: { entry: ProviderCard; window: CardWindowRow; fraction: number } | undefined;
|
|
1266
|
+
for (const entry of framed) {
|
|
1267
|
+
for (const window of entry.windows) {
|
|
1268
|
+
if (window.fraction === undefined) continue;
|
|
1269
|
+
if (!tightest || window.fraction > tightest.fraction)
|
|
1270
|
+
tightest = { entry, window, fraction: window.fraction };
|
|
1096
1271
|
}
|
|
1097
|
-
if (notes.length > 0) children.push(node("col", { gap: "xs" }, notes, "footnotes"));
|
|
1098
1272
|
}
|
|
1099
|
-
|
|
1100
|
-
|
|
1273
|
+
if (tightest) {
|
|
1274
|
+
const where = `${sanitizeDisplayLine(tightest.window.label)} · ${tightest.entry.name}`;
|
|
1275
|
+
const reset =
|
|
1276
|
+
tightest.window.resetMs !== undefined ? resetLabel(this.#nowMs, tightest.window.resetMs) : undefined;
|
|
1277
|
+
tiles.push(
|
|
1278
|
+
statTile(
|
|
1279
|
+
"quota",
|
|
1280
|
+
"Tightest quota",
|
|
1281
|
+
leftSpans(tightest.fraction, tightest.window.status),
|
|
1282
|
+
[span(where, "dim")],
|
|
1283
|
+
{
|
|
1284
|
+
title: reset ? `${where} · ${reset.title}` : where,
|
|
1285
|
+
},
|
|
1286
|
+
),
|
|
1287
|
+
);
|
|
1288
|
+
} else {
|
|
1289
|
+
tiles.push(
|
|
1290
|
+
statTile(
|
|
1291
|
+
"quota",
|
|
1292
|
+
"Tightest quota",
|
|
1293
|
+
[span("—", "dim")],
|
|
1294
|
+
[span(this.#cards.length > 0 ? "Every quota untouched" : "No quotas reported", "dim")],
|
|
1295
|
+
),
|
|
1296
|
+
);
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
if (!figures || !layout) {
|
|
1300
|
+
const sub = [span(this.#activityError ? "History unavailable" : "Loading…", "dim")];
|
|
1301
|
+
tiles.push(
|
|
1302
|
+
statTile("year", "Past year", [span("—", "dim")], sub),
|
|
1303
|
+
statTile("month", "Last 30 days", [span("—", "dim")], sub),
|
|
1304
|
+
statTile("busiest", "Busiest day", [span("—", "dim")], sub),
|
|
1305
|
+
);
|
|
1306
|
+
return node("row", { role: "zeta.usage.stats", wrap: true }, tiles, "stats");
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
const amount = (cost: number, requests: number): TspSpan[] =>
|
|
1310
|
+
figures.priced ? [span(formatCost(cost))] : [span(COMPACT_COUNT.format(requests))];
|
|
1311
|
+
const counted = (requests: number): string =>
|
|
1312
|
+
figures.priced ? requestsText(requests) : `request${requests === 1 ? "" : "s"}`;
|
|
1313
|
+
tiles.push(
|
|
1314
|
+
statTile("year", "Past year", amount(layout.totalCost, layout.totalRequests), [
|
|
1315
|
+
span(counted(layout.totalRequests), "dim"),
|
|
1316
|
+
]),
|
|
1317
|
+
);
|
|
1318
|
+
|
|
1319
|
+
const spark =
|
|
1320
|
+
chart && (figures.monthCost > 0 || figures.monthRequests > 0)
|
|
1321
|
+
? node(
|
|
1322
|
+
"chart",
|
|
1323
|
+
{
|
|
1324
|
+
kind: "spark",
|
|
1325
|
+
token: "accent",
|
|
1326
|
+
series: figures.month.map(day => ({
|
|
1327
|
+
value: figures.priced ? day.cost : day.requests,
|
|
1328
|
+
title: `${shortDate(day.date)} · ${formatCost(day.cost)} · ${requestsText(day.requests)}`,
|
|
1329
|
+
})),
|
|
1330
|
+
},
|
|
1331
|
+
undefined,
|
|
1332
|
+
"spark",
|
|
1333
|
+
)
|
|
1334
|
+
: undefined;
|
|
1335
|
+
tiles.push(
|
|
1336
|
+
statTile(
|
|
1337
|
+
"month",
|
|
1338
|
+
"Last 30 days",
|
|
1339
|
+
amount(figures.monthCost, figures.monthRequests),
|
|
1340
|
+
[span(counted(figures.monthRequests), "dim")],
|
|
1341
|
+
spark ? { chart: spark } : {},
|
|
1342
|
+
),
|
|
1343
|
+
);
|
|
1344
|
+
|
|
1345
|
+
const busiest = figures.busiest;
|
|
1346
|
+
tiles.push(
|
|
1347
|
+
busiest
|
|
1348
|
+
? statTile("busiest", "Busiest day", amount(busiest.cost, busiest.requests), [
|
|
1349
|
+
span(
|
|
1350
|
+
figures.priced
|
|
1351
|
+
? `${shortDate(busiest.date)} · ${requestsText(busiest.requests)}`
|
|
1352
|
+
: `requests · ${shortDate(busiest.date)}`,
|
|
1353
|
+
"dim",
|
|
1354
|
+
),
|
|
1355
|
+
])
|
|
1356
|
+
: statTile("busiest", "Busiest day", [span("—", "dim")], [span("No activity yet", "dim")]),
|
|
1357
|
+
);
|
|
1358
|
+
return node("row", { role: "zeta.usage.stats", wrap: true }, tiles, "stats");
|
|
1101
1359
|
}
|
|
1102
1360
|
|
|
1103
|
-
|
|
1361
|
+
/**
|
|
1362
|
+
* One provider: its name, status and accounts in the left column, one row
|
|
1363
|
+
* per quota window on the right (label · meter · left · reset), aligned
|
|
1364
|
+
* with every other provider's.
|
|
1365
|
+
*/
|
|
1366
|
+
#describeProvider(entry: ProviderCard, meter: boolean): NativeNode {
|
|
1104
1367
|
const cardStatus =
|
|
1105
1368
|
entry.unavailableAccounts.length > 0
|
|
1106
1369
|
? statusWithUnavailableAccounts(entry.windows)
|
|
1107
1370
|
: entry.unlimited
|
|
1108
1371
|
? "ok"
|
|
1109
1372
|
: aggregateStatus(entry.windows);
|
|
1110
|
-
const
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1373
|
+
const who: NativeChild[] = [
|
|
1374
|
+
node(
|
|
1375
|
+
"row",
|
|
1376
|
+
{ gap: "sm", align: "center" },
|
|
1377
|
+
[
|
|
1378
|
+
statusDot(cardStatus),
|
|
1379
|
+
text([span(entry.name)], { role: "zeta.usage.provider.name", truncate: "end", title: entry.name }),
|
|
1380
|
+
],
|
|
1381
|
+
"name",
|
|
1382
|
+
),
|
|
1115
1383
|
];
|
|
1116
|
-
|
|
1117
|
-
|
|
1384
|
+
if (entry.accounts > 1) {
|
|
1385
|
+
who.push(text([span(`${entry.accounts} accounts`, "dim")], { role: "zeta.usage.provider.meta" }));
|
|
1118
1386
|
}
|
|
1119
1387
|
if (entry.resetCredits) {
|
|
1120
1388
|
const resets = entry.resetCredits;
|
|
@@ -1129,83 +1397,155 @@ export class UsageDashboardComponent implements Component {
|
|
|
1129
1397
|
details.push(`unavailable: ${sanitizeDisplayLine(resets.unavailableReasons.join(" • "))}`);
|
|
1130
1398
|
}
|
|
1131
1399
|
const label = `${resets.bankedCount} reset${resets.bankedCount === 1 ? "" : "s"} banked`;
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
"
|
|
1135
|
-
{
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
"
|
|
1144
|
-
|
|
1400
|
+
who.push(
|
|
1401
|
+
text([span(label, resets.redeemableCount > 0 ? "success" : "warning")], {
|
|
1402
|
+
role: "zeta.usage.provider.meta",
|
|
1403
|
+
title: details.length > 0 ? `${label} · ${details.join(" · ")}` : label,
|
|
1404
|
+
}),
|
|
1405
|
+
);
|
|
1406
|
+
}
|
|
1407
|
+
for (const account of entry.daybreakAccounts ?? []) {
|
|
1408
|
+
const name = sanitizeDisplayLine(account);
|
|
1409
|
+
who.push(
|
|
1410
|
+
text([span(`Daybreak · ${name}`, "success")], {
|
|
1411
|
+
role: "zeta.usage.provider.meta",
|
|
1412
|
+
truncate: "middle",
|
|
1413
|
+
title: `Daybreak access · ${name}`,
|
|
1414
|
+
}),
|
|
1145
1415
|
);
|
|
1146
1416
|
}
|
|
1147
1417
|
for (const account of entry.unavailableAccounts) {
|
|
1148
|
-
|
|
1418
|
+
const name = sanitizeDisplayLine(account);
|
|
1419
|
+
who.push(
|
|
1420
|
+
text([span(`${name}: no usage data`, "warning")], {
|
|
1421
|
+
role: "zeta.usage.provider.meta",
|
|
1422
|
+
truncate: "middle",
|
|
1423
|
+
title: `${name}: no usage data`,
|
|
1424
|
+
}),
|
|
1425
|
+
);
|
|
1149
1426
|
}
|
|
1427
|
+
|
|
1428
|
+
const rows: NativeChild[] = [];
|
|
1150
1429
|
if (entry.unlimited) {
|
|
1151
|
-
|
|
1430
|
+
rows.push(text([span("No quotas reported", "muted")], { role: "zeta.usage.none" }));
|
|
1152
1431
|
} else {
|
|
1153
1432
|
for (const [index, window] of entry.windows.slice(0, CARD_MAX_WINDOWS).entries()) {
|
|
1154
1433
|
const label: TspSpan[] = [span(sanitizeDisplayLine(window.label))];
|
|
1155
1434
|
if (window.windowTag) label.push(span(` ${sanitizeDisplayLine(window.windowTag)}`, "dim"));
|
|
1156
1435
|
const cells: NativeChild[] = [
|
|
1157
|
-
text(label, { role: "zeta.usage.label", truncate: "
|
|
1436
|
+
text(label, { role: "zeta.usage.label", truncate: "end", title: sanitizeDisplayLine(window.label) }),
|
|
1158
1437
|
];
|
|
1159
1438
|
if (window.fraction === undefined) {
|
|
1160
1439
|
cells.push(
|
|
1161
1440
|
text([span(window.usedText ?? "No data", "muted")], { role: "zeta.usage.amount", truncate: "end" }),
|
|
1162
1441
|
);
|
|
1163
1442
|
} else {
|
|
1164
|
-
const token =
|
|
1165
|
-
window.status === "exhausted" ? "error" : window.status === "warning" ? "warning" : undefined;
|
|
1166
1443
|
cells.push(
|
|
1167
1444
|
usageMeter(window.fraction, window.status, meter),
|
|
1168
|
-
text(
|
|
1445
|
+
text(leftSpans(window.fraction, window.status), { role: "zeta.usage.pct" }),
|
|
1169
1446
|
);
|
|
1170
1447
|
if (window.resetMs !== undefined) {
|
|
1171
1448
|
const reset = resetLabel(this.#nowMs, window.resetMs);
|
|
1172
1449
|
cells.push(text([span(reset.text, "dim")], { role: "zeta.usage.reset", title: reset.title }));
|
|
1173
1450
|
}
|
|
1174
1451
|
}
|
|
1175
|
-
|
|
1452
|
+
rows.push(node("row", { role: "zeta.usage.window", gap: "md", align: "center" }, cells, `w${index}`));
|
|
1176
1453
|
}
|
|
1177
1454
|
const hidden = entry.windows.length - CARD_MAX_WINDOWS;
|
|
1178
|
-
if (hidden > 0)
|
|
1455
|
+
if (hidden > 0) {
|
|
1456
|
+
rows.push(
|
|
1457
|
+
text([span(`${hidden} more in Details`, "dim")], {
|
|
1458
|
+
role: "zeta.usage.more",
|
|
1459
|
+
title: "Details lists every quota per account ⏎",
|
|
1460
|
+
}),
|
|
1461
|
+
);
|
|
1462
|
+
}
|
|
1179
1463
|
}
|
|
1180
1464
|
return node(
|
|
1181
|
-
"
|
|
1182
|
-
{ role: "zeta.usage.provider",
|
|
1183
|
-
|
|
1465
|
+
"row",
|
|
1466
|
+
{ role: "zeta.usage.provider", gap: "lg", align: "start" },
|
|
1467
|
+
[
|
|
1468
|
+
node("col", { role: "zeta.usage.provider.head", gap: "xs" }, who, "who"),
|
|
1469
|
+
node("col", { role: "zeta.usage.windows", grow: 1 }, rows, "windows"),
|
|
1470
|
+
],
|
|
1184
1471
|
entry.provider,
|
|
1185
1472
|
);
|
|
1186
1473
|
}
|
|
1187
1474
|
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1475
|
+
/**
|
|
1476
|
+
* The activity calendar: a year of daily spend as a heatmap (per-day
|
|
1477
|
+
* tooltips), then active days and the longest streak beside a Less–More
|
|
1478
|
+
* legend; loading, failure and a quiet year each say so in its place.
|
|
1479
|
+
*/
|
|
1480
|
+
#describeActivity(
|
|
1481
|
+
chart: boolean,
|
|
1482
|
+
layout: HeatmapLayout | undefined,
|
|
1483
|
+
figures: ActivityFigures | undefined,
|
|
1484
|
+
): NativeNode {
|
|
1485
|
+
const meta: NativeChild[] = [];
|
|
1486
|
+
if (this.#syncing && this.#activity) {
|
|
1487
|
+
meta.push(node("spinner", { style: "dots", label: [span("Syncing", "dim")] }));
|
|
1488
|
+
} else if (layout) {
|
|
1489
|
+
const today = new Date();
|
|
1490
|
+
const range = `${layout.start.getDate()} ${MONTH_NAMES[layout.start.getMonth()]} ${layout.start.getFullYear()} – ${today.getDate()} ${MONTH_NAMES[today.getMonth()]} ${today.getFullYear()}`;
|
|
1491
|
+
meta.push(text([span(range, "dim")], { role: "zeta.usage.section.meta" }));
|
|
1492
|
+
}
|
|
1493
|
+
const children: NativeChild[] = [sectionHead("Activity", meta)];
|
|
1192
1494
|
const points = this.#activity;
|
|
1193
1495
|
if (this.#activityError) {
|
|
1194
1496
|
const detail = formatActivityErrorDetail(this.#activityError);
|
|
1195
1497
|
children.push(
|
|
1196
|
-
|
|
1498
|
+
text([span(detail ? `Usage history unavailable (${detail}).` : "Usage history unavailable.", "muted")], {
|
|
1499
|
+
role: "zeta.usage.none",
|
|
1500
|
+
wrap: "word",
|
|
1501
|
+
}),
|
|
1502
|
+
);
|
|
1503
|
+
} else if (!points || !layout || !figures) {
|
|
1504
|
+
children.push(
|
|
1505
|
+
node("spinner", { style: "dots", label: [span("Loading usage history…", "muted")] }, undefined, "loading"),
|
|
1197
1506
|
);
|
|
1198
|
-
} else if (!points) {
|
|
1199
|
-
children.push(node("spinner", { label: [span("Loading usage history…", "muted")] }));
|
|
1200
1507
|
} else {
|
|
1201
|
-
const layout = buildHeatmapLayout(points, NATIVE_HEATMAP_WEEKS);
|
|
1202
1508
|
const summary =
|
|
1203
1509
|
layout.totalRequests > 0 || layout.totalCost > 0
|
|
1204
1510
|
? `${formatActivityTotals(layout)} · last ${NATIVE_HEATMAP_WEEKS} weeks`
|
|
1205
1511
|
: `No activity in the last ${NATIVE_HEATMAP_WEEKS} weeks`;
|
|
1206
1512
|
children.push(chart ? this.#heatmapChart(layout, points, summary) : this.#heatmapTable(layout, summary));
|
|
1513
|
+
const foot: NativeChild[] = [
|
|
1514
|
+
text(
|
|
1515
|
+
[
|
|
1516
|
+
span(
|
|
1517
|
+
figures.activeDays > 0
|
|
1518
|
+
? `${figures.activeDays} active day${figures.activeDays === 1 ? "" : "s"} · longest streak ${figures.longestStreak} day${figures.longestStreak === 1 ? "" : "s"}`
|
|
1519
|
+
: `No activity in the last ${NATIVE_HEATMAP_WEEKS} weeks`,
|
|
1520
|
+
"dim",
|
|
1521
|
+
),
|
|
1522
|
+
],
|
|
1523
|
+
{ truncate: "end" },
|
|
1524
|
+
),
|
|
1525
|
+
];
|
|
1526
|
+
if (chart) {
|
|
1527
|
+
foot.push(
|
|
1528
|
+
node("spacer", { grow: 1 }),
|
|
1529
|
+
node(
|
|
1530
|
+
"row",
|
|
1531
|
+
{ role: "zeta.usage.legend", gap: "sm", align: "center" },
|
|
1532
|
+
[
|
|
1533
|
+
text([span("Less", "dim")]),
|
|
1534
|
+
node(
|
|
1535
|
+
"chart",
|
|
1536
|
+
{ kind: "heatmap", cells: [[0, 0.25, 0.5, 0.75, 1]], token: "accent", size: "md" },
|
|
1537
|
+
undefined,
|
|
1538
|
+
"scale",
|
|
1539
|
+
),
|
|
1540
|
+
text([span("More", "dim")]),
|
|
1541
|
+
],
|
|
1542
|
+
"legend",
|
|
1543
|
+
),
|
|
1544
|
+
);
|
|
1545
|
+
}
|
|
1546
|
+
children.push(node("row", { role: "zeta.usage.activity.foot", gap: "md", align: "center" }, foot, "foot"));
|
|
1207
1547
|
}
|
|
1208
|
-
return node("col", { role: "zeta.usage.activity"
|
|
1548
|
+
return node("col", { role: "zeta.usage.activity" }, children, "activity");
|
|
1209
1549
|
}
|
|
1210
1550
|
|
|
1211
1551
|
/** The heatmap as a `chart`: 0–1 intensities, month columns, M/W/F rows, per-day tooltips. */
|
|
@@ -1260,7 +1600,11 @@ export class UsageDashboardComponent implements Component {
|
|
|
1260
1600
|
);
|
|
1261
1601
|
}
|
|
1262
1602
|
|
|
1263
|
-
/**
|
|
1603
|
+
/**
|
|
1604
|
+
* Per provider: a head like the overview's provider column, notes and
|
|
1605
|
+
* saved resets as a `kv`, the limit buckets as a `table` (one row per
|
|
1606
|
+
* account).
|
|
1607
|
+
*/
|
|
1264
1608
|
#describeDetail(): NativeChild[] {
|
|
1265
1609
|
const nowMs = this.#nowMs;
|
|
1266
1610
|
const grouped = new Map<string, UsageReport[]>();
|
|
@@ -1273,7 +1617,22 @@ export class UsageDashboardComponent implements Component {
|
|
|
1273
1617
|
const sections: NativeChild[] = [];
|
|
1274
1618
|
for (const entry of this.#cards) {
|
|
1275
1619
|
const reports = grouped.get(entry.provider) ?? [];
|
|
1276
|
-
const
|
|
1620
|
+
const cardStatus =
|
|
1621
|
+
entry.unavailableAccounts.length > 0
|
|
1622
|
+
? statusWithUnavailableAccounts(entry.windows)
|
|
1623
|
+
: entry.unlimited
|
|
1624
|
+
? "ok"
|
|
1625
|
+
: aggregateStatus(entry.windows);
|
|
1626
|
+
const head: NativeChild[] = [
|
|
1627
|
+
statusDot(cardStatus),
|
|
1628
|
+
text([span(entry.name)], { role: "zeta.usage.provider.name", truncate: "end" }),
|
|
1629
|
+
];
|
|
1630
|
+
if (entry.accounts > 1) {
|
|
1631
|
+
head.push(text([span(`${entry.accounts} accounts`, "dim")], { role: "zeta.usage.provider.meta" }));
|
|
1632
|
+
}
|
|
1633
|
+
const children: NativeChild[] = [
|
|
1634
|
+
node("row", { role: "zeta.usage.report.head", gap: "sm", align: "baseline" }, head, "head"),
|
|
1635
|
+
];
|
|
1277
1636
|
for (const account of unavailable) {
|
|
1278
1637
|
if (account.provider !== entry.provider) continue;
|
|
1279
1638
|
children.push(mutedText(`${sanitizeDisplayLine(account.label)}: no usage data`, true));
|
|
@@ -1312,7 +1671,9 @@ export class UsageDashboardComponent implements Component {
|
|
|
1312
1671
|
v: value,
|
|
1313
1672
|
});
|
|
1314
1673
|
}
|
|
1315
|
-
if (facts.length > 0)
|
|
1674
|
+
if (facts.length > 0) {
|
|
1675
|
+
children.push(node("kv", { items: facts, layout: "grid", role: "zeta.usage.facts" }, undefined, "facts"));
|
|
1676
|
+
}
|
|
1316
1677
|
|
|
1317
1678
|
const buckets = new Map<string, { label: string; window?: string; rows: Record<string, TspText>[] }>();
|
|
1318
1679
|
const limitNotes: string[] = [];
|
|
@@ -1326,15 +1687,13 @@ export class UsageDashboardComponent implements Component {
|
|
|
1326
1687
|
buckets.set(key, bucket);
|
|
1327
1688
|
}
|
|
1328
1689
|
const fraction = resolveUsedFraction(limit);
|
|
1329
|
-
const token =
|
|
1330
|
-
limit.status === "exhausted" ? "error" : limit.status === "warning" ? "warning" : undefined;
|
|
1331
1690
|
const resetsAt = limit.window?.resetsAt;
|
|
1332
1691
|
bucket.rows.push({
|
|
1333
1692
|
account: [span(reportAccountLabel(report, limit, index), "muted")],
|
|
1334
1693
|
left:
|
|
1335
1694
|
fraction === undefined
|
|
1336
1695
|
? [span(formatAbsoluteOnlyAmount([limit]) ?? "No data", "muted")]
|
|
1337
|
-
:
|
|
1696
|
+
: leftSpans(fraction, limit.status),
|
|
1338
1697
|
reset:
|
|
1339
1698
|
resetsAt !== undefined && resetsAt > nowMs
|
|
1340
1699
|
? [
|
|
@@ -1365,21 +1724,16 @@ export class UsageDashboardComponent implements Component {
|
|
|
1365
1724
|
{ id: "left", head: "Left", align: "end", priority: 3 },
|
|
1366
1725
|
{ id: "reset", head: "Resets", align: "end", priority: 2 },
|
|
1367
1726
|
];
|
|
1368
|
-
children.push(node("table", { cols, rows }, undefined, "limits"));
|
|
1727
|
+
children.push(node("table", { cols, rows, role: "zeta.usage.limits" }, undefined, "limits"));
|
|
1369
1728
|
for (const note of new Set(limitNotes)) children.push(mutedText(note, true));
|
|
1370
1729
|
} else if (unavailable.every(account => account.provider !== entry.provider)) {
|
|
1371
1730
|
children.push(mutedText("No quotas reported"));
|
|
1372
1731
|
}
|
|
1373
|
-
sections.push(
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
[col(children, { gap: "sm" })],
|
|
1378
|
-
entry.provider,
|
|
1379
|
-
),
|
|
1380
|
-
);
|
|
1732
|
+
sections.push(node("col", { role: "zeta.usage.report", gap: "sm" }, children, entry.provider));
|
|
1733
|
+
}
|
|
1734
|
+
if (sections.length === 0) {
|
|
1735
|
+
sections.push(text([span("No usage data available.", "muted")], { role: "zeta.usage.none" }));
|
|
1381
1736
|
}
|
|
1382
|
-
if (sections.length === 0) sections.push(mutedText("No usage data available."));
|
|
1383
1737
|
return sections;
|
|
1384
1738
|
}
|
|
1385
1739
|
|