synabun 2.1.0 → 2.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (332) hide show
  1. package/CHANGELOG.md +139 -0
  2. package/README.md +1 -1
  3. package/docs/whatsapp.md +11 -11
  4. package/lib/client-config-repair.js +13 -9
  5. package/lib/external-tools.js +6 -5
  6. package/lib/update-snapshot.js +77 -3
  7. package/lib/virustotal.mjs +12 -1
  8. package/mcp-server/dist/index.js +1 -1
  9. package/mcp-server/dist/index.js.map +1 -1
  10. package/mcp-server/dist/services/budget-judgments.d.ts +1 -1
  11. package/mcp-server/dist/services/budget-judgments.d.ts.map +1 -1
  12. package/mcp-server/dist/services/budget-judgments.js +5 -3
  13. package/mcp-server/dist/services/budget-judgments.js.map +1 -1
  14. package/mcp-server/dist/services/identity.d.ts +2 -2
  15. package/mcp-server/dist/services/identity.js +2 -2
  16. package/mcp-server/dist/services/memory-sessions.d.ts +1 -1
  17. package/mcp-server/dist/services/memory-sessions.d.ts.map +1 -1
  18. package/mcp-server/dist/services/memory-sessions.js +28 -2
  19. package/mcp-server/dist/services/memory-sessions.js.map +1 -1
  20. package/mcp-server/dist/services/neural-interface.d.ts +2 -1
  21. package/mcp-server/dist/services/neural-interface.d.ts.map +1 -1
  22. package/mcp-server/dist/services/neural-interface.js.map +1 -1
  23. package/mcp-server/dist/services/server-instructions.d.ts.map +1 -1
  24. package/mcp-server/dist/services/server-instructions.js +2 -1
  25. package/mcp-server/dist/services/server-instructions.js.map +1 -1
  26. package/mcp-server/dist/services/tool-catalog.js +1 -1
  27. package/mcp-server/dist/services/tool-catalog.js.map +1 -1
  28. package/mcp-server/dist/services/tool-list.d.ts +1 -1
  29. package/mcp-server/dist/services/tool-list.d.ts.map +1 -1
  30. package/mcp-server/dist/services/tool-runtime.d.ts +2 -2
  31. package/mcp-server/dist/services/tool-runtime.d.ts.map +1 -1
  32. package/mcp-server/dist/services/tool-runtime.js +1 -1
  33. package/mcp-server/dist/services/tool-runtime.js.map +1 -1
  34. package/mcp-server/dist/services/typesafe-config.js +1 -1
  35. package/mcp-server/dist/services/typesafe-config.js.map +1 -1
  36. package/mcp-server/dist/tools/agents.d.ts +8 -8
  37. package/mcp-server/dist/tools/agents.d.ts.map +1 -1
  38. package/mcp-server/dist/tools/agents.js +10 -9
  39. package/mcp-server/dist/tools/agents.js.map +1 -1
  40. package/mcp-server/dist/tools/choice.d.ts +6 -0
  41. package/mcp-server/dist/tools/choice.d.ts.map +1 -1
  42. package/mcp-server/dist/tools/choice.js +18 -13
  43. package/mcp-server/dist/tools/choice.js.map +1 -1
  44. package/mcp-server/dist/tools/git-tools.js +2 -2
  45. package/mcp-server/dist/tools/git-tools.js.map +1 -1
  46. package/mcp-server/dist/tools/morelogin.d.ts.map +1 -1
  47. package/mcp-server/dist/tools/morelogin.js +9 -6
  48. package/mcp-server/dist/tools/morelogin.js.map +1 -1
  49. package/mcp-server/dist/tools/skill-tools.d.ts +8 -8
  50. package/mcp-server/dist/tools/skill-tools.d.ts.map +1 -1
  51. package/mcp-server/dist/tools/skill-tools.js +11 -8
  52. package/mcp-server/dist/tools/skill-tools.js.map +1 -1
  53. package/mcp-server/package-lock.json +2 -2
  54. package/mcp-server/package.json +1 -1
  55. package/mcp-server/src/index.ts +1 -1
  56. package/mcp-server/src/services/budget-judgments.ts +5 -3
  57. package/mcp-server/src/services/identity.ts +2 -2
  58. package/mcp-server/src/services/memory-sessions.ts +24 -3
  59. package/mcp-server/src/services/neural-interface.ts +2 -1
  60. package/mcp-server/src/services/server-instructions.ts +2 -1
  61. package/mcp-server/src/services/tool-catalog.ts +1 -1
  62. package/mcp-server/src/services/tool-list.ts +1 -1
  63. package/mcp-server/src/services/tool-runtime.ts +2 -2
  64. package/mcp-server/src/services/typesafe-config.ts +1 -1
  65. package/mcp-server/src/tools/agents.ts +10 -9
  66. package/mcp-server/src/tools/choice.ts +17 -12
  67. package/mcp-server/src/tools/git-tools.ts +2 -2
  68. package/mcp-server/src/tools/morelogin.ts +6 -4
  69. package/mcp-server/src/tools/skill-tools.ts +11 -8
  70. package/neural-interface/i18n/en.json +504 -39
  71. package/neural-interface/i18n/pt-BR.json +504 -39
  72. package/neural-interface/lib/agent-sandbox-elevation.js +6 -0
  73. package/neural-interface/lib/antigravity/acp-client-host.js +554 -0
  74. package/neural-interface/lib/antigravity/acp-connection.js +299 -0
  75. package/neural-interface/lib/antigravity/bypass.js +100 -0
  76. package/neural-interface/lib/antigravity/catalog.js +273 -0
  77. package/neural-interface/lib/antigravity/home.js +260 -0
  78. package/neural-interface/lib/antigravity/migrate-gemini.js +139 -0
  79. package/neural-interface/lib/antigravity/panel-bridge.js +1058 -0
  80. package/neural-interface/lib/antigravity/policy-hook.mjs +78 -0
  81. package/neural-interface/lib/antigravity/policy.js +74 -0
  82. package/neural-interface/lib/antigravity/tool-call.js +178 -0
  83. package/neural-interface/lib/antigravity/transcripts.js +252 -0
  84. package/neural-interface/lib/antigravity/usage.js +43 -0
  85. package/neural-interface/lib/assistant-api.js +1 -0
  86. package/neural-interface/lib/assistant-brains/antigravity.js +423 -0
  87. package/neural-interface/lib/assistant-brains/codex.js +6 -1
  88. package/neural-interface/lib/assistant-budget-planner.js +7 -3
  89. package/neural-interface/lib/assistant-budget-split.js +42 -15
  90. package/neural-interface/lib/assistant-catalog.js +76 -9
  91. package/neural-interface/lib/assistant-config.js +6 -3
  92. package/neural-interface/lib/assistant-dispatch.js +211 -50
  93. package/neural-interface/lib/assistant-envelope.js +153 -1
  94. package/neural-interface/lib/assistant-evidence.js +80 -0
  95. package/neural-interface/lib/assistant-hidden-models.js +7 -5
  96. package/neural-interface/lib/assistant-persona.js +16 -11
  97. package/neural-interface/lib/assistant-plan-permissions.js +15 -0
  98. package/neural-interface/lib/assistant-router.js +237 -35
  99. package/neural-interface/lib/assistant-runtime.js +158 -12
  100. package/neural-interface/lib/assistant-task-prompt.js +10 -8
  101. package/neural-interface/lib/assistant-tools.js +2 -2
  102. package/neural-interface/lib/assistant-usage.js +29 -0
  103. package/neural-interface/lib/backup-service.js +6 -1
  104. package/neural-interface/lib/browser-guard-cdp.js +5 -4
  105. package/neural-interface/lib/browser-network-guard.js +274 -82
  106. package/neural-interface/lib/browser-tool-policy.js +12 -7
  107. package/neural-interface/lib/claude-agent-bridge.js +7 -0
  108. package/neural-interface/lib/claude-history-cache.js +210 -0
  109. package/neural-interface/lib/claude-model-catalog.js +5 -1
  110. package/neural-interface/lib/cli-install-api.js +1 -1
  111. package/neural-interface/lib/cli-install.js +160 -31
  112. package/neural-interface/lib/cli-resolver.js +9 -2
  113. package/neural-interface/lib/cli-update-planner.js +25 -15
  114. package/neural-interface/lib/codex-rollout-reader.js +52 -0
  115. package/neural-interface/lib/codex-worker-app-server.js +25 -6
  116. package/neural-interface/lib/editor-git-api.js +3 -20
  117. package/neural-interface/lib/exec-loop-command.js +9 -2
  118. package/neural-interface/lib/file-explorer-api.js +74 -40
  119. package/neural-interface/lib/file-explorer-watch.js +9 -3
  120. package/neural-interface/lib/git-bash/install.js +52 -6
  121. package/neural-interface/lib/git-bash/manager.js +59 -17
  122. package/neural-interface/lib/git-bash/pins.js +12 -0
  123. package/neural-interface/lib/git-read.js +2 -2
  124. package/neural-interface/lib/git-status-runner.js +390 -0
  125. package/neural-interface/lib/git-status-watchdog.js +40 -0
  126. package/neural-interface/lib/greeting/budgets.js +1 -0
  127. package/neural-interface/lib/greeting/engine.js +2 -2
  128. package/neural-interface/lib/greeting/sources.js +10 -12
  129. package/neural-interface/lib/greeting/writer.js +1 -1
  130. package/neural-interface/lib/mcp-client-config.js +37 -6
  131. package/neural-interface/lib/mcp-external-servers.js +40 -7
  132. package/neural-interface/lib/mcp-installer.js +2 -1
  133. package/neural-interface/lib/memory-session-capture.js +17 -3
  134. package/neural-interface/lib/morelogin.js +147 -25
  135. package/neural-interface/lib/native-loop-providers.js +423 -23
  136. package/neural-interface/lib/native-loop-runtime.js +23 -2
  137. package/neural-interface/lib/opencode-native-boundary.js +165 -22
  138. package/neural-interface/lib/owner-authority.js +1 -1
  139. package/neural-interface/lib/path-confine.js +12 -9
  140. package/neural-interface/lib/peer-identity.js +80 -6
  141. package/neural-interface/lib/pty-host/core.js +28 -1
  142. package/neural-interface/lib/pty-host/lifetime.js +12 -0
  143. package/neural-interface/lib/pty-host/manager.js +12 -1
  144. package/neural-interface/lib/remote-policy.js +1 -1
  145. package/neural-interface/lib/request-gate.js +3 -0
  146. package/neural-interface/lib/rulesets/api.js +6 -5
  147. package/neural-interface/lib/rulesets/fs-safe.js +1 -1
  148. package/neural-interface/lib/rulesets/installer.js +89 -10
  149. package/neural-interface/lib/rulesets/render.js +1 -1
  150. package/neural-interface/lib/session-cache-builder.js +64 -1
  151. package/neural-interface/lib/skills/catalog.js +5 -1
  152. package/neural-interface/lib/skills/creation-brief.js +1 -1
  153. package/neural-interface/lib/skills/creation-sources.js +19 -1
  154. package/neural-interface/lib/skills/deploy.js +9 -4
  155. package/neural-interface/lib/skills/draft-confinement.js +3 -1
  156. package/neural-interface/lib/skills/eval-confinement.js +3 -1
  157. package/neural-interface/lib/skills/eval-trace.js +16 -0
  158. package/neural-interface/lib/skills/export.js +5 -2
  159. package/neural-interface/lib/skills/known-bridge-digests.json +58 -4
  160. package/neural-interface/lib/skills/ownership.js +1 -1
  161. package/neural-interface/lib/skills/roots.js +10 -3
  162. package/neural-interface/lib/skills/runtime-checks.js +6 -3
  163. package/neural-interface/lib/skills/service.js +15 -7
  164. package/neural-interface/lib/skills/telemetry.js +6 -0
  165. package/neural-interface/lib/synabun-version.js +1 -1
  166. package/neural-interface/lib/system-activity.js +1 -1
  167. package/neural-interface/lib/whatsapp/brain.js +7 -3
  168. package/neural-interface/lib/whatsapp/cards.js +22 -9
  169. package/neural-interface/lib/whatsapp/config.js +1 -1
  170. package/neural-interface/lib/worker-confinement.js +77 -1
  171. package/neural-interface/lib/worker-question.js +74 -0
  172. package/neural-interface/lib/worker-resume.js +13 -4
  173. package/neural-interface/lib/worker-sandbox.js +46 -16
  174. package/neural-interface/package-lock.json +18 -2
  175. package/neural-interface/package.json +2 -1
  176. package/neural-interface/public/onboarding.html +1 -1
  177. package/neural-interface/public/shared/agent-runtime-options.js +89 -11
  178. package/neural-interface/public/shared/agy/agy-bypass.js +47 -0
  179. package/neural-interface/public/shared/agy/agy-client.js +266 -0
  180. package/neural-interface/public/shared/agy/agy-commands.js +120 -0
  181. package/neural-interface/public/shared/agy/agy-copy.js +326 -0
  182. package/neural-interface/public/shared/agy/agy-diff.js +161 -0
  183. package/neural-interface/public/shared/agy/agy-events.js +469 -0
  184. package/neural-interface/public/shared/agy/agy-greeting.js +26 -0
  185. package/neural-interface/public/shared/agy/agy-options.js +232 -0
  186. package/neural-interface/public/shared/agy/agy-panel.js +2543 -0
  187. package/neural-interface/public/shared/agy/agy-render.js +392 -0
  188. package/neural-interface/public/shared/agy/agy-setup.js +149 -0
  189. package/neural-interface/public/shared/agy/agy-styles.js +604 -0
  190. package/neural-interface/public/shared/agy/agy-tabs-logic.js +181 -0
  191. package/neural-interface/public/shared/api.js +4 -4
  192. package/neural-interface/public/shared/assistant/asst-agents-dock.js +3 -4
  193. package/neural-interface/public/shared/assistant/asst-api.js +15 -3
  194. package/neural-interface/public/shared/assistant/asst-brain-picker.js +56 -9
  195. package/neural-interface/public/shared/assistant/asst-budget.js +5 -4
  196. package/neural-interface/public/shared/assistant/asst-computer.js +26 -2
  197. package/neural-interface/public/shared/assistant/asst-control.js +48 -0
  198. package/neural-interface/public/shared/assistant/asst-hosts.js +16 -7
  199. package/neural-interface/public/shared/assistant/asst-limit-dock.js +6 -1
  200. package/neural-interface/public/shared/assistant/asst-limit-wait.js +38 -5
  201. package/neural-interface/public/shared/assistant/asst-motion-gate.js +80 -0
  202. package/neural-interface/public/shared/assistant/asst-panel.js +46 -9
  203. package/neural-interface/public/shared/assistant/asst-render.js +112 -18
  204. package/neural-interface/public/shared/assistant/asst-route.js +237 -57
  205. package/neural-interface/public/shared/assistant/asst-routes-editor.js +9 -8
  206. package/neural-interface/public/shared/assistant/asst-scene-styles.js +55 -23
  207. package/neural-interface/public/shared/assistant/asst-scenes.js +279 -80
  208. package/neural-interface/public/shared/assistant/asst-sidepanel-styles.js +4 -4
  209. package/neural-interface/public/shared/assistant/asst-sidepanel.js +51 -7
  210. package/neural-interface/public/shared/assistant/asst-slash.js +8 -2
  211. package/neural-interface/public/shared/assistant/asst-state.js +76 -9
  212. package/neural-interface/public/shared/assistant/asst-style-cache.js +44 -0
  213. package/neural-interface/public/shared/assistant/asst-styles.js +58 -4
  214. package/neural-interface/public/shared/assistant/asst-tool-kinds.js +34 -7
  215. package/neural-interface/public/shared/assistant/asst-usage.js +2 -1
  216. package/neural-interface/public/shared/cdx/cdx-output.js +26 -3
  217. package/neural-interface/public/shared/cdx/cdx-panel.js +14 -2
  218. package/neural-interface/public/shared/cdx/cdx-preview.js +12 -0
  219. package/neural-interface/public/shared/cdx/cdx-protocol.js +11 -10
  220. package/neural-interface/public/shared/cdx/cdx-render.js +10 -9
  221. package/neural-interface/public/shared/cdx/cdx-requests.js +11 -1
  222. package/neural-interface/public/shared/cdx/cdx-styles.js +4 -0
  223. package/neural-interface/public/shared/cdx/cdx-tabs.js +75 -5
  224. package/neural-interface/public/shared/cli-install-controller.js +218 -0
  225. package/neural-interface/public/shared/cli-install-view.js +7 -4
  226. package/neural-interface/public/shared/cli-status.js +9 -8
  227. package/neural-interface/public/shared/constants.js +3 -2
  228. package/neural-interface/public/shared/context-menu-model.js +1 -0
  229. package/neural-interface/public/shared/cp/cp-permissions.js +12 -0
  230. package/neural-interface/public/shared/cp/cp-restore.js +162 -0
  231. package/neural-interface/public/shared/cp/cp-tool-cards.js +10 -3
  232. package/neural-interface/public/shared/desktop-bridge.js +17 -0
  233. package/neural-interface/public/shared/desktop-titlebar-view.js +12 -0
  234. package/neural-interface/public/shared/desktop-titlebar.js +13 -4
  235. package/neural-interface/public/shared/desktop-update-view.js +1 -0
  236. package/neural-interface/public/shared/explorer/fx-api.js +4 -4
  237. package/neural-interface/public/shared/explorer/fx-links.js +3 -3
  238. package/neural-interface/public/shared/explorer/fx-logic.js +1 -1
  239. package/neural-interface/public/shared/explorer/fx-main.js +48 -16
  240. package/neural-interface/public/shared/explorer/fx-tree.js +1 -0
  241. package/neural-interface/public/shared/greeting/greeting-card.js +2 -2
  242. package/neural-interface/public/shared/greeting/greeting-view.js +1 -0
  243. package/neural-interface/public/shared/html-shell.js +21 -4
  244. package/neural-interface/public/shared/loading-teardown.js +34 -0
  245. package/neural-interface/public/shared/ocp-v2/ocp-v2-childpanel.js +6 -1
  246. package/neural-interface/public/shared/ocp-v2/ocp-v2-panel.js +28 -7
  247. package/neural-interface/public/shared/ocp-v2/ocp-v2-rehydrate.js +13 -4
  248. package/neural-interface/public/shared/ocp-v2/ocp-v2-render.js +17 -1
  249. package/neural-interface/public/shared/ocp-v2/ocp-v2-send.js +6 -0
  250. package/neural-interface/public/shared/ocp-v2/ocp-v2-styles.js +101 -68
  251. package/neural-interface/public/shared/ocp-v2/ocp-v2-ws.js +46 -3
  252. package/neural-interface/public/shared/provider-icons.js +4 -2
  253. package/neural-interface/public/shared/settings/settings-rules.js +2 -2
  254. package/neural-interface/public/shared/settings/settings-skill-folders-pane.js +1 -1
  255. package/neural-interface/public/shared/settings/settings-skill-folders.js +2 -2
  256. package/neural-interface/public/shared/showcase-privacy.js +6 -1
  257. package/neural-interface/public/shared/sidepanel-session-status.js +3 -3
  258. package/neural-interface/public/shared/skills/ss-creation.js +6 -3
  259. package/neural-interface/public/shared/skills/ss-dialog.js +5 -2
  260. package/neural-interface/public/shared/skills/ss-lint.js +9 -1
  261. package/neural-interface/public/shared/skills/ss-profiles.js +44 -6
  262. package/neural-interface/public/shared/skills/ss-review-view.js +2 -2
  263. package/neural-interface/public/shared/skills/studio/sx-actions.js +1 -1
  264. package/neural-interface/public/shared/skills/studio/sx-create-ai-view.js +1 -1
  265. package/neural-interface/public/shared/skills/studio/sx-create-ai.js +2 -1
  266. package/neural-interface/public/shared/skills/studio/sx-dom.js +2 -1
  267. package/neural-interface/public/shared/skills/studio/sx-fixes-view.js +359 -0
  268. package/neural-interface/public/shared/skills/studio/sx-fixes.js +327 -0
  269. package/neural-interface/public/shared/skills/studio/sx-inspector.js +1 -1
  270. package/neural-interface/public/shared/skills/studio/sx-install-flow.js +9 -6
  271. package/neural-interface/public/shared/skills/studio/sx-install.js +20 -3
  272. package/neural-interface/public/shared/skills/studio/sx-library-view.js +144 -35
  273. package/neural-interface/public/shared/skills/studio/sx-library.js +6 -2
  274. package/neural-interface/public/shared/skills/studio/sx-main.js +11 -2
  275. package/neural-interface/public/shared/skills/studio/sx-review-view.js +1 -1
  276. package/neural-interface/public/shared/skills/studio/sx-subject.js +7 -6
  277. package/neural-interface/public/shared/skills/studio/sx-workspace-view.js +6 -5
  278. package/neural-interface/public/shared/state.js +1 -1
  279. package/neural-interface/public/shared/storage-retry.js +47 -0
  280. package/neural-interface/public/shared/storage.js +25 -4
  281. package/neural-interface/public/shared/streaming-markdown.js +58 -0
  282. package/neural-interface/public/shared/styles.css +362 -35
  283. package/neural-interface/public/shared/synabun-mascot.js +27 -8
  284. package/neural-interface/public/shared/synabun-ticker.js +147 -12
  285. package/neural-interface/public/shared/text-cache.js +29 -0
  286. package/neural-interface/public/shared/tool-list-picker.js +2 -2
  287. package/neural-interface/public/shared/tool-list-view.js +2 -2
  288. package/neural-interface/public/shared/topright-overflow-logic.js +54 -0
  289. package/neural-interface/public/shared/ui-antigravity-panel.js +19 -0
  290. package/neural-interface/public/shared/ui-assistant.js +2 -0
  291. package/neural-interface/public/shared/ui-automation-studio.js +20 -12
  292. package/neural-interface/public/shared/ui-claude-panel.js +374 -134
  293. package/neural-interface/public/shared/ui-cli-banner.js +23 -167
  294. package/neural-interface/public/shared/ui-file-explorer.js +48 -6
  295. package/neural-interface/public/shared/ui-file-links.js +2 -2
  296. package/neural-interface/public/shared/ui-games.js +2 -2
  297. package/neural-interface/public/shared/ui-judgments.js +1 -1
  298. package/neural-interface/public/shared/ui-keybinds.js +8 -2
  299. package/neural-interface/public/shared/ui-link.js +3 -3
  300. package/neural-interface/public/shared/ui-loading.js +5 -4
  301. package/neural-interface/public/shared/ui-menubar.js +2 -2
  302. package/neural-interface/public/shared/ui-native-loop-router.js +4 -0
  303. package/neural-interface/public/shared/ui-navbar.js +45 -0
  304. package/neural-interface/public/shared/ui-notifications.js +1 -0
  305. package/neural-interface/public/shared/ui-resume.js +9 -7
  306. package/neural-interface/public/shared/ui-schedules-studio.js +41 -10
  307. package/neural-interface/public/shared/ui-scroll-follow.js +16 -5
  308. package/neural-interface/public/shared/ui-sessions.js +22 -12
  309. package/neural-interface/public/shared/ui-settings.js +90 -65
  310. package/neural-interface/public/shared/ui-sidepanel-session-entry.js +9 -2
  311. package/neural-interface/public/shared/ui-sidepanel-sessions.js +3 -3
  312. package/neural-interface/public/shared/ui-sidepanel-tray.js +48 -7
  313. package/neural-interface/public/shared/ui-sidepanel-windows.js +3 -3
  314. package/neural-interface/public/shared/ui-terminal.js +146 -10
  315. package/neural-interface/public/shared/ui-tool-updates.js +3 -3
  316. package/neural-interface/public/shared/ui-tooltip.js +71 -46
  317. package/neural-interface/public/shared/ui-topright-overflow.js +233 -0
  318. package/neural-interface/public/shared/ui-whiteboard.js +1 -1
  319. package/neural-interface/public/shared/ui-worker-end.js +1 -1
  320. package/neural-interface/public/shared/whatsapp/wa-view.js +2 -2
  321. package/neural-interface/public/shared/worker-questions.js +34 -0
  322. package/neural-interface/public/sidepanel-session.html +1 -1
  323. package/neural-interface/server.js +731 -466
  324. package/neural-interface/templates/rulesets/README.md +1 -1
  325. package/neural-interface/templates/rulesets/legacy-hashes.json +5 -5
  326. package/neural-interface/templates/rulesets/manifest.json +1 -1
  327. package/npm-shrinkwrap.json +18 -2
  328. package/package.json +2 -1
  329. package/skills/skill-forge/SKILL.md +4 -4
  330. package/skills/synabun/SKILL.md +1 -1
  331. package/skills/synabun/modules/schedule.md +3 -3
  332. /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
- case 'current': return tr('assistant.routes.badge.current', 'Current');
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 (!n?.requires && n?.visionOnly) return tr('assistant.routes.badge.canSee', 'Can see');
231
- return n?.requires === 'video' ? tr('assistant.routes.badge.makesVideo', 'Makes video') : tr('assistant.routes.badge.makesImages', 'Makes images');
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
- const rank = (o) => (o.id === defaultId ? -2 : (o.id === 'here' || o.isHere) ? -1 : (BADGE_RANK[o.badge] ?? 9));
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.id === 'here' || !!o.isHere;
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
- title: isHere ? tr('assistant.routes.doHere', 'Do it here with {model}', { model: str(o.label) || hereModel }) : (label || o.id),
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
- /** Budget messages are UI copy; never show the server's English disabledReason. */
373
- export function routeBudgetReason({ provider, dry } = {}, t) {
374
- const tr = tf(t), names = { 'claude-code': 'Claude Code', codex: 'Codex', opencode: 'OpenCode' };
375
- if (provider) return tr('assistant.routes.budgetProvider', "Budget: the {provider} share of this session's budget is used up.", { provider: names[provider] || provider });
376
- const list = [...new Set(arr(dry))].map(id => names[id] || id);
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' ? routeKindLabel(kind) : '',
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 getProviderMeta(provider).icon || '';
578
+ return providerMeta(provider).icon || '';
489
579
  } catch { return ''; }
490
580
  }
491
581
 
492
582
  function providerColor(provider) {
493
- try { return getProviderMeta(provider).color || ''; } catch { 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
- selectedId: (options.find(o => o.selected) || options.find(o => !o.disabled) || {}).id || null,
716
- other: null,
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' ? routeKindLabel(o.kind) : '';
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
- const allOptions = () => (state.other ? [...options, state.other] : options);
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 === state.selectedId;
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
- runBtn.disabled = state.pending || state.resolved || state.expired || !state.selectedId;
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 (state.pending || state.resolved || state.expired) return;
965
+ if (busy()) return;
815
966
  const o = allOptions().find(x => x.id === id);
816
967
  if (!o || o.disabled) return;
817
- state.selectedId = id;
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 === state.selectedId);
985
+ const selected = () => allOptions().find(o => o.id === selectedId());
835
986
  function editTarget(patch) {
836
- if (state.pending || state.resolved || state.expired) return;
837
- state.other = { ...selected(), accountId: selected()?.accountChoice || selected()?.accountId || null, accountChoice: Object.hasOwn(patch, 'accountId') ? patch.accountId : selected()?.accountChoice || null, disabled: false, disabledReason: null, accountError: null, ...patch, id: 'other', number: options.length + 1, badge: null, badgeText: tr('assistant.routes.badge.other', 'Your choice') };
838
- state.selectedId = 'other';
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 = state.pending || state.resolved || state.expired;
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 = state.pending || state.resolved || state.expired || !levels.some(e => e.id !== 'off');
852
- accountBtn.hidden = !['claude-code', 'codex'].includes(target?.provider);
853
- accountBtn.disabled = state.pending || state.resolved || state.expired;
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 && !state.pending && !state.resolved && !state.expired,
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
- providers: n.other.providers.length ? n.other.providers.map(p => normalizeProvider(p, p)) : null,
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 = n.dispatchOnly || previous?.kind === 'dispatch' || (target.provider && target.provider !== brain.provider) ? 'dispatch' : 'direct';
903
- state.other = {
904
- id: 'other', kind, provider: target.provider, model: target.model, effort: null, accountId: previous?.provider === target.provider ? previous.accountChoice || previous.accountId || null : null, account: previous?.provider === target.provider ? previous.account || null : null,
905
- label: target.label || '', badge: null, number: options.length + 1, here: false,
906
- title: target.label || target.model, badgeText: tr('assistant.routes.badge.other', 'Your choice'),
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.selectedId = 'other';
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 hint = el('span', 'asst-route-keys', tr('assistant.routes.keys', '1–4 choose · Enter runs · Esc cancels'));
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 = allOptions().find(x => x.id === state.selectedId) || null;
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: state.selectedId,
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 !== state.selectedId).map(x => ({ provider: x.provider, model: x.model, effort: x.effort, label: x.label })),
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 (state.pending || state.resolved || state.expired || !state.selectedId) return;
1015
- const response = buildRouteResponse(n, { optionId: state.selectedId, target: state.other, remember: state.remember });
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 === state.selectedId));
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
- const row = list.querySelector('.asst-route-option[aria-checked="true"]') || list.querySelector('.asst-route-option');
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 = getProviderMeta(provider);
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 MODEL_SECTIONS) {
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 MODEL_SECTIONS) {
1580
+ for (const provider of shownSections()) {
1401
1581
  const entries = arr(state.archived[provider]);
1402
1582
  if (!entries.length) continue;
1403
1583
  any = true;