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,2543 @@
1
+ import { answerWorkerQuestion, syncWorkerQuestions } from '../worker-questions.js';
2
+ // ═══════════════════════════════════════════
3
+ // SynaBun — Antigravity side panel
4
+ // ═══════════════════════════════════════════
5
+ //
6
+ // Google Antigravity (`agy`) in a side panel, on par with the Claude Code, Codex and OpenCode panels: tabs (one ACP
7
+ // session each, in a project folder and any extra folders), streamed replies and thoughts, tool calls with their
8
+ // output, live terminal output and diffs, the agent's plan, permission and question cards, model / thinking / mode
9
+ // pickers and every other option the session offers (Session settings), the agent's own slash commands beside the
10
+ // panel's, @ files, attachments (paste, drop, pick, voice), a queue while a turn runs, Copy and Retry, the session
11
+ // list (resume, rename, fork and delete where the agent supports them), three output detail levels, notifications,
12
+ // and the tab a dispatched run of the Assistant shows itself in. When the CLI is missing the panel installs it
13
+ // (Homebrew's cask or Google's installer, through /api/cli/install), walks through sign-in and connects.
14
+ // The server end is /ws/antigravity (lib/antigravity/panel-bridge.js).
15
+ //
16
+ // What decides lives in DOM-free modules (agy-events.js, agy-options.js, agy-tabs-logic.js, agy-commands.js,
17
+ // agy-setup.js, agy-diff.js, agy-client.js) and the HTML in agy-render.js; this file wires. Every string is
18
+ // agyText() (agy-copy.js), every dropdown the app's .cc-dropdown.
19
+
20
+ import { AGY_COPY, agyText, setAntigravityTranslator } from './agy-copy.js';
21
+ import { injectAntigravityStyles } from './agy-styles.js';
22
+ import { createAntigravityClient } from './agy-client.js';
23
+ import {
24
+ addUserMessage, answerOfTurn, applyTerminalOutput, applyUpdate, contextGauge, createTranscript, endTurn, lastPrompt,
25
+ settleOpenTools, turnFooters,
26
+ } from './agy-events.js';
27
+ import {
28
+ dropdownEntries, elicitationContent, elicitationFields, modeEntries, optionByCategory, permissionCard, quickOption,
29
+ secondaryOptions, toggledValue, withValue,
30
+ } from './agy-options.js';
31
+ import {
32
+ attachmentProblem, attachmentType, automationEvent, MAX_TABS, newTab, openSessionRequest, promptRequest, restoreTabs, runGeminiDir, serializeTabs, sessionLoadNotice, stagedPicture, tabLabel,
33
+ } from './agy-tabs-logic.js';
34
+ import { applyMention, DETAIL_LEVELS, joinProjectPath, mentionAt, PANEL_COMMANDS, relativeToProject, resolveSlash, slashInsert, slashMenu } from './agy-commands.js';
35
+ import { blockedReasonKey, setupView } from './agy-setup.js';
36
+ import { diffView } from './agy-diff.js';
37
+ import { autoAnswerRow, bypassControl, pickPermission } from './agy-bypass.js';
38
+ import {
39
+ contextWords, elicitationHtml, emptyHtml, esc, ICONS, permissionHtml, rowHtml, setupHtml, turnFooterHtml,
40
+ } from './agy-render.js';
41
+ import { t } from '../i18n.js';
42
+ import { storage } from '../storage.js';
43
+ import { state, emit, on } from '../state.js';
44
+ import { registerSidepanel, setSidepanelVisible, sidepanelAcceptsKeyboard, focusSidepanel, openAnotherSidepanelSession, getSidepanelPresentation, isHostedSessionFocused } from '../ui-sidepanel-windows.js';
45
+ import { hostedSidepanelId } from '../ui-sidepanel-runtime.js';
46
+ import { dropdownHTML, setDropdownDisabled, setDropdownItems, wireDropdowns, closeDropdowns } from '../cc-dropdown.js';
47
+ import { renderMarkdown } from '../cp/cp-markdown.js';
48
+ import { fetchProjects } from '../api.js';
49
+ import { createCliInstallController } from '../cli-install-controller.js';
50
+ import { setCliInstallTranslator } from '../cli-install-view.js';
51
+ import { workerRestart } from '../worker-restart.js';
52
+ import { endWorkerRunOnClose, forgetWorkerEnd, trackWorkerEnd } from '../ui-worker-end.js';
53
+ import { nativeLoopWindowId } from '../ui-native-window-id.js';
54
+ import { runAwaitsAnswer } from '../sidepanel-session-status.js';
55
+ import { wireFileLinks } from '../ui-file-links.js';
56
+ import { notify, NOTIF_TYPE } from '../ui-notifications.js';
57
+ import { forgetGreeting, mountGreeting } from '../greeting/greeting-card.js';
58
+
59
+ export const PANEL_OWNER = 'antigravity-sidepanel';
60
+ const ICON = ICONS.mark;
61
+ const SVG = {
62
+ plus: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M12 5v14M5 12h14"/></svg>',
63
+ minus: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M5 12h14"/></svg>',
64
+ close: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M6 6l12 12M18 6 6 18"/></svg>',
65
+ cog: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M12 2v3M12 19v3M4.2 4.2l2.1 2.1M17.7 17.7l2.1 2.1M2 12h3M19 12h3M4.2 19.8l2.1-2.1M17.7 6.3l2.1-2.1"/></svg>',
66
+ edit: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20h9"/><path d="M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4z"/></svg>',
67
+ clip: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="m21.4 11.1-9.2 9.2a6 6 0 0 1-8.5-8.5l9.2-9.2a4 4 0 0 1 5.7 5.7l-9.2 9.2a2 2 0 0 1-2.8-2.8l8.5-8.5"/></svg>',
68
+ send: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 19V5M5 12l7-7 7 7"/></svg>',
69
+ stop: '<svg viewBox="0 0 24 24" fill="currentColor" stroke="none"><rect x="7" y="7" width="10" height="10" rx="2"/></svg>',
70
+ queue: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><path d="M5 7h14M5 12h14M5 17h9"/></svg>',
71
+ mic: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6"><rect x="9" y="2" width="6" height="12" rx="3"/><path d="M5 11a7 7 0 0 0 14 0M12 18v4M8 22h8"/></svg>',
72
+ folder: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><path d="M12 11v5M9.5 13.5h5"/></svg>',
73
+ trash: '<svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M3 6h18M8 6V4h8v2M19 6l-1 14H6L5 6"/></svg>',
74
+ };
75
+
76
+ // ── Window identity and storage ──
77
+ export const windowId = hostedSidepanelId || sessionStorage.getItem('agy-window-id') || (() => {
78
+ const id = globalThis.crypto?.randomUUID?.() || `agy-${Date.now()}`;
79
+ sessionStorage.setItem('agy-window-id', id);
80
+ return id;
81
+ })();
82
+ const STOR = {
83
+ tabs: `synabun-antigravity-panel-tabs-${windowId}`,
84
+ project: 'synabun-antigravity-panel-project',
85
+ prefs: 'synabun-antigravity-panel-prefs',
86
+ names: 'synabun-antigravity-session-names',
87
+ };
88
+ const MAX_QUEUE = 20;
89
+ const MAX_FOLDERS = 8;
90
+
91
+ let _panel = null;
92
+ let _visible = false;
93
+ let _client = null;
94
+ let _tabs = [];
95
+ let _activeIdx = 0;
96
+ let _projects = [];
97
+ let _marked = null;
98
+ let _pop = null; // { el, kind, anchor, cleanup }
99
+ let _renderQueued = false;
100
+ let _hints = null; // { kind: 'slash' | 'mention', items, active, mention }
101
+ let _mentionSeq = 0;
102
+ let _recorder = null;
103
+ let _workingTimer = null;
104
+ const _mdCache = new Map();
105
+ const _runtime = new Map(); // tab id → see runtimeOf
106
+ const _setup = { controller: null, view: null, ran: false, connecting: false, dismissed: false, nudge: false, logOpen: false };
107
+ const _conn = { phase: 'idle', reason: null, message: '', waitMs: 0, opened: false };
108
+
109
+ const readJson = (key, fallback) => { try { const value = JSON.parse(storage.getItem(key) || 'null'); return value ?? fallback; } catch { return fallback; } };
110
+ let _prefs = { detail: 'standard', clientFs: false, clientTerminal: false, ...readJson(STOR.prefs, {}) };
111
+ if (!DETAIL_LEVELS.includes(_prefs.detail)) _prefs.detail = 'standard';
112
+ let _names = readJson(STOR.names, {});
113
+ if (!_names || typeof _names !== 'object' || Array.isArray(_names)) _names = {};
114
+
115
+ const $ = (selector) => _panel?.querySelector(selector) || null;
116
+ const activeTab = () => _tabs[_activeIdx] || null;
117
+ const tabBySession = (sessionId) => _tabs.find((tab) => tab.sessionId === sessionId) || null;
118
+ const T = agyText;
119
+
120
+ function runtimeOf(tab) {
121
+ if (!tab) return null;
122
+ if (!_runtime.has(tab.id)) {
123
+ _runtime.set(tab.id, {
124
+ transcript: createTranscript(), configOptions: [], modes: null, models: null, permissions: new Map(), elicitations: new Map(),
125
+ staged: [], loaded: false, openedInPanel: false, loading: null, creating: null, auth: null, notice: null, scrolledUp: false,
126
+ opened: new Set(), closed: new Set(), fullDiffs: new Set(), folds: new Map(), queue: [], queuePaused: false, elicitDrafts: {},
127
+ additionalDirectories: null,
128
+ // The greeting (offerGreeting): `fresh` a session this page started (it has no history of its own),
129
+ // `greeting` the one on screen until the first prompt carries its id.
130
+ fresh: false, greeting: null,
131
+ });
132
+ }
133
+ return _runtime.get(tab.id);
134
+ }
135
+
136
+ function savePrefs() { try { storage.setItem(STOR.prefs, JSON.stringify(_prefs)); } catch { /* the choice stays for this page */ } }
137
+ function saveNames() { try { storage.setItem(STOR.names, JSON.stringify(_names)); } catch { /* storage full */ } }
138
+
139
+ /** The tab's title: a name given here, the session's own title, else a short id. */
140
+ function titleOf(tab) {
141
+ const named = tab?.sessionId ? _names[tab.sessionId] : '';
142
+ return tabLabel({ ...tab, title: tab?.titleByUser || named || tab?.title || null }, { newSession: T('newSession'), run: T('automation') });
143
+ }
144
+
145
+ // What the agent can do, from the hello (the bridge's sanitized agentCapabilities, or the older raw fields).
146
+ function agentCaps() {
147
+ const hello = _client?.hello || {};
148
+ const caps = hello.agentCapabilities || {};
149
+ const session = caps.sessionCapabilities || hello.sessionCapabilities || {};
150
+ const has = (key) => !!session && session[key] !== undefined && session[key] !== null && session[key] !== false;
151
+ const supports = (capability) => !!_client?.supports(capability);
152
+ return {
153
+ resume: has('resume') && supports('session-resume'),
154
+ fork: has('fork') && supports('session-fork'),
155
+ remove: has('delete') && supports('session-delete'),
156
+ list: has('list') || supports('session-list'),
157
+ folders: has('additionalDirectories') && supports('additional-directories'),
158
+ logout: !!(caps.auth?.logout ?? hello.agentCapabilities?.auth?.logout) || supports('logout') || supports('auth'),
159
+ audio: !!(caps.promptCapabilities?.audio ?? hello.promptCapabilities?.audio) && supports('audio'),
160
+ image: !!(caps.promptCapabilities?.image ?? hello.promptCapabilities?.image),
161
+ restart: supports('agent-restart'),
162
+ clientFs: supports('client-fs'),
163
+ clientTerminal: supports('client-terminal'),
164
+ elicitation: supports('elicitation'),
165
+ bypass: supports('bypass'),
166
+ };
167
+ }
168
+ const capAvailable = (need) => (need === 'fork' ? agentCaps().fork : need === 'logout' ? agentCaps().logout : true);
169
+
170
+ // ── Markdown (the vendored marked, as the other panels load it) ──
171
+ function ensureMarked() {
172
+ if (_marked || typeof window === 'undefined') return;
173
+ import('/vendor/marked/marked-14.1.4.esm.js')
174
+ .then((mod) => {
175
+ _marked = mod?.marked || mod?.default || null;
176
+ _marked?.setOptions?.({ breaks: true, gfm: true });
177
+ _mdCache.clear();
178
+ scheduleRender();
179
+ })
180
+ .catch(() => {});
181
+ }
182
+ function md(text) {
183
+ const key = String(text ?? '');
184
+ const hit = _mdCache.get(key);
185
+ if (hit !== undefined) return hit;
186
+ const html = renderMarkdown(key, { parse: _marked ? (src) => _marked.parse(src) : null });
187
+ _mdCache.set(key, html);
188
+ if (_mdCache.size > 400) _mdCache.delete(_mdCache.keys().next().value);
189
+ return html;
190
+ }
191
+
192
+ // ── Saving ──
193
+ function saveTabs() {
194
+ try { storage.setItem(STOR.tabs, JSON.stringify(serializeTabs(_tabs, _activeIdx))); } catch { /* storage full: the tabs stay in this page */ }
195
+ renderPills();
196
+ }
197
+ function loadTabs() {
198
+ const { tabs, activeIdx } = restoreTabs(storage.getItem(STOR.tabs));
199
+ _tabs = tabs;
200
+ const pendingTabId = globalThis.__synabunPendingTab?.provider === 'antigravity'
201
+ ? String(globalThis.__synabunPendingTab.tabId || '')
202
+ : '';
203
+ const pendingIdx = pendingTabId ? _tabs.findIndex((tab) => String(tab.id) === pendingTabId) : -1;
204
+ _activeIdx = pendingIdx >= 0 ? pendingIdx : activeIdx;
205
+ }
206
+
207
+ // ── The socket ──
208
+ function helloPayload() {
209
+ return { clientCapabilities: { fs: !!_prefs.clientFs, terminal: !!_prefs.clientTerminal } };
210
+ }
211
+
212
+ function client() {
213
+ if (_client) return _client;
214
+ _client = createAntigravityClient({ helloPayload });
215
+ _client.on('session:update', ({ sessionId, update, replay }) => {
216
+ const tab = tabBySession(sessionId);
217
+ if (!tab) return;
218
+ const rt = runtimeOf(tab);
219
+ const changed = applyUpdate(rt.transcript, update, { replay });
220
+ if (update?.sessionUpdate === 'config_option_update' && Array.isArray(update.configOptions)) { rt.configOptions = update.configOptions; renderPickers(); }
221
+ if (update?.sessionUpdate === 'current_mode_update' && rt.modes) { rt.modes = { ...rt.modes, currentModeId: update.currentModeId }; renderPickers(); }
222
+ if (update?.sessionUpdate === 'session_info_update' && rt.transcript.title && rt.transcript.title !== tab.title) { tab.title = rt.transcript.title; saveTabs(); renderTabs(); renderHeader(); }
223
+ if (update?.sessionUpdate === 'usage_update') renderCog();
224
+ if (changed) scheduleRender(tab);
225
+ });
226
+ _client.on('permission:request', (message) => {
227
+ const tab = tabBySession(message.sessionId);
228
+ if (!tab) return;
229
+ const rt = runtimeOf(tab);
230
+ rt.permissions.set(message.requestId, { ...message, card: permissionCard(message), state: 'open' });
231
+ tab.needsYou = true;
232
+ saveTabs();
233
+ renderTabs();
234
+ scheduleRender(tab, { follow: true });
235
+ notifyAsk(tab);
236
+ });
237
+ // Bypass answered a request itself (lib/antigravity/bypass.js): a line in the transcript says what and why.
238
+ _client.on('permission:auto', (message) => {
239
+ const tab = tabBySession(message.sessionId);
240
+ if (!tab) return;
241
+ const row = autoAnswerRow(message, T);
242
+ runtimeOf(tab).transcript.rows.push({ id: `b${message.requestId || Date.now()}`, kind: 'notice', text: row.text, severity: row.severity });
243
+ scheduleRender(tab, { follow: true });
244
+ });
245
+ _client.on('permission:resolved', ({ requestId, sessionId, outcome }) => {
246
+ const tab = tabBySession(sessionId);
247
+ if (!tab) return;
248
+ const rt = runtimeOf(tab);
249
+ const entry = rt.permissions.get(requestId);
250
+ if (entry && (entry.state === 'open' || entry.state === 'answering')) entry.state = outcome?.outcome === 'cancelled' ? 'cancelled' : 'answered';
251
+ refreshNeedsYou(tab);
252
+ scheduleRender(tab);
253
+ });
254
+ _client.on('elicitation:request', (message) => {
255
+ // A question tied to no session (one asked during sign-in) is shown in the active tab.
256
+ const tab = message.sessionId ? tabBySession(message.sessionId) : activeTab();
257
+ if (!tab) return;
258
+ runtimeOf(tab).elicitations.set(message.requestId, { ...message, state: 'open', errors: {} });
259
+ tab.needsYou = true;
260
+ saveTabs();
261
+ renderTabs();
262
+ scheduleRender(tab, { follow: true });
263
+ notifyAsk(tab);
264
+ });
265
+ _client.on('elicitation:resolved', ({ requestId, sessionId, action }) => {
266
+ const tab = tabBySession(sessionId) || _tabs.find((entry) => runtimeOf(entry).elicitations.has(requestId));
267
+ if (!tab) return;
268
+ const entry = runtimeOf(tab).elicitations.get(requestId);
269
+ if (entry && entry.state !== 'answered' && entry.state !== 'declined') entry.state = action === 'accept' || action === 'complete' ? 'answered' : action === 'decline' ? 'declined' : 'cancelled';
270
+ refreshNeedsYou(tab);
271
+ scheduleRender(tab);
272
+ });
273
+ _client.on('terminal:output', (message) => {
274
+ const tab = tabBySession(message.sessionId);
275
+ if (!tab) return;
276
+ if (applyTerminalOutput(runtimeOf(tab).transcript, message)) scheduleRender(tab);
277
+ });
278
+ _client.on('turn:end', ({ sessionId, stopReason, usage, error }) => {
279
+ const tab = tabBySession(sessionId);
280
+ if (!tab) return;
281
+ const rt = runtimeOf(tab);
282
+ if (!rt.transcript.running && !tab.running) return;
283
+ endTurn(rt.transcript, { stopReason, usage, error });
284
+ settleOpenTools(rt.transcript);
285
+ tab.running = false;
286
+ // The turn is over: its answered requests go (the tool cards say what happened); one still open was cancelled.
287
+ for (const [id, entry] of rt.permissions) if (entry.state !== 'open') rt.permissions.delete(id); else entry.state = 'cancelled';
288
+ for (const [id, entry] of rt.elicitations) if (entry.state !== 'open') rt.elicitations.delete(id); else entry.state = 'cancelled';
289
+ refreshNeedsYou(tab);
290
+ scheduleRender(tab);
291
+ notifyOutcome(tab, stopReason === 'error' ? NOTIF_TYPE.ERROR : NOTIF_TYPE.DONE);
292
+ drainQueue(tab);
293
+ });
294
+ _client.on('agent:exit', ({ error, signal, code, geminiDir = null }) => {
295
+ for (const tab of _tabs) {
296
+ if (geminiDir ? tab.geminiDir !== geminiDir : !!tab.geminiDir) continue;
297
+ const rt = runtimeOf(tab);
298
+ rt.loaded = false;
299
+ if (tab.running && !tab.automationRunId) {
300
+ endTurn(rt.transcript, { stopReason: 'error', error: { message: T('agentStopped', { detail: error ? `: ${error}` : signal ? ` (${signal})` : code ? ` (exit ${code})` : '' }) } });
301
+ settleOpenTools(rt.transcript);
302
+ tab.running = false;
303
+ }
304
+ for (const entry of rt.permissions.values()) if (entry.state === 'open') entry.state = 'cancelled';
305
+ for (const entry of rt.elicitations.values()) if (entry.state === 'open') entry.state = 'cancelled';
306
+ tab.needsYou = false;
307
+ }
308
+ saveTabs();
309
+ renderTabs();
310
+ scheduleRender();
311
+ });
312
+ _client.on('disconnected', ({ opened } = {}) => {
313
+ for (const tab of _tabs) runtimeOf(tab).loaded = false;
314
+ if (opened) _conn.phase = 'retrying';
315
+ else if (_conn.phase !== 'blocked') _conn.phase = _conn.opened ? 'retrying' : 'connecting';
316
+ renderBanner();
317
+ renderComposer();
318
+ });
319
+ _client.on('retrying', ({ waitMs }) => { if (_conn.phase !== 'blocked') { _conn.phase = 'retrying'; _conn.waitMs = waitMs; renderBanner(); } });
320
+ _client.on('offline', () => { _conn.phase = 'offline'; renderBanner(); });
321
+ _client.on('blocked', ({ reason, message }) => {
322
+ _conn.phase = 'blocked';
323
+ _conn.reason = reason;
324
+ _conn.message = message || '';
325
+ renderBanner();
326
+ renderComposer();
327
+ });
328
+ _client.on('connected', (hello) => {
329
+ _conn.phase = 'connected';
330
+ _conn.opened = true;
331
+ _conn.reason = null;
332
+ onHello(hello);
333
+ });
334
+ return _client;
335
+ }
336
+
337
+ // The hello says whether `agy acp` runs. A missing CLI is the setup card's (tool-versions says so too); any other
338
+ // failure to start is "failed to start" with its words. Never a second notice in the transcript.
339
+ function onHello(hello) {
340
+ const ctl = setupController();
341
+ if (hello?.running === false && hello?.cliMissing) {
342
+ if (ctl && ctl.view().kind === 'hidden') ctl.recheck();
343
+ } else if (hello?.running === false && hello?.error?.message) {
344
+ ctl?.flagFailure(hello.error.message);
345
+ }
346
+ if (hello?.running) _setup.connecting = false;
347
+ renderBanner();
348
+ renderAll();
349
+ // A session that was open before the socket dropped (or before an install) comes back.
350
+ const tab = activeTab();
351
+ if (hello?.running && tab && !tab.automationRunId && (tab.sessionId ? !runtimeOf(tab).loaded : _setup.ran)) {
352
+ ensureSession(tab).catch(() => {});
353
+ }
354
+ }
355
+
356
+ function refreshNeedsYou(tab) {
357
+ const rt = runtimeOf(tab);
358
+ tab.needsYou = [...rt.permissions.values()].some((p) => p.state === 'open') || [...rt.elicitations.values()].some((e) => e.state === 'open');
359
+ saveTabs();
360
+ renderTabs();
361
+ }
362
+
363
+ // ── Notifications (Settings → Notifications decides which reach the user) ──
364
+ function seenNow(tab) {
365
+ return _visible && tab === activeTab() && !document.hidden;
366
+ }
367
+ function notifyOutcome(tab, type) {
368
+ if (tab.automationRunId || seenNow(tab)) return;
369
+ try { notify('panel', type, titleOf(tab), { panel: 'antigravity', provider: 'antigravity', tabId: tab.id }); } catch { /* notifications are optional */ }
370
+ }
371
+ function notifyAsk(tab) {
372
+ if (tab.automationRunId || seenNow(tab)) return;
373
+ try { notify('panel', NOTIF_TYPE.ASK, titleOf(tab), { panel: 'antigravity', provider: 'antigravity', tabId: tab.id }); } catch { /* optional */ }
374
+ }
375
+
376
+ // ── The CLI and sign-in (the setup card) ──
377
+ function setupController() {
378
+ if (_setup.controller || typeof window === 'undefined') return _setup.controller;
379
+ setCliInstallTranslator((key, params) => { try { return t(`cliInstall.${key}`, params); } catch { return key; } });
380
+ try {
381
+ _setup.controller = createCliInstallController({
382
+ toolKey: 'antigravity',
383
+ onChange: (view) => {
384
+ const before = _setup.view?.kind;
385
+ _setup.view = view;
386
+ // The CLI became usable after it was missing or broken: start the agent and go on to sign-in.
387
+ if (view.kind === 'hidden' || view.kind === 'done' || view.kind === 'path') {
388
+ if (before && ['missing', 'installing', 'failed', 'broken'].includes(before)) connectAfterInstall();
389
+ }
390
+ scheduleRender();
391
+ renderComposer();
392
+ },
393
+ });
394
+ } catch { _setup.controller = null; }
395
+ return _setup.controller;
396
+ }
397
+
398
+ function currentSetup() {
399
+ if (_setup.dismissed && !(_setup.view?.blocked)) return { show: false };
400
+ const tab = activeTab();
401
+ const rt = runtimeOf(tab);
402
+ return setupView({
403
+ cli: _setup.view,
404
+ hello: _client?.hello || null,
405
+ auth: rt?.auth || null,
406
+ installed: !!(_client?.hello?.running),
407
+ ran: _setup.ran,
408
+ connecting: _setup.connecting,
409
+ });
410
+ }
411
+
412
+ async function connectAfterInstall() {
413
+ _setup.connecting = true;
414
+ // A restarted agent has none of this socket's sessions open: every tab opens its own again.
415
+ for (const tab of _tabs) runtimeOf(tab).loaded = false;
416
+ scheduleRender();
417
+ try {
418
+ if (agentCaps().restart) await client().refreshHello('agent:restart', helloPayload());
419
+ else await client().refreshHello('hello', helloPayload());
420
+ } catch { /* the hello's own result says what happened */ }
421
+ finally {
422
+ _setup.connecting = false;
423
+ scheduleRender();
424
+ }
425
+ }
426
+
427
+ async function runSetupAction(action) {
428
+ const ctl = setupController();
429
+ if (!ctl) return;
430
+ if (action === 'install') {
431
+ _setup.ran = true;
432
+ _setup.dismissed = false;
433
+ const final = await ctl.install();
434
+ if (final?.ok) await connectAfterInstall();
435
+ return;
436
+ }
437
+ if (action === 'recheck') { await ctl.recheck(); if (_client?.hello && !_client.hello.running) connectAfterInstall(); return; }
438
+ if (action === 'path') { await ctl.addPath(); return; }
439
+ if (action === 'dismiss') { _setup.dismissed = true; _setup.ran = false; ctl.clearDone(); scheduleRender(); return; }
440
+ if (action === 'copy') {
441
+ const command = currentSetup().command;
442
+ if (command) { try { await navigator.clipboard.writeText(command); flashCopied($('.agy-command-copy span')); } catch { /* no clipboard */ } }
443
+ return;
444
+ }
445
+ if (action === 'signin-check') {
446
+ const tab = activeTab();
447
+ const rt = runtimeOf(tab);
448
+ if (rt) rt.auth = null;
449
+ scheduleRender();
450
+ try { await ensureSession(tab); } catch { /* ensureSession set rt.auth again when it is still needed */ }
451
+ }
452
+ }
453
+
454
+ function flashCopied(el) {
455
+ if (!el) return;
456
+ const before = el.textContent;
457
+ el.textContent = T('copied');
458
+ setTimeout(() => { if (el.isConnected) el.textContent = before; }, 1400);
459
+ }
460
+
461
+ // ── Sessions ──
462
+ async function ensureSession(tab, { load = true } = {}) {
463
+ if (!tab || tab.automationRunId) return null;
464
+ const rt = runtimeOf(tab);
465
+ if (tab.sessionId && rt.loaded) return tab.sessionId;
466
+ const folders = foldersOf(tab);
467
+ if (tab.sessionId && load) {
468
+ if (!rt.loading) {
469
+ rt.loading = (async () => {
470
+ // A transcript already on screen that this panel opened (the socket dropped, the agent restarted):
471
+ // resume it without a replay where the agent can; a finished dispatched run's first takeover loads from its store.
472
+ const resume = rt.openedInPanel && rt.transcript.rows.length > 0 && agentCaps().resume;
473
+ const previousTranscript = rt.transcript;
474
+ if (!resume) rt.transcript = createTranscript();
475
+ rt.notice = resume ? null : { text: T('loadingHistory'), severity: 'info' };
476
+ scheduleRender(tab);
477
+ try {
478
+ if (!tab.geminiDir && tab.releasedRunId && typeof fetch === 'function') {
479
+ try {
480
+ const res = await fetch(`/api/sidepanel-runs/${encodeURIComponent(tab.releasedRunId)}`);
481
+ if (res.ok) {
482
+ const home = runGeminiDir((await res.json())?.run);
483
+ if (home) { tab.geminiDir = home; saveTabs(); }
484
+ }
485
+ } catch { /* best effort */ }
486
+ }
487
+ const req = openSessionRequest(tab, { resume, folders });
488
+ const answer = await client().request(req.type, req.payload);
489
+ rt.configOptions = answer.configOptions || [];
490
+ rt.modes = answer.modes || null;
491
+ rt.models = answer.models || null;
492
+ rt.loaded = true;
493
+ rt.openedInPanel = true;
494
+ rt.notice = null;
495
+ rt.auth = null;
496
+ } catch (error) {
497
+ if (!resume && previousTranscript.rows.length) rt.transcript = previousTranscript;
498
+ rt.notice = sessionLoadNotice(error, T);
499
+ if (error.authRequired) rt.auth = { state: 'needed' };
500
+ throw error;
501
+ } finally {
502
+ rt.loading = null;
503
+ scheduleRender(tab);
504
+ renderPickers();
505
+ }
506
+ })();
507
+ }
508
+ await rt.loading;
509
+ return tab.sessionId;
510
+ }
511
+ if (!tab.project) throw Object.assign(new Error(T('noProject')), { code: 'NO_PROJECT' });
512
+ if (!rt.creating) {
513
+ rt.creating = (async () => {
514
+ try {
515
+ const answer = await client().request('session:new', { cwd: tab.project, tabId: tab.id, ...bypassParam(tab), ...(folders.length ? { additionalDirectories: folders } : {}) });
516
+ tab.sessionId = answer.sessionId;
517
+ tab.geminiDir = null;
518
+ tab.releasedRunId = null;
519
+ rt.fresh = true;
520
+ if (tab.titleByUser) { _names[answer.sessionId] = tab.titleByUser; saveNames(); }
521
+ rt.configOptions = answer.configOptions || [];
522
+ rt.modes = answer.modes || null;
523
+ rt.models = answer.models || null;
524
+ rt.loaded = true;
525
+ rt.openedInPanel = true;
526
+ rt.auth = null;
527
+ if (_setup.ran) { _setup.ran = false; }
528
+ saveTabs();
529
+ renderPickers();
530
+ renderHeader();
531
+ scheduleRender(tab);
532
+ return answer.sessionId;
533
+ } catch (error) {
534
+ if (error.authRequired) { rt.auth = { state: 'needed' }; scheduleRender(tab); }
535
+ throw error;
536
+ } finally {
537
+ rt.creating = null;
538
+ }
539
+ })();
540
+ }
541
+ return rt.creating;
542
+ }
543
+
544
+ function foldersOf(tab) {
545
+ return (Array.isArray(tab?.additionalDirectories) ? tab.additionalDirectories : []).filter((p) => typeof p === 'string' && p && p !== tab.project).slice(0, MAX_FOLDERS);
546
+ }
547
+
548
+ function blockedNow() {
549
+ const setup = currentSetup();
550
+ if (setup.show && ['missing', 'installing', 'failed', 'broken', 'connecting'].includes(setup.stage)) return blockedReasonKey(setup) || 'blockedMissing';
551
+ if (_conn.phase === 'blocked') return 'blockedConnection';
552
+ return '';
553
+ }
554
+
555
+ async function send({ text: given = null, staged: givenStaged = null, fromQueue = false } = {}) {
556
+ const tab = activeTab();
557
+ if (!tab || tab.automationActive) return;
558
+ const input = $('.agy-input');
559
+ const text = given ?? (input?.value || '');
560
+ const rt = runtimeOf(tab);
561
+ const staged = givenStaged ?? rt.staged;
562
+ if (!text.trim() && !staged.length) return;
563
+
564
+ // The panel's own commands run here.
565
+ if (!fromQueue && givenStaged === null) {
566
+ const resolved = resolveSlash(text, { agentCommands: rt.transcript.commands, available: capAvailable });
567
+ if (resolved.kind === 'panel' && !staged.length) {
568
+ if (input) { input.value = ''; autosize(); }
569
+ tab.draft = '';
570
+ hideHints();
571
+ await runPanelCommand(resolved.name, resolved.args);
572
+ renderComposer();
573
+ return;
574
+ }
575
+ }
576
+
577
+ const blocked = blockedNow();
578
+ if (blocked) { nudgeSetup(); return; }
579
+
580
+ // While a turn runs (or a send is still starting its session), a message waits its turn in the queue.
581
+ if ((tab.running || rt.sending) && !fromQueue) {
582
+ if (rt.queue.length >= MAX_QUEUE) { rt.notice = { text: T('queueFull', { count: MAX_QUEUE }), severity: 'warning' }; scheduleRender(tab); return; }
583
+ rt.queue.push({ text, staged: staged.splice(0) });
584
+ if (input) { input.value = ''; autosize(); }
585
+ tab.draft = '';
586
+ hideHints();
587
+ renderQueue();
588
+ renderAttachments();
589
+ renderComposer();
590
+ return;
591
+ }
592
+
593
+ // A dispatched run's tab after its run ended: the conversation goes on here, in this window's own Antigravity
594
+ // (its history is Antigravity's, so session/load brings it back), and the run is let go.
595
+ let releasedRunId = null;
596
+ if (tab.automationRunId) {
597
+ releasedRunId = tab.automationRunId;
598
+ tab.releasedRunId = releasedRunId;
599
+ tab.automationRunId = null;
600
+ tab.automationAwaits = false;
601
+ rt.loaded = false;
602
+ rt.openedInPanel = false;
603
+ saveTabs();
604
+ window.dispatchEvent(new CustomEvent('native-loop:dismissed', { detail: { runId: releasedRunId } }));
605
+ }
606
+ // The message is taken now, before the first await: starting a session after a reload spawns agy and takes
607
+ // seconds, and an Enter pressed again meanwhile must not send it a second time (agy folds both into one turn).
608
+ const files = givenStaged ? staged : rt.staged.splice(0);
609
+ const clearedInput = !fromQueue && given === null && !!input;
610
+ if (clearedInput) { input.value = ''; autosize(); }
611
+ if (!fromQueue) tab.draft = '';
612
+ rt.sending = true;
613
+ renderAttachments();
614
+ try {
615
+ if (releasedRunId) {
616
+ try { await endWorkerRunOnClose(releasedRunId, () => {}, { busy: false }); } catch { /* best effort */ }
617
+ forgetWorkerEnd(releasedRunId);
618
+ }
619
+ await ensureSession(tab);
620
+ } catch (error) {
621
+ rt.sending = false;
622
+ if (!error.authRequired) rt.notice = error?.code === 'HOME_PRUNED' ? sessionLoadNotice(error, T) : { text: T('errorPrefix', { message: error.message }), severity: 'error' };
623
+ if (fromQueue) rt.queue.unshift({ text, staged: files });
624
+ else {
625
+ // Nothing was sent: the words and files go back where they were.
626
+ if (!givenStaged) rt.staged.unshift(...files);
627
+ if (clearedInput && !input.value) { input.value = text; tab.draft = text; autosize(); }
628
+ renderAttachments();
629
+ }
630
+ renderComposer();
631
+ scheduleRender(tab);
632
+ return;
633
+ }
634
+ rt.sending = false;
635
+ addUserMessage(rt.transcript, text, files.map((file) => ({ kind: file.mimeType?.startsWith('image/') ? 'image' : 'file', name: file.name, mimeType: file.mimeType, preview: file.mimeType?.startsWith('image/') ? file.dataUrl : '' })));
636
+ rt.transcript.running = true;
637
+ rt.notice = null;
638
+ tab.running = true;
639
+ tab.startedAt = Date.now();
640
+ hideHints();
641
+ saveTabs();
642
+ renderTabs();
643
+ renderAttachments();
644
+ scheduleRender(tab, { follow: true });
645
+ startWorkingClock();
646
+ try {
647
+ const greetingId = takeGreetingId(tab);
648
+ await client().request('prompt', { ...promptRequest(tab, text, files), ...(greetingId ? { greetingId } : {}) });
649
+ } catch (error) {
650
+ if (tab.running) {
651
+ endTurn(rt.transcript, { stopReason: 'error', error: { message: error.message } });
652
+ settleOpenTools(rt.transcript);
653
+ tab.running = false;
654
+ if (error.authRequired) rt.auth = { state: 'needed' };
655
+ saveTabs();
656
+ renderTabs();
657
+ scheduleRender(tab);
658
+ }
659
+ }
660
+ }
661
+
662
+ function drainQueue(tab) {
663
+ const rt = runtimeOf(tab);
664
+ if (!rt.queue.length || rt.queuePaused || tab.running) { renderQueue(); return; }
665
+ const next = rt.queue.shift();
666
+ renderQueue();
667
+ if (tab === activeTab()) send({ text: next.text, staged: next.staged, fromQueue: true });
668
+ else rt.queue.unshift(next);
669
+ }
670
+
671
+ async function stop() {
672
+ const tab = activeTab();
673
+ if (!tab?.sessionId || !tab.running) return;
674
+ try { await client().request('cancel', { sessionId: tab.sessionId }); } catch { /* the turn ends either way */ }
675
+ }
676
+
677
+ async function answerPermission(tab, requestId, optionId) {
678
+ const rt = runtimeOf(tab);
679
+ const entry = rt.permissions.get(requestId);
680
+ if (!entry || entry.state !== 'open') return;
681
+ entry.state = 'answering';
682
+ scheduleRender(tab);
683
+ try {
684
+ await client().request('permission:answer', { requestId, optionId });
685
+ } catch (error) {
686
+ entry.state = 'open';
687
+ rt.notice = { text: T('errorPrefix', { message: error.message }), severity: 'error' };
688
+ }
689
+ refreshNeedsYou(tab);
690
+ scheduleRender(tab);
691
+ }
692
+
693
+ async function answerElicitation(tab, requestId, action, content = undefined) {
694
+ const rt = runtimeOf(tab);
695
+ const entry = rt.elicitations.get(requestId);
696
+ if (!entry || entry.state !== 'open') return;
697
+ try {
698
+ if (tab.automationRunId) await answerWorkerQuestion(tab.automationRunId, requestId, { behavior: action === 'accept' ? 'allow' : 'deny', action, content });
699
+ else await client().request('elicitation:answer', { requestId, action, ...(content ? { content } : {}) });
700
+ entry.state = action === 'accept' ? 'answered' : action === 'decline' ? 'declined' : 'cancelled';
701
+ delete rt.elicitDrafts[requestId];
702
+ } catch (error) {
703
+ rt.notice = { text: T('errorPrefix', { message: error.message }), severity: 'error' };
704
+ }
705
+ refreshNeedsYou(tab);
706
+ scheduleRender(tab);
707
+ }
708
+
709
+ function submitElicitation(tab, form) {
710
+ const requestId = form.dataset.elicitForm;
711
+ const rt = runtimeOf(tab);
712
+ const entry = rt.elicitations.get(requestId);
713
+ if (!entry) return;
714
+ const fields = elicitationFields(entry.requestedSchema);
715
+ const values = readFormValues(form, fields);
716
+ const { content, errors } = elicitationContent(fields, values);
717
+ entry.errors = errors;
718
+ if (Object.keys(errors).length) { scheduleRender(tab); return; }
719
+ answerElicitation(tab, requestId, 'accept', content);
720
+ }
721
+
722
+ function readFormValues(form, fields) {
723
+ const values = {};
724
+ for (const field of fields) {
725
+ const inputs = [...form.querySelectorAll(`[data-field="${CSS.escape(field.name)}"]`)];
726
+ if (field.type === 'boolean') values[field.name] = !!inputs[0]?.checked;
727
+ else if (field.choices.length && field.multiple) values[field.name] = inputs.filter((i) => i.checked).map((i) => i.value);
728
+ else if (field.choices.length) values[field.name] = inputs.find((i) => i.checked)?.value || '';
729
+ else values[field.name] = inputs[0]?.value ?? '';
730
+ }
731
+ return values;
732
+ }
733
+
734
+ async function signIn(tab, methodId) {
735
+ const rt = runtimeOf(tab);
736
+ rt.auth = { state: 'waiting' };
737
+ scheduleRender(tab);
738
+ try {
739
+ await client().request('auth:start', { methodId });
740
+ rt.auth = null;
741
+ rt.notice = null;
742
+ scheduleRender(tab);
743
+ await ensureSession(tab);
744
+ } catch (error) {
745
+ if (!rt.auth || rt.auth.state === 'waiting') rt.auth = { state: 'failed', error: error.message };
746
+ scheduleRender(tab);
747
+ }
748
+ }
749
+
750
+ function openSignInTerminal(tab) {
751
+ emit('terminal:open', { profile: 'antigravity' });
752
+ const rt = runtimeOf(tab);
753
+ if (rt) rt.auth = { state: 'needed', terminal: true };
754
+ scheduleRender(tab);
755
+ }
756
+
757
+ async function signOut() {
758
+ try {
759
+ await client().request('auth:logout', {});
760
+ for (const tab of _tabs) { const rt = runtimeOf(tab); rt.loaded = false; rt.auth = { state: 'needed' }; }
761
+ } catch (error) {
762
+ const rt = runtimeOf(activeTab());
763
+ if (rt) rt.notice = { text: T('errorPrefix', { message: error.message }), severity: 'error' };
764
+ }
765
+ closePop();
766
+ scheduleRender();
767
+ }
768
+
769
+ async function setConfig(tab, configId, value) {
770
+ const rt = runtimeOf(tab);
771
+ try {
772
+ await ensureSession(tab);
773
+ rt.configOptions = withValue(rt.configOptions, configId, value);
774
+ renderPickers();
775
+ const answer = await client().request('config:set', { sessionId: tab.sessionId, configId, value });
776
+ if (Array.isArray(answer.configOptions) && answer.configOptions.length) rt.configOptions = answer.configOptions;
777
+ } catch (error) {
778
+ if (!error.authRequired) rt.notice = { text: T('errorPrefix', { message: error.message }), severity: 'error' };
779
+ scheduleRender(tab);
780
+ }
781
+ renderPickers();
782
+ refreshPop();
783
+ }
784
+ async function setMode(tab, modeId) {
785
+ const rt = runtimeOf(tab);
786
+ try {
787
+ await ensureSession(tab);
788
+ await client().request('mode:set', { sessionId: tab.sessionId, modeId });
789
+ if (rt.modes) rt.modes = { ...rt.modes, currentModeId: modeId };
790
+ } catch (error) {
791
+ if (!error.authRequired) rt.notice = { text: T('errorPrefix', { message: error.message }), severity: 'error' };
792
+ scheduleRender(tab);
793
+ }
794
+ renderPickers();
795
+ refreshPop();
796
+ }
797
+ async function setModel(tab, modelId) {
798
+ const rt = runtimeOf(tab);
799
+ try {
800
+ await ensureSession(tab);
801
+ await client().request('model:set', { sessionId: tab.sessionId, modelId });
802
+ if (rt.models) rt.models = { ...rt.models, currentModelId: modelId };
803
+ } catch (error) {
804
+ if (!error.authRequired) rt.notice = { text: T('errorPrefix', { message: error.message }), severity: 'error' };
805
+ scheduleRender(tab);
806
+ }
807
+ renderPickers();
808
+ }
809
+
810
+ async function forkSession(tab) {
811
+ if (!tab?.sessionId || !agentCaps().fork) return;
812
+ const rt = runtimeOf(tab);
813
+ try {
814
+ const answer = await client().request('session:fork', { sessionId: tab.sessionId, cwd: tab.project, tabId: tab.id, ...(tab.geminiDir ? { geminiDir: tab.geminiDir } : {}) });
815
+ const fork = createTab({ project: tab.project, sessionId: answer.sessionId, title: T('forkTitle', { title: titleOf(tab) }), geminiDir: tab.geminiDir || null });
816
+ if (!fork) return;
817
+ const frt = runtimeOf(fork);
818
+ frt.transcript = structuredCloneTranscript(rt.transcript);
819
+ frt.configOptions = answer.configOptions || [];
820
+ frt.modes = answer.modes || null;
821
+ frt.models = answer.models || null;
822
+ frt.loaded = true;
823
+ frt.openedInPanel = true;
824
+ fork.additionalDirectories = foldersOf(tab);
825
+ saveTabs();
826
+ renderAll();
827
+ } catch (error) {
828
+ rt.notice = { text: T('errorPrefix', { message: error.message }), severity: 'error' };
829
+ scheduleRender(tab);
830
+ }
831
+ }
832
+
833
+ function structuredCloneTranscript(source) {
834
+ const next = createTranscript();
835
+ for (const row of source.rows) {
836
+ const copy = { ...row, closed: true, attachments: row.attachments ? [...row.attachments] : row.attachments, content: row.content ? [...row.content] : row.content };
837
+ next.rows.push(copy);
838
+ if (copy.kind === 'tool') next.tools.set(copy.toolCallId, copy);
839
+ }
840
+ next.turns = source.turns.map((turn) => ({ ...turn }));
841
+ next.seq = source.seq;
842
+ next.commands = source.commands;
843
+ next.title = source.title;
844
+ next.context = source.context;
845
+ return next;
846
+ }
847
+
848
+ async function deleteSession(sessionId) {
849
+ if (!sessionId || !agentCaps().remove) return false;
850
+ const tabWithHome = _tabs.find((entry) => entry.sessionId === sessionId && entry.geminiDir);
851
+ await client().request('session:delete', { sessionId, ...(tabWithHome?.geminiDir ? { geminiDir: tabWithHome.geminiDir } : {}) });
852
+ delete _names[sessionId];
853
+ saveNames();
854
+ for (let i = _tabs.length - 1; i >= 0; i -= 1) {
855
+ if (_tabs[i].sessionId === sessionId && !_tabs[i].automationRunId) closeTab(i, { closeSession: false });
856
+ }
857
+ return true;
858
+ }
859
+
860
+ function renameTab(tab, name) {
861
+ const value = String(name || '').trim().slice(0, 120);
862
+ tab.titleByUser = value || null;
863
+ if (tab.sessionId) {
864
+ if (value) _names[tab.sessionId] = value; else delete _names[tab.sessionId];
865
+ saveNames();
866
+ }
867
+ saveTabs();
868
+ renderTabs();
869
+ renderHeader();
870
+ }
871
+
872
+ function startRename(tab = activeTab()) {
873
+ if (!tab) return;
874
+ const label = $('.agy-session-label');
875
+ if (!label || label.querySelector('input')) return;
876
+ const input = document.createElement('input');
877
+ input.type = 'text';
878
+ input.value = tab.titleByUser || (tab.sessionId && _names[tab.sessionId]) || tab.title || '';
879
+ input.setAttribute('aria-label', T('rename'));
880
+ input.maxLength = 120;
881
+ label.textContent = '';
882
+ label.appendChild(input);
883
+ input.focus();
884
+ input.select();
885
+ let done = false;
886
+ const finish = (save) => {
887
+ if (done) return;
888
+ done = true;
889
+ if (save) renameTab(tab, input.value); else renderHeader();
890
+ };
891
+ input.addEventListener('keydown', (event) => {
892
+ event.stopPropagation();
893
+ if (event.key === 'Enter') { event.preventDefault(); finish(true); }
894
+ else if (event.key === 'Escape') { event.preventDefault(); finish(false); }
895
+ });
896
+ input.addEventListener('blur', () => finish(true));
897
+ input.addEventListener('click', (event) => event.stopPropagation());
898
+ }
899
+
900
+ // ── Panel commands ──
901
+ async function runPanelCommand(name, args) {
902
+ const tab = activeTab();
903
+ const rt = runtimeOf(tab);
904
+ switch (name) {
905
+ case 'new': newSessionTab(); break;
906
+ case 'sessions': openSessionsMenu($('.agy-session-btn')); break;
907
+ case 'rename': if (args) renameTab(tab, args); else startRename(tab); break;
908
+ case 'fork': await forkSession(tab); break;
909
+ case 'retry': retryLast(tab); break;
910
+ case 'copy': copyLastAnswer(tab); break;
911
+ case 'model': openPicker('#agy-model'); break;
912
+ case 'mode': openPicker('#agy-mode'); break;
913
+ case 'thinking': openPicker('#agy-thinking'); break;
914
+ case 'detail': {
915
+ const level = DETAIL_LEVELS.includes(args) ? args : DETAIL_LEVELS[(DETAIL_LEVELS.indexOf(_prefs.detail) + 1) % DETAIL_LEVELS.length];
916
+ setDetail(level);
917
+ break;
918
+ }
919
+ case 'settings': openSettingsPop($('.agy-cog')); break;
920
+ case 'login': if (rt) { rt.auth = { state: 'needed' }; scheduleRender(tab); } break;
921
+ case 'logout': await signOut(); break;
922
+ case 'help': if (rt) { rt.notice = { text: helpText(), severity: 'info' }; scheduleRender(tab); } break;
923
+ default: break;
924
+ }
925
+ }
926
+
927
+ function helpText() {
928
+ const panel = PANEL_COMMANDS.filter((c) => !c.needs || capAvailable(c.needs)).map((c) => `/${c.name} — ${T(c.key)}`);
929
+ const agent = (runtimeOf(activeTab())?.transcript.commands || []).map((c) => `/${c.name}${c.description ? ` — ${c.description}` : ''}`);
930
+ return [T('helpPanel'), ...panel, ...(agent.length ? ['', T('helpAgent'), ...agent] : []), '', T('helpKeys')].join('\n');
931
+ }
932
+
933
+ function openPicker(selector) {
934
+ const trigger = $(`${selector}-trigger`);
935
+ if (trigger && !trigger.disabled) { trigger.focus(); trigger.click(); }
936
+ }
937
+
938
+ function setDetail(level) {
939
+ if (!DETAIL_LEVELS.includes(level)) return;
940
+ _prefs.detail = level;
941
+ savePrefs();
942
+ scheduleRender();
943
+ refreshPop();
944
+ }
945
+
946
+ function retryLast(tab) {
947
+ const rt = runtimeOf(tab);
948
+ const last = lastPrompt(rt?.transcript || createTranscript());
949
+ if (!last || tab.running) return;
950
+ send({ text: last.text, staged: [] });
951
+ }
952
+
953
+ async function copyText(text, flashEl = null) {
954
+ if (!text) return;
955
+ try { await navigator.clipboard.writeText(text); if (flashEl) flashEl.classList.add('copied'); } catch { /* no clipboard */ }
956
+ if (flashEl) setTimeout(() => flashEl.classList.remove('copied'), 1200);
957
+ }
958
+
959
+ function copyLastAnswer(tab) {
960
+ const rt = runtimeOf(tab);
961
+ const turns = rt?.transcript.turns || [];
962
+ for (let i = turns.length - 1; i >= 0; i -= 1) {
963
+ const answer = answerOfTurn(rt.transcript, turns[i].id);
964
+ if (answer) { copyText(answer); return; }
965
+ }
966
+ }
967
+
968
+ // ── Tabs ──
969
+ function createTab({ project = storage.getItem(STOR.project) || _projects[0]?.path || '', sessionId = null, title = null, geminiDir = null, activate = true } = {}) {
970
+ if (_tabs.length >= MAX_TABS) {
971
+ const idle = _tabs.findIndex((tab) => !tab.running && !tab.sessionId && !tab.automationRunId && tab !== activeTab());
972
+ if (idle >= 0) _tabs.splice(idle, 1); else return null;
973
+ }
974
+ const tab = newTab({ project, sessionId, title, geminiDir });
975
+ _tabs.push(tab);
976
+ if (activate) _activeIdx = _tabs.length - 1;
977
+ saveTabs();
978
+ renderAll();
979
+ return tab;
980
+ }
981
+
982
+ function newSessionTab() {
983
+ const current = activeTab();
984
+ if (openAnotherSidepanelSession(PANEL_OWNER, 'antigravity', { project: current?.project || '' })) return;
985
+ // An empty tab is reused rather than stacked.
986
+ if (current && !current.sessionId && !current.running && !current.automationRunId && !runtimeOf(current).transcript.rows.length) {
987
+ current.geminiDir = null;
988
+ current.releasedRunId = null;
989
+ runtimeOf(current).openedInPanel = false;
990
+ $('.agy-input')?.focus();
991
+ return;
992
+ }
993
+ createTab({ project: current?.project || '' });
994
+ $('.agy-input')?.focus();
995
+ }
996
+
997
+ function switchTab(idx) {
998
+ if (idx < 0 || idx >= _tabs.length) return;
999
+ const current = activeTab();
1000
+ const input = $('.agy-input');
1001
+ if (current && input) current.draft = input.value;
1002
+ _activeIdx = idx;
1003
+ const tab = activeTab();
1004
+ if (input) { input.value = tab.draft || ''; autosize(); }
1005
+ hideHints();
1006
+ saveTabs();
1007
+ renderAll();
1008
+ if (tab.sessionId && !runtimeOf(tab).loaded && !tab.automationRunId) ensureSession(tab).catch(() => {});
1009
+ }
1010
+
1011
+ function closeTab(idx, { closeSession = true } = {}) {
1012
+ const tab = _tabs[idx];
1013
+ if (!tab) return;
1014
+ if (tab.running && tab.sessionId && !tab.automationRunId) client().request('cancel', { sessionId: tab.sessionId }).catch(() => {});
1015
+ if (closeSession && tab.sessionId && !tab.automationRunId) client().request('session:close', { sessionId: tab.sessionId }).catch(() => {});
1016
+ if (tab.automationRunId) {
1017
+ const runId = tab.automationRunId;
1018
+ window.dispatchEvent(new CustomEvent('native-loop:dismissed', { detail: { runId } }));
1019
+ endWorkerRunOnClose(runId, () => fetch('/api/loop/stop', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ runId }) }).catch(() => {}), { busy: !!tab.running });
1020
+ forgetWorkerEnd(runId);
1021
+ }
1022
+ tab.pillEl?.remove();
1023
+ _runtime.delete(tab.id);
1024
+ _tabs.splice(idx, 1);
1025
+ if (_activeIdx >= _tabs.length) _activeIdx = Math.max(0, _tabs.length - 1);
1026
+ if (!_tabs.length) {
1027
+ saveTabs();
1028
+ setVisible(false);
1029
+ return;
1030
+ }
1031
+ saveTabs();
1032
+ renderAll();
1033
+ }
1034
+
1035
+ // ── Rendering ──
1036
+ function scheduleRender(tab = null, { follow = false } = {}) {
1037
+ if (tab && tab !== activeTab()) { renderPills(); return; }
1038
+ if (follow) { const rt = runtimeOf(activeTab()); if (rt) rt.scrolledUp = false; }
1039
+ if (_renderQueued) return;
1040
+ _renderQueued = true;
1041
+ requestAnimationFrame(() => {
1042
+ _renderQueued = false;
1043
+ renderTranscript();
1044
+ renderComposer();
1045
+ renderCog();
1046
+ renderQueue();
1047
+ });
1048
+ }
1049
+
1050
+ function renderAll() {
1051
+ if (!_panel) return;
1052
+ renderHeader();
1053
+ renderTabs();
1054
+ renderProjectBar();
1055
+ renderPickers();
1056
+ renderAttachments();
1057
+ renderTranscript();
1058
+ renderComposer();
1059
+ renderBanner();
1060
+ renderQueue();
1061
+ renderCog();
1062
+ }
1063
+
1064
+ function renderHeader() {
1065
+ const label = $('.agy-session-label');
1066
+ if (label && !label.querySelector('input')) label.textContent = activeTab() ? titleOf(activeTab()) : T('newSession');
1067
+ const btn = $('.agy-session-btn');
1068
+ if (btn) btn.title = `${T('sessions')} — ${label?.textContent || ''}`;
1069
+ const rename = $('.agy-header-rename');
1070
+ if (rename) rename.disabled = !activeTab() || !!activeTab()?.automationRunId;
1071
+ }
1072
+
1073
+ function renderTabs() {
1074
+ const strip = $('.agy-tabs');
1075
+ if (!strip) return;
1076
+ if (_tabs.length < 2) { strip.innerHTML = ''; renderPills(); renderHeader(); return; }
1077
+ strip.innerHTML = _tabs.map((tab, idx) => `
1078
+ <button type="button" role="tab" class="agy-tab${tab.running ? ' running' : ''}${tab.bypass === true ? ' bypass' : ''}${tab.needsYou || tab.automationAwaits ? ' needs-you' : ''}" aria-selected="${idx === _activeIdx}" data-idx="${idx}" title="${esc(titleOf(tab))}">
1079
+ <i class="agy-dot" aria-hidden="true"></i><span class="agy-tab-label">${esc(titleOf(tab))}</span>
1080
+ <span class="agy-tab-close" role="button" tabindex="-1" data-close="${idx}" aria-label="${esc(T('closeTab'))}">&times;</span>
1081
+ </button>`).join('');
1082
+ renderHeader();
1083
+ renderPills();
1084
+ }
1085
+
1086
+ function renderProjectBar() {
1087
+ const input = $('#agy-project');
1088
+ const tab = activeTab();
1089
+ if (input) {
1090
+ const entries = _projects.map((project) => ({ value: project.path, label: project.label || project.path.split(/[\\/]/).pop(), selected: project.path === tab?.project }));
1091
+ if (tab?.project && !entries.some((entry) => entry.value === tab.project)) entries.unshift({ value: tab.project, label: tab.project.split(/[\\/]/).pop() || tab.project, selected: true });
1092
+ setDropdownItems(input, entries.length ? entries : [{ value: '', label: T('chooseProject'), selected: true }]);
1093
+ setDropdownDisabled(input, !!tab?.automationRunId || !!tab?.running);
1094
+ }
1095
+ loadBranches(tab?.project || '');
1096
+ const chips = $('.agy-folders');
1097
+ if (chips) {
1098
+ const folders = foldersOf(tab);
1099
+ chips.innerHTML = folders.map((path) => `<span class="agy-folder-chip" title="${esc(path)}"><span>${esc(path.split(/[\\/]/).pop() || path)}</span><button type="button" data-folder-remove="${esc(path)}" aria-label="${esc(T('folderRemove', { name: path.split(/[\\/]/).pop() || path }))}">&times;</button></span>`).join('');
1100
+ }
1101
+ const add = $('.agy-add-folder');
1102
+ if (add) {
1103
+ add.disabled = !tab || !!tab.automationRunId || foldersOf(tab).length >= MAX_FOLDERS;
1104
+ add.hidden = !!_client?.hello && !agentCaps().folders;
1105
+ }
1106
+ }
1107
+
1108
+ // The project's git branch, as the Codex panel shows it (GET /api/terminal/branches); a folder that is not a git
1109
+ // repository hides the picker. Switching runs POST /api/terminal/checkout, never while a turn runs.
1110
+ let _branchSeq = 0;
1111
+ async function loadBranches(project) {
1112
+ const input = $('#agy-branch');
1113
+ if (!input) return;
1114
+ const dd = input.nextElementSibling;
1115
+ const seq = ++_branchSeq;
1116
+ if (!project) { dd?.toggleAttribute('hidden', true); return; }
1117
+ try {
1118
+ const res = await fetch(`/api/terminal/branches?path=${encodeURIComponent(project)}`);
1119
+ const data = res.ok ? await res.json() : null;
1120
+ if (seq !== _branchSeq) return;
1121
+ const branches = Array.isArray(data?.branches) ? data.branches.filter((b) => typeof b === 'string' && b) : [];
1122
+ input.dataset.current = data?.current || '';
1123
+ dd?.toggleAttribute('hidden', !branches.length);
1124
+ if (!branches.length) return;
1125
+ setDropdownItems(input, branches.map((b) => ({ value: b, label: b, selected: b === data.current })));
1126
+ setDropdownDisabled(input, !!activeTab()?.automationRunId);
1127
+ } catch {
1128
+ if (seq === _branchSeq) dd?.toggleAttribute('hidden', true);
1129
+ }
1130
+ }
1131
+
1132
+ async function switchBranch(branch) {
1133
+ const tab = activeTab();
1134
+ const input = $('#agy-branch');
1135
+ const project = tab?.project || '';
1136
+ const previous = input?.dataset.current || '';
1137
+ if (!tab || !project || !branch || branch === previous) return;
1138
+ const rt = runtimeOf(tab);
1139
+ const say = (text, severity = 'info') => { rt.notice = { text, severity }; scheduleRender(tab); };
1140
+ if (_tabs.some((entry) => entry.running && entry.project === project)) { say(T('branchStopFirst'), 'warning'); loadBranches(project); return; }
1141
+ try {
1142
+ const status = await fetch(`/api/git/status?path=${encodeURIComponent(project)}`).then((r) => (r.ok ? r.json() : null)).catch(() => null);
1143
+ const changes = Array.isArray(status?.changes) ? status.changes.length : 0;
1144
+ if (changes && !window.confirm(T('branchConfirm', { from: previous || '—', to: branch, count: changes }))) { loadBranches(project); return; }
1145
+ const res = await fetch('/api/terminal/checkout', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ path: project, branch }) });
1146
+ const result = await res.json().catch(() => ({}));
1147
+ if (!res.ok || !result.ok) throw new Error(result.error || `HTTP ${res.status}`);
1148
+ say(T('branchSwitched', { branch: result.branch || branch }));
1149
+ } catch (error) {
1150
+ say(T('branchFailed', { error: error.message }), 'error');
1151
+ }
1152
+ loadBranches(project);
1153
+ }
1154
+
1155
+ function renderPickers() {
1156
+ renderBypass();
1157
+ const tab = activeTab();
1158
+ const rt = runtimeOf(tab);
1159
+ const model = optionByCategory(rt?.configOptions, 'model');
1160
+ const thinking = optionByCategory(rt?.configOptions, 'thought_level');
1161
+ const modeOption = optionByCategory(rt?.configOptions, 'mode');
1162
+ const modelInput = $('#agy-model');
1163
+ const thinkingInput = $('#agy-thinking');
1164
+ const modeInput = $('#agy-mode');
1165
+ const locked = !tab || !!tab.automationRunId;
1166
+ if (modelInput) {
1167
+ let entries;
1168
+ if (model) entries = dropdownEntries(model);
1169
+ else if (Array.isArray(rt?.models?.availableModels) && rt.models.availableModels.length) {
1170
+ entries = rt.models.availableModels.filter((m) => m?.modelId).map((m) => ({ value: m.modelId, label: m.name || m.modelId, selected: m.modelId === rt.models.currentModelId, title: m.description || '' }));
1171
+ } else entries = [{ value: '', label: T('modelDefault'), selected: true }];
1172
+ setDropdownItems(modelInput, entries);
1173
+ setDropdownDisabled(modelInput, locked || entries.length < 2);
1174
+ modelInput.dataset.configId = model?.id || '';
1175
+ }
1176
+ if (thinkingInput) {
1177
+ const entries = thinking ? dropdownEntries(thinking) : [{ value: '', label: T('thinking'), selected: true }];
1178
+ setDropdownItems(thinkingInput, entries);
1179
+ setDropdownDisabled(thinkingInput, locked || !thinking);
1180
+ thinkingInput.dataset.configId = thinking?.id || '';
1181
+ thinkingInput.nextElementSibling?.toggleAttribute('hidden', !thinking);
1182
+ }
1183
+ if (modeInput) {
1184
+ // A mode the agent offers as a config option (category `mode`) wins over the older session modes.
1185
+ const entries = modeOption ? dropdownEntries(modeOption) : modeEntries(rt?.modes);
1186
+ setDropdownItems(modeInput, entries.length ? entries : [{ value: '', label: T('mode'), selected: true }]);
1187
+ setDropdownDisabled(modeInput, locked || !entries.length);
1188
+ modeInput.dataset.configId = modeOption?.id || '';
1189
+ modeInput.nextElementSibling?.toggleAttribute('hidden', !entries.length);
1190
+ }
1191
+ }
1192
+
1193
+ // ── Bypass: the tab's permissions picker, its warning strip and its confirm (agy-bypass.js) ──
1194
+ // Always stated (the request is built before the socket's hello is known; a server without `bypass` ignores it).
1195
+ const bypassParam = (tab) => ({ bypass: tab?.bypass === true });
1196
+ let _bypassConfirmFor = null; // the tab id whose Bypass waits for its confirm
1197
+ function renderBypass() {
1198
+ const tab = activeTab();
1199
+ const input = $('#agy-perm');
1200
+ const supported = agentCaps().bypass;
1201
+ const view = bypassControl({ bypass: tab?.bypass === true, supported }, T);
1202
+ if (input) {
1203
+ setDropdownItems(input, view.entries);
1204
+ setDropdownDisabled(input, !tab || !!tab.automationRunId);
1205
+ const dd = input.nextElementSibling;
1206
+ if (dd) { dd.dataset.state = view.state; dd.title = view.title; }
1207
+ }
1208
+ const strip = $('.agy-bypass-strip');
1209
+ if (strip) {
1210
+ strip.hidden = view.state !== 'bypass';
1211
+ strip.textContent = view.waiting ? T('permBypassWaiting') : T('permBypassStrip');
1212
+ }
1213
+ _panel?.toggleAttribute('data-bypass', view.state === 'bypass');
1214
+ const badge = $('.agy-bypass-badge');
1215
+ if (badge) { badge.hidden = view.state !== 'bypass'; badge.title = view.title; }
1216
+ const confirm = $('.agy-bypass-confirm');
1217
+ if (confirm) {
1218
+ if (_bypassConfirmFor && _bypassConfirmFor !== tab?.id) _bypassConfirmFor = null;
1219
+ confirm.hidden = !_bypassConfirmFor;
1220
+ }
1221
+ }
1222
+ async function setBypass(tab, on) {
1223
+ if (!tab) return;
1224
+ const previous = tab.bypass === true;
1225
+ tab.bypass = on;
1226
+ saveTabs();
1227
+ renderTabs();
1228
+ renderBypass();
1229
+ if (!tab.sessionId || !runtimeOf(tab).loaded || !agentCaps().bypass) return;
1230
+ try {
1231
+ await client().request('session:bypass', { sessionId: tab.sessionId, on });
1232
+ } catch (error) {
1233
+ tab.bypass = previous;
1234
+ saveTabs();
1235
+ const rt = runtimeOf(tab);
1236
+ rt.transcript.rows.push({ id: `b${Date.now()}`, kind: 'notice', text: T('permBypassFailed', { error: error.message }), severity: 'error' });
1237
+ scheduleRender(tab);
1238
+ renderTabs();
1239
+ renderBypass();
1240
+ }
1241
+ }
1242
+
1243
+ function renderAttachments() {
1244
+ const box = $('.agy-staged');
1245
+ const rt = runtimeOf(activeTab());
1246
+ if (!box) return;
1247
+ const staged = rt?.staged || [];
1248
+ box.hidden = !staged.length;
1249
+ box.innerHTML = staged.map((file, idx) => `<span class="agy-chip" title="${esc(file.path || file.name)}">${file.mimeType?.startsWith('image/') && file.dataUrl ? `<img src="${esc(file.dataUrl)}" alt="">` : ''}<span>${esc(file.name)}</span><button type="button" data-unstage="${idx}" aria-label="${esc(T('attachmentRemove', { name: file.name }))}">&times;</button></span>`).join('');
1250
+ }
1251
+
1252
+ function renderQueue() {
1253
+ const box = $('.agy-queue');
1254
+ const rt = runtimeOf(activeTab());
1255
+ if (!box) return;
1256
+ const queue = rt?.queue || [];
1257
+ box.hidden = !queue.length;
1258
+ if (!queue.length) { box.innerHTML = ''; return; }
1259
+ box.innerHTML = `<div class="agy-queue-head"><span>${esc(T('queueTitle'))}<span class="agy-queue-badge">${queue.length}</span></span><span class="agy-queue-actions">`
1260
+ + `<button type="button" class="agy-icon-btn" data-queue="pause" aria-pressed="${rt.queuePaused}" title="${esc(T(rt.queuePaused ? 'queueResume' : 'queuePause'))}" aria-label="${esc(T(rt.queuePaused ? 'queueResume' : 'queuePause'))}">${rt.queuePaused ? '&#9654;' : '&#10074;&#10074;'}</button>`
1261
+ + `<button type="button" class="agy-icon-btn" data-queue="clear" title="${esc(T('queueClear'))}" aria-label="${esc(T('queueClear'))}">&times;</button></span></div>`
1262
+ + queue.map((item, idx) => `<div class="agy-queue-item"><span class="agy-queue-text" title="${esc(item.text)}">${esc(item.text || item.staged.map((f) => f.name).join(', '))}</span><button type="button" class="agy-icon-btn" data-queue-edit="${idx}" title="${esc(T('queueEdit'))}" aria-label="${esc(T('queueEdit'))}">${SVG.edit}</button><button type="button" class="agy-icon-btn" data-queue-remove="${idx}" title="${esc(T('queueRemove'))}" aria-label="${esc(T('queueRemove'))}">&times;</button></div>`).join('');
1263
+ }
1264
+
1265
+ /** innerHTML, keeping the focused field, its selection and what was typed into question forms. */
1266
+ function replaceKeepingFocus(box, html, rt) {
1267
+ const active = document.activeElement;
1268
+ const keep = active && box.contains(active) && active.id ? { id: active.id, start: active.selectionStart, end: active.selectionEnd } : null;
1269
+ box.innerHTML = html;
1270
+ for (const [requestId, values] of Object.entries(rt?.elicitDrafts || {})) {
1271
+ const form = box.querySelector(`[data-elicit-form="${CSS.escape(requestId)}"]`);
1272
+ if (!form) continue;
1273
+ for (const [name, value] of Object.entries(values)) {
1274
+ for (const input of form.querySelectorAll(`[data-field="${CSS.escape(name)}"]`)) {
1275
+ if (input.type === 'checkbox' || input.type === 'radio') input.checked = Array.isArray(value) ? value.includes(input.value) : input.type === 'checkbox' && !input.name ? !!value : value === input.value;
1276
+ else input.value = value;
1277
+ }
1278
+ }
1279
+ }
1280
+ if (keep) {
1281
+ const again = box.querySelector(`#${CSS.escape(keep.id)}`);
1282
+ if (again) { again.focus(); try { if (keep.start !== null && keep.start !== undefined) again.setSelectionRange(keep.start, keep.end); } catch { /* not a text field */ } }
1283
+ }
1284
+ }
1285
+
1286
+ function renderTranscript() {
1287
+ const box = $('.agy-transcript');
1288
+ if (!box) return;
1289
+ const tab = activeTab();
1290
+ const rt = runtimeOf(tab);
1291
+ if (!tab || !rt) { box.innerHTML = ''; return; }
1292
+ const nearBottom = box.scrollHeight - box.scrollTop - box.clientHeight < 60;
1293
+ const setup = currentSetup();
1294
+ const rows = rt.transcript.rows;
1295
+ const ctx = {
1296
+ T, md, cwd: tab.project, detail: _prefs.detail, running: !!tab.running,
1297
+ opened: rt.opened, closed: rt.closed, terminals: rt.transcript.terminals, fullDiffs: rt.fullDiffs, folds: rt.folds,
1298
+ };
1299
+ const footers = turnFooters(rt.transcript);
1300
+ const lastTurnId = rt.transcript.turns.length ? rt.transcript.turns[rt.transcript.turns.length - 1].id : null;
1301
+ const parts = [];
1302
+ for (const row of rows) {
1303
+ parts.push(rowHtml(row, ctx));
1304
+ const turn = footers.get(row.id);
1305
+ if (turn && !(tab.running && turn.id === lastTurnId)) parts.push(turnFooterHtml(turn, { T, last: turn.id === lastTurnId && !tab.running && !tab.automationRunId, answer: answerOfTurn(rt.transcript, turn.id) }));
1306
+ }
1307
+ for (const [requestId, entry] of rt.permissions) if (entry.state !== 'gone') parts.push(permissionHtml(requestId, entry, { T, cwd: tab.project }));
1308
+ for (const [requestId, entry] of rt.elicitations) parts.push(elicitationHtml(requestId, entry, { T }));
1309
+ if (tab.running && _prefs.detail === 'compact' && !rows.some((row) => row.kind === 'thought' && !row.closed)) parts.push(`<div class="agy-system working">${esc(T('working'))}</div>`);
1310
+ if (rt.notice) parts.push(`<div class="agy-system${rt.notice.severity === 'error' ? ' error' : rt.notice.severity === 'warning' ? ' warning' : ''}">${esc(rt.notice.text)}</div>`);
1311
+ const body = parts.filter(Boolean).join('');
1312
+ const setupBlock = setup.show ? setupHtml({ ...setup, logOpen: _setup.logOpen }, { T, centered: !body }) : '';
1313
+ if (!body && !setupBlock) {
1314
+ // Drawn again only when it changes: the greeting card mounted in it stays (and keeps its disclosure).
1315
+ const empty = emptyHtml({ T, project: tab.project });
1316
+ const key = `${tab.id}\n${empty}`;
1317
+ if (box._agyEmpty !== key || !box.querySelector(':scope > .agy-empty')) { box.innerHTML = empty; box._agyEmpty = key; }
1318
+ offerGreeting(tab, box.querySelector(':scope > .agy-empty'));
1319
+ return;
1320
+ }
1321
+ box._agyEmpty = null;
1322
+ replaceKeepingFocus(box, setupBlock + body, rt);
1323
+ if (_setup.nudge) { box.querySelector('.agy-setup')?.classList.add('agy-nudge'); }
1324
+ if (nearBottom || !rt.scrolledUp) box.scrollTop = box.scrollHeight;
1325
+ const jump = $('.agy-jump');
1326
+ if (jump) jump.hidden = !rt.scrolledUp;
1327
+ }
1328
+
1329
+ function workingSeconds(tab) {
1330
+ return tab?.startedAt ? Math.max(0, Math.round((Date.now() - tab.startedAt) / 1000)) : 0;
1331
+ }
1332
+
1333
+ function startWorkingClock() {
1334
+ if (_workingTimer) return;
1335
+ _workingTimer = setInterval(() => {
1336
+ if (!_tabs.some((tab) => tab.running)) { clearInterval(_workingTimer); _workingTimer = null; }
1337
+ renderComposer();
1338
+ }, 1000);
1339
+ }
1340
+
1341
+ function renderComposer() {
1342
+ const tab = activeTab();
1343
+ const input = $('.agy-input');
1344
+ const sendBtn = $('.agy-send');
1345
+ const status = $('.agy-status-line');
1346
+ const rt = runtimeOf(tab);
1347
+ const running = !!tab?.running;
1348
+ const locked = !!tab?.automationActive;
1349
+ const blocked = blockedNow();
1350
+ _panel?.classList.toggle('agy-blocked', !!blocked);
1351
+ if (input) {
1352
+ input.disabled = locked;
1353
+ input.placeholder = locked ? T('placeholderAutomation') : running ? T('placeholderQueue') : T('placeholder');
1354
+ }
1355
+ const hasText = !!(input?.value.trim() || rt?.staged.length);
1356
+ if (sendBtn) {
1357
+ const mode = running && !hasText ? 'stop' : running ? 'queue' : 'send';
1358
+ sendBtn.innerHTML = mode === 'stop' ? SVG.stop : mode === 'queue' ? SVG.queue : SVG.send;
1359
+ sendBtn.dataset.mode = mode;
1360
+ const label = mode === 'stop' ? T('stop') : mode === 'queue' ? T('queueAdd') : blocked ? T(blocked) : T('send');
1361
+ sendBtn.setAttribute('aria-label', label);
1362
+ sendBtn.title = label;
1363
+ sendBtn.disabled = locked || (mode === 'send' && !hasText);
1364
+ }
1365
+ if (status) {
1366
+ const awaits = tab?.needsYou || tab?.automationAwaits;
1367
+ let text = '';
1368
+ if (locked) text = T('automation');
1369
+ else if (awaits) text = T('needsYou');
1370
+ else if (running) text = T('workingFor', { seconds: workingSeconds(tab) });
1371
+ else if (blocked) text = T(blocked);
1372
+ status.textContent = text;
1373
+ status.classList.toggle('working', running && !awaits);
1374
+ status.classList.toggle('attention', !!awaits);
1375
+ }
1376
+ const attach = $('.agy-attach');
1377
+ if (attach) attach.disabled = locked;
1378
+ const mic = $('.agy-mic');
1379
+ if (mic) mic.hidden = !agentCaps().audio || typeof MediaRecorder === 'undefined';
1380
+ }
1381
+
1382
+ function renderBanner() {
1383
+ const banner = $('.agy-banner');
1384
+ if (!banner) return;
1385
+ const tab = activeTab();
1386
+ let html = '';
1387
+ let tone = '';
1388
+ if (_conn.phase === 'blocked') {
1389
+ const key = { automation: 'connAutomation', guest: 'connGuest', restart: 'connRestart' }[_conn.reason] || 'connRefused';
1390
+ tone = 'err';
1391
+ html = `<span class="agy-banner-text">${esc(T(key))}${_conn.message && _conn.reason === 'refused' ? ` <span class="agy-visually-hidden">${esc(_conn.message)}</span>` : ''}</span><button type="button" class="agy-action" data-conn="retry">${esc(T('connRetry'))}</button>`;
1392
+ } else if (_conn.phase === 'retrying' || _conn.phase === 'offline') {
1393
+ tone = 'warn';
1394
+ html = `<span class="agy-banner-text">${esc(T(_conn.phase === 'offline' ? 'connOffline' : 'disconnected'))}</span><button type="button" class="agy-action" data-conn="retry">${esc(T('connRetryNow'))}</button>`;
1395
+ } else if (tab?.automationRunId) {
1396
+ tone = '';
1397
+ html = `<span class="agy-banner-text">${esc(tab.automationActive ? T('automationBanner') : T('automationEnded'))}</span>`;
1398
+ }
1399
+ banner.hidden = !html;
1400
+ banner.dataset.tone = tone;
1401
+ banner.innerHTML = html;
1402
+ }
1403
+
1404
+ function renderCog() {
1405
+ const dot = $('.agy-cog-dot');
1406
+ const g = contextGauge(runtimeOf(activeTab())?.transcript);
1407
+ if (dot) {
1408
+ dot.hidden = !g || g.percent < 80;
1409
+ dot.classList.toggle('critical', !!g && g.percent >= 90);
1410
+ }
1411
+ const cog = $('.agy-cog');
1412
+ if (cog) cog.title = g ? `${T('settingsTitle')} — ${T('context', { label: contextWords(g, T) })}` : T('settingsTitle');
1413
+ if (_pop?.kind === 'settings') refreshPop();
1414
+ }
1415
+
1416
+ function autosize() {
1417
+ const input = $('.agy-input');
1418
+ if (!input) return;
1419
+ input.style.height = 'auto';
1420
+ input.style.height = `${Math.min(180, Math.max(24, input.scrollHeight))}px`;
1421
+ }
1422
+
1423
+ function nudgeSetup() {
1424
+ _setup.nudge = true;
1425
+ scheduleRender();
1426
+ setTimeout(() => { _setup.nudge = false; }, 600);
1427
+ $('.agy-banner:not([hidden])')?.classList.add('agy-nudge');
1428
+ }
1429
+
1430
+ // ── Hints: slash commands and @ files ──
1431
+ function hideHints() {
1432
+ _hints = null;
1433
+ const box = $('.agy-hints');
1434
+ if (box) { box.hidden = true; box.innerHTML = ''; }
1435
+ $('.agy-input')?.removeAttribute('aria-activedescendant');
1436
+ }
1437
+
1438
+ function drawHints() {
1439
+ const box = $('.agy-hints');
1440
+ if (!box || !_hints) return;
1441
+ if (!_hints.items.length) {
1442
+ if (_hints.kind === 'mention' && _hints.loading) { box.hidden = false; box.innerHTML = `<div class="agy-hint-empty">${esc(T('mentionLoading'))}</div>`; return; }
1443
+ hideHints();
1444
+ return;
1445
+ }
1446
+ box.hidden = false;
1447
+ let lastGroup = '';
1448
+ box.innerHTML = _hints.items.map((item, idx) => {
1449
+ const group = _hints.kind === 'slash' ? (item.source === 'agent' ? T('hintsAgent') : T('hintsPanel')) : '';
1450
+ const head = group && group !== lastGroup ? `<div class="agy-hint-group">${esc(group)}</div>` : '';
1451
+ lastGroup = group || lastGroup;
1452
+ if (_hints.kind === 'slash') {
1453
+ const desc = item.source === 'agent' ? item.description : T(item.key);
1454
+ return `${head}<div class="agy-hint${idx === _hints.active ? ' active' : ''}" role="option" id="agy-hint-${idx}" aria-selected="${idx === _hints.active}" data-hint="${idx}"><span class="agy-hint-name">/${esc(item.name)}</span>${item.hint ? `<span class="agy-hint-arg">${esc(item.hint)}</span>` : ''}<span class="agy-hint-desc">${esc(desc || '')}</span></div>`;
1455
+ }
1456
+ return `<div class="agy-hint${idx === _hints.active ? ' active' : ''}" role="option" id="agy-hint-${idx}" aria-selected="${idx === _hints.active}" data-hint="${idx}"><span class="agy-hint-name">@${esc(item.name)}</span><span class="agy-hint-desc">${esc(item.path)}</span></div>`;
1457
+ }).join('');
1458
+ $('.agy-input')?.setAttribute('aria-activedescendant', `agy-hint-${_hints.active}`);
1459
+ box.querySelector('.agy-hint.active')?.scrollIntoView?.({ block: 'nearest' });
1460
+ }
1461
+
1462
+ function updateHints() {
1463
+ const input = $('.agy-input');
1464
+ const tab = activeTab();
1465
+ const rt = runtimeOf(tab);
1466
+ if (!input || !tab) { hideHints(); return; }
1467
+ const slash = slashMenu(input.value, { agentCommands: rt.transcript.commands, available: capAvailable });
1468
+ if (slash) {
1469
+ _hints = { kind: 'slash', items: slash.items, active: 0 };
1470
+ drawHints();
1471
+ return;
1472
+ }
1473
+ const mention = mentionAt(input.value, input.selectionStart);
1474
+ if (mention && tab.project) {
1475
+ const seq = ++_mentionSeq;
1476
+ _hints = { kind: 'mention', items: [], active: 0, mention, loading: true };
1477
+ drawHints();
1478
+ fetchFiles(tab.project, mention.query).then((items) => {
1479
+ if (seq !== _mentionSeq || _hints?.kind !== 'mention') return;
1480
+ _hints = { kind: 'mention', items, active: 0, mention, loading: false };
1481
+ drawHints();
1482
+ });
1483
+ return;
1484
+ }
1485
+ hideHints();
1486
+ }
1487
+
1488
+ async function fetchFiles(project, query) {
1489
+ try {
1490
+ const url = `/api/project-files?${new URLSearchParams({ path: project, search: query || '', recursive: query ? '0' : '1', filesOnly: '1' })}`;
1491
+ const res = await fetch(url);
1492
+ if (!res.ok) return [];
1493
+ const data = await res.json();
1494
+ return (Array.isArray(data?.items) ? data.items : []).filter((item) => item.type === 'file')
1495
+ .map((item) => {
1496
+ const path = String(item.relativePath || item.path || item.name || '').replace(/^[\\/]+/, '');
1497
+ return { name: path.split('/').pop() || path, path };
1498
+ }).filter((item) => item.path).slice(0, 20);
1499
+ } catch { return []; }
1500
+ }
1501
+
1502
+ function chooseHint(idx) {
1503
+ const input = $('.agy-input');
1504
+ const tab = activeTab();
1505
+ if (!_hints || !input || !tab) return;
1506
+ const item = _hints.items[idx];
1507
+ if (!item) return;
1508
+ if (_hints.kind === 'slash') {
1509
+ input.value = slashInsert(item);
1510
+ hideHints();
1511
+ // A panel command without an argument runs at once.
1512
+ if (item.source === 'panel' && !item.hint) { send(); return; }
1513
+ } else {
1514
+ const next = applyMention(input.value, _hints.mention, item.path);
1515
+ input.value = next.text;
1516
+ input.setSelectionRange(next.caret, next.caret);
1517
+ const abs = joinProjectPath(tab.project, item.path);
1518
+ const rt = runtimeOf(tab);
1519
+ if (abs && !rt.staged.some((file) => file.path === abs)) rt.staged.push({ name: item.path, path: abs, mimeType: '' });
1520
+ hideHints();
1521
+ renderAttachments();
1522
+ }
1523
+ tab.draft = input.value;
1524
+ input.focus();
1525
+ autosize();
1526
+ renderComposer();
1527
+ }
1528
+
1529
+ // ── Tray pills (shown for tabs with something in them while the panel is closed, or not the active tab) ──
1530
+ function renderPills() {
1531
+ const tray = document.getElementById('term-minimized-tray');
1532
+ for (const tab of _tabs) {
1533
+ if (!tab.pillEl?.isConnected) {
1534
+ if (!tray) continue;
1535
+ const pill = document.createElement('div');
1536
+ pill.className = 'term-minimized-pill agy-session-pill';
1537
+ pill.dataset.tabId = tab.id;
1538
+ pill.tabIndex = 0;
1539
+ pill.innerHTML = `<span class="term-minimized-pill-icon agy-mark">${ICON}</span><span class="term-minimized-pill-label"></span><button class="term-minimized-pill-close" data-tooltip="${esc(T('closeTab'))}" data-tooltip-pos="top">&times;</button>`;
1540
+ const openIt = () => {
1541
+ const idx = _tabs.indexOf(tab);
1542
+ if (idx < 0) return;
1543
+ if (!_visible) setVisible(true);
1544
+ switchTab(idx);
1545
+ try { focusSidepanel(PANEL_OWNER); } catch { /* not hosted */ }
1546
+ };
1547
+ pill.addEventListener('click', openIt);
1548
+ pill.addEventListener('keydown', (event) => { if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); openIt(); } });
1549
+ pill.querySelector('.term-minimized-pill-close')?.addEventListener('click', (event) => {
1550
+ event.stopPropagation();
1551
+ const idx = _tabs.indexOf(tab);
1552
+ if (idx >= 0) closeTab(idx);
1553
+ });
1554
+ pill.style.display = 'none';
1555
+ tray.appendChild(pill);
1556
+ tab.pillEl = pill;
1557
+ }
1558
+ const label = tab.pillEl.querySelector('.term-minimized-pill-label');
1559
+ if (label) label.textContent = titleOf(tab);
1560
+ const needsYou = !!(tab.needsYou || tab.automationAwaits);
1561
+ tab.pillEl.classList.toggle('agy-pill-attention', needsYou);
1562
+ tab.pillEl.classList.toggle('agy-pill-running', !needsYou && !!tab.running);
1563
+ const meaningful = !!(tab.sessionId || tab.running || tab.draft || tab.automationRunId || tab.needsYou);
1564
+ tab.pillEl.style.display = meaningful && (!_visible || tab !== activeTab()) ? '' : 'none';
1565
+ }
1566
+ }
1567
+
1568
+ // ── Popovers (children of <body>, placed from their anchor) ──
1569
+ function closePop() {
1570
+ if (!_pop) return;
1571
+ _pop.cleanup?.();
1572
+ _pop.el.remove();
1573
+ const anchor = _pop.anchor;
1574
+ _pop = null;
1575
+ anchor?.setAttribute?.('aria-expanded', 'false');
1576
+ }
1577
+
1578
+ function openPop(anchor, kind, html, { wide = false, onClick = null, onInput = null, onChange = null, align = 'left' } = {}) {
1579
+ closePop();
1580
+ const el = document.createElement('div');
1581
+ el.className = `agy-pop${wide ? ' agy-pop-wide' : ''}`;
1582
+ el.setAttribute('role', kind === 'settings' ? 'dialog' : 'menu');
1583
+ el.setAttribute('aria-label', kind === 'settings' ? T('settingsTitle') : kind === 'sessions' ? T('sessions') : T('folderAdd'));
1584
+ el.innerHTML = html;
1585
+ document.body.appendChild(el);
1586
+ wireDropdowns(el);
1587
+ const place = () => {
1588
+ const rect = anchor.getBoundingClientRect();
1589
+ const width = el.offsetWidth;
1590
+ const height = el.offsetHeight;
1591
+ let left = align === 'right' ? rect.right - width : rect.left;
1592
+ left = Math.max(8, Math.min(left, window.innerWidth - width - 8));
1593
+ let top = rect.bottom + 4;
1594
+ if (top + height > window.innerHeight - 8) top = Math.max(8, rect.top - height - 4);
1595
+ el.style.left = `${Math.round(left)}px`;
1596
+ el.style.top = `${Math.round(top)}px`;
1597
+ };
1598
+ place();
1599
+ const away = (event) => { if (!el.contains(event.target) && !anchor.contains(event.target) && !event.target.closest?.('.cc-dropdown-menu')) closePop(); };
1600
+ const keys = (event) => {
1601
+ if (event.key === 'Escape') {
1602
+ if (closeDropdowns(el, { focus: true, cancel: true })) { event.stopPropagation(); return; }
1603
+ event.stopPropagation(); event.preventDefault(); closePop(); anchor.focus?.();
1604
+ }
1605
+ };
1606
+ setTimeout(() => document.addEventListener('pointerdown', away, true), 0);
1607
+ window.addEventListener('keydown', keys, true);
1608
+ window.addEventListener('resize', place);
1609
+ if (onClick) el.addEventListener('click', onClick);
1610
+ if (onInput) el.addEventListener('input', onInput);
1611
+ if (onChange) el.addEventListener('change', onChange);
1612
+ anchor.setAttribute?.('aria-expanded', 'true');
1613
+ _pop = {
1614
+ el, kind, anchor, place,
1615
+ cleanup: () => { document.removeEventListener('pointerdown', away, true); window.removeEventListener('keydown', keys, true); window.removeEventListener('resize', place); },
1616
+ };
1617
+ return el;
1618
+ }
1619
+
1620
+ // The session menu: a new session, then Antigravity's conversations in this project (search, rename, delete).
1621
+ async function openSessionsMenu(anchor) {
1622
+ if (!anchor) return;
1623
+ if (_pop?.kind === 'sessions') { closePop(); return; }
1624
+ const tab = activeTab();
1625
+ const listState = { sessions: [], cursor: null, filter: '', confirm: null };
1626
+ const draw = () => {
1627
+ if (_pop?.kind !== 'sessions') return;
1628
+ const list = _pop.el.querySelector('.agy-sess-list');
1629
+ const q = listState.filter.toLowerCase();
1630
+ const shown = listState.sessions.filter((s) => !q || `${_names[s.sessionId] || ''} ${s.title || ''} ${s.sessionId}`.toLowerCase().includes(q));
1631
+ const caps = agentCaps();
1632
+ list.innerHTML = shown.length ? shown.map((s) => {
1633
+ const name = _names[s.sessionId] || s.title || s.sessionId;
1634
+ const when = s.updatedAt ? relativeTime(s.updatedAt) : '';
1635
+ const active = s.sessionId === tab?.sessionId;
1636
+ if (listState.confirm === s.sessionId) {
1637
+ return `<div class="agy-pop-confirm"><span>${esc(T('deleteConfirm', { title: name }))}</span><button type="button" class="agy-action agy-btn-deny" data-sess-delete-yes="${esc(s.sessionId)}">${esc(T('delete'))}</button><button type="button" class="agy-action" data-sess-delete-no="1">${esc(T('cancel'))}</button></div>`;
1638
+ }
1639
+ return `<div class="agy-pop-row${active ? ' active' : ''}"><button type="button" class="agy-pop-item" role="menuitem" data-sess-open="${esc(s.sessionId)}" data-title="${esc(s.title || '')}"><span class="agy-pop-item-main"><span class="agy-pop-item-title">${esc(name)}</span><span class="agy-pop-item-meta">${esc([when, s.cwd && s.cwd !== tab?.project ? relativeToProject('', s.cwd).split(/[\\/]/).pop() : ''].filter(Boolean).join(' · '))}</span></span></button>`
1640
+ + `<span class="agy-pop-row-actions"><button type="button" class="agy-icon-btn" data-sess-rename="${esc(s.sessionId)}" title="${esc(T('rename'))}" aria-label="${esc(T('rename'))}">${SVG.edit}</button>`
1641
+ + `${caps.remove ? `<button type="button" class="agy-icon-btn" data-sess-delete="${esc(s.sessionId)}" title="${esc(T('delete'))}" aria-label="${esc(T('delete'))}">${SVG.trash}</button>` : ''}</span></div>`;
1642
+ }).join('') : `<div class="agy-pop-note">${esc(listState.loading ? T('sessionsLoading') : listState.error || (tab?.project ? T('sessionsEmpty') : T('noProject')))}</div>`;
1643
+ const more = _pop.el.querySelector('.agy-sess-more');
1644
+ if (more) more.hidden = !listState.cursor;
1645
+ };
1646
+ const load = async (cursor = null) => {
1647
+ if (!tab?.project) { listState.loading = false; draw(); return; }
1648
+ listState.loading = true;
1649
+ draw();
1650
+ try {
1651
+ const answer = await client().request('session:list', { cwd: tab.project, ...(cursor ? { cursor } : {}) });
1652
+ listState.sessions = cursor ? [...listState.sessions, ...answer.sessions] : answer.sessions;
1653
+ listState.cursor = answer.nextCursor || null;
1654
+ listState.error = '';
1655
+ } catch (error) {
1656
+ listState.error = error.authRequired ? T('signInBody') : T('sessionsFailed', { error: error.message });
1657
+ }
1658
+ listState.loading = false;
1659
+ draw();
1660
+ };
1661
+ const el = openPop(anchor, 'sessions', `<button type="button" class="agy-pop-item" role="menuitem" data-sess-new="1"><span class="agy-pop-item-main"><span class="agy-pop-item-title">+ ${esc(T('newSession'))}</span></span></button><div class="agy-pop-sep"></div><input type="search" class="agy-pop-search" data-sess-search="1" placeholder="${esc(T('sessionsSearch'))}" aria-label="${esc(T('sessionsSearch'))}"><div class="agy-sess-list" role="group"></div><button type="button" class="agy-pop-item agy-sess-more" hidden><span class="agy-pop-item-main"><span class="agy-pop-item-meta">${esc(T('sessionsMore'))}</span></span></button>`, {
1662
+ wide: true,
1663
+ onInput: (event) => { if (event.target.matches('[data-sess-search]')) { listState.filter = event.target.value; draw(); } },
1664
+ onClick: async (event) => {
1665
+ const target = event.target;
1666
+ if (target.closest('[data-sess-new]')) { closePop(); newSessionTab(); return; }
1667
+ if (target.closest('.agy-sess-more')) { load(listState.cursor); return; }
1668
+ const rename = target.closest('[data-sess-rename]');
1669
+ if (rename) {
1670
+ const id = rename.dataset.sessRename;
1671
+ const current = _names[id] || listState.sessions.find((s) => s.sessionId === id)?.title || '';
1672
+ const row = rename.closest('.agy-pop-row');
1673
+ row.innerHTML = `<input type="text" class="agy-pop-search agy-pop-rename" value="${esc(current)}" maxlength="120" aria-label="${esc(T('rename'))}">`;
1674
+ const input = row.querySelector('input');
1675
+ input.focus(); input.select();
1676
+ input.addEventListener('keydown', (e) => {
1677
+ e.stopPropagation();
1678
+ if (e.key === 'Enter') {
1679
+ const value = input.value.trim();
1680
+ if (value) _names[id] = value; else delete _names[id];
1681
+ saveNames();
1682
+ for (const t of _tabs) if (t.sessionId === id) t.titleByUser = value || null;
1683
+ saveTabs(); renderTabs(); renderHeader(); draw();
1684
+ } else if (e.key === 'Escape') { e.preventDefault(); draw(); }
1685
+ });
1686
+ return;
1687
+ }
1688
+ const del = target.closest('[data-sess-delete]');
1689
+ if (del) { listState.confirm = del.dataset.sessDelete; draw(); return; }
1690
+ if (target.closest('[data-sess-delete-no]')) { listState.confirm = null; draw(); return; }
1691
+ const yes = target.closest('[data-sess-delete-yes]');
1692
+ if (yes) {
1693
+ const id = yes.dataset.sessDeleteYes;
1694
+ try { await deleteSession(id); listState.sessions = listState.sessions.filter((s) => s.sessionId !== id); listState.error = ''; }
1695
+ catch (error) { listState.error = T('errorPrefix', { message: error.message }); }
1696
+ listState.confirm = null;
1697
+ draw();
1698
+ return;
1699
+ }
1700
+ const open = target.closest('[data-sess-open]');
1701
+ if (open) { closePop(); openSession(open.dataset.sessOpen, open.dataset.title || null); }
1702
+ },
1703
+ });
1704
+ el.querySelector('.agy-pop-search')?.focus();
1705
+ load();
1706
+ }
1707
+
1708
+ function relativeTime(iso) {
1709
+ const at = Date.parse(iso);
1710
+ if (!Number.isFinite(at)) return '';
1711
+ const seconds = Math.round((at - Date.now()) / 1000);
1712
+ const lang = document.documentElement.lang || undefined;
1713
+ try {
1714
+ const rtf = new Intl.RelativeTimeFormat(lang, { numeric: 'auto' });
1715
+ const abs = Math.abs(seconds);
1716
+ if (abs < 60) return rtf.format(seconds, 'second');
1717
+ if (abs < 3600) return rtf.format(Math.round(seconds / 60), 'minute');
1718
+ if (abs < 86400) return rtf.format(Math.round(seconds / 3600), 'hour');
1719
+ if (abs < 86400 * 30) return rtf.format(Math.round(seconds / 86400), 'day');
1720
+ } catch { /* fall through */ }
1721
+ return new Date(at).toLocaleDateString(lang);
1722
+ }
1723
+
1724
+ function openSession(sessionId, title) {
1725
+ const current = activeTab();
1726
+ const existing = _tabs.findIndex((tab) => tab.sessionId === sessionId);
1727
+ if (existing >= 0) { switchTab(existing); return; }
1728
+ const reuse = current && !current.sessionId && !current.running && !current.automationRunId && !runtimeOf(current).transcript.rows.length;
1729
+ const tab = reuse ? current : createTab({ project: current?.project || '' });
1730
+ if (!tab) return;
1731
+ tab.sessionId = sessionId;
1732
+ tab.title = title || null;
1733
+ tab.geminiDir = null;
1734
+ tab.releasedRunId = null;
1735
+ runtimeOf(tab).loaded = false;
1736
+ runtimeOf(tab).openedInPanel = false;
1737
+ runtimeOf(tab).transcript = createTranscript();
1738
+ saveTabs();
1739
+ renderAll();
1740
+ ensureSession(tab).catch(() => {});
1741
+ }
1742
+
1743
+ // Session settings: the context window, the session, every option the agent offers, the panel's own settings,
1744
+ // the account and the versions. Read again each time something it shows changes.
1745
+ function settingsHtml() {
1746
+ const tab = activeTab();
1747
+ const rt = runtimeOf(tab);
1748
+ const hello = _client?.hello || {};
1749
+ const caps = agentCaps();
1750
+ const g = contextGauge(rt?.transcript);
1751
+ const kv = (label, value, { muted = false, mono = false, raw = false } = {}) => `<div class="agy-kv"><span class="agy-kv-label">${esc(label)}</span><span class="agy-kv-value${muted ? ' muted' : ''}${mono ? ' mono' : ''}">${raw ? value : esc(value)}</span></div>`;
1752
+ const hasCompact = (rt?.transcript.commands || []).some((c) => c.name === 'compact');
1753
+ const context = `<section class="agy-sec"><div class="agy-sec-head"><span class="agy-sec-title">${esc(T('settingsContext'))}</span>${hasCompact ? `<button type="button" class="agy-action" data-pop="compact"${tab?.running ? ' disabled' : ''}>${esc(T('compact'))}</button>` : ''}</div>`
1754
+ + (g ? `<div class="agy-ctx-bar${g.percent >= 90 ? ' critical' : g.percent >= 80 ? ' high' : ''}"><span style="width:${g.percent}%"></span></div>${kv(T('settingsTokens'), contextWords(g, T))}` : kv(T('settingsTokens'), T('contextUnknown'), { muted: true }))
1755
+ + '</section>';
1756
+ const options = rt?.configOptions || [];
1757
+ const optionRows = options.filter((o) => o && (o.type === 'select' || o.type === 'boolean')).map((option) => {
1758
+ if (option.type === 'boolean') return `<div class="agy-kv"><span class="agy-kv-label">${esc(option.name || option.id)}</span><button type="button" class="agy-switch" role="switch" aria-checked="${option.currentValue === true}" aria-label="${esc(option.name || option.id)}" data-pop-bool="${esc(option.id)}"${!tab || tab.automationRunId ? ' disabled' : ''}></button></div>${option.description ? `<div class="agy-kv-help">${esc(option.description)}</div>` : ''}`;
1759
+ return `<div class="agy-kv"><span class="agy-kv-label">${esc(option.name || option.id)}</span>${dropdownHTML(`agy-opt-${option.id.replace(/[^A-Za-z0-9_-]/g, '_')}`, dropdownEntries(option), { label: option.name || option.id, inputAttrs: `data-pop-select="${esc(option.id)}"`, disabled: !tab || !!tab.automationRunId })}</div>${option.description ? `<div class="agy-kv-help">${esc(option.description)}</div>` : ''}`;
1760
+ }).join('');
1761
+ const modeRow = !optionByCategory(options, 'mode') && Array.isArray(rt?.modes?.availableModes) && rt.modes.availableModes.length
1762
+ ? `<div class="agy-kv"><span class="agy-kv-label">${esc(T('mode'))}</span>${dropdownHTML('agy-opt-mode', modeEntries(rt.modes), { label: T('mode'), inputAttrs: 'data-pop-mode="1"', disabled: !tab || !!tab.automationRunId })}</div>` : '';
1763
+ const sessionOptions = `<section class="agy-sec"><div class="agy-sec-head"><span class="agy-sec-title">${esc(T('settingsOptions'))}</span></div>${optionRows || modeRow ? optionRows + modeRow : `<div class="agy-kv-help">${esc(tab?.sessionId ? T('settingsNoOptions') : T('settingsStartSession'))}</div>`}${secondaryOptions(options).length ? '' : ''}</section>`;
1764
+ const session = `<section class="agy-sec"><div class="agy-sec-head"><span class="agy-sec-title">${esc(T('settingsSession'))}</span>${tab?.sessionId && caps.fork ? `<button type="button" class="agy-action" data-pop="fork">${esc(T('fork'))}</button>` : ''}</div>`
1765
+ + kv(T('settingsSessionId'), tab?.sessionId ? `<button type="button" data-pop="copy-id" title="${esc(T('copy'))}">${esc(tab.sessionId)}</button>` : esc(T('settingsNoSession')), { mono: !!tab?.sessionId, muted: !tab?.sessionId, raw: true })
1766
+ + kv(T('project'), tab?.project ? relativeToProject('', tab.project) : T('chooseProject'), { mono: !!tab?.project, muted: !tab?.project })
1767
+ + (foldersOf(tab).length ? kv(T('settingsFolders'), foldersOf(tab).map((p) => p.split(/[\\/]/).pop()).join(', ')) : '')
1768
+ + kv(T('settingsTurns'), String((rt?.transcript.turns || []).length))
1769
+ + '</section>';
1770
+ const detailSeg = `<div class="agy-seg" role="group" aria-label="${esc(T('settingsDetail'))}">${DETAIL_LEVELS.map((level) => `<button type="button" data-pop-detail="${level}" aria-pressed="${_prefs.detail === level}">${esc(T(`detail_${level}`))}</button>`).join('')}</div>`;
1771
+ const running = _tabs.some((entry) => entry.running);
1772
+ const panel = `<section class="agy-sec"><div class="agy-sec-head"><span class="agy-sec-title">${esc(T('settingsPanel'))}</span></div>`
1773
+ + `<div class="agy-kv"><span class="agy-kv-label">${esc(T('settingsDetail'))}</span>${detailSeg}</div><div class="agy-kv-help">${esc(T(`detailHelp_${_prefs.detail}`))}</div>`
1774
+ + `<div class="agy-kv"><span class="agy-kv-label">${esc(T('settingsClientFs'))}</span><button type="button" class="agy-switch" role="switch" aria-checked="${!!_prefs.clientFs}" aria-label="${esc(T('settingsClientFs'))}" data-pop-cap="clientFs"${!caps.clientFs || running ? ' disabled' : ''}></button></div><div class="agy-kv-help">${esc(T(caps.clientFs ? 'settingsClientFsHelp' : 'settingsNeedsRestart'))}</div>`
1775
+ + `<div class="agy-kv"><span class="agy-kv-label">${esc(T('settingsClientTerminal'))}</span><button type="button" class="agy-switch" role="switch" aria-checked="${!!_prefs.clientTerminal}" aria-label="${esc(T('settingsClientTerminal'))}" data-pop-cap="clientTerminal"${!caps.clientTerminal || running ? ' disabled' : ''}></button></div><div class="agy-kv-help">${esc(T(caps.clientTerminal ? 'settingsClientTerminalHelp' : 'settingsNeedsRestart'))}</div>`
1776
+ + '</section>';
1777
+ const signedIn = hello.running && !(rt?.auth);
1778
+ const account = `<section class="agy-sec"><div class="agy-sec-head"><span class="agy-sec-title">${esc(T('settingsAccount'))}</span>${hello.running ? (signedIn && caps.logout ? `<button type="button" class="agy-action" data-pop="logout">${esc(T('signOut'))}</button>` : `<button type="button" class="agy-action" data-pop="login">${esc(T('signIn'))}</button>`) : ''}</div>`
1779
+ + kv(T('settingsSignIn'), hello.running ? (rt?.auth ? T('settingsSignedOut') : T('settingsSignedIn')) : T('settingsAgentOff'), { muted: !hello.running })
1780
+ + kv(T('settingsMethods'), (hello.authMethods || []).map((m) => m?.name || m?.id).filter(Boolean).join(', ') || '—', { muted: !(hello.authMethods || []).length })
1781
+ + '</section>';
1782
+ const about = `<section class="agy-sec"><div class="agy-sec-head"><span class="agy-sec-title">${esc(T('settingsAbout'))}</span></div>`
1783
+ + kv(T('settingsAgent'), hello.agent ? `${hello.agent.title || hello.agent.name || 'Antigravity'} ${hello.agent.version || ''}`.trim() : '—', { muted: !hello.agent })
1784
+ + kv(T('settingsProtocol'), 'ACP', { mono: true })
1785
+ + `<div class="agy-kv"><span class="agy-kv-label">${esc(T('settingsLinks'))}</span><span class="agy-kv-value"><a class="agy-setup-guide" href="https://antigravity.google/docs/cli/reference" target="_blank" rel="noopener noreferrer">${esc(T('cliReference'))}</a></span></div>`
1786
+ + '</section>';
1787
+ return context + session + sessionOptions + panel + account + about;
1788
+ }
1789
+
1790
+ function refreshPop() {
1791
+ if (_pop?.kind !== 'settings') return;
1792
+ if (_pop.el.querySelector('.cc-dropdown.open')) return; // never redraw under an open menu
1793
+ const scroll = _pop.el.scrollTop;
1794
+ _pop.el.innerHTML = settingsHtml();
1795
+ _pop.el.scrollTop = scroll;
1796
+ _pop.place?.();
1797
+ }
1798
+
1799
+ function openSettingsPop(anchor) {
1800
+ if (!anchor) return;
1801
+ if (_pop?.kind === 'settings') { closePop(); return; }
1802
+ openPop(anchor, 'settings', settingsHtml(), {
1803
+ wide: true,
1804
+ align: 'right',
1805
+ onClick: async (event) => {
1806
+ const tab = activeTab();
1807
+ const action = event.target.closest('[data-pop]')?.dataset.pop;
1808
+ if (action === 'compact' && tab && !tab.running) { closePop(); send({ text: '/compact', staged: [] }); return; }
1809
+ if (action === 'fork') { closePop(); forkSession(tab); return; }
1810
+ if (action === 'copy-id' && tab?.sessionId) { copyText(tab.sessionId); return; }
1811
+ if (action === 'logout') { signOut(); return; }
1812
+ if (action === 'login') { const rt = runtimeOf(tab); if (rt) rt.auth = { state: 'needed' }; closePop(); scheduleRender(tab); return; }
1813
+ const bool = event.target.closest('[data-pop-bool]');
1814
+ if (bool && tab && !bool.disabled) {
1815
+ const option = (runtimeOf(tab).configOptions || []).find((o) => o.id === bool.dataset.popBool);
1816
+ if (option) setConfig(tab, option.id, toggledValue(option));
1817
+ return;
1818
+ }
1819
+ const detail = event.target.closest('[data-pop-detail]');
1820
+ if (detail) { setDetail(detail.dataset.popDetail); return; }
1821
+ const cap = event.target.closest('[data-pop-cap]');
1822
+ if (cap && !cap.disabled) {
1823
+ const key = cap.dataset.popCap;
1824
+ _prefs[key] = !_prefs[key];
1825
+ savePrefs();
1826
+ refreshPop();
1827
+ try { await client().refreshHello('agent:restart', helloPayload()); } catch (error) {
1828
+ const rt = runtimeOf(tab);
1829
+ if (rt) { rt.notice = { text: T('errorPrefix', { message: error.message }), severity: 'error' }; scheduleRender(tab); }
1830
+ }
1831
+ for (const entry of _tabs) runtimeOf(entry).loaded = false;
1832
+ if (tab?.sessionId) ensureSession(tab).catch(() => {});
1833
+ refreshPop();
1834
+ }
1835
+ },
1836
+ onChange: (event) => {
1837
+ const tab = activeTab();
1838
+ const target = event.target;
1839
+ if (!tab || !target.value) return;
1840
+ if (target.dataset.popSelect) setConfig(tab, target.dataset.popSelect, target.value);
1841
+ else if (target.dataset.popMode) setMode(tab, target.value);
1842
+ },
1843
+ });
1844
+ }
1845
+
1846
+ function openFolderMenu(anchor) {
1847
+ const tab = activeTab();
1848
+ if (!tab || !anchor) return;
1849
+ if (_pop?.kind === 'folders') { closePop(); return; }
1850
+ const taken = new Set([tab.project, ...foldersOf(tab)]);
1851
+ const choices = _projects.filter((p) => !taken.has(p.path));
1852
+ openPop(anchor, 'folders', `<div class="agy-pop-note">${esc(T('folderHelp'))}</div>${choices.length ? choices.map((p) => `<button type="button" class="agy-pop-item" role="menuitem" data-folder-add="${esc(p.path)}"><span class="agy-pop-item-main"><span class="agy-pop-item-title">${esc(p.label || p.path.split(/[\\/]/).pop())}</span><span class="agy-pop-item-meta">${esc(p.path)}</span></span></button>`).join('') : `<div class="agy-pop-note">${esc(T('folderNone'))}</div>`}`, {
1853
+ onClick: (event) => {
1854
+ const add = event.target.closest('[data-folder-add]');
1855
+ if (!add) return;
1856
+ closePop();
1857
+ setFolders(tab, [...foldersOf(tab), add.dataset.folderAdd]);
1858
+ },
1859
+ });
1860
+ }
1861
+
1862
+ async function setFolders(tab, folders) {
1863
+ tab.additionalDirectories = [...new Set(folders)].filter((p) => p && p !== tab.project).slice(0, MAX_FOLDERS);
1864
+ saveTabs();
1865
+ renderProjectBar();
1866
+ const rt = runtimeOf(tab);
1867
+ // A session that is open takes the new folders now where the agent can resume with them; otherwise the next one.
1868
+ if (tab.sessionId && rt.loaded && agentCaps().resume && !tab.running) {
1869
+ rt.loaded = false;
1870
+ try { await ensureSession(tab); rt.notice = { text: T('foldersApplied'), severity: 'info' }; }
1871
+ catch (error) { rt.notice = { text: T('errorPrefix', { message: error.message }), severity: 'error' }; }
1872
+ } else if (tab.sessionId) {
1873
+ rt.notice = { text: T('foldersNext'), severity: 'info' };
1874
+ }
1875
+ scheduleRender(tab);
1876
+ }
1877
+
1878
+ // ── Greeting (shared/greeting/, lib/greeting/) ──
1879
+ // What SynaBun says when a person comes back and opens a new conversation: one or two sentences in the empty
1880
+ // state, written by SynaBun ahead of time (the conversation's model never greets). Only for a conversation this page
1881
+ // started (no history of its own) and never a dispatched run's. Its id goes with the first prompt (`greetingId`,
1882
+ // while the bridge lists 'greeting'), so the model is told once what was said.
1883
+ const GREETING_SURFACE = 'antigravity-panel';
1884
+ function offerGreeting(tab, host) {
1885
+ if (!tab || !host || !tab.project || tab.automationRunId || tab.automationActive || tab.releasedRunId) return;
1886
+ const rt = runtimeOf(tab);
1887
+ if ((tab.sessionId && !rt.fresh) || rt.transcript.rows.length) return;
1888
+ const session = tab.id;
1889
+ if (host.dataset.greetingFor === session) return;
1890
+ host.dataset.greetingFor = session;
1891
+ mountGreeting(host, {
1892
+ surface: GREETING_SURFACE, session, cwd: tab.project,
1893
+ onOpenMemory: (id) => { if (id) emit('memory:open', { id }); },
1894
+ isCurrent: () => tab === activeTab() && host.isConnected && !runtimeOf(tab).transcript.rows.length,
1895
+ }).then((greeting) => {
1896
+ const now = runtimeOf(tab);
1897
+ if (now && !now.transcript.rows.length) now.greeting = greeting ? { id: greeting.id, session } : null;
1898
+ }).catch(() => { /* a conversation always opens */ });
1899
+ }
1900
+ /** The greeting's id for a tab's first prompt, once ('' when it opened with none, or the bridge cannot tell it). */
1901
+ function takeGreetingId(tab) {
1902
+ const rt = runtimeOf(tab);
1903
+ const held = rt?.greeting;
1904
+ if (!held) return '';
1905
+ rt.greeting = null;
1906
+ forgetGreeting(GREETING_SURFACE, held.session);
1907
+ return _client?.supports('greeting') ? held.id : '';
1908
+ }
1909
+
1910
+ // ── Attachments ──
1911
+ function readAsDataUrl(file) {
1912
+ return new Promise((resolve, reject) => {
1913
+ const reader = new FileReader();
1914
+ reader.onload = () => resolve(String(reader.result || ''));
1915
+ reader.onerror = () => reject(reader.error || new Error('read failed'));
1916
+ reader.readAsDataURL(file);
1917
+ });
1918
+ }
1919
+ async function stageFiles(files) {
1920
+ const tab = activeTab();
1921
+ const rt = runtimeOf(tab);
1922
+ if (!rt || tab.automationActive) return;
1923
+ for (const file of files) {
1924
+ const problem = attachmentProblem(file, rt.staged);
1925
+ if (problem) { rt.notice = { text: T(problem.key, problem.params), severity: 'warning' }; scheduleRender(tab); continue; }
1926
+ try {
1927
+ const dataUrl = await readAsDataUrl(file);
1928
+ rt.staged.push({ name: file.name || 'pasted', mimeType: attachmentType(file), dataUrl });
1929
+ } catch (error) {
1930
+ rt.notice = { text: T('errorPrefix', { message: error.message }), severity: 'error' };
1931
+ }
1932
+ }
1933
+ renderAttachments();
1934
+ renderComposer();
1935
+ scheduleRender(tab);
1936
+ }
1937
+
1938
+ /** A picture from elsewhere in the app (a data URL): staged on the active tab, the panel shown. */
1939
+ function stagePicture(dataUrl) {
1940
+ if (!stagedPicture(dataUrl)) return false; // not a picture: the panel stays as it is
1941
+ if (!_visible) setVisible(true);
1942
+ const tab = activeTab() || createTab();
1943
+ const rt = runtimeOf(tab);
1944
+ if (!rt || tab.automationActive) return false;
1945
+ const checked = stagedPicture(dataUrl, rt.staged);
1946
+ if (checked.problem) { rt.notice = { text: T(checked.problem.key, checked.problem.params), severity: 'warning' }; scheduleRender(tab); return false; }
1947
+ rt.staged.push(checked.file);
1948
+ renderAttachments();
1949
+ renderComposer();
1950
+ scheduleRender(tab);
1951
+ $('.agy-input')?.focus();
1952
+ return true;
1953
+ }
1954
+
1955
+ // Voice: a recording becomes an audio attachment (only when the agent takes audio).
1956
+ async function toggleRecording() {
1957
+ const mic = $('.agy-mic');
1958
+ if (_recorder) { _recorder.stop(); return; }
1959
+ try {
1960
+ const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
1961
+ const recorder = new MediaRecorder(stream);
1962
+ const chunks = [];
1963
+ recorder.ondataavailable = (event) => { if (event.data?.size) chunks.push(event.data); };
1964
+ recorder.onstop = async () => {
1965
+ stream.getTracks().forEach((track) => track.stop());
1966
+ _recorder = null;
1967
+ mic?.classList.remove('recording');
1968
+ mic?.setAttribute('aria-pressed', 'false');
1969
+ const type = (recorder.mimeType || 'audio/webm').split(';')[0];
1970
+ const blob = new Blob(chunks, { type });
1971
+ if (blob.size) await stageFiles([new File([blob], `voice-${new Date().toISOString().slice(11, 19).replace(/:/g, '')}.${type.split('/')[1] || 'webm'}`, { type })]);
1972
+ };
1973
+ recorder.start();
1974
+ _recorder = recorder;
1975
+ mic?.classList.add('recording');
1976
+ mic?.setAttribute('aria-pressed', 'true');
1977
+ } catch (error) {
1978
+ const rt = runtimeOf(activeTab());
1979
+ if (rt) { rt.notice = { text: T('micFailed', { error: error.message }), severity: 'warning' }; scheduleRender(); }
1980
+ }
1981
+ }
1982
+
1983
+ // ── Building and wiring ──
1984
+ function buildPanel() {
1985
+ const panel = document.createElement('aside');
1986
+ panel.className = 'agy-panel';
1987
+ panel.setAttribute('aria-label', T('title'));
1988
+ panel.innerHTML = `
1989
+ <div class="agy-resize-handle" aria-hidden="true"></div>
1990
+ <header class="agy-header">
1991
+ <button type="button" class="agy-session-btn" aria-haspopup="menu" aria-expanded="false"><span class="agy-mark">${ICON}</span><span class="agy-session-label">${esc(T('newSession'))}</span><span class="agy-bypass-badge" hidden>${esc(T('permBypass'))}</span><span class="agy-dd-arrow" aria-hidden="true">&#x25BE;</span></button>
1992
+ <button type="button" class="agy-header-rename" title="${esc(T('rename'))}" aria-label="${esc(T('rename'))}">${SVG.edit}</button>
1993
+ <div class="agy-actions">
1994
+ <button type="button" class="agy-btn agy-cog" aria-haspopup="dialog" aria-expanded="false" aria-label="${esc(T('settingsTitle'))}" title="${esc(T('settingsTitle'))}">${SVG.cog}<i class="agy-cog-dot" hidden></i></button>
1995
+ <button type="button" class="agy-btn agy-new" aria-label="${esc(T('newTab'))}" title="${esc(T('newTab'))}">${SVG.plus}</button>
1996
+ <button type="button" class="agy-btn agy-minimize" aria-label="${esc(T('minimize'))}" title="${esc(T('minimize'))}">${SVG.minus}</button>
1997
+ <button type="button" class="agy-btn agy-btn-danger agy-close-tab" aria-label="${esc(T('closeTab'))}" title="${esc(T('closeTab'))}">${SVG.close}</button>
1998
+ </div>
1999
+ </header>
2000
+ <div class="agy-tabs" role="tablist" aria-label="${esc(T('sessions'))}"></div>
2001
+ <div class="agy-messages-container">
2002
+ <div class="agy-transcript" role="log" aria-live="polite" aria-label="${esc(T('transcript'))}"></div>
2003
+ <button type="button" class="agy-jump" hidden>&darr; ${esc(T('jumpLatest'))}</button>
2004
+ </div>
2005
+ <div class="agy-bottom">
2006
+ <div class="agy-banner" role="status" hidden></div>
2007
+ <div class="agy-queue" hidden></div>
2008
+ <div class="agy-projectbar">
2009
+ ${dropdownHTML('agy-project', [{ value: '', label: T('chooseProject'), selected: true }], { label: T('project') })}
2010
+ ${dropdownHTML('agy-branch', [{ value: '', label: T('branch'), selected: true }], { label: T('branch'), disabled: true })}
2011
+ <span class="agy-folders"></span>
2012
+ <button type="button" class="agy-bar-action agy-add-folder" aria-label="${esc(T('folderAdd'))}" title="${esc(T('folderAdd'))}">${SVG.folder}</button>
2013
+ </div>
2014
+ <div class="agy-staged" hidden></div>
2015
+ <div class="agy-bypass-confirm" role="alertdialog" aria-label="${esc(T('permBypassConfirmTitle'))}" hidden>
2016
+ <div class="agy-bypass-confirm-text"><strong>${esc(T('permBypassConfirmTitle'))}</strong><span>${esc(T('permBypassConfirmBody'))}</span></div>
2017
+ <div class="agy-bypass-confirm-actions"><button type="button" class="agy-action" data-bypass-no="1">${esc(T('cancel'))}</button><button type="button" class="agy-action agy-btn-deny" data-bypass-yes="1">${esc(T('permBypassConfirmYes'))}</button></div>
2018
+ </div>
2019
+ <div class="agy-bypass-strip" role="status" hidden></div>
2020
+ <form class="agy-composer" autocomplete="off">
2021
+ <div class="agy-input-wrap"><div class="agy-input-shell">
2022
+ <textarea class="agy-input" rows="1" placeholder="${esc(T('placeholder'))}" aria-label="${esc(T('placeholder'))}" aria-autocomplete="list" aria-controls="agy-hints" spellcheck="false"></textarea>
2023
+ <button type="button" class="agy-mic" aria-pressed="false" aria-label="${esc(T('mic'))}" title="${esc(T('mic'))}" hidden>${SVG.mic}</button>
2024
+ <button type="submit" class="agy-send" aria-label="${esc(T('send'))}" data-mode="send" disabled>${SVG.send}</button>
2025
+ </div></div>
2026
+ </form>
2027
+ <div class="agy-hints" id="agy-hints" role="listbox" aria-label="${esc(T('commands'))}" hidden></div>
2028
+ <div class="agy-footer">
2029
+ <div class="agy-footer-left">
2030
+ <a class="agy-brand-link" href="https://antigravity.google/docs/cli/reference" target="_blank" rel="noopener noreferrer" title="${esc(T('cliReference'))}" aria-label="${esc(T('cliReference'))}">${ICON}</a>
2031
+ <button type="button" class="agy-attach" aria-label="${esc(T('attach'))}" title="${esc(T('attach'))}">${SVG.clip}</button>
2032
+ <input type="file" class="agy-file" multiple hidden>
2033
+ <span class="agy-status-line" aria-live="polite"></span>
2034
+ </div>
2035
+ <div class="agy-footer-right">
2036
+ ${dropdownHTML('agy-perm', bypassControl({}, T).entries, { label: T('permissions'), classes: 'agy-perm-dd' })}
2037
+ ${dropdownHTML('agy-mode', [{ value: '', label: T('mode'), selected: true }], { label: T('mode'), disabled: true })}
2038
+ ${dropdownHTML('agy-thinking', [{ value: '', label: T('thinking'), selected: true }], { label: T('thinking'), disabled: true })}
2039
+ ${dropdownHTML('agy-model', [{ value: '', label: T('modelDefault'), selected: true }], { label: T('model'), disabled: true })}
2040
+ </div>
2041
+ </div>
2042
+ </div>`;
2043
+ return panel;
2044
+ }
2045
+
2046
+ function toggleCard(head) {
2047
+ const card = head.closest('.agy-card');
2048
+ if (!card) return;
2049
+ const collapsed = card.classList.toggle('agy-collapsed');
2050
+ head.setAttribute('aria-expanded', String(!collapsed));
2051
+ const id = card.dataset.tool;
2052
+ const rt = runtimeOf(activeTab());
2053
+ if (id && rt) {
2054
+ if (collapsed) { rt.opened.delete(id); rt.closed.add(id); } else { rt.closed.delete(id); rt.opened.add(id); }
2055
+ }
2056
+ }
2057
+
2058
+ function patchText(rt, path) {
2059
+ for (const row of rt.transcript.rows) {
2060
+ if (row.kind !== 'tool') continue;
2061
+ const item = row.content.find((c) => c.type === 'diff' && c.path === path);
2062
+ if (!item) continue;
2063
+ const view = diffView(item);
2064
+ const lines = [`--- ${item.oldText === null ? '/dev/null' : `a/${path}`}`, `+++ b/${path}`];
2065
+ for (const r of view.rows) {
2066
+ if (r.kind === 'hunk') lines.push(r.text);
2067
+ else if (r.kind === 'add') lines.push(`+${r.text}`);
2068
+ else if (r.kind === 'delete') lines.push(`-${r.text}`);
2069
+ else if (r.kind === 'context') lines.push(` ${r.text}`);
2070
+ }
2071
+ return lines.join('\n');
2072
+ }
2073
+ return '';
2074
+ }
2075
+
2076
+ function wire() {
2077
+ wireDropdowns(_panel);
2078
+ $('.agy-minimize').addEventListener('click', () => setVisible(false));
2079
+ $('.agy-close-tab').addEventListener('click', () => closeTab(_activeIdx));
2080
+ $('.agy-new').addEventListener('click', () => newSessionTab());
2081
+ $('.agy-session-btn').addEventListener('click', (event) => openSessionsMenu(event.currentTarget));
2082
+ $('.agy-header-rename').addEventListener('click', () => startRename());
2083
+ $('.agy-cog').addEventListener('click', (event) => openSettingsPop(event.currentTarget));
2084
+ $('.agy-add-folder').addEventListener('click', (event) => openFolderMenu(event.currentTarget));
2085
+ $('.agy-folders').addEventListener('click', (event) => {
2086
+ const remove = event.target.closest('[data-folder-remove]');
2087
+ const tab = activeTab();
2088
+ if (remove && tab) setFolders(tab, foldersOf(tab).filter((p) => p !== remove.dataset.folderRemove));
2089
+ });
2090
+ $('.agy-tabs').addEventListener('click', (event) => {
2091
+ const close = event.target.closest('[data-close]');
2092
+ if (close) { event.stopPropagation(); closeTab(Number(close.dataset.close)); return; }
2093
+ const tabEl = event.target.closest('.agy-tab');
2094
+ if (tabEl) switchTab(Number(tabEl.dataset.idx));
2095
+ });
2096
+ $('.agy-tabs').addEventListener('dblclick', (event) => {
2097
+ const tabEl = event.target.closest('.agy-tab');
2098
+ if (!tabEl) return;
2099
+ switchTab(Number(tabEl.dataset.idx));
2100
+ startRename();
2101
+ });
2102
+ $('.agy-tabs').addEventListener('auxclick', (event) => {
2103
+ const tabEl = event.target.closest('.agy-tab');
2104
+ if (tabEl && event.button === 1) closeTab(Number(tabEl.dataset.idx));
2105
+ });
2106
+ $('#agy-project').addEventListener('change', (event) => {
2107
+ const value = event.target.value;
2108
+ const tab = activeTab();
2109
+ if (!value) return;
2110
+ storage.setItem(STOR.project, value);
2111
+ if (tab && !tab.sessionId && !tab.running && !tab.automationRunId) { tab.project = value; tab.additionalDirectories = foldersOf(tab).filter((p) => p !== value); saveTabs(); renderAll(); return; }
2112
+ createTab({ project: value });
2113
+ });
2114
+ $('#agy-branch').addEventListener('change', (event) => { if (event.target.value) switchBranch(event.target.value); });
2115
+ $('#agy-model').addEventListener('change', (event) => {
2116
+ const tab = activeTab();
2117
+ if (!tab || !event.target.value) return;
2118
+ if (event.target.dataset.configId) setConfig(tab, event.target.dataset.configId, event.target.value);
2119
+ else setModel(tab, event.target.value);
2120
+ });
2121
+ $('#agy-thinking').addEventListener('change', (event) => {
2122
+ const tab = activeTab();
2123
+ if (tab && event.target.dataset.configId && event.target.value) setConfig(tab, event.target.dataset.configId, event.target.value);
2124
+ });
2125
+ $('#agy-perm').addEventListener('change', (event) => {
2126
+ const tab = activeTab();
2127
+ if (!tab) return;
2128
+ const action = pickPermission(event.target.value, { bypass: tab.bypass === true, supported: agentCaps().bypass });
2129
+ if (action?.confirm) { _bypassConfirmFor = tab.id; renderBypass(); $('[data-bypass-yes]')?.focus(); return; }
2130
+ _bypassConfirmFor = null;
2131
+ if (action) setBypass(tab, action.set); else renderBypass();
2132
+ });
2133
+ $('.agy-bypass-confirm').addEventListener('click', (event) => {
2134
+ const tab = activeTab();
2135
+ if (event.target.closest('[data-bypass-yes]')) { _bypassConfirmFor = null; if (tab) setBypass(tab, true); return; }
2136
+ if (event.target.closest('[data-bypass-no]')) { _bypassConfirmFor = null; renderBypass(); $('.agy-input')?.focus(); }
2137
+ });
2138
+ $('.agy-bypass-confirm').addEventListener('keydown', (event) => {
2139
+ if (event.key === 'Escape') { event.preventDefault(); event.stopPropagation(); _bypassConfirmFor = null; renderBypass(); $('.agy-input')?.focus(); }
2140
+ });
2141
+ $('#agy-mode').addEventListener('change', (event) => {
2142
+ const tab = activeTab();
2143
+ if (!tab || !event.target.value) return;
2144
+ if (event.target.dataset.configId) setConfig(tab, event.target.dataset.configId, event.target.value);
2145
+ else setMode(tab, event.target.value);
2146
+ });
2147
+ // A picker or the composer on a tab without a session yet: the session starts, so the pickers show its choices.
2148
+ const warm = () => { const tab = activeTab(); if (tab?.project && !tab.sessionId && !tab.automationRunId && !blockedNow()) ensureSession(tab).catch(() => {}); };
2149
+ $('.agy-footer-right').addEventListener('pointerdown', (event) => { if (event.target.closest('.cc-dropdown')) warm(); });
2150
+ const input = $('.agy-input');
2151
+ input.addEventListener('focus', warm);
2152
+ input.addEventListener('input', () => { autosize(); renderComposer(); updateHints(); const tab = activeTab(); if (tab) { tab.draft = input.value; } });
2153
+ input.addEventListener('click', () => updateHints());
2154
+ input.addEventListener('blur', () => setTimeout(() => { if (!$('.agy-hints')?.matches(':hover')) hideHints(); }, 150));
2155
+ $('.agy-hints').addEventListener('mousedown', (event) => event.preventDefault());
2156
+ $('.agy-hints').addEventListener('click', (event) => {
2157
+ const item = event.target.closest('[data-hint]');
2158
+ if (item) chooseHint(Number(item.dataset.hint));
2159
+ });
2160
+ input.addEventListener('keydown', (event) => {
2161
+ if (_hints?.items.length) {
2162
+ if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
2163
+ event.preventDefault();
2164
+ const step = event.key === 'ArrowDown' ? 1 : -1;
2165
+ _hints.active = (_hints.active + step + _hints.items.length) % _hints.items.length;
2166
+ drawHints();
2167
+ return;
2168
+ }
2169
+ if (event.key === 'Tab' || (event.key === 'Enter' && !event.shiftKey && !event.isComposing)) { event.preventDefault(); chooseHint(_hints.active); return; }
2170
+ if (event.key === 'Escape') { event.preventDefault(); event.stopPropagation(); hideHints(); return; }
2171
+ }
2172
+ if (event.key === 'Enter' && !event.shiftKey && !event.isComposing) { event.preventDefault(); send(); return; }
2173
+ if (event.key === 'ArrowUp' && !input.value) {
2174
+ const last = lastPrompt(runtimeOf(activeTab())?.transcript || createTranscript());
2175
+ if (last) { event.preventDefault(); input.value = last.text; autosize(); renderComposer(); }
2176
+ }
2177
+ });
2178
+ input.addEventListener('paste', (event) => {
2179
+ const files = [...(event.clipboardData?.files || [])];
2180
+ if (files.length) { event.preventDefault(); stageFiles(files); }
2181
+ });
2182
+ const form = $('.agy-composer');
2183
+ form.addEventListener('submit', (event) => { event.preventDefault(); if ($('.agy-send').dataset.mode === 'stop') stop(); else send(); });
2184
+ _panel.addEventListener('dragover', (event) => { if (event.dataTransfer?.types?.includes('Files')) { event.preventDefault(); _panel.classList.add('agy-dragging'); } });
2185
+ _panel.addEventListener('dragleave', (event) => { if (!_panel.contains(event.relatedTarget)) _panel.classList.remove('agy-dragging'); });
2186
+ _panel.addEventListener('drop', (event) => { _panel.classList.remove('agy-dragging'); const files = [...(event.dataTransfer?.files || [])]; if (files.length) { event.preventDefault(); stageFiles(files); } });
2187
+ $('.agy-attach').addEventListener('click', () => $('.agy-file').click());
2188
+ $('.agy-file').addEventListener('change', (event) => { stageFiles([...event.target.files]); event.target.value = ''; });
2189
+ $('.agy-mic').addEventListener('click', () => toggleRecording());
2190
+ $('.agy-staged').addEventListener('click', (event) => {
2191
+ const button = event.target.closest('[data-unstage]');
2192
+ if (!button) return;
2193
+ runtimeOf(activeTab())?.staged.splice(Number(button.dataset.unstage), 1);
2194
+ renderAttachments();
2195
+ renderComposer();
2196
+ });
2197
+ $('.agy-queue').addEventListener('click', (event) => {
2198
+ const tab = activeTab();
2199
+ const rt = runtimeOf(tab);
2200
+ if (!rt) return;
2201
+ const action = event.target.closest('[data-queue]')?.dataset.queue;
2202
+ if (action === 'pause') { rt.queuePaused = !rt.queuePaused; renderQueue(); if (!rt.queuePaused) drainQueue(tab); return; }
2203
+ if (action === 'clear') { rt.queue = []; renderQueue(); return; }
2204
+ const edit = event.target.closest('[data-queue-edit]');
2205
+ if (edit) {
2206
+ const [item] = rt.queue.splice(Number(edit.dataset.queueEdit), 1);
2207
+ if (item) { $('.agy-input').value = item.text; rt.staged.push(...item.staged); autosize(); renderAttachments(); renderComposer(); $('.agy-input').focus(); }
2208
+ renderQueue();
2209
+ return;
2210
+ }
2211
+ const remove = event.target.closest('[data-queue-remove]');
2212
+ if (remove) { rt.queue.splice(Number(remove.dataset.queueRemove), 1); renderQueue(); }
2213
+ });
2214
+ $('.agy-banner').addEventListener('click', (event) => {
2215
+ if (!event.target.closest('[data-conn="retry"]')) return;
2216
+ _conn.phase = 'connecting';
2217
+ renderBanner();
2218
+ client().reconnectNow().catch(() => {});
2219
+ });
2220
+ $('.agy-jump').addEventListener('click', () => {
2221
+ const rt = runtimeOf(activeTab());
2222
+ if (rt) rt.scrolledUp = false;
2223
+ const box = $('.agy-transcript');
2224
+ if (box) box.scrollTop = box.scrollHeight;
2225
+ $('.agy-jump').hidden = true;
2226
+ });
2227
+ const transcript = $('.agy-transcript');
2228
+ wireFileLinks(transcript, { cwd: () => activeTab()?.project || '' });
2229
+ transcript.addEventListener('scroll', () => {
2230
+ const rt = runtimeOf(activeTab());
2231
+ if (!rt) return;
2232
+ rt.scrolledUp = transcript.scrollHeight - transcript.scrollTop - transcript.clientHeight > 60;
2233
+ const jump = $('.agy-jump');
2234
+ if (jump) jump.hidden = !rt.scrolledUp;
2235
+ });
2236
+ // A fold the user opens or closes (a thought, the installer's output) stays that way through every redraw. Read
2237
+ // from the click on its summary, before the browser flips it: a fold drawn open by the detail level is not a choice.
2238
+ transcript.addEventListener('click', (event) => {
2239
+ const summary = event.target.closest?.('summary');
2240
+ const fold = summary?.parentElement;
2241
+ if (!fold) return;
2242
+ if (fold.matches('.agy-thought') && fold.dataset.row) runtimeOf(activeTab())?.folds.set(fold.dataset.row, !fold.open);
2243
+ else if (fold.matches('.agy-setup-log')) _setup.logOpen = !fold.open;
2244
+ }, true);
2245
+ transcript.addEventListener('input', (event) => {
2246
+ const form = event.target.closest('[data-elicit-form]');
2247
+ if (!form) return;
2248
+ const rt = runtimeOf(activeTab());
2249
+ const entry = rt?.elicitations.get(form.dataset.elicitForm);
2250
+ if (!entry) return;
2251
+ rt.elicitDrafts[form.dataset.elicitForm] = readFormValues(form, elicitationFields(entry.requestedSchema));
2252
+ });
2253
+ transcript.addEventListener('submit', (event) => {
2254
+ const form = event.target.closest('[data-elicit-form]');
2255
+ if (!form) return;
2256
+ event.preventDefault();
2257
+ const tab = activeTab();
2258
+ if (tab) submitElicitation(tab, form);
2259
+ });
2260
+ transcript.addEventListener('keydown', (event) => {
2261
+ const head = event.target.closest?.('.agy-card-head[role="button"]');
2262
+ if (head && (event.key === 'Enter' || event.key === ' ')) { event.preventDefault(); toggleCard(head); }
2263
+ });
2264
+ transcript.addEventListener('click', async (event) => {
2265
+ const tab = activeTab();
2266
+ if (!tab) return;
2267
+ const rt = runtimeOf(tab);
2268
+ const target = event.target;
2269
+ const permission = target.closest('[data-permission]');
2270
+ if (permission) { answerPermission(tab, permission.dataset.permission, permission.dataset.option || null); return; }
2271
+ const elicit = target.closest('[data-elicit]');
2272
+ if (elicit) { answerElicitation(tab, elicit.dataset.elicit, elicit.dataset.action); return; }
2273
+ const auth = target.closest('[data-auth]');
2274
+ if (auth) { if (auth.dataset.authKind === 'terminal') openSignInTerminal(tab); else signIn(tab, auth.dataset.auth); return; }
2275
+ if (target.closest('[data-auth-terminal]')) { openSignInTerminal(tab); return; }
2276
+ const setup = target.closest('[data-setup]');
2277
+ if (setup) { runSetupAction(setup.dataset.setup); return; }
2278
+ const copyTurn = target.closest('[data-copy-turn]');
2279
+ if (copyTurn) { copyText(answerOfTurn(rt.transcript, copyTurn.dataset.copyTurn), copyTurn); return; }
2280
+ const retry = target.closest('[data-retry]');
2281
+ if (retry) { retryLast(tab); return; }
2282
+ const copyPre = target.closest('[data-copy-pre]');
2283
+ if (copyPre) { copyText(copyPre.parentElement?.querySelector('pre')?.textContent || '', copyPre); return; }
2284
+ const copyDiff = target.closest('[data-copy-diff]');
2285
+ if (copyDiff) { event.preventDefault(); copyText(patchText(rt, copyDiff.dataset.copyDiff), copyDiff); return; }
2286
+ const full = target.closest('[data-diff-full]');
2287
+ if (full) { rt.fullDiffs.add(full.dataset.diffFull); scheduleRender(tab); return; }
2288
+ const head = target.closest('.agy-card-head[role="button"]');
2289
+ if (head && !target.closest('a, button, input, label')) toggleCard(head);
2290
+ });
2291
+ // Keyboard inside the panel: Escape stops a running turn (or denies an open permission), Enter allows it once.
2292
+ _panel.addEventListener('keydown', (event) => {
2293
+ if (!sidepanelAcceptsKeyboard('antigravity', event)) return;
2294
+ if (event.target.closest('.agy-session-label input')) return;
2295
+ const tab = activeTab();
2296
+ const rt = runtimeOf(tab);
2297
+ const open = rt && [...rt.permissions.entries()].find(([, entry]) => entry.state === 'open');
2298
+ const inField = event.target.closest('textarea, input, select, [contenteditable="true"]');
2299
+ if (event.key === 'Escape') {
2300
+ if (_hints) { hideHints(); event.preventDefault(); return; }
2301
+ if (open && !inField) { event.preventDefault(); answerPermission(tab, open[0], quickOption(open[1].card, 'deny')); return; }
2302
+ if (tab?.running && !tab.automationRunId) { event.preventDefault(); stop(); }
2303
+ return;
2304
+ }
2305
+ if (event.key === 'Enter' && open && !inField && !event.target.closest('button, a')) {
2306
+ const option = quickOption(open[1].card, 'allow');
2307
+ if (option) { event.preventDefault(); answerPermission(tab, open[0], option); }
2308
+ }
2309
+ });
2310
+ }
2311
+
2312
+ function setVisible(next) {
2313
+ setSidepanelVisible(PANEL_OWNER, next);
2314
+ }
2315
+
2316
+ function applyVisibility(next) {
2317
+ if (!_panel) return;
2318
+ _visible = !!next;
2319
+ _panel.classList.toggle('open', _visible);
2320
+ if (!_visible) { closePop(); hideHints(); }
2321
+ renderPills();
2322
+ emit('antigravity-panel:visibility', _visible);
2323
+ if (_visible) {
2324
+ state.lastActivePanel = 'antigravity';
2325
+ if (!_tabs.length) createTab();
2326
+ renderAll();
2327
+ setupController()?.refresh();
2328
+ if (_conn.phase !== 'blocked') client().open().catch(() => {});
2329
+ const tab = activeTab();
2330
+ if (tab?.sessionId && !runtimeOf(tab).loaded && !tab.automationRunId) ensureSession(tab).catch(() => {});
2331
+ $('.agy-input')?.focus();
2332
+ }
2333
+ window.dispatchEvent(new Event('resize'));
2334
+ }
2335
+
2336
+ let _projectsPromise = null;
2337
+ async function ensureProjects() {
2338
+ if (_projects.length) return;
2339
+ if (_projectsPromise) return _projectsPromise;
2340
+ _projectsPromise = (async () => {
2341
+ try {
2342
+ const data = await fetchProjects();
2343
+ _projects = Array.isArray(data?.projects) ? data.projects.filter((p) => p && typeof p.path === 'string') : [];
2344
+ } catch { _projects = []; }
2345
+ for (const tab of _tabs) if (!tab.project && _projects[0]) tab.project = storage.getItem(STOR.project) || _projects[0].path;
2346
+ renderProjectBar();
2347
+ scheduleRender();
2348
+ })().finally(() => { _projectsPromise = null; });
2349
+ return _projectsPromise;
2350
+ }
2351
+
2352
+ function ensurePanel() {
2353
+ if (_panel) return;
2354
+ setAntigravityTranslator((key, params) => {
2355
+ try { return t(key, params); } catch { return key; }
2356
+ });
2357
+ injectAntigravityStyles();
2358
+ ensureMarked();
2359
+ _panel = buildPanel();
2360
+ document.body.appendChild(_panel);
2361
+ registerSidepanel({
2362
+ owner: PANEL_OWNER, provider: 'antigravity', element: _panel,
2363
+ header: $('.agy-header'), actions: $('.agy-actions'), buttonClass: 'agy-btn', dockHandle: $('.agy-resize-handle'), applyVisibility,
2364
+ });
2365
+ loadTabs();
2366
+ wire();
2367
+ setupController();
2368
+ renderAll();
2369
+ on('antigravity-panel:show', ({ tabId } = {}) => {
2370
+ const idx = _tabs.findIndex((tab) => tab.id === tabId);
2371
+ if (!_visible) setVisible(true);
2372
+ if (idx >= 0) switchTab(idx);
2373
+ });
2374
+ // The whiteboard's Send to Panel (and a detached window's image): staged like a pasted picture, by the panel used
2375
+ // last only (the other panels gate it the same way, so exactly one takes it).
2376
+ on('wb:send-to-panel', ({ dataUrl } = {}) => {
2377
+ if (state.lastActivePanel !== 'antigravity' || isHostedSessionFocused()) return;
2378
+ stagePicture(dataUrl);
2379
+ });
2380
+ window.dispatchEvent(new CustomEvent('sidepanel-tray:provider-loaded', { detail: { provider: 'antigravity' } }));
2381
+ ensureProjects();
2382
+ }
2383
+
2384
+ // ── Public API ──
2385
+ export function isAntigravityPanelOpen() { return _visible; }
2386
+ export function bootAntigravityPanel() { ensurePanel(); }
2387
+ export async function toggleAntigravityPanel() {
2388
+ ensurePanel();
2389
+ if (_visible) { setVisible(false); return; }
2390
+ setVisible(true);
2391
+ await ensureProjects();
2392
+ }
2393
+ /**
2394
+ * Opens the panel with text in the composer (and a file path attached, when given). `newTab`: in a tab of its own;
2395
+ * `send`: sent at once, as if the person pressed Enter (Run Pending Remembers, the Schedules window's prompt);
2396
+ * `model`: the tab's session runs on that model when it offers it (a model it does not offer is left alone).
2397
+ * → false when there was no room for a new tab.
2398
+ */
2399
+ export async function openAntigravityWithPrompt(text = '', { path = null, newTab = false, send: sendNow = false, model = null } = {}) {
2400
+ ensurePanel();
2401
+ await ensureProjects();
2402
+ if (!_visible) setVisible(true);
2403
+ if (newTab) {
2404
+ const busy = activeTab();
2405
+ // An empty tab (a session this page warmed counts as empty) is reused rather than stacked.
2406
+ const reuse = busy && (!busy.sessionId || runtimeOf(busy).fresh) && !busy.running && !busy.automationRunId && !runtimeOf(busy).transcript.rows.length && !$('.agy-input')?.value;
2407
+ if (!reuse && !createTab({ project: busy?.project || undefined })) return false;
2408
+ }
2409
+ const input = $('.agy-input');
2410
+ if (input && text) { input.value = text; activeTab().draft = text; autosize(); }
2411
+ if (path) attachPathToAntigravity(path);
2412
+ renderComposer();
2413
+ if (model) await chooseModel(activeTab(), String(model));
2414
+ if (sendNow && text) await send();
2415
+ return true;
2416
+ }
2417
+ /** A tab's model, when its session offers it (its session starts here when it has none yet). */
2418
+ async function chooseModel(tab, model) {
2419
+ if (!tab || !model || tab.automationRunId || blockedNow()) return;
2420
+ try { await ensureSession(tab); } catch { return; }
2421
+ const option = optionByCategory(runtimeOf(tab).configOptions, 'model');
2422
+ const offered = (option?.options || []).flatMap((entry) => (Array.isArray(entry?.options) ? entry.options : [entry])).some((entry) => String(entry?.value) === model);
2423
+ if (option && offered && String(option.currentValue) !== model) await setConfig(tab, option.id, model);
2424
+ }
2425
+ /**
2426
+ * Attaches a file by its absolute path (the File Explorer's Send to, a detached window's path): the agent reads it
2427
+ * itself. `false` when nothing was attached (a dispatched run holds the tab, or no path), so the sender does not say
2428
+ * it was sent; `true` otherwise (a path already attached counts as taken).
2429
+ */
2430
+ export function attachPathToAntigravity(path) {
2431
+ ensurePanel();
2432
+ const tab = activeTab() || createTab();
2433
+ const rt = runtimeOf(tab);
2434
+ if (!rt || typeof path !== 'string' || !path || tab.automationActive) return false;
2435
+ if (!rt.staged.some((file) => file.path === path)) rt.staged.push({ name: path.split(/[\\/]/).pop() || path, path, mimeType: '' });
2436
+ renderAttachments();
2437
+ renderComposer();
2438
+ return true;
2439
+ }
2440
+ /** What a new detached window of this panel starts with (ui-sidepanel-session-entry.js): the active tab's project. */
2441
+ export function antigravityPanelDefaults() {
2442
+ return { project: activeTab()?.project || storage.getItem(STOR.project) || '' };
2443
+ }
2444
+ /** A detached window closes: its tabs end (running turns cancelled, sessions closed). */
2445
+ export function endHostedAntigravitySessions() {
2446
+ while (_tabs.length) closeTab(_tabs.length - 1);
2447
+ }
2448
+ /** Whether the panel is docked or floating (for tests and the window controller's callers). */
2449
+ export function antigravityPanelPresentation() {
2450
+ try { return getSidepanelPresentation(PANEL_OWNER)?.layout?.mode || 'docked'; } catch { return 'docked'; }
2451
+ }
2452
+
2453
+ // ── Dispatched runs (ui-native-loop-router.js) ──
2454
+ export async function attachAntigravityAutomation(run, { focus = !!run?.focus, isCancelled = () => false, bufferedEvents = [] } = {}) {
2455
+ if (!run?.runId || !run.providerSessionId) return { ok: false, reason: 'identity_pending' };
2456
+ ensurePanel();
2457
+ await ensureProjects();
2458
+ if (isCancelled()) return { ok: false, reason: 'dismissed' };
2459
+ const running = run.status === 'starting' || run.status === 'running';
2460
+ const geminiDir = runGeminiDir(run);
2461
+ const own = _tabs.find((tab) => !tab.automationRunId && tab.releasedRunId === run.runId);
2462
+ if (own && !running) {
2463
+ if (geminiDir && !own.geminiDir) { own.geminiDir = geminiDir; saveTabs(); }
2464
+ window.dispatchEvent(new CustomEvent('native-loop:dismissed', { detail: { runId: run.runId } }));
2465
+ if (focus) { setVisible(true); switchTab(_tabs.indexOf(own)); }
2466
+ return { ok: true, released: true };
2467
+ }
2468
+ let tab = _tabs.find((entry) => entry.automationRunId === run.runId);
2469
+ if (!tab) {
2470
+ tab = createTab({ project: run.cwd || run.project || '', geminiDir, activate: !!focus });
2471
+ if (!tab) return { ok: false, reason: 'tab_limit' };
2472
+ tab.automationRunId = run.runId;
2473
+ tab.sessionId = run.providerSessionId;
2474
+ tab.title = run.title || null;
2475
+ runtimeOf(tab).loaded = true; // its events are the transcript: never loaded into this window's own agent process
2476
+ for (const payload of bufferedEvents || []) applyAutomationEvent(tab, payload);
2477
+ } else {
2478
+ if (run.providerSessionId && tab.sessionId !== run.providerSessionId) tab.sessionId = run.providerSessionId;
2479
+ if (geminiDir) tab.geminiDir = geminiDir;
2480
+ }
2481
+ const restart = workerRestart(run);
2482
+ tab.automationActive = !!restart?.active || running;
2483
+ tab.automationOwnerId = nativeLoopWindowId;
2484
+ tab.automationAwaits = runAwaitsAnswer(run);
2485
+ tab.running = running && run.turnState !== 'idle';
2486
+ const rt = runtimeOf(tab);
2487
+ syncWorkerQuestions(tab, run, {
2488
+ open: (id, pending) => rt.elicitations.set(id, { ...pending.input, ...(pending.questions?.length ? { message: pending.questions.map((q) => q.header || q.question).join(' · ') } : {}), requestId: id, state: 'open', errors: {} }),
2489
+ close: (id) => { rt.elicitations.delete(id); delete rt.elicitDrafts[id]; },
2490
+ });
2491
+ refreshNeedsYou(tab);
2492
+ try {
2493
+ trackWorkerEnd(run, {
2494
+ apply: (view) => {
2495
+ if (view) { tab.automationActive = false; tab.running = false; const rt = runtimeOf(tab); rt.notice = { text: T('automationEnded'), severity: 'info' }; scheduleRender(tab); renderTabs(); renderBanner(); renderComposer(); }
2496
+ },
2497
+ });
2498
+ } catch { /* the end card is optional */ }
2499
+ saveTabs();
2500
+ if (focus) { setVisible(true); switchTab(_tabs.indexOf(tab)); }
2501
+ scheduleRender(tab);
2502
+ renderBanner();
2503
+ return { ok: true, tabId: tab.id };
2504
+ }
2505
+
2506
+ function applyAutomationEvent(tab, payload) {
2507
+ const rt = runtimeOf(tab);
2508
+ const action = automationEvent(payload);
2509
+ if (!action) return;
2510
+ if (action.kind === 'user') {
2511
+ const last = rt.transcript.rows[rt.transcript.rows.length - 1];
2512
+ if (!(last?.kind === 'user' && last.text === action.text)) addUserMessage(rt.transcript, action.text);
2513
+ tab.running = true;
2514
+ } else if (action.kind === 'update') {
2515
+ applyUpdate(rt.transcript, action.update);
2516
+ if (action.update.sessionUpdate === 'session_info_update' && rt.transcript.title) tab.title = rt.transcript.title;
2517
+ } else if (action.kind === 'notice') {
2518
+ rt.transcript.rows.push({ id: `n${Date.now()}`, kind: 'notice', text: action.text, severity: action.severity });
2519
+ }
2520
+ }
2521
+
2522
+ export function handleAntigravityAutomationEvent(runId, payload) {
2523
+ const tab = _tabs.find((entry) => entry.automationRunId === runId);
2524
+ if (!tab) return false;
2525
+ applyAutomationEvent(tab, payload);
2526
+ scheduleRender(tab);
2527
+ return true;
2528
+ }
2529
+
2530
+ export function detachAntigravityAutomation(runId) {
2531
+ const idx = _tabs.findIndex((tab) => tab.automationRunId === runId);
2532
+ if (idx < 0) return;
2533
+ const tab = _tabs[idx];
2534
+ tab.pillEl?.remove();
2535
+ _runtime.delete(tab.id);
2536
+ _tabs.splice(idx, 1);
2537
+ if (_activeIdx >= _tabs.length) _activeIdx = Math.max(0, _tabs.length - 1);
2538
+ saveTabs();
2539
+ if (_panel) renderAll();
2540
+ }
2541
+
2542
+ // For tests: the copy keys this file uses are all in AGY_COPY.
2543
+ export const _copyKeys = Object.keys(AGY_COPY);