synabun 2.1.0 → 2.2.0
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 +121 -0
- package/README.md +1 -1
- package/docs/whatsapp.md +11 -11
- package/lib/client-config-repair.js +13 -9
- package/lib/external-tools.js +6 -5
- package/lib/update-snapshot.js +77 -3
- package/lib/virustotal.mjs +12 -1
- package/mcp-server/dist/index.js +1 -1
- package/mcp-server/dist/index.js.map +1 -1
- package/mcp-server/dist/services/budget-judgments.d.ts +1 -1
- package/mcp-server/dist/services/budget-judgments.d.ts.map +1 -1
- package/mcp-server/dist/services/budget-judgments.js +5 -3
- package/mcp-server/dist/services/budget-judgments.js.map +1 -1
- package/mcp-server/dist/services/identity.d.ts +2 -2
- package/mcp-server/dist/services/identity.js +2 -2
- package/mcp-server/dist/services/memory-sessions.d.ts +1 -1
- package/mcp-server/dist/services/memory-sessions.d.ts.map +1 -1
- package/mcp-server/dist/services/memory-sessions.js +28 -2
- package/mcp-server/dist/services/memory-sessions.js.map +1 -1
- package/mcp-server/dist/services/neural-interface.d.ts +2 -1
- package/mcp-server/dist/services/neural-interface.d.ts.map +1 -1
- package/mcp-server/dist/services/neural-interface.js.map +1 -1
- package/mcp-server/dist/services/server-instructions.d.ts.map +1 -1
- package/mcp-server/dist/services/server-instructions.js +2 -1
- package/mcp-server/dist/services/server-instructions.js.map +1 -1
- package/mcp-server/dist/services/tool-catalog.js +1 -1
- package/mcp-server/dist/services/tool-catalog.js.map +1 -1
- package/mcp-server/dist/services/tool-list.d.ts +1 -1
- package/mcp-server/dist/services/tool-list.d.ts.map +1 -1
- package/mcp-server/dist/services/tool-runtime.d.ts +2 -2
- package/mcp-server/dist/services/tool-runtime.d.ts.map +1 -1
- package/mcp-server/dist/services/tool-runtime.js +1 -1
- package/mcp-server/dist/services/tool-runtime.js.map +1 -1
- package/mcp-server/dist/services/typesafe-config.js +1 -1
- package/mcp-server/dist/services/typesafe-config.js.map +1 -1
- package/mcp-server/dist/tools/agents.d.ts +8 -8
- package/mcp-server/dist/tools/agents.d.ts.map +1 -1
- package/mcp-server/dist/tools/agents.js +10 -9
- package/mcp-server/dist/tools/agents.js.map +1 -1
- package/mcp-server/dist/tools/choice.d.ts +6 -0
- package/mcp-server/dist/tools/choice.d.ts.map +1 -1
- package/mcp-server/dist/tools/choice.js +18 -13
- package/mcp-server/dist/tools/choice.js.map +1 -1
- package/mcp-server/dist/tools/git-tools.js +2 -2
- package/mcp-server/dist/tools/git-tools.js.map +1 -1
- package/mcp-server/dist/tools/morelogin.d.ts.map +1 -1
- package/mcp-server/dist/tools/morelogin.js +9 -6
- package/mcp-server/dist/tools/morelogin.js.map +1 -1
- package/mcp-server/dist/tools/skill-tools.d.ts +8 -8
- package/mcp-server/dist/tools/skill-tools.d.ts.map +1 -1
- package/mcp-server/dist/tools/skill-tools.js +11 -8
- package/mcp-server/dist/tools/skill-tools.js.map +1 -1
- package/mcp-server/package-lock.json +2 -2
- package/mcp-server/package.json +1 -1
- package/mcp-server/src/index.ts +1 -1
- package/mcp-server/src/services/budget-judgments.ts +5 -3
- package/mcp-server/src/services/identity.ts +2 -2
- package/mcp-server/src/services/memory-sessions.ts +24 -3
- package/mcp-server/src/services/neural-interface.ts +2 -1
- package/mcp-server/src/services/server-instructions.ts +2 -1
- package/mcp-server/src/services/tool-catalog.ts +1 -1
- package/mcp-server/src/services/tool-list.ts +1 -1
- package/mcp-server/src/services/tool-runtime.ts +2 -2
- package/mcp-server/src/services/typesafe-config.ts +1 -1
- package/mcp-server/src/tools/agents.ts +10 -9
- package/mcp-server/src/tools/choice.ts +17 -12
- package/mcp-server/src/tools/git-tools.ts +2 -2
- package/mcp-server/src/tools/morelogin.ts +6 -4
- package/mcp-server/src/tools/skill-tools.ts +11 -8
- package/neural-interface/i18n/en.json +501 -39
- package/neural-interface/i18n/pt-BR.json +501 -39
- package/neural-interface/lib/agent-sandbox-elevation.js +6 -0
- package/neural-interface/lib/antigravity/acp-client-host.js +554 -0
- package/neural-interface/lib/antigravity/acp-connection.js +299 -0
- package/neural-interface/lib/antigravity/bypass.js +100 -0
- package/neural-interface/lib/antigravity/catalog.js +273 -0
- package/neural-interface/lib/antigravity/home.js +260 -0
- package/neural-interface/lib/antigravity/migrate-gemini.js +139 -0
- package/neural-interface/lib/antigravity/panel-bridge.js +1058 -0
- package/neural-interface/lib/antigravity/policy-hook.mjs +78 -0
- package/neural-interface/lib/antigravity/policy.js +74 -0
- package/neural-interface/lib/antigravity/tool-call.js +178 -0
- package/neural-interface/lib/antigravity/transcripts.js +252 -0
- package/neural-interface/lib/antigravity/usage.js +43 -0
- package/neural-interface/lib/assistant-api.js +1 -0
- package/neural-interface/lib/assistant-brains/antigravity.js +423 -0
- package/neural-interface/lib/assistant-budget-planner.js +7 -3
- package/neural-interface/lib/assistant-budget-split.js +42 -15
- package/neural-interface/lib/assistant-catalog.js +76 -9
- package/neural-interface/lib/assistant-config.js +6 -3
- package/neural-interface/lib/assistant-dispatch.js +207 -49
- package/neural-interface/lib/assistant-envelope.js +153 -1
- package/neural-interface/lib/assistant-evidence.js +80 -0
- package/neural-interface/lib/assistant-hidden-models.js +7 -5
- package/neural-interface/lib/assistant-persona.js +16 -11
- package/neural-interface/lib/assistant-plan-permissions.js +15 -0
- package/neural-interface/lib/assistant-router.js +237 -35
- package/neural-interface/lib/assistant-runtime.js +158 -12
- package/neural-interface/lib/assistant-task-prompt.js +10 -8
- package/neural-interface/lib/assistant-tools.js +2 -2
- package/neural-interface/lib/assistant-usage.js +29 -0
- package/neural-interface/lib/backup-service.js +6 -1
- package/neural-interface/lib/browser-guard-cdp.js +5 -4
- package/neural-interface/lib/browser-network-guard.js +274 -82
- package/neural-interface/lib/browser-tool-policy.js +12 -7
- package/neural-interface/lib/claude-agent-bridge.js +7 -0
- package/neural-interface/lib/claude-history-cache.js +210 -0
- package/neural-interface/lib/claude-model-catalog.js +5 -1
- package/neural-interface/lib/cli-install-api.js +1 -1
- package/neural-interface/lib/cli-install.js +160 -31
- package/neural-interface/lib/cli-resolver.js +9 -2
- package/neural-interface/lib/cli-update-planner.js +25 -15
- package/neural-interface/lib/codex-rollout-reader.js +52 -0
- package/neural-interface/lib/codex-worker-app-server.js +25 -6
- package/neural-interface/lib/editor-git-api.js +3 -20
- package/neural-interface/lib/exec-loop-command.js +9 -2
- package/neural-interface/lib/file-explorer-api.js +74 -40
- package/neural-interface/lib/file-explorer-watch.js +9 -3
- package/neural-interface/lib/git-bash/install.js +52 -6
- package/neural-interface/lib/git-bash/manager.js +59 -17
- package/neural-interface/lib/git-bash/pins.js +12 -0
- package/neural-interface/lib/git-read.js +2 -2
- package/neural-interface/lib/git-status-runner.js +390 -0
- package/neural-interface/lib/git-status-watchdog.js +40 -0
- package/neural-interface/lib/greeting/budgets.js +1 -0
- package/neural-interface/lib/greeting/engine.js +2 -2
- package/neural-interface/lib/greeting/sources.js +10 -12
- package/neural-interface/lib/greeting/writer.js +1 -1
- package/neural-interface/lib/mcp-client-config.js +37 -6
- package/neural-interface/lib/mcp-external-servers.js +13 -4
- package/neural-interface/lib/mcp-installer.js +2 -1
- package/neural-interface/lib/memory-session-capture.js +17 -3
- package/neural-interface/lib/morelogin.js +147 -25
- package/neural-interface/lib/native-loop-providers.js +420 -21
- package/neural-interface/lib/native-loop-runtime.js +23 -2
- package/neural-interface/lib/opencode-native-boundary.js +11 -2
- package/neural-interface/lib/owner-authority.js +1 -1
- package/neural-interface/lib/path-confine.js +12 -9
- package/neural-interface/lib/peer-identity.js +32 -2
- package/neural-interface/lib/pty-host/core.js +28 -1
- package/neural-interface/lib/pty-host/lifetime.js +12 -0
- package/neural-interface/lib/pty-host/manager.js +12 -1
- package/neural-interface/lib/remote-policy.js +1 -1
- package/neural-interface/lib/request-gate.js +3 -0
- package/neural-interface/lib/rulesets/api.js +6 -5
- package/neural-interface/lib/rulesets/fs-safe.js +1 -1
- package/neural-interface/lib/rulesets/installer.js +89 -10
- package/neural-interface/lib/rulesets/render.js +1 -1
- package/neural-interface/lib/session-cache-builder.js +64 -1
- package/neural-interface/lib/skills/catalog.js +5 -1
- package/neural-interface/lib/skills/creation-brief.js +1 -1
- package/neural-interface/lib/skills/creation-sources.js +19 -1
- package/neural-interface/lib/skills/deploy.js +9 -4
- package/neural-interface/lib/skills/draft-confinement.js +3 -1
- package/neural-interface/lib/skills/eval-confinement.js +3 -1
- package/neural-interface/lib/skills/eval-trace.js +16 -0
- package/neural-interface/lib/skills/export.js +5 -2
- package/neural-interface/lib/skills/known-bridge-digests.json +58 -4
- package/neural-interface/lib/skills/ownership.js +1 -1
- package/neural-interface/lib/skills/roots.js +10 -3
- package/neural-interface/lib/skills/runtime-checks.js +6 -3
- package/neural-interface/lib/skills/service.js +15 -7
- package/neural-interface/lib/skills/telemetry.js +6 -0
- package/neural-interface/lib/synabun-version.js +1 -1
- package/neural-interface/lib/system-activity.js +1 -1
- package/neural-interface/lib/whatsapp/brain.js +7 -3
- package/neural-interface/lib/whatsapp/cards.js +22 -9
- package/neural-interface/lib/whatsapp/config.js +1 -1
- package/neural-interface/lib/worker-confinement.js +77 -1
- package/neural-interface/lib/worker-question.js +74 -0
- package/neural-interface/lib/worker-resume.js +13 -4
- package/neural-interface/lib/worker-sandbox.js +46 -16
- package/neural-interface/package-lock.json +18 -2
- package/neural-interface/package.json +2 -1
- package/neural-interface/public/onboarding.html +1 -1
- package/neural-interface/public/shared/agent-runtime-options.js +89 -11
- package/neural-interface/public/shared/agy/agy-bypass.js +47 -0
- package/neural-interface/public/shared/agy/agy-client.js +266 -0
- package/neural-interface/public/shared/agy/agy-commands.js +120 -0
- package/neural-interface/public/shared/agy/agy-copy.js +326 -0
- package/neural-interface/public/shared/agy/agy-diff.js +161 -0
- package/neural-interface/public/shared/agy/agy-events.js +469 -0
- package/neural-interface/public/shared/agy/agy-greeting.js +26 -0
- package/neural-interface/public/shared/agy/agy-options.js +232 -0
- package/neural-interface/public/shared/agy/agy-panel.js +2543 -0
- package/neural-interface/public/shared/agy/agy-render.js +392 -0
- package/neural-interface/public/shared/agy/agy-setup.js +149 -0
- package/neural-interface/public/shared/agy/agy-styles.js +604 -0
- package/neural-interface/public/shared/agy/agy-tabs-logic.js +181 -0
- package/neural-interface/public/shared/api.js +4 -4
- package/neural-interface/public/shared/assistant/asst-agents-dock.js +3 -4
- package/neural-interface/public/shared/assistant/asst-api.js +1 -1
- package/neural-interface/public/shared/assistant/asst-brain-picker.js +56 -9
- package/neural-interface/public/shared/assistant/asst-budget.js +5 -4
- package/neural-interface/public/shared/assistant/asst-control.js +48 -0
- package/neural-interface/public/shared/assistant/asst-hosts.js +16 -7
- package/neural-interface/public/shared/assistant/asst-limit-dock.js +6 -1
- package/neural-interface/public/shared/assistant/asst-limit-wait.js +38 -5
- package/neural-interface/public/shared/assistant/asst-panel.js +45 -8
- package/neural-interface/public/shared/assistant/asst-render.js +51 -12
- package/neural-interface/public/shared/assistant/asst-route.js +237 -57
- package/neural-interface/public/shared/assistant/asst-routes-editor.js +9 -8
- package/neural-interface/public/shared/assistant/asst-scenes.js +54 -1
- package/neural-interface/public/shared/assistant/asst-sidepanel.js +51 -7
- package/neural-interface/public/shared/assistant/asst-slash.js +8 -2
- package/neural-interface/public/shared/assistant/asst-state.js +76 -9
- package/neural-interface/public/shared/assistant/asst-styles.js +34 -0
- package/neural-interface/public/shared/assistant/asst-tool-kinds.js +34 -7
- package/neural-interface/public/shared/assistant/asst-usage.js +2 -1
- package/neural-interface/public/shared/cdx/cdx-output.js +26 -3
- package/neural-interface/public/shared/cdx/cdx-panel.js +14 -2
- package/neural-interface/public/shared/cdx/cdx-preview.js +12 -0
- package/neural-interface/public/shared/cdx/cdx-protocol.js +11 -10
- package/neural-interface/public/shared/cdx/cdx-render.js +10 -9
- package/neural-interface/public/shared/cdx/cdx-requests.js +11 -1
- package/neural-interface/public/shared/cdx/cdx-styles.js +4 -0
- package/neural-interface/public/shared/cdx/cdx-tabs.js +75 -5
- package/neural-interface/public/shared/cli-install-controller.js +218 -0
- package/neural-interface/public/shared/cli-install-view.js +7 -4
- package/neural-interface/public/shared/cli-status.js +9 -8
- package/neural-interface/public/shared/constants.js +3 -2
- package/neural-interface/public/shared/context-menu-model.js +1 -0
- package/neural-interface/public/shared/cp/cp-restore.js +162 -0
- package/neural-interface/public/shared/cp/cp-tool-cards.js +10 -3
- package/neural-interface/public/shared/desktop-bridge.js +17 -0
- package/neural-interface/public/shared/desktop-titlebar-view.js +12 -0
- package/neural-interface/public/shared/desktop-titlebar.js +13 -4
- package/neural-interface/public/shared/desktop-update-view.js +1 -0
- package/neural-interface/public/shared/explorer/fx-api.js +4 -4
- package/neural-interface/public/shared/explorer/fx-links.js +3 -3
- package/neural-interface/public/shared/explorer/fx-logic.js +1 -1
- package/neural-interface/public/shared/explorer/fx-main.js +48 -16
- package/neural-interface/public/shared/explorer/fx-tree.js +1 -0
- package/neural-interface/public/shared/greeting/greeting-card.js +2 -2
- package/neural-interface/public/shared/greeting/greeting-view.js +1 -0
- package/neural-interface/public/shared/html-shell.js +21 -4
- package/neural-interface/public/shared/ocp-v2/ocp-v2-panel.js +27 -7
- package/neural-interface/public/shared/ocp-v2/ocp-v2-rehydrate.js +13 -4
- package/neural-interface/public/shared/ocp-v2/ocp-v2-render.js +7 -1
- package/neural-interface/public/shared/ocp-v2/ocp-v2-styles.js +17 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-ws.js +46 -3
- package/neural-interface/public/shared/provider-icons.js +4 -2
- package/neural-interface/public/shared/settings/settings-rules.js +2 -2
- package/neural-interface/public/shared/settings/settings-skill-folders-pane.js +1 -1
- package/neural-interface/public/shared/settings/settings-skill-folders.js +2 -2
- package/neural-interface/public/shared/showcase-privacy.js +6 -1
- package/neural-interface/public/shared/sidepanel-session-status.js +3 -3
- package/neural-interface/public/shared/skills/ss-creation.js +6 -3
- package/neural-interface/public/shared/skills/ss-dialog.js +5 -2
- package/neural-interface/public/shared/skills/ss-lint.js +9 -1
- package/neural-interface/public/shared/skills/ss-profiles.js +44 -6
- package/neural-interface/public/shared/skills/ss-review-view.js +2 -2
- package/neural-interface/public/shared/skills/studio/sx-actions.js +1 -1
- package/neural-interface/public/shared/skills/studio/sx-create-ai-view.js +1 -1
- package/neural-interface/public/shared/skills/studio/sx-create-ai.js +2 -1
- package/neural-interface/public/shared/skills/studio/sx-dom.js +2 -1
- package/neural-interface/public/shared/skills/studio/sx-fixes-view.js +359 -0
- package/neural-interface/public/shared/skills/studio/sx-fixes.js +327 -0
- package/neural-interface/public/shared/skills/studio/sx-inspector.js +1 -1
- package/neural-interface/public/shared/skills/studio/sx-install-flow.js +9 -6
- package/neural-interface/public/shared/skills/studio/sx-install.js +20 -3
- package/neural-interface/public/shared/skills/studio/sx-library-view.js +144 -35
- package/neural-interface/public/shared/skills/studio/sx-library.js +6 -2
- package/neural-interface/public/shared/skills/studio/sx-main.js +11 -2
- package/neural-interface/public/shared/skills/studio/sx-review-view.js +1 -1
- package/neural-interface/public/shared/skills/studio/sx-subject.js +7 -6
- package/neural-interface/public/shared/skills/studio/sx-workspace-view.js +6 -5
- package/neural-interface/public/shared/state.js +1 -1
- package/neural-interface/public/shared/streaming-markdown.js +58 -0
- package/neural-interface/public/shared/styles.css +354 -34
- package/neural-interface/public/shared/text-cache.js +29 -0
- package/neural-interface/public/shared/tool-list-picker.js +2 -2
- package/neural-interface/public/shared/tool-list-view.js +2 -2
- package/neural-interface/public/shared/topright-overflow-logic.js +54 -0
- package/neural-interface/public/shared/ui-antigravity-panel.js +19 -0
- package/neural-interface/public/shared/ui-assistant.js +2 -0
- package/neural-interface/public/shared/ui-automation-studio.js +20 -12
- package/neural-interface/public/shared/ui-claude-panel.js +299 -77
- package/neural-interface/public/shared/ui-cli-banner.js +23 -167
- package/neural-interface/public/shared/ui-file-explorer.js +48 -6
- package/neural-interface/public/shared/ui-file-links.js +2 -2
- package/neural-interface/public/shared/ui-games.js +2 -2
- package/neural-interface/public/shared/ui-judgments.js +1 -1
- package/neural-interface/public/shared/ui-keybinds.js +8 -2
- package/neural-interface/public/shared/ui-link.js +3 -3
- package/neural-interface/public/shared/ui-menubar.js +2 -2
- package/neural-interface/public/shared/ui-native-loop-router.js +4 -0
- package/neural-interface/public/shared/ui-navbar.js +45 -0
- package/neural-interface/public/shared/ui-notifications.js +1 -0
- package/neural-interface/public/shared/ui-resume.js +9 -7
- package/neural-interface/public/shared/ui-schedules-studio.js +41 -10
- package/neural-interface/public/shared/ui-scroll-follow.js +16 -5
- package/neural-interface/public/shared/ui-sessions.js +22 -12
- package/neural-interface/public/shared/ui-settings.js +90 -65
- package/neural-interface/public/shared/ui-sidepanel-session-entry.js +9 -2
- package/neural-interface/public/shared/ui-sidepanel-sessions.js +3 -3
- package/neural-interface/public/shared/ui-sidepanel-tray.js +48 -7
- package/neural-interface/public/shared/ui-sidepanel-windows.js +3 -3
- package/neural-interface/public/shared/ui-terminal.js +146 -10
- package/neural-interface/public/shared/ui-tool-updates.js +3 -3
- package/neural-interface/public/shared/ui-tooltip.js +71 -46
- package/neural-interface/public/shared/ui-topright-overflow.js +233 -0
- package/neural-interface/public/shared/ui-whiteboard.js +1 -1
- package/neural-interface/public/shared/ui-worker-end.js +1 -1
- package/neural-interface/public/shared/whatsapp/wa-view.js +2 -2
- package/neural-interface/public/shared/worker-questions.js +34 -0
- package/neural-interface/public/sidepanel-session.html +1 -1
- package/neural-interface/server.js +644 -426
- package/neural-interface/templates/rulesets/README.md +1 -1
- package/neural-interface/templates/rulesets/legacy-hashes.json +5 -5
- package/neural-interface/templates/rulesets/manifest.json +1 -1
- package/npm-shrinkwrap.json +18 -2
- package/package.json +2 -1
- package/skills/skill-forge/SKILL.md +4 -4
- package/skills/synabun/SKILL.md +1 -1
- package/skills/synabun/modules/schedule.md +3 -3
- /package/neural-interface/templates/rulesets/{gemini.md → antigravity.md} +0 -0
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
// normalizeRouteRequest / routeOptions / buildRouteResponse / routeLineParts
|
|
11
11
|
// are pure (node:test covers them); nothing touches `document` at import time.
|
|
12
12
|
//
|
|
13
|
+
// The card's Here / Dispatch switch reads the packet's `sides` / `defaults`
|
|
14
|
+
// (routeCardSides; derived from the options' kinds for an older server).
|
|
13
15
|
// Wire contract: answer = control_response { kind:'route', optionId, target?,
|
|
14
16
|
// remember, decline? }; "Other…" sends optionId:'other' + target. Invalid
|
|
15
17
|
// answers come back as { type:'error', code:'ROUTE_INVALID' } — the card stays
|
|
@@ -17,7 +19,6 @@
|
|
|
17
19
|
// declined|cancelled) update the line in place by routeId.
|
|
18
20
|
|
|
19
21
|
import { fetchClaudeAccounts, fetchCodexAccounts, getEffortLevelsForModel } from '../agent-runtime-options.js';
|
|
20
|
-
import { getProviderMeta } from '../provider-icons.js';
|
|
21
22
|
import { fetchManageCatalog, patchHiddenModels } from './asst-api.js';
|
|
22
23
|
import { catalogModelRow, fmtModelPrice, hiddenModelFor, ICON_CARET, invalidateModelCatalog, openModelMenu } from './asst-brain-picker.js';
|
|
23
24
|
// The Routes editor's model lists (where they come from, how they stay current): asst-route-models.js.
|
|
@@ -28,12 +29,17 @@ import { accountUsageText, autoAccountLabel } from '../account-usage.js';
|
|
|
28
29
|
import {
|
|
29
30
|
DEFAULT_ROUTE_MODE,
|
|
30
31
|
fmtConfidence,
|
|
32
|
+
modelShortName,
|
|
31
33
|
normalizeBrain,
|
|
32
34
|
normalizeProvider,
|
|
33
35
|
normalizeRouteMode,
|
|
36
|
+
providerLabel,
|
|
37
|
+
providerMeta,
|
|
34
38
|
ROUTE_MODES,
|
|
35
39
|
routeKindLabel,
|
|
36
40
|
routeTargetLabel,
|
|
41
|
+
OPTIONAL_PROVIDERS,
|
|
42
|
+
UNPRICED_PROVIDERS,
|
|
37
43
|
} from './asst-state.js';
|
|
38
44
|
|
|
39
45
|
function str(v) { return v == null ? '' : String(v).trim(); }
|
|
@@ -109,8 +115,12 @@ function normalizeRouteOption(o) {
|
|
|
109
115
|
model: str(src.model),
|
|
110
116
|
effort: str(src.effort) || null,
|
|
111
117
|
...(Object.hasOwn(src, 'accountId') ? { accountId: src.accountId, accountChoice: src.accountChoice || null, account: obj(src.account), accountError: obj(src.accountError) } : {}),
|
|
118
|
+
// 'direct' on a worker ("here" on an account other than this conversation's): it runs on a worker.
|
|
119
|
+
accountKind: str(src.accountKind) || null,
|
|
112
120
|
label: str(src.label),
|
|
113
121
|
badge: str(src.badge) || null,
|
|
122
|
+
// Always ask: the user's saved route for the class (its side starts on it; the card still asks).
|
|
123
|
+
saved: src.saved === true,
|
|
114
124
|
tier: str(src.tier) || null,
|
|
115
125
|
vision: src.vision === true,
|
|
116
126
|
price: priceOf(src.price),
|
|
@@ -123,6 +133,13 @@ function normalizeRouteOption(o) {
|
|
|
123
133
|
};
|
|
124
134
|
}
|
|
125
135
|
|
|
136
|
+
function normalizeRouteSides(sides) {
|
|
137
|
+
const src = obj(sides);
|
|
138
|
+
if (!src) return null;
|
|
139
|
+
const side = (s) => (obj(s) ? { available: s.available !== false, reason: str(s.reason) || null, code: str(s.code) || null } : null);
|
|
140
|
+
return { direct: side(src.direct), dispatch: side(src.dispatch) };
|
|
141
|
+
}
|
|
142
|
+
|
|
126
143
|
/** Normalize a route control packet (or its `request`) into the card model. */
|
|
127
144
|
export function normalizeRouteRequest(packet) {
|
|
128
145
|
const p = obj(packet);
|
|
@@ -162,6 +179,9 @@ export function normalizeRouteRequest(packet) {
|
|
|
162
179
|
},
|
|
163
180
|
options: arr(req.options).map(normalizeRouteOption).filter(Boolean),
|
|
164
181
|
defaultOptionId: str(req.defaultOptionId) || null,
|
|
182
|
+
// The Here / Dispatch switch, as the router read it (null from a server older than the switch).
|
|
183
|
+
sides: normalizeRouteSides(req.sides),
|
|
184
|
+
defaults: obj(req.defaults) ? { direct: str(req.defaults.direct) || null, dispatch: str(req.defaults.dispatch) || null } : null,
|
|
165
185
|
other: {
|
|
166
186
|
allowed: other.allowed !== false,
|
|
167
187
|
providers: arr(other.providers).map(String),
|
|
@@ -220,15 +240,25 @@ export function routeReasonLabels(n, t) {
|
|
|
220
240
|
return arr(n?.reasons).map(r => reasonLabel(r, tr, n?.requires)).filter(Boolean);
|
|
221
241
|
}
|
|
222
242
|
|
|
243
|
+
/** The word for a route kind in the user's language: "here", "dispatch", "computer". */
|
|
244
|
+
export function routeKindWord(kind, t = null) {
|
|
245
|
+
const key = str(kind).toLowerCase();
|
|
246
|
+
const word = key === 'direct' ? 'here' : key;
|
|
247
|
+
return ['here', 'dispatch', 'computer'].includes(word) ? tf(t)(`assistant.routes.kind.${word}`, routeKindLabel(key)) : routeKindLabel(key);
|
|
248
|
+
}
|
|
249
|
+
|
|
223
250
|
function badgeText(o, n, tr) {
|
|
224
251
|
const pct = fmtConfidence(n?.confidence);
|
|
252
|
+
if (o.saved) return tr('assistant.routes.badge.saved', 'Saved route');
|
|
225
253
|
switch (o.badge) {
|
|
226
254
|
case 'suggested': return pct ? tr('assistant.routes.badge.suggestedPct', 'Suggested · {pct}', { pct }) : tr('assistant.routes.badge.suggested', 'Suggested');
|
|
227
255
|
case 'remembered': return tr('assistant.routes.badge.remembered', 'Your choice');
|
|
228
|
-
|
|
256
|
+
// On a worker, the model this conversation runs on is "the same model"; here it is the current one.
|
|
257
|
+
case 'current': return o.kind === 'dispatch' ? tr('assistant.routes.badge.sameModel', 'Same model') : tr('assistant.routes.badge.current', 'Current');
|
|
229
258
|
case 'capable':
|
|
230
|
-
if (
|
|
231
|
-
|
|
259
|
+
if (n?.requires) return n.requires === 'video' ? tr('assistant.routes.badge.makesVideo', 'Makes video') : tr('assistant.routes.badge.makesImages', 'Makes images');
|
|
260
|
+
if (n?.visionOnly || n?.needsVision) return tr('assistant.routes.badge.canSee', 'Can see');
|
|
261
|
+
return tr('assistant.routes.badge.canRun', 'Can run it');
|
|
232
262
|
case 'cheaper': return tr('assistant.routes.badge.cheaper', 'Cheaper');
|
|
233
263
|
case 'stronger': return tr('assistant.routes.badge.stronger', 'Stronger');
|
|
234
264
|
default: return o.badge ? humanizeClass(o.badge) : '';
|
|
@@ -272,26 +302,78 @@ export function routeOptions(n, { brainLabel = '', t = null } = {}) {
|
|
|
272
302
|
if (!o.isHere && o.id !== 'here' && o.kind === 'direct' && brainProvider && o.provider === brainProvider
|
|
273
303
|
&& modelKey(o.model) === modelKey(n.brain?.model)) o.isHere = true;
|
|
274
304
|
}
|
|
275
|
-
|
|
305
|
+
// Only a direct option is "here": a "here" the account rule moved to a worker (kind "dispatch") is a worker row.
|
|
306
|
+
const hereRow = (o) => o.kind === 'direct' && (o.id === 'here' || !!o.isHere);
|
|
307
|
+
const rank = (o) => (o.id === defaultId ? -2 : hereRow(o) ? -1 : (BADGE_RANK[o.badge] ?? 9));
|
|
276
308
|
kept.sort((a, b) => rank(a) - rank(b));
|
|
277
309
|
if (!kept.some(o => o.id === defaultId && !o.disabled)) defaultId = kept.find(o => !o.disabled)?.id || null;
|
|
278
310
|
const hereModel = brainLabel || routeTargetLabel(n.brain) || tr('assistant.routes.currentModel', 'the current model');
|
|
311
|
+
// The router labels every option done here "Do it here with <model>" (English): the title names the model once,
|
|
312
|
+
// in the user's language ("Do it here with Do it here with Sonnet 5" before).
|
|
313
|
+
const labelOf = (o) => str(o.label).replace(/^do it here with\s+/i, '');
|
|
279
314
|
return kept.map((o, i) => {
|
|
280
|
-
const isHere = o
|
|
315
|
+
const isHere = hereRow(o);
|
|
281
316
|
const label = routeTargetLabel(o);
|
|
282
317
|
return {
|
|
283
318
|
...o,
|
|
284
319
|
number: i + 1,
|
|
285
320
|
here: isHere,
|
|
286
|
-
// "here" is the brain's own model: prefer its display label over a raw id.
|
|
287
|
-
|
|
321
|
+
// "here" is the brain's own model: prefer its display label over a raw id. Another model of its
|
|
322
|
+
// provider (a continuation) is done here too, and says so the same way.
|
|
323
|
+
title: isHere ? tr('assistant.routes.doHere', 'Do it here with {model}', { model: labelOf(o) || hereModel })
|
|
324
|
+
: o.kind === 'direct' ? tr('assistant.routes.doHere', 'Do it here with {model}', { model: labelOf(o) || modelShortName(o.model) || o.id }) : (label || o.id),
|
|
288
325
|
badgeText: badgeText(o, n, tr),
|
|
289
|
-
disabledReason: o.budget === 'share_used_up' ? routeBudgetReason({ provider: o.provider }, t) : o.disabledReason,
|
|
326
|
+
disabledReason: o.budget === 'share_used_up' || o.budget === 'unpriced' ? routeBudgetReason({ provider: o.provider, unpriced: o.budget === 'unpriced' }, t) : o.disabledReason,
|
|
290
327
|
selected: o.id === defaultId,
|
|
291
328
|
};
|
|
292
329
|
});
|
|
293
330
|
}
|
|
294
331
|
|
|
332
|
+
export const ROUTE_SIDES = Object.freeze(['direct', 'dispatch']);
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* The card's Here / Dispatch switch (pure): `options` (routeOptions) split by where they run, each
|
|
336
|
+
* side numbered from 1, with whether it can be chosen, why not, and the option it starts on.
|
|
337
|
+
* → { initial: 'direct' | 'dispatch', sides: { direct, dispatch } }, a side being
|
|
338
|
+
* { kind, options, available, reason, code, needsPick, defaultId }.
|
|
339
|
+
* The router says which side can run (n.sides) and where each starts (n.defaults); a card from a
|
|
340
|
+
* server older than the switch has neither, so both are read from the options: there, a card with
|
|
341
|
+
* no worker option still has a Dispatch side, empty, that asks for a model (`needsPick`: the model
|
|
342
|
+
* picker answers optionId "other" with a dispatch target, which every server takes).
|
|
343
|
+
*/
|
|
344
|
+
export function routeCardSides(n, options, { t = null } = {}) {
|
|
345
|
+
const tr = tf(t);
|
|
346
|
+
const server = n?.sides || null;
|
|
347
|
+
const side = (kind) => {
|
|
348
|
+
const list = arr(options).filter((o) => (o.kind === 'direct' ? 'direct' : 'dispatch') === kind).map((o, i) => ({ ...o, number: i + 1 }));
|
|
349
|
+
const usable = list.filter((o) => !o.disabled);
|
|
350
|
+
const said = server?.[kind] || null;
|
|
351
|
+
const out = { kind, options: list, available: usable.length > 0, reason: null, code: null, needsPick: false, defaultId: null };
|
|
352
|
+
out.defaultId = (usable.find((o) => o.id === n?.defaultOptionId) || usable.find((o) => o.id === n?.defaults?.[kind]) || usable.find((o) => o.selected) || usable[0])?.id || null;
|
|
353
|
+
if (out.available) return out;
|
|
354
|
+
if (kind === 'direct' && n?.dispatchOnly) {
|
|
355
|
+
return { ...out, code: 'worker_only', reason: tr('assistant.routes.side.workerOnly', '{taskClass} always runs on a worker.', { taskClass: n.taskClassLabel || humanizeClass(n.taskClass) }) };
|
|
356
|
+
}
|
|
357
|
+
if (kind === 'direct' && (said?.code === 'account_worker' || arr(options).some((o) => o.kind === 'dispatch' && o.accountKind === 'direct'))) {
|
|
358
|
+
return { ...out, code: 'account_worker', reason: tr('assistant.routes.side.accountWorker', "This conversation's account can't take it now, so it runs on a worker.") };
|
|
359
|
+
}
|
|
360
|
+
// A worker can still be picked by model (an older server sent none, or every one offered is blocked).
|
|
361
|
+
if (kind === 'dispatch' && n?.other?.allowed !== false) {
|
|
362
|
+
return { ...out, available: true, needsPick: true, code: list.length ? 'disabled' : 'none', reason: tr('assistant.routes.side.pickWorker', 'Pick a worker model to run it.') };
|
|
363
|
+
}
|
|
364
|
+
const blocked = list.find((o) => o.disabled);
|
|
365
|
+
return {
|
|
366
|
+
...out, code: said?.code || (blocked ? 'disabled' : 'none'),
|
|
367
|
+
reason: blocked?.disabledReason || said?.reason || (kind === 'direct' ? tr('assistant.routes.side.noHere', 'Nothing in this conversation can run this task.') : tr('assistant.routes.side.noWorker', 'No worker model can run this task.')),
|
|
368
|
+
};
|
|
369
|
+
};
|
|
370
|
+
const sides = { direct: side('direct'), dispatch: side('dispatch') };
|
|
371
|
+
const chosen = arr(options).find((o) => o.selected && !o.disabled);
|
|
372
|
+
let initial = chosen ? (chosen.kind === 'direct' ? 'direct' : 'dispatch') : null;
|
|
373
|
+
if (!initial || !sides[initial].available) initial = sides.direct.available && sides.direct.defaultId ? 'direct' : 'dispatch';
|
|
374
|
+
return { initial, sides };
|
|
375
|
+
}
|
|
376
|
+
|
|
295
377
|
/** control_response payload for a route card. `decline:true` is the Cancel answer. */
|
|
296
378
|
export function buildRouteResponse(n, choice = {}) {
|
|
297
379
|
if (choice.decline) return { kind: 'route', optionId: null, remember: false, decline: true };
|
|
@@ -369,11 +451,16 @@ export function routeCatalogRow(pref, catalog) {
|
|
|
369
451
|
return pref.model ? catalogModelRow(pref.provider, pref.model, catalog) || aliasRow(pref.provider, pref.model, rows) : rows.find(m => m.isDefault) || null;
|
|
370
452
|
}
|
|
371
453
|
|
|
372
|
-
/**
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
454
|
+
/**
|
|
455
|
+
* Budget messages are UI copy; never show the server's English disabledReason. `unpriced` (option.budget
|
|
456
|
+
* 'unpriced'): the provider reports tokens and no price, so a session with a budget cannot hold its spend.
|
|
457
|
+
*/
|
|
458
|
+
export function routeBudgetReason({ provider, dry, unpriced = false } = {}, t) {
|
|
459
|
+
const tr = tf(t);
|
|
460
|
+
if (provider && unpriced) return tr('assistant.routes.budgetUnpriced', 'Budget: {provider} reports tokens, not prices, so a session with a budget cannot run it. Pick another option, or switch the budget to No limit.', { provider: providerLabel(provider) });
|
|
461
|
+
if (provider) return tr('assistant.routes.budgetProvider', "Budget: the {provider} share of this session's budget is used up.", { provider: providerLabel(provider) });
|
|
462
|
+
// A provider that reports no prices (Antigravity) never had a share under a budget: it is not "used up".
|
|
463
|
+
const list = [...new Set(arr(dry))].filter(id => !UNPRICED_PROVIDERS.includes(id)).map(id => providerLabel(id));
|
|
377
464
|
if (!list.length) return tr('assistant.routes.budgetSpent', "Budget: this session's budget is spent.");
|
|
378
465
|
return tr(list.length === 1 ? 'assistant.routes.budgetDryOne' : 'assistant.routes.budgetDryMany',
|
|
379
466
|
list.length === 1 ? 'Budget: the {list} share is used up.' : 'Budget: the {list} shares are used up.', { list: list.join(', ') });
|
|
@@ -415,7 +502,7 @@ export function routeLineParts(route, phase = 'decided', { brainLabel = '', t =
|
|
|
415
502
|
return {
|
|
416
503
|
taskClass: cls,
|
|
417
504
|
kind,
|
|
418
|
-
kindLabel: kind && kind !== 'direct' ?
|
|
505
|
+
kindLabel: kind && kind !== 'direct' ? routeKindWord(kind, t) : '',
|
|
419
506
|
provider: target ? normalizeProvider(target.provider, str(target.provider) || null) : null,
|
|
420
507
|
targetLabel: target && !state ? routeTargetLabel(target) : '',
|
|
421
508
|
meta,
|
|
@@ -441,6 +528,9 @@ function correctionText(c, tr) {
|
|
|
441
528
|
}
|
|
442
529
|
if (c.reason === 'remembered_route') return tr('assistant.routes.correction.remembered', 'your saved route {to} replaced {from}', { to: str(c.to), from: str(c.from) });
|
|
443
530
|
if (c.reason === 'remembered_disabled') return tr('assistant.routes.correction.rememberedDisabled', 'saved route {from} is disabled, kept the pick', { from: str(c.from) });
|
|
531
|
+
// A "here" pick that runs on a worker: another account, or another provider than this conversation's.
|
|
532
|
+
if (c.reason === 'account_worker') return tr('assistant.routes.correction.accountWorker', '{to}: runs on a worker, because this conversation belongs to another account', { to: str(c.to) });
|
|
533
|
+
if (c.reason === 'other_provider' && c.provider) return tr('assistant.routes.correction.otherProvider', "{provider} is not this conversation's provider, so it runs on a worker", { provider: providerLabel(c.provider) });
|
|
444
534
|
const text = firstStr(c.text, c.summary, c.message);
|
|
445
535
|
if (text) return text;
|
|
446
536
|
if (c.field && (c.from || c.to)) return `${c.field}: ${str(c.from) || '—'} → ${str(c.to) || 'default'}`;
|
|
@@ -485,12 +575,12 @@ function providerIconHtml(provider, hooks) {
|
|
|
485
575
|
if (!provider) return '';
|
|
486
576
|
try {
|
|
487
577
|
if (typeof hooks.providerIcon === 'function') return hooks.providerIcon(provider) || '';
|
|
488
|
-
return
|
|
578
|
+
return providerMeta(provider).icon || '';
|
|
489
579
|
} catch { return ''; }
|
|
490
580
|
}
|
|
491
581
|
|
|
492
582
|
function providerColor(provider) {
|
|
493
|
-
try { return
|
|
583
|
+
try { return providerMeta(provider).color || ''; } catch { return ''; }
|
|
494
584
|
}
|
|
495
585
|
|
|
496
586
|
let _lineUid = 0;
|
|
@@ -711,9 +801,12 @@ export function renderRouteCard(container, input, hooks = {}) {
|
|
|
711
801
|
const tr = tf(hooks.t);
|
|
712
802
|
const brainLabel = () => (typeof hooks.brainLabel === 'function' ? hooks.brainLabel() : '') || routeTargetLabel(n.brain) || tr('assistant.routes.theBrainLower', 'the assistant');
|
|
713
803
|
const options = routeOptions(n, { brainLabel: brainLabel(), t: hooks.t });
|
|
804
|
+
// Here / Dispatch: each side has its own list, pick and "other" model; the switch decides which is shown and answered.
|
|
805
|
+
const view = routeCardSides(n, options, { t: hooks.t });
|
|
714
806
|
const state = {
|
|
715
|
-
|
|
716
|
-
|
|
807
|
+
side: view.initial,
|
|
808
|
+
picks: { direct: view.sides.direct.defaultId, dispatch: view.sides.dispatch.defaultId },
|
|
809
|
+
others: { direct: null, dispatch: null },
|
|
717
810
|
remember: false,
|
|
718
811
|
pending: false,
|
|
719
812
|
resolved: false,
|
|
@@ -752,6 +845,25 @@ export function renderRouteCard(container, input, hooks = {}) {
|
|
|
752
845
|
card.appendChild(note);
|
|
753
846
|
}
|
|
754
847
|
|
|
848
|
+
// The switch: Here (this conversation) or Dispatch (a worker). A side that cannot run the task stays
|
|
849
|
+
// in view, marked unavailable, with why (worker-only classes, a blind or hidden model, another account).
|
|
850
|
+
const sidesEl = el('div', 'asst-route-sides');
|
|
851
|
+
sidesEl.setAttribute('role', 'radiogroup');
|
|
852
|
+
sidesEl.setAttribute('aria-label', tr('assistant.routes.side.label', 'Where it runs'));
|
|
853
|
+
const sideBtns = {};
|
|
854
|
+
for (const kind of ROUTE_SIDES) {
|
|
855
|
+
const btn = el('button', 'asst-route-side', kind === 'direct' ? tr('assistant.routes.side.here', 'Here') : tr('assistant.routes.side.dispatch', 'Dispatch'));
|
|
856
|
+
btn.type = 'button';
|
|
857
|
+
btn.dataset.side = kind;
|
|
858
|
+
btn.setAttribute('role', 'radio');
|
|
859
|
+
btn.addEventListener('click', () => setSide(kind));
|
|
860
|
+
sideBtns[kind] = btn;
|
|
861
|
+
sidesEl.appendChild(btn);
|
|
862
|
+
}
|
|
863
|
+
const sideNote = el('div', 'asst-route-side-note');
|
|
864
|
+
sideNote.setAttribute('aria-live', 'polite');
|
|
865
|
+
card.append(sidesEl, sideNote);
|
|
866
|
+
|
|
755
867
|
const list = el('div', 'asst-route-options');
|
|
756
868
|
list.setAttribute('role', 'radiogroup');
|
|
757
869
|
list.setAttribute('aria-label', tr('assistant.routes.chooseModel', 'Where should this run?'));
|
|
@@ -764,7 +876,7 @@ export function renderRouteCard(container, input, hooks = {}) {
|
|
|
764
876
|
row.setAttribute('aria-checked', 'false');
|
|
765
877
|
row.setAttribute('aria-disabled', o.disabled ? 'true' : 'false');
|
|
766
878
|
row.tabIndex = -1;
|
|
767
|
-
const kindWord = o.kind && o.kind !== 'direct' ?
|
|
879
|
+
const kindWord = o.kind && o.kind !== 'direct' ? routeKindWord(o.kind, hooks.t) : '';
|
|
768
880
|
const sub = [kindWord, routeDisplayAccount(o, hooks.t), o.effort && o.effort !== 'off' ? o.effort : '', o.price ? priceText(o.price) : '', o.vision ? tr('assistant.brain.vision', 'vision') : '', o.reason].filter(Boolean).join(' · ');
|
|
769
881
|
row.innerHTML = `
|
|
770
882
|
<span class="asst-route-num" aria-hidden="true">${esc(o.number)}</span>
|
|
@@ -791,7 +903,11 @@ export function renderRouteCard(container, input, hooks = {}) {
|
|
|
791
903
|
return b != null ? `$${a != null ? f(a) : '?'}/$${f(b)}` : `$${f(a)}`;
|
|
792
904
|
}
|
|
793
905
|
|
|
794
|
-
|
|
906
|
+
// The shown side's options (numbered from 1), then its "other" model when one was picked.
|
|
907
|
+
const sideOptions = () => view.sides[state.side].options;
|
|
908
|
+
const allOptions = () => (state.others[state.side] ? [...sideOptions(), state.others[state.side]] : sideOptions());
|
|
909
|
+
const selectedId = () => state.picks[state.side] || null;
|
|
910
|
+
const busy = () => state.pending || state.resolved || state.expired;
|
|
795
911
|
|
|
796
912
|
function renderOptions() {
|
|
797
913
|
list.innerHTML = '';
|
|
@@ -801,20 +917,55 @@ export function renderRouteCard(container, input, hooks = {}) {
|
|
|
801
917
|
|
|
802
918
|
function syncSelection() {
|
|
803
919
|
for (const row of list.querySelectorAll('.asst-route-option')) {
|
|
804
|
-
const on = row.dataset.optionId ===
|
|
920
|
+
const on = row.dataset.optionId === selectedId();
|
|
805
921
|
row.setAttribute('aria-checked', on ? 'true' : 'false');
|
|
806
922
|
row.classList.toggle('selected', on);
|
|
807
923
|
row.tabIndex = on ? 0 : -1;
|
|
808
924
|
}
|
|
809
925
|
syncFields();
|
|
810
|
-
|
|
926
|
+
syncSides();
|
|
927
|
+
runBtn.disabled = busy() || !selectedId();
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
/** The switch, its note (why the other side is unavailable, what a worker side still needs, else what the side means) and the Run label. */
|
|
931
|
+
function syncSides() {
|
|
932
|
+
card.dataset.side = state.side;
|
|
933
|
+
for (const kind of ROUTE_SIDES) {
|
|
934
|
+
const btn = sideBtns[kind];
|
|
935
|
+
const side = view.sides[kind];
|
|
936
|
+
const on = state.side === kind;
|
|
937
|
+
btn.setAttribute('aria-checked', on ? 'true' : 'false');
|
|
938
|
+
btn.classList.toggle('selected', on);
|
|
939
|
+
btn.setAttribute('aria-disabled', side.available && !busy() ? 'false' : 'true');
|
|
940
|
+
btn.tabIndex = on ? 0 : -1;
|
|
941
|
+
if (!side.available && side.reason) btn.setAttribute('title', side.reason);
|
|
942
|
+
else btn.removeAttribute('title');
|
|
943
|
+
}
|
|
944
|
+
const current = view.sides[state.side];
|
|
945
|
+
const other = view.sides[state.side === 'direct' ? 'dispatch' : 'direct'];
|
|
946
|
+
sideNote.textContent = !other.available && other.reason ? other.reason
|
|
947
|
+
: current.needsPick && !selectedId() ? current.reason
|
|
948
|
+
: state.side === 'direct' ? tr('assistant.routes.side.hereDesc', 'Runs in this conversation.') : tr('assistant.routes.side.dispatchDesc', 'A worker runs it; the result comes back here.');
|
|
949
|
+
runBtn.textContent = state.side === 'direct' ? tr('assistant.routes.runHere', 'Run here') : tr('assistant.routes.runDispatch', 'Dispatch');
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
/** Show the other side (its own pick stays as it was). `focus`: 'switch' (arrow keys on it) or 'option'. */
|
|
953
|
+
function setSide(kind, { focus = null } = {}) {
|
|
954
|
+
if (busy() || !view.sides[kind]?.available) return;
|
|
955
|
+
if (state.side !== kind) {
|
|
956
|
+
state.side = kind;
|
|
957
|
+
error.hidden = true;
|
|
958
|
+
renderOptions();
|
|
959
|
+
}
|
|
960
|
+
if (focus === 'switch') sideBtns[kind].focus();
|
|
961
|
+
else if (focus === 'option') entry.focus();
|
|
811
962
|
}
|
|
812
963
|
|
|
813
964
|
function select(id, { focus = false } = {}) {
|
|
814
|
-
if (
|
|
965
|
+
if (busy()) return;
|
|
815
966
|
const o = allOptions().find(x => x.id === id);
|
|
816
967
|
if (!o || o.disabled) return;
|
|
817
|
-
state.
|
|
968
|
+
state.picks[state.side] = id;
|
|
818
969
|
syncSelection();
|
|
819
970
|
if (focus) list.querySelector(`.asst-route-option[data-option-id="${CSS.escape(id)}"]`)?.focus();
|
|
820
971
|
}
|
|
@@ -831,29 +982,33 @@ export function renderRouteCard(container, input, hooks = {}) {
|
|
|
831
982
|
const effortBtn = taskField('asst-route-effort');
|
|
832
983
|
const accountBtn = taskField('asst-route-account');
|
|
833
984
|
const setFieldLabel = (button, text) => { button.querySelector('.asst-dd-label').textContent = text; button.title = text; };
|
|
834
|
-
const selected = () => allOptions().find(o => o.id ===
|
|
985
|
+
const selected = () => allOptions().find(o => o.id === selectedId());
|
|
835
986
|
function editTarget(patch) {
|
|
836
|
-
if (
|
|
837
|
-
|
|
838
|
-
|
|
987
|
+
if (busy()) return;
|
|
988
|
+
const from = selected();
|
|
989
|
+
if (!from) return;
|
|
990
|
+
state.others[state.side] = { ...from, accountId: from.accountChoice || from.accountId || null, accountChoice: Object.hasOwn(patch, 'accountId') ? patch.accountId : from.accountChoice || null, disabled: false, disabledReason: null, accountError: null, ...patch, id: 'other', number: sideOptions().length + 1, badge: null, badgeText: tr('assistant.routes.badge.other', 'Your choice') };
|
|
991
|
+
state.picks[state.side] = 'other';
|
|
839
992
|
renderOptions();
|
|
840
993
|
}
|
|
841
994
|
function syncFields() {
|
|
842
995
|
const target = selected();
|
|
843
996
|
setFieldLabel(otherBtn, target ? routeTargetLabel(target) : tr('assistant.routes.other', 'Other model…'));
|
|
844
997
|
otherBtn.setAttribute('aria-label', tr('assistant.brain.model', 'Model'));
|
|
845
|
-
otherBtn.disabled =
|
|
998
|
+
otherBtn.disabled = busy();
|
|
846
999
|
setFieldLabel(effortBtn, target?.effort || tr('assistant.routes.editorEffortAuto', 'Auto'));
|
|
847
1000
|
effortBtn.setAttribute('aria-label', tr('assistant.brain.effort', 'Thinking effort'));
|
|
848
1001
|
setFieldLabel(accountBtn, routeDisplayAccount(target, hooks.t) || tr('assistant.routes.account.workersDefault', 'Workers default'));
|
|
849
1002
|
accountBtn.setAttribute('aria-label', tr('assistant.brain.account', 'Account profile'));
|
|
850
1003
|
const levels = target ? getEffortLevelsForModel(target.provider, catalogModelRow(target.provider, target.model)) : [];
|
|
851
|
-
effortBtn.disabled =
|
|
852
|
-
|
|
853
|
-
accountBtn.
|
|
1004
|
+
effortBtn.disabled = busy() || !target || !levels.some(e => e.id !== 'off');
|
|
1005
|
+
// Here runs on this conversation's own account (another one is a worker): only a worker picks one.
|
|
1006
|
+
accountBtn.hidden = state.side === 'direct' || !['claude-code', 'codex'].includes(target?.provider);
|
|
1007
|
+
accountBtn.disabled = busy();
|
|
854
1008
|
}
|
|
855
1009
|
effortBtn.addEventListener('click', () => {
|
|
856
1010
|
const target = selected();
|
|
1011
|
+
if (!target) return;
|
|
857
1012
|
const levels = getEffortLevelsForModel(target.provider, catalogModelRow(target.provider, target.model));
|
|
858
1013
|
openMenu(effortBtn, { title: tr('assistant.brain.effort', 'Thinking effort'), placement: 'auto', items: [
|
|
859
1014
|
{ kind: 'radio', label: tr('assistant.routes.editorEffortAuto', 'Auto'), selected: !target.effort, onSelect: () => editTarget({ effort: null }) },
|
|
@@ -862,6 +1017,7 @@ export function renderRouteCard(container, input, hooks = {}) {
|
|
|
862
1017
|
});
|
|
863
1018
|
accountBtn.addEventListener('click', async () => {
|
|
864
1019
|
const target = selected();
|
|
1020
|
+
if (!target) return;
|
|
865
1021
|
try {
|
|
866
1022
|
const rows = await (target.provider === 'codex' ? fetchCodexAccounts() : fetchClaudeAccounts());
|
|
867
1023
|
if (!card.isConnected || state.pending || state.resolved || state.expired || selected() !== target) return;
|
|
@@ -882,31 +1038,43 @@ export function renderRouteCard(container, input, hooks = {}) {
|
|
|
882
1038
|
e.stopPropagation();
|
|
883
1039
|
if (isMenuOpen(otherBtn)) { closeMenu(); return; }
|
|
884
1040
|
const brain = normalizeBrain(hooks.getBrain?.() || { provider: n.brain.provider || 'claude-code', model: n.brain.model });
|
|
1041
|
+
// The menu follows the switch: Here lists this conversation's provider (another model continues it there);
|
|
1042
|
+
// Dispatch lists every provider, and its pick is always a worker.
|
|
1043
|
+
const here = state.side === 'direct' && !n.dispatchOnly;
|
|
1044
|
+
// A worker: the providers the server lists (its CATALOG_PROVIDERS).
|
|
1045
|
+
const workerMenu = {
|
|
1046
|
+
providers: n.other.providers.length ? n.other.providers.map(p => normalizeProvider(p, p)) : null,
|
|
1047
|
+
};
|
|
885
1048
|
openModelMenu(otherBtn, {
|
|
886
1049
|
brain,
|
|
887
|
-
shouldOpen: () => card.isConnected && !
|
|
1050
|
+
shouldOpen: () => card.isConnected && !busy(),
|
|
888
1051
|
t: hooks.t,
|
|
889
|
-
title: tr('assistant.routes.otherTitle', 'Run on another model'),
|
|
1052
|
+
title: here ? tr('assistant.routes.otherHereTitle', 'Continue here on another model') : tr('assistant.routes.otherTitle', 'Run on another model'),
|
|
890
1053
|
placement: 'auto',
|
|
891
|
-
|
|
1054
|
+
...(here ? { providers: [brain.provider] } : workerMenu),
|
|
892
1055
|
current: selected() ? { provider: selected().provider, model: selected().model } : null,
|
|
893
1056
|
onToast: hooks.onToast,
|
|
894
1057
|
// Image / video creation: only models that generate the medium (no custom id: its output is unknown).
|
|
895
1058
|
...(n.requires ? { accept: (_p, m) => m?.outputs?.[n.requires] === true, allowCustom: false, emptyLabel: tr('assistant.routes.noCapableModel', 'No model in your catalog can make {medium} yet.', { medium: mediumWords(n.requires, tr) }) } : {}),
|
|
896
1059
|
// Design: only models that can see.
|
|
897
1060
|
...(!n.requires && n.visionOnly ? { accept: (_p, m) => m?.vision === true && m?.status !== 'unavailable', allowCustom: false, emptyLabel: tr('assistant.routes.noSeeingModel', 'No model in your catalog can see images yet.') } : {}),
|
|
1061
|
+
// Computer use: never a model known to be blind.
|
|
1062
|
+
...(!n.requires && !n.visionOnly && n.needsVision ? { accept: (_p, m) => m?.vision !== false } : {}),
|
|
898
1063
|
onSelect: (target) => {
|
|
899
|
-
if (!target?.model || !card.isConnected) return;
|
|
900
|
-
if (state.pending || state.resolved || state.expired) return;
|
|
1064
|
+
if (!target?.model || !card.isConnected || busy()) return;
|
|
901
1065
|
const previous = selected();
|
|
902
|
-
const kind =
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
1066
|
+
const kind = here && target.provider === brain.provider ? 'direct' : 'dispatch';
|
|
1067
|
+
const side = kind === 'direct' ? 'direct' : 'dispatch';
|
|
1068
|
+
state.others[side] = {
|
|
1069
|
+
id: 'other', kind, provider: target.provider, model: target.model, effort: null,
|
|
1070
|
+
...(kind === 'dispatch' ? { accountId: previous?.provider === target.provider ? previous.accountChoice || previous.accountId || null : null, account: previous?.provider === target.provider ? previous.account || null : null } : {}),
|
|
1071
|
+
label: target.label || '', badge: null, number: view.sides[side].options.length + 1, here: false,
|
|
1072
|
+
title: kind === 'direct' ? tr('assistant.routes.doHere', 'Do it here with {model}', { model: target.label || target.model }) : (target.label || target.model),
|
|
1073
|
+
badgeText: tr('assistant.routes.badge.other', 'Your choice'),
|
|
907
1074
|
vision: target.vision === true, price: target.price || null, reason: '', warning: null, disabled: false,
|
|
908
1075
|
};
|
|
909
|
-
state.
|
|
1076
|
+
state.picks[side] = 'other';
|
|
1077
|
+
state.side = side;
|
|
910
1078
|
renderOptions();
|
|
911
1079
|
list.querySelector('.asst-route-option[data-option-id="other"]')?.focus();
|
|
912
1080
|
},
|
|
@@ -941,7 +1109,8 @@ export function renderRouteCard(container, input, hooks = {}) {
|
|
|
941
1109
|
cancelBtn.hidden = !n.decline.allowed;
|
|
942
1110
|
const runBtn = el('button', 'asst-btn asst-btn-primary asst-route-run', tr('assistant.routes.run', 'Run'));
|
|
943
1111
|
runBtn.type = 'button';
|
|
944
|
-
const
|
|
1112
|
+
const bothSides = view.sides.direct.available && view.sides.dispatch.available;
|
|
1113
|
+
const hint = el('span', 'asst-route-keys', bothSides ? tr('assistant.routes.keysSides', '← → Here or Dispatch · 1–9 choose · Enter runs · Esc cancels') : tr('assistant.routes.keys', '1–4 choose · Enter runs · Esc cancels'));
|
|
945
1114
|
actions.append(hint, cancelBtn, runBtn);
|
|
946
1115
|
card.appendChild(actions);
|
|
947
1116
|
|
|
@@ -957,7 +1126,7 @@ export function renderRouteCard(container, input, hooks = {}) {
|
|
|
957
1126
|
}
|
|
958
1127
|
|
|
959
1128
|
function chosenRoute() {
|
|
960
|
-
const o =
|
|
1129
|
+
const o = selected() || null;
|
|
961
1130
|
return {
|
|
962
1131
|
routeId: n.routeId,
|
|
963
1132
|
taskClass: n.taskClass,
|
|
@@ -965,10 +1134,10 @@ export function renderRouteCard(container, input, hooks = {}) {
|
|
|
965
1134
|
summary: n.summary,
|
|
966
1135
|
confidence: n.confidence,
|
|
967
1136
|
decidedBy: 'user',
|
|
968
|
-
optionId:
|
|
1137
|
+
optionId: selectedId(),
|
|
969
1138
|
remembered: state.remember,
|
|
970
|
-
target: o ? { kind: o.kind || 'direct', provider: o.provider, model: o.model, effort: o.effort, accountId: o.accountId || null, label: o.label || (o.here ? routeTargetLabel(o) : o.title) } : null,
|
|
971
|
-
alternatives: allOptions().filter(x => x.id !==
|
|
1139
|
+
target: o ? { kind: o.kind || 'direct', provider: o.provider, model: o.model, effort: o.effort, accountId: o.accountId || null, label: (o.kind === 'direct' ? str(o.label).replace(/^do it here with\s+/i, '') : o.label) || (o.here ? routeTargetLabel(o) : o.title) } : null,
|
|
1140
|
+
alternatives: allOptions().filter(x => x.id !== selectedId()).map(x => ({ provider: x.provider, model: x.model, effort: x.effort, label: x.label })),
|
|
972
1141
|
};
|
|
973
1142
|
}
|
|
974
1143
|
|
|
@@ -1011,8 +1180,8 @@ export function renderRouteCard(container, input, hooks = {}) {
|
|
|
1011
1180
|
}
|
|
1012
1181
|
|
|
1013
1182
|
function submit() {
|
|
1014
|
-
if (
|
|
1015
|
-
const response = buildRouteResponse(n, { optionId:
|
|
1183
|
+
if (busy() || !selectedId()) return;
|
|
1184
|
+
const response = buildRouteResponse(n, { optionId: selectedId(), target: state.others[state.side], remember: state.remember });
|
|
1016
1185
|
error.hidden = true;
|
|
1017
1186
|
setPending(true, tr('assistant.routes.routing', 'Routing…'));
|
|
1018
1187
|
try { hooks.onRespond?.(response, n); } catch { /* ignore */ }
|
|
@@ -1045,11 +1214,18 @@ export function renderRouteCard(container, input, hooks = {}) {
|
|
|
1045
1214
|
if (o && !o.disabled) { e.preventDefault(); e.stopPropagation(); select(o.id, { focus: true }); }
|
|
1046
1215
|
return;
|
|
1047
1216
|
}
|
|
1217
|
+
// ← → switch Here / Dispatch, from the switch itself or from the list.
|
|
1218
|
+
if ((e.key === 'ArrowLeft' || e.key === 'ArrowRight') && !e.metaKey && !e.ctrlKey && !e.altKey
|
|
1219
|
+
&& (e.target?.classList?.contains('asst-route-side') || e.target?.classList?.contains('asst-route-option') || e.target === card)) {
|
|
1220
|
+
e.preventDefault(); e.stopPropagation();
|
|
1221
|
+
setSide(e.key === 'ArrowLeft' ? 'direct' : 'dispatch', { focus: e.target?.classList?.contains('asst-route-side') ? 'switch' : 'option' });
|
|
1222
|
+
return;
|
|
1223
|
+
}
|
|
1048
1224
|
if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
|
|
1049
1225
|
const enabled = all.filter(o => !o.disabled);
|
|
1050
1226
|
if (!enabled.length) return;
|
|
1051
1227
|
e.preventDefault(); e.stopPropagation();
|
|
1052
|
-
const idx = Math.max(0, enabled.findIndex(o => o.id ===
|
|
1228
|
+
const idx = Math.max(0, enabled.findIndex(o => o.id === selectedId()));
|
|
1053
1229
|
const next = enabled[(idx + (e.key === 'ArrowDown' ? 1 : -1) + enabled.length) % enabled.length];
|
|
1054
1230
|
select(next.id, { focus: true });
|
|
1055
1231
|
return;
|
|
@@ -1110,7 +1286,8 @@ export function renderRouteCard(container, input, hooks = {}) {
|
|
|
1110
1286
|
get line() { return state.line; },
|
|
1111
1287
|
/** Focus the preselected option (the panel calls this when the composer is empty). */
|
|
1112
1288
|
focus() {
|
|
1113
|
-
|
|
1289
|
+
// A side with nothing listed yet (a worker to pick by model) focuses the switch instead.
|
|
1290
|
+
const row = list.querySelector('.asst-route-option[aria-checked="true"]') || list.querySelector('.asst-route-option') || sideBtns[state.side];
|
|
1114
1291
|
try { row?.focus({ preventScroll: false }); } catch { row?.focus(); }
|
|
1115
1292
|
},
|
|
1116
1293
|
focusIfIdle() { if (typeof hooks.shouldFocus !== 'function' || hooks.shouldFocus()) entry.focus(); },
|
|
@@ -1146,7 +1323,7 @@ export { openRoutesEditor } from './asst-routes-editor.js';
|
|
|
1146
1323
|
|
|
1147
1324
|
// ── Models manager (Assistant → Models…) ───────────────────────────────────
|
|
1148
1325
|
|
|
1149
|
-
const MODEL_SECTIONS = ['claude-code', 'codex', 'opencode'];
|
|
1326
|
+
const MODEL_SECTIONS = ['claude-code', 'codex', 'opencode', 'antigravity'];
|
|
1150
1327
|
const MODEL_TABS = ['available', 'archived'];
|
|
1151
1328
|
let _modelsManager = null;
|
|
1152
1329
|
|
|
@@ -1371,7 +1548,7 @@ export async function openModelsManager(hooks = {}) {
|
|
|
1371
1548
|
}
|
|
1372
1549
|
|
|
1373
1550
|
function sectionHead(provider, countText) {
|
|
1374
|
-
const meta =
|
|
1551
|
+
const meta = providerMeta(provider);
|
|
1375
1552
|
const section = el('section', 'asst-routes-section asst-models-section');
|
|
1376
1553
|
section.dataset.provider = provider;
|
|
1377
1554
|
const head = el('div', 'asst-routes-section-title asst-models-head');
|
|
@@ -1381,8 +1558,11 @@ export async function openModelsManager(hooks = {}) {
|
|
|
1381
1558
|
return { section, head };
|
|
1382
1559
|
}
|
|
1383
1560
|
|
|
1561
|
+
// An optional provider's section shows once the server lists it (an older server knows no Antigravity).
|
|
1562
|
+
const shownSections = () => MODEL_SECTIONS.filter(p => !OPTIONAL_PROVIDERS.includes(p) || Array.isArray(state.models?.[p]) || arr(state.archived?.[p]).length > 0);
|
|
1563
|
+
|
|
1384
1564
|
function renderAvailable() {
|
|
1385
|
-
for (const provider of
|
|
1565
|
+
for (const provider of shownSections()) {
|
|
1386
1566
|
const rows = arr(state.models[provider]);
|
|
1387
1567
|
const { section } = sectionHead(provider, modelsOnText(rows, hooks.t));
|
|
1388
1568
|
if (provider === 'opencode') section.appendChild(el('div', 'asst-modal-note', tr('assistant.models.opencodeNote', 'Connected OpenCode models only. Settings → OpenCode edits the same list.')));
|
|
@@ -1397,7 +1577,7 @@ export async function openModelsManager(hooks = {}) {
|
|
|
1397
1577
|
|
|
1398
1578
|
function renderArchived() {
|
|
1399
1579
|
let any = false;
|
|
1400
|
-
for (const provider of
|
|
1580
|
+
for (const provider of shownSections()) {
|
|
1401
1581
|
const entries = arr(state.archived[provider]);
|
|
1402
1582
|
if (!entries.length) continue;
|
|
1403
1583
|
any = true;
|