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
@@ -0,0 +1,181 @@
1
+ // ═══════════════════════════════════════════
2
+ // SynaBun — Antigravity panel: tabs, attachments and dispatched-run events (no DOM)
3
+ // ═══════════════════════════════════════════
4
+ //
5
+ // A tab is one ACP session of Antigravity's in one project folder. What is saved
6
+ // per window (`synabun-antigravity-panel-tabs-<windowId>`, read by the side panel
7
+ // tray too) is `{ tabs, activeIdx }` with the fields below; the transcript is not
8
+ // saved: a tab with a session id replays it from Antigravity (session/load).
9
+ // `bypass` is the tab's Bypass (agy-bypass.js): off unless the owner turned it on.
10
+
11
+ export const TAB_FIELDS = Object.freeze(['id', 'sessionId', 'project', 'geminiDir', 'label', 'title', 'titleByUser', 'additionalDirectories', 'draft',
12
+ 'running', 'needsYou', 'automationRunId', 'automationActive', 'automationOwnerId', 'releasedRunId', 'createdAt', 'bypass']);
13
+ export const MAX_TABS = 12;
14
+ export const ATTACH_LIMITS = Object.freeze({ maxAttachments: 8, maxBytes: 15 * 1024 * 1024 });
15
+ const ATTACHABLE = /^(image\/(png|jpeg|gif|webp)|application\/pdf|text\/[a-z0-9.+-]+|application\/(json|xml|x-yaml|yaml)|audio\/(wav|x-wav|mpeg|mp4|aac|flac|ogg|opus|webm))$/;
16
+ const TEXT_EXTENSIONS = /\.(txt|md|markdown|json|ya?ml|xml|csv|log|js|mjs|cjs|ts|tsx|jsx|py|rb|go|rs|java|kt|swift|c|cc|cpp|h|hpp|cs|php|sh|zsh|bash|css|scss|html|sql|toml|ini|env)$/i;
17
+
18
+ let counter = 0;
19
+ const newId = () => (globalThis.crypto?.randomUUID?.() || `agy-${Date.now().toString(36)}-${(counter += 1)}`);
20
+
21
+ export function newTab({ project = '', sessionId = null, title = null, geminiDir = null } = {}) {
22
+ return {
23
+ id: newId(), sessionId, project: String(project || ''), geminiDir: typeof geminiDir === 'string' && geminiDir.trim() ? geminiDir.trim() : null,
24
+ label: 'New session', title, titleByUser: null, additionalDirectories: [], draft: '',
25
+ running: false, needsYou: false, automationRunId: null, automationActive: false, automationOwnerId: null,
26
+ releasedRunId: null, createdAt: Date.now(), bypass: false,
27
+ };
28
+ }
29
+
30
+ /**
31
+ * What the tray and the panel show for a tab: its session's title, else a short id, else "New session". `words`
32
+ * gives the page's language for the last three (the saved label stays English when none is given).
33
+ */
34
+ export function tabLabel(tab, words = {}) {
35
+ const title = typeof tab?.title === 'string' ? tab.title.trim() : '';
36
+ if (title) return title.length > 60 ? `${title.slice(0, 57)}…` : title;
37
+ if (tab?.automationRunId) return `${words.run || 'Dispatched run'} · ${String(tab.automationRunId).slice(0, 8)}`;
38
+ if (tab?.sessionId) return `Antigravity · ${String(tab.sessionId).slice(0, 8)}`;
39
+ return words.newSession || 'New session';
40
+ }
41
+
42
+ /** The saved form: `{ tabs, activeIdx }`, each tab its persisted fields only. */
43
+ export function serializeTabs(tabs, activeIdx) {
44
+ const list = (Array.isArray(tabs) ? tabs : []).slice(0, MAX_TABS).map((tab) => {
45
+ const out = {};
46
+ for (const key of TAB_FIELDS) if (tab?.[key] !== undefined) out[key] = tab[key];
47
+ out.label = tabLabel(tab);
48
+ return out;
49
+ });
50
+ const idx = Number.isInteger(activeIdx) ? Math.min(Math.max(activeIdx, 0), Math.max(list.length - 1, 0)) : 0;
51
+ return { tabs: list, activeIdx: idx };
52
+ }
53
+
54
+ /**
55
+ * Saved tabs as the panel restores them: invalid rows dropped, nothing marked running (the page that ran it is
56
+ * gone: a live run is reattached by the native-loop router, not by a flag), an id for every tab.
57
+ */
58
+ export function restoreTabs(raw) {
59
+ let data = raw;
60
+ if (typeof raw === 'string') { try { data = JSON.parse(raw); } catch { data = null; } }
61
+ const rows = Array.isArray(data?.tabs) ? data.tabs : [];
62
+ const tabs = [];
63
+ const seen = new Set();
64
+ for (const row of rows) {
65
+ if (!row || typeof row !== 'object') continue;
66
+ const tab = { ...newTab(), ...Object.fromEntries(TAB_FIELDS.filter((key) => row[key] !== undefined).map((key) => [key, row[key]])) };
67
+ if (typeof tab.id !== 'string' || !tab.id || seen.has(tab.id)) tab.id = newId();
68
+ seen.add(tab.id);
69
+ if (tab.sessionId !== null && (typeof tab.sessionId !== 'string' || !/^[A-Za-z0-9._:@-]{1,200}$/.test(tab.sessionId))) tab.sessionId = null;
70
+ tab.project = typeof tab.project === 'string' ? tab.project : '';
71
+ tab.geminiDir = typeof tab.geminiDir === 'string' && tab.geminiDir.trim() ? tab.geminiDir.trim() : null;
72
+ tab.draft = typeof tab.draft === 'string' ? tab.draft : '';
73
+ tab.titleByUser = typeof tab.titleByUser === 'string' && tab.titleByUser.trim() ? tab.titleByUser.trim().slice(0, 120) : null;
74
+ tab.additionalDirectories = Array.isArray(tab.additionalDirectories)
75
+ ? [...new Set(tab.additionalDirectories.filter((p) => typeof p === 'string' && p && p !== tab.project))].slice(0, 8)
76
+ : [];
77
+ // Bypass comes back only as the owner's own pick (a literal true), never from anything else in storage.
78
+ tab.bypass = tab.bypass === true;
79
+ tab.running = false;
80
+ tab.needsYou = false;
81
+ tabs.push(tab);
82
+ if (tabs.length >= MAX_TABS) break;
83
+ }
84
+ const activeIdx = Number.isInteger(data?.activeIdx) && data.activeIdx >= 0 && data.activeIdx < tabs.length ? data.activeIdx : 0;
85
+ return { tabs, activeIdx };
86
+ }
87
+
88
+ /** The MIME type a dropped or pasted file is sent as (a browser leaves it empty for many text files). */
89
+ export function attachmentType(file) {
90
+ const declared = String(file?.type || '').toLowerCase();
91
+ if (declared) return declared;
92
+ return TEXT_EXTENSIONS.test(String(file?.name || '')) ? 'text/plain' : '';
93
+ }
94
+
95
+ /** Whether a file may be attached now: null when it may, else { key, params } for agyText. */
96
+ export function attachmentProblem(file, staged = [], limits = ATTACH_LIMITS) {
97
+ const name = String(file?.name || 'file');
98
+ if (staged.length >= limits.maxAttachments) return { key: 'attachmentTooMany', params: { count: limits.maxAttachments } };
99
+ if (!ATTACHABLE.test(attachmentType(file))) return { key: 'attachmentType', params: { name } };
100
+ if (Number(file?.size) > limits.maxBytes) return { key: 'attachmentTooLarge', params: { name, size: Math.round(limits.maxBytes / 1048576) } };
101
+ return null;
102
+ }
103
+
104
+ /**
105
+ * A picture another part of the app hands the panel (the whiteboard's Send to Panel, also through a detached window)
106
+ * as a staged attachment: `{ file: { name, mimeType, dataUrl } }`, `{ problem: { key, params } }` when it may not
107
+ * be attached now (the same limits as a pasted file), or null when it is not a PNG, JPEG, GIF or WebP data URL.
108
+ */
109
+ export function stagedPicture(dataUrl, staged = [], { name = 'whiteboard', now = Date.now(), limits = ATTACH_LIMITS } = {}) {
110
+ const match = /^data:(image\/(png|jpeg|gif|webp));base64,([A-Za-z0-9+/]+={0,2})$/.exec(String(dataUrl || ''));
111
+ if (!match) return null;
112
+ const body = match[3];
113
+ const size = Math.floor((body.length * 3) / 4) - (body.endsWith('==') ? 2 : body.endsWith('=') ? 1 : 0);
114
+ const file = { name: `${name}-${now}.${match[2] === 'jpeg' ? 'jpg' : match[2]}`, type: match[1], size };
115
+ const problem = attachmentProblem(file, staged, limits);
116
+ return problem ? { problem } : { file: { name: file.name, mimeType: match[1], dataUrl } };
117
+ }
118
+
119
+ /** A data URL's base64 body, or '' for anything else. */
120
+ export function base64Of(dataUrl) {
121
+ const match = /^data:[^;,]*(?:;[^;,]*)*;base64,([A-Za-z0-9+/]+={0,2})$/.exec(String(dataUrl || ''));
122
+ return match ? match[1] : '';
123
+ }
124
+
125
+ /** The bridge's `prompt` request for a tab: its text and staged attachments ({ name, mimeType, dataUrl }). */
126
+ export function promptRequest(tab, text, staged = []) {
127
+ return {
128
+ sessionId: tab.sessionId,
129
+ text: String(text || ''),
130
+ attachments: staged.map((file) => ({ name: file.name, mimeType: file.mimeType, data: base64Of(file.dataUrl) })).filter((file) => file.data),
131
+ paths: staged.filter((file) => file.path).map((file) => file.path),
132
+ };
133
+ }
134
+
135
+ /**
136
+ * One event of a dispatched run (the native-loop router's `sync:sidepanel:provider-event` payload, or its `event`)
137
+ * as what the transcript does with it: { kind: 'user', text, iteration } | { kind: 'update', update } |
138
+ * { kind: 'notice', text, severity } | null.
139
+ */
140
+ export function automationEvent(payload) {
141
+ const message = payload?.event && typeof payload.event === 'object' && (payload.eventType || payload.provider) ? payload : (payload?.event || payload);
142
+ const eventType = message?.eventType || message?.event?.type || message?.type;
143
+ const event = message?.event ?? message;
144
+ if (!event || typeof event !== 'object') return null;
145
+ if (event.type === 'synabun.user_prompt' || eventType === 'synabun.user_prompt') return { kind: 'user', text: String(event.text || ''), iteration: message.iteration ?? null };
146
+ if (event.type === 'synabun.confinement') return { kind: 'notice', text: `${event.action === 'warned' ? 'Guard warning' : 'Refused'}: ${event.reason || event.rule || ''}`.trim(), severity: event.action === 'warned' ? 'warning' : 'error' };
147
+ if (event.type === 'synabun.provider_exit') return { kind: 'notice', text: `Antigravity stopped${event.error ? `: ${event.error}` : ''}`, severity: 'error' };
148
+ if (event.type === 'synabun.error') return { kind: 'notice', text: String(event.message || event.error || 'Error'), severity: 'error' };
149
+ if (typeof event.sessionUpdate === 'string') return { kind: 'update', update: event };
150
+ return null;
151
+ }
152
+
153
+ /** The per-run Antigravity home (`--gemini-dir`) a dispatched run executed in, or null when none is named. */
154
+ export function runGeminiDir(run) {
155
+ const raw = run?.antigravityHome ?? run?.geminiDir ?? run?.resumeInfo?.antigravityHome ?? run?.runtime?.antigravityHome ?? null;
156
+ return typeof raw === 'string' && raw.trim() ? raw.trim() : null;
157
+ }
158
+
159
+ /** The bridge request (`session:load` or `session:resume`) that opens an existing tab's session in the panel. */
160
+ export function openSessionRequest(tab, { resume = false, folders = [] } = {}) {
161
+ const dirs = Array.isArray(folders) ? folders.filter((p) => typeof p === 'string' && p) : [];
162
+ const geminiDir = typeof tab?.geminiDir === 'string' && tab.geminiDir ? tab.geminiDir : null;
163
+ return {
164
+ type: resume ? 'session:resume' : 'session:load',
165
+ payload: {
166
+ sessionId: tab.sessionId,
167
+ cwd: tab.project,
168
+ tabId: tab.id,
169
+ bypass: tab?.bypass === true,
170
+ ...(geminiDir ? { geminiDir } : {}),
171
+ ...(dirs.length ? { additionalDirectories: dirs } : {}),
172
+ },
173
+ };
174
+ }
175
+
176
+ /** The notice shown when loading or resuming a tab's session fails (null when sign-in handles it instead). */
177
+ export function sessionLoadNotice(error, textOf) {
178
+ if (error?.authRequired) return null;
179
+ if (error?.code === 'HOME_PRUNED') return { text: textOf('runHomePruned'), severity: 'error' };
180
+ return { text: textOf('historyFailed', { error: error?.message || String(error ?? '') }), severity: 'error' };
181
+ }
@@ -881,15 +881,15 @@ export async function fetchOpencodeSessions({ project, limit, offset, search } =
881
881
  return jsonFetch(`/api/opencode/sessions?${params}`);
882
882
  }
883
883
 
884
- // ─── Gemini CLI Sessions (Resume) ─────
884
+ // ─── Antigravity CLI Sessions (Resume) ─────
885
885
 
886
- export async function fetchGeminiSessions({ project, limit, offset, search } = {}) {
886
+ export async function fetchAntigravitySessions({ project, limit, offset, search } = {}) {
887
887
  const params = new URLSearchParams();
888
888
  if (project) params.set('project', project);
889
889
  if (limit) params.set('limit', String(limit));
890
890
  if (offset) params.set('offset', String(offset));
891
891
  if (search) params.set('search', search);
892
- return jsonFetch(`/api/gemini/sessions?${params}`);
892
+ return jsonFetch(`/api/antigravity/sessions?${params}`);
893
893
  }
894
894
 
895
895
  // ─── Hybrid session content search (FTS5 + semantic for Claude) ─────
@@ -1143,7 +1143,7 @@ export async function fetchRulesStatus() {
1143
1143
  }
1144
1144
  }
1145
1145
 
1146
- /** The rendered text for a host (claude, codex, opencode, gemini, cursor) or the copy-only `coexistence` snippet. */
1146
+ /** The rendered text for a host (claude, codex, opencode, antigravity, cursor) or the copy-only `coexistence` snippet. */
1147
1147
  export async function fetchRulesText(host) {
1148
1148
  return jsonFetch(`/api/setup/rules/${encodeURIComponent(host)}/text`);
1149
1149
  }
@@ -23,13 +23,12 @@
23
23
  // last card, so one stray click or key there must not end the session's work.
24
24
 
25
25
  import { on } from '../state.js';
26
- import { getProviderMeta } from '../provider-icons.js';
27
26
  import { listAssistantRuns } from './asst-api.js';
28
27
  import { buildRunCard, tickRunCard, updateRunCard } from './asst-render.js';
29
28
  import { awaitsAnswer, compactActivity, isLivePhase, phaseLine, sceneFor, sceneSvg, stageLabel, stagePhase, stepLine } from './asst-scenes.js';
30
29
  import { injectSceneStyles } from './asst-scene-styles.js';
31
30
  import { plural } from './asst-tool-kinds.js';
32
- import { isRemovableRun, isRunIdle, isTerminalRunStatus, mergeRunDescriptor, runFromPayload, runNeedsAttention, sortRuns } from './asst-state.js';
31
+ import { isRemovableRun, isRunIdle, isTerminalRunStatus, mergeRunDescriptor, providerMeta, runFromPayload, runNeedsAttention, sortRuns } from './asst-state.js';
33
32
  import { createConfirmGate } from './asst-stop.js';
34
33
 
35
34
  const TICK_MS = 5000;
@@ -166,8 +165,8 @@ export function createAgentsDock(hostEl, hooks = {}) {
166
165
  return {
167
166
  t: hooks.t,
168
167
  compact: true,
169
- providerIcon: (p) => getProviderMeta(p).icon,
170
- providerColor: (p) => getProviderMeta(p).color,
168
+ providerIcon: (p) => providerMeta(p).icon,
169
+ providerColor: (p) => providerMeta(p).color,
171
170
  onFocus: hooks.onFocus,
172
171
  onStop: hooks.onStop,
173
172
  onRead: hooks.onRead,
@@ -48,7 +48,7 @@ export function fetchAssistantCatalog({ force = false } = {}) {
48
48
 
49
49
  // ── Hidden models (Assistant → Models) ───────
50
50
 
51
- /** → { ok, providers: { 'claude-code': [], codex: [], opencode: [] } } */
51
+ /** → { ok, providers: { 'claude-code': [], codex: [], opencode: [], antigravity: [] } } */
52
52
  export function getHiddenModelLists() {
53
53
  return jsonFetch('/api/assistant/hidden-models');
54
54
  }
@@ -42,6 +42,7 @@ import { createMascotControl } from './asst-mascot.js';
42
42
  import { modelChipForms, modelChipName, modelChipTag } from './asst-model-chip.js';
43
43
  import {
44
44
  ACCOUNT_PROVIDERS,
45
+ BRAIN_PROVIDERS,
45
46
  brainRequiresSwitch,
46
47
  emptyProjectView,
47
48
  modelShortName,
@@ -50,6 +51,7 @@ import {
50
51
  PLAN_MODE_PROVIDERS,
51
52
  permissionModeLabel,
52
53
  PROVIDERS,
54
+ providerMeta,
53
55
  projectLabel,
54
56
  resolveBrainDefaults,
55
57
  TEST_DENSITIES,
@@ -113,6 +115,7 @@ const PLAN_DESC = {
113
115
  'claude-code': 'Plan first; approving the plan continues in the approval mode below',
114
116
  codex: 'Native Codex plan mode (read-only sandbox)',
115
117
  opencode: 'OpenCode plan agent (no file edits)',
118
+ antigravity: 'Antigravity explores and proposes a plan; code changes wait for your approval',
116
119
  };
117
120
 
118
121
  function agentName(agent) {
@@ -194,7 +197,7 @@ function hiddenOpencodeIds(remote) {
194
197
  }
195
198
 
196
199
  // The models the user hid (Assistant → Models), per provider, as the last catalog load saw them.
197
- let _hidden = { 'claude-code': new Set(), codex: new Set(), opencode: new Set() };
200
+ let _hidden = { 'claude-code': new Set(), codex: new Set(), opencode: new Set(), antigravity: new Set() };
198
201
 
199
202
  function hiddenSets(remote) {
200
203
  const out = {};
@@ -295,7 +298,7 @@ function catalogSaysNone(remote, provider, local) {
295
298
  }
296
299
 
297
300
  /**
298
- * { 'claude-code': [...], codex: [...], opencode: [...] }: the models of every
301
+ * { 'claude-code': [...], codex: [...], opencode: [...], antigravity: [...] }: the models of every
299
302
  * Assistant model menu and of the Routes editor, from GET /api/assistant/catalog
300
303
  * (see mergeProviderModels). Models the user hid (Assistant → Models, and
301
304
  * Settings → OpenCode for OpenCode) never appear. `force` asks the server for a
@@ -316,6 +319,7 @@ export async function loadModelCatalog({ force = false } = {}) {
316
319
  // No catalog (request failed): the last known disabled lists still apply.
317
320
  const sets = remote ? hiddenSets(remote) : { ..._hidden, opencode: new Set([...(_hidden.opencode || []), ...hiddenOpencodeIds(null)]) };
318
321
  _hidden = sets;
322
+ if (remote?.providers) _providerInfo = remote.providers;
319
323
  for (const provider of PROVIDERS) {
320
324
  const local = getModelsForProfile(provider) || [];
321
325
  // An empty list the catalog vouches for is its answer: the window's own rows do not stand in for it.
@@ -366,6 +370,33 @@ function keepCatalog(catalog, sets) {
366
370
  /** The last loadModelCatalog() result, synchronously (null before the first load). */
367
371
  export function cachedModelCatalog() { return _catalog; }
368
372
 
373
+ // The catalog's provider entries ([{ id, brain, accounts, priced, signedIn?, tier? }]) as the last load saw them:
374
+ // Antigravity's sign-in state is read from there (it signs in inside its own CLI; SynaBun keeps no account of it).
375
+ let _providerInfo = null;
376
+
377
+ /** One provider's catalog entry ({ id, brain, signedIn?, tier?, … }), from an array or a map; null when unknown. */
378
+ export function catalogProviderInfo(provider, providers = _providerInfo) {
379
+ if (!provider || !providers) return null;
380
+ if (Array.isArray(providers)) return providers.find(p => p && p.id === provider) || null;
381
+ const entry = providers[provider];
382
+ return entry && typeof entry === 'object' ? entry : null;
383
+ }
384
+
385
+ /**
386
+ * What the model menu and the model chip say about Antigravity's sign-in (`info`: its catalog entry):
387
+ * { state: 'signedIn' | 'signedOut' | 'unknown', text }. Antigravity signs in with Google inside its own panel;
388
+ * the tier is the plan its session reported ("Google AI Pro").
389
+ */
390
+ export function antigravitySignInView(info, t = null) {
391
+ const tr = tf(t);
392
+ const tier = typeof info?.tier === 'string' && info.tier.trim() ? info.tier.trim() : (typeof info?.tier?.name === 'string' ? info.tier.name.trim() : '');
393
+ if (info?.signedIn === true) {
394
+ return { state: 'signedIn', text: tier ? tr('assistant.brain.antigravitySignedInTier', 'Signed in with Google ({tier})', { tier }) : tr('assistant.brain.antigravitySignedIn', 'Signed in with Google') };
395
+ }
396
+ if (info?.signedIn === false) return { state: 'signedOut', text: tr('assistant.brain.antigravitySignedOut', 'Not signed in: open the Antigravity panel to sign in') };
397
+ return { state: 'unknown', text: tr('assistant.brain.antigravitySignInUnknown', 'Sign-in not checked yet: Antigravity signs in inside its own panel') };
398
+ }
399
+
369
400
  /** UI metadata from the same catalog load; no second list or extra endpoint. */
370
401
  export function modelCatalogStatus() {
371
402
  return { partial: _catalogPartial, hiddenCounts: Object.fromEntries(PROVIDERS.map(provider => [provider, _hidden[provider]?.size || 0])) };
@@ -474,7 +505,8 @@ function tierTag(m, tr) {
474
505
  /**
475
506
  * Model menu grouped by provider, filterable, with "Custom model id…".
476
507
  * opts: { brain, onSelect(target), includeHere, hereLabel, title, t, placement,
477
- * current:{provider, model}|null, warnSwitch, providers, onToast(text),
508
+ * current:{provider, model}|null, warnSwitch, providers (default: the brain
509
+ * providers, so a worker-only one is listed only when named), onToast(text),
478
510
  * accept(provider, model) → bool (only those models), emptyLabel (when none
479
511
  * passes), allowCustom (default true), zIndex (see asst-menu.js),
480
512
  * accountItems (menu items listed first, under accountTitle) }
@@ -546,13 +578,15 @@ export async function openModelMenu(anchor, opts = {}) {
546
578
  if (includeHere) {
547
579
  items.push({ kind: 'radio', id: '__here', label: hereLabel || tr('assistant.routes.brainDecides', 'Let the brain decide'), selected: !current, onSelect: () => onSelect?.({ here: true }) });
548
580
  }
549
- const list = Array.isArray(providers) && providers.length ? PROVIDERS.filter(p => providers.includes(p)) : PROVIDERS;
581
+ // Without `providers` the menu is a brain's (the toolbar's model field, the WhatsApp brain): every provider the
582
+ // Assistant can run on (BRAIN_PROVIDERS, Antigravity included); a route or a dispatch names its own providers.
583
+ const list = Array.isArray(providers) && providers.length ? PROVIDERS.filter(p => providers.includes(p)) : BRAIN_PROVIDERS;
550
584
  let listed = 0;
551
585
  for (const provider of list) {
552
586
  const models = (catalog[provider] || []).filter(m => typeof accept !== 'function' || accept(provider, m));
553
587
  if (!models.length && !opts.routingEditor) continue;
554
588
  listed += models.length;
555
- const meta = getProviderMeta(provider);
589
+ const meta = providerMeta(provider);
556
590
  const hiddenCount = metadata.hiddenCounts[provider] || 0;
557
591
  const note = opts.routingEditor ? tr('assistant.routes.menu.hiddenCount', '{count} hidden in Models', { count: hiddenCount }) : warnSwitch && provider !== b.provider ? tr('assistant.brain.newConversation', 'starts a new conversation') : '';
558
592
  items.push({ kind: 'header', label: meta.label, icon: meta.icon, color: meta.color, note });
@@ -780,6 +814,8 @@ export function createBrainPicker(host, hooks = {}) {
780
814
  // The composer chip's account cue: the account menu's initial avatar when the provider has more than one
781
815
  // account (or the account is signed out), "Auto" for Auto; null otherwise. Its full words are the tooltip's.
782
816
  function accountCue(brain) {
817
+ // Antigravity signed out: a warning cue on the chip (its words are the tooltip's and the Model menu's).
818
+ if (brain.provider === 'antigravity') return catalogProviderInfo('antigravity')?.signedIn === false ? { text: '!', tone: 'warn' } : null;
783
819
  if (!ACCOUNT_PROVIDERS.has(brain.provider)) return null;
784
820
  if (brain.accountId === 'auto') return { text: t('assistant.brain.accountAutoShort', 'Auto'), tone: 'muted' };
785
821
  const accounts = brain.provider === 'codex' ? getCachedCodexAccounts() : getCachedClaudeAccounts();
@@ -804,7 +840,8 @@ export function createBrainPicker(host, hooks = {}) {
804
840
  effortSupported: brain.provider === 'opencode' || hasEfforts,
805
841
  effort: brain.provider === 'opencode'
806
842
  ? (brain.agent || t('assistant.brain.agentDefault', 'default'))
807
- : (level ? level.label : (brain.effort || t('assistant.brain.effortOff', 'off'))),
843
+ // Antigravity has no "off" level: no level is the model's own default ("Default").
844
+ : (level ? level.label : (brain.effort || (brain.provider === 'antigravity' ? (efforts.find(e => e.id === 'off')?.label || t('assistant.brain.effortOff', 'off')) : t('assistant.brain.effortOff', 'off')))),
808
845
  variantSupported: brain.provider === 'opencode' && hasEfforts,
809
846
  variant: level ? level.label : (brain.effort || t('assistant.brain.variantDefault', 'default variant')),
810
847
  project: brain.project ? projectLabel(brain.project, _projectsCache || []) : t('assistant.brain.projectHome', 'Home'),
@@ -888,6 +925,7 @@ export function createBrainPicker(host, hooks = {}) {
888
925
  const tip = [
889
926
  v.meta.label, model, who ? t('assistant.brain.asAccount', 'as {account}', { account: who }) : '',
890
927
  acc ? accountIdentity(acc) : '', acc ? accountBadge(acc, p).text : '',
928
+ brain.provider === 'antigravity' ? antigravitySignInView(catalogProviderInfo('antigravity'), hooks.t).text : '',
891
929
  ].filter(Boolean).join(' · ');
892
930
  setBtn('model', { label: '', icon: v.meta.icon, color: v.meta.color, tooltip: tip, aria: `${t('assistant.brain.model', 'Model')}: ${tip}` });
893
931
 
@@ -1111,10 +1149,12 @@ export function createBrainPicker(host, hooks = {}) {
1111
1149
  const brain = currentBrain();
1112
1150
  if (field === 'model') {
1113
1151
  // The account rows come from the kept lists; they are read again in the background once loaded.
1114
- if (ACCOUNT_PROVIDERS.has(brain.provider)) {
1152
+ // Antigravity has no SynaBun account: its sign-in state comes with the catalog.
1153
+ if (ACCOUNT_PROVIDERS.has(brain.provider) || brain.provider === 'antigravity') {
1115
1154
  if (_warmProviders.has(brain.provider)) ensureLoaded(brain.provider).catch(() => {});
1116
1155
  else await ensureLoaded(brain.provider);
1117
1156
  }
1157
+ const antigravity = brain.provider === 'antigravity';
1118
1158
  await openModelMenu(anchor, {
1119
1159
  brain,
1120
1160
  shouldOpen,
@@ -1122,8 +1162,8 @@ export function createBrainPicker(host, hooks = {}) {
1122
1162
  placement,
1123
1163
  current: { provider: brain.provider, model: brain.model },
1124
1164
  warnSwitch: true,
1125
- accountItems: ACCOUNT_PROVIDERS.has(brain.provider) ? accountItems(brain) : [],
1126
- accountTitle: accountText('menuTitle', {}, accountProvider(brain)),
1165
+ accountItems: ACCOUNT_PROVIDERS.has(brain.provider) ? accountItems(brain) : antigravity ? antigravitySignInItems() : [],
1166
+ accountTitle: antigravity ? t('assistant.brain.antigravitySignIn', 'Antigravity sign-in') : accountText('menuTitle', {}, accountProvider(brain)),
1127
1167
  onToast: hooks.onToast,
1128
1168
  onManageModels: hooks.onManageModels,
1129
1169
  onSelect: (target) => {
@@ -1298,6 +1338,12 @@ export function createBrainPicker(host, hooks = {}) {
1298
1338
  };
1299
1339
  }
1300
1340
 
1341
+ // Antigravity's place for accounts in the Model menu: its sign-in state, one line (antigravitySignInView).
1342
+ function antigravitySignInItems() {
1343
+ const view = antigravitySignInView(catalogProviderInfo('antigravity'), hooks.t);
1344
+ return [{ kind: 'info', id: '__antigravity_signin', label: view.text, className: `asst-agy-signin asst-agy-signin-${view.state}` }];
1345
+ }
1346
+
1301
1347
  // The composer's Model menu lists the brain's accounts too (Claude or Codex), as the cog's account menu
1302
1348
  // does (asst-account-menu.js): Auto, every account once (a signed-out one opens its sign-in), the Uses
1303
1349
  // switch of an account holding both credentials, Add account… and Manage….
@@ -1369,6 +1415,7 @@ export function createBrainPicker(host, hooks = {}) {
1369
1415
  refresh,
1370
1416
  snapshot,
1371
1417
  openField,
1418
+ switchProvider,
1372
1419
  fieldSummary,
1373
1420
  modelLabel: () => modelLabel(),
1374
1421
  mountEmptyProject,
@@ -228,10 +228,11 @@ export function renderBudgetSession(view, { t } = {}) {
228
228
  ? tr('assistant.budget.providerSpend', '{spent} spent', { spent: money(row.spentUsd) })
229
229
  : tr('assistant.budget.providerAmounts', 'Share {share} · spent {spent} · left for tasks {left}', { share: row.shareUsd == null ? '—' : money(row.shareUsd), spent: money(row.spentUsd), left: row.workerLeftUsd == null ? '—' : money(row.workerLeftUsd) })));
230
230
  if (!view.unlimited && row.isBrain && view.split) item.append(el('div', 'asst-modal-note', tr('assistant.budget.reserve', '{reserve} reserved for the brain within this share.', { reserve: money(view.split.brainReserveLeftUsd) })));
231
- if (!view.unlimited && (view.dry || []).includes(row.provider)) item.append(el('div', 'asst-budget-dry', tr('assistant.budget.dry', 'Share used up')));
232
- if (!row.eligible && row.reason) {
233
- const reason = String(row.reason).replace(/^brain:/, '');
234
- item.append(el('div', 'asst-modal-note', tr(`assistant.budget.ineligible.${reason}`, { no_models: 'No available models', signed_out: 'Signed out', limited: 'Provider usage limit reached' }[reason] || reason)));
231
+ const reason = !row.eligible && row.reason ? String(row.reason).replace(/^brain:/, '') : '';
232
+ // A provider that reports no prices (Antigravity) gets no share under a budget: nothing of it was "used up".
233
+ if (!view.unlimited && (view.dry || []).includes(row.provider) && reason !== 'unpriced') item.append(el('div', 'asst-budget-dry', tr('assistant.budget.dry', 'Share used up')));
234
+ if (reason) {
235
+ item.append(el('div', 'asst-modal-note', tr(`assistant.budget.ineligible.${reason}`, { no_models: 'No available models', signed_out: 'Signed out', limited: 'Provider usage limit reached', unpriced: 'Reports tokens, not prices' }[reason] || reason)));
235
236
  }
236
237
  providers.append(item);
237
238
  }
@@ -10,6 +10,8 @@
10
10
  // Claude → cp/cp-permissions.js { behavior, always, updatedInput, updatedPermissions, message, planDecision }
11
11
  // Codex → cdx/cdx-requests.js result:{ decision } | result:{ permissions, scope } | result:{ answers } | result:{ action, content }
12
12
  // OpenCode → ocp-v2/ocp-v2-render.js reply:'once'|'always'|'reject' | answers:[[label]]
13
+ // Antigravity (a dispatched worker's ACP session/request_permission, relayed by the broker as
14
+ // { toolName, input: <the ACP toolCall> }) → { behavior } once or deny, never "always"
13
15
  // Every response additionally carries `behavior`, `provider` and `kind` so the
14
16
  // server-side envelope translator can route without re-deriving them; Codex
15
17
  // replies repeat `answers` / `content` and OpenCode replies repeat `always` at
@@ -192,6 +194,9 @@ export function describeToolInput(toolName, input = {}) {
192
194
  function inferProvider(req, packet) {
193
195
  const explicit = normalizeProvider(packet?.provider ?? req?.provider);
194
196
  if (explicit) return explicit;
197
+ // An ACP permission request (Antigravity): a tool call with its id, options of allow_* / reject_* kinds.
198
+ if (req?.brain_native?.toolCall?.toolCallId || req?.toolCall?.toolCallId) return 'antigravity';
199
+ if (Array.isArray(req?.options) && req.options.some(o => typeof o?.optionId === 'string' && /^(allow|reject)_(once|always)$/.test(String(o?.kind || '')))) return 'antigravity';
195
200
  if (typeof req?.method === 'string' && req.method.includes('/')) return 'codex';
196
201
  if (typeof req?.brain_native?.method === 'string' && req.brain_native.method.includes('/')) return 'codex';
197
202
  if (req?.subtype === 'can_use_tool' || req?.subtype === 'permission_request' || req?.tool_name) return 'claude-code';
@@ -296,9 +301,49 @@ export function normalizeControlRequest(packet, { origin = null } = {}) {
296
301
  if (firstStr(req.tool_name, req.toolName) === 'ExitPlanMode') return normalizeClaude(req, base);
297
302
  if (provider === 'codex') return normalizeCodex(req, base);
298
303
  if (provider === 'opencode') return normalizeOpenCode(req, base);
304
+ if (provider === 'antigravity') return normalizeAntigravity(req, base);
299
305
  return normalizeClaude(req, base);
300
306
  }
301
307
 
308
+ // The path-like fields of an Antigravity tool call's arguments (lib/worker-confinement.js reads the same ones).
309
+ const ACP_PATH_KEYS = ['path', 'file_path', 'filePath', 'Path', 'AbsolutePath', 'TargetFile', 'DirectoryPath', 'SearchPath'];
310
+
311
+ /**
312
+ * An Antigravity worker's permission request: the broker relays the ACP tool call
313
+ * ({ toolCallId, title, kind, status, locations, rawInput }) as `input`. The card shows
314
+ * the call's own arguments, and its command (execute) or the paths it touches as the detail.
315
+ */
316
+ function normalizeAntigravity(req, base) {
317
+ // A worker's relay sends the ACP tool call as `input`; the brain sends the call's arguments as `input` and the
318
+ // call itself as `brain_native.toolCall`. Either way: the call, then its arguments.
319
+ const given = obj(req.input) || {};
320
+ const looksLikeCall = given.rawInput !== undefined || given.toolCallId !== undefined || Array.isArray(given.locations);
321
+ const call = obj(req.brain_native?.toolCall) || obj(req.toolCall) || (looksLikeCall ? given : {});
322
+ const args = obj(call.rawInput) || (looksLikeCall ? {} : given);
323
+ const kind = str(call.kind) || 'other';
324
+ // An MCP call names its server and tool in its arguments ({ ServerName, ToolName, Arguments }).
325
+ const mcpServer = firstStr(args.ServerName, args.serverName);
326
+ const mcpTool = firstStr(args.ToolName, args.toolName);
327
+ const mcpArgs = obj(args.Arguments) || obj(args.arguments);
328
+ const command = Array.isArray(args.command) ? args.command.map(str).join(' ') : firstStr(args.command, args.CommandLine, args.cmd);
329
+ const paths = [...new Set([
330
+ ...(Array.isArray(call.locations) ? call.locations : []).map(location => str(location?.path)),
331
+ ...ACP_PATH_KEYS.map(key => (typeof args[key] === 'string' ? str(args[key]) : '')),
332
+ ].filter(Boolean))];
333
+ let mcpDetail = '';
334
+ if (mcpServer && mcpTool && mcpArgs) { try { mcpDetail = JSON.stringify(mcpArgs).slice(0, 400); } catch { mcpDetail = ''; } }
335
+ const detail = mcpServer && mcpTool ? (mcpDetail || `${mcpServer} · ${mcpTool}`)
336
+ : kind === 'execute' && command ? command : paths.length ? paths.join('\n') : str(call.title);
337
+ return {
338
+ ...base,
339
+ kind: CONTROL_KINDS.PERMISSION,
340
+ toolName: (mcpServer && mcpTool ? `${mcpServer}_${mcpTool}` : '') || firstStr(req.toolName, req.tool_name, call.title, kind) || 'tool',
341
+ title: 'Permission required',
342
+ detail,
343
+ input: mcpServer && mcpTool && mcpArgs ? mcpArgs : args,
344
+ };
345
+ }
346
+
302
347
  function normalizeClaude(req, base) {
303
348
  const toolName = firstStr(req.tool_name, req.toolName) || 'Unknown';
304
349
  const input = obj(req.input) || {};
@@ -543,6 +588,8 @@ export function buildControlResponse(normalized, decision = {}) {
543
588
  if (normalized.kind === CONTROL_KINDS.PLAN) return buildClaude(normalized, decision, base, allow, always);
544
589
  if (normalized.provider === 'codex') return buildCodex(normalized, decision, base, allow, always);
545
590
  if (normalized.provider === 'opencode') return buildOpenCode(normalized, decision, base, allow, always);
591
+ // Antigravity: allowed once or denied; an "always" is never sent (see controlActions).
592
+ if (normalized.provider === 'antigravity') return buildClaude(normalized, decision, base, allow, false);
546
593
  return buildClaude(normalized, decision, base, allow, always);
547
594
  }
548
595
 
@@ -651,6 +698,7 @@ export function controlActions(normalized) {
651
698
  ];
652
699
  }
653
700
  const actions = [];
701
+ // Never for Antigravity: its "always" option is saved by Antigravity for later sessions too, beyond this run.
654
702
  const canAlways = n.provider === 'claude-code'
655
703
  || n.provider === 'opencode'
656
704
  || (n.provider === 'codex' && (n.method === 'item/permissions/requestApproval'
@@ -108,14 +108,22 @@ export function createAssistantHosts({
108
108
  return { host: hostId, sessionId, focused: true };
109
109
  }
110
110
 
111
- /** Map(id → session) of the sessions still open on the server (boot's saved-tab restore). */
111
+ /**
112
+ * Map(id → session) of the sessions still open on the server, or null when the list could not be
113
+ * read (a server that is starting answers before its Assistant routes exist): a host restoring its
114
+ * saved tabs from it keeps them then, instead of reading "none is open".
115
+ */
116
+ async function readLive() {
117
+ let list;
118
+ try { list = await api.list(); } catch { return null; }
119
+ return new Map((Array.isArray(list) ? list : [])
120
+ .filter(s => s?.id && isAssistantSessionLive(s))
121
+ .map(s => [s.id, s]));
122
+ }
123
+
124
+ /** Map(id → session) of the sessions still open on the server (boot's saved-tab restore); a failing server reads as none. */
112
125
  async function fetchLive() {
113
- try {
114
- const list = await api.list();
115
- return new Map((Array.isArray(list) ? list : [])
116
- .filter(s => s?.id && isAssistantSessionLive(s))
117
- .map(s => [s.id, s]));
118
- } catch { return new Map(); }
126
+ return (await readLive()) || new Map();
119
127
  }
120
128
 
121
129
  /**
@@ -302,6 +310,7 @@ export function createAssistantHosts({
302
310
  gone,
303
311
  noteFocus,
304
312
  fetchLive,
313
+ readLive,
305
314
  isLive: isAssistantSessionLive,
306
315
  lastUsed: () => lastOpen(),
307
316
  hosts: () => [...adapters.keys()],
@@ -20,7 +20,12 @@ export function createLimitDock(transcript, { t, createWait = createLimitWait, c
20
20
  host.replaceChildren();
21
21
  active = key;
22
22
  provider = next?.[1].provider;
23
- wait = next ? createWait({ t, ...next[1], onRetry: next[1].onRetry ? () => entries.get(key)?.onRetry?.() : null }) : null;
23
+ wait = next ? createWait({
24
+ t, ...next[1],
25
+ onRetry: next[1].onRetry ? () => entries.get(key)?.onRetry?.() : null,
26
+ onSwitchProvider: next[1].onSwitchProvider ? (anchor) => entries.get(key)?.onSwitchProvider?.(anchor) : null,
27
+ onClose: next[1].onClose ? () => entries.get(key)?.onClose?.() : null,
28
+ }) : null;
24
29
  if (wait) host.appendChild(wait.el);
25
30
  } else if (wait) wait.update({ resetsAt: next[1].resetsAt });
26
31
  host.hidden = !wait;