@worca/app 1.3.0 → 1.4.0-rc.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/README.md +85 -6
- package/agents/clarify.meta.json +1 -0
- package/agents/memoryDefragmenter.meta.json +2 -1
- package/agents/reviewer.meta.json +60 -0
- package/agents/worca-cc-code-reviewer.md +33 -0
- package/agents/worca-cc-memory-defragmenter.md +5 -3
- package/agents/workspaceScanner.meta.json +1 -0
- package/package.json +14 -10
- package/scripts/git-diff.mjs +25 -0
- package/scripts/gitDiff.meta.json +18 -0
- package/scripts/js-inline.mjs +11 -0
- package/scripts/js.meta.json +22 -0
- package/scripts/py-inline.py +27 -0
- package/scripts/py.meta.json +22 -0
- package/scripts/shell.meta.json +24 -0
- package/skills/worca/SKILL.md +3 -2
- package/src/cli/models.mjs +247 -0
- package/src/cli/render.mjs +72 -4
- package/src/cli/schedule.mjs +494 -0
- package/src/cli/worca-cc.mjs +1001 -22
- package/src/core/agent-registry.mjs +75 -23
- package/src/core/agent-store.mjs +51 -2
- package/src/core/artifacts.mjs +73 -10
- package/src/core/ask/events.mjs +119 -1
- package/src/core/ask/limits.mjs +32 -4
- package/src/core/ask/mcp-stdio.mjs +12 -0
- package/src/core/ask/model-deps.mjs +126 -0
- package/src/core/ask/model-proposal.mjs +370 -0
- package/src/core/ask/models.mjs +12 -0
- package/src/core/ask/policy-deps.mjs +124 -0
- package/src/core/ask/policy-proposal.mjs +363 -0
- package/src/core/ask/prompt.mjs +74 -9
- package/src/core/ask/proposal.mjs +54 -5
- package/src/core/ask/schedule-deps.mjs +83 -0
- package/src/core/ask/schedule-spec.mjs +310 -0
- package/src/core/ask/script-deps.mjs +357 -0
- package/src/core/ask/source-deps.mjs +52 -0
- package/src/core/ask/source-spec.mjs +157 -0
- package/src/core/ask/spawn.mjs +1 -0
- package/src/core/ask/store.mjs +6 -3
- package/src/core/ask/tool-deps.mjs +4 -0
- package/src/core/ask/tools.mjs +657 -37
- package/src/core/ask/turn.mjs +109 -2
- package/src/core/ask-files.mjs +406 -0
- package/src/core/ask-forms.mjs +195 -0
- package/src/core/ask-projection.mjs +72 -0
- package/src/core/bridge/errors.mjs +84 -0
- package/src/core/bridge/provider-ops.mjs +281 -0
- package/src/core/bridge/providers/copilot.mjs +269 -0
- package/src/core/bridge/providers/endpoint.mjs +257 -0
- package/src/core/bridge/registry.mjs +88 -0
- package/src/core/bridge/semaphore.mjs +73 -0
- package/src/core/bridge/server.mjs +184 -0
- package/src/core/bridge/telemetry.mjs +53 -0
- package/src/core/bridge/translate/request.mjs +252 -0
- package/src/core/bridge/translate/response.mjs +82 -0
- package/src/core/bridge/translate/stream.mjs +242 -0
- package/src/core/bridge/upstream.mjs +209 -0
- package/src/core/chat/command-router.mjs +58 -4
- package/src/core/chat/notifier.mjs +14 -1
- package/src/core/chat/renderers.mjs +35 -0
- package/src/core/claude-runner.mjs +126 -19
- package/src/core/config.mjs +212 -31
- package/src/core/cost-budget.mjs +3 -2
- package/src/core/db.mjs +169 -15
- package/src/core/failure-policy.mjs +10 -0
- package/src/core/fs-browse.mjs +16 -4
- package/src/core/git-info.mjs +22 -0
- package/src/core/graph/builtin-workflows.mjs +3 -1
- package/src/core/graph/exec-io.mjs +71 -0
- package/src/core/graph/executor.mjs +139 -70
- package/src/core/graph/human-evidence.mjs +131 -0
- package/src/core/graph/python-probe.mjs +172 -0
- package/src/core/graph/registry-ports.mjs +10 -6
- package/src/core/graph/scheduler.mjs +39 -24
- package/src/core/graph/script-child.mjs +81 -0
- package/src/core/graph/script-runner.mjs +597 -0
- package/src/core/graph/worca_script.py +207 -0
- package/src/core/guardrail-store.mjs +16 -0
- package/src/core/human-backfill.mjs +108 -0
- package/src/core/human-rate.mjs +17 -0
- package/src/core/index-html.mjs +6 -2
- package/src/core/memory-defrag-model.mjs +112 -0
- package/src/core/memory-store.mjs +70 -18
- package/src/core/memory-sync.mjs +22 -11
- package/src/core/metrics/read.mjs +4 -1
- package/src/core/metrics/record.mjs +50 -2
- package/src/core/metrics/sync.mjs +6 -4
- package/src/core/model-env.mjs +149 -0
- package/src/core/model-test.mjs +14 -1
- package/src/core/notifications.mjs +128 -0
- package/src/core/onboarding.mjs +8 -2
- package/src/core/orchestrator.mjs +357 -26
- package/src/core/phases.mjs +95 -6
- package/src/core/plugin-api.mjs +24 -7
- package/src/core/plugin-manifest.mjs +184 -18
- package/src/core/plugin-models.mjs +1 -0
- package/src/core/plugin-script-cases.mjs +118 -0
- package/src/core/plugin-store.mjs +163 -17
- package/src/core/plugin-workflows.mjs +71 -17
- package/src/core/policy/cache.mjs +116 -0
- package/src/core/policy/effective.mjs +175 -0
- package/src/core/policy/gate.mjs +91 -0
- package/src/core/policy/local.mjs +145 -0
- package/src/core/policy/registry.mjs +330 -0
- package/src/core/policy/scope.mjs +61 -0
- package/src/core/policy/state.mjs +79 -0
- package/src/core/policy/sync.mjs +513 -0
- package/src/core/protocol.mjs +43 -0
- package/src/core/run-harness.mjs +421 -72
- package/src/core/scheduler.mjs +980 -0
- package/src/core/script-bench.mjs +628 -0
- package/src/core/script-registry.mjs +116 -0
- package/src/core/script-store.mjs +563 -0
- package/src/core/settings.mjs +489 -14
- package/src/core/stats.mjs +33 -2
- package/src/core/workflow-export.mjs +94 -3
- package/src/core/workflow-share.mjs +67 -18
- package/src/core/workflows.mjs +47 -11
- package/src/core/workspaces.mjs +18 -12
- package/src/shared/forms/answer.mjs +164 -0
- package/src/shared/forms/catalog.mjs +91 -0
- package/src/shared/forms/form-def.mjs +290 -0
- package/src/shared/forms/layout.mjs +67 -0
- package/src/shared/forms/paths.mjs +47 -0
- package/src/shared/forms/project.mjs +309 -0
- package/src/shared/forms/schema.mjs +205 -0
- package/src/shared/graph/agent-meta.mjs +55 -5
- package/src/shared/graph/constants.mjs +14 -2
- package/src/shared/graph/flow-layout.mjs +2 -1
- package/src/shared/graph/isomorphic.mjs +5 -3
- package/src/shared/graph/manifest.mjs +22 -13
- package/src/shared/graph/ports.mjs +45 -19
- package/src/shared/graph/script-cases.mjs +257 -0
- package/src/shared/graph/script-icons.mjs +46 -0
- package/src/shared/graph/script-infer.mjs +259 -0
- package/src/shared/graph/script-meta.mjs +408 -0
- package/src/shared/graph/script-templates.mjs +201 -0
- package/src/shared/graph/template.mjs +4 -4
- package/src/shared/graph/validate.mjs +89 -16
- package/src/shared/human-estimate.mjs +100 -0
- package/src/shared/schedule/recurrence.mjs +353 -0
- package/src/shared/team-metrics/aggregate.mjs +51 -11
- package/{scripts → tools}/install.mjs +3 -3
- package/ui/public/app.js +4390 -683
- package/ui/public/artifact-picker.mjs +189 -0
- package/ui/public/ask/dom.mjs +121 -0
- package/ui/public/ask/form-preview.mjs +55 -0
- package/ui/public/ask/form-renderer.mjs +250 -0
- package/ui/public/ask/registry.mjs +53 -0
- package/ui/public/ask/widgets-display.mjs +370 -0
- package/ui/public/ask/widgets-input.mjs +624 -0
- package/ui/public/ask/widgets-layout.mjs +90 -0
- package/ui/public/ask-panel.mjs +401 -27
- package/ui/public/ask-run-card.mjs +1 -1
- package/ui/public/bridge-view.mjs +694 -0
- package/ui/public/chat-settings-view.mjs +24 -0
- package/ui/public/code-editor.mjs +181 -0
- package/ui/public/getting-started.mjs +34 -7
- package/ui/public/graph/composer.mjs +138 -10
- package/ui/public/graph/inspector.mjs +61 -58
- package/ui/public/graph/palette.mjs +27 -9
- package/ui/public/graph/run-decor.mjs +34 -17
- package/ui/public/graph/run-hosts.mjs +7 -1
- package/ui/public/graph/save-dialog.mjs +3 -0
- package/ui/public/graph/view.mjs +23 -7
- package/ui/public/guardrails-view.mjs +15 -3
- package/ui/public/guide-spot.mjs +87 -9
- package/ui/public/index.html +480 -141
- package/ui/public/memory-view.mjs +22 -4
- package/ui/public/models-view.mjs +162 -17
- package/ui/public/node-tunables.mjs +33 -4
- package/ui/public/plugins-view.mjs +23 -1
- package/ui/public/results-view.mjs +4 -2
- package/ui/public/schedule-sheet.mjs +430 -0
- package/ui/public/schedules-view.mjs +432 -0
- package/ui/public/script-bench-view.mjs +1154 -0
- package/ui/public/script-forms.mjs +282 -0
- package/ui/public/script-wizard.mjs +529 -0
- package/ui/public/scripts-view.mjs +868 -0
- package/ui/public/stats-view.mjs +159 -52
- package/ui/public/style.css +1461 -44
- package/ui/public/team-metrics-surfaces.mjs +77 -16
- package/ui/public/team-metrics-view.mjs +68 -5
- package/ui/public/team-policy-view.mjs +1402 -0
- package/ui/public/ui-level.mjs +237 -0
- package/ui/server.mjs +1827 -73
package/ui/public/stats-view.mjs
CHANGED
|
@@ -6,9 +6,11 @@
|
|
|
6
6
|
// tree. app.js owns endpoint calls and mounting; node:test drives these via
|
|
7
7
|
// jsdom. Interactive elements carry data-* + routing classes (cb-override,
|
|
8
8
|
// cb-settings, ch-hit, data-nav) so app.js wires delegated listeners.
|
|
9
|
-
// Formatters are injected via opts.fmt = { usd, usd4, duration, estTitle };
|
|
9
|
+
// Formatters are injected via opts.fmt = { usd, usd4, duration, hours, estTitle };
|
|
10
10
|
// DEFAULT_FMT keeps pure tests standalone.
|
|
11
11
|
|
|
12
|
+
import { renderTeamCapPauseBanner, POLICY_PAUSE_REASONS } from './team-policy-view.mjs';
|
|
13
|
+
|
|
12
14
|
const SVG_NS = 'http://www.w3.org/2000/svg';
|
|
13
15
|
|
|
14
16
|
/** Spend meter turns amber at 80% of the total limit (display-only). */
|
|
@@ -17,9 +19,13 @@ export const BUDGET_WARN_AT = 0.8;
|
|
|
17
19
|
const WD = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
|
18
20
|
const MO = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
19
21
|
|
|
22
|
+
// en-US grouping ($10,456.12), the same shape Team metrics prints (TM_FMT.usd); app.js's fmtUsd matches.
|
|
23
|
+
const USD_2DP = { minimumFractionDigits: 2, maximumFractionDigits: 2 };
|
|
24
|
+
const USD_4DP = { minimumFractionDigits: 4, maximumFractionDigits: 4 };
|
|
25
|
+
|
|
20
26
|
export const DEFAULT_FMT = {
|
|
21
|
-
usd: (n) => `$${(Math.round(((n || 0) + Number.EPSILON) * 100) / 100).
|
|
22
|
-
usd4: (n) => `$${(n || 0).
|
|
27
|
+
usd: (n) => `$${(Math.round(((n || 0) + Number.EPSILON) * 100) / 100).toLocaleString('en-US', USD_2DP)}`,
|
|
28
|
+
usd4: (n) => `$${(n || 0).toLocaleString('en-US', USD_4DP)}`,
|
|
23
29
|
duration: (ms) => {
|
|
24
30
|
const s = Math.floor((ms || 0) / 1000);
|
|
25
31
|
if (s < 60) return `${s}s`;
|
|
@@ -27,7 +33,11 @@ export const DEFAULT_FMT = {
|
|
|
27
33
|
if (m < 60) return `${m}m ${s % 60}s`;
|
|
28
34
|
return `${Math.floor(m / 60)}h ${m % 60}m`;
|
|
29
35
|
},
|
|
30
|
-
|
|
36
|
+
hours: (h) => {
|
|
37
|
+
const v = Math.round(((h || 0) + Number.EPSILON) * 10) / 10;
|
|
38
|
+
return `${v.toLocaleString('en-US', { maximumFractionDigits: 1 })} h`;
|
|
39
|
+
},
|
|
40
|
+
estTitle: (n) => `Estimated cost $${(n || 0).toLocaleString('en-US', USD_4DP)} — Claude Code client-side estimate (total_cost_usd), not authoritative billing`,
|
|
31
41
|
};
|
|
32
42
|
|
|
33
43
|
function h(doc, tag, cls, text) {
|
|
@@ -60,6 +70,8 @@ const ICONS = {
|
|
|
60
70
|
ask: 'M12 19.6l-3.2-2.8H6.4A3.4 3.4 0 0 1 3 13.4V7.8a3.4 3.4 0 0 1 3.4-3.4h11.2A3.4 3.4 0 0 1 21 7.8v5.6a3.4 3.4 0 0 1-3.4 3.4h-2.4L12 19.6Z',
|
|
61
71
|
time: 'M12 8v4l3 2M12 20a8 8 0 1 1 0-16 8 8 0 0 1 0 16Z',
|
|
62
72
|
finished: 'M20 7 9 18l-5-5',
|
|
73
|
+
// Money bag resting on an open hand (not a board glyph): bag tie, knot, body; thumb, fingers, cuff.
|
|
74
|
+
saved: 'M9.6 4.6h3.8 M9.9 4.6c.3-1.5 2.9-1.5 3.2 0 M9.6 4.6C7.8 6.2 6.8 7.8 6.8 9.4c0 1.6.6 2.9 1.4 3.8h6.6c.8-.9 1.4-2.2 1.4-3.8 0-1.6-1-3.2-2.8-4.8 M8.2 13.2c-.5 0-1 .2-1.3.6L3 17.6 M7 21l1.6-1.4c.3-.4.8-.6 1.4-.6h4c1.1 0 2.1-.4 2.8-1.2l4.6-4.4a2 2 0 0 0-2.75-2.91L14.8 13.2 M2 16.6l6 6',
|
|
63
75
|
prs: 'M6 8.6v6.8M18 15.4V11a4 4 0 0 0-4-4h-2M6 3.4a2.6 2.6 0 1 1 0 5.2 2.6 2.6 0 0 1 0-5.2ZM6 15.4a2.6 2.6 0 1 1 0 5.2 2.6 2.6 0 0 1 0-5.2ZM18 15.4a2.6 2.6 0 1 1 0 5.2 2.6 2.6 0 0 1 0-5.2Z',
|
|
64
76
|
};
|
|
65
77
|
|
|
@@ -87,6 +99,19 @@ function deltaChip(doc, cur, prevVal, range) {
|
|
|
87
99
|
return chip;
|
|
88
100
|
}
|
|
89
101
|
|
|
102
|
+
/** "9.0× spend" pill: Saved ÷ spent over the same window. Null unless both are positive — a
|
|
103
|
+
* loss or a free window has no multiplier worth printing. One decimal below 10×, a whole
|
|
104
|
+
* number from 10× up (rounded to the decimal band first, so 9.96 prints "10×", never "10.0×"). */
|
|
105
|
+
export function savedMultChip(doc, saved, spent) {
|
|
106
|
+
if (!(saved > 0) || !(spent > 0)) return null;
|
|
107
|
+
const ratio = saved / spent;
|
|
108
|
+
const tenth = Math.round(ratio * 10) / 10;
|
|
109
|
+
const text = tenth >= 10 ? Math.round(ratio).toLocaleString('en-US') : tenth.toFixed(1);
|
|
110
|
+
const chip = h(doc, 'span', 'stat-mult', `${text}× spend`);
|
|
111
|
+
chip.title = 'Saved ÷ spent in this period';
|
|
112
|
+
return chip;
|
|
113
|
+
}
|
|
114
|
+
|
|
90
115
|
// Numeric tokens in tile sub-lines ($50.00, 3d 4h, bare counts) get <b> so the
|
|
91
116
|
// figures read at a glance; surrounding prose stays plain. Unit groups like
|
|
92
117
|
// "3d 4h" bold as one token.
|
|
@@ -162,6 +187,24 @@ export function renderKpiRow(model, { doc = globalThis.document, fmt = DEFAULT_F
|
|
|
162
187
|
title: fmt.estTitle(totals.spentUsd),
|
|
163
188
|
}));
|
|
164
189
|
|
|
190
|
+
// Saved (money-saved design §10): hours × rate − spent. Negative is a real outcome. No
|
|
191
|
+
// delta pill here: the "× spend" multiplier is the tile's one pill (a week-over-week swing in
|
|
192
|
+
// a derived figure read as noise next to it — dropped 2026-09-22).
|
|
193
|
+
const fmtHours = fmt.hours || DEFAULT_FMT.hours;
|
|
194
|
+
const saved = Number(totals.savedUsd || 0);
|
|
195
|
+
const savedTile = tile(doc, {
|
|
196
|
+
iconD: ICONS.saved, label: 'Saved',
|
|
197
|
+
chip: null,
|
|
198
|
+
valueNodes: [doc.createTextNode(`${saved < 0 ? '−' : ''}${fmt.usd(Math.abs(saved))}`)],
|
|
199
|
+
sub: `≈ ${fmtHours(totals.humanHours)} of human work`,
|
|
200
|
+
});
|
|
201
|
+
savedTile.querySelector('.stat-value').classList.toggle('is-neg', saved < 0);
|
|
202
|
+
savedTile.querySelector('.stat-value').classList.toggle('is-pos', saved > 0);
|
|
203
|
+
// Far right of the label row: how many times over the spend paid for itself.
|
|
204
|
+
const mult = savedMultChip(doc, saved, totals.spentUsd);
|
|
205
|
+
if (mult) savedTile.querySelector('.stat-label').appendChild(mult);
|
|
206
|
+
row.appendChild(savedTile);
|
|
207
|
+
|
|
165
208
|
// Pipeline spend (D7): pipeline-only money; sub = share of the combined
|
|
166
209
|
// total. Falls back to spentUsd when the payload predates the ask split.
|
|
167
210
|
const pipeSpend = totals.pipelineSpendUsd ?? totals.spentUsd;
|
|
@@ -221,80 +264,141 @@ export function renderKpiRow(model, { doc = globalThis.document, fmt = DEFAULT_F
|
|
|
221
264
|
return row;
|
|
222
265
|
}
|
|
223
266
|
|
|
224
|
-
/**
|
|
267
|
+
/** The payload's "Saved this <period>" figure, or null when it has none: /api/budget
|
|
268
|
+
* degrades it to null when the savings read fails, and a server that predates the
|
|
269
|
+
* field sends nothing — either way the indicator shows Spent alone rather than "$0". */
|
|
270
|
+
function windowSaved(b) {
|
|
271
|
+
return typeof b.windowSavedUsd === 'number' && Number.isFinite(b.windowSavedUsd)
|
|
272
|
+
? b.windowSavedUsd : null;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/** Signed money in the Statistics Saved tile's own shape: "−$12.50", never "$-12.50". */
|
|
276
|
+
const signedUsd = (fmt, n) => `${n < 0 ? '−' : ''}${fmt.usd(Math.abs(n))}`;
|
|
277
|
+
|
|
278
|
+
/** Where "Saved" comes from, appended to both sidebar titles when the figure shows. */
|
|
279
|
+
const SAVED_NOTE = 'Saved = estimated human hours × your rate − spent';
|
|
280
|
+
|
|
281
|
+
function indRow(doc, label, amount) {
|
|
282
|
+
const row = h(doc, 'span', 'spend-ind-row');
|
|
283
|
+
row.appendChild(h(doc, 'span', 'spend-ind-label', label));
|
|
284
|
+
row.appendChild(h(doc, 'span', 'spend-ind-amt mono', amount));
|
|
285
|
+
return row;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/** Sidebar spend indicator (whole block navigates to #stats). With a total limit: Spent +
|
|
289
|
+
* meter. Without one there is no meter to show, so the second row states what the period
|
|
290
|
+
* saved instead — the old "no total limit" note said only what was missing. */
|
|
225
291
|
export function renderBudgetIndicator(budget, { doc = globalThis.document, fmt = DEFAULT_FMT } = {}) {
|
|
226
292
|
const b = budget || {};
|
|
227
293
|
const btn = h(doc, 'button', 'spend-ind');
|
|
228
294
|
btn.type = 'button';
|
|
229
295
|
btn.dataset.nav = 'stats';
|
|
230
|
-
const
|
|
296
|
+
const hasLimit = b.totalLimitUsd != null;
|
|
297
|
+
const ratio = hasLimit ? b.windowSpendUsd / b.totalLimitUsd : 0;
|
|
298
|
+
const saved = hasLimit ? null : windowSaved(b);
|
|
231
299
|
if (b.blocked) btn.classList.add('over');
|
|
232
|
-
else if (
|
|
300
|
+
else if (hasLimit && ratio >= BUDGET_WARN_AT) btn.classList.add('warn');
|
|
233
301
|
btn.title = `Estimated spend this ${periodWord(b)}: ${fmt.usd4(b.windowSpendUsd)}` +
|
|
234
|
-
(
|
|
235
|
-
` · resets ${fmtResetAt(b.windowEndMs)} — Claude Code client-side estimate (total_cost_usd), not authoritative billing
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
btn.appendChild(rowEl);
|
|
240
|
-
if (b.totalLimitUsd != null) {
|
|
302
|
+
(hasLimit ? ` of ${fmt.usd(b.totalLimitUsd)}` : '') +
|
|
303
|
+
` · resets ${fmtResetAt(b.windowEndMs)} — Claude Code client-side estimate (total_cost_usd), not authoritative billing` +
|
|
304
|
+
(saved != null ? `. Saved this ${periodWord(b)}: ${signedUsd(fmt, saved)} (${SAVED_NOTE})` : '');
|
|
305
|
+
btn.appendChild(indRow(doc, `Spent this ${periodWord(b)}`, fmt.usd(b.windowSpendUsd)));
|
|
306
|
+
if (hasLimit) {
|
|
241
307
|
btn.appendChild(meterEl(doc, 'spend-ind-meter', b.blocked ? 100 : ratio * 100));
|
|
242
308
|
if (b.blocked) btn.appendChild(h(doc, 'small', 'spend-ind-sub', 'limit reached · new runs blocked'));
|
|
243
|
-
} else {
|
|
244
|
-
|
|
309
|
+
} else if (saved != null) {
|
|
310
|
+
// A gain is green (.pos → --green-ink-strong, which clears verify:theme's 4.5:1 on the
|
|
311
|
+
// card's hover fill). A loss stays neutral ink — --red-ink is 4.07:1 there — and its
|
|
312
|
+
// "−" sign carries it.
|
|
313
|
+
const row = indRow(doc, `Saved this ${periodWord(b)}`, signedUsd(fmt, saved));
|
|
314
|
+
row.classList.add('spend-ind-saved');
|
|
315
|
+
if (saved >= 0) row.classList.add('pos');
|
|
316
|
+
btn.appendChild(row);
|
|
245
317
|
}
|
|
246
318
|
return btn;
|
|
247
319
|
}
|
|
248
320
|
|
|
249
|
-
/** Compact
|
|
250
|
-
*
|
|
251
|
-
*
|
|
252
|
-
*
|
|
253
|
-
*
|
|
254
|
-
*
|
|
255
|
-
*
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
321
|
+
/** Compact signed money for the collapsed rail's 40px stack:
|
|
322
|
+
* $4 · $317 · $1.2k · $9.9k · $11k · $999k · $1.2M · $12M, with "−" before any of them.
|
|
323
|
+
* At most five glyphs unsigned, six signed ("−$8.8k" ≈ 36px at 10px mono, inside the
|
|
324
|
+
* stack's 38px). Every tier is decided on the ROUNDED value, so 999.5 is "$1k" (not
|
|
325
|
+
* "$1000"), 9,950 is "$10k" (not "$10.0k") and 999,500 is "$1M" (not "$1000k").
|
|
326
|
+
* NAME: `railUsd` — this module already declares `compactUsd(fmt, v)` (the spend-chart
|
|
327
|
+
* y-axis formatter), and a second top-level declaration of one name in an ES module is
|
|
328
|
+
* a fatal SyntaxError that takes down stats-view.mjs, app.js and the whole UI. */
|
|
329
|
+
export function railUsd(n) {
|
|
330
|
+
const v = Number(n) || 0;
|
|
331
|
+
const a = Math.abs(v);
|
|
332
|
+
let body;
|
|
333
|
+
if (Math.round(a) < 1000) body = `$${Math.round(a)}`;
|
|
334
|
+
else if (Math.round(a / 100) / 10 < 10) body = `$${Math.round(a / 100) / 10}k`;
|
|
335
|
+
else if (Math.round(a / 1000) < 1000) body = `$${Math.round(a / 1000)}k`;
|
|
336
|
+
else if (Math.round(a / 1e5) / 10 < 10) body = `$${Math.round(a / 1e5) / 10}M`;
|
|
337
|
+
else body = `$${Math.round(a / 1e6)}M`;
|
|
338
|
+
return v < 0 && body !== '$0' ? `−${body}` : body;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function stackPair(doc, label, value) {
|
|
342
|
+
const pair = h(doc, 'span', 'spend-stack-pair');
|
|
343
|
+
pair.appendChild(h(doc, 'span', 'spend-stack-lbl', label));
|
|
344
|
+
pair.appendChild(h(doc, 'span', 'spend-stack-val', value));
|
|
345
|
+
return pair;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/** Collapsed-rail Spent/Saved stack — what the 76px rail shows while NO total limit is
|
|
349
|
+
* set. A ring needs a denominator; without one it could only print a bare amount in a
|
|
350
|
+
* disc, with nothing saying what the amount was. The stack drops the disc and states both
|
|
351
|
+
* figures, a small caps label over a compact amount, in a 40px column: the width of every
|
|
352
|
+
* other rail square, so it overhangs the 39px content box exactly as they do and nothing
|
|
353
|
+
* clips. Exact figures live in the title and the accessible name. Keeps `.spend-ind` and
|
|
354
|
+
* data-nav="stats" so app.js's closest('.spend-ind') click routing still reaches it. */
|
|
355
|
+
export function renderBudgetStack(budget, { doc = globalThis.document, fmt = DEFAULT_FMT } = {}) {
|
|
356
|
+
const b = budget || {};
|
|
357
|
+
const word = periodWord(b);
|
|
358
|
+
const saved = windowSaved(b);
|
|
359
|
+
const btn = h(doc, 'button', 'spend-ind spend-stack');
|
|
360
|
+
btn.type = 'button';
|
|
361
|
+
btn.dataset.nav = 'stats';
|
|
362
|
+
const figures = `Spent this ${word}: ${fmt.usd(b.windowSpendUsd)}` +
|
|
363
|
+
(saved != null ? ` · Saved this ${word}: ${signedUsd(fmt, saved)}` : '');
|
|
364
|
+
btn.setAttribute('aria-label', figures);
|
|
365
|
+
btn.title = `${figures} · resets ${fmtResetAt(b.windowEndMs)} — Claude Code client-side ` +
|
|
366
|
+
`estimate (total_cost_usd), not authoritative billing` + (saved != null ? `. ${SAVED_NOTE}` : '');
|
|
367
|
+
btn.appendChild(stackPair(doc, 'Spent', railUsd(b.windowSpendUsd)));
|
|
368
|
+
if (saved != null) {
|
|
369
|
+
const pair = stackPair(doc, 'Saved', railUsd(saved));
|
|
370
|
+
if (saved >= 0) pair.classList.add('pos');
|
|
371
|
+
btn.appendChild(pair);
|
|
372
|
+
}
|
|
373
|
+
return btn;
|
|
262
374
|
}
|
|
263
375
|
|
|
264
|
-
/** Collapsed-rail budget
|
|
265
|
-
*
|
|
266
|
-
*
|
|
267
|
-
*
|
|
268
|
-
*
|
|
269
|
-
*
|
|
270
|
-
*
|
|
271
|
-
export function renderBudgetRing(budget,
|
|
376
|
+
/** Collapsed-rail budget control — the sidebar indicator's compact twin. Under a total
|
|
377
|
+
* limit: a 38px ring metering spend against it. Without one there is no denominator, so
|
|
378
|
+
* it hands over to renderBudgetStack (app.js calls this one function for the rail either
|
|
379
|
+
* way). Keeps the `spend-ind` class because app.js routes the sidebar spend click through
|
|
380
|
+
* `closest('.spend-ind')`. The arc percentage travels as the custom property `--ring-pct`
|
|
381
|
+
* and the gradient is composed in the stylesheet, so there is one definition of it and
|
|
382
|
+
* the cascade can swap the band colours by class. */
|
|
383
|
+
export function renderBudgetRing(budget, opts = {}) {
|
|
272
384
|
const b = budget || {};
|
|
385
|
+
if (b.totalLimitUsd == null) return renderBudgetStack(b, opts);
|
|
386
|
+
const { doc = globalThis.document, fmt = DEFAULT_FMT } = opts;
|
|
273
387
|
const btn = h(doc, 'button', 'spend-ind spend-ring');
|
|
274
388
|
btn.type = 'button';
|
|
275
389
|
btn.dataset.nav = 'stats';
|
|
276
|
-
const
|
|
277
|
-
const ratio = hasLimit ? b.windowSpendUsd / b.totalLimitUsd : 0;
|
|
390
|
+
const ratio = b.windowSpendUsd / b.totalLimitUsd;
|
|
278
391
|
// Clamped both ways: over-cap spend must not sweep past a full circle, and a
|
|
279
392
|
// refund must not sweep a negative arc.
|
|
280
393
|
const pct = b.blocked ? 100 : Math.max(0, Math.min(100, Math.round(ratio * 100)));
|
|
281
|
-
|
|
282
|
-
// `no-limit` is tested FIRST, unlike renderBudgetIndicator, which tests
|
|
283
|
-
// `blocked` first. Not a divergence: src/core/cost-budget.mjs:90 is
|
|
284
|
-
// `blocked = totalLimitUsd != null && windowSpendUsd >= totalLimitUsd`, and
|
|
285
|
-
// budgetStatus() is the only producer the UI ever sees, so a no-limit budget
|
|
286
|
-
// can never arrive blocked. Do not "fix" the order.
|
|
287
|
-
if (!hasLimit) btn.classList.add('no-limit');
|
|
288
|
-
else if (b.blocked) btn.classList.add('over');
|
|
394
|
+
if (b.blocked) btn.classList.add('over');
|
|
289
395
|
else if (ratio >= BUDGET_WARN_AT) btn.classList.add('warn');
|
|
290
|
-
|
|
291
|
-
btn.style.setProperty('--ring-pct', String(hasLimit ? pct : 0));
|
|
396
|
+
btn.style.setProperty('--ring-pct', String(pct));
|
|
292
397
|
btn.title = `Estimated spend this ${periodWord(b)}: ${fmt.usd4(b.windowSpendUsd)}` +
|
|
293
|
-
|
|
398
|
+
` of ${fmt.usd(b.totalLimitUsd)}` +
|
|
294
399
|
` · resets ${fmtResetAt(b.windowEndMs)} — Claude Code client-side estimate ` +
|
|
295
400
|
`(total_cost_usd), not authoritative billing`;
|
|
296
|
-
btn.appendChild(h(doc, 'span', 'spend-ring-val',
|
|
297
|
-
hasLimit ? `${pct}%` : ringAmount(b.windowSpendUsd)));
|
|
401
|
+
btn.appendChild(h(doc, 'span', 'spend-ring-val', `${pct}%`));
|
|
298
402
|
return btn;
|
|
299
403
|
}
|
|
300
404
|
|
|
@@ -322,6 +426,9 @@ export function renderBudgetReadout(budget, { doc = globalThis.document, fmt = D
|
|
|
322
426
|
/** Cost-pause banner for run/history cards. rec = {pauseReason, pipelineId,
|
|
323
427
|
* totalCostUsd}; opts.budget supplies limits + window figures. */
|
|
324
428
|
export function renderCostPauseBanner(rec, { doc = globalThis.document, fmt = DEFAULT_FMT, budget = null } = {}) {
|
|
429
|
+
// Team policy (team-policy design board 9): a pause on a TEAM cap is the blue variant with
|
|
430
|
+
// "continue past" — every existing caller keeps calling this one function.
|
|
431
|
+
if (POLICY_PAUSE_REASONS.includes(rec.pauseReason)) return renderTeamCapPauseBanner(rec, { doc, budget });
|
|
325
432
|
const b = budget || {};
|
|
326
433
|
const kind = rec.pauseReason === 'cost_total' ? 'cb-total' : 'cb-pipeline';
|
|
327
434
|
const el = h(doc, 'div', `cost-banner ${kind}`);
|