synabun 2.0.5 → 2.0.7
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 +40 -0
- package/README.md +2 -1
- package/THIRD-PARTY-LICENSES.md +13 -0
- package/docs/whatsapp.md +151 -16
- package/hooks/claude-code/shared.mjs +6 -3
- package/lib/child-path.js +303 -0
- package/lib/claude-hooks.js +2 -1
- package/lib/data-home-migration.js +2 -0
- package/lib/desktop-app.js +265 -0
- package/lib/external-tools.js +7 -3
- package/lib/log-redact.js +102 -0
- package/lib/packaged-runtime.js +138 -11
- package/mcp-server/dist/http.d.ts +3 -1
- package/mcp-server/dist/http.d.ts.map +1 -1
- package/mcp-server/dist/http.js +15 -4
- package/mcp-server/dist/http.js.map +1 -1
- package/mcp-server/dist/index.d.ts.map +1 -1
- package/mcp-server/dist/index.js +9 -6
- package/mcp-server/dist/index.js.map +1 -1
- package/mcp-server/dist/services/budget-judgments.d.ts +108 -0
- package/mcp-server/dist/services/budget-judgments.d.ts.map +1 -0
- package/mcp-server/dist/services/budget-judgments.js +153 -0
- package/mcp-server/dist/services/budget-judgments.js.map +1 -0
- package/mcp-server/dist/services/codex-config-heal.d.ts +5 -1
- package/mcp-server/dist/services/codex-config-heal.d.ts.map +1 -1
- package/mcp-server/dist/services/codex-config-heal.js +41 -4
- package/mcp-server/dist/services/codex-config-heal.js.map +1 -1
- package/mcp-server/dist/services/identity.d.ts +11 -0
- package/mcp-server/dist/services/identity.d.ts.map +1 -1
- package/mcp-server/dist/services/identity.js +24 -0
- package/mcp-server/dist/services/identity.js.map +1 -1
- package/mcp-server/dist/services/neural-interface.d.ts +13 -0
- package/mcp-server/dist/services/neural-interface.d.ts.map +1 -1
- package/mcp-server/dist/services/neural-interface.js +34 -2
- package/mcp-server/dist/services/neural-interface.js.map +1 -1
- package/mcp-server/dist/services/typesafe-config.d.ts +1 -1
- package/mcp-server/dist/services/typesafe-config.d.ts.map +1 -1
- package/mcp-server/dist/services/typesafe-config.js +4 -1
- package/mcp-server/dist/services/typesafe-config.js.map +1 -1
- package/mcp-server/dist/tools/agents.d.ts +10 -7
- package/mcp-server/dist/tools/agents.d.ts.map +1 -1
- package/mcp-server/dist/tools/agents.js +34 -13
- package/mcp-server/dist/tools/agents.js.map +1 -1
- package/mcp-server/dist/tools/whatsapp-send-file.d.ts +17 -0
- package/mcp-server/dist/tools/whatsapp-send-file.d.ts.map +1 -0
- package/mcp-server/dist/tools/whatsapp-send-file.js +44 -0
- package/mcp-server/dist/tools/whatsapp-send-file.js.map +1 -0
- package/mcp-server/dist/tui.js +2 -1
- package/mcp-server/dist/tui.js.map +1 -1
- package/mcp-server/dist/version.d.ts +5 -0
- package/mcp-server/dist/version.d.ts.map +1 -0
- package/mcp-server/dist/version.js +21 -0
- package/mcp-server/dist/version.js.map +1 -0
- package/mcp-server/package-lock.json +2 -2
- package/mcp-server/package.json +1 -1
- package/mcp-server/run.mjs +15 -9
- package/mcp-server/src/http.ts +18 -3
- package/mcp-server/src/index.ts +9 -6
- package/mcp-server/src/services/budget-judgments.ts +183 -0
- package/mcp-server/src/services/codex-config-heal.ts +35 -4
- package/mcp-server/src/services/identity.ts +27 -0
- package/mcp-server/src/services/neural-interface.ts +34 -2
- package/mcp-server/src/services/typesafe-config.ts +4 -1
- package/mcp-server/src/tools/agents.ts +33 -14
- package/mcp-server/src/tools/whatsapp-send-file.ts +51 -0
- package/mcp-server/src/tui.ts +2 -1
- package/mcp-server/src/version.ts +22 -0
- package/neural-interface/i18n/en.json +627 -57
- package/neural-interface/i18n/pt-BR.json +626 -56
- package/neural-interface/lib/account-usage.js +163 -0
- package/neural-interface/lib/assistant-api.js +255 -52
- package/neural-interface/lib/assistant-brains/claude.js +15 -2
- package/neural-interface/lib/assistant-brains/codex.js +3 -0
- package/neural-interface/lib/assistant-brains/opencode.js +17 -4
- package/neural-interface/lib/assistant-budget-planner.js +390 -0
- package/neural-interface/lib/assistant-budget-split.js +408 -0
- package/neural-interface/lib/assistant-budget.js +94 -95
- package/neural-interface/lib/assistant-config.js +62 -14
- package/neural-interface/lib/assistant-dispatch.js +432 -125
- package/neural-interface/lib/assistant-jev.js +26 -3
- package/neural-interface/lib/assistant-persona.js +68 -11
- package/neural-interface/lib/assistant-plan-permissions.js +8 -1
- package/neural-interface/lib/assistant-route-accounts.js +14 -0
- package/neural-interface/lib/assistant-route-gate.js +19 -3
- package/neural-interface/lib/assistant-router.js +373 -42
- package/neural-interface/lib/assistant-runtime.js +307 -47
- package/neural-interface/lib/assistant-user-writer.js +4 -2
- package/neural-interface/lib/augmented-path.js +317 -53
- package/neural-interface/lib/claude-accounts.js +342 -31
- package/neural-interface/lib/claude-agent-bridge.js +90 -3
- package/neural-interface/lib/claude-executable.js +6 -1
- package/neural-interface/lib/claude-model-catalog.js +3 -3
- package/neural-interface/lib/claude-panel-auth.js +221 -0
- package/neural-interface/lib/claude-profile-api-key.js +62 -0
- package/neural-interface/lib/cli-install-api.js +137 -0
- package/neural-interface/lib/cli-install.js +797 -0
- package/neural-interface/lib/cli-path.js +130 -0
- package/neural-interface/lib/cli-resolver.js +744 -0
- package/neural-interface/lib/cli-update-planner.js +143 -7
- package/neural-interface/lib/codex-account-auth.js +178 -0
- package/neural-interface/lib/codex-accounts.js +489 -0
- package/neural-interface/lib/codex-capabilities.js +2 -1
- package/neural-interface/lib/codex-compat-rpc.js +8 -2
- package/neural-interface/lib/codex-mcp-approvals.js +5 -1
- package/neural-interface/lib/codex-panel-auth.js +62 -0
- package/neural-interface/lib/codex-session-settings.js +1 -1
- package/neural-interface/lib/codex-settings-schema.js +120 -0
- package/neural-interface/lib/desktop/config.js +3 -0
- package/neural-interface/lib/git-bash/bzip2.js +339 -0
- package/neural-interface/lib/git-bash/claude-settings.js +73 -0
- package/neural-interface/lib/git-bash/extract-worker.js +34 -0
- package/neural-interface/lib/git-bash/install.js +529 -0
- package/neural-interface/lib/git-bash/licenses/COPYING +360 -0
- package/neural-interface/lib/git-bash/licenses/LGPL-2.1 +511 -0
- package/neural-interface/lib/git-bash/manager.js +268 -0
- package/neural-interface/lib/git-bash/pins.js +206 -0
- package/neural-interface/lib/git-bash/pins.json +34 -0
- package/neural-interface/lib/git-bash/resolver.js +194 -0
- package/neural-interface/lib/git-read.js +34 -2
- package/neural-interface/lib/install-details.js +145 -0
- package/neural-interface/lib/mcp-installer.js +14 -2
- package/neural-interface/lib/native-loop-providers.js +16 -3
- package/neural-interface/lib/native-loop-runtime.js +9 -4
- package/neural-interface/lib/os-path.js +319 -0
- package/neural-interface/lib/provider-panel-api-key-auth.js +92 -0
- package/neural-interface/lib/provider-panel-terminal-auth.js +95 -0
- package/neural-interface/lib/pty-host/core.js +2 -1
- package/neural-interface/lib/pty-host/manager.js +27 -2
- package/neural-interface/lib/remote-policy.js +20 -11
- package/neural-interface/lib/session-title-generator.js +4 -3
- package/neural-interface/lib/system-about.js +340 -0
- package/neural-interface/lib/system-activity.js +66 -0
- package/neural-interface/lib/terminal-env.js +11 -3
- package/neural-interface/lib/terminal-path.js +55 -0
- package/neural-interface/lib/virtual-socket.js +12 -1
- package/neural-interface/lib/whatsapp/baileys-adapter.js +165 -45
- package/neural-interface/lib/whatsapp/bridge.js +78 -34
- package/neural-interface/lib/whatsapp/commands.js +13 -14
- package/neural-interface/lib/whatsapp/config.js +134 -20
- package/neural-interface/lib/whatsapp/fake-baileys.js +99 -14
- package/neural-interface/lib/whatsapp/host-core.js +168 -35
- package/neural-interface/lib/whatsapp/inbound.js +5 -3
- package/neural-interface/lib/whatsapp/local-file.js +524 -0
- package/neural-interface/lib/whatsapp/manager.js +92 -32
- package/neural-interface/lib/whatsapp/paths.js +11 -1
- package/neural-interface/lib/whatsapp/protocol.js +65 -5
- package/neural-interface/lib/whatsapp/redact.js +63 -0
- package/neural-interface/lib/whatsapp/service.js +343 -91
- package/neural-interface/lib/whatsapp/transcription.js +463 -0
- package/neural-interface/package-lock.json +2 -2
- package/neural-interface/package.json +1 -1
- package/neural-interface/public/index.html +2 -0
- package/neural-interface/public/index2d.html +2 -0
- package/neural-interface/public/offline.html +16 -1
- package/neural-interface/public/onboarding.html +96 -2
- package/neural-interface/public/shared/account-i18n.js +7 -0
- package/neural-interface/public/shared/account-menu.js +539 -0
- package/neural-interface/public/shared/account-usage.js +19 -0
- package/neural-interface/public/shared/account-view.js +335 -0
- package/neural-interface/public/shared/agent-runtime-options.js +28 -8
- package/neural-interface/public/shared/assistant/asst-accounts.js +182 -184
- package/neural-interface/public/shared/assistant/asst-api.js +32 -2
- package/neural-interface/public/shared/assistant/asst-brain-picker.js +124 -25
- package/neural-interface/public/shared/assistant/asst-budget.js +259 -303
- package/neural-interface/public/shared/assistant/asst-mascot.js +126 -7
- package/neural-interface/public/shared/assistant/asst-menu.js +15 -3
- package/neural-interface/public/shared/assistant/asst-panel.js +48 -14
- package/neural-interface/public/shared/assistant/asst-render.js +38 -7
- package/neural-interface/public/shared/assistant/asst-route.js +72 -14
- package/neural-interface/public/shared/assistant/asst-routes-editor.js +45 -6
- package/neural-interface/public/shared/assistant/asst-slash.js +1 -1
- package/neural-interface/public/shared/assistant/asst-state.js +1 -1
- package/neural-interface/public/shared/assistant/asst-styles.js +99 -17
- package/neural-interface/public/shared/cdx/cdx-commands.js +4 -1
- package/neural-interface/public/shared/cdx/cdx-features.js +22 -14
- package/neural-interface/public/shared/cdx/cdx-icons.js +2 -0
- package/neural-interface/public/shared/cdx/cdx-panel.js +48 -107
- package/neural-interface/public/shared/cdx/cdx-settings.js +231 -0
- package/neural-interface/public/shared/cdx/cdx-styles.js +128 -107
- package/neural-interface/public/shared/cdx/cdx-tabs.js +261 -664
- package/neural-interface/public/shared/cdx/cdx-ws.js +2 -0
- package/neural-interface/public/shared/claude-account-view.js +3 -0
- package/neural-interface/public/shared/cli-install-view.js +392 -0
- package/neural-interface/public/shared/cli-status.js +282 -22
- package/neural-interface/public/shared/cp/cp-events.js +2 -1
- package/neural-interface/public/shared/cp/cp-styles.js +46 -0
- package/neural-interface/public/shared/desktop-bridge.js +39 -0
- package/neural-interface/public/shared/desktop-titlebar-page.js +59 -0
- package/neural-interface/public/shared/desktop-titlebar-view.js +122 -0
- package/neural-interface/public/shared/desktop-titlebar.js +153 -0
- package/neural-interface/public/shared/desktop-update-view.js +462 -0
- package/neural-interface/public/shared/editor/editor-panel.js +1 -1
- package/neural-interface/public/shared/explorer/fx-main.js +1 -1
- package/neural-interface/public/shared/git-bash-view.js +145 -0
- package/neural-interface/public/shared/html-shell.js +1 -0
- package/neural-interface/public/shared/i18n.js +19 -1
- package/neural-interface/public/shared/install-details-view.js +86 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-panel.js +32 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-send.js +17 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-styles.js +118 -0
- package/neural-interface/public/shared/pwa-install-page.js +3 -1
- package/neural-interface/public/shared/settings/settings-about-pane.js +271 -0
- package/neural-interface/public/shared/settings/settings-about.js +254 -0
- package/neural-interface/public/shared/settings/settings-ia.js +15 -0
- package/neural-interface/public/shared/showcase-privacy.js +489 -0
- package/neural-interface/public/shared/storage.js +30 -3
- package/neural-interface/public/shared/styles.css +278 -31
- package/neural-interface/public/shared/synabun-mascot.js +48 -2
- package/neural-interface/public/shared/ui-claude-panel.js +212 -213
- package/neural-interface/public/shared/ui-cli-banner.js +355 -0
- package/neural-interface/public/shared/ui-git-bash.js +124 -0
- package/neural-interface/public/shared/ui-judgments.js +10 -1
- package/neural-interface/public/shared/ui-loading.js +30 -6
- package/neural-interface/public/shared/ui-menubar.js +13 -3
- package/neural-interface/public/shared/ui-navbar.js +3 -1
- package/neural-interface/public/shared/ui-provider-auth.js +104 -0
- package/neural-interface/public/shared/ui-provider-terminal-auth.js +223 -0
- package/neural-interface/public/shared/ui-sessions.js +9 -3
- package/neural-interface/public/shared/ui-settings.js +55 -3
- package/neural-interface/public/shared/ui-sidepanel-sessions.js +3 -1
- package/neural-interface/public/shared/ui-sync.js +6 -0
- package/neural-interface/public/shared/ui-terminal.js +17 -0
- package/neural-interface/public/shared/ui-tool-updates.js +5 -0
- package/neural-interface/public/shared/ui-update.js +594 -5
- package/neural-interface/public/shared/ui-whatsapp.js +21 -6
- package/neural-interface/public/shared/whatsapp/wa-view.js +51 -8
- package/neural-interface/public/shared/worker-end.js +2 -0
- package/neural-interface/public/sw.js +1 -1
- package/neural-interface/public/variant/2d/main.js +10 -1
- package/neural-interface/public/variant/3d/main.js +10 -1
- package/neural-interface/server.js +1219 -608
- package/package.json +4 -1
- package/setup.js +9 -8
|
@@ -1,333 +1,289 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
// ═══════════════════════════════════════════
|
|
4
|
-
// The money caps of the assistant: default and largest per-run cap, the
|
|
5
|
-
// session warning, the session hard cap (brain + every run) and the brain cap.
|
|
6
|
-
// Saved to assistant-config.json through PUT /api/assistant/budget; the
|
|
7
|
-
// dispatcher and the runtime read them on their next check, so a change
|
|
8
|
-
// applies to running sessions without a restart. The pure helpers
|
|
9
|
-
// (validateBudgetDraft, budgetTone, budgetChipText) are covered by node:test;
|
|
10
|
-
// nothing here touches `document` at import time.
|
|
11
|
-
|
|
1
|
+
// One saved per-session budget. Both entry points use this controller and form;
|
|
2
|
+
// chips display confirmed server values, while an unsaved edit stays in the form.
|
|
12
3
|
import { getBudget, putBudget } from './asst-api.js';
|
|
4
|
+
import { openMenu, closeMenu, isMenuOpen } from './asst-menu.js';
|
|
5
|
+
import { createMascotControl } from './asst-mascot.js';
|
|
13
6
|
|
|
14
|
-
export const BUDGET_FIELDS = Object.freeze([
|
|
15
|
-
{ id: 'defaultRunUsd', key: 'assistant.budget.defaultRun', label: 'Default per-run cap', desc: 'Each dispatched run stops at this spend unless the brain asks for another cap.' },
|
|
16
|
-
{ id: 'maxRunUsd', key: 'assistant.budget.maxRun', label: 'Largest per-run cap', desc: 'The most a brain may give one run (budget_usd).' },
|
|
17
|
-
{ id: 'sessionWarnUsd', key: 'assistant.budget.sessionWarn', label: 'Session warning', desc: 'The brain and this panel are warned once when the session spends this much.' },
|
|
18
|
-
{ id: 'sessionHardUsd', key: 'assistant.budget.sessionHard', label: 'Session hard cap', desc: 'Brain + every run of the session. At this spend runs stop, and dispatches, follow-ups and brain turns are refused.' },
|
|
19
|
-
{ id: 'brainUsd', key: 'assistant.budget.brain', label: 'Brain cap', desc: 'The central brain\'s own spend in one session, checked before every brain turn.' },
|
|
20
|
-
]);
|
|
21
7
|
export const BUDGET_BOUNDS = Object.freeze({ minUsd: 0.1, maxUsd: 1000 });
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
}
|
|
31
|
-
const money = (value) => `$${(Number(value) || 0).toFixed(2)}`;
|
|
32
|
-
function esc(value) {
|
|
33
|
-
return String(value ?? '').replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
|
|
34
|
-
}
|
|
35
|
-
function el(tag, className, text) {
|
|
8
|
+
const money = value => `$${(Number(value) || 0).toFixed(2)}`;
|
|
9
|
+
// The saved amount: whole dollars as typed ("$1000"), cents always with two digits ("$12.50").
|
|
10
|
+
const amountText = value => (Number.isInteger(Number(value)) ? `$${Number(value)}` : money(value));
|
|
11
|
+
const tf = t => (key, fallback, vars = {}) => {
|
|
12
|
+
const value = t?.(key, vars);
|
|
13
|
+
return String(!value || value === key ? fallback : value).replace(/\{(\w+)\}/g, (all, key) => vars[key] ?? all);
|
|
14
|
+
};
|
|
15
|
+
const el = (tag, cls, text) => {
|
|
36
16
|
const node = document.createElement(tag);
|
|
37
|
-
if (
|
|
17
|
+
if (cls) node.className = cls;
|
|
38
18
|
if (text != null) node.textContent = text;
|
|
39
19
|
return node;
|
|
40
|
-
}
|
|
20
|
+
};
|
|
41
21
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
const values = {};
|
|
50
|
-
const errors = {};
|
|
51
|
-
for (const field of BUDGET_FIELDS) {
|
|
52
|
-
const raw = draft[field.id];
|
|
53
|
-
const text = typeof raw === 'string' ? raw.trim().replace(/^\$/, '') : raw;
|
|
54
|
-
const n = text === '' || text === null || text === undefined ? NaN : Number(text);
|
|
55
|
-
if (!Number.isFinite(n)) { errors[field.id] = tr('assistant.budget.errNumber', 'Enter an amount in US dollars.'); continue; }
|
|
56
|
-
if (n < bounds.minUsd || n > bounds.maxUsd) { errors[field.id] = tr('assistant.budget.errRange', 'Between {min} and {max}.', { min: money(bounds.minUsd), max: `$${bounds.maxUsd}` }); continue; }
|
|
57
|
-
values[field.id] = Math.round(n * 100) / 100;
|
|
22
|
+
export function validateBudgetDraft(draft = {}, { bounds = BUDGET_BOUNDS, t } = {}) {
|
|
23
|
+
const tr = tf(t), errors = {}, values = { unlimited: draft.unlimited === true };
|
|
24
|
+
const raw = typeof draft.amountUsd === 'string' ? draft.amountUsd.trim() : draft.amountUsd;
|
|
25
|
+
const amount = raw === '' || raw == null ? NaN : Number(raw);
|
|
26
|
+
if (!values.unlimited) {
|
|
27
|
+
if (!Number.isFinite(amount)) errors.amountUsd = tr('assistant.budget.errNumber', 'Enter an amount in US dollars.');
|
|
28
|
+
else if (amount < bounds.minUsd || amount > bounds.maxUsd) errors.amountUsd = tr('assistant.budget.errRange', 'Between {min} and {max}.', { min: money(bounds.minUsd), max: money(bounds.maxUsd) });
|
|
58
29
|
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
if (has('maxRunUsd', 'sessionHardUsd') && v.maxRunUsd > v.sessionHardUsd && !errors.maxRunUsd) errors.maxRunUsd = tr('assistant.budget.errMaxOverHard', 'Cannot exceed the session hard cap ({hard}).', { hard: money(v.sessionHardUsd) });
|
|
62
|
-
if (has('defaultRunUsd', 'maxRunUsd') && v.defaultRunUsd > v.maxRunUsd && !errors.defaultRunUsd) errors.defaultRunUsd = tr('assistant.budget.errDefaultOverMax', 'Cannot exceed the largest per-run cap ({max}).', { max: money(v.maxRunUsd) });
|
|
63
|
-
if (has('sessionWarnUsd', 'sessionHardUsd') && v.sessionWarnUsd >= v.sessionHardUsd && !errors.sessionWarnUsd) errors.sessionWarnUsd = tr('assistant.budget.errWarnOverHard', 'Must be below the session hard cap ({hard}).', { hard: money(v.sessionHardUsd) });
|
|
64
|
-
if (has('brainUsd', 'sessionHardUsd') && v.brainUsd > v.sessionHardUsd && !errors.brainUsd) errors.brainUsd = tr('assistant.budget.errBrainOverHard', 'Cannot exceed the session hard cap ({hard}).', { hard: money(v.sessionHardUsd) });
|
|
30
|
+
// No limit must work even if the unfinished amount field is invalid.
|
|
31
|
+
if (Number.isFinite(amount) && amount >= bounds.minUsd && amount <= bounds.maxUsd) values.amountUsd = Math.round(amount * 100) / 100;
|
|
65
32
|
return { values, errors };
|
|
66
33
|
}
|
|
67
|
-
|
|
68
|
-
/** 'over' at the hard cap, 'warn' at the warning, else 'ok' (null without a budget view). */
|
|
69
34
|
export function budgetTone(view) {
|
|
70
|
-
if (!view
|
|
71
|
-
if (
|
|
72
|
-
if (Number(view.
|
|
35
|
+
if (!view) return null;
|
|
36
|
+
if (view.unlimited) return 'ok';
|
|
37
|
+
if (view.exceeded || Number(view.spentUsd) >= Number(view.amountUsd)) return 'over';
|
|
38
|
+
if (Number(view.spentUsd) >= Number(view.warnAtUsd ?? Number(view.amountUsd) * 0.8)) return 'warn';
|
|
73
39
|
return 'ok';
|
|
74
40
|
}
|
|
75
|
-
|
|
76
|
-
/** Cost chip tooltip: "Brain $x · agents $y · $z of $cap cap". */
|
|
77
|
-
export function budgetChipText(view, { t = null, brainMetered = true } = {}) {
|
|
78
|
-
const tr = tf(t);
|
|
41
|
+
export function budgetSpendText(view, { t } = {}) {
|
|
79
42
|
if (!view) return '';
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
if (
|
|
86
|
-
return
|
|
43
|
+
return view.unlimited
|
|
44
|
+
? tf(t)('assistant.budget.spendUnlimited', '{spent} · no limit', { spent: money(view.spentUsd) })
|
|
45
|
+
: `${money(view.spentUsd)} / ${money(view.amountUsd)}`;
|
|
46
|
+
}
|
|
47
|
+
export function budgetSettingText(budget, { t } = {}) {
|
|
48
|
+
if (!budget) return '…';
|
|
49
|
+
return budget.unlimited ? tf(t)('assistant.budget.noLimit', 'No limit')
|
|
50
|
+
: tf(t)('assistant.budget.perSession', '{amount} / session', { amount: amountText(budget.amountUsd) });
|
|
51
|
+
}
|
|
52
|
+
export function budgetChipText(view, { t } = {}) {
|
|
53
|
+
if (!view) return '';
|
|
54
|
+
const tr = tf(t);
|
|
55
|
+
return [budgetSpendText(view, { t }), tr('assistant.budget.chipSplit', 'Brain {brain} · agents {agents}', { brain: money(view.brainUsd), agents: money(view.dispatchUsd) }),
|
|
56
|
+
Number(view.unpricedRuns) > 0 ? tr('assistant.budget.chipUnpriced', '{n} run(s) with unpriced usage', { n: view.unpricedRuns }) : ''].filter(Boolean).join(' · ');
|
|
87
57
|
}
|
|
88
58
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
if (
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
if (!overlay.isConnected) return;
|
|
119
|
-
document.removeEventListener('keydown', onKey, true);
|
|
120
|
-
overlay.remove();
|
|
121
|
-
_editor = null;
|
|
122
|
-
if (returnTo?.isConnected) { try { returnTo.focus({ preventScroll: true }); } catch { /* ignore */ } }
|
|
59
|
+
/** Serializable writer with one conflict retry, no polling and no local persistence. */
|
|
60
|
+
export function createBudgetController({ sessionId = null, t, get = getBudget, put = putBudget, onSaved, debounceMs = 450 } = {}) {
|
|
61
|
+
const tr = tf(t), listeners = new Set(), commits = new Set(), edits = new Map();
|
|
62
|
+
const state = { data: null, draft: { unlimited: false, amountUsd: '' }, dirty: false, saving: false, loading: false, error: '', fieldError: '', saved: false };
|
|
63
|
+
let timer, revision = 0, loading, writing, disposed = false;
|
|
64
|
+
const notify = () => { if (!disposed) for (const fn of listeners) fn(state); };
|
|
65
|
+
const committed = event => { if (!disposed) for (const fn of commits) { try { fn(event); } catch { /* an observer cannot fail a save */ } } };
|
|
66
|
+
const adopt = data => {
|
|
67
|
+
if (!data?.budget || (state.data && Number(data.version) < Number(state.data.version))) return;
|
|
68
|
+
// A PUT returns before the split finishes: don't overwrite a newer live split
|
|
69
|
+
// that arrived during this request with the old split from its response.
|
|
70
|
+
const live = state.data?.session;
|
|
71
|
+
if (live && data.session && live.version >= data.session.version &&
|
|
72
|
+
Date.parse(live.split?.updatedAt || 0) > Date.parse(data.session.split?.updatedAt || 0)) data = { ...data, session: live };
|
|
73
|
+
state.data = data;
|
|
74
|
+
if (!edits.has('unlimited')) state.draft.unlimited = data.budget.unlimited;
|
|
75
|
+
if (!edits.has('amountUsd')) state.draft.amountUsd = String(data.budget.amountUsd ?? '');
|
|
76
|
+
if (!disposed) onSaved?.(data);
|
|
77
|
+
};
|
|
78
|
+
async function load() {
|
|
79
|
+
if (loading) return loading;
|
|
80
|
+
state.loading = true; state.error = ''; notify();
|
|
81
|
+
loading = (async () => {
|
|
82
|
+
try { adopt(await get(sessionId)); }
|
|
83
|
+
catch { state.error = tr('assistant.budget.loadFailed', 'Could not load the budget. Try again.'); }
|
|
84
|
+
finally { state.loading = false; loading = null; notify(); }
|
|
85
|
+
return state.data;
|
|
86
|
+
})();
|
|
87
|
+
return loading;
|
|
123
88
|
}
|
|
124
|
-
function
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
89
|
+
function edit(patch, { immediate = false } = {}) {
|
|
90
|
+
revision++; state.draft = { ...state.draft, ...patch }; state.dirty = true;
|
|
91
|
+
for (const field of Object.keys(patch)) edits.set(field, revision);
|
|
92
|
+
state.error = ''; state.fieldError = ''; state.saved = false;
|
|
93
|
+
clearTimeout(timer); notify();
|
|
94
|
+
if (immediate) return flush();
|
|
95
|
+
timer = setTimeout(flush, debounceMs);
|
|
128
96
|
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
97
|
+
async function flush() {
|
|
98
|
+
clearTimeout(timer);
|
|
99
|
+
if (writing) return writing;
|
|
100
|
+
if (!state.data || !state.dirty) return;
|
|
101
|
+
writing = (async () => {
|
|
102
|
+
while (state.dirty) {
|
|
103
|
+
const changed = new Map(edits);
|
|
104
|
+
const { values, errors } = validateBudgetDraft(state.draft, { bounds: state.data.bounds, t });
|
|
105
|
+
state.fieldError = errors.amountUsd || '';
|
|
106
|
+
if (state.fieldError) { notify(); break; }
|
|
107
|
+
const patch = Object.fromEntries([...changed.keys()].filter(field => Object.hasOwn(values, field)).map(field => [field, values[field]]));
|
|
108
|
+
state.saving = true; state.error = ''; notify();
|
|
109
|
+
try {
|
|
110
|
+
let response;
|
|
111
|
+
try { response = await put(patch, state.data.version, sessionId); }
|
|
112
|
+
catch (err) {
|
|
113
|
+
if (err.code !== 'VERSION_CONFLICT' && err.status !== 409) throw err;
|
|
114
|
+
adopt(await get(sessionId));
|
|
115
|
+
response = await put(patch, state.data.version, sessionId); // retry the edit once
|
|
116
|
+
}
|
|
117
|
+
for (const [field, at] of changed) if (edits.get(field) === at) edits.delete(field);
|
|
118
|
+
state.dirty = edits.size > 0;
|
|
119
|
+
adopt(response); state.saved = !state.dirty;
|
|
120
|
+
committed({ ok: true, patch, budget: response.budget });
|
|
121
|
+
} catch (err) {
|
|
122
|
+
if (err.code === 'BUDGET_INVALID') state.fieldError = tr('assistant.budget.invalid', 'The server rejected this budget. Check the amount and try again.');
|
|
123
|
+
else if (err.code === 'VERSION_CONFLICT' || err.status === 409) state.error = tr('assistant.budget.conflict', 'Changed elsewhere again. Your edit is kept; try again.');
|
|
124
|
+
else state.error = tr('assistant.budget.saveFailed', 'Could not save the budget. Your edit is kept; try again.');
|
|
125
|
+
committed({ ok: false, patch, error: err });
|
|
126
|
+
break;
|
|
127
|
+
} finally { state.saving = false; notify(); }
|
|
128
|
+
}
|
|
129
|
+
})();
|
|
130
|
+
try { await writing; } finally { writing = null; }
|
|
145
131
|
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
const meter = el('div', 'asst-budget-meter');
|
|
153
|
-
meter.dataset.tone = tone;
|
|
154
|
-
meter.setAttribute('role', 'meter');
|
|
155
|
-
meter.setAttribute('aria-valuemin', '0');
|
|
156
|
-
meter.setAttribute('aria-valuemax', String(hard));
|
|
157
|
-
meter.setAttribute('aria-valuenow', String(Number(view.totalUsd) || 0));
|
|
158
|
-
meter.setAttribute('aria-label', tr('assistant.budget.meterLabel', 'Session spend against the hard cap'));
|
|
159
|
-
const fill = el('span', 'asst-budget-meter-fill');
|
|
160
|
-
fill.style.width = `${Math.min(100, (Number(view.totalUsd) / hard) * 100)}%`;
|
|
161
|
-
const tick = el('span', 'asst-budget-meter-warn');
|
|
162
|
-
tick.style.left = `${Math.min(100, (Number(view.warnUsd) / hard) * 100)}%`;
|
|
163
|
-
meter.append(fill, tick);
|
|
164
|
-
section.appendChild(meter);
|
|
165
|
-
const brain = hooks.brainMetered === false ? tr('assistant.budget.notMeteredCodex', 'not metered (Codex brain)') : money(view.brainUsd);
|
|
166
|
-
const line = tr('assistant.budget.sessionLine', '{total} spent of {hard} — brain {brain}, agents {agents}; {reserved} reserved by live runs, {available} left for new runs.', {
|
|
167
|
-
total: money(view.totalUsd), hard: money(view.hardUsd), brain, agents: money(view.dispatchUsd), reserved: money(view.reservedUsd), available: money(view.availableUsd),
|
|
168
|
-
});
|
|
169
|
-
section.appendChild(el('div', 'asst-modal-note', line));
|
|
170
|
-
if (Number(view.unpricedRuns) > 0) {
|
|
171
|
-
const note = el('div', 'asst-modal-note asst-budget-unpriced', tr('assistant.budget.unpriced', '{n} run(s) used a model with no known price: their tokens are recorded but the dollar caps cannot meter them.', { n: view.unpricedRuns }));
|
|
172
|
-
section.appendChild(note);
|
|
173
|
-
}
|
|
174
|
-
return section;
|
|
132
|
+
function receiveSession(view) {
|
|
133
|
+
if (!view || (sessionId && view.sessionId !== sessionId)) return;
|
|
134
|
+
if (state.data && Number(view.version) < Number(state.data.version)) return;
|
|
135
|
+
const saved = state.data?.budget || {};
|
|
136
|
+
adopt({ ...(state.data || {}), version: view.version, budget: { ...saved, unlimited: view.unlimited, amountUsd: view.amountUsd ?? saved.amountUsd, version: view.version }, session: view });
|
|
137
|
+
notify();
|
|
175
138
|
}
|
|
139
|
+
return { state, load, edit, flush, receiveSession,
|
|
140
|
+
subscribe(fn) { listeners.add(fn); fn(state); return () => listeners.delete(fn); },
|
|
141
|
+
onCommit(fn) { commits.add(fn); return () => commits.delete(fn); },
|
|
142
|
+
destroy() { flush(); disposed = true; clearTimeout(timer); listeners.clear(); commits.clear(); },
|
|
143
|
+
};
|
|
144
|
+
}
|
|
176
145
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
input.
|
|
204
|
-
input.min = String(data
|
|
205
|
-
input.
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
again?.focus({ preventScroll: true });
|
|
215
|
-
try { again?.setSelectionRange?.(pos, pos); } catch { /* number inputs have no selection */ }
|
|
216
|
-
});
|
|
217
|
-
wrap.appendChild(input);
|
|
218
|
-
row.appendChild(wrap);
|
|
219
|
-
const source = String(state.draft[field.id]) === String(effective) ? (data.sources?.[field.id] || 'default') : 'edited';
|
|
220
|
-
const badge = el('span', 'asst-routes-source', source === 'edited' ? tr('assistant.budget.sourceEdited', 'edited') : sourceLabel(source));
|
|
221
|
-
badge.dataset.source = source === 'saved' || source === 'edited' ? 'yours' : 'default';
|
|
222
|
-
row.appendChild(badge);
|
|
223
|
-
return row;
|
|
224
|
-
}
|
|
146
|
+
let uid = 0;
|
|
147
|
+
/** The identical autosave control in the empty-state popover and the session modal. */
|
|
148
|
+
export function createBudgetControls(controller, { t } = {}) {
|
|
149
|
+
const tr = tf(t), id = `asst-budget-${++uid}`;
|
|
150
|
+
const root = el('section', 'asst-budget-controls'); root.tabIndex = -1;
|
|
151
|
+
const label = el('label', 'asst-routes-row-label', tr('assistant.budget.amount', 'Budget per session (USD)'));
|
|
152
|
+
label.htmlFor = id;
|
|
153
|
+
const input = el('input', 'asst-dd-input asst-budget-input');
|
|
154
|
+
input.id = id; input.type = 'number'; input.inputMode = 'decimal'; input.step = '0.01';
|
|
155
|
+
const switchLabel = el('label', 'asst-budget-switch-label');
|
|
156
|
+
const toggle = el('input', 'asst-budget-switch');
|
|
157
|
+
toggle.type = 'checkbox'; toggle.setAttribute('role', 'switch');
|
|
158
|
+
switchLabel.append(toggle, el('span', '', tr('assistant.budget.noLimit', 'No limit')));
|
|
159
|
+
const note = el('div', 'asst-modal-note', tr('assistant.budget.applies', 'Applies to each new session and all open sessions. Jev splits it across providers.'));
|
|
160
|
+
const error = el('div', 'asst-budget-field-error'); error.id = `${id}-error`; error.setAttribute('role', 'alert');
|
|
161
|
+
const status = el('div', 'asst-modal-note'); status.setAttribute('role', 'status');
|
|
162
|
+
note.id = `${id}-note`; input.setAttribute('aria-describedby', `${note.id} ${error.id}`);
|
|
163
|
+
const retry = el('button', 'asst-btn asst-btn-secondary', tr('assistant.budget.retry', 'Try again')); retry.type = 'button';
|
|
164
|
+
retry.addEventListener('click', () => controller.state.dirty ? controller.flush() : controller.load());
|
|
165
|
+
input.addEventListener('input', () => controller.edit({ amountUsd: input.value }));
|
|
166
|
+
input.addEventListener('blur', () => controller.flush());
|
|
167
|
+
input.addEventListener('keydown', e => { if (e.key === 'Enter') { e.preventDefault(); controller.flush(); } });
|
|
168
|
+
toggle.addEventListener('change', () => controller.edit({ unlimited: toggle.checked }, { immediate: true }));
|
|
169
|
+
root.append(label, input, switchLabel, note, error, status, retry);
|
|
170
|
+
root.addEventListener('keydown', e => { if (e.key !== 'Escape' && e.key !== 'Tab') e.stopPropagation(); });
|
|
171
|
+
const unsubscribe = controller.subscribe(state => {
|
|
172
|
+
if (input.value !== String(state.draft.amountUsd)) input.value = state.draft.amountUsd;
|
|
173
|
+
input.min = String(state.data?.bounds?.minUsd ?? BUDGET_BOUNDS.minUsd); input.max = String(state.data?.bounds?.maxUsd ?? BUDGET_BOUNDS.maxUsd);
|
|
174
|
+
input.disabled = !state.data || state.draft.unlimited;
|
|
175
|
+
toggle.disabled = !state.data; toggle.checked = state.draft.unlimited;
|
|
176
|
+
input.setAttribute('aria-invalid', state.fieldError ? 'true' : 'false');
|
|
177
|
+
error.textContent = state.fieldError || state.error; error.hidden = !error.textContent;
|
|
178
|
+
status.textContent = state.loading ? tr('common.loading', 'Loading...') : state.saving ? tr('assistant.budget.saving', 'Saving…') : state.saved ? tr('assistant.budget.saved', 'Budget saved — applies now.') : '';
|
|
179
|
+
retry.hidden = !(state.error || state.fieldError);
|
|
180
|
+
});
|
|
181
|
+
return { el: root, focus: () => (!input.disabled ? input : !toggle.disabled ? toggle : !retry.hidden ? retry : root).focus({ preventScroll: true }), destroy: unsubscribe };
|
|
182
|
+
}
|
|
225
183
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
return section;
|
|
254
|
-
}
|
|
184
|
+
export function mountBudgetChip(slot, controller, { t, mascot } = {}) {
|
|
185
|
+
const tr = tf(t), btn = el('button', 'asst-dd asst-empty-control-btn asst-empty-budget-btn');
|
|
186
|
+
btn.type = 'button'; btn.setAttribute('aria-haspopup', 'dialog'); btn.setAttribute('aria-expanded', 'false');
|
|
187
|
+
const icon = el('span', 'asst-icon'); icon.setAttribute('aria-hidden', 'true');
|
|
188
|
+
icon.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="5" width="18" height="14" rx="2"/><path d="M16 10h5v5h-5zM7 9h4"/></svg>';
|
|
189
|
+
const value = el('span', 'asst-dd-label'), caret = el('span', 'asst-dd-caret'); caret.setAttribute('aria-hidden', 'true');
|
|
190
|
+
caret.innerHTML = '<svg viewBox="0 0 12 12"><path d="m3 4.5 3 3 3-3"/></svg>';
|
|
191
|
+
btn.append(icon, el('span', 'asst-empty-project-key', tr('assistant.budget.title', 'Budget')), value, caret);
|
|
192
|
+
const actor = createMascotControl(btn, { director: mascot, pose: 'budget', options: () => ({ unlimited: !!controller.state.data?.budget.unlimited }) });
|
|
193
|
+
const offCommit = controller.onCommit(event => actor.commit(!event.ok ? 'wince' : Object.hasOwn(event.patch, 'unlimited') ? event.patch.unlimited ? 'pulse' : 'pop' : 'nod'));
|
|
194
|
+
let unlimited;
|
|
195
|
+
const off = controller.subscribe(state => {
|
|
196
|
+
value.textContent = budgetSettingText(state.data?.budget, { t });
|
|
197
|
+
btn.setAttribute('aria-label', `${tr('assistant.budget.title', 'Budget')}: ${value.textContent}`);
|
|
198
|
+
if (unlimited !== state.data?.budget.unlimited) { unlimited = state.data?.budget.unlimited; actor.refresh(); }
|
|
199
|
+
});
|
|
200
|
+
btn.addEventListener('click', () => {
|
|
201
|
+
if (isMenuOpen(btn)) { closeMenu({ restoreFocus: true }); return; }
|
|
202
|
+
const controls = createBudgetControls(controller, { t });
|
|
203
|
+
openMenu(btn, { title: tr('assistant.budget.title', 'Budget'), role: 'dialog', content: controls.el, width: 320, repositionOnResize: true,
|
|
204
|
+
className: 'asst-budget-popover', placement: 'prefer-below', onOpen: node => actor.open(node), onClose: () => { actor.close(); controller.flush(); controls.destroy(); } });
|
|
205
|
+
controls.focus();
|
|
206
|
+
if (!controller.state.data) controller.load().then(() => { if (isMenuOpen(btn)) controls.focus(); });
|
|
207
|
+
});
|
|
208
|
+
slot.appendChild(btn);
|
|
209
|
+
return { el: btn, destroy() { if (isMenuOpen(btn)) closeMenu(); actor.destroy(); offCommit(); off(); btn.remove(); } };
|
|
210
|
+
}
|
|
255
211
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
if (state.conflict) {
|
|
264
|
-
const reload = el('button', 'asst-btn asst-btn-secondary', tr('assistant.budget.reload', 'Reload'));
|
|
265
|
-
reload.type = 'button';
|
|
266
|
-
reload.addEventListener('click', () => load());
|
|
267
|
-
banner.appendChild(reload);
|
|
268
|
-
}
|
|
269
|
-
body.appendChild(banner);
|
|
270
|
-
}
|
|
271
|
-
if (!state.data) { saveBtn.disabled = true; return; }
|
|
272
|
-
if (state.data.session) body.appendChild(sessionSection(state.data.session));
|
|
273
|
-
const caps = el('section', 'asst-routes-section');
|
|
274
|
-
caps.appendChild(el('div', 'asst-routes-section-title', tr('assistant.budget.caps', 'Caps (US dollars)')));
|
|
275
|
-
const list = el('div', 'asst-routes-list');
|
|
276
|
-
for (const field of BUDGET_FIELDS) list.appendChild(capRow(field, errors));
|
|
277
|
-
caps.appendChild(list);
|
|
278
|
-
caps.appendChild(el('div', 'asst-modal-note', tr('assistant.budget.applies', 'Saved caps apply at once to open sessions and their runs: the next dispatch, follow-up, turn end and metered step. A Claude run keeps the CLI cap it started with, and the dispatcher also stops it at the new cap after its turn.')));
|
|
279
|
-
body.appendChild(caps);
|
|
280
|
-
body.appendChild(enforcementSection(state.data));
|
|
281
|
-
saveBtn.disabled = state.saving || Object.keys(errors).length > 0;
|
|
212
|
+
export function renderBudgetSession(view, { t } = {}) {
|
|
213
|
+
const tr = tf(t), section = el('section', 'asst-budget-session');
|
|
214
|
+
section.append(el('h3', 'asst-routes-section-title', tr('assistant.budget.thisSession', 'This session')), el('div', 'asst-budget-total', budgetSpendText(view, { t })));
|
|
215
|
+
section.dataset.tone = budgetTone(view);
|
|
216
|
+
if (!view.unlimited) {
|
|
217
|
+
const meter = el('meter', 'asst-budget-meter'); meter.min = 0; meter.max = view.amountUsd; meter.value = view.spentUsd;
|
|
218
|
+
meter.setAttribute('aria-label', tr('assistant.budget.meterLabel', 'Session spend against the budget')); section.append(meter);
|
|
282
219
|
}
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
220
|
+
const providers = el('div', 'asst-budget-providers');
|
|
221
|
+
for (const row of view.providers || []) {
|
|
222
|
+
const item = el('div', 'asst-budget-provider'); item.dataset.provider = row.provider;
|
|
223
|
+
item.dataset.eligible = String(row.eligible); item.dataset.dry = String((view.dry || []).includes(row.provider));
|
|
224
|
+
const heading = el('div', 'asst-budget-provider-name', row.label || row.provider);
|
|
225
|
+
if (row.isBrain) heading.append(el('span', 'asst-budget-brain', tr('assistant.budget.brainRow', 'Brain')));
|
|
226
|
+
item.append(heading);
|
|
227
|
+
item.append(el('div', 'asst-budget-provider-money', view.unlimited
|
|
228
|
+
? tr('assistant.budget.providerSpend', '{spent} spent', { spent: money(row.spentUsd) })
|
|
229
|
+
: tr('assistant.budget.providerAmounts', 'Share {share} · spent {spent} · left for tasks {left}', { share: row.shareUsd == null ? '—' : money(row.shareUsd), spent: money(row.spentUsd), left: row.workerLeftUsd == null ? '—' : money(row.workerLeftUsd) })));
|
|
230
|
+
if (!view.unlimited && row.isBrain && view.split) item.append(el('div', 'asst-modal-note', tr('assistant.budget.reserve', '{reserve} reserved for the brain within this share.', { reserve: money(view.split.brainReserveLeftUsd) })));
|
|
231
|
+
if (!view.unlimited && (view.dry || []).includes(row.provider)) item.append(el('div', 'asst-budget-dry', tr('assistant.budget.dry', 'Share used up')));
|
|
232
|
+
if (!row.eligible && row.reason) {
|
|
233
|
+
const reason = String(row.reason).replace(/^brain:/, '');
|
|
234
|
+
item.append(el('div', 'asst-modal-note', tr(`assistant.budget.ineligible.${reason}`, { no_models: 'No available models', signed_out: 'Signed out', limited: 'Provider usage limit reached' }[reason] || reason)));
|
|
297
235
|
}
|
|
298
|
-
|
|
299
|
-
|
|
236
|
+
providers.append(item);
|
|
237
|
+
}
|
|
238
|
+
section.append(providers);
|
|
239
|
+
if (!view.unlimited) {
|
|
240
|
+
if (view.split) {
|
|
241
|
+
const split = view.split;
|
|
242
|
+
const source = split.source === 'jev' ? 'Jev' : tr('assistant.budget.fixedRule', 'Fixed rule');
|
|
243
|
+
const why = split.fallbackWhy ? tr(`assistant.budget.fallback.${split.fallbackWhy}`, { jev_off: 'Jev is off', jev_unavailable: 'Jev is unavailable', jev_incomplete: 'Jev returned an incomplete split', nothing_left: 'No budget left' }[split.fallbackWhy] || split.fallbackWhy) : '';
|
|
244
|
+
const time = el('time'); time.dateTime = split.updatedAt || '';
|
|
245
|
+
time.textContent = split.updatedAt && Number.isFinite(Date.parse(split.updatedAt)) ? new Date(split.updatedAt).toLocaleString() : '';
|
|
246
|
+
const meta = el('div', 'asst-modal-note', [source, why].filter(Boolean).join(' · ') + ' · '); meta.append(time); section.append(meta);
|
|
247
|
+
} else section.append(el('div', 'asst-modal-note', tr('assistant.budget.awaitingSplit', 'The split is prepared when work starts.')));
|
|
300
248
|
}
|
|
249
|
+
if (view.unpricedRuns > 0) section.append(el('div', 'asst-budget-unpriced', tr('assistant.budget.chipUnpriced', '{n} run(s) with unpriced usage', { n: view.unpricedRuns })));
|
|
250
|
+
return section;
|
|
251
|
+
}
|
|
301
252
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
}
|
|
326
|
-
|
|
253
|
+
let editor;
|
|
254
|
+
export async function openBudgetEditor(hooks = {}) {
|
|
255
|
+
editor?.close();
|
|
256
|
+
const controller = hooks.controller || createBudgetController(hooks), tr = tf(hooks.t);
|
|
257
|
+
const returnTo = document.activeElement, overlay = el('div', 'asst-modal-overlay asst-budget-overlay');
|
|
258
|
+
overlay.setAttribute('role', 'dialog'); overlay.setAttribute('aria-modal', 'true'); overlay.setAttribute('aria-label', tr('assistant.budget.title', 'Budget'));
|
|
259
|
+
const modal = el('div', 'asst-modal asst-budget-modal'), head = el('div', 'asst-modal-head');
|
|
260
|
+
const closeBtn = el('button', 'asst-iconbtn asst-modal-close', '✕'); closeBtn.type = 'button'; closeBtn.setAttribute('aria-label', tr('common.close', 'Close'));
|
|
261
|
+
head.append(el('span', '', tr('assistant.budget.title', 'Budget')), closeBtn);
|
|
262
|
+
const body = el('div', 'asst-modal-body'), controls = createBudgetControls(controller, hooks), live = el('div');
|
|
263
|
+
body.append(controls.el, live); modal.append(head, body); overlay.append(modal); document.body.append(overlay);
|
|
264
|
+
const off = controller.subscribe(state => { live.replaceChildren(...(state.data?.session ? [renderBudgetSession(state.data.session, hooks)] : [])); });
|
|
265
|
+
function close() {
|
|
266
|
+
if (!overlay.isConnected) return;
|
|
267
|
+
controller.flush(); off(); controls.destroy(); if (!hooks.controller) controller.destroy();
|
|
268
|
+
window.removeEventListener('keydown', onKey, true); overlay.remove(); editor = null;
|
|
269
|
+
if (returnTo?.isConnected) returnTo.focus({ preventScroll: true });
|
|
270
|
+
}
|
|
271
|
+
function onKey(e) {
|
|
272
|
+
if (e.key === 'Escape') { e.preventDefault(); e.stopPropagation(); close(); return; }
|
|
273
|
+
if (e.key === 'Tab') {
|
|
274
|
+
const nodes = [...modal.querySelectorAll('button, input')].filter(node => !node.disabled && !node.hidden);
|
|
275
|
+
const index = nodes.indexOf(document.activeElement);
|
|
276
|
+
if (e.shiftKey && index <= 0) { e.preventDefault(); nodes.at(-1)?.focus(); }
|
|
277
|
+
else if (!e.shiftKey && (index < 0 || index === nodes.length - 1)) { e.preventDefault(); nodes[0]?.focus(); }
|
|
327
278
|
}
|
|
328
279
|
}
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
280
|
+
closeBtn.addEventListener('click', close); overlay.addEventListener('mousedown', e => { if (e.target === overlay) close(); });
|
|
281
|
+
window.addEventListener('keydown', onKey, true);
|
|
282
|
+
overlay.addEventListener('keydown', e => e.stopPropagation());
|
|
283
|
+
const handle = { el: overlay, close, controller }; editor = handle;
|
|
284
|
+
closeBtn.focus({ preventScroll: true });
|
|
285
|
+
await controller.load(); if (overlay.isConnected) controls.focus();
|
|
286
|
+
return handle;
|
|
333
287
|
}
|
|
288
|
+
|
|
289
|
+
export function closeBudgetEditor(controller) { if (editor?.controller === controller) editor.close(); }
|