synabun 2.0.0 → 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 +97 -0
- package/NOTICE +5 -0
- package/README.md +2 -1
- package/THIRD-PARTY-LICENSES.md +59 -0
- package/benchmarks/fixtures/browser/README.md +1 -1
- package/benchmarks/fixtures/desktop/README.md +1 -1
- package/docs/whatsapp.md +153 -18
- package/hooks/claude-code/pre-compact.mjs +11 -0
- package/hooks/claude-code/prompt-submit.mjs +60 -184
- package/hooks/claude-code/session-start.mjs +73 -16
- package/hooks/claude-code/shared.mjs +26 -3
- package/hooks/claude-code/stop.mjs +5 -5
- 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/lib/start-launcher.js +88 -1
- 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 -3
- 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 +2126 -209
- package/neural-interface/i18n/pt-BR.json +2123 -207
- package/neural-interface/lib/account-usage.js +163 -0
- package/neural-interface/lib/assistant-api.js +316 -57
- package/neural-interface/lib/assistant-brains/claude.js +19 -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-catalog.js +179 -27
- package/neural-interface/lib/assistant-config.js +91 -14
- package/neural-interface/lib/assistant-dispatch.js +494 -138
- package/neural-interface/lib/assistant-hidden-models.js +393 -66
- package/neural-interface/lib/assistant-jev.js +26 -3
- package/neural-interface/lib/assistant-memory.js +24 -10
- package/neural-interface/lib/assistant-persona.js +73 -12
- package/neural-interface/lib/assistant-plan-permissions.js +8 -1
- package/neural-interface/lib/assistant-pricing.js +21 -6
- 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 +488 -59
- package/neural-interface/lib/assistant-task-prompt.js +13 -3
- package/neural-interface/lib/assistant-test-density.js +142 -0
- package/neural-interface/lib/assistant-user-context-budgets.js +123 -0
- package/neural-interface/lib/assistant-user-context.js +908 -0
- package/neural-interface/lib/assistant-user-knowledge.js +328 -0
- package/neural-interface/lib/assistant-user-learn.js +404 -0
- package/neural-interface/lib/assistant-user-threads.js +226 -0
- package/neural-interface/lib/assistant-user-writer.js +183 -0
- package/neural-interface/lib/augmented-path.js +317 -53
- package/neural-interface/lib/browser-background-tab.js +134 -0
- package/neural-interface/lib/browser-screenshot.js +678 -61
- package/neural-interface/lib/claude-accounts.js +342 -31
- package/neural-interface/lib/claude-agent-bridge.js +123 -9
- package/neural-interface/lib/claude-executable.js +6 -1
- package/neural-interface/lib/claude-model-catalog.js +33 -5
- 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/claude-temporary.js +46 -7
- 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/cloudflared/install.js +533 -0
- package/neural-interface/lib/cloudflared/manager.js +545 -0
- package/neural-interface/lib/cloudflared/pins.js +115 -0
- package/neural-interface/lib/cloudflared/pins.json +63 -0
- package/neural-interface/lib/cloudflared/tunnel.js +265 -0
- 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/editor-completion-endpoint.js +264 -0
- package/neural-interface/lib/editor-completion-opencode.js +436 -0
- package/neural-interface/lib/editor-completion.js +714 -0
- package/neural-interface/lib/editor-config.js +149 -0
- package/neural-interface/lib/editor-files-api.js +268 -0
- package/neural-interface/lib/editor-git-api.js +134 -0
- package/neural-interface/lib/file-explorer-api.js +1039 -0
- package/neural-interface/lib/file-explorer-system.js +355 -0
- package/neural-interface/lib/file-explorer-trash.ps1 +85 -0
- package/neural-interface/lib/file-explorer-watch.js +239 -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-diff-api.js +103 -0
- package/neural-interface/lib/git-read.js +234 -0
- package/neural-interface/lib/greeting/api.js +89 -0
- package/neural-interface/lib/greeting/budgets.js +90 -0
- package/neural-interface/lib/greeting/engine.js +461 -0
- package/neural-interface/lib/greeting/facts.js +320 -0
- package/neural-interface/lib/greeting/sources.js +252 -0
- package/neural-interface/lib/greeting/writer.js +139 -0
- package/neural-interface/lib/http-guards.js +189 -13
- package/neural-interface/lib/install-details.js +145 -0
- package/neural-interface/lib/machine-names.js +88 -0
- package/neural-interface/lib/mcp-installer.js +14 -2
- package/neural-interface/lib/memory-api.js +31 -7
- package/neural-interface/lib/native-loop-providers.js +26 -8
- package/neural-interface/lib/native-loop-runtime.js +9 -4
- package/neural-interface/lib/os-path.js +319 -0
- package/neural-interface/lib/plugin-installer.js +4 -2
- package/neural-interface/lib/project-storage.js +5 -7
- 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/request-gate.js +589 -0
- 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/ui-blob-api.js +108 -0
- package/neural-interface/lib/ui-blob-store.js +68 -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 +701 -2
- package/neural-interface/package.json +13 -2
- package/neural-interface/public/index.html +2 -0
- package/neural-interface/public/index2d.html +2 -0
- package/neural-interface/public/install-app.html +9 -6
- package/neural-interface/public/offline.html +19 -3
- package/neural-interface/public/onboarding.html +107 -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 +50 -14
- 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-automation.js +286 -0
- package/neural-interface/public/shared/assistant/asst-brain-picker.js +430 -81
- package/neural-interface/public/shared/assistant/asst-budget.js +259 -303
- package/neural-interface/public/shared/assistant/asst-composer.js +11 -1
- package/neural-interface/public/shared/assistant/asst-mascot.js +140 -61
- package/neural-interface/public/shared/assistant/asst-menu.js +27 -11
- package/neural-interface/public/shared/assistant/asst-panel.js +198 -42
- package/neural-interface/public/shared/assistant/asst-render.js +197 -23
- package/neural-interface/public/shared/assistant/asst-route-models.js +108 -0
- package/neural-interface/public/shared/assistant/asst-route.js +95 -343
- package/neural-interface/public/shared/assistant/asst-routes-editor.js +534 -0
- package/neural-interface/public/shared/assistant/asst-sanitize.js +8 -1
- package/neural-interface/public/shared/assistant/asst-sidepanel.js +31 -9
- package/neural-interface/public/shared/assistant/asst-slash.js +1 -1
- package/neural-interface/public/shared/assistant/asst-state.js +186 -2
- package/neural-interface/public/shared/assistant/asst-styles.js +247 -11
- package/neural-interface/public/shared/blob-store.js +111 -22
- 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 +80 -118
- package/neural-interface/public/shared/cdx/cdx-render.js +25 -89
- package/neural-interface/public/shared/cdx/cdx-settings.js +231 -0
- package/neural-interface/public/shared/cdx/cdx-styles.js +164 -107
- package/neural-interface/public/shared/cdx/cdx-tabs.js +348 -668
- 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/constants.js +6 -1
- package/neural-interface/public/shared/cp/cp-events.js +2 -1
- package/neural-interface/public/shared/cp/cp-rehydrate.js +2 -1
- package/neural-interface/public/shared/cp/cp-styles.js +61 -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-ai-logic.js +409 -0
- package/neural-interface/public/shared/editor/editor-ai.js +619 -0
- package/neural-interface/public/shared/editor/editor-commands.js +212 -0
- package/neural-interface/public/shared/editor/editor-compare.js +103 -0
- package/neural-interface/public/shared/editor/editor-completion-list.js +172 -0
- package/neural-interface/public/shared/editor/editor-doc.js +257 -0
- package/neural-interface/public/shared/editor/editor-git-logic.js +131 -0
- package/neural-interface/public/shared/editor/editor-git.js +95 -0
- package/neural-interface/public/shared/editor/editor-languages.js +156 -0
- package/neural-interface/public/shared/editor/editor-lint.js +229 -0
- package/neural-interface/public/shared/editor/editor-loader.js +51 -0
- package/neural-interface/public/shared/editor/editor-markdown.js +90 -0
- package/neural-interface/public/shared/editor/editor-menu.js +233 -0
- package/neural-interface/public/shared/editor/editor-minimap.js +371 -0
- package/neural-interface/public/shared/editor/editor-palette-logic.js +192 -0
- package/neural-interface/public/shared/editor/editor-palette.js +125 -0
- package/neural-interface/public/shared/editor/editor-panel-logic.js +288 -0
- package/neural-interface/public/shared/editor/editor-panel.js +2099 -0
- package/neural-interface/public/shared/editor/editor-prefs.js +224 -0
- package/neural-interface/public/shared/editor/editor-safety.js +500 -0
- package/neural-interface/public/shared/editor/editor-search.js +352 -0
- package/neural-interface/public/shared/editor/editor-statusbar.js +144 -0
- package/neural-interface/public/shared/editor/editor-tabs-logic.js +331 -0
- package/neural-interface/public/shared/editor/editor-tabs.js +233 -0
- package/neural-interface/public/shared/editor/editor-theme.js +244 -0
- package/neural-interface/public/shared/editor/editor-view.js +332 -0
- package/neural-interface/public/shared/explorer/fx-api.js +216 -0
- package/neural-interface/public/shared/explorer/fx-links.js +204 -0
- package/neural-interface/public/shared/explorer/fx-logic.js +537 -0
- package/neural-interface/public/shared/explorer/fx-main.js +865 -0
- package/neural-interface/public/shared/explorer/fx-names.js +87 -0
- package/neural-interface/public/shared/explorer/fx-ops.js +370 -0
- package/neural-interface/public/shared/explorer/fx-popups.js +490 -0
- package/neural-interface/public/shared/explorer/fx-tree.js +790 -0
- package/neural-interface/public/shared/file-links.js +339 -0
- package/neural-interface/public/shared/git-bash-view.js +145 -0
- package/neural-interface/public/shared/greeting/greeting-card.js +178 -0
- package/neural-interface/public/shared/greeting/greeting-view.js +148 -0
- package/neural-interface/public/shared/html-shell.js +161 -165
- 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-childpanel.js +4 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-greeting.js +47 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-panel.js +131 -17
- package/neural-interface/public/shared/ocp-v2/ocp-v2-render.js +7 -1
- package/neural-interface/public/shared/ocp-v2/ocp-v2-send.js +22 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-styles.js +158 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-ws.js +3 -2
- package/neural-interface/public/shared/pwa-install-page.js +32 -2
- package/neural-interface/public/shared/pwa-install.css +3 -3
- package/neural-interface/public/shared/pwa-install.js +100 -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-code-editor-pane.js +412 -0
- package/neural-interface/public/shared/settings/settings-code-editor.js +328 -0
- package/neural-interface/public/shared/settings/settings-greeting.js +133 -0
- package/neural-interface/public/shared/settings/settings-ia.js +33 -4
- package/neural-interface/public/shared/settings/settings-user-context.js +154 -0
- package/neural-interface/public/shared/showcase-privacy.js +489 -0
- package/neural-interface/public/shared/start-bridge.js +238 -19
- package/neural-interface/public/shared/storage.js +30 -3
- package/neural-interface/public/shared/styles.css +1710 -770
- package/neural-interface/public/shared/synabun-mascot.js +68 -58
- package/neural-interface/public/shared/tunnel-view.js +167 -0
- package/neural-interface/public/shared/ui-claude-panel.js +369 -229
- package/neural-interface/public/shared/ui-cli-banner.js +355 -0
- package/neural-interface/public/shared/ui-detail.js +15 -0
- package/neural-interface/public/shared/ui-explorer.js +22 -0
- package/neural-interface/public/shared/ui-file-explorer.js +481 -2496
- package/neural-interface/public/shared/ui-file-links.js +103 -0
- package/neural-interface/public/shared/ui-git-bash.js +124 -0
- package/neural-interface/public/shared/ui-help.js +26 -0
- package/neural-interface/public/shared/ui-invite.js +126 -116
- package/neural-interface/public/shared/ui-judgments.js +10 -1
- package/neural-interface/public/shared/ui-keybinds.js +4 -0
- package/neural-interface/public/shared/ui-loading.js +187 -19
- package/neural-interface/public/shared/ui-menubar.js +25 -33
- package/neural-interface/public/shared/ui-native-loop-router-state.js +11 -0
- package/neural-interface/public/shared/ui-native-loop-router.js +15 -2
- package/neural-interface/public/shared/ui-navbar.js +48 -22
- 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 +509 -490
- package/neural-interface/public/shared/ui-sidepanel-runtime.js +2 -1
- package/neural-interface/public/shared/ui-sidepanel-sessions.js +3 -1
- package/neural-interface/public/shared/ui-sync.js +22 -0
- package/neural-interface/public/shared/ui-terminal.js +40 -5
- package/neural-interface/public/shared/ui-tool-updates.js +5 -0
- package/neural-interface/public/shared/ui-tunnel.js +130 -0
- package/neural-interface/public/shared/ui-tutorial-steps.js +6 -2
- 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/ui-whiteboard.js +12 -0
- package/neural-interface/public/shared/ui-worker-end.js +245 -0
- package/neural-interface/public/shared/ui-workspaces.js +41 -27
- package/neural-interface/public/shared/whatsapp/wa-view.js +51 -8
- package/neural-interface/public/shared/worker-end.js +253 -0
- package/neural-interface/public/sw.js +2 -2
- package/neural-interface/public/variant/2d/main.js +10 -1
- package/neural-interface/public/variant/3d/main.js +10 -1
- package/neural-interface/public/vendor/codemirror/chunks/apl-CSDKT72Z.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/asciiarmor-JYYPU5BM.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/asn1-NRWCZDA6.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/asterisk-5OU6D4ZQ.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/brainfuck-ID4N4SYC.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/chunk-4RZWGCS2.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/chunk-ACTJM3JR.js +11 -0
- package/neural-interface/public/vendor/codemirror/chunks/chunk-S6BMAXLK.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/chunk-TO2RDOHQ.js +3 -0
- package/neural-interface/public/vendor/codemirror/chunks/chunk-US2GMZZQ.js +13 -0
- package/neural-interface/public/vendor/codemirror/chunks/chunk-WYYO5ZAJ.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/chunk-XALZTDJC.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/chunk-YRNUTRDV.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/chunk-Z3QOKH62.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/clike-OWQBSOGL.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/clojure-KV75UQWC.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/cmake-GTEP5Y3C.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/cobol-WCZ7QEGJ.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/coffeescript-TGLBOT6B.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/commonlisp-WVVI432V.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/crystal-S6R6HLKQ.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/css-PDA2RINP.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/cypher-HL5QPL4C.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/d-HHJH2HAJ.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/diff-MN3A35BY.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-25SQLBVS.js +3 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-27ZGGJJS.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-4E52OJIF.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-6FT73XDC.js +2 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-BE6YGRMW.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-CJSGKW2O.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-CRPH2WLI.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-CTG7JEQN.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-I3FEVELX.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-IM5BBSWZ.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-KERUKTIF.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-LXTMWZAM.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-MABASSET.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-MJVBUZLL.js +3 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-MNB3WZVU.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-QYWXOE3U.js +7 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-TEBOFWLZ.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-UNXFROM3.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-XL2SLQO6.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-XO42Y3AP.js +6 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-Y2WWDWJK.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-YFEZKWMU.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-ZY7SWQO6.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dockerfile-3Y4R4NCH.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dtd-5343GJ4P.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dylan-FI2CXSW2.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/ebnf-GNQ77YWA.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/ecl-MRPUAE2L.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/eiffel-DZROES27.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/elm-4KICRFKQ.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/erlang-3IHYC263.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/factor-C6D3E5UK.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/fcl-VYTWUJMI.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/forth-ZBO2EH5M.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/fortran-5NDCRWIK.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/gas-SKRMOXFP.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/gherkin-L45YXZLW.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/groovy-GLPTLN44.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/haskell-E7NF7P4Y.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/haxe-K2NVDOB6.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/http-Q6DRQIJB.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/idl-MZYNJVLN.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/javascript-6YRYK2HR.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/julia-HXEL2T5G.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/livescript-MV7CI2JZ.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/lua-E36NHOOD.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/mathematica-3XOZZCMC.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/mbox-ODYGFKS7.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/mirc-WB43AWF5.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/mllike-ZT6ET7ZH.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/modelica-LLMZPFGA.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/mscgen-2OPNNYFH.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/mumps-6P3HWJH6.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/nginx-TCGKQTRW.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/nsis-ATIGIXTJ.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/ntriples-5PLBI5OV.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/octave-QF2S3NL6.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/oz-JFIVFFH2.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/pascal-JG6SW76V.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/perl-4B3IWVVO.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/pig-ABEBH4MI.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/powershell-H32XE7TL.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/properties-NN644MMA.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/protobuf-YKWQK2J3.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/pug-RIYXEKFW.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/puppet-JZQ5WPEV.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/python-EBCGGHAC.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/q-5UMH2YQY.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/r-7XMGG453.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/rpm-IEV7TSLR.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/ruby-T4SPFPNM.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/sas-GEDX67ET.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/scheme-FTLOWKPC.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/shell-HUFRKFSJ.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/sieve-P5QUDVYO.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/smalltalk-2SORJNQI.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/solr-M22TGAR5.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/sparql-4M3KL752.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/spreadsheet-3K23LA4N.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/sql-VHRFMUBI.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/stex-QZDAQUY2.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/stylus-7ZDKAKBD.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/swift-WCLCLGMR.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/tcl-JLMU6WOK.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/textile-4CHDKZ4T.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/tiddlywiki-DKMCZZQ4.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/tiki-F4JIG36Q.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/toml-BHMHTIM2.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/troff-YQ2KV5NT.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/ttcn-ZHRCVFJY.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/ttcn-cfg-KJOVTY4X.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/turtle-535P5N6I.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/vb-76UMQHOT.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/vbscript-CGNDTK6Z.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/velocity-VSJDO5T5.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/verilog-4T6ETXIP.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/vhdl-T7FNAXUN.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/webidl-P6MJSJ6J.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/xquery-UOVILRGN.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/yacas-CV2APROL.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/z80-3KFNVM4M.js +1 -0
- package/neural-interface/public/vendor/codemirror/codemirror-6.43.14-75f05e67.min.js +2 -0
- package/neural-interface/public/vendor/codemirror/manifest.json +60 -0
- package/neural-interface/scripts/build-vendor.mjs +78 -2
- package/neural-interface/scripts/vendor-codemirror-entry.js +121 -0
- package/neural-interface/server.js +2085 -1629
- package/neural-interface/templates/rulesets/README.md +1 -1
- package/package.json +5 -8
- package/setup.js +53 -15
- package/benchmarks/browser-live-results.json +0 -79
- package/benchmarks/browser-v2-results.json +0 -258
- package/neural-interface/lib/codex-capabilities.d.ts +0 -41
- package/neural-interface/public/category-logos/claude.svg +0 -1
- package/neural-interface/public/category-logos/gemini-color.svg +0 -1
- package/neural-interface/public/category-logos/openai.svg +0 -1
- package/neural-interface/public/category-logos/synabun.png +0 -0
- package/neural-interface/public/claude.svg +0 -1
- package/neural-interface/public/gallery.svg +0 -6
- package/neural-interface/public/leonardoai.svg +0 -1
- package/neural-interface/public/mcp.svg +0 -4
- package/neural-interface/public/opencode-logo-dark-square.svg +0 -1
- package/neural-interface/public/pin-svgrepo-com.svg +0 -25
- package/neural-interface/public/sessionicon.svg +0 -8
- package/neural-interface/public/toolbar-album.svg +0 -8
- package/neural-interface/public/toolbar-question.svg +0 -6
- package/neural-interface/public/toolbar-sessions.svg +0 -6
- package/neural-interface/scripts/codex-protocol-fixture.mjs +0 -83
- package/skills/synabun/synabunicon.png +0 -0
|
@@ -19,6 +19,8 @@ import {
|
|
|
19
19
|
getCachedCodexAccounts,
|
|
20
20
|
getCachedMcpProfilePresets,
|
|
21
21
|
getCachedOpencodeAgents,
|
|
22
|
+
getClaudeWorkerAccountId,
|
|
23
|
+
getCodexWorkerAccountId,
|
|
22
24
|
getEffortLevelsForModel,
|
|
23
25
|
getModelsForProfile,
|
|
24
26
|
modelEffortIds,
|
|
@@ -27,22 +29,33 @@ import {
|
|
|
27
29
|
import { getHiddenModels, HIDDEN_MODELS_EVENT, hiddenModelMatch } from '../ocp-hidden-models.js';
|
|
28
30
|
import { getProviderMeta } from '../provider-icons.js';
|
|
29
31
|
import { on } from '../state.js';
|
|
30
|
-
import { fetchAssistantCatalog, fetchProjects } from './asst-api.js';
|
|
31
|
-
import {
|
|
32
|
+
import { fetchAssistantCatalog, fetchProjects, patchClaudeAccount, patchCodexAccount, setClaudeWorkerAccount, setCodexWorkerAccount } from './asst-api.js';
|
|
33
|
+
import { accountBadge, accountCompactMode, accountIdentity, accountName, accountUsable, modeChoices } from '../account-view.js';
|
|
34
|
+
import { accountUsageText, autoAccountLabel } from '../account-usage.js';
|
|
35
|
+
import { closeMenu, currentMenu, isMenuOpen, openMenu } from './asst-menu.js';
|
|
36
|
+
import { createMascotControl } from './asst-mascot.js';
|
|
32
37
|
import {
|
|
33
38
|
ACCOUNT_PROVIDERS,
|
|
34
39
|
brainRequiresSwitch,
|
|
40
|
+
emptyProjectView,
|
|
35
41
|
modelShortName,
|
|
36
42
|
normalizeBrain,
|
|
37
43
|
PERMISSION_MODES,
|
|
38
44
|
PLAN_MODE_PROVIDERS,
|
|
39
45
|
permissionModeLabel,
|
|
40
46
|
PROVIDERS,
|
|
47
|
+
projectLabel,
|
|
41
48
|
resolveBrainDefaults,
|
|
49
|
+
TEST_DENSITIES,
|
|
50
|
+
normalizeTestDensity,
|
|
51
|
+
testDensityBars,
|
|
42
52
|
} from './asst-state.js';
|
|
43
53
|
|
|
44
54
|
let _projectsCache = null;
|
|
45
55
|
|
|
56
|
+
/** SynaBun's registered projects as the last load saw them ([{ path, label }]; [] before the first). */
|
|
57
|
+
export function cachedProjects() { return _projectsCache || []; }
|
|
58
|
+
|
|
46
59
|
const ICON_FOLDER = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/></svg>';
|
|
47
60
|
const ICON_SHIELD = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3 5 6v5c0 4.5 3 8 7 10 4-2 7-5.5 7-10V6z"/></svg>';
|
|
48
61
|
const ICON_PLUG = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 3v5M15 3v5M6 8h12v3a6 6 0 0 1-12 0zM12 17v4"/></svg>';
|
|
@@ -53,6 +66,25 @@ export const ICON_ACCOUNT = '<svg viewBox="0 0 24 24" fill="none" stroke="curren
|
|
|
53
66
|
/** Dropdown caret shared by every toolbar dropdown (fields, route mode). */
|
|
54
67
|
export const ICON_CARET = '<svg viewBox="0 0 10 10" aria-hidden="true"><path d="M2.5 3.75 5 6.25l2.5-2.5"/></svg>';
|
|
55
68
|
|
|
69
|
+
// Test density: three bars centred in the 24 box (x 6–18, y 6–18; 5–19 with the stroke), `filled` of them at full strength.
|
|
70
|
+
function testsIcon(filled) {
|
|
71
|
+
const bar = (index, d) => `<path d="${d}"${index < filled ? '' : ' opacity=".3"'}/>`;
|
|
72
|
+
return `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">${bar(0, 'M6 18v-4')}${bar(1, 'M12 18v-8')}${bar(2, 'M18 18V6')}</svg>`;
|
|
73
|
+
}
|
|
74
|
+
// English copy of the Test density menu (i18n: assistant.tests.levels.* / assistant.tests.desc.*).
|
|
75
|
+
const TEST_DENSITY_COPY = {
|
|
76
|
+
none: ['None', 'No tests. Only checks that the change loads.'],
|
|
77
|
+
light: ['Light', 'Runs the tests that cover the change. New tests only when they matter.'],
|
|
78
|
+
standard: ['Standard', 'Adds focused tests and runs the area\'s suite once.'],
|
|
79
|
+
thorough: ['Thorough', 'Full coverage, whole suite, live checks.'],
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
/** "Light" — the level's name (a missing or unknown level is the default one). */
|
|
83
|
+
export function testDensityLabel(level, t) {
|
|
84
|
+
const id = normalizeTestDensity(level);
|
|
85
|
+
return tf(t)(`assistant.tests.levels.${id}`, TEST_DENSITY_COPY[id][0]);
|
|
86
|
+
}
|
|
87
|
+
|
|
56
88
|
function el(tag, className, text) {
|
|
57
89
|
const node = document.createElement(tag);
|
|
58
90
|
if (className) node.className = className;
|
|
@@ -92,6 +124,9 @@ let _catalog = null;
|
|
|
92
124
|
let _catalogAt = 0;
|
|
93
125
|
let _catalogPartial = false; // built without the server's catalog (no context variants)
|
|
94
126
|
let _catalogLoading = null;
|
|
127
|
+
let _catalogEpoch = 0; // bumped by every invalidation: a load that began before it is not kept
|
|
128
|
+
let _catalogForceNext = false; // the lists changed: the next load asks the server for a new build
|
|
129
|
+
const _catalogListeners = new Set();
|
|
95
130
|
const CATALOG_TTL_MS = 60_000;
|
|
96
131
|
|
|
97
132
|
function trimNumber(n) {
|
|
@@ -150,11 +185,16 @@ let _hidden = { 'claude-code': new Set(), codex: new Set(), opencode: new Set()
|
|
|
150
185
|
|
|
151
186
|
function hiddenSets(remote) {
|
|
152
187
|
const out = {};
|
|
188
|
+
// Entries with a pin record (an alias hidden as the model it named then): each hides its own row only.
|
|
189
|
+
const rowOnly = {};
|
|
153
190
|
for (const provider of PROVIDERS) {
|
|
154
191
|
const list = Array.isArray(remote?.hidden?.[provider]) ? remote.hidden[provider] : [];
|
|
155
192
|
out[provider] = new Set(list.map(id => String(id).toLowerCase()));
|
|
193
|
+
const pins = remote?.hiddenPins?.[provider];
|
|
194
|
+
rowOnly[provider] = new Set(pins && typeof pins === 'object' ? Object.keys(pins).map(id => id.toLowerCase()) : []);
|
|
156
195
|
}
|
|
157
196
|
out.opencode = new Set([...(out.opencode || []), ...hiddenOpencodeIds(remote)]);
|
|
197
|
+
out.rowOnly = rowOnly;
|
|
158
198
|
return out;
|
|
159
199
|
}
|
|
160
200
|
|
|
@@ -163,6 +203,9 @@ function hiddenSets(remote) {
|
|
|
163
203
|
* id, or the model it resolves to at the same context window ("opus[1m]" →
|
|
164
204
|
* "claude-opus-5-5[1m]"); "opus" and "opus[1m]" hide independently. OpenCode:
|
|
165
205
|
* full or bare id (this window's Settings → OpenCode mirror counts too).
|
|
206
|
+
* A hidden alias hides the model it named when it was hidden, and the server
|
|
207
|
+
* keeps the list true to that (an alias that moved to another model is taken
|
|
208
|
+
* off it); such an entry hides its own row only, never through `resolved`.
|
|
166
209
|
*/
|
|
167
210
|
export function hiddenModelFor(provider, id, { resolved = null, sets = _hidden } = {}) {
|
|
168
211
|
const want = String(id || '').trim().toLowerCase();
|
|
@@ -173,22 +216,87 @@ export function hiddenModelFor(provider, id, { resolved = null, sets = _hidden }
|
|
|
173
216
|
const suffix = (want.match(/\[(1m|extended)\]$/) || [''])[0];
|
|
174
217
|
const up = String(resolved || '').trim().toLowerCase().replace(/\[(1m|extended)\]$/, '');
|
|
175
218
|
const key = up ? `${up}${suffix}` : '';
|
|
176
|
-
return key && key !== want && list.has(key) ? key : null;
|
|
219
|
+
return key && key !== want && list.has(key) && !sets.rowOnly?.[provider]?.has(key) ? key : null;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* One provider's menu rows. Which models exist is the server catalog's word: it
|
|
224
|
+
* is built from the list the provider's own selector reads, with the models the
|
|
225
|
+
* user hid already left out. The window's runtime list (`local`) only adds what
|
|
226
|
+
* the catalog does not carry (the tagline, the default / top tag, a legacy
|
|
227
|
+
* selector). It stands in for the catalog only when the catalog has no row for
|
|
228
|
+
* the provider (request failed, older server, provider not answering there), or
|
|
229
|
+
* holds the provider's static stand-in while this window has a verified list.
|
|
230
|
+
*/
|
|
231
|
+
export function mergeProviderModels(provider, remoteList, localList, { sets = _hidden, remoteSource = null } = {}) {
|
|
232
|
+
const remote = (Array.isArray(remoteList) ? remoteList : []).filter(m => m?.id);
|
|
233
|
+
const local = (Array.isArray(localList) ? localList : []).filter(m => m?.id || m?.model);
|
|
234
|
+
const localVerified = local.length > 0 && !local.some(m => m.catalogSource === 'fallback');
|
|
235
|
+
const out = [];
|
|
236
|
+
const seen = new Set();
|
|
237
|
+
const push = (m) => { if (m && !seen.has(m.id)) { seen.add(m.id); out.push(m); } };
|
|
238
|
+
if (remote.length && !(remoteSource === 'fallback' && localVerified)) {
|
|
239
|
+
const own = new Map(local.map(m => [String(m.id || m.model), m]));
|
|
240
|
+
for (const r of remote) {
|
|
241
|
+
if (hiddenModelFor(provider, r.id, { resolved: r.upstream || null, sets })) continue;
|
|
242
|
+
const mine = own.get(String(r.id)) || null;
|
|
243
|
+
// The catalog row first. What it does not carry (an older server, a row with no
|
|
244
|
+
// label or effort list) is read from the window's row for the same id.
|
|
245
|
+
const row = normalizeCatalogModel(
|
|
246
|
+
{ ...r, desc: mine?.desc || r.desc || r.description || '', tier: mine?.tier || r.tier || null, upstream: r.upstream || mine?.resolvedModel || null },
|
|
247
|
+
mine ? {
|
|
248
|
+
label: mine.label || mine.displayName || '', efforts: modelEffortIds(mine) || undefined, contextWindow: mine.contextWindow || null,
|
|
249
|
+
defaultEffort: mine.defaultEffort || mine.defaultReasoningEffort || null, isDefault: mine.isDefault,
|
|
250
|
+
} : {},
|
|
251
|
+
);
|
|
252
|
+
if (row) row.selector = mine ? (modelSelectorValue(mine) || row.id) : row.id;
|
|
253
|
+
push(row);
|
|
254
|
+
}
|
|
255
|
+
return out;
|
|
256
|
+
}
|
|
257
|
+
const byId = new Map(remote.map(m => [String(m.id), m]));
|
|
258
|
+
for (const m of local) {
|
|
259
|
+
const extra = byId.get(String(m?.id)) || {};
|
|
260
|
+
if (hiddenModelFor(provider, m.id || m.model, { resolved: extra.upstream || m.resolvedModel || null, sets })) continue;
|
|
261
|
+
push(normalizeCatalogModel(m, extra));
|
|
262
|
+
}
|
|
263
|
+
return out;
|
|
177
264
|
}
|
|
178
265
|
|
|
179
266
|
/**
|
|
180
|
-
*
|
|
181
|
-
*
|
|
182
|
-
*
|
|
183
|
-
*
|
|
184
|
-
*
|
|
267
|
+
* The catalog's empty list for `provider` is its answer, not a gap: the list was
|
|
268
|
+
* read (`sources`: live, or the last verified one) and the provider said it has
|
|
269
|
+
* no model (`known`: OpenCode with nothing connected) or listed some and the
|
|
270
|
+
* user hid every one (`listed`). Not an answer: no list or no `sources` (request
|
|
271
|
+
* failed, an older server), a provider that did not answer ('unavailable'), and
|
|
272
|
+
* the server's static stand-in while this window holds a verified list (the
|
|
273
|
+
* same precedence as mergeProviderModels).
|
|
274
|
+
*/
|
|
275
|
+
function catalogSaysNone(remote, provider, local) {
|
|
276
|
+
const rows = remote?.models?.[provider];
|
|
277
|
+
if (!Array.isArray(rows) || rows.length) return false;
|
|
278
|
+
const source = remote.sources?.[provider];
|
|
279
|
+
if (source !== 'live' && source !== 'stale' && source !== 'fallback') return false;
|
|
280
|
+
if (!(Number(remote.listed?.[provider]) > 0) && remote.known?.[provider] !== true) return false;
|
|
281
|
+
return !(source === 'fallback' && local.length > 0 && !local.some(m => m.catalogSource === 'fallback'));
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* { 'claude-code': [...], codex: [...], opencode: [...] }: the models of every
|
|
286
|
+
* Assistant model menu and of the Routes editor, from GET /api/assistant/catalog
|
|
287
|
+
* (see mergeProviderModels). Models the user hid (Assistant → Models, and
|
|
288
|
+
* Settings → OpenCode for OpenCode) never appear. `force` asks the server for a
|
|
289
|
+
* new build instead of its cached one.
|
|
185
290
|
*/
|
|
186
291
|
export async function loadModelCatalog({ force = false } = {}) {
|
|
187
292
|
if (!force && _catalog && Date.now() - _catalogAt < CATALOG_TTL_MS) return _catalog;
|
|
188
293
|
if (_catalogLoading) return _catalogLoading;
|
|
294
|
+
const epoch = _catalogEpoch;
|
|
295
|
+
const serverForce = force || _catalogForceNext;
|
|
296
|
+
_catalogForceNext = false;
|
|
189
297
|
_catalogLoading = (async () => {
|
|
190
298
|
const [remote] = await Promise.all([
|
|
191
|
-
fetchAssistantCatalog().catch(() => null),
|
|
299
|
+
fetchAssistantCatalog({ force: serverForce }).catch(() => null),
|
|
192
300
|
...PROVIDERS.map(p => Promise.resolve(ensureModelsForProfile(p, force)).catch(() => [])),
|
|
193
301
|
]);
|
|
194
302
|
const out = {};
|
|
@@ -196,36 +304,16 @@ export async function loadModelCatalog({ force = false } = {}) {
|
|
|
196
304
|
const sets = remote ? hiddenSets(remote) : { ..._hidden, opencode: new Set([...(_hidden.opencode || []), ...hiddenOpencodeIds(null)]) };
|
|
197
305
|
_hidden = sets;
|
|
198
306
|
for (const provider of PROVIDERS) {
|
|
199
|
-
const remoteList = Array.isArray(remote?.models?.[provider]) ? remote.models[provider] : [];
|
|
200
|
-
const byId = new Map(remoteList.filter(m => m?.id).map(m => [String(m.id), m]));
|
|
201
307
|
const local = getModelsForProfile(provider) || [];
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
const extra = byId.get(String(m.id)) || {};
|
|
207
|
-
if (hiddenModelFor(provider, m.id, { resolved: extra.upstream || m.resolvedModel || null, sets })) return false;
|
|
208
|
-
seen.add(m.id);
|
|
209
|
-
return true;
|
|
210
|
-
});
|
|
211
|
-
// Context variants ("opus[1m]", "gpt-x[extended]") the local list lacks: the
|
|
212
|
-
// catalog lists every one, each right after its base model.
|
|
213
|
-
if (provider !== 'opencode' && source !== remoteList) {
|
|
214
|
-
for (const r of remoteList) {
|
|
215
|
-
if (!/\[(1m|extended)\]$/i.test(String(r?.id || '')) || seen.has(String(r.id))) continue;
|
|
216
|
-
if (hiddenModelFor(provider, r.id, { resolved: r.upstream || null, sets })) continue;
|
|
217
|
-
const m = normalizeCatalogModel(r, r);
|
|
218
|
-
if (!m) continue;
|
|
219
|
-
seen.add(m.id);
|
|
220
|
-
const base = String(r.id).replace(/\[(1m|extended)\]$/i, '');
|
|
221
|
-
const at = out[provider].findIndex(x => x.id === base);
|
|
222
|
-
if (at >= 0) out[provider].splice(at + 1, 0, m); else out[provider].push(m);
|
|
223
|
-
}
|
|
224
|
-
}
|
|
308
|
+
// An empty list the catalog vouches for is its answer: the window's own rows do not stand in for it.
|
|
309
|
+
out[provider] = catalogSaysNone(remote, provider, local)
|
|
310
|
+
? []
|
|
311
|
+
: mergeProviderModels(provider, remote?.models?.[provider], local, { sets, remoteSource: remote?.sources?.[provider] || null });
|
|
225
312
|
}
|
|
226
313
|
_catalog = out;
|
|
227
|
-
//
|
|
228
|
-
|
|
314
|
+
// Not kept when built without the server's catalog, or when the lists changed
|
|
315
|
+
// while it loaded: the next open asks again.
|
|
316
|
+
_catalogAt = remote && epoch === _catalogEpoch ? Date.now() : 0;
|
|
229
317
|
_catalogPartial = !remote;
|
|
230
318
|
return out;
|
|
231
319
|
})().finally(() => { _catalogLoading = null; });
|
|
@@ -235,6 +323,11 @@ export async function loadModelCatalog({ force = false } = {}) {
|
|
|
235
323
|
/** The last loadModelCatalog() result, synchronously (null before the first load). */
|
|
236
324
|
export function cachedModelCatalog() { return _catalog; }
|
|
237
325
|
|
|
326
|
+
/** UI metadata from the same catalog load; no second list or extra endpoint. */
|
|
327
|
+
export function modelCatalogStatus() {
|
|
328
|
+
return { partial: _catalogPartial, hiddenCounts: Object.fromEntries(PROVIDERS.map(provider => [provider, _hidden[provider]?.size || 0])) };
|
|
329
|
+
}
|
|
330
|
+
|
|
238
331
|
/** The catalog row for provider/model (covers "[1m]" / "[extended]" ids), else null. */
|
|
239
332
|
export function catalogModelRow(provider, model, catalog = _catalog) {
|
|
240
333
|
if (!provider || !model) return null;
|
|
@@ -246,6 +339,27 @@ export function invalidateModelCatalog() {
|
|
|
246
339
|
_catalog = null;
|
|
247
340
|
_catalogAt = 0;
|
|
248
341
|
_catalogPartial = false;
|
|
342
|
+
_catalogEpoch += 1;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Tells `listener` whenever the lists behind loadModelCatalog() changed: a model
|
|
347
|
+
* hidden or shown, a provider listing other models (the server's
|
|
348
|
+
* `assistant:catalog-changed`), an OpenCode provider connected or removed. The
|
|
349
|
+
* cache is already dropped when it is called. Returns the function that stops it.
|
|
350
|
+
*/
|
|
351
|
+
export function onModelCatalogChange(listener) {
|
|
352
|
+
if (typeof listener !== 'function') return () => {};
|
|
353
|
+
_catalogListeners.add(listener);
|
|
354
|
+
return () => { _catalogListeners.delete(listener); };
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function modelListsChanged() {
|
|
358
|
+
invalidateModelCatalog();
|
|
359
|
+
_catalogForceNext = true;
|
|
360
|
+
for (const listener of [..._catalogListeners]) {
|
|
361
|
+
try { listener(); } catch { /* a listener's failure is its own */ }
|
|
362
|
+
}
|
|
249
363
|
}
|
|
250
364
|
|
|
251
365
|
/**
|
|
@@ -267,19 +381,25 @@ export function resolvableModels(provider, keep = '', catalog = _catalog) {
|
|
|
267
381
|
return hiddenModelFor(provider, keep) || unsure ? [...rows, { id: keep, selector: keep }] : rows;
|
|
268
382
|
}
|
|
269
383
|
|
|
270
|
-
// A Settings → OpenCode checkbox or an Assistant → Models toggle (this window, or another via
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
384
|
+
// A Settings → OpenCode checkbox or an Assistant → Models toggle (this window, or another via
|
|
385
|
+
// sync), an OpenCode provider connected or removed in Settings, a provider listing other models.
|
|
386
|
+
if (typeof document !== 'undefined') {
|
|
387
|
+
document.addEventListener(HIDDEN_MODELS_EVENT, modelListsChanged);
|
|
388
|
+
document.addEventListener('ocp-providers-changed', modelListsChanged);
|
|
389
|
+
}
|
|
390
|
+
on('sync:opencode:hidden-models-changed', modelListsChanged);
|
|
391
|
+
on('sync:assistant:hidden-models-changed', modelListsChanged);
|
|
392
|
+
on('sync:assistant:catalog-changed', modelListsChanged);
|
|
274
393
|
|
|
275
394
|
function sameModel(selected, m) {
|
|
276
395
|
if (!selected) return false;
|
|
277
396
|
return selected === m.id || selected === m.selector;
|
|
278
397
|
}
|
|
279
398
|
|
|
280
|
-
function modelDesc(m, tr) {
|
|
399
|
+
function modelDesc(m, tr, showUnknown = false) {
|
|
281
400
|
return [
|
|
282
|
-
m.ctxLabel
|
|
401
|
+
m.ctxLabel || (showUnknown ? m.contextWindow ? tr('assistant.routes.menu.contextTokens', '{count} tokens', { count: m.contextWindow.toLocaleString() }) : tr('assistant.routes.menu.contextUnknown', 'Context unknown') : ''), m.desc,
|
|
402
|
+
fmtModelPrice(m.price) || (showUnknown ? tr('assistant.routes.menu.priceUnknown', 'Price unknown') : ''), m.vision === true ? tr('assistant.brain.vision', 'vision') : '',
|
|
283
403
|
m.outputs?.image === true ? tr('assistant.brain.imageOut', 'makes images') : '',
|
|
284
404
|
m.outputs?.video === true ? tr('assistant.brain.videoOut', 'makes video') : '',
|
|
285
405
|
].filter(Boolean).join(' · ');
|
|
@@ -309,10 +429,23 @@ export async function openModelMenu(anchor, opts = {}) {
|
|
|
309
429
|
const tr = tf(t);
|
|
310
430
|
const b = normalizeBrain(brain);
|
|
311
431
|
anchor.setAttribute('aria-busy', 'true');
|
|
432
|
+
// Open immediately: a slow provider still has a visible, cancellable state.
|
|
433
|
+
const loading = opts.routingEditor ? openMenu(anchor, {
|
|
434
|
+
title, className: 'asst-model-menu asst-route-model-menu',
|
|
435
|
+
items: [{ kind: 'info', label: tr('assistant.routes.editorState.loadingModels', 'Loading models…') }],
|
|
436
|
+
width: 380, zIndex: opts.zIndex ?? null, repositionOnResize: true,
|
|
437
|
+
}) : null;
|
|
438
|
+
if (loading) loading.el.setAttribute('aria-busy', 'true');
|
|
439
|
+
if (loading) { loading.el.setAttribute('role', 'dialog'); anchor.setAttribute('aria-haspopup', 'dialog'); }
|
|
312
440
|
let catalog = {};
|
|
313
|
-
|
|
314
|
-
|
|
441
|
+
let failed = false;
|
|
442
|
+
try { catalog = await loadModelCatalog({ force: opts.forceReload === true }); } catch { failed = true; } finally { anchor.removeAttribute('aria-busy'); }
|
|
443
|
+
if (!anchor.isConnected || (loading && currentMenu() !== loading.el)) return null;
|
|
444
|
+
const metadata = modelCatalogStatus();
|
|
315
445
|
const items = [];
|
|
446
|
+
if (Array.isArray(opts.accountItems) && opts.accountItems.length) {
|
|
447
|
+
items.push({ kind: 'header', label: tr('assistant.brain.account', 'Account profile') }, ...opts.accountItems, { kind: 'separator' });
|
|
448
|
+
}
|
|
316
449
|
if (includeHere) {
|
|
317
450
|
items.push({ kind: 'radio', id: '__here', label: hereLabel || tr('assistant.routes.brainDecides', 'Let the brain decide'), selected: !current, onSelect: () => onSelect?.({ here: true }) });
|
|
318
451
|
}
|
|
@@ -320,26 +453,30 @@ export async function openModelMenu(anchor, opts = {}) {
|
|
|
320
453
|
let listed = 0;
|
|
321
454
|
for (const provider of list) {
|
|
322
455
|
const models = (catalog[provider] || []).filter(m => typeof accept !== 'function' || accept(provider, m));
|
|
323
|
-
if (!models.length) continue;
|
|
456
|
+
if (!models.length && !opts.routingEditor) continue;
|
|
324
457
|
listed += models.length;
|
|
325
458
|
const meta = getProviderMeta(provider);
|
|
326
|
-
const
|
|
459
|
+
const hiddenCount = metadata.hiddenCounts[provider] || 0;
|
|
460
|
+
const note = opts.routingEditor ? tr('assistant.routes.menu.hiddenCount', '{count} hidden in Models', { count: hiddenCount }) : warnSwitch && provider !== b.provider ? tr('assistant.brain.newConversation', 'starts a new conversation') : '';
|
|
327
461
|
items.push({ kind: 'header', label: meta.label, icon: meta.icon, color: meta.color, note });
|
|
328
462
|
for (const m of models) {
|
|
329
463
|
items.push({
|
|
330
464
|
kind: 'radio',
|
|
331
465
|
id: `${provider}:${m.id}`,
|
|
332
466
|
label: m.label,
|
|
333
|
-
desc: modelDesc(m, tr),
|
|
467
|
+
desc: modelDesc(m, tr, opts.routingEditor),
|
|
468
|
+
title: `${m.label || m.id} · ${m.id}`,
|
|
334
469
|
tag: tierTag(m, tr),
|
|
335
470
|
keywords: `${meta.label} ${m.id}`,
|
|
336
471
|
selected: !!current && current.provider === provider && sameModel(current.model, m),
|
|
337
472
|
onSelect: () => onSelect?.({ provider, model: m.id, label: m.label, tier: m.tier || null, vision: m.vision ?? null, outputs: m.outputs || null, efforts: m.efforts || null, price: m.price || null }),
|
|
338
473
|
});
|
|
339
474
|
}
|
|
475
|
+
if (opts.routingEditor && !models.length) items.push({ kind: 'info', label: typeof accept === 'function' ? tr('assistant.routes.menu.noCompatible', 'No compatible models shown for this provider.') : tr('assistant.routes.menu.providerEmpty', 'No models shown for this provider.') });
|
|
340
476
|
}
|
|
341
477
|
// Nothing passes the filter (e.g. no model makes video yet): say so instead of an empty list.
|
|
342
478
|
if (!listed && typeof accept === 'function' && emptyLabel) items.push({ kind: 'info', label: emptyLabel });
|
|
479
|
+
if (opts.routingEditor && !listed && typeof accept !== 'function') items.push({ kind: 'info', label: tr('assistant.routes.editorState.modelsEmpty', 'No models are available. Check Models or connect a provider, then reload.') });
|
|
343
480
|
if (allowCustom) {
|
|
344
481
|
items.push({ kind: 'separator' });
|
|
345
482
|
items.push({
|
|
@@ -356,29 +493,63 @@ export async function openModelMenu(anchor, opts = {}) {
|
|
|
356
493
|
},
|
|
357
494
|
});
|
|
358
495
|
}
|
|
359
|
-
if (typeof opts.onManageModels === 'function') {
|
|
496
|
+
if (!opts.routingEditor && typeof opts.onManageModels === 'function') {
|
|
360
497
|
items.push({ id: '__manage_models', label: tr('assistant.models.manage', 'Manage models…'), onSelect: () => opts.onManageModels() });
|
|
361
498
|
}
|
|
362
|
-
|
|
499
|
+
const menu = openMenu(anchor, {
|
|
363
500
|
title: title || tr('assistant.brain.model', 'Model'),
|
|
364
501
|
items,
|
|
365
502
|
filter: true,
|
|
366
503
|
filterPlaceholder: tr('assistant.brain.filterModels', 'Filter models…'),
|
|
367
504
|
placement,
|
|
368
|
-
width: 300,
|
|
505
|
+
width: opts.routingEditor ? 380 : 300,
|
|
369
506
|
role: 'menu',
|
|
370
|
-
className: 'asst-model-menu'
|
|
507
|
+
className: `asst-model-menu${opts.routingEditor ? ' asst-route-model-menu' : ''}`,
|
|
371
508
|
zIndex: opts.zIndex ?? null,
|
|
509
|
+
repositionOnResize: true,
|
|
510
|
+
filterEmptyLabel: tr('assistant.routes.menu.noMatches', 'No matching models. Check Models for hidden choices.'),
|
|
372
511
|
});
|
|
512
|
+
if (opts.routingEditor && menu) {
|
|
513
|
+
// Search and a custom-id input belong to a dialog; choices are a radio group.
|
|
514
|
+
// A menu role cannot contain these form fields or recovery buttons.
|
|
515
|
+
menu.el.setAttribute('role', 'dialog');
|
|
516
|
+
anchor.setAttribute('aria-haspopup', 'dialog');
|
|
517
|
+
const choices = menu.list.querySelectorAll('[role="menuitemradio"]');
|
|
518
|
+
for (const choice of choices) choice.setAttribute('role', 'radio');
|
|
519
|
+
const groupRole = () => {
|
|
520
|
+
const visible = [...choices].filter(n => !n.hidden);
|
|
521
|
+
const focusChoice = visible.find(n => n.getAttribute('aria-checked') === 'true') || visible[0];
|
|
522
|
+
for (const choice of choices) choice.tabIndex = choice === focusChoice ? 0 : -1;
|
|
523
|
+
menu.list.setAttribute('role', visible.length ? 'radiogroup' : 'group');
|
|
524
|
+
menu.list.setAttribute('aria-labelledby', `${menu.el.id}-title`);
|
|
525
|
+
};
|
|
526
|
+
groupRole(); menu.el.querySelector('.asst-dd-filter')?.addEventListener('input', groupRole);
|
|
527
|
+
// Outside the filter's scroller: recovery stays visible, including zero results.
|
|
528
|
+
const footer = document.createElement('div'); footer.className = 'asst-route-menu-foot';
|
|
529
|
+
const note = document.createElement('div'); note.className = 'asst-dd-info';
|
|
530
|
+
note.textContent = failed || metadata.partial ? tr('assistant.routes.menu.failed', 'Model list could not be refreshed. Provider lists may be out of date.') : tr('assistant.routes.menu.hiddenHelp', 'Missing a model? Your Models list controls what is shown.');
|
|
531
|
+
note.setAttribute('role', 'status'); footer.append(note);
|
|
532
|
+
const customInput = menu.list.querySelector('.asst-dd-input');
|
|
533
|
+
if (customInput) footer.append(customInput);
|
|
534
|
+
for (const [label, action] of [
|
|
535
|
+
...(typeof opts.onManageModels === 'function' ? [[tr('assistant.models.manage', 'Manage models…'), () => { closeMenu(); opts.onManageModels(); }]] : []),
|
|
536
|
+
[tr('assistant.routes.reloadModels', 'Reload models'), () => { closeMenu(); openModelMenu(anchor, { ...opts, forceReload: true }); }],
|
|
537
|
+
]) {
|
|
538
|
+
const button = document.createElement('button'); button.type = 'button'; button.className = 'asst-dd-item'; button.tabIndex = -1; button.textContent = label; button.addEventListener('click', action); footer.append(button);
|
|
539
|
+
}
|
|
540
|
+
menu.el.append(footer); menu.reposition();
|
|
541
|
+
}
|
|
542
|
+
return menu;
|
|
373
543
|
}
|
|
374
544
|
|
|
375
545
|
// ── Picker ──────────────────────────────────────────────────────────────────
|
|
376
546
|
|
|
377
|
-
// Creation order is DOM order within a slot: model · effort · variant · mode · account in the composer.
|
|
547
|
+
// Creation order is DOM order within a slot: model · effort · variant · mode · tests · account in the composer.
|
|
378
548
|
// `effort` is the OpenCode agent for OpenCode; `variant` is OpenCode's effort (the model's variants).
|
|
379
|
-
|
|
549
|
+
// `tests` is the session's Test density, not a brain field: it shows only while the host supplies a level.
|
|
550
|
+
const FIELDS = ['model', 'effort', 'variant', 'mode', 'tests', 'account', 'project', 'mcp'];
|
|
380
551
|
// Slot of each field: the composer row (the brain and how it acts) · the toolbar (where it works).
|
|
381
|
-
const FIELD_GROUP = { model: 'brain', effort: 'brain', variant: 'brain', mode: 'brain', account: 'brain', project: 'context', mcp: 'context' };
|
|
552
|
+
const FIELD_GROUP = { model: 'brain', effort: 'brain', variant: 'brain', mode: 'brain', tests: 'brain', account: 'brain', project: 'context', mcp: 'context' };
|
|
382
553
|
|
|
383
554
|
/**
|
|
384
555
|
* createBrainPicker(host, hooks) — `host` is either one element (all fields in
|
|
@@ -386,7 +557,9 @@ const FIELD_GROUP = { model: 'brain', effort: 'brain', variant: 'brain', mode: '
|
|
|
386
557
|
* hooks: { t, getBrain() → brain, onChange(nextBrain, { requiresSwitch, field }),
|
|
387
558
|
* onAddAccount(provider), onManageAccounts(provider), onToast(text),
|
|
388
559
|
* getMenuAnchor() → fallback anchor (the toolbar "⋯"),
|
|
389
|
-
* menuPlacement: 'below' (default) | 'above' | 'auto'
|
|
560
|
+
* menuPlacement: 'below' (default) | 'above' | 'auto',
|
|
561
|
+
* getTestDensity() → level | null (null hides the field), onTestDensity(level),
|
|
562
|
+
* onProjects(projects) — the registered projects were (re)loaded }
|
|
390
563
|
*/
|
|
391
564
|
export function createBrainPicker(host, hooks = {}) {
|
|
392
565
|
const t = tf(hooks.t);
|
|
@@ -415,6 +588,9 @@ export function createBrainPicker(host, hooks = {}) {
|
|
|
415
588
|
|
|
416
589
|
let destroyed = false;
|
|
417
590
|
let loading = null;
|
|
591
|
+
// The empty state's "Choose a project" (mountEmptyProject): the project field's second button.
|
|
592
|
+
let heroProject = null;
|
|
593
|
+
let heroProjectMascot = null;
|
|
418
594
|
|
|
419
595
|
function currentBrain() {
|
|
420
596
|
return normalizeBrain(hooks.getBrain?.());
|
|
@@ -454,9 +630,16 @@ export function createBrainPicker(host, hooks = {}) {
|
|
|
454
630
|
return accounts.find(a => a.id === brain.accountId) || null;
|
|
455
631
|
}
|
|
456
632
|
|
|
633
|
+
const accountProvider = (brain) => (brain.provider === 'codex' ? 'codex' : 'claude');
|
|
457
634
|
function accountLabel(brain) {
|
|
458
635
|
const acc = accountOf(brain);
|
|
459
|
-
|
|
636
|
+
const name = acc ? accountName(acc, accountProvider(brain)) : (brain.accountId || t('assistant.brain.accountDefault', 'Default'));
|
|
637
|
+
// An account says what it runs on (its subscription or its API key), or that it is signed out.
|
|
638
|
+
if (acc && acc.mode !== undefined) {
|
|
639
|
+
const badge = accountBadge(acc, accountProvider(brain));
|
|
640
|
+
return badge.text ? `${name} · ${badge.text}` : name;
|
|
641
|
+
}
|
|
642
|
+
return name;
|
|
460
643
|
}
|
|
461
644
|
|
|
462
645
|
function modelLabel(brain = currentBrain()) {
|
|
@@ -479,13 +662,19 @@ export function createBrainPicker(host, hooks = {}) {
|
|
|
479
662
|
: (level ? level.label : (brain.effort || t('assistant.brain.effortOff', 'off'))),
|
|
480
663
|
variantSupported: brain.provider === 'opencode' && hasEfforts,
|
|
481
664
|
variant: level ? level.label : (brain.effort || t('assistant.brain.variantDefault', 'default variant')),
|
|
482
|
-
project: brain.project ?
|
|
665
|
+
project: brain.project ? projectLabel(brain.project, _projectsCache || []) : t('assistant.brain.projectHome', 'Home'),
|
|
483
666
|
mode: permissionModeLabel(brain, (id, fallback) => t(`assistant.brain.modes.${id}`, fallback)),
|
|
484
667
|
mcp: presets[brain.mcpProfile]?.label || brain.mcpProfile || 'full',
|
|
485
668
|
account: ACCOUNT_PROVIDERS.has(brain.provider) ? accountLabel(brain) : '',
|
|
486
669
|
};
|
|
487
670
|
}
|
|
488
671
|
|
|
672
|
+
/** The session's Test density, or null while the host has none to show (a server that does not know it). */
|
|
673
|
+
function testDensity() {
|
|
674
|
+
const level = typeof hooks.getTestDensity === 'function' ? hooks.getTestDensity() : null;
|
|
675
|
+
return level ? normalizeTestDensity(level) : null;
|
|
676
|
+
}
|
|
677
|
+
|
|
489
678
|
function setBtn(field, { label, icon, color, hidden, disabled, tooltip, aria }) {
|
|
490
679
|
const btn = buttons[field];
|
|
491
680
|
if (!btn) return;
|
|
@@ -506,6 +695,13 @@ export function createBrainPicker(host, hooks = {}) {
|
|
|
506
695
|
const v = values(brain);
|
|
507
696
|
const modelTip = [v.meta.label, brain.model || t('assistant.brain.modelDefault', 'Default model'), v.account ? t('assistant.brain.asAccount', 'as {account}', { account: v.account }) : ''].filter(Boolean).join(' · ');
|
|
508
697
|
setBtn('model', { label: v.model, icon: v.meta.icon, color: v.meta.color, tooltip: modelTip, aria: `${t('assistant.brain.model', 'Model')}: ${modelTip}` });
|
|
698
|
+
if (ACCOUNT_PROVIDERS.has(brain.provider)) {
|
|
699
|
+
const acc = accountOf(brain);
|
|
700
|
+
const label = buttons.model.querySelector('.asst-dd-label');
|
|
701
|
+
label.classList.add('asst-model-account-label');
|
|
702
|
+
const mode = acc ? accountCompactMode(acc, accountProvider(brain)) : t('accounts.checking', 'Checking…');
|
|
703
|
+
label.replaceChildren(el('span', 'asst-model-name', v.model), el('span', '', '·'), el('span', 'asst-model-account', acc ? accountName(acc, accountProvider(brain)) : brain.accountId || t('assistant.brain.accountDefault', 'Default')), el('span', '', '·'), el('span', 'asst-model-mode', mode));
|
|
704
|
+
} else buttons.model.querySelector('.asst-dd-label').classList.remove('asst-model-account-label');
|
|
509
705
|
// A display name ("Sonnet 5") reads in sans; a raw id nothing names ("opus[1m]") stays mono.
|
|
510
706
|
buttons.model?.toggleAttribute('data-raw', /^[a-z0-9][\w.\-[\]:/]*$/.test(String(v.model || '')));
|
|
511
707
|
|
|
@@ -515,12 +711,70 @@ export function createBrainPicker(host, hooks = {}) {
|
|
|
515
711
|
setBtn('effort', { label: v.effort, icon: ICON_EFFORT, hidden: !v.effortSupported, tooltip: `${t('assistant.brain.effort', 'Thinking effort')}: ${v.effort}` });
|
|
516
712
|
}
|
|
517
713
|
setBtn('variant', { label: v.variant, icon: ICON_EFFORT, hidden: !v.variantSupported, tooltip: `${t('assistant.brain.variant', 'Model variant')}: ${v.variant}` });
|
|
518
|
-
//
|
|
519
|
-
|
|
520
|
-
|
|
714
|
+
// A Claude or Codex brain always shows it: which account and which credential (its subscription or
|
|
715
|
+
// its API key) it runs on, or that the account is signed out.
|
|
716
|
+
const brainAcc = ACCOUNT_PROVIDERS.has(brain.provider) ? accountOf(brain) : null;
|
|
717
|
+
const accountTip = [v.account, brainAcc ? accountIdentity(brainAcc) : ''].filter(Boolean).join(' · ');
|
|
718
|
+
setBtn('account', { label: v.account, icon: ICON_ACCOUNT, hidden: true, tooltip: `${t('assistant.brain.account', 'Account profile')}: ${accountTip}` });
|
|
719
|
+
buttons.account?.classList.toggle('asst-dd-warn', !!brainAcc && brainAcc.signedIn === false);
|
|
521
720
|
setBtn('project', { label: v.project, icon: ICON_FOLDER, tooltip: brain.project || t('assistant.brain.projectNoneTip', 'Working project: home directory'), aria: `${t('assistant.brain.project', 'Working project')}: ${brain.project || v.project}` });
|
|
522
721
|
setBtn('mode', { label: v.mode, icon: ICON_SHIELD, tooltip: `${t('assistant.brain.mode', 'Permission mode')}: ${v.mode}` });
|
|
722
|
+
const tests = testDensity();
|
|
723
|
+
const testsLabel = tests ? testDensityLabel(tests, hooks.t) : '';
|
|
724
|
+
setBtn('tests', { label: testsLabel, icon: testsIcon(testDensityBars(tests)), hidden: !tests, tooltip: `${t('assistant.tests.title', 'Test density')}: ${testsLabel}` });
|
|
725
|
+
if (tests) buttons.tests.dataset.level = tests; else delete buttons.tests.dataset.level;
|
|
523
726
|
setBtn('mcp', { label: v.mcp, icon: ICON_PLUG, tooltip: `${t('assistant.brain.mcp', 'MCP tool profile')}: ${v.mcp}` });
|
|
727
|
+
renderEmptyProject(brain);
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
function renderEmptyProject(brain = currentBrain()) {
|
|
731
|
+
const btn = heroProject;
|
|
732
|
+
if (!btn) return;
|
|
733
|
+
const view = emptyProjectView(brain.project, _projectsCache || [], t);
|
|
734
|
+
btn.dataset.chosen = view.chosen ? '1' : '0';
|
|
735
|
+
const key = btn.querySelector('.asst-empty-project-key');
|
|
736
|
+
key.textContent = view.key;
|
|
737
|
+
key.hidden = !view.key;
|
|
738
|
+
btn.querySelector('.asst-dd-label').textContent = view.label;
|
|
739
|
+
if (view.title) btn.setAttribute('data-tooltip', view.title); else btn.removeAttribute('data-tooltip');
|
|
740
|
+
btn.setAttribute('aria-label', view.aria);
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
/**
|
|
744
|
+
* The empty state's project picker, in `slot` (asst-render.js): one more
|
|
745
|
+
* button for the project field, drawn by render() like the toolbar's and
|
|
746
|
+
* opening the same menu, so the two always show the same project. A pick
|
|
747
|
+
* sends focus back to it. A new slot replaces the last one; null removes it.
|
|
748
|
+
*/
|
|
749
|
+
function mountEmptyProject(slot) {
|
|
750
|
+
if (heroProject && isMenuOpen(heroProject)) closeMenu();
|
|
751
|
+
heroProjectMascot?.destroy();
|
|
752
|
+
heroProjectMascot = null;
|
|
753
|
+
heroProject?.remove();
|
|
754
|
+
heroProject = null;
|
|
755
|
+
if (destroyed || !slot || typeof slot.appendChild !== 'function') return null;
|
|
756
|
+
const btn = el('button', 'asst-dd asst-empty-control-btn asst-empty-project-btn');
|
|
757
|
+
btn.type = 'button';
|
|
758
|
+
btn.dataset.field = 'project';
|
|
759
|
+
btn.setAttribute('aria-haspopup', 'menu');
|
|
760
|
+
btn.setAttribute('aria-expanded', 'false');
|
|
761
|
+
// glyph | seam | "Project" + name | caret: the field's capsule at the suggestions' md size.
|
|
762
|
+
const icon = el('span', 'asst-icon');
|
|
763
|
+
icon.innerHTML = ICON_FOLDER;
|
|
764
|
+
const caret = el('span', 'asst-dd-caret');
|
|
765
|
+
caret.innerHTML = ICON_CARET;
|
|
766
|
+
for (const part of [icon, caret]) part.setAttribute('aria-hidden', 'true');
|
|
767
|
+
btn.append(icon, el('span', 'asst-empty-project-key'), el('span', 'asst-dd-label'), caret);
|
|
768
|
+
btn.addEventListener('click', (e) => {
|
|
769
|
+
e.stopPropagation();
|
|
770
|
+
if (isMenuOpen(btn)) { closeMenu(); return; }
|
|
771
|
+
openField('project', btn, { restoreFocus: true });
|
|
772
|
+
});
|
|
773
|
+
slot.appendChild(btn);
|
|
774
|
+
heroProject = btn;
|
|
775
|
+
heroProjectMascot = createMascotControl(btn, { director: hooks.mascot, pose: 'project' });
|
|
776
|
+
renderEmptyProject();
|
|
777
|
+
return btn;
|
|
524
778
|
}
|
|
525
779
|
|
|
526
780
|
/** Current values for the toolbar "⋯" menu (fields hidden inline stay reachable there). */
|
|
@@ -535,6 +789,7 @@ export function createBrainPicker(host, hooks = {}) {
|
|
|
535
789
|
{ field: 'variant', label: t('assistant.brain.variant', 'Model variant'), value: v.variant, icon: ICON_EFFORT, hidden: !v.variantSupported },
|
|
536
790
|
{ field: 'project', label: t('assistant.brain.project', 'Working project'), value: brain.project || v.project, icon: ICON_FOLDER, hidden: false },
|
|
537
791
|
{ field: 'mode', label: t('assistant.brain.mode', 'Permission mode'), value: v.mode, icon: ICON_SHIELD, hidden: false },
|
|
792
|
+
{ field: 'tests', label: t('assistant.tests.title', 'Test density'), value: testDensity() ? testDensityLabel(testDensity(), hooks.t) : '', icon: testsIcon(testDensityBars(testDensity())), hidden: !testDensity() },
|
|
538
793
|
{ field: 'mcp', label: t('assistant.brain.mcp', 'MCP tool profile'), value: v.mcp, icon: ICON_PLUG, hidden: false },
|
|
539
794
|
{ field: 'account', label: t('assistant.brain.account', 'Account profile'), value: v.account, icon: ICON_ACCOUNT, hidden: !ACCOUNT_PROVIDERS.has(brain.provider) },
|
|
540
795
|
];
|
|
@@ -555,7 +810,12 @@ export function createBrainPicker(host, hooks = {}) {
|
|
|
555
810
|
if (provider === 'codex') jobs.push(fetchCodexAccounts(force));
|
|
556
811
|
if (provider === 'claude-code') jobs.push(fetchClaudeAccounts(force));
|
|
557
812
|
if (provider === 'opencode') jobs.push(fetchOpencodeAgents(force));
|
|
558
|
-
if (!_projectsCache || force)
|
|
813
|
+
if (!_projectsCache || force) {
|
|
814
|
+
jobs.push(fetchProjects().then((p) => {
|
|
815
|
+
_projectsCache = p;
|
|
816
|
+
if (!destroyed) { try { hooks.onProjects?.(p); } catch { /* the host's own failure */ } }
|
|
817
|
+
}).catch(() => { _projectsCache = _projectsCache || []; }));
|
|
818
|
+
}
|
|
559
819
|
await Promise.allSettled(jobs);
|
|
560
820
|
}
|
|
561
821
|
|
|
@@ -580,17 +840,22 @@ export function createBrainPicker(host, hooks = {}) {
|
|
|
580
840
|
return fallback || btn || root;
|
|
581
841
|
}
|
|
582
842
|
|
|
583
|
-
/**
|
|
584
|
-
|
|
843
|
+
/**
|
|
844
|
+
* Open a field menu; `anchorOverride` (e.g. the toolbar "⋯") when the inline
|
|
845
|
+
* button is hidden. `restoreFocus`: a pick sends focus back to the anchor.
|
|
846
|
+
*/
|
|
847
|
+
async function openField(field, anchorOverride = null, { restoreFocus = false } = {}) {
|
|
585
848
|
const anchor = anchorFor(field, anchorOverride);
|
|
586
849
|
const brain = currentBrain();
|
|
587
850
|
if (field === 'model') {
|
|
851
|
+
if (ACCOUNT_PROVIDERS.has(brain.provider)) await ensureLoaded(brain.provider);
|
|
588
852
|
await openModelMenu(anchor, {
|
|
589
853
|
brain,
|
|
590
854
|
t: hooks.t,
|
|
591
855
|
placement,
|
|
592
856
|
current: { provider: brain.provider, model: brain.model },
|
|
593
857
|
warnSwitch: true,
|
|
858
|
+
accountItems: ACCOUNT_PROVIDERS.has(brain.provider) ? accountItems(brain, { composer: true }) : [],
|
|
594
859
|
onToast: hooks.onToast,
|
|
595
860
|
onManageModels: hooks.onManageModels,
|
|
596
861
|
onSelect: (target) => {
|
|
@@ -601,6 +866,22 @@ export function createBrainPicker(host, hooks = {}) {
|
|
|
601
866
|
});
|
|
602
867
|
return;
|
|
603
868
|
}
|
|
869
|
+
// The session's own setting: nothing to load.
|
|
870
|
+
if (field === 'tests') {
|
|
871
|
+
const current = testDensity();
|
|
872
|
+
if (!current) return;
|
|
873
|
+
openMenu(anchor, {
|
|
874
|
+
title: t('assistant.tests.title', 'Test density'),
|
|
875
|
+
items: TEST_DENSITIES.map((id) => ({
|
|
876
|
+
kind: 'radio', id, label: testDensityLabel(id, hooks.t),
|
|
877
|
+
desc: t(`assistant.tests.desc.${id}`, TEST_DENSITY_COPY[id][1]),
|
|
878
|
+
selected: id === current,
|
|
879
|
+
onSelect: () => { hooks.onTestDensity?.(id); render(); },
|
|
880
|
+
})),
|
|
881
|
+
placement, role: 'menu', className: 'asst-tests-menu',
|
|
882
|
+
});
|
|
883
|
+
return;
|
|
884
|
+
}
|
|
604
885
|
// Everything else needs the provider catalog — load it lazily on first open.
|
|
605
886
|
anchor.setAttribute('aria-busy', 'true');
|
|
606
887
|
try { await ensureLoaded(brain.provider); } finally { anchor.removeAttribute('aria-busy'); }
|
|
@@ -639,29 +920,23 @@ export function createBrainPicker(host, hooks = {}) {
|
|
|
639
920
|
|
|
640
921
|
if (field === 'account') {
|
|
641
922
|
if (!ACCOUNT_PROVIDERS.has(brain.provider)) return;
|
|
642
|
-
|
|
643
|
-
const items = accounts.map((a) => ({
|
|
644
|
-
kind: 'radio',
|
|
645
|
-
id: a.id,
|
|
646
|
-
label: a.label || a.email || a.id,
|
|
647
|
-
desc: a.email && a.email !== a.label ? a.email : (a.isDefault ? t('assistant.accounts.ambient', 'ambient config') : ''),
|
|
648
|
-
selected: a.id === brain.accountId,
|
|
649
|
-
onSelect: () => commit({ accountId: a.id }, 'account'),
|
|
650
|
-
}));
|
|
651
|
-
items.push({ kind: 'separator' });
|
|
652
|
-
items.push({ id: '__add', label: t('assistant.accounts.add', 'Add account…'), onSelect: () => hooks.onAddAccount?.(brain.provider) });
|
|
653
|
-
items.push({ id: '__manage', label: t('assistant.accounts.manage', 'Manage…'), onSelect: () => hooks.onManageAccounts?.(brain.provider) });
|
|
654
|
-
menu(t('assistant.brain.account', 'Account profile'), items);
|
|
923
|
+
menu(t('assistant.brain.account', 'Account profile'), accountItems(brain), { width: 300 });
|
|
655
924
|
return;
|
|
656
925
|
}
|
|
657
926
|
|
|
658
927
|
if (field === 'project') {
|
|
928
|
+
const actor = anchor === heroProject ? heroProjectMascot : null;
|
|
929
|
+
const pick = project => {
|
|
930
|
+
const changed = (currentBrain().project || null) !== (project || null);
|
|
931
|
+
commit({ project }, 'project');
|
|
932
|
+
actor?.commit(changed ? 'hop' : 'nod');
|
|
933
|
+
};
|
|
659
934
|
const projects = _projectsCache || [];
|
|
660
|
-
const items = [{ kind: 'radio', id: '', label: t('assistant.brain.projectNone', 'Home directory'), desc: t('assistant.brain.projectNoneDesc', 'no project'), selected: !brain.project, onSelect: () =>
|
|
661
|
-
for (const p of projects) items.push({ kind: 'radio', id: p.path, label: p.label || basename(p.path), desc: p.path, selected: p.path === brain.project, onSelect: () =>
|
|
935
|
+
const items = [{ kind: 'radio', id: '', label: t('assistant.brain.projectNone', 'Home directory'), desc: t('assistant.brain.projectNoneDesc', 'no project'), selected: !brain.project, restoreFocus, onSelect: () => pick(null) }];
|
|
936
|
+
for (const p of projects) items.push({ kind: 'radio', id: p.path, label: p.label || basename(p.path), desc: p.path, selected: p.path === brain.project, restoreFocus, onSelect: () => pick(p.path) });
|
|
662
937
|
items.push({ kind: 'separator' });
|
|
663
|
-
items.push({ kind: 'input', placeholder: t('assistant.brain.projectCustom', 'Type a directory path…'), value: brain.project && !projects.some(p => p.path === brain.project) ? brain.project : '', onSubmit:
|
|
664
|
-
menu(t('assistant.brain.project', 'Working project'), items, { width: 280, filter: projects.length > 8, filterPlaceholder: t('assistant.brain.filterProjects', 'Filter projects…') });
|
|
938
|
+
items.push({ kind: 'input', placeholder: t('assistant.brain.projectCustom', 'Type a directory path…'), value: brain.project && !projects.some(p => p.path === brain.project) ? brain.project : '', restoreFocus, onSubmit: pick });
|
|
939
|
+
menu(t('assistant.brain.project', 'Working project'), items, { width: 280, filter: projects.length > 8, filterPlaceholder: t('assistant.brain.filterProjects', 'Filter projects…'), onOpen: node => actor?.open(node), onClose: () => actor?.close() });
|
|
665
940
|
return;
|
|
666
941
|
}
|
|
667
942
|
|
|
@@ -704,6 +979,69 @@ export function createBrainPicker(host, hooks = {}) {
|
|
|
704
979
|
}
|
|
705
980
|
}
|
|
706
981
|
|
|
982
|
+
// The brain's account menu (Claude or Codex): every account with what it runs on and who it is (a
|
|
983
|
+
// signed-out one is listed, not offered), the mode of the brain's account when it holds both credentials,
|
|
984
|
+
// and the account dispatched workers of that provider use when a dispatch names none.
|
|
985
|
+
function accountItems(brain, { composer = false } = {}) {
|
|
986
|
+
const codex = brain.provider === 'codex';
|
|
987
|
+
const p = codex ? 'codex' : 'claude';
|
|
988
|
+
const accounts = codex ? getCachedCodexAccounts() : getCachedClaudeAccounts();
|
|
989
|
+
const refetch = () => (codex ? fetchCodexAccounts(true) : fetchClaudeAccounts(true));
|
|
990
|
+
const items = accounts.map((a) => {
|
|
991
|
+
const badge = accountBadge(a, p);
|
|
992
|
+
const usable = accountUsable(a);
|
|
993
|
+
return {
|
|
994
|
+
kind: 'radio', id: a.id, label: accountName(a, p),
|
|
995
|
+
desc: [accountIdentity(a), accountUsageText(a, hooks.t), !usable ? t('assistant.accounts.signInFirst', 'Signed out: sign it in from Manage…') : composer && a.id !== brain.accountId ? t('assistant.brain.newConversation', 'starts a new conversation') : ''].filter(Boolean).join(' · '),
|
|
996
|
+
tag: badge.text, disabled: !usable,
|
|
997
|
+
selected: a.id === brain.accountId,
|
|
998
|
+
onSelect: () => commit({ accountId: a.id }, 'account'),
|
|
999
|
+
};
|
|
1000
|
+
});
|
|
1001
|
+
items.unshift({ kind: 'radio', id: 'auto', label: autoAccountLabel(hooks.t), desc: t('assistant.accountUsage.composerHint', 'Pick the subscription with most usage left and start a new conversation.'), onSelect: () => commit({ accountId: 'auto' }, 'account') });
|
|
1002
|
+
if (composer) for (const a of accounts.filter(a => !accountUsable(a))) items.push({ id: `signin:${a.id}`, label: t('assistant.accounts.signInAccount', 'Sign in to {label}…', { label: accountName(a, p) }), onSelect: () => hooks.onManageAccounts?.(brain.provider, a.id) });
|
|
1003
|
+
const current = accounts.find((a) => a.id === brain.accountId);
|
|
1004
|
+
const modes = modeChoices(current, p);
|
|
1005
|
+
if (modes.length) {
|
|
1006
|
+
items.push({ kind: 'separator' });
|
|
1007
|
+
items.push({ kind: 'header', label: t('assistant.accounts.usesHeader', '{label} uses (every session on it)', { label: accountName(current, p) }) });
|
|
1008
|
+
for (const m of modes) {
|
|
1009
|
+
items.push({
|
|
1010
|
+
kind: 'radio', id: `mode:${m.mode}`, label: m.label, desc: [m.detail, !m.current ? t('assistant.brain.newConversation', 'starts a new conversation') : ''].filter(Boolean).join(' · '), selected: m.current, disabled: hooks.getRunning?.() === true,
|
|
1011
|
+
onSelect: async () => {
|
|
1012
|
+
if (m.current) return;
|
|
1013
|
+
try {
|
|
1014
|
+
await (codex ? patchCodexAccount : patchClaudeAccount)(current.id, { mode: m.mode }); await refetch();
|
|
1015
|
+
hooks.onChange?.(currentBrain(), { requiresSwitch: true, restartBrain: true, field: 'accountMode', previous: brain }); render();
|
|
1016
|
+
}
|
|
1017
|
+
catch (err) { hooks.onToast?.(err?.message || String(err)); }
|
|
1018
|
+
},
|
|
1019
|
+
});
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
const worker = codex ? getCodexWorkerAccountId() : getClaudeWorkerAccountId();
|
|
1023
|
+
if (!composer) {
|
|
1024
|
+
items.push({ kind: 'separator' });
|
|
1025
|
+
items.push({ kind: 'header', label: codex ? t('assistant.accounts.codexWorkersHeader', 'Codex workers use') : t('assistant.accounts.workersHeader', 'Claude Code workers use') });
|
|
1026
|
+
for (const a of accounts) {
|
|
1027
|
+
const usable = accountUsable(a);
|
|
1028
|
+
items.push({
|
|
1029
|
+
kind: 'radio', id: `worker:${a.id}`, label: accountName(a, p), tag: accountBadge(a, p).text,
|
|
1030
|
+
disabled: !usable && a.id !== worker, selected: a.id === worker,
|
|
1031
|
+
onSelect: async () => {
|
|
1032
|
+
if (a.id === worker) return;
|
|
1033
|
+
try { await (codex ? setCodexWorkerAccount : setClaudeWorkerAccount)(a.id); await refetch(); render(); }
|
|
1034
|
+
catch (err) { hooks.onToast?.(err?.message || String(err)); }
|
|
1035
|
+
},
|
|
1036
|
+
});
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
items.push({ kind: 'separator' });
|
|
1040
|
+
items.push({ id: '__add', label: t('assistant.accounts.add', 'Add account…'), onSelect: () => hooks.onAddAccount?.(brain.provider) });
|
|
1041
|
+
items.push({ id: '__manage', label: t('assistant.accounts.manage', 'Manage…'), onSelect: () => hooks.onManageAccounts?.(brain.provider) });
|
|
1042
|
+
return items;
|
|
1043
|
+
}
|
|
1044
|
+
|
|
707
1045
|
async function switchProvider(provider, model = '') {
|
|
708
1046
|
const before = currentBrain();
|
|
709
1047
|
if (provider === before.provider) return;
|
|
@@ -727,6 +1065,16 @@ export function createBrainPicker(host, hooks = {}) {
|
|
|
727
1065
|
|
|
728
1066
|
render();
|
|
729
1067
|
refresh().catch(() => {});
|
|
1068
|
+
// Any window changed a Claude or Codex account (signed in or out, a key, its mode, the worker choice,
|
|
1069
|
+
// added, removed): its list is read again and the picker re-drawn, so it never shows a stale state.
|
|
1070
|
+
const accountsChanged = (provider) => (msg) => {
|
|
1071
|
+
const which = provider || (msg?.provider === 'codex' ? 'codex' : msg?.provider === 'claude-code' ? 'claude-code' : null);
|
|
1072
|
+
const jobs = [];
|
|
1073
|
+
if (!which || which === 'claude-code') jobs.push(fetchClaudeAccounts(true));
|
|
1074
|
+
if (!which || which === 'codex') jobs.push(fetchCodexAccounts(true));
|
|
1075
|
+
Promise.all(jobs).then(() => { if (!destroyed) render(); }).catch(() => {});
|
|
1076
|
+
};
|
|
1077
|
+
const offAccounts = [on('sync:claude:accounts-changed', accountsChanged('claude-code')), on('sync:codex:accounts-changed', accountsChanged('codex')), on('sync:assistant:accounts-changed', accountsChanged(null))];
|
|
730
1078
|
|
|
731
1079
|
return {
|
|
732
1080
|
el: root,
|
|
@@ -737,8 +1085,9 @@ export function createBrainPicker(host, hooks = {}) {
|
|
|
737
1085
|
openField,
|
|
738
1086
|
fieldSummary,
|
|
739
1087
|
modelLabel: () => modelLabel(),
|
|
1088
|
+
mountEmptyProject,
|
|
740
1089
|
setBrain() { render(); },
|
|
741
1090
|
close: closeMenu,
|
|
742
|
-
destroy() { destroyed = true; closeMenu(); Object.values(buttons).forEach((b) => b.remove()); root.remove(); },
|
|
1091
|
+
destroy() { destroyed = true; for (const off of offAccounts) { try { off?.(); } catch {} } closeMenu(); heroProjectMascot?.destroy(); Object.values(buttons).forEach((b) => b.remove()); heroProject?.remove(); heroProject = null; root.remove(); },
|
|
743
1092
|
};
|
|
744
1093
|
}
|