synabun 2.1.0 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (317) hide show
  1. package/CHANGELOG.md +121 -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 +501 -39
  71. package/neural-interface/i18n/pt-BR.json +501 -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-budget-planner.js +7 -3
  88. package/neural-interface/lib/assistant-budget-split.js +42 -15
  89. package/neural-interface/lib/assistant-catalog.js +76 -9
  90. package/neural-interface/lib/assistant-config.js +6 -3
  91. package/neural-interface/lib/assistant-dispatch.js +207 -49
  92. package/neural-interface/lib/assistant-envelope.js +153 -1
  93. package/neural-interface/lib/assistant-evidence.js +80 -0
  94. package/neural-interface/lib/assistant-hidden-models.js +7 -5
  95. package/neural-interface/lib/assistant-persona.js +16 -11
  96. package/neural-interface/lib/assistant-plan-permissions.js +15 -0
  97. package/neural-interface/lib/assistant-router.js +237 -35
  98. package/neural-interface/lib/assistant-runtime.js +158 -12
  99. package/neural-interface/lib/assistant-task-prompt.js +10 -8
  100. package/neural-interface/lib/assistant-tools.js +2 -2
  101. package/neural-interface/lib/assistant-usage.js +29 -0
  102. package/neural-interface/lib/backup-service.js +6 -1
  103. package/neural-interface/lib/browser-guard-cdp.js +5 -4
  104. package/neural-interface/lib/browser-network-guard.js +274 -82
  105. package/neural-interface/lib/browser-tool-policy.js +12 -7
  106. package/neural-interface/lib/claude-agent-bridge.js +7 -0
  107. package/neural-interface/lib/claude-history-cache.js +210 -0
  108. package/neural-interface/lib/claude-model-catalog.js +5 -1
  109. package/neural-interface/lib/cli-install-api.js +1 -1
  110. package/neural-interface/lib/cli-install.js +160 -31
  111. package/neural-interface/lib/cli-resolver.js +9 -2
  112. package/neural-interface/lib/cli-update-planner.js +25 -15
  113. package/neural-interface/lib/codex-rollout-reader.js +52 -0
  114. package/neural-interface/lib/codex-worker-app-server.js +25 -6
  115. package/neural-interface/lib/editor-git-api.js +3 -20
  116. package/neural-interface/lib/exec-loop-command.js +9 -2
  117. package/neural-interface/lib/file-explorer-api.js +74 -40
  118. package/neural-interface/lib/file-explorer-watch.js +9 -3
  119. package/neural-interface/lib/git-bash/install.js +52 -6
  120. package/neural-interface/lib/git-bash/manager.js +59 -17
  121. package/neural-interface/lib/git-bash/pins.js +12 -0
  122. package/neural-interface/lib/git-read.js +2 -2
  123. package/neural-interface/lib/git-status-runner.js +390 -0
  124. package/neural-interface/lib/git-status-watchdog.js +40 -0
  125. package/neural-interface/lib/greeting/budgets.js +1 -0
  126. package/neural-interface/lib/greeting/engine.js +2 -2
  127. package/neural-interface/lib/greeting/sources.js +10 -12
  128. package/neural-interface/lib/greeting/writer.js +1 -1
  129. package/neural-interface/lib/mcp-client-config.js +37 -6
  130. package/neural-interface/lib/mcp-external-servers.js +13 -4
  131. package/neural-interface/lib/mcp-installer.js +2 -1
  132. package/neural-interface/lib/memory-session-capture.js +17 -3
  133. package/neural-interface/lib/morelogin.js +147 -25
  134. package/neural-interface/lib/native-loop-providers.js +420 -21
  135. package/neural-interface/lib/native-loop-runtime.js +23 -2
  136. package/neural-interface/lib/opencode-native-boundary.js +11 -2
  137. package/neural-interface/lib/owner-authority.js +1 -1
  138. package/neural-interface/lib/path-confine.js +12 -9
  139. package/neural-interface/lib/peer-identity.js +32 -2
  140. package/neural-interface/lib/pty-host/core.js +28 -1
  141. package/neural-interface/lib/pty-host/lifetime.js +12 -0
  142. package/neural-interface/lib/pty-host/manager.js +12 -1
  143. package/neural-interface/lib/remote-policy.js +1 -1
  144. package/neural-interface/lib/request-gate.js +3 -0
  145. package/neural-interface/lib/rulesets/api.js +6 -5
  146. package/neural-interface/lib/rulesets/fs-safe.js +1 -1
  147. package/neural-interface/lib/rulesets/installer.js +89 -10
  148. package/neural-interface/lib/rulesets/render.js +1 -1
  149. package/neural-interface/lib/session-cache-builder.js +64 -1
  150. package/neural-interface/lib/skills/catalog.js +5 -1
  151. package/neural-interface/lib/skills/creation-brief.js +1 -1
  152. package/neural-interface/lib/skills/creation-sources.js +19 -1
  153. package/neural-interface/lib/skills/deploy.js +9 -4
  154. package/neural-interface/lib/skills/draft-confinement.js +3 -1
  155. package/neural-interface/lib/skills/eval-confinement.js +3 -1
  156. package/neural-interface/lib/skills/eval-trace.js +16 -0
  157. package/neural-interface/lib/skills/export.js +5 -2
  158. package/neural-interface/lib/skills/known-bridge-digests.json +58 -4
  159. package/neural-interface/lib/skills/ownership.js +1 -1
  160. package/neural-interface/lib/skills/roots.js +10 -3
  161. package/neural-interface/lib/skills/runtime-checks.js +6 -3
  162. package/neural-interface/lib/skills/service.js +15 -7
  163. package/neural-interface/lib/skills/telemetry.js +6 -0
  164. package/neural-interface/lib/synabun-version.js +1 -1
  165. package/neural-interface/lib/system-activity.js +1 -1
  166. package/neural-interface/lib/whatsapp/brain.js +7 -3
  167. package/neural-interface/lib/whatsapp/cards.js +22 -9
  168. package/neural-interface/lib/whatsapp/config.js +1 -1
  169. package/neural-interface/lib/worker-confinement.js +77 -1
  170. package/neural-interface/lib/worker-question.js +74 -0
  171. package/neural-interface/lib/worker-resume.js +13 -4
  172. package/neural-interface/lib/worker-sandbox.js +46 -16
  173. package/neural-interface/package-lock.json +18 -2
  174. package/neural-interface/package.json +2 -1
  175. package/neural-interface/public/onboarding.html +1 -1
  176. package/neural-interface/public/shared/agent-runtime-options.js +89 -11
  177. package/neural-interface/public/shared/agy/agy-bypass.js +47 -0
  178. package/neural-interface/public/shared/agy/agy-client.js +266 -0
  179. package/neural-interface/public/shared/agy/agy-commands.js +120 -0
  180. package/neural-interface/public/shared/agy/agy-copy.js +326 -0
  181. package/neural-interface/public/shared/agy/agy-diff.js +161 -0
  182. package/neural-interface/public/shared/agy/agy-events.js +469 -0
  183. package/neural-interface/public/shared/agy/agy-greeting.js +26 -0
  184. package/neural-interface/public/shared/agy/agy-options.js +232 -0
  185. package/neural-interface/public/shared/agy/agy-panel.js +2543 -0
  186. package/neural-interface/public/shared/agy/agy-render.js +392 -0
  187. package/neural-interface/public/shared/agy/agy-setup.js +149 -0
  188. package/neural-interface/public/shared/agy/agy-styles.js +604 -0
  189. package/neural-interface/public/shared/agy/agy-tabs-logic.js +181 -0
  190. package/neural-interface/public/shared/api.js +4 -4
  191. package/neural-interface/public/shared/assistant/asst-agents-dock.js +3 -4
  192. package/neural-interface/public/shared/assistant/asst-api.js +1 -1
  193. package/neural-interface/public/shared/assistant/asst-brain-picker.js +56 -9
  194. package/neural-interface/public/shared/assistant/asst-budget.js +5 -4
  195. package/neural-interface/public/shared/assistant/asst-control.js +48 -0
  196. package/neural-interface/public/shared/assistant/asst-hosts.js +16 -7
  197. package/neural-interface/public/shared/assistant/asst-limit-dock.js +6 -1
  198. package/neural-interface/public/shared/assistant/asst-limit-wait.js +38 -5
  199. package/neural-interface/public/shared/assistant/asst-panel.js +45 -8
  200. package/neural-interface/public/shared/assistant/asst-render.js +51 -12
  201. package/neural-interface/public/shared/assistant/asst-route.js +237 -57
  202. package/neural-interface/public/shared/assistant/asst-routes-editor.js +9 -8
  203. package/neural-interface/public/shared/assistant/asst-scenes.js +54 -1
  204. package/neural-interface/public/shared/assistant/asst-sidepanel.js +51 -7
  205. package/neural-interface/public/shared/assistant/asst-slash.js +8 -2
  206. package/neural-interface/public/shared/assistant/asst-state.js +76 -9
  207. package/neural-interface/public/shared/assistant/asst-styles.js +34 -0
  208. package/neural-interface/public/shared/assistant/asst-tool-kinds.js +34 -7
  209. package/neural-interface/public/shared/assistant/asst-usage.js +2 -1
  210. package/neural-interface/public/shared/cdx/cdx-output.js +26 -3
  211. package/neural-interface/public/shared/cdx/cdx-panel.js +14 -2
  212. package/neural-interface/public/shared/cdx/cdx-preview.js +12 -0
  213. package/neural-interface/public/shared/cdx/cdx-protocol.js +11 -10
  214. package/neural-interface/public/shared/cdx/cdx-render.js +10 -9
  215. package/neural-interface/public/shared/cdx/cdx-requests.js +11 -1
  216. package/neural-interface/public/shared/cdx/cdx-styles.js +4 -0
  217. package/neural-interface/public/shared/cdx/cdx-tabs.js +75 -5
  218. package/neural-interface/public/shared/cli-install-controller.js +218 -0
  219. package/neural-interface/public/shared/cli-install-view.js +7 -4
  220. package/neural-interface/public/shared/cli-status.js +9 -8
  221. package/neural-interface/public/shared/constants.js +3 -2
  222. package/neural-interface/public/shared/context-menu-model.js +1 -0
  223. package/neural-interface/public/shared/cp/cp-restore.js +162 -0
  224. package/neural-interface/public/shared/cp/cp-tool-cards.js +10 -3
  225. package/neural-interface/public/shared/desktop-bridge.js +17 -0
  226. package/neural-interface/public/shared/desktop-titlebar-view.js +12 -0
  227. package/neural-interface/public/shared/desktop-titlebar.js +13 -4
  228. package/neural-interface/public/shared/desktop-update-view.js +1 -0
  229. package/neural-interface/public/shared/explorer/fx-api.js +4 -4
  230. package/neural-interface/public/shared/explorer/fx-links.js +3 -3
  231. package/neural-interface/public/shared/explorer/fx-logic.js +1 -1
  232. package/neural-interface/public/shared/explorer/fx-main.js +48 -16
  233. package/neural-interface/public/shared/explorer/fx-tree.js +1 -0
  234. package/neural-interface/public/shared/greeting/greeting-card.js +2 -2
  235. package/neural-interface/public/shared/greeting/greeting-view.js +1 -0
  236. package/neural-interface/public/shared/html-shell.js +21 -4
  237. package/neural-interface/public/shared/ocp-v2/ocp-v2-panel.js +27 -7
  238. package/neural-interface/public/shared/ocp-v2/ocp-v2-rehydrate.js +13 -4
  239. package/neural-interface/public/shared/ocp-v2/ocp-v2-render.js +7 -1
  240. package/neural-interface/public/shared/ocp-v2/ocp-v2-styles.js +17 -0
  241. package/neural-interface/public/shared/ocp-v2/ocp-v2-ws.js +46 -3
  242. package/neural-interface/public/shared/provider-icons.js +4 -2
  243. package/neural-interface/public/shared/settings/settings-rules.js +2 -2
  244. package/neural-interface/public/shared/settings/settings-skill-folders-pane.js +1 -1
  245. package/neural-interface/public/shared/settings/settings-skill-folders.js +2 -2
  246. package/neural-interface/public/shared/showcase-privacy.js +6 -1
  247. package/neural-interface/public/shared/sidepanel-session-status.js +3 -3
  248. package/neural-interface/public/shared/skills/ss-creation.js +6 -3
  249. package/neural-interface/public/shared/skills/ss-dialog.js +5 -2
  250. package/neural-interface/public/shared/skills/ss-lint.js +9 -1
  251. package/neural-interface/public/shared/skills/ss-profiles.js +44 -6
  252. package/neural-interface/public/shared/skills/ss-review-view.js +2 -2
  253. package/neural-interface/public/shared/skills/studio/sx-actions.js +1 -1
  254. package/neural-interface/public/shared/skills/studio/sx-create-ai-view.js +1 -1
  255. package/neural-interface/public/shared/skills/studio/sx-create-ai.js +2 -1
  256. package/neural-interface/public/shared/skills/studio/sx-dom.js +2 -1
  257. package/neural-interface/public/shared/skills/studio/sx-fixes-view.js +359 -0
  258. package/neural-interface/public/shared/skills/studio/sx-fixes.js +327 -0
  259. package/neural-interface/public/shared/skills/studio/sx-inspector.js +1 -1
  260. package/neural-interface/public/shared/skills/studio/sx-install-flow.js +9 -6
  261. package/neural-interface/public/shared/skills/studio/sx-install.js +20 -3
  262. package/neural-interface/public/shared/skills/studio/sx-library-view.js +144 -35
  263. package/neural-interface/public/shared/skills/studio/sx-library.js +6 -2
  264. package/neural-interface/public/shared/skills/studio/sx-main.js +11 -2
  265. package/neural-interface/public/shared/skills/studio/sx-review-view.js +1 -1
  266. package/neural-interface/public/shared/skills/studio/sx-subject.js +7 -6
  267. package/neural-interface/public/shared/skills/studio/sx-workspace-view.js +6 -5
  268. package/neural-interface/public/shared/state.js +1 -1
  269. package/neural-interface/public/shared/streaming-markdown.js +58 -0
  270. package/neural-interface/public/shared/styles.css +354 -34
  271. package/neural-interface/public/shared/text-cache.js +29 -0
  272. package/neural-interface/public/shared/tool-list-picker.js +2 -2
  273. package/neural-interface/public/shared/tool-list-view.js +2 -2
  274. package/neural-interface/public/shared/topright-overflow-logic.js +54 -0
  275. package/neural-interface/public/shared/ui-antigravity-panel.js +19 -0
  276. package/neural-interface/public/shared/ui-assistant.js +2 -0
  277. package/neural-interface/public/shared/ui-automation-studio.js +20 -12
  278. package/neural-interface/public/shared/ui-claude-panel.js +299 -77
  279. package/neural-interface/public/shared/ui-cli-banner.js +23 -167
  280. package/neural-interface/public/shared/ui-file-explorer.js +48 -6
  281. package/neural-interface/public/shared/ui-file-links.js +2 -2
  282. package/neural-interface/public/shared/ui-games.js +2 -2
  283. package/neural-interface/public/shared/ui-judgments.js +1 -1
  284. package/neural-interface/public/shared/ui-keybinds.js +8 -2
  285. package/neural-interface/public/shared/ui-link.js +3 -3
  286. package/neural-interface/public/shared/ui-menubar.js +2 -2
  287. package/neural-interface/public/shared/ui-native-loop-router.js +4 -0
  288. package/neural-interface/public/shared/ui-navbar.js +45 -0
  289. package/neural-interface/public/shared/ui-notifications.js +1 -0
  290. package/neural-interface/public/shared/ui-resume.js +9 -7
  291. package/neural-interface/public/shared/ui-schedules-studio.js +41 -10
  292. package/neural-interface/public/shared/ui-scroll-follow.js +16 -5
  293. package/neural-interface/public/shared/ui-sessions.js +22 -12
  294. package/neural-interface/public/shared/ui-settings.js +90 -65
  295. package/neural-interface/public/shared/ui-sidepanel-session-entry.js +9 -2
  296. package/neural-interface/public/shared/ui-sidepanel-sessions.js +3 -3
  297. package/neural-interface/public/shared/ui-sidepanel-tray.js +48 -7
  298. package/neural-interface/public/shared/ui-sidepanel-windows.js +3 -3
  299. package/neural-interface/public/shared/ui-terminal.js +146 -10
  300. package/neural-interface/public/shared/ui-tool-updates.js +3 -3
  301. package/neural-interface/public/shared/ui-tooltip.js +71 -46
  302. package/neural-interface/public/shared/ui-topright-overflow.js +233 -0
  303. package/neural-interface/public/shared/ui-whiteboard.js +1 -1
  304. package/neural-interface/public/shared/ui-worker-end.js +1 -1
  305. package/neural-interface/public/shared/whatsapp/wa-view.js +2 -2
  306. package/neural-interface/public/shared/worker-questions.js +34 -0
  307. package/neural-interface/public/sidepanel-session.html +1 -1
  308. package/neural-interface/server.js +644 -426
  309. package/neural-interface/templates/rulesets/README.md +1 -1
  310. package/neural-interface/templates/rulesets/legacy-hashes.json +5 -5
  311. package/neural-interface/templates/rulesets/manifest.json +1 -1
  312. package/npm-shrinkwrap.json +18 -2
  313. package/package.json +2 -1
  314. package/skills/skill-forge/SKILL.md +4 -4
  315. package/skills/synabun/SKILL.md +1 -1
  316. package/skills/synabun/modules/schedule.md +3 -3
  317. /package/neural-interface/templates/rulesets/{gemini.md → antigravity.md} +0 -0
@@ -3,13 +3,12 @@
3
3
  import { clampEffortId, getEffortLevelsForModel, modelEffortIds, fetchClaudeAccounts, fetchCodexAccounts, getCachedClaudeAccounts, getCachedCodexAccounts, getClaudeWorkerAccountId, getCodexWorkerAccountId } from '../agent-runtime-options.js';
4
4
  import { accountSummary, accountName, accountBadge, accountIdentity } from '../account-view.js';
5
5
  import { accountUsageText, autoAccountLabel } from '../account-usage.js';
6
- import { getProviderMeta } from '../provider-icons.js';
7
6
  import { getRouting, putRouting } from './asst-api.js';
8
7
  import { catalogModelRow, hiddenModelFor, ICON_CARET, openModelMenu } from './asst-brain-picker.js';
9
8
  import { closeMenu, currentMenu } from './asst-menu.js';
10
9
  import { loadRouteModels, routeModelLabel, routeModelsStatus, watchRouteModels } from './asst-route-models.js';
11
10
  import { designRouteSight, openModelsManager, routeCatalogRow, routeModeLabel } from './asst-route.js';
12
- import { DEFAULT_ROUTE_MODE, normalizeRouteMode, ROUTE_MODES } from './asst-state.js';
11
+ import { DEFAULT_ROUTE_MODE, normalizeRouteMode, PROVIDERS, providerMeta, ROUTE_MODES } from './asst-state.js';
13
12
 
14
13
  const ICON_ROUTE = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M4 7h15l-4-4M20 17H5l4 4"/></svg>';
15
14
  const ICON_X = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"><path d="m6 6 12 12M18 6 6 18"/></svg>';
@@ -37,7 +36,8 @@ function choiceKey(p) { return p ? JSON.stringify([p.kind || 'direct', p.provide
37
36
  export function routeAccountView(pref, task, brain, accounts, workers = {}, t = null) {
38
37
  const tr = translator(t);
39
38
  const empty = { label: tr('assistant.routes.editorUnusedValue', '—'), readOnly: true, options: [], hint: '' };
40
- if (!pref || pref.provider === 'opencode') return empty;
39
+ // Only Claude Code and Codex run under an account (OpenCode and Antigravity sign in inside their own CLIs).
40
+ if (!pref || pref.provider === 'opencode' || pref.provider === 'antigravity') return empty;
41
41
  const direct = task.id === 'chat' || routeExecutionKind(pref, task, brain) === 'direct';
42
42
  const provider = task.id === 'chat' ? brain.provider : pref.provider;
43
43
  if (!['claude-code', 'codex'].includes(provider)) return empty;
@@ -381,7 +381,8 @@ export async function openRoutesEditor(hooks = {}) {
381
381
  state.menuFocus = modelBtn.dataset.focusKey;
382
382
  const emptyLabel = task.requires ? tr('assistant.routes.noCapableModel', 'No model in your catalog can make {medium} yet.', { medium: tr(`assistant.routes.medium.${task.requires === 'video' ? 'videos' : 'images'}`, task.requires) }) : tr('assistant.routes.noSeeingModel', 'No model in your catalog can see images yet.');
383
383
  const menu = await openModelMenu(modelBtn, {
384
- brain: brain(), t: hooks.t, includeHere: true, hereLabel: tr('assistant.routes.brainDecides', 'Assistant chooses'), title: task.label,
384
+ // Every provider a route can name (Antigravity included).
385
+ brain: brain(), t: hooks.t, includeHere: true, hereLabel: tr('assistant.routes.brainDecides', 'Assistant chooses'), title: task.label, providers: PROVIDERS,
385
386
  current: pref ? { provider: pref.provider, model: pref.model } : null, onToast: hooks.onToast,
386
387
  routingEditor: true, onManageModels: () => manageModels(modelBtn.dataset.focusKey),
387
388
  ...(task.requires ? { accept: (_p, m) => m.outputs?.[task.requires] === true, allowCustom: false, emptyLabel } : {}),
@@ -400,7 +401,7 @@ export async function openRoutesEditor(hooks = {}) {
400
401
  // Keys are stable across translations; accessible names use the translated field.
401
402
  modelBtn.dataset.focusKey = `${task.id}-model`; caret(modelBtn); modelBtn.setAttribute('aria-haspopup', 'dialog');
402
403
  modelBtn.title = label;
403
- if (pref?.provider) { const mark = el('span', 'asst-icon'); mark.setAttribute('aria-hidden', 'true'); mark.innerHTML = getProviderMeta(pref.provider).icon; mark.style.color = getProviderMeta(pref.provider).color; modelBtn.prepend(mark); }
404
+ if (pref?.provider) { const mark = el('span', 'asst-icon'); mark.setAttribute('aria-hidden', 'true'); mark.innerHTML = providerMeta(pref.provider).icon; mark.style.color = providerMeta(pref.provider).color; modelBtn.prepend(mark); }
404
405
  modelCell.append(modelBtn);
405
406
  const source = el('span', 'asst-routes-source');
406
407
  let sourceText = pref ? tr('assistant.routes.source.yours', 'Your choice') : tr('assistant.routes.source.default', 'Assistant chooses');
@@ -433,14 +434,14 @@ export async function openRoutesEditor(hooks = {}) {
433
434
  if (effortValue !== unusedValue && effortValue !== effortText) effortBtn.setAttribute('aria-label', tr('assistant.routes.fieldLabel', '{task}: {field}, {value}', { task: task.label, field: tr('assistant.routes.columns.effort', 'Effort'), value: `${effortValue} · ${effortText}` }));
434
435
  const effortCell = el('div', 'asst-routes-cell asst-routes-effort-cell'); effortCell.append(el('span', 'asst-routes-cell-label', tr('assistant.routes.columns.effort', 'Effort')), effortBtn); node.append(effortCell);
435
436
  const kind = routeExecutionKind(pref, task, brain());
436
- const runLabel = task.id === 'chat' ? tr('assistant.routes.inConversation', 'In conversation') : !pref ? task.dispatchOnly ? tr('assistant.routes.workerChooses', 'Worker · assistant chooses') : tr('assistant.routes.brainDecides', 'Assistant chooses') : kind === 'direct' ? tr('assistant.routes.inConversation', 'In conversation') : tr('assistant.routes.onWorker', '{provider} worker', { provider: getProviderMeta(pref.provider).label });
437
+ const runLabel = task.id === 'chat' ? tr('assistant.routes.inConversation', 'In conversation') : !pref ? task.dispatchOnly ? tr('assistant.routes.workerChooses', 'Worker · assistant chooses') : tr('assistant.routes.brainDecides', 'Assistant chooses') : kind === 'direct' ? tr('assistant.routes.inConversation', 'In conversation') : tr('assistant.routes.onWorker', '{provider} worker', { provider: providerMeta(pref.provider).label });
437
438
  const run = fieldButton('asst-routes-run', task, tr('assistant.routes.columns.run', 'Runs in'), runLabel, () => openMenuFor(run, tr('assistant.routes.columns.run', 'Runs in'), [
438
439
  { kind: 'radio', id: 'direct', label: tr('assistant.routes.inConversation', 'In conversation'), desc: tr('assistant.routes.conversationDesc', 'Continue this conversation on the chosen model for this task.'), selected: kind === 'direct', onSelect: () => setChoice(task.id, { ...pref, kind: 'direct' }, run.dataset.focusKey) },
439
- { kind: 'radio', id: 'dispatch', label: tr('assistant.routes.onWorker', '{provider} worker', { provider: getProviderMeta(pref?.provider).label }), desc: tr('assistant.routes.workerDesc', 'Hand the task to a worker and show its result here.'), selected: kind === 'dispatch', onSelect: () => setChoice(task.id, { ...pref, kind: 'dispatch' }, run.dataset.focusKey) },
440
+ { kind: 'radio', id: 'dispatch', label: tr('assistant.routes.onWorker', '{provider} worker', { provider: providerMeta(pref?.provider).label }), desc: tr('assistant.routes.workerDesc', 'Hand the task to a worker and show its result here.'), selected: kind === 'dispatch', onSelect: () => setChoice(task.id, { ...pref, kind: 'dispatch' }, run.dataset.focusKey) },
440
441
  ]));
441
442
  run.dataset.focusKey = `${task.id}-run`; run.disabled = !pref || task.dispatchOnly || pref.provider !== brain().provider || task.id === 'chat';
442
443
  const inConversation = task.id === 'chat' || (pref && kind === 'direct');
443
- const runValue = inConversation ? tr('assistant.routes.editorInConversation', 'This conversation') : pref ? getProviderMeta(pref.provider).label : unusedValue;
444
+ const runValue = inConversation ? tr('assistant.routes.editorInConversation', 'This conversation') : pref ? providerMeta(pref.provider).label : unusedValue;
444
445
  run.querySelector('.asst-dd-label').textContent = runValue;
445
446
  if (inConversation) run.setAttribute('aria-label', tr('assistant.routes.fieldLabel', '{task}: {field}, {value}', { task: task.label, field: tr('assistant.routes.columns.run', 'Runs in'), value: runValue }));
446
447
  if (!run.disabled) caret(run);
@@ -116,6 +116,8 @@ export function runChip(run, t = tFallback) {
116
116
  }
117
117
  if (awaitsAnswer(run)) return { tone: 'action', label: t('assistant.run.waitingInput', 'waiting for your answer') };
118
118
  const tone = runTone(run);
119
+ // A held run whose card the user answered "here": the assistant does it, this worker never started.
120
+ if (run?.completionReason === 'route_here') return { tone, label: t('assistant.run.routedHere', 'not started: done here instead') };
119
121
  if (run?.turnState === 'awaiting_permission') return { tone, label: t('assistant.run.needsApproval', 'needs approval') };
120
122
  if (run?.turnState === 'needs_input') return { tone, label: t('assistant.run.needsInput', 'needs input') };
121
123
  // A Claude Code worker a server restart cut off, until the next start has resumed it (lib/assistant-dispatch.js).
@@ -169,6 +171,46 @@ export const CODEX_ITEM_EVENTS = Object.freeze({
169
171
  */
170
172
  export const OPENCODE_TOOL_STATES = Object.freeze(['running', 'completed', 'error']);
171
173
 
174
+ // The path-like fields of an Antigravity tool call's arguments (lib/worker-confinement.js reads the same ones).
175
+ const ACP_PATH_KEYS = ['path', 'file_path', 'filePath', 'Path', 'AbsolutePath', 'TargetFile', 'DirectoryPath', 'SearchPath'];
176
+
177
+ /**
178
+ * An Antigravity (ACP) tool call as an activity: its `kind` says what it does (read, edit, delete,
179
+ * move, search, execute, think, fetch, switch_mode, other), its `rawInput` holds the tool's own
180
+ * arguments and its `locations` the files it touches. Another tool (an MCP server's) is read by its
181
+ * name, as for the other providers. null for a mode switch.
182
+ */
183
+ function acpToolActivity(call) {
184
+ const args = obj(call.rawInput);
185
+ const kind = str(call.kind) || 'other';
186
+ const title = str(call.title).trim();
187
+ const paths = [...new Set([
188
+ ...(Array.isArray(call.locations) ? call.locations : []).map((location) => str(location?.path)),
189
+ ...ACP_PATH_KEYS.map((key) => (typeof args[key] === 'string' ? args[key] : '')),
190
+ ].filter(Boolean))];
191
+ const pick = (...values) => values.map((v) => (typeof v === 'string' ? v.trim() : '')).find(Boolean) || '';
192
+ switch (kind) {
193
+ case 'execute': {
194
+ const command = Array.isArray(args.command) ? args.command.map(str).join(' ') : pick(args.command, args.CommandLine, args.cmd);
195
+ return { kind: 'shell', name: 'exec_command', input: { command: command || title } };
196
+ }
197
+ case 'read': return { kind: 'read', name: 'read', input: { file_path: paths[0] || title } };
198
+ case 'edit': case 'delete': case 'move':
199
+ return { kind: 'edit', name: kind === 'edit' ? 'edit' : 'apply_patch', input: paths.length > 1 ? { changes: paths.map((path) => ({ path })) } : { file_path: paths[0] || title } };
200
+ case 'search': return { kind: 'search', name: 'grep', input: { pattern: pick(args.pattern, args.query, args.Query, args.Pattern) || title, path: paths[0] || '' } };
201
+ case 'fetch': {
202
+ const url = pick(args.url, args.Url, args.href);
203
+ return url ? { kind: 'web', name: 'WebFetch', input: { url } } : { kind: 'web', name: 'WebSearch', input: { query: pick(args.query, args.Query) || title } };
204
+ }
205
+ case 'think': return { kind: 'think', name: 'thinking', input: {} };
206
+ case 'switch_mode': return null;
207
+ default: {
208
+ const name = pick(call.name, args.toolName, title) || 'tool';
209
+ return { kind: kindOf(name, args), name, input: args };
210
+ }
211
+ }
212
+ }
213
+
172
214
  /**
173
215
  * A worker's provider event ({ provider, eventType?, event }, the payload of
174
216
  * sync:sidepanel:provider-event) as what it is doing: { kind, name, input }
@@ -177,7 +219,11 @@ export const OPENCODE_TOOL_STATES = Object.freeze(['running', 'completed', 'erro
177
219
  * stage and the tray keep only compactActivity() of it. OpenCode names its
178
220
  * tools bash, read, edit, write, patch, glob, grep, list, webfetch,
179
221
  * websearch, codesearch, task, todowrite, and MCP tools `<server>_<tool>`
180
- * (SynaBun_browser_navigate): kindOf() reads all of them.
222
+ * (SynaBun_browser_navigate): kindOf() reads all of them. An Antigravity
223
+ * worker's events are ACP session updates (eventType = their `sessionUpdate`):
224
+ * a tool call when it is announced (`tool_call`, or an update that starts it
225
+ * and names its kind), a thought chunk as thinking, a plan as planning; its
226
+ * message text is no activity, as for the others.
181
227
  */
182
228
  export function activityOf(payload) {
183
229
  const provider = str(payload?.provider);
@@ -222,6 +268,13 @@ export function activityOf(payload) {
222
268
  const input = obj(part.state?.input);
223
269
  return { kind: kindOf(name, input), name, input };
224
270
  }
271
+ if (provider === 'antigravity') {
272
+ const type = str(payload.eventType || event.sessionUpdate);
273
+ if (type === 'agent_thought_chunk') return { kind: 'think', name: 'thinking', input: {} };
274
+ if (type === 'plan') return { kind: 'plan', name: 'update_plan', input: { todos: Array.isArray(event.entries) ? event.entries : [] } };
275
+ if (type === 'tool_call' || (type === 'tool_call_update' && str(event.kind) && event.status === 'in_progress')) return acpToolActivity(event);
276
+ return null;
277
+ }
225
278
  return null;
226
279
  }
227
280
 
@@ -22,7 +22,7 @@ import { getProviderMeta } from '../provider-icons.js';
22
22
  import { showGuestToast } from '../ui-sync.js';
23
23
  import { registerSidepanel, setSidepanelVisible, syncSidepanelLayout, focusSidepanel } from '../ui-sidepanel-windows.js';
24
24
  import {
25
- assistantBlocked, assistantOwnerOf, fetchLiveAssistantSessions, noteAssistantFocus, openAssistant, registerAssistantHost,
25
+ assistantBlocked, assistantOwnerOf, noteAssistantFocus, openAssistant, readLiveAssistantSessions, registerAssistantHost,
26
26
  } from '../ui-assistant.js';
27
27
  import { mountAssistant } from './asst-panel.js';
28
28
  import { openModelsManager } from './asst-route.js';
@@ -57,6 +57,11 @@ let _renaming = false;
57
57
  const _tabs = []; // { sessionId, viewport, toolbar, ctl, pill, label, userRenamed, status, brain }
58
58
  let _activeIdx = -1;
59
59
  let _tourPresenting = false;
60
+ // The saved tabs ({ activeIdx, tabs }) while the session list could not be read (a server that is
61
+ // starting answers before its Assistant routes exist): they stay saved and are asked about again.
62
+ let _unrestored = null;
63
+ let _restoring = null; // that restore under way: a session start waits for it
64
+ const RESTORE_RETRY_MS = [500, 1000, 2000, 4000, 8000, 15_000];
60
65
 
61
66
  // ── Helpers ──────────────────────────────────────────────────────────────────
62
67
 
@@ -94,11 +99,13 @@ function conversationOf(tab) {
94
99
 
95
100
  function saveTabs() {
96
101
  try {
97
- if (!_tabs.length) { storage.removeItem(KEYS.ASSISTANT_PANEL_TABS); return; }
102
+ // Saved tabs not restored yet stay saved (their names with them), after the open ones.
103
+ const waiting = (_unrestored?.tabs || []).filter(entry => !findTab(entry.sessionId));
104
+ if (!_tabs.length) { if (!waiting.length) storage.removeItem(KEYS.ASSISTANT_PANEL_TABS); return; }
98
105
  storage.setItem(KEYS.ASSISTANT_PANEL_TABS, JSON.stringify({
99
106
  v: 1,
100
107
  activeIdx: Math.max(0, _activeIdx),
101
- tabs: _tabs.map(tab => ({ sessionId: tab.sessionId, label: tab.userRenamed ? tab.label : '', userRenamed: !!tab.userRenamed })),
108
+ tabs: [..._tabs.map(tab => ({ sessionId: tab.sessionId, label: tab.userRenamed ? tab.label : '', userRenamed: !!tab.userRenamed })), ...waiting],
102
109
  }));
103
110
  } catch { /* storage unavailable */ }
104
111
  }
@@ -416,6 +423,16 @@ async function startSession(prompt = '') {
416
423
  _startError = '';
417
424
  renderPlaceholder('starting');
418
425
  try {
426
+ // The saved tabs come back before a new session is made: the restore under way (the server is
427
+ // starting), or once more for the ones it could not ask about.
428
+ if (!_restoring && _unrestored) startRestore(_unrestored, []);
429
+ if (_restoring) await _restoring.catch(() => { /* a new session, then */ });
430
+ if (_tabs.length) {
431
+ const tab = activeTab() || _tabs[0];
432
+ switchTo(_tabs.indexOf(tab));
433
+ if (prompt) { tab.ctl?.insertText?.(prompt); tab.ctl?.focus?.(); } // a conversation's draft, never sent for them
434
+ return;
435
+ }
419
436
  const res = await openAssistant(prompt ? { host: HOST_ID, prompt } : { host: HOST_ID });
420
437
  if (!_tabs.length) renderPlaceholder(res ? null : 'failed');
421
438
  } finally {
@@ -635,7 +652,7 @@ export function beginAssistantTour({ onPreviewSend = () => {} } = {}) {
635
652
  const wasVisible = _visible;
636
653
  const previousPanel = state.lastActivePanel;
637
654
  const previousSidepanel = state.lastActiveSidepanel;
638
- const peers = ['#topright-claude-panel-btn', '#topright-codex-panel-btn', '#topright-opencode-panel-btn']
655
+ const peers = ['#topright-claude-panel-btn', '#topright-codex-panel-btn', '#topright-opencode-panel-btn', '#topright-antigravity-panel-btn']
639
656
  .map(s => document.querySelector(s)).filter(b => b?.classList.contains('active'));
640
657
  _tourPresenting = true;
641
658
  const tab = activeTab();
@@ -731,7 +748,30 @@ export async function initAssistantPanel() {
731
748
 
732
749
  const saved = readSavedTabs();
733
750
  if (!saved?.tabs.length) return;
734
- const live = await fetchLiveAssistantSessions();
751
+ await startRestore(saved, RESTORE_RETRY_MS);
752
+ }
753
+
754
+ function startRestore(saved, retries) {
755
+ _restoring = restoreSavedTabs(saved, retries).finally(() => { _restoring = null; });
756
+ return _restoring;
757
+ }
758
+
759
+ /**
760
+ * The saved tabs come back (hidden, as pills) for the sessions the server still has open. A list
761
+ * that cannot be read restores nothing and forgets nothing: the tabs stay saved, names included
762
+ * (saveTabs), the list is asked again after each wait of `retries`, then at the next session start.
763
+ */
764
+ async function restoreSavedTabs(saved, retries) {
765
+ _unrestored = saved;
766
+ let live = await readLiveAssistantSessions();
767
+ for (const wait of retries) {
768
+ if (live || assistantBlocked()) break;
769
+ await new Promise(done => setTimeout(done, wait));
770
+ live = await readLiveAssistantSessions();
771
+ }
772
+ if (!live) return;
773
+ _unrestored = null;
774
+ const hadTabs = _tabs.length > 0; // one opened meanwhile stays the one showing
735
775
  for (const entry of saved.tabs) {
736
776
  const meta = live.get(entry.sessionId);
737
777
  if (!meta) continue;
@@ -746,8 +786,12 @@ export async function initAssistantPanel() {
746
786
  restoring: true,
747
787
  });
748
788
  }
749
- if (_tabs.length) {
750
- _activeIdx = Math.min(Math.max(0, saved.activeIdx), _tabs.length - 1);
789
+ if (_tabs.length && !hadTabs) {
790
+ // The tab that was showing, by its session: a saved tab before it that did not come back (a draft,
791
+ // an ended session) must not hand the header to its neighbour.
792
+ const shownId = saved.tabs[saved.activeIdx]?.sessionId;
793
+ const shown = _tabs.findIndex(tab => tab.sessionId === shownId);
794
+ _activeIdx = shown >= 0 ? shown : Math.min(Math.max(0, saved.activeIdx), _tabs.length - 1);
751
795
  markActive();
752
796
  renderHeader();
753
797
  renderPills();
@@ -4,7 +4,7 @@
4
4
  // Pure module (no DOM, no i18n import) so node:test can exercise it. The
5
5
  // composer resolves `descKey` through i18n and falls back to `desc`.
6
6
 
7
- import { normalizeComputerMode, normalizeProvider, normalizeRouteMode } from './asst-state.js';
7
+ import { isBrainProvider, normalizeComputerMode, normalizeProvider, normalizeRouteMode } from './asst-state.js';
8
8
 
9
9
  export const SLASH_COMMANDS = [
10
10
  { name: 'dispatch', usage: '/dispatch <provider> [--model m] [--effort e] [--agent a] [--project p] [--tools a,b] [--account id] [--ask] [--title "t"] <task>', descKey: 'assistant.slash.dispatch', desc: 'Dispatch a task to a sidepanel agent' },
@@ -143,6 +143,8 @@ function parseModel(rest) {
143
143
  provider = normalizeProvider(first);
144
144
  model = null;
145
145
  }
146
+ // /model switches the brain: a provider that only runs dispatched work (none of the four today) is refused, not swapped.
147
+ if (provider && !isBrainProvider(provider)) return { name: 'model', provider, model, effort: tokens[1] || null, error: 'not_brain' };
146
148
  return { name: 'model', provider, model, effort: tokens[1] || null };
147
149
  }
148
150
 
@@ -182,7 +184,11 @@ export function parseSlashCommand(text) {
182
184
  return enabled == null ? { name, mode: null, error: 'invalid_mode', value: restTrim } : { name, mode: enabled ? 'on' : 'off' };
183
185
  }
184
186
  case 'stop': return { name, target: restTrim && !/^all$/i.test(restTrim) ? restTrim : null, all: /^all$/i.test(restTrim) };
185
- case 'new': return { name, provider: restTrim ? normalizeProvider(restTrim) : null };
187
+ case 'new': {
188
+ const provider = restTrim ? normalizeProvider(restTrim) : null;
189
+ // A new session's brain: a provider that only runs dispatched work cannot be one (Antigravity can, since it runs the brain too).
190
+ return provider && !isBrainProvider(provider) ? { name, provider, error: 'not_brain' } : { name, provider };
191
+ }
186
192
  case 'resume': return { name, sessionId: restTrim || null };
187
193
  case 'compact':
188
194
  case 'clear':
@@ -8,9 +8,23 @@
8
8
  import { KEYS } from '../constants.js';
9
9
  import { isNewerRunDescriptor } from '../ui-native-loop-router-state.js';
10
10
  import { modelEffortIds } from '../agent-runtime-options.js';
11
+ import { getProviderMeta } from '../provider-icons.js';
11
12
  import { usagePacketIsStale } from './asst-usage.js';
12
13
 
13
- export const PROVIDERS = ['claude-code', 'codex', 'opencode'];
14
+ // The providers the Assistant itself can run on (its brain): the brain picker, /model and /new.
15
+ // Antigravity runs over ACP (lib/assistant-brains/antigravity.js), like a worker.
16
+ export const BRAIN_PROVIDERS = ['claude-code', 'codex', 'opencode', 'antigravity'];
17
+ // Providers that only ever run dispatched work. None any more (Antigravity became a brain too); kept so a module
18
+ // or a test that still imports the list reads it as empty.
19
+ export const WORKER_ONLY_PROVIDERS = [];
20
+ // Every provider the Assistant lists models for and can route a task to.
21
+ export const PROVIDERS = [...BRAIN_PROVIDERS, ...WORKER_ONLY_PROVIDERS];
22
+ // Providers that report tokens and no prices (Antigravity bills through its Google sign-in): a budget gives them no
23
+ // share, so a dry share never names them (asst-route.js), and their rows read "unpriced".
24
+ export const UNPRICED_PROVIDERS = ['antigravity'];
25
+ // Providers an older server may not list in its catalog yet: a model section without rows and without an archive
26
+ // for them stays hidden rather than showing an empty provider.
27
+ export const OPTIONAL_PROVIDERS = ['antigravity'];
14
28
 
15
29
  export const PROVIDER_ALIASES = {
16
30
  'claude-code': 'claude-code',
@@ -22,24 +36,38 @@ export const PROVIDER_ALIASES = {
22
36
  openai: 'codex',
23
37
  opencode: 'opencode',
24
38
  oc: 'opencode',
39
+ antigravity: 'antigravity',
40
+ agy: 'antigravity',
41
+ 'google antigravity': 'antigravity',
42
+ // Antigravity's CLI took the place of Gemini CLI: an old name or saved route means it.
43
+ gemini: 'antigravity',
25
44
  };
26
45
 
27
46
  export const PROVIDER_SHORT_LABELS = {
28
47
  'claude-code': 'Claude',
29
48
  codex: 'Codex',
30
49
  opencode: 'OpenCode',
50
+ antigravity: 'Antigravity',
51
+ };
52
+
53
+ export const PROVIDER_LABELS = {
54
+ 'claude-code': 'Claude Code',
55
+ codex: 'Codex',
56
+ opencode: 'OpenCode',
57
+ antigravity: 'Antigravity',
31
58
  };
32
59
 
33
60
  // Providers whose brain carries an account profile picker.
34
61
  export const ACCOUNT_PROVIDERS = new Set(['claude-code', 'codex']);
35
62
 
36
63
  // Approval modes are sent as `permissionMode` (`set_permission_mode {mode}` /
37
- // `query.permissionMode`). Claude uses the SDK enum; Codex/OpenCode brains only
38
- // distinguish "ask me" from "auto-approve". Plan mode is a separate flag
39
- // (`planMode`) that combines with any approval mode, each provider in its own
40
- // way: Claude runs the SDK 'plan' mode and returns to the approval mode once
64
+ // `query.permissionMode`). Claude uses the SDK enum; Codex/OpenCode/Antigravity
65
+ // brains only distinguish "ask me" from "auto-approve". Plan mode is a separate
66
+ // flag (`planMode`) that combines with any approval mode, each provider in its
67
+ // own way: Claude runs the SDK 'plan' mode and returns to the approval mode once
41
68
  // the plan is approved; Codex runs its native plan collaboration mode (read-only
42
- // sandbox); OpenCode runs its own `plan` agent.
69
+ // sandbox); OpenCode runs its own `plan` agent; Antigravity refuses code changes
70
+ // through SynaBun's hook and proposes the plan in a <proposed_plan> block.
43
71
  export const PERMISSION_MODES = {
44
72
  'claude-code': [
45
73
  { id: 'default', label: 'Default' },
@@ -54,9 +82,13 @@ export const PERMISSION_MODES = {
54
82
  { id: 'default', label: 'Default' },
55
83
  { id: 'auto', label: 'Auto-accept' },
56
84
  ],
85
+ antigravity: [
86
+ { id: 'default', label: 'Default' },
87
+ { id: 'auto', label: 'Auto-accept' },
88
+ ],
57
89
  };
58
90
 
59
- export const PLAN_MODE_PROVIDERS = new Set(['claude-code', 'codex', 'opencode']);
91
+ export const PLAN_MODE_PROVIDERS = new Set(['claude-code', 'codex', 'opencode', 'antigravity']);
60
92
 
61
93
  export const DEFAULT_BRAIN = Object.freeze({
62
94
  provider: 'claude-code',
@@ -80,10 +112,42 @@ export function normalizeProvider(value, fallback = null) {
80
112
  return PROVIDER_ALIASES[key] || fallback;
81
113
  }
82
114
 
115
+ /** Can the Assistant itself run on `provider` (a canonical id)? */
116
+ export function isBrainProvider(provider) {
117
+ return BRAIN_PROVIDERS.includes(provider);
118
+ }
119
+
120
+ /** Does `provider` report tokens and no prices (Antigravity)? */
121
+ export function isUnpricedProvider(provider) {
122
+ return UNPRICED_PROVIDERS.includes(provider);
123
+ }
124
+
125
+ /** `value` as a brain provider, else `fallback`: an unknown name (or a worker-only provider) is never a brain. */
126
+ export function normalizeBrainProvider(value, fallback = null) {
127
+ const provider = normalizeProvider(value);
128
+ return provider && isBrainProvider(provider) ? provider : fallback;
129
+ }
130
+
83
131
  export function providerShortLabel(provider) {
84
132
  return PROVIDER_SHORT_LABELS[provider] || String(provider || '');
85
133
  }
86
134
 
135
+ /** "Claude Code", "Antigravity": a provider's full name ('' for none). */
136
+ export function providerLabel(provider) {
137
+ return PROVIDER_LABELS[provider] || String(provider || '');
138
+ }
139
+
140
+ /**
141
+ * The icon, colour and name of a provider (provider-icons.js). A provider that file does not know
142
+ * yet keeps its own name and shows no icon, never another provider's (getProviderMeta falls back to
143
+ * Claude Code's for an unknown id).
144
+ */
145
+ export function providerMeta(provider) {
146
+ const meta = getProviderMeta(provider || undefined);
147
+ if (!provider || meta?.id === provider) return meta;
148
+ return { id: provider, label: providerLabel(provider), shortLabel: providerShortLabel(provider), icon: '', color: '' };
149
+ }
150
+
87
151
  function str(value) {
88
152
  if (value == null) return '';
89
153
  return String(value).trim();
@@ -93,16 +157,19 @@ function str(value) {
93
157
  export function normalizeBrain(raw, fallback = DEFAULT_BRAIN) {
94
158
  const base = { ...DEFAULT_BRAIN, ...(fallback || {}) };
95
159
  const src = raw && typeof raw === 'object' ? raw : {};
96
- const provider = normalizeProvider(src.provider ?? src.brain, base.provider) || 'claude-code';
160
+ // A provider that cannot be the brain (an unknown name) falls back like any unknown one.
161
+ const provider = normalizeBrainProvider(src.provider ?? src.brain, normalizeBrainProvider(base.provider)) || 'claude-code';
97
162
  const modes = PERMISSION_MODES[provider] || PERMISSION_MODES['claude-code'];
98
163
  const rawMode = str(src.permissionMode);
99
164
  // 'plan' used to be one of Claude's permission modes: it is the plan flag now.
100
165
  const legacyPlan = rawMode === 'plan';
101
166
  const mode = legacyPlan ? 'default' : rawMode;
167
+ const effort = str(src.effort) || null;
102
168
  return {
103
169
  provider,
104
170
  model: str(src.model),
105
- effort: str(src.effort) || null,
171
+ // Antigravity's thinking levels have no "off": no level is the model's own default (null).
172
+ effort: provider === 'antigravity' && effort === 'off' ? null : effort,
106
173
  agent: provider === 'opencode' ? (str(src.agent) || null) : null,
107
174
  project: str(src.project ?? src.cwd) || null,
108
175
  tools: provider === 'opencode' ? (Array.isArray(src.tools) ? [...src.tools] : null) : ['all'],
@@ -1368,6 +1368,40 @@ export function injectAssistantStyles() {
1368
1368
  /* Route card */
1369
1369
  .asst-route-summary { color: var(--asst-text); }
1370
1370
  .asst-route-reasons { margin-top: -4px; color: var(--asst-text-3); font-size: 11.5px; }
1371
+ /* Here / Dispatch: a segmented choice of where the task runs; the list, the model menu and Run follow it.
1372
+ sm capsules in one xs-padded track; the pick takes the gold "on" pair, like the option's key. */
1373
+ .asst-route-sides {
1374
+ display: inline-flex;
1375
+ align-self: flex-start;
1376
+ gap: 2px;
1377
+ padding: 2px;
1378
+ border-radius: var(--asst-pill-r-md);
1379
+ background: var(--asst-fill-1);
1380
+ box-shadow: inset 0 0 0 1px var(--asst-line);
1381
+ }
1382
+ .asst-route-side {
1383
+ display: inline-flex;
1384
+ align-items: center;
1385
+ justify-content: center;
1386
+ min-width: 76px;
1387
+ height: var(--asst-pill-h-sm);
1388
+ padding: 0 var(--asst-pill-px-sm);
1389
+ border: 0;
1390
+ border-radius: var(--asst-pill-r-sm);
1391
+ background: transparent;
1392
+ color: var(--asst-text-2);
1393
+ font: inherit;
1394
+ font-size: 12px;
1395
+ font-weight: 500;
1396
+ white-space: nowrap;
1397
+ cursor: pointer;
1398
+ transition: background-color var(--asst-dur-fast) var(--asst-ease-standard), box-shadow var(--asst-dur-fast) var(--asst-ease-standard), color var(--asst-dur-fast) var(--asst-ease-standard);
1399
+ }
1400
+ .asst-route-side:hover { background: var(--asst-fill-2); color: var(--asst-text); }
1401
+ .asst-route-side.selected { background: var(--asst-pill-on-fill); box-shadow: inset 0 0 0 1px var(--asst-pill-on-ring); color: var(--asst-text); }
1402
+ .asst-route-side[aria-disabled="true"] { opacity: .45; cursor: not-allowed; }
1403
+ .asst-route-side[aria-disabled="true"]:not(.selected):hover { background: transparent; color: var(--asst-text-2); }
1404
+ .asst-route-side-note { margin-top: -6px; color: var(--asst-text-3); font-size: 11.5px; }
1371
1405
  .asst-route-options { display: flex; flex-direction: column; gap: 4px; }
1372
1406
  .asst-route-option {
1373
1407
  display: flex;
@@ -13,16 +13,43 @@
13
13
 
14
14
  export const KIND_IDS = Object.freeze(['shell', 'read', 'edit', 'search', 'web', 'subagent', 'plan', 'memory', 'runs', 'computer', 'think', 'mcp']);
15
15
 
16
- // Tool names after lowercasing and stripping `mcp__<server>__` / `<server>_`.
16
+ // Tool names after lowercasing and stripping `mcp__<server>__` / `<server>_`. Antigravity's own tools keep their
17
+ // snake_case names (run_command, read_file, write_to_file, grep_search, search_web …).
17
18
  const KIND_TOOLS = {
18
- shell: ['bash', 'bashoutput', 'killshell', 'shell', 'exec_command', 'commandexecution'],
19
- read: ['read', 'notebookread', 'view'],
20
- edit: ['edit', 'multiedit', 'write', 'notebookedit', 'patch', 'apply_patch', 'filechange'],
21
- search: ['grep', 'glob', 'ls', 'list', 'codesearch', 'toolsearch'],
22
- web: ['webfetch', 'websearch'],
23
- subagent: ['task', 'agent', 'collabtoolcall', 'taskstop'],
19
+ shell: ['bash', 'bashoutput', 'killshell', 'shell', 'exec_command', 'commandexecution', 'run_command', 'send_command_input', 'command_status', 'read_terminal', 'shell_exec'],
20
+ read: ['read', 'notebookread', 'view', 'read_file', 'view_file', 'view_file_outline', 'view_code_item', 'view_content_chunk', 'read_resource', 'read_notebook'],
21
+ edit: ['edit', 'multiedit', 'write', 'notebookedit', 'patch', 'apply_patch', 'filechange', 'write_to_file', 'write_file', 'create_file', 'replace_file_content', 'multi_replace_file_content', 'edit_file', 'edit_notebook', 'delete_file', 'move_file'],
22
+ search: ['grep', 'glob', 'ls', 'list', 'codesearch', 'toolsearch', 'grep_search', 'find_by_name', 'list_dir', 'list_directory', 'codebase_search', 'code_search'],
23
+ web: ['webfetch', 'websearch', 'search_web', 'read_url_content', 'open_browser_url', 'read_browser_page', 'browser_subagent'],
24
+ subagent: ['task', 'agent', 'collabtoolcall', 'taskstop', 'invoke_subagent'],
24
25
  plan: ['todowrite', 'todoread', 'update_plan'],
25
26
  };
27
+
28
+ // Antigravity names its arguments in its own words: each alias is filled from the first of them that is set.
29
+ const INPUT_ALIASES = [
30
+ ['command', ['CommandLine']],
31
+ ['cwd', ['Cwd']],
32
+ ['file_path', ['AbsolutePath', 'TargetFile', 'Path']],
33
+ ['path', ['DirectoryPath', 'SearchPath', 'SearchDirectory']],
34
+ ['query', ['Query']],
35
+ ['url', ['Url']],
36
+ ];
37
+
38
+ /**
39
+ * A tool call's input with the names the rack reads (command, file_path, path, query, url) filled from
40
+ * Antigravity's own (CommandLine, AbsolutePath, TargetFile, DirectoryPath, Query, Url …). A name that is set
41
+ * already is kept; anything else is left as it came. Not an object: {}.
42
+ */
43
+ export function canonicalToolInput(input) {
44
+ const i = input && typeof input === 'object' && !Array.isArray(input) ? input : {};
45
+ let out = null;
46
+ for (const [alias, sources] of INPUT_ALIASES) {
47
+ if (i[alias] !== undefined && i[alias] !== null && i[alias] !== '') continue;
48
+ const value = sources.map(key => i[key]).find(v => typeof v === 'string' && v);
49
+ if (value) { out = out || { ...i }; out[alias] = value; }
50
+ }
51
+ return out || i;
52
+ }
26
53
  // SynaBun's own tools: only when the call is SynaBun's (or carries no server).
27
54
  const MEMORY_TOOLS = ['recall', 'remember', 'reflect', 'forget', 'restore', 'memories', 'sync', 'category'];
28
55
  const COMPUTER_TOOLS = ['computer', 'computer_apps', 'computer_ax', 'computer_status'];
@@ -13,6 +13,7 @@ const REASONS = {
13
13
  'codex-no-records': ['reason.codex-no-records', 'Codex ended without final usage records.'],
14
14
  'codex-rollout-missing': ['reason.codex-rollout-missing', "Codex usage records were missing; the turn's own totals were used."],
15
15
  'opencode-not-reconciled': ['reason.opencode-not-reconciled', "OpenCode's final message check did not finish."],
16
+ 'antigravity-total-mismatch': ['reason.antigravity-total-mismatch', "Antigravity's token total did not match its token classes; the classes were counted as reported."],
16
17
  interrupted: ['reason.interrupted', 'A turn was interrupted; the call it cut off is counted from the stream.'],
17
18
  'no-result': ['reason.no-result', 'A run ended mid-turn; its last calls are counted from the stream.'],
18
19
  };
@@ -34,7 +35,7 @@ const node = (tag, cls, value) => {
34
35
  };
35
36
  const valid = (n) => Number.isSafeInteger(n) && n >= 0;
36
37
  const safe = (n) => (valid(n) ? n : 0);
37
- const providerName = (id) => ['claude-code', 'codex', 'opencode'].includes(id) ? getProviderMeta(id).label : id === 'jev' ? 'Jev' : String(id || '');
38
+ const providerName = (id) => ['claude-code', 'codex', 'opencode'].includes(id) ? getProviderMeta(id).label : id === 'antigravity' ? 'Antigravity' : id === 'jev' ? 'Jev' : String(id || '');
38
39
  const defaultTr = (_, fallback, params) => String(fallback).replace(/\{(\w+)\}/g, (_m, key) => params?.[key] ?? `{${key}}`);
39
40
  export const formatExactTokens = (n) => new Intl.NumberFormat('en-US', { maximumFractionDigits: 0 }).format(valid(n) ? n : 0);
40
41
  export function formatCompactTokens(n) {
@@ -6,7 +6,7 @@ const bound = new WeakSet();
6
6
  const diffStates = new WeakMap();
7
7
  const pending = new WeakMap();
8
8
  const cleaned = new WeakMap();
9
- const watched = new WeakSet();
9
+ const watched = new WeakMap(); // transcript -> teardown (including queued mutation work)
10
10
  const observer = typeof IntersectionObserver === 'function' ? new IntersectionObserver(entries => {
11
11
  for (const entry of entries) if (entry.isIntersecting) { pending.get(entry.target)?.(); observer.unobserve(entry.target); pending.delete(entry.target); }
12
12
  }, { rootMargin: '200px' }) : null;
@@ -23,6 +23,7 @@ const measurements = new WeakMap();
23
23
  const measurementQueue = new Set();
24
24
  let measurementFrame = false, capId = 0;
25
25
  function queueMeasurement(el) {
26
+ if (!measurements.has(el)) return;
26
27
  measurementQueue.add(el);
27
28
  if (measurementFrame) return;
28
29
  measurementFrame = true;
@@ -50,6 +51,7 @@ function measureChanges(el, update) {
50
51
  }
51
52
  function releaseMeasurements(root) {
52
53
  for (const el of [root, ...(root.querySelectorAll?.('*') || [])]) {
54
+ observer?.unobserve(el); pending.delete(el);
53
55
  sizeObserver?.unobserve(el); contentObservers.get(el)?.disconnect();
54
56
  contentObservers.delete(el); measurements.delete(el); measurementQueue.delete(el);
55
57
  }
@@ -249,11 +251,13 @@ export function cleanOutputBlocks(container) {
249
251
  // Covers generic/legacy cards and request builders which assign pre.textContent
250
252
  // directly. Observe each transcript once; our own writes settle after one pass.
251
253
  export function observeOutputBlocks(container) {
252
- if (!container || watched.has(container) || typeof MutationObserver !== 'function') return;
253
- watched.add(container);
254
+ if (!container || typeof MutationObserver !== 'function') return;
255
+ if (watched.has(container)) return watched.get(container);
254
256
  let queued = false;
257
+ let disposed = false;
255
258
  const dirty = new Set();
256
259
  const watcher = new MutationObserver(records => {
260
+ if (disposed) return;
257
261
  for (const record of records) {
258
262
  for (const removed of record.removedNodes || []) if (removed.nodeType === 1 && !removed.isConnected) releaseMeasurements(removed);
259
263
  const target = record.target.nodeType === 1 ? record.target : record.target.parentElement;
@@ -265,6 +269,7 @@ export function observeOutputBlocks(container) {
265
269
  queued = true;
266
270
  queueMicrotask(() => {
267
271
  queued = false;
272
+ if (disposed) return;
268
273
  for (const root of dirty) {
269
274
  if (root.matches?.('pre, .cxp-card-pre') && !root.closest('.cxp-readable, .cxp-output-source, .cxp-diff-view') && !root.querySelector('.cxp-readable')) {
270
275
  const code = root.querySelector('code');
@@ -276,6 +281,24 @@ export function observeOutputBlocks(container) {
276
281
  });
277
282
  });
278
283
  watcher.observe(container, { childList: true, characterData: true, subtree: true });
284
+ const dispose = () => {
285
+ disposed = true;
286
+ watcher.disconnect();
287
+ dirty.clear();
288
+ watched.delete(container);
289
+ releaseMeasurements(container);
290
+ };
291
+ watched.set(container, dispose);
292
+ return dispose;
293
+ }
294
+
295
+ // Removing a whole transcript does not produce a mutation *inside* it. Both
296
+ // global observers must release its outputs and lazy diffs explicitly on close.
297
+ export function disposeOutputBlocks(container) {
298
+ if (!container) return;
299
+ const dispose = watched.get(container);
300
+ if (dispose) dispose();
301
+ else releaseMeasurements(container);
279
302
  }
280
303
 
281
304
  function diffRows(file, target, count) {