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
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
// success, error once, when a turn that did real work ends (then idle)
|
|
13
13
|
// watch while you type: eyes on the caret column, look() at most
|
|
14
14
|
// every 120 ms; back 1.5 s after the last key, at once on send
|
|
15
|
+
// project/budget attention owned by this panel's empty-state chip/menu;
|
|
16
|
+
// commits react once; a menu prevents idle sleep
|
|
15
17
|
// sleep the hero only, after 90 s with no input while nothing runs;
|
|
16
18
|
// the rig wakes itself on a pointer move or a key
|
|
17
19
|
//
|
|
@@ -28,6 +30,7 @@
|
|
|
28
30
|
export const MASCOT_TIMING = Object.freeze({
|
|
29
31
|
watchEveryMs: 120,
|
|
30
32
|
watchIdleMs: 1500,
|
|
33
|
+
controlDwellMs: 1500,
|
|
31
34
|
sleepAfterMs: 90_000,
|
|
32
35
|
realWorkMs: 3000,
|
|
33
36
|
oneShotMs: Object.freeze({ success: 1000, error: 900 }),
|
|
@@ -67,6 +70,9 @@ export function lookToward(box, point, { horizontal = false } = {}) {
|
|
|
67
70
|
* getTarget() → the renderer's visible mascot { kind: 'hero'|'stage'|'working',
|
|
68
71
|
* rig, el, parked?(), hold(on), restore() } or null; caret() → the caret's
|
|
69
72
|
* client point or null.
|
|
73
|
+
* Control API: attend(owner, { pose, rectOf, options, open }), leave(owner),
|
|
74
|
+
* controlCommitted({ pose, options, reaction }); type(present, pointOf?) also
|
|
75
|
+
* watches a budget amount field using the same throttling as the composer.
|
|
70
76
|
* env: { now, setTimeout, clearTimeout, input(fn) → unsubscribe (page pointer
|
|
71
77
|
* and key input), lastInputAt() }.
|
|
72
78
|
* All of its state is its own: one director per panel, any number at once.
|
|
@@ -83,6 +89,9 @@ export function createMascotDirector({ getTarget = () => null, caret = () => nul
|
|
|
83
89
|
let holding = false; // hold(true) is in force on `target`
|
|
84
90
|
let shown = ''; // the pose (and then) set on `target`
|
|
85
91
|
let typing = false;
|
|
92
|
+
let typingPoint = null;
|
|
93
|
+
let attention = null; // { owner, pose, rectOf, options, open }; this panel's empty-state control
|
|
94
|
+
let attentionTimer = 0;
|
|
86
95
|
let typingTimer = 0;
|
|
87
96
|
let lookTimer = 0;
|
|
88
97
|
let lookedAt = -Infinity;
|
|
@@ -106,9 +115,16 @@ export function createMascotDirector({ getTarget = () => null, caret = () => nul
|
|
|
106
115
|
const point = card && card.width ? { x: card.left + Math.min(card.width * 0.3, 160), y: card.top + Math.min(card.height / 2, 28) } : null;
|
|
107
116
|
return { pose: 'blocked', look: lookToward(boxOf(t), point) };
|
|
108
117
|
}
|
|
109
|
-
|
|
110
|
-
|
|
118
|
+
const attentionLook = () => {
|
|
119
|
+
const rect = attention && safe(attention.rectOf);
|
|
120
|
+
return lookToward(boxOf(t), rect && { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 });
|
|
121
|
+
};
|
|
122
|
+
if (oneShot && oneShot.target === t) return { pose: oneShot.pose, opts: oneShot.opts || { then: 'idle' }, ...(attention ? { look: attentionLook() } : {}) };
|
|
123
|
+
if (typing) return { pose: 'watch', look: lookToward(boxOf(t), safe(typingPoint || caret), { horizontal: true }) };
|
|
111
124
|
if (sleeping && t.kind === 'hero' && !safe(() => t.parked?.())) return { pose: 'sleep' };
|
|
125
|
+
if (attention && t.kind === 'hero' && !safe(() => t.parked?.())) {
|
|
126
|
+
return { pose: attention.pose, opts: safe(attention.options) || {}, look: attentionLook() };
|
|
127
|
+
}
|
|
112
128
|
return null;
|
|
113
129
|
}
|
|
114
130
|
|
|
@@ -136,7 +152,7 @@ export function createMascotDirector({ getTarget = () => null, caret = () => nul
|
|
|
136
152
|
return;
|
|
137
153
|
}
|
|
138
154
|
if (!holding) { holding = true; safe(() => t.hold(true)); }
|
|
139
|
-
const key = `${want.pose}|${want.opts?.then || ''}`;
|
|
155
|
+
const key = `${want.pose}|${want.opts?.then || ''}|${!!want.opts?.unlimited}`;
|
|
140
156
|
if (key !== shown) { shown = key; safe(() => t.rig.setPose(want.pose, want.opts)); }
|
|
141
157
|
if (want.look) safe(() => t.rig.look(want.look.x, want.look.y));
|
|
142
158
|
}
|
|
@@ -155,7 +171,7 @@ export function createMascotDirector({ getTarget = () => null, caret = () => nul
|
|
|
155
171
|
|
|
156
172
|
function canSleep() {
|
|
157
173
|
const t = target;
|
|
158
|
-
return !destroyed && !!t && t.kind === 'hero' && !safe(() => t.parked?.()) && !turnActive && !typing && !blocked && !offline && !oneShot;
|
|
174
|
+
return !destroyed && !!t && t.kind === 'hero' && !safe(() => t.parked?.()) && !turnActive && !typing && !attention?.open && !blocked && !offline && !oneShot;
|
|
159
175
|
}
|
|
160
176
|
|
|
161
177
|
function armSleep() {
|
|
@@ -177,17 +193,20 @@ export function createMascotDirector({ getTarget = () => null, caret = () => nul
|
|
|
177
193
|
if (lookTimer) { cancel(lookTimer); lookTimer = 0; }
|
|
178
194
|
if (!typing) return;
|
|
179
195
|
typing = false;
|
|
196
|
+
typingPoint = null;
|
|
180
197
|
apply();
|
|
181
198
|
armSleep();
|
|
182
199
|
}
|
|
183
200
|
|
|
184
201
|
/** The draft changed: `present` false (an empty draft) ends the watch. */
|
|
185
|
-
function type(present = true) {
|
|
202
|
+
function type(present = true, pointOf = null) {
|
|
186
203
|
if (destroyed) return;
|
|
187
204
|
noteInput();
|
|
188
205
|
if (!present) { stopTyping(); return; }
|
|
206
|
+
if (oneShot?.control) dropOneShot(); // a new edit resumes watching after an acknowledgement
|
|
189
207
|
const first = !typing;
|
|
190
208
|
typing = true;
|
|
209
|
+
typingPoint = typeof pointOf === 'function' ? pointOf : null;
|
|
191
210
|
if (sleepTimer) { cancel(sleepTimer); sleepTimer = 0; }
|
|
192
211
|
if (typingTimer) cancel(typingTimer);
|
|
193
212
|
typingTimer = later(() => { typingTimer = 0; stopTyping(); }, MASCOT_TIMING.watchIdleMs);
|
|
@@ -221,13 +240,53 @@ export function createMascotDirector({ getTarget = () => null, caret = () => nul
|
|
|
221
240
|
armSleep();
|
|
222
241
|
}
|
|
223
242
|
|
|
243
|
+
function clearAttention() {
|
|
244
|
+
if (attentionTimer) { cancel(attentionTimer); attentionTimer = 0; }
|
|
245
|
+
attention = null;
|
|
246
|
+
}
|
|
247
|
+
|
|
224
248
|
return {
|
|
225
249
|
/** The draft changed (typing): the mascot watches the caret column. */
|
|
226
250
|
type,
|
|
227
251
|
/** Blur, or the draft went away. */
|
|
228
252
|
stopTyping,
|
|
253
|
+
/** Chip previews relax after a short dwell; only open menus hold attention and block sleep. */
|
|
254
|
+
attend(owner, spec) {
|
|
255
|
+
if (destroyed || (attention?.open && attention.owner !== owner && !spec.open)) return;
|
|
256
|
+
noteInput();
|
|
257
|
+
clearAttention();
|
|
258
|
+
attention = { owner, ...spec };
|
|
259
|
+
if (!attention.open) attentionTimer = later(() => {
|
|
260
|
+
clearAttention();
|
|
261
|
+
apply();
|
|
262
|
+
armSleep();
|
|
263
|
+
}, MASCOT_TIMING.controlDwellMs);
|
|
264
|
+
apply();
|
|
265
|
+
armSleep();
|
|
266
|
+
},
|
|
267
|
+
leave(owner) {
|
|
268
|
+
if (attention?.owner !== owner) return;
|
|
269
|
+
clearAttention();
|
|
270
|
+
apply();
|
|
271
|
+
armSleep();
|
|
272
|
+
},
|
|
273
|
+
/** Only acknowledged user commits call this; loads, live updates and keystrokes do not. */
|
|
274
|
+
controlCommitted({ pose, options = {}, reaction = 'nod' }) {
|
|
275
|
+
if (destroyed) return;
|
|
276
|
+
const t = safe(getTarget);
|
|
277
|
+
if (!t || t.kind !== 'hero' || safe(() => t.parked?.()) || turnActive || offline || blocked) return;
|
|
278
|
+
stopTyping();
|
|
279
|
+
noteInput();
|
|
280
|
+
dropOneShot();
|
|
281
|
+
const shot = { pose, opts: options, target: t, timer: 0, control: true };
|
|
282
|
+
shot.timer = later(() => { if (oneShot !== shot) return; oneShot = null; apply(); armSleep(); }, 900);
|
|
283
|
+
oneShot = shot;
|
|
284
|
+
apply();
|
|
285
|
+
safe(() => t.rig.react(reaction));
|
|
286
|
+
armSleep();
|
|
287
|
+
},
|
|
229
288
|
/** A prompt went out: the watch hands over at once (the renderer gives the hero to the "Thinking" row). */
|
|
230
|
-
sent() { stopTyping(); },
|
|
289
|
+
sent() { clearAttention(); dropOneShot(); stopTyping(); apply(); armSleep(); },
|
|
231
290
|
/** A card waits on the user (`rectOf()` → its box), or none (null). */
|
|
232
291
|
setBlocked(rectOf) {
|
|
233
292
|
blocked = typeof rectOf === 'function' ? rectOf : null;
|
|
@@ -248,6 +307,7 @@ export function createMascotDirector({ getTarget = () => null, caret = () => nul
|
|
|
248
307
|
},
|
|
249
308
|
turnStarted() {
|
|
250
309
|
turnActive = true;
|
|
310
|
+
clearAttention();
|
|
251
311
|
dropOneShot();
|
|
252
312
|
if (sleeping) sleeping = false;
|
|
253
313
|
apply();
|
|
@@ -260,14 +320,73 @@ export function createMascotDirector({ getTarget = () => null, caret = () => nul
|
|
|
260
320
|
/** The user is here (focus, a click): a sleeping hero wakes, the quiet starts over. */
|
|
261
321
|
touch() { if (!destroyed) noteInput(); },
|
|
262
322
|
/** For tests and devtools: what the director holds right now. */
|
|
263
|
-
state: () => ({ target: target?.kind || null, pose: shown.split('|')[0] || null, typing, blocked: !!blocked, offline, sleeping, oneShot: oneShot?.pose || null }),
|
|
323
|
+
state: () => ({ target: target?.kind || null, pose: shown.split('|')[0] || null, typing, attention: attention?.pose || null, menuOpen: !!attention?.open, blocked: !!blocked, offline, sleeping, oneShot: oneShot?.pose || null }),
|
|
264
324
|
destroy() {
|
|
265
325
|
if (destroyed) return;
|
|
266
326
|
if (target && holding) safe(() => target.hold(false));
|
|
267
327
|
destroyed = true;
|
|
328
|
+
clearAttention();
|
|
268
329
|
for (const id of [typingTimer, lookTimer, sleepTimer]) if (id) cancel(id);
|
|
269
330
|
dropOneShot();
|
|
270
331
|
if (stopInput) { safe(stopInput); stopInput = null; }
|
|
271
332
|
},
|
|
272
333
|
};
|
|
273
334
|
}
|
|
335
|
+
|
|
336
|
+
/** Wire one empty-state chip and its body-level menu to its panel's director. No rig or clock here. */
|
|
337
|
+
export function createMascotControl(anchor, { director = () => null, pose, options = () => ({}) } = {}) {
|
|
338
|
+
const owner = {};
|
|
339
|
+
let menu = null, preview = null, watched = false, destroyed = false;
|
|
340
|
+
const listeners = [];
|
|
341
|
+
const listen = (node, event, fn) => { node.addEventListener(event, fn); listeners.push(() => node.removeEventListener(event, fn)); };
|
|
342
|
+
function attend(node = anchor) {
|
|
343
|
+
if (destroyed) return;
|
|
344
|
+
preview = node;
|
|
345
|
+
director()?.attend(owner, { pose, options, open: !!menu, rectOf: () => node.getBoundingClientRect() });
|
|
346
|
+
}
|
|
347
|
+
const leave = () => { if (!menu) { preview = null; director()?.leave(owner); } };
|
|
348
|
+
listen(anchor, 'pointerenter', () => attend(menu || anchor));
|
|
349
|
+
listen(anchor, 'focus', () => attend(menu || anchor));
|
|
350
|
+
listen(anchor, 'pointerleave', () => { if (document.activeElement !== anchor) leave(); });
|
|
351
|
+
listen(anchor, 'blur', leave);
|
|
352
|
+
let stopMenu = () => {};
|
|
353
|
+
return {
|
|
354
|
+
open(node) {
|
|
355
|
+
if (destroyed || !node) return;
|
|
356
|
+
stopMenu();
|
|
357
|
+
menu = node;
|
|
358
|
+
const follow = e => {
|
|
359
|
+
const option = e.target.closest?.('.asst-dd-item, input, label, button');
|
|
360
|
+
if (option && node.contains(option)) attend(option);
|
|
361
|
+
};
|
|
362
|
+
const type = e => {
|
|
363
|
+
if (!e.target.matches?.('.asst-budget-input')) return;
|
|
364
|
+
watched = true;
|
|
365
|
+
director()?.type(true, () => {
|
|
366
|
+
const r = e.target.getBoundingClientRect();
|
|
367
|
+
return { x: r.left + Math.min(r.width - 12, 12 + e.target.value.length * 7), y: r.top + r.height / 2 };
|
|
368
|
+
});
|
|
369
|
+
};
|
|
370
|
+
node.addEventListener('pointerover', follow);
|
|
371
|
+
node.addEventListener('focusin', follow);
|
|
372
|
+
node.addEventListener('input', type);
|
|
373
|
+
stopMenu = () => {
|
|
374
|
+
node.removeEventListener('pointerover', follow);
|
|
375
|
+
node.removeEventListener('focusin', follow);
|
|
376
|
+
node.removeEventListener('input', type);
|
|
377
|
+
};
|
|
378
|
+
attend(node.contains(document.activeElement) ? document.activeElement : node);
|
|
379
|
+
},
|
|
380
|
+
close() {
|
|
381
|
+
stopMenu(); menu = null; preview = null;
|
|
382
|
+
if (watched) { director()?.stopTyping(); watched = false; }
|
|
383
|
+
director()?.leave(owner);
|
|
384
|
+
},
|
|
385
|
+
commit(reaction) { if (!destroyed) director()?.controlCommitted({ pose, options: options(), reaction }); },
|
|
386
|
+
refresh() { if (preview) attend(preview); },
|
|
387
|
+
destroy() {
|
|
388
|
+
this.close(); destroyed = true;
|
|
389
|
+
for (const off of listeners) off();
|
|
390
|
+
},
|
|
391
|
+
};
|
|
392
|
+
}
|
|
@@ -288,7 +288,8 @@ export function openMenu(anchor, opts = {}) {
|
|
|
288
288
|
closeMenu({ restoreFocus: !!item.restoreFocus });
|
|
289
289
|
item.onSelect?.(item, node);
|
|
290
290
|
};
|
|
291
|
-
|
|
291
|
+
if (opts.content) list.appendChild(opts.content);
|
|
292
|
+
else buildItems(list, items, { role, onActivate: activate });
|
|
292
293
|
if (width) menu.style.minWidth = opts.repositionOnResize ? `min(${width}px, calc(100vw - 16px))` : `${width}px`;
|
|
293
294
|
if (width && opts.repositionOnResize) menu.style.width = `min(${width}px, calc(100vw - 16px))`;
|
|
294
295
|
document.body.appendChild(menu);
|
|
@@ -311,6 +312,15 @@ export function openMenu(anchor, opts = {}) {
|
|
|
311
312
|
const nodes = focusables(menu);
|
|
312
313
|
const idx = nodes.indexOf(document.activeElement);
|
|
313
314
|
if (e.key === 'Escape') { e.preventDefault(); e.stopPropagation(); closeMenu({ restoreFocus: true }); return; }
|
|
315
|
+
if (role === 'dialog') {
|
|
316
|
+
if (e.key === 'Tab') {
|
|
317
|
+
const fields = [...menu.querySelectorAll('button, input')].filter(node => !node.disabled && !node.hidden);
|
|
318
|
+
const index = fields.indexOf(document.activeElement);
|
|
319
|
+
if (e.shiftKey && index <= 0) { e.preventDefault(); fields.at(-1)?.focus(); }
|
|
320
|
+
else if (!e.shiftKey && (index < 0 || index === fields.length - 1)) { e.preventDefault(); fields[0]?.focus(); }
|
|
321
|
+
}
|
|
322
|
+
return; // native number/switch keys and Enter belong to the form
|
|
323
|
+
}
|
|
314
324
|
if (e.key === 'Tab') { closeMenu(); return; }
|
|
315
325
|
if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
|
|
316
326
|
e.preventDefault(); e.stopPropagation();
|
|
@@ -347,12 +357,13 @@ export function openMenu(anchor, opts = {}) {
|
|
|
347
357
|
const onResize = () => opts.repositionOnResize ? positionMenu(menu, anchor, placement) : closeMenu();
|
|
348
358
|
const onBlur = () => closeMenu();
|
|
349
359
|
document.addEventListener('mousedown', onDocDown, true);
|
|
350
|
-
|
|
360
|
+
const keyTarget = role === 'dialog' ? window : document;
|
|
361
|
+
keyTarget.addEventListener('keydown', onKey, true);
|
|
351
362
|
window.addEventListener('resize', onResize);
|
|
352
363
|
window.addEventListener('blur', onBlur);
|
|
353
364
|
const cleanup = () => {
|
|
354
365
|
document.removeEventListener('mousedown', onDocDown, true);
|
|
355
|
-
|
|
366
|
+
keyTarget.removeEventListener('keydown', onKey, true);
|
|
356
367
|
window.removeEventListener('resize', onResize);
|
|
357
368
|
window.removeEventListener('blur', onBlur);
|
|
358
369
|
};
|
|
@@ -360,6 +371,7 @@ export function openMenu(anchor, opts = {}) {
|
|
|
360
371
|
_open = { menu, anchor, onClose, cleanup };
|
|
361
372
|
if (anchor.getAttribute('aria-haspopup')) anchor.setAttribute('aria-expanded', 'true');
|
|
362
373
|
anchor.setAttribute('aria-controls', menu.id);
|
|
374
|
+
opts.onOpen?.(menu);
|
|
363
375
|
|
|
364
376
|
const selected = list.querySelector('.asst-dd-item[data-checked="1"]:not([disabled])');
|
|
365
377
|
const first = focusables(menu).find(n => n.classList.contains('asst-dd-item')) || focusables(menu)[0];
|
|
@@ -29,6 +29,9 @@ import { emit, on, state as appState } from '../state.js';
|
|
|
29
29
|
import { storage } from '../storage.js';
|
|
30
30
|
import { KEYS } from '../constants.js';
|
|
31
31
|
import { t } from '../i18n.js';
|
|
32
|
+
// The account words of the brain picker and the accounts manager (account-view.js) in the page's language.
|
|
33
|
+
// Here, not in those modules: node tests load them, and i18n.js reads the page's storage.
|
|
34
|
+
import '../account-i18n.js';
|
|
32
35
|
import { forgetGreeting, mountGreeting } from '../greeting/greeting-card.js';
|
|
33
36
|
import { notify, NOTIF_TYPE } from '../ui-notifications.js';
|
|
34
37
|
import { getProviderMeta } from '../provider-icons.js';
|
|
@@ -51,7 +54,7 @@ import { createAgentsDock } from './asst-agents-dock.js';
|
|
|
51
54
|
import { createUsageGauge } from './asst-usage.js';
|
|
52
55
|
import { closeMenu, isMenuOpen, openMenu } from './asst-menu.js';
|
|
53
56
|
import { createRouteModeControl, openModelsManager, openRoutesEditor, refreshModelsManager, renderRouteCard, routeModeLabel } from './asst-route.js';
|
|
54
|
-
import { budgetChipText, budgetTone, openBudgetEditor } from './asst-budget.js';
|
|
57
|
+
import { budgetChipText, budgetSpendText, budgetTone, closeBudgetEditor, createBudgetController, mountBudgetChip, openBudgetEditor } from './asst-budget.js';
|
|
55
58
|
import { computerRemoteView, createComputerPanel, createComputerToggle, normalizeComputerRemote, openFrameLightbox, closeFrameLightbox, requestDesktopStop } from './asst-computer.js';
|
|
56
59
|
import {
|
|
57
60
|
applyLimit, applyLimitPacket, applyUsagePacket, brainLabel, createRenameWriter, createTabState, DEFAULT_ROUTE_MODE, deserializeTabState, fmtCost, fmtRunLine, limitExpiresIn, limitNoticeText, modelShortName,
|
|
@@ -365,11 +368,11 @@ export function mountAssistant(viewport, { sessionId, brain, host = {}, session
|
|
|
365
368
|
}
|
|
366
369
|
|
|
367
370
|
function renderCost() {
|
|
368
|
-
// The
|
|
369
|
-
const budget = st.budget && Number.isFinite(Number(st.budget.
|
|
370
|
-
const total = budget ? Number(budget.
|
|
371
|
-
costEl.textContent = fmtCost(total);
|
|
372
|
-
costEl.hidden = !(total > 0);
|
|
371
|
+
// The confirmed SessionBudget includes both brain and worker spend.
|
|
372
|
+
const budget = st.budget && Number.isFinite(Number(st.budget.spentUsd)) ? st.budget : null;
|
|
373
|
+
const total = budget ? Number(budget.spentUsd) : totalCost();
|
|
374
|
+
costEl.textContent = budget ? budgetSpendText(budget, { t }) : fmtCost(total);
|
|
375
|
+
costEl.hidden = !budget && !(total > 0);
|
|
373
376
|
const tone = budgetTone(budget);
|
|
374
377
|
if (tone && tone !== 'ok') costEl.dataset.tone = tone;
|
|
375
378
|
else delete costEl.dataset.tone;
|
|
@@ -379,6 +382,7 @@ export function mountAssistant(viewport, { sessionId, brain, host = {}, session
|
|
|
379
382
|
? tt('assistant.topbar.costSplit', 'Session {session} · agents {agents}', { session: fmtCost(st.costUsd || 0), agents: fmtCost(st.dispatchUsd) })
|
|
380
383
|
: tt('assistant.topbar.cost', 'Session cost');
|
|
381
384
|
costEl.setAttribute('data-tooltip', tip);
|
|
385
|
+
costEl.setAttribute('aria-label', `${tt('assistant.budget.title', 'Budget')}: ${tip}`);
|
|
382
386
|
}
|
|
383
387
|
|
|
384
388
|
function brainModelLabel() {
|
|
@@ -441,6 +445,12 @@ export function mountAssistant(viewport, { sessionId, brain, host = {}, session
|
|
|
441
445
|
});
|
|
442
446
|
|
|
443
447
|
// ── Renderer ──
|
|
448
|
+
let emptyBudgetChip = null;
|
|
449
|
+
const budgetController = createBudgetController({ t, sessionId, onSaved: (data) => {
|
|
450
|
+
if (destroyed) return;
|
|
451
|
+
if (data?.session) st.budget = data.session;
|
|
452
|
+
renderCost();
|
|
453
|
+
} });
|
|
444
454
|
const renderer = createRenderer(transcriptEl, {
|
|
445
455
|
t,
|
|
446
456
|
providerIcon,
|
|
@@ -463,6 +473,16 @@ export function mountAssistant(viewport, { sessionId, brain, host = {}, session
|
|
|
463
473
|
emptySuggestions: () => emptySuggestions(),
|
|
464
474
|
// "Choose a project" in the empty state is the toolbar's Working project field (asst-brain-picker.js).
|
|
465
475
|
mountEmptyProject: (slot) => picker.mountEmptyProject(slot),
|
|
476
|
+
mountEmptyBudget: (slot) => {
|
|
477
|
+
emptyBudgetChip?.destroy();
|
|
478
|
+
emptyBudgetChip = mountBudgetChip(slot, budgetController, { t, mascot: () => director });
|
|
479
|
+
return emptyBudgetChip.el;
|
|
480
|
+
},
|
|
481
|
+
unmountEmptyControls: () => {
|
|
482
|
+
picker.mountEmptyProject(null);
|
|
483
|
+
emptyBudgetChip?.destroy();
|
|
484
|
+
emptyBudgetChip = null;
|
|
485
|
+
},
|
|
466
486
|
onSuggestion: (template) => composer.fillTemplate(template),
|
|
467
487
|
heroOrigin: () => { try { return host.takeHeroOrigin?.() || null; } catch { return null; } },
|
|
468
488
|
onMascotChange: () => director?.retarget(),
|
|
@@ -549,7 +569,9 @@ export function mountAssistant(viewport, { sessionId, brain, host = {}, session
|
|
|
549
569
|
// Model · effort · permission mode · account sit in the composer row next to attach; project and MCP in the toolbar.
|
|
550
570
|
const picker = createBrainPicker({ brain: composer.el.querySelector('.asst-composer-brain'), context: barGroups.context }, {
|
|
551
571
|
t,
|
|
572
|
+
mascot: () => director,
|
|
552
573
|
getBrain: () => st.brain,
|
|
574
|
+
getRunning: () => st.running,
|
|
553
575
|
onChange: (next, meta) => applyBrain(next, meta),
|
|
554
576
|
onAddAccount: (provider) => openAccountsManager({ provider, ...accountHooks() }).openAdd(),
|
|
555
577
|
onManageAccounts: (provider) => openAccountsManager({ provider, ...accountHooks() }),
|
|
@@ -625,7 +647,10 @@ export function mountAssistant(viewport, { sessionId, brain, host = {}, session
|
|
|
625
647
|
function fetchUsage() {
|
|
626
648
|
const seq = ++usageFetchSeq;
|
|
627
649
|
getAssistantSessionUsage(sessionId).then((data) => {
|
|
628
|
-
if (!destroyed && seq === usageFetchSeq && data?.ok
|
|
650
|
+
if (!destroyed && seq === usageFetchSeq && data?.ok) {
|
|
651
|
+
if (data.budget) budgetController.receiveSession(data.budget);
|
|
652
|
+
if (data.usage) applyUsage(data.usage);
|
|
653
|
+
}
|
|
629
654
|
}).catch(() => { /* older servers do not expose usage */ });
|
|
630
655
|
}
|
|
631
656
|
function applyUsage(packet) {
|
|
@@ -813,6 +838,9 @@ export function mountAssistant(viewport, { sessionId, brain, host = {}, session
|
|
|
813
838
|
case 'assistant:mailbox':
|
|
814
839
|
renderer.appendMailbox(packet.items || packet.events || packet.mailbox || []);
|
|
815
840
|
return;
|
|
841
|
+
case 'assistant:budget':
|
|
842
|
+
budgetController.receiveSession(packet.budget);
|
|
843
|
+
return;
|
|
816
844
|
case 'assistant:cost':
|
|
817
845
|
applyCost(packet);
|
|
818
846
|
return;
|
|
@@ -885,7 +913,7 @@ export function mountAssistant(viewport, { sessionId, brain, host = {}, session
|
|
|
885
913
|
if (session != null) st.costUsd = session;
|
|
886
914
|
else if (!packet?.runId && num(packet?.costUsd) != null) st.costUsd = num(packet.costUsd);
|
|
887
915
|
if (num(packet?.dispatchUsd) != null) st.dispatchUsd = num(packet.dispatchUsd);
|
|
888
|
-
if (packet?.budget && typeof packet.budget === 'object')
|
|
916
|
+
if (packet?.budget && typeof packet.budget === 'object') budgetController.receiveSession(packet.budget);
|
|
889
917
|
if (typeof packet?.brainMetered === 'boolean') st.brainMetered = packet.brainMetered;
|
|
890
918
|
if (packet?.runId && num(packet.costUsd) != null) dock.patchRun(packet.runId, { costUsd: num(packet.costUsd) });
|
|
891
919
|
if (session != null) st.serverCost = true;
|
|
@@ -905,7 +933,7 @@ export function mountAssistant(viewport, { sessionId, brain, host = {}, session
|
|
|
905
933
|
const next = normalizeBrain({ ...st.brain, provider: meta.brain, model: meta.model ?? st.brain.model, effort: meta.effort ?? st.brain.effort, accountId: meta.accountId ?? st.brain.accountId, mcpProfile: meta.mcpProfile ?? st.brain.mcpProfile, project: meta.cwd ?? st.brain.project });
|
|
906
934
|
if (JSON.stringify(next) !== JSON.stringify(st.brain)) { st.brain = next; picker.render(); host.setBrain?.(next); persist(); }
|
|
907
935
|
}
|
|
908
|
-
if (meta.budget && typeof meta.budget === 'object')
|
|
936
|
+
if (meta.budget && typeof meta.budget === 'object') budgetController.receiveSession(meta.budget);
|
|
909
937
|
if (Number.isFinite(Number(meta.costUsd))) { st.costUsd = Number(meta.costUsd); renderCost(); }
|
|
910
938
|
if (meta.providerSessionId) st.providerSessionId = meta.providerSessionId;
|
|
911
939
|
if (meta.routing && typeof meta.routing === 'object') {
|
|
@@ -1468,13 +1496,13 @@ export function mountAssistant(viewport, { sessionId, brain, host = {}, session
|
|
|
1468
1496
|
if (meta.silent) return;
|
|
1469
1497
|
if (meta.requiresSwitch) {
|
|
1470
1498
|
const b = st.brain;
|
|
1471
|
-
sock.send({ type: 'switch_brain', brain: b.provider, model: b.model || undefined, effort: b.effort || undefined, accountId: b.accountId || undefined, mcpProfile: b.mcpProfile || undefined, cwd: b.project || undefined, agent: b.agent || undefined, permissionMode: b.permissionMode, planMode: b.planMode === true });
|
|
1499
|
+
if (!meta.restartBrain) sock.send({ type: 'switch_brain', brain: b.provider, model: b.model || undefined, effort: b.effort || undefined, accountId: b.accountId || undefined, mcpProfile: b.mcpProfile || undefined, cwd: b.project || undefined, agent: b.agent || undefined, permissionMode: b.permissionMode, planMode: b.planMode === true });
|
|
1472
1500
|
renderer.appendDivider(`${tt('assistant.status.brain', 'brain')} → ${brainLabel(b)}`);
|
|
1473
1501
|
renderer.forgetSeen(); // the new brain's ids start over
|
|
1474
1502
|
} else if (before.permissionMode !== st.brain.permissionMode || !!before.planMode !== !!st.brain.planMode) {
|
|
1475
1503
|
sock.send({ type: 'set_permission_mode', mode: st.brain.permissionMode, planMode: st.brain.planMode === true });
|
|
1476
1504
|
}
|
|
1477
|
-
patchAssistantSession(sessionId, { brain: st.brain }).catch((err) => {
|
|
1505
|
+
patchAssistantSession(sessionId, { brain: st.brain, ...(meta.restartBrain ? { restartBrain: true } : {}) }).catch((err) => {
|
|
1478
1506
|
// Refused (a model disabled in Assistant → Models): undo, so the refused
|
|
1479
1507
|
// brain is neither shown nor saved as the last used one — the next start
|
|
1480
1508
|
// would ask for it again. A brain switch already got the error on the socket.
|
|
@@ -1653,7 +1681,7 @@ export function mountAssistant(viewport, { sessionId, brain, host = {}, session
|
|
|
1653
1681
|
items.push({ id: 'compact', label: tt('assistant.topbar.compact', 'Compact context'), onSelect: () => compact() });
|
|
1654
1682
|
if (routingAvailable()) items.push({ id: 'routes', label: tt('assistant.routes.editor', 'Model routes…'), icon: ICON_ROUTE, onSelect: () => openRoutes() });
|
|
1655
1683
|
items.push({ id: 'models', label: tt('assistant.models.menu', 'Models…'), onSelect: () => openModels() });
|
|
1656
|
-
items.push({ id: 'budget', label: tt('assistant.budget.menu', 'Budget…'), desc: st.budget
|
|
1684
|
+
items.push({ id: 'budget', label: tt('assistant.budget.menu', 'Budget…'), desc: budgetSpendText(st.budget, { t }), onSelect: () => openBudget() });
|
|
1657
1685
|
items.push({ id: 'clear', label: tt('assistant.topbar.clear', 'Clear transcript'), onSelect: () => clearTranscript() });
|
|
1658
1686
|
items.push({ id: 'help', label: tt('assistant.topbar.helpShortcuts', 'Help & shortcuts'), onSelect: () => openHelp() });
|
|
1659
1687
|
items.push({ kind: 'separator' });
|
|
@@ -1707,12 +1735,12 @@ export function mountAssistant(viewport, { sessionId, brain, host = {}, session
|
|
|
1707
1735
|
}
|
|
1708
1736
|
}
|
|
1709
1737
|
|
|
1710
|
-
// Assistant → Budget:
|
|
1738
|
+
// Assistant → Budget: one saved amount plus the current session's live split.
|
|
1711
1739
|
function openBudget() {
|
|
1712
1740
|
openBudgetEditor({
|
|
1713
1741
|
t,
|
|
1714
1742
|
sessionId,
|
|
1715
|
-
|
|
1743
|
+
controller: budgetController,
|
|
1716
1744
|
onToast: toast,
|
|
1717
1745
|
onSaved: (res) => { if (res?.session) { st.budget = res.session; renderCost(); } },
|
|
1718
1746
|
}).catch(() => {});
|
|
@@ -1821,6 +1849,8 @@ export function mountAssistant(viewport, { sessionId, brain, host = {}, session
|
|
|
1821
1849
|
|
|
1822
1850
|
// ── Bus ──
|
|
1823
1851
|
unsubs.push(on('sync:assistant:session-updated', (msg) => { const s = msg?.session || msg; if (s?.id === sessionId) applySessionMeta(s); }));
|
|
1852
|
+
unsubs.push(on('sync:assistant:budget', (msg) => { if ((msg?.assistantSessionId || msg?.sessionId) === sessionId) budgetController.receiveSession(msg.budget); }));
|
|
1853
|
+
unsubs.push(on('sync:assistant:budget-changed', () => budgetController.load()));
|
|
1824
1854
|
unsubs.push(on('sync:assistant:cost', (msg) => { if (!msg?.assistantSessionId || msg.assistantSessionId === sessionId) applyCost(msg); }));
|
|
1825
1855
|
unsubs.push(on('sync:assistant:mailbox', (msg) => { if (msg?.assistantSessionId === sessionId) renderer.appendMailbox(msg.items || msg.events || []); }));
|
|
1826
1856
|
unsubs.push(on('sync:assistant:permission-request', (msg) => {
|
|
@@ -1891,6 +1921,7 @@ export function mountAssistant(viewport, { sessionId, brain, host = {}, session
|
|
|
1891
1921
|
sock.connect();
|
|
1892
1922
|
});
|
|
1893
1923
|
fetchUsage();
|
|
1924
|
+
budgetController.load();
|
|
1894
1925
|
|
|
1895
1926
|
const controller = {
|
|
1896
1927
|
sessionId,
|
|
@@ -1934,6 +1965,9 @@ export function mountAssistant(viewport, { sessionId, brain, host = {}, session
|
|
|
1934
1965
|
if (persistTimer) { clearTimeout(persistTimer); persistTimer = null; }
|
|
1935
1966
|
clearTimeout(announceTimer);
|
|
1936
1967
|
if (replay) { clearTimeout(replay.timer); replay = null; }
|
|
1968
|
+
closeBudgetEditor(budgetController);
|
|
1969
|
+
emptyBudgetChip?.destroy();
|
|
1970
|
+
budgetController.destroy();
|
|
1937
1971
|
director.destroy();
|
|
1938
1972
|
usage.destroy();
|
|
1939
1973
|
clearTimeout(limitTimer);
|
|
@@ -228,6 +228,7 @@ export function updateRunCard(card, run, opts = {}) {
|
|
|
228
228
|
const r = run.route;
|
|
229
229
|
const routeBits = [r.taskClassLabel || r.taskClass || t('assistant.routes.task', 'task')];
|
|
230
230
|
if (r.status === 'pending') routeBits.push(t('assistant.routes.state.pending', 'waiting for your choice'));
|
|
231
|
+
else if (r.decidedBy === 'budget' || r.source === 'budget') routeBits.push(t('assistant.routes.budgetDecision', 'budget re-route'));
|
|
231
232
|
else if (r.decidedBy === 'user') routeBits.push(t('assistant.routes.youChose', 'you chose'));
|
|
232
233
|
else if (r.remembered || r.decidedBy === 'remembered') routeBits.push(t('assistant.routes.remembered', 'your saved choice'));
|
|
233
234
|
else if (Number.isFinite(Number(r.confidence))) routeBits.push(`${Math.round(Number(r.confidence) <= 1 ? Number(r.confidence) * 100 : Number(r.confidence))}%`);
|
|
@@ -548,8 +549,10 @@ export function createRenderer(transcriptEl, hooks = {}) {
|
|
|
548
549
|
|
|
549
550
|
/** A menu opened from the empty state's project picker does not outlive it. */
|
|
550
551
|
function closeEmptyMenu() {
|
|
551
|
-
const btn
|
|
552
|
-
|
|
552
|
+
for (const btn of emptyEl?.querySelectorAll('.asst-empty-project-btn') || []) {
|
|
553
|
+
if (isMenuOpen(btn)) closeMenu();
|
|
554
|
+
}
|
|
555
|
+
hooks.unmountEmptyControls?.();
|
|
553
556
|
}
|
|
554
557
|
|
|
555
558
|
// ── The hero ──
|
|
@@ -2689,7 +2692,12 @@ export function createRenderer(transcriptEl, hooks = {}) {
|
|
|
2689
2692
|
|
|
2690
2693
|
function upsertRunCard(run, { result = null } = {}) {
|
|
2691
2694
|
if (!run?.runId) return null;
|
|
2692
|
-
|
|
2695
|
+
const prev = runInfo.get(run.runId);
|
|
2696
|
+
const status = run.status || prev?.state;
|
|
2697
|
+
const tone = status ? runTone({ ...run, status }) : '';
|
|
2698
|
+
const routeId = run.route?.routeId || prev?.routeId || '';
|
|
2699
|
+
runInfo.set(run.runId, { ...prev, title: run.title || run.task || prev?.title, state: status, provider: run.provider, routeId, working: tone === 'running' || tone === 'queued' });
|
|
2700
|
+
if (routeId) syncRouteLive(routeId);
|
|
2693
2701
|
const cardOpts = runCardOpts(result);
|
|
2694
2702
|
let card = runCards.get(run.runId);
|
|
2695
2703
|
if (card && card.isConnected) {
|
|
@@ -2745,21 +2753,42 @@ export function createRenderer(transcriptEl, hooks = {}) {
|
|
|
2745
2753
|
return { phase, t: hooks.t, providerIcon: hooks.providerIcon, brainLabel: hooks.brainLabel, onChangeRoutes: hooks.onChangeRoutes };
|
|
2746
2754
|
}
|
|
2747
2755
|
|
|
2748
|
-
/** Create or update the line for `route.routeId` in place. */
|
|
2756
|
+
/** Create or update the line for `route.routeId` in place. Only a new line enters (never on replay). */
|
|
2749
2757
|
function upsertRouteLine(route, phase = 'decided') {
|
|
2750
2758
|
const routeId = route?.routeId;
|
|
2751
2759
|
if (!routeId) return null;
|
|
2752
2760
|
const existing = routeLines.get(routeId);
|
|
2753
|
-
if (existing && existing.el.isConnected) { existing.update(route, phase); return existing; }
|
|
2761
|
+
if (existing && existing.el.isConnected) { existing.update(route, phase); syncRouteLive(routeId); return existing; }
|
|
2754
2762
|
const line = createRouteLine(route, routeLineOpts(phase));
|
|
2755
2763
|
routeLines.set(routeId, line);
|
|
2756
2764
|
append(line.el);
|
|
2765
|
+
syncRouteLive(routeId);
|
|
2766
|
+
enterRow(line.el);
|
|
2757
2767
|
return line;
|
|
2758
2768
|
}
|
|
2759
2769
|
|
|
2760
2770
|
/** Adopt a line created by a collapsing route card so later events update it. */
|
|
2761
2771
|
function registerRouteLine(routeId, line) {
|
|
2762
|
-
if (routeId
|
|
2772
|
+
if (!routeId || !line) return;
|
|
2773
|
+
const fresh = routeLines.get(routeId) !== line;
|
|
2774
|
+
routeLines.set(routeId, line);
|
|
2775
|
+
syncRouteLive(routeId);
|
|
2776
|
+
if (fresh && line.el?.isConnected) enterRow(line.el);
|
|
2777
|
+
}
|
|
2778
|
+
|
|
2779
|
+
/** The line is live while a run it dispatched is queued or working (run cards keep runInfo current). */
|
|
2780
|
+
function syncRouteLive(routeId) {
|
|
2781
|
+
const line = routeLines.get(routeId);
|
|
2782
|
+
if (!line?.setRunning) return;
|
|
2783
|
+
let working = false;
|
|
2784
|
+
for (const info of runInfo.values()) if (info.routeId === routeId && info.working) { working = true; break; }
|
|
2785
|
+
line.setRunning(working);
|
|
2786
|
+
}
|
|
2787
|
+
|
|
2788
|
+
/** A new row's entrance: the rack ledger's (4px rise and fade, --asst-dur-fast, --asst-ease-enter); none when calm. */
|
|
2789
|
+
function enterRow(node) {
|
|
2790
|
+
if (calm() || typeof node?.animate !== 'function') return;
|
|
2791
|
+
node.animate([{ opacity: 0, transform: 'translateY(4px)' }, { opacity: 1, transform: 'none' }], { duration: 140, easing: cssEase('--asst-ease-enter', 'cubic-bezier(0,0,.38,.9)') });
|
|
2763
2792
|
}
|
|
2764
2793
|
|
|
2765
2794
|
function removeRouteLine(routeId) {
|
|
@@ -2899,7 +2928,9 @@ export function createRenderer(transcriptEl, hooks = {}) {
|
|
|
2899
2928
|
const projectSlot = emptyEl.querySelector('.asst-empty-project');
|
|
2900
2929
|
let picker = null;
|
|
2901
2930
|
try { picker = typeof hooks.mountEmptyProject === 'function' ? hooks.mountEmptyProject(projectSlot) : null; } catch { picker = null; }
|
|
2902
|
-
|
|
2931
|
+
let budget = null;
|
|
2932
|
+
try { budget = hooks.mountEmptyBudget?.(projectSlot); } catch { /* unavailable */ }
|
|
2933
|
+
projectSlot.hidden = !picker && !budget;
|
|
2903
2934
|
renderSuggestions();
|
|
2904
2935
|
$msgs.appendChild(emptyEl);
|
|
2905
2936
|
// The host's "Starting…" character flies into place (not on replay or reduced motion).
|