synabun 2.1.0 → 2.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (332) hide show
  1. package/CHANGELOG.md +139 -0
  2. package/README.md +1 -1
  3. package/docs/whatsapp.md +11 -11
  4. package/lib/client-config-repair.js +13 -9
  5. package/lib/external-tools.js +6 -5
  6. package/lib/update-snapshot.js +77 -3
  7. package/lib/virustotal.mjs +12 -1
  8. package/mcp-server/dist/index.js +1 -1
  9. package/mcp-server/dist/index.js.map +1 -1
  10. package/mcp-server/dist/services/budget-judgments.d.ts +1 -1
  11. package/mcp-server/dist/services/budget-judgments.d.ts.map +1 -1
  12. package/mcp-server/dist/services/budget-judgments.js +5 -3
  13. package/mcp-server/dist/services/budget-judgments.js.map +1 -1
  14. package/mcp-server/dist/services/identity.d.ts +2 -2
  15. package/mcp-server/dist/services/identity.js +2 -2
  16. package/mcp-server/dist/services/memory-sessions.d.ts +1 -1
  17. package/mcp-server/dist/services/memory-sessions.d.ts.map +1 -1
  18. package/mcp-server/dist/services/memory-sessions.js +28 -2
  19. package/mcp-server/dist/services/memory-sessions.js.map +1 -1
  20. package/mcp-server/dist/services/neural-interface.d.ts +2 -1
  21. package/mcp-server/dist/services/neural-interface.d.ts.map +1 -1
  22. package/mcp-server/dist/services/neural-interface.js.map +1 -1
  23. package/mcp-server/dist/services/server-instructions.d.ts.map +1 -1
  24. package/mcp-server/dist/services/server-instructions.js +2 -1
  25. package/mcp-server/dist/services/server-instructions.js.map +1 -1
  26. package/mcp-server/dist/services/tool-catalog.js +1 -1
  27. package/mcp-server/dist/services/tool-catalog.js.map +1 -1
  28. package/mcp-server/dist/services/tool-list.d.ts +1 -1
  29. package/mcp-server/dist/services/tool-list.d.ts.map +1 -1
  30. package/mcp-server/dist/services/tool-runtime.d.ts +2 -2
  31. package/mcp-server/dist/services/tool-runtime.d.ts.map +1 -1
  32. package/mcp-server/dist/services/tool-runtime.js +1 -1
  33. package/mcp-server/dist/services/tool-runtime.js.map +1 -1
  34. package/mcp-server/dist/services/typesafe-config.js +1 -1
  35. package/mcp-server/dist/services/typesafe-config.js.map +1 -1
  36. package/mcp-server/dist/tools/agents.d.ts +8 -8
  37. package/mcp-server/dist/tools/agents.d.ts.map +1 -1
  38. package/mcp-server/dist/tools/agents.js +10 -9
  39. package/mcp-server/dist/tools/agents.js.map +1 -1
  40. package/mcp-server/dist/tools/choice.d.ts +6 -0
  41. package/mcp-server/dist/tools/choice.d.ts.map +1 -1
  42. package/mcp-server/dist/tools/choice.js +18 -13
  43. package/mcp-server/dist/tools/choice.js.map +1 -1
  44. package/mcp-server/dist/tools/git-tools.js +2 -2
  45. package/mcp-server/dist/tools/git-tools.js.map +1 -1
  46. package/mcp-server/dist/tools/morelogin.d.ts.map +1 -1
  47. package/mcp-server/dist/tools/morelogin.js +9 -6
  48. package/mcp-server/dist/tools/morelogin.js.map +1 -1
  49. package/mcp-server/dist/tools/skill-tools.d.ts +8 -8
  50. package/mcp-server/dist/tools/skill-tools.d.ts.map +1 -1
  51. package/mcp-server/dist/tools/skill-tools.js +11 -8
  52. package/mcp-server/dist/tools/skill-tools.js.map +1 -1
  53. package/mcp-server/package-lock.json +2 -2
  54. package/mcp-server/package.json +1 -1
  55. package/mcp-server/src/index.ts +1 -1
  56. package/mcp-server/src/services/budget-judgments.ts +5 -3
  57. package/mcp-server/src/services/identity.ts +2 -2
  58. package/mcp-server/src/services/memory-sessions.ts +24 -3
  59. package/mcp-server/src/services/neural-interface.ts +2 -1
  60. package/mcp-server/src/services/server-instructions.ts +2 -1
  61. package/mcp-server/src/services/tool-catalog.ts +1 -1
  62. package/mcp-server/src/services/tool-list.ts +1 -1
  63. package/mcp-server/src/services/tool-runtime.ts +2 -2
  64. package/mcp-server/src/services/typesafe-config.ts +1 -1
  65. package/mcp-server/src/tools/agents.ts +10 -9
  66. package/mcp-server/src/tools/choice.ts +17 -12
  67. package/mcp-server/src/tools/git-tools.ts +2 -2
  68. package/mcp-server/src/tools/morelogin.ts +6 -4
  69. package/mcp-server/src/tools/skill-tools.ts +11 -8
  70. package/neural-interface/i18n/en.json +504 -39
  71. package/neural-interface/i18n/pt-BR.json +504 -39
  72. package/neural-interface/lib/agent-sandbox-elevation.js +6 -0
  73. package/neural-interface/lib/antigravity/acp-client-host.js +554 -0
  74. package/neural-interface/lib/antigravity/acp-connection.js +299 -0
  75. package/neural-interface/lib/antigravity/bypass.js +100 -0
  76. package/neural-interface/lib/antigravity/catalog.js +273 -0
  77. package/neural-interface/lib/antigravity/home.js +260 -0
  78. package/neural-interface/lib/antigravity/migrate-gemini.js +139 -0
  79. package/neural-interface/lib/antigravity/panel-bridge.js +1058 -0
  80. package/neural-interface/lib/antigravity/policy-hook.mjs +78 -0
  81. package/neural-interface/lib/antigravity/policy.js +74 -0
  82. package/neural-interface/lib/antigravity/tool-call.js +178 -0
  83. package/neural-interface/lib/antigravity/transcripts.js +252 -0
  84. package/neural-interface/lib/antigravity/usage.js +43 -0
  85. package/neural-interface/lib/assistant-api.js +1 -0
  86. package/neural-interface/lib/assistant-brains/antigravity.js +423 -0
  87. package/neural-interface/lib/assistant-brains/codex.js +6 -1
  88. package/neural-interface/lib/assistant-budget-planner.js +7 -3
  89. package/neural-interface/lib/assistant-budget-split.js +42 -15
  90. package/neural-interface/lib/assistant-catalog.js +76 -9
  91. package/neural-interface/lib/assistant-config.js +6 -3
  92. package/neural-interface/lib/assistant-dispatch.js +211 -50
  93. package/neural-interface/lib/assistant-envelope.js +153 -1
  94. package/neural-interface/lib/assistant-evidence.js +80 -0
  95. package/neural-interface/lib/assistant-hidden-models.js +7 -5
  96. package/neural-interface/lib/assistant-persona.js +16 -11
  97. package/neural-interface/lib/assistant-plan-permissions.js +15 -0
  98. package/neural-interface/lib/assistant-router.js +237 -35
  99. package/neural-interface/lib/assistant-runtime.js +158 -12
  100. package/neural-interface/lib/assistant-task-prompt.js +10 -8
  101. package/neural-interface/lib/assistant-tools.js +2 -2
  102. package/neural-interface/lib/assistant-usage.js +29 -0
  103. package/neural-interface/lib/backup-service.js +6 -1
  104. package/neural-interface/lib/browser-guard-cdp.js +5 -4
  105. package/neural-interface/lib/browser-network-guard.js +274 -82
  106. package/neural-interface/lib/browser-tool-policy.js +12 -7
  107. package/neural-interface/lib/claude-agent-bridge.js +7 -0
  108. package/neural-interface/lib/claude-history-cache.js +210 -0
  109. package/neural-interface/lib/claude-model-catalog.js +5 -1
  110. package/neural-interface/lib/cli-install-api.js +1 -1
  111. package/neural-interface/lib/cli-install.js +160 -31
  112. package/neural-interface/lib/cli-resolver.js +9 -2
  113. package/neural-interface/lib/cli-update-planner.js +25 -15
  114. package/neural-interface/lib/codex-rollout-reader.js +52 -0
  115. package/neural-interface/lib/codex-worker-app-server.js +25 -6
  116. package/neural-interface/lib/editor-git-api.js +3 -20
  117. package/neural-interface/lib/exec-loop-command.js +9 -2
  118. package/neural-interface/lib/file-explorer-api.js +74 -40
  119. package/neural-interface/lib/file-explorer-watch.js +9 -3
  120. package/neural-interface/lib/git-bash/install.js +52 -6
  121. package/neural-interface/lib/git-bash/manager.js +59 -17
  122. package/neural-interface/lib/git-bash/pins.js +12 -0
  123. package/neural-interface/lib/git-read.js +2 -2
  124. package/neural-interface/lib/git-status-runner.js +390 -0
  125. package/neural-interface/lib/git-status-watchdog.js +40 -0
  126. package/neural-interface/lib/greeting/budgets.js +1 -0
  127. package/neural-interface/lib/greeting/engine.js +2 -2
  128. package/neural-interface/lib/greeting/sources.js +10 -12
  129. package/neural-interface/lib/greeting/writer.js +1 -1
  130. package/neural-interface/lib/mcp-client-config.js +37 -6
  131. package/neural-interface/lib/mcp-external-servers.js +40 -7
  132. package/neural-interface/lib/mcp-installer.js +2 -1
  133. package/neural-interface/lib/memory-session-capture.js +17 -3
  134. package/neural-interface/lib/morelogin.js +147 -25
  135. package/neural-interface/lib/native-loop-providers.js +423 -23
  136. package/neural-interface/lib/native-loop-runtime.js +23 -2
  137. package/neural-interface/lib/opencode-native-boundary.js +165 -22
  138. package/neural-interface/lib/owner-authority.js +1 -1
  139. package/neural-interface/lib/path-confine.js +12 -9
  140. package/neural-interface/lib/peer-identity.js +80 -6
  141. package/neural-interface/lib/pty-host/core.js +28 -1
  142. package/neural-interface/lib/pty-host/lifetime.js +12 -0
  143. package/neural-interface/lib/pty-host/manager.js +12 -1
  144. package/neural-interface/lib/remote-policy.js +1 -1
  145. package/neural-interface/lib/request-gate.js +3 -0
  146. package/neural-interface/lib/rulesets/api.js +6 -5
  147. package/neural-interface/lib/rulesets/fs-safe.js +1 -1
  148. package/neural-interface/lib/rulesets/installer.js +89 -10
  149. package/neural-interface/lib/rulesets/render.js +1 -1
  150. package/neural-interface/lib/session-cache-builder.js +64 -1
  151. package/neural-interface/lib/skills/catalog.js +5 -1
  152. package/neural-interface/lib/skills/creation-brief.js +1 -1
  153. package/neural-interface/lib/skills/creation-sources.js +19 -1
  154. package/neural-interface/lib/skills/deploy.js +9 -4
  155. package/neural-interface/lib/skills/draft-confinement.js +3 -1
  156. package/neural-interface/lib/skills/eval-confinement.js +3 -1
  157. package/neural-interface/lib/skills/eval-trace.js +16 -0
  158. package/neural-interface/lib/skills/export.js +5 -2
  159. package/neural-interface/lib/skills/known-bridge-digests.json +58 -4
  160. package/neural-interface/lib/skills/ownership.js +1 -1
  161. package/neural-interface/lib/skills/roots.js +10 -3
  162. package/neural-interface/lib/skills/runtime-checks.js +6 -3
  163. package/neural-interface/lib/skills/service.js +15 -7
  164. package/neural-interface/lib/skills/telemetry.js +6 -0
  165. package/neural-interface/lib/synabun-version.js +1 -1
  166. package/neural-interface/lib/system-activity.js +1 -1
  167. package/neural-interface/lib/whatsapp/brain.js +7 -3
  168. package/neural-interface/lib/whatsapp/cards.js +22 -9
  169. package/neural-interface/lib/whatsapp/config.js +1 -1
  170. package/neural-interface/lib/worker-confinement.js +77 -1
  171. package/neural-interface/lib/worker-question.js +74 -0
  172. package/neural-interface/lib/worker-resume.js +13 -4
  173. package/neural-interface/lib/worker-sandbox.js +46 -16
  174. package/neural-interface/package-lock.json +18 -2
  175. package/neural-interface/package.json +2 -1
  176. package/neural-interface/public/onboarding.html +1 -1
  177. package/neural-interface/public/shared/agent-runtime-options.js +89 -11
  178. package/neural-interface/public/shared/agy/agy-bypass.js +47 -0
  179. package/neural-interface/public/shared/agy/agy-client.js +266 -0
  180. package/neural-interface/public/shared/agy/agy-commands.js +120 -0
  181. package/neural-interface/public/shared/agy/agy-copy.js +326 -0
  182. package/neural-interface/public/shared/agy/agy-diff.js +161 -0
  183. package/neural-interface/public/shared/agy/agy-events.js +469 -0
  184. package/neural-interface/public/shared/agy/agy-greeting.js +26 -0
  185. package/neural-interface/public/shared/agy/agy-options.js +232 -0
  186. package/neural-interface/public/shared/agy/agy-panel.js +2543 -0
  187. package/neural-interface/public/shared/agy/agy-render.js +392 -0
  188. package/neural-interface/public/shared/agy/agy-setup.js +149 -0
  189. package/neural-interface/public/shared/agy/agy-styles.js +604 -0
  190. package/neural-interface/public/shared/agy/agy-tabs-logic.js +181 -0
  191. package/neural-interface/public/shared/api.js +4 -4
  192. package/neural-interface/public/shared/assistant/asst-agents-dock.js +3 -4
  193. package/neural-interface/public/shared/assistant/asst-api.js +15 -3
  194. package/neural-interface/public/shared/assistant/asst-brain-picker.js +56 -9
  195. package/neural-interface/public/shared/assistant/asst-budget.js +5 -4
  196. package/neural-interface/public/shared/assistant/asst-computer.js +26 -2
  197. package/neural-interface/public/shared/assistant/asst-control.js +48 -0
  198. package/neural-interface/public/shared/assistant/asst-hosts.js +16 -7
  199. package/neural-interface/public/shared/assistant/asst-limit-dock.js +6 -1
  200. package/neural-interface/public/shared/assistant/asst-limit-wait.js +38 -5
  201. package/neural-interface/public/shared/assistant/asst-motion-gate.js +80 -0
  202. package/neural-interface/public/shared/assistant/asst-panel.js +46 -9
  203. package/neural-interface/public/shared/assistant/asst-render.js +112 -18
  204. package/neural-interface/public/shared/assistant/asst-route.js +237 -57
  205. package/neural-interface/public/shared/assistant/asst-routes-editor.js +9 -8
  206. package/neural-interface/public/shared/assistant/asst-scene-styles.js +55 -23
  207. package/neural-interface/public/shared/assistant/asst-scenes.js +279 -80
  208. package/neural-interface/public/shared/assistant/asst-sidepanel-styles.js +4 -4
  209. package/neural-interface/public/shared/assistant/asst-sidepanel.js +51 -7
  210. package/neural-interface/public/shared/assistant/asst-slash.js +8 -2
  211. package/neural-interface/public/shared/assistant/asst-state.js +76 -9
  212. package/neural-interface/public/shared/assistant/asst-style-cache.js +44 -0
  213. package/neural-interface/public/shared/assistant/asst-styles.js +58 -4
  214. package/neural-interface/public/shared/assistant/asst-tool-kinds.js +34 -7
  215. package/neural-interface/public/shared/assistant/asst-usage.js +2 -1
  216. package/neural-interface/public/shared/cdx/cdx-output.js +26 -3
  217. package/neural-interface/public/shared/cdx/cdx-panel.js +14 -2
  218. package/neural-interface/public/shared/cdx/cdx-preview.js +12 -0
  219. package/neural-interface/public/shared/cdx/cdx-protocol.js +11 -10
  220. package/neural-interface/public/shared/cdx/cdx-render.js +10 -9
  221. package/neural-interface/public/shared/cdx/cdx-requests.js +11 -1
  222. package/neural-interface/public/shared/cdx/cdx-styles.js +4 -0
  223. package/neural-interface/public/shared/cdx/cdx-tabs.js +75 -5
  224. package/neural-interface/public/shared/cli-install-controller.js +218 -0
  225. package/neural-interface/public/shared/cli-install-view.js +7 -4
  226. package/neural-interface/public/shared/cli-status.js +9 -8
  227. package/neural-interface/public/shared/constants.js +3 -2
  228. package/neural-interface/public/shared/context-menu-model.js +1 -0
  229. package/neural-interface/public/shared/cp/cp-permissions.js +12 -0
  230. package/neural-interface/public/shared/cp/cp-restore.js +162 -0
  231. package/neural-interface/public/shared/cp/cp-tool-cards.js +10 -3
  232. package/neural-interface/public/shared/desktop-bridge.js +17 -0
  233. package/neural-interface/public/shared/desktop-titlebar-view.js +12 -0
  234. package/neural-interface/public/shared/desktop-titlebar.js +13 -4
  235. package/neural-interface/public/shared/desktop-update-view.js +1 -0
  236. package/neural-interface/public/shared/explorer/fx-api.js +4 -4
  237. package/neural-interface/public/shared/explorer/fx-links.js +3 -3
  238. package/neural-interface/public/shared/explorer/fx-logic.js +1 -1
  239. package/neural-interface/public/shared/explorer/fx-main.js +48 -16
  240. package/neural-interface/public/shared/explorer/fx-tree.js +1 -0
  241. package/neural-interface/public/shared/greeting/greeting-card.js +2 -2
  242. package/neural-interface/public/shared/greeting/greeting-view.js +1 -0
  243. package/neural-interface/public/shared/html-shell.js +21 -4
  244. package/neural-interface/public/shared/loading-teardown.js +34 -0
  245. package/neural-interface/public/shared/ocp-v2/ocp-v2-childpanel.js +6 -1
  246. package/neural-interface/public/shared/ocp-v2/ocp-v2-panel.js +28 -7
  247. package/neural-interface/public/shared/ocp-v2/ocp-v2-rehydrate.js +13 -4
  248. package/neural-interface/public/shared/ocp-v2/ocp-v2-render.js +17 -1
  249. package/neural-interface/public/shared/ocp-v2/ocp-v2-send.js +6 -0
  250. package/neural-interface/public/shared/ocp-v2/ocp-v2-styles.js +101 -68
  251. package/neural-interface/public/shared/ocp-v2/ocp-v2-ws.js +46 -3
  252. package/neural-interface/public/shared/provider-icons.js +4 -2
  253. package/neural-interface/public/shared/settings/settings-rules.js +2 -2
  254. package/neural-interface/public/shared/settings/settings-skill-folders-pane.js +1 -1
  255. package/neural-interface/public/shared/settings/settings-skill-folders.js +2 -2
  256. package/neural-interface/public/shared/showcase-privacy.js +6 -1
  257. package/neural-interface/public/shared/sidepanel-session-status.js +3 -3
  258. package/neural-interface/public/shared/skills/ss-creation.js +6 -3
  259. package/neural-interface/public/shared/skills/ss-dialog.js +5 -2
  260. package/neural-interface/public/shared/skills/ss-lint.js +9 -1
  261. package/neural-interface/public/shared/skills/ss-profiles.js +44 -6
  262. package/neural-interface/public/shared/skills/ss-review-view.js +2 -2
  263. package/neural-interface/public/shared/skills/studio/sx-actions.js +1 -1
  264. package/neural-interface/public/shared/skills/studio/sx-create-ai-view.js +1 -1
  265. package/neural-interface/public/shared/skills/studio/sx-create-ai.js +2 -1
  266. package/neural-interface/public/shared/skills/studio/sx-dom.js +2 -1
  267. package/neural-interface/public/shared/skills/studio/sx-fixes-view.js +359 -0
  268. package/neural-interface/public/shared/skills/studio/sx-fixes.js +327 -0
  269. package/neural-interface/public/shared/skills/studio/sx-inspector.js +1 -1
  270. package/neural-interface/public/shared/skills/studio/sx-install-flow.js +9 -6
  271. package/neural-interface/public/shared/skills/studio/sx-install.js +20 -3
  272. package/neural-interface/public/shared/skills/studio/sx-library-view.js +144 -35
  273. package/neural-interface/public/shared/skills/studio/sx-library.js +6 -2
  274. package/neural-interface/public/shared/skills/studio/sx-main.js +11 -2
  275. package/neural-interface/public/shared/skills/studio/sx-review-view.js +1 -1
  276. package/neural-interface/public/shared/skills/studio/sx-subject.js +7 -6
  277. package/neural-interface/public/shared/skills/studio/sx-workspace-view.js +6 -5
  278. package/neural-interface/public/shared/state.js +1 -1
  279. package/neural-interface/public/shared/storage-retry.js +47 -0
  280. package/neural-interface/public/shared/storage.js +25 -4
  281. package/neural-interface/public/shared/streaming-markdown.js +58 -0
  282. package/neural-interface/public/shared/styles.css +362 -35
  283. package/neural-interface/public/shared/synabun-mascot.js +27 -8
  284. package/neural-interface/public/shared/synabun-ticker.js +147 -12
  285. package/neural-interface/public/shared/text-cache.js +29 -0
  286. package/neural-interface/public/shared/tool-list-picker.js +2 -2
  287. package/neural-interface/public/shared/tool-list-view.js +2 -2
  288. package/neural-interface/public/shared/topright-overflow-logic.js +54 -0
  289. package/neural-interface/public/shared/ui-antigravity-panel.js +19 -0
  290. package/neural-interface/public/shared/ui-assistant.js +2 -0
  291. package/neural-interface/public/shared/ui-automation-studio.js +20 -12
  292. package/neural-interface/public/shared/ui-claude-panel.js +374 -134
  293. package/neural-interface/public/shared/ui-cli-banner.js +23 -167
  294. package/neural-interface/public/shared/ui-file-explorer.js +48 -6
  295. package/neural-interface/public/shared/ui-file-links.js +2 -2
  296. package/neural-interface/public/shared/ui-games.js +2 -2
  297. package/neural-interface/public/shared/ui-judgments.js +1 -1
  298. package/neural-interface/public/shared/ui-keybinds.js +8 -2
  299. package/neural-interface/public/shared/ui-link.js +3 -3
  300. package/neural-interface/public/shared/ui-loading.js +5 -4
  301. package/neural-interface/public/shared/ui-menubar.js +2 -2
  302. package/neural-interface/public/shared/ui-native-loop-router.js +4 -0
  303. package/neural-interface/public/shared/ui-navbar.js +45 -0
  304. package/neural-interface/public/shared/ui-notifications.js +1 -0
  305. package/neural-interface/public/shared/ui-resume.js +9 -7
  306. package/neural-interface/public/shared/ui-schedules-studio.js +41 -10
  307. package/neural-interface/public/shared/ui-scroll-follow.js +16 -5
  308. package/neural-interface/public/shared/ui-sessions.js +22 -12
  309. package/neural-interface/public/shared/ui-settings.js +90 -65
  310. package/neural-interface/public/shared/ui-sidepanel-session-entry.js +9 -2
  311. package/neural-interface/public/shared/ui-sidepanel-sessions.js +3 -3
  312. package/neural-interface/public/shared/ui-sidepanel-tray.js +48 -7
  313. package/neural-interface/public/shared/ui-sidepanel-windows.js +3 -3
  314. package/neural-interface/public/shared/ui-terminal.js +146 -10
  315. package/neural-interface/public/shared/ui-tool-updates.js +3 -3
  316. package/neural-interface/public/shared/ui-tooltip.js +71 -46
  317. package/neural-interface/public/shared/ui-topright-overflow.js +233 -0
  318. package/neural-interface/public/shared/ui-whiteboard.js +1 -1
  319. package/neural-interface/public/shared/ui-worker-end.js +1 -1
  320. package/neural-interface/public/shared/whatsapp/wa-view.js +2 -2
  321. package/neural-interface/public/shared/worker-questions.js +34 -0
  322. package/neural-interface/public/sidepanel-session.html +1 -1
  323. package/neural-interface/server.js +731 -466
  324. package/neural-interface/templates/rulesets/README.md +1 -1
  325. package/neural-interface/templates/rulesets/legacy-hashes.json +5 -5
  326. package/neural-interface/templates/rulesets/manifest.json +1 -1
  327. package/npm-shrinkwrap.json +18 -2
  328. package/package.json +2 -1
  329. package/skills/skill-forge/SKILL.md +4 -4
  330. package/skills/synabun/SKILL.md +1 -1
  331. package/skills/synabun/modules/schedule.md +3 -3
  332. /package/neural-interface/templates/rulesets/{gemini.md → antigravity.md} +0 -0
@@ -1,3 +1,5 @@
1
+ import { t } from './i18n.js';
2
+ import { answerWorkerQuestion, syncWorkerQuestionQueue } from './worker-questions.js';
1
3
  import { providerAuthSnapshot } from './ui-provider-auth.js';
2
4
  import { mountProviderTerminalAuth, providerApiKeyRequest, startProviderTerminalLogin } from './ui-provider-terminal-auth.js';
3
5
  import { accountName, accountSwitchPlan, accountText, tabAccountId, serverAccountId } from './account-view.js';
@@ -52,7 +54,7 @@ import { sanitizeStoredHtml } from './assistant/asst-sanitize.js';
52
54
  import { escapeHtml, classToken } from './cp/cp-markdown.js';
53
55
  import { rehydrateStoredTranscript } from './cp/cp-rehydrate.js';
54
56
  import { compactHolds, compactQueue, isCompactCommand, newConversation, nextQueued, ownsQueued, queuedRefusal, restoredQueue, QUEUED_REFUSAL_LINES } from './cp/cp-compose.js';
55
- import { scrubSavedTab, historyProjectOf, shownUuids, shownTail, snapshotVerdict, withStartConfig, historyOutcome, transcriptOwner, historyNotFoundText } from './cp/cp-restore.js';
57
+ import { scrubSavedTab, historyProjectOf, shownUuids, shownTail, snapshotVerdict, withStartConfig, historyOutcome, transcriptOwner, historyNotFoundText, splitHistoryTail, replayTodosFromMessages, markHistoryRows, earlierHistoryRows, HISTORY_ROW as HISTORY_ROW_KEY, liveRows, queueHistoryLoad, historyGate, renderHistoryRowsInto, holdPendingResult, takePendingResult } from './cp/cp-restore.js';
56
58
  import { hasCapability, slashCommandRoute, bypassesPromptBuffer, idleEndsTurn, describeResult, describeTurnFooter, localCommandOutput, splitAssistantBlocks, normalizeSlashCommands, matchSlashCommands, listSlashCommands, slashMenuMaxHeight, splitCommandsByCapability, readInit, statusRows } from './cp/cp-events.js';
57
59
  import {
58
60
  renderSdkEvent, noteUnhandledEvent, applyEngineHello, renderCompactBoundary, markDeniedFromResult, endTurnRows,
@@ -282,7 +284,7 @@ const _snapshotsReady = fetchBlobNamespace('claude-snapshots')
282
284
  const EFFORT_LEVELS = ['off', 'low', 'medium', 'high', 'xhigh', 'max'];
283
285
  const EFFORT_LABELS = { off: 'Think', low: 'lo', medium: 'med', high: 'hi', xhigh: 'xhi', max: 'max' };
284
286
  const EFFORT_TITLES = {
285
- off: 'Effort: model default (thinking not shown; /session can turn thinking off)',
287
+ off: 'Thinking off',
286
288
  low: 'Thinking: low',
287
289
  medium: 'Thinking: medium',
288
290
  high: 'Thinking: high',
@@ -417,6 +419,7 @@ function buildPanel() {
417
419
  </div>
418
420
  <div class="cp-input-area">
419
421
  <div class="cp-input-wrap">
422
+ <span class="cp-spin-ring" aria-hidden="true"><i></i></span>
420
423
  <div class="cp-image-preview" id="cp-image-preview"></div>
421
424
  <div class="cp-input-inner">
422
425
  <textarea class="cp-input" id="cp-input" placeholder="Message SynaBun..." rows="1" autocomplete="off" spellcheck="false"></textarea>
@@ -483,6 +486,10 @@ function injectStyles() {
483
486
  opacity: 1;
484
487
  overflow: visible;
485
488
  }
489
+ /* Windows: the shell is 92% opaque, so the blur behind it barely shows, yet Chromium recomputes it on every repaint
490
+ inside the panel (a transcript rendering, a stream). There it goes; the colour stays. */
491
+ html.platform-win32 .claude-panel,
492
+ html.desktop-titlebar--win32 .claude-panel { backdrop-filter: none; -webkit-backdrop-filter: none; }
486
493
  /* Raise panel above floating terminal windows while a dropdown is open
487
494
  so project/branch/recall menus aren't clipped behind them. */
488
495
  .claude-panel:has(.cp-dropdown.open) { z-index: 99999; }
@@ -1055,26 +1062,17 @@ function injectStyles() {
1055
1062
  margin-top: 8px; transition: opacity 0.3s, border-color 0.2s, box-shadow 0.3s;
1056
1063
  }
1057
1064
  .perm-card { position: relative; }
1058
- .perm-card::before {
1059
- content: '';
1060
- position: absolute; inset: 0;
1061
- border-radius: 8px;
1062
- padding: 1px;
1063
- pointer-events: none;
1065
+ /* A card that waits on you turns an amber ring inside its edge: the shared .cp-spin-ring (with the
1066
+ composer's, below), turning only while the card is .active-perm. */
1067
+ .perm-card > .cp-spin-ring { border-radius: 8px; }
1068
+ .perm-card > .cp-spin-ring > i {
1064
1069
  background: conic-gradient(
1065
- from var(--cp-border-angle, 0deg),
1066
1070
  rgba(245,180,60,0.0) 0%,
1067
1071
  rgba(245,180,60,0.55) 25%,
1068
1072
  rgba(245,180,60,0.1) 50%,
1069
1073
  rgba(245,180,60,0.55) 75%,
1070
1074
  rgba(245,180,60,0.0) 100%
1071
1075
  );
1072
- -webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
1073
- -webkit-mask-composite: xor;
1074
- mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
1075
- mask-composite: exclude;
1076
- opacity: 0;
1077
- transition: opacity 0.4s;
1078
1076
  }
1079
1077
  .perm-card.active-perm {
1080
1078
  border-color: rgba(245,180,60,0.25);
@@ -1082,9 +1080,10 @@ function injectStyles() {
1082
1080
  box-shadow: 0 0 16px rgba(245,180,60,0.08), 0 0 40px rgba(245,180,60,0.04);
1083
1081
  animation: cp-perm-glow 2.5s ease-in-out infinite;
1084
1082
  }
1085
- .perm-card.active-perm::before {
1086
- opacity: 1;
1087
- animation: cp-border-spin 3s linear infinite;
1083
+ .perm-card.active-perm > .cp-spin-ring { opacity: 1; }
1084
+ .perm-card.active-perm > .cp-spin-ring > i {
1085
+ animation: cp-spin-ring-turn 3s linear infinite;
1086
+ will-change: rotate;
1088
1087
  }
1089
1088
  .perm-card:hover { border-color: var(--b-light); }
1090
1089
  .perm-card.active-perm:hover { border-color: rgba(245,180,60,0.4); }
@@ -1228,47 +1227,64 @@ function injectStyles() {
1228
1227
  transition: background 0.4s;
1229
1228
  min-width: 0;
1230
1229
  }
1231
- .cp-input-wrap::before {
1232
- content: '';
1230
+ /* ── Spinning border ring (composer focus, a waiting permission card) ──
1231
+ A conic gradient turning inside the 1px edge, the mask keeping only the ring. The gradient is a
1232
+ square box that turns (rotate, run by the compositor) under the mask, and only while its owner's
1233
+ state is on; an animated custom property (the old --cp-border-angle, @property + a conic gradient
1234
+ from it) restyled and repainted the owner on the main thread 60 times a second. The ring is a size
1235
+ container and the square 142cqmax + 4px, wider than the owner's diagonal at any shape, so what
1236
+ the mask shows is the same conic gradient at every angle. Markup: <span class="cp-spin-ring"
1237
+ aria-hidden="true"><i></i></span>, the owner's first child. */
1238
+ .cp-spin-ring {
1233
1239
  position: absolute; inset: 0;
1234
- border-radius: 14px;
1235
1240
  padding: 1px;
1241
+ overflow: hidden;
1236
1242
  pointer-events: none;
1243
+ container-type: size;
1244
+ -webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
1245
+ -webkit-mask-composite: xor;
1246
+ mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
1247
+ mask-composite: exclude;
1248
+ opacity: 0;
1249
+ transition: opacity 0.4s;
1250
+ }
1251
+ .cp-spin-ring > i {
1252
+ position: absolute; left: 50%; top: 50%;
1253
+ width: calc(142cqmax + 4px);
1254
+ aspect-ratio: 1;
1255
+ translate: -50% -50%;
1256
+ }
1257
+ @keyframes cp-spin-ring-turn {
1258
+ to { rotate: 360deg; }
1259
+ }
1260
+ body.ui-interacting .cp-spin-ring > i { animation-play-state: paused !important; }
1261
+ /* Reduced motion: the ring holds still where the turn starts (the focus / waiting look stays). */
1262
+ @media (prefers-reduced-motion: reduce) {
1263
+ .cp-spin-ring > i { animation: none !important; will-change: auto !important; }
1264
+ }
1265
+ .cp-input-wrap > .cp-spin-ring { border-radius: 14px; }
1266
+ .cp-input-wrap > .cp-spin-ring > i {
1237
1267
  background: conic-gradient(
1238
- from var(--cp-border-angle, 0deg),
1239
1268
  rgba(212,162,127,0.0) 0%,
1240
1269
  rgba(212,162,127,0.35) 25%,
1241
1270
  rgba(212,162,127,0.10) 50%,
1242
1271
  rgba(212,162,127,0.35) 75%,
1243
1272
  rgba(212,162,127,0.0) 100%
1244
1273
  );
1245
- -webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
1246
- -webkit-mask-composite: xor;
1247
- mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
1248
- mask-composite: exclude;
1249
- opacity: 0;
1250
- transition: opacity 0.4s;
1251
1274
  }
1252
- .cp-input-wrap:focus-within::before {
1253
- opacity: 1;
1254
- animation: cp-border-spin 3s linear infinite;
1275
+ .cp-input-wrap:focus-within > .cp-spin-ring { opacity: 1; }
1276
+ .cp-input-wrap:focus-within > .cp-spin-ring > i {
1277
+ animation: cp-spin-ring-turn 3s linear infinite;
1278
+ will-change: rotate;
1255
1279
  }
1256
1280
  .cp-input-wrap:focus-within {
1257
1281
  background: rgba(212,162,127,0.03);
1258
1282
  box-shadow: 0 0 20px rgba(212,162,127,0.04), 0 0 60px rgba(212,162,127,0.015);
1259
1283
  }
1260
- @keyframes cp-border-spin {
1261
- to { --cp-border-angle: 360deg; }
1262
- }
1263
1284
  @keyframes cp-perm-glow {
1264
1285
  0%, 100% { box-shadow: 0 0 16px rgba(245,180,60,0.08), 0 0 40px rgba(245,180,60,0.04); }
1265
1286
  50% { box-shadow: 0 0 20px rgba(245,180,60,0.14), 0 0 50px rgba(245,180,60,0.07); }
1266
1287
  }
1267
- @property --cp-border-angle {
1268
- syntax: '<angle>';
1269
- initial-value: 0deg;
1270
- inherits: false;
1271
- }
1272
1288
 
1273
1289
  .cp-input-inner {
1274
1290
  display: flex; align-items: flex-end;
@@ -1768,8 +1784,8 @@ function injectStyles() {
1768
1784
  pointer-events: auto;
1769
1785
  width: 0; opacity: 0; overflow: hidden;
1770
1786
  background: rgba(18, 18, 20, 0.92);
1771
- backdrop-filter: blur(60px) saturate(1.5);
1772
- -webkit-backdrop-filter: blur(60px) saturate(1.5);
1787
+ backdrop-filter: none;
1788
+ -webkit-backdrop-filter: none;
1773
1789
  border: 0.5px solid rgba(255,255,255,0.06);
1774
1790
  border-radius: 14px;
1775
1791
  box-shadow:
@@ -1782,7 +1798,19 @@ function injectStyles() {
1782
1798
  transition: width 0.22s cubic-bezier(0.2,0,0.2,1), opacity 0.18s;
1783
1799
  max-height: 100%;
1784
1800
  }
1785
- .cp-todo-dock.is-open .cp-todo-drawer { width: 260px; opacity: 1; }
1801
+ .cp-todo-dock.is-open .cp-todo-drawer {
1802
+ width: 260px; opacity: 1;
1803
+ backdrop-filter: blur(60px) saturate(1.5);
1804
+ -webkit-backdrop-filter: blur(60px) saturate(1.5);
1805
+ }
1806
+ html.platform-win32 .cp-todo-tag,
1807
+ html.desktop-titlebar--win32 .cp-todo-tag,
1808
+ html.platform-win32 .cp-todo-dock.is-open .cp-todo-drawer,
1809
+ html.desktop-titlebar--win32 .cp-todo-dock.is-open .cp-todo-drawer {
1810
+ background: rgba(18, 18, 20, 0.96);
1811
+ backdrop-filter: none;
1812
+ -webkit-backdrop-filter: none;
1813
+ }
1786
1814
  .cp-todo-dock.is-open .cp-todo-tag { display: none; }
1787
1815
 
1788
1816
  .cp-todo-drawer .cp-todo-head {
@@ -2283,52 +2311,53 @@ function injectStyles() {
2283
2311
  }
2284
2312
 
2285
2313
  /* ── Claude session pills in shared tray ── */
2286
- @property --cp-pill-angle {
2287
- syntax: '<angle>';
2288
- initial-value: 0deg;
2289
- inherits: false;
2290
- }
2291
2314
  .cp-session-pill {
2292
2315
  position: relative;
2293
2316
  overflow: hidden;
2294
2317
  border-color: rgba(212, 162, 127, 0.14);
2295
2318
  transition: border-color 0.2s;
2296
2319
  }
2297
- .cp-session-pill::before {
2298
- content: '';
2320
+ /* A working pill's ring: a conic gradient turning inside the 1px border (the mask keeps only the
2321
+ ring). The gradient is a square box that turns (rotate, run by the compositor) under the mask; an
2322
+ animated custom property (the old --cp-pill-angle) repainted the pill on the main thread every
2323
+ frame for as long as a session worked. The square is wider than the pill's diagonal, so the ring
2324
+ is the same conic gradient at every angle. */
2325
+ .cp-pill-ring {
2299
2326
  position: absolute; inset: 0;
2327
+ display: none;
2300
2328
  border-radius: 8px;
2301
2329
  padding: 1px;
2330
+ overflow: hidden;
2302
2331
  pointer-events: none;
2303
- background: transparent;
2304
2332
  -webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
2305
2333
  -webkit-mask-composite: xor;
2306
2334
  mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
2307
2335
  mask-composite: exclude;
2308
- opacity: 0;
2309
2336
  }
2310
- .cp-session-pill.cp-pill-running {
2311
- border-color: transparent;
2312
- }
2313
- .cp-session-pill.cp-pill-running::before {
2337
+ .cp-pill-ring > i {
2338
+ position: absolute; left: 50%; top: 50%;
2339
+ width: calc(100% + 32px);
2340
+ aspect-ratio: 1;
2341
+ translate: -50% -50%;
2314
2342
  background: conic-gradient(
2315
- from var(--cp-pill-angle, 0deg),
2316
2343
  rgba(212,162,127,0.0) 0%,
2317
2344
  rgba(212,162,127,0.6) 25%,
2318
2345
  rgba(212,162,127,0.12) 50%,
2319
2346
  rgba(212,162,127,0.6) 75%,
2320
2347
  rgba(212,162,127,0.0) 100%
2321
2348
  );
2322
- animation: cp-pill-border-spin 2.4s linear infinite;
2323
- opacity: 1;
2349
+ animation: cp-pill-ring-turn 2.4s linear infinite;
2350
+ }
2351
+ .cp-session-pill.cp-pill-running {
2352
+ border-color: transparent;
2324
2353
  }
2325
- @keyframes cp-pill-border-spin {
2326
- to { --cp-pill-angle: 360deg; }
2354
+ .cp-session-pill.cp-pill-running .cp-pill-ring { display: block; }
2355
+ @keyframes cp-pill-ring-turn {
2356
+ to { rotate: 360deg; }
2327
2357
  }
2328
2358
  .cp-session-pill:hover { border-color: rgba(212, 162, 127, 0.28); }
2329
- .cp-session-pill:hover.cp-pill-running::before {
2359
+ .cp-session-pill:hover .cp-pill-ring > i {
2330
2360
  background: conic-gradient(
2331
- from var(--cp-pill-angle, 0deg),
2332
2361
  rgba(212,162,127,0.0) 0%,
2333
2362
  rgba(212,162,127,0.8) 25%,
2334
2363
  rgba(212,162,127,0.20) 50%,
@@ -2336,6 +2365,19 @@ function injectStyles() {
2336
2365
  rgba(212,162,127,0.0) 100%
2337
2366
  );
2338
2367
  }
2368
+ html.platform-win32 .cp-session-pill.cp-pill-running,
2369
+ html.desktop-titlebar--win32 .cp-session-pill.cp-pill-running {
2370
+ border-color: rgba(212, 162, 127, 0.42);
2371
+ }
2372
+ html.platform-win32 .cp-session-pill.cp-pill-running:hover,
2373
+ html.desktop-titlebar--win32 .cp-session-pill.cp-pill-running:hover {
2374
+ border-color: rgba(212, 162, 127, 0.62);
2375
+ }
2376
+ html.platform-win32 .cp-session-pill.cp-pill-running .cp-pill-ring,
2377
+ html.desktop-titlebar--win32 .cp-session-pill.cp-pill-running .cp-pill-ring {
2378
+ display: none;
2379
+ }
2380
+ body.ui-interacting .cp-pill-ring > i { animation-play-state: paused; }
2339
2381
  .cp-session-pill .term-minimized-pill-icon { color: rgba(212, 162, 127, 0.45); transition: color 0.2s; }
2340
2382
  .cp-session-pill.cp-pill-running .term-minimized-pill-icon { color: rgba(212, 162, 127, 1); }
2341
2383
  .cp-session-pill:hover .term-minimized-pill-icon { color: rgba(212, 162, 127, 0.85); }
@@ -2370,6 +2412,12 @@ function injectStyles() {
2370
2412
  pointer-events: none;
2371
2413
  }
2372
2414
  @keyframes cp-pill-pulse { 0%,100% { opacity: 0.4; } 50% { opacity: 1; } }
2415
+ /* Reduced motion: the ring, the dot and the edge hold still (the working look stays). */
2416
+ @media (prefers-reduced-motion: reduce) {
2417
+ .cp-pill-ring > i,
2418
+ .cp-session-pill.cp-pill-running .term-minimized-pill-label::before,
2419
+ .cp-session-pill.cp-pill-running::after { animation: none; }
2420
+ }
2373
2421
 
2374
2422
  /* ── Think intensity toggle ── */
2375
2423
  /* ── Shared toggle base ── */
@@ -2802,6 +2850,13 @@ function _effortLevelsForModel() {
2802
2850
  return ['off', ...advertised.filter(id => id !== 'off')];
2803
2851
  }
2804
2852
 
2853
+ // Whether the tab's model (the selector's, for the active tab) has effort levels.
2854
+ function _modelSupportsEffort(tab) {
2855
+ if (tab === activeTab()) return _effortLevelsForModel().length > 1;
2856
+ const model = tab?.model ? _models.find(m => m.id === tab.model) : null;
2857
+ return !Array.isArray(model?.effortLevels) || model.effortLevels.some(id => id !== 'off');
2858
+ }
2859
+
2805
2860
  // Some models have no thinking at all (Haiku). Disable the toggle and force 'off'
2806
2861
  // rather than passing a --effort the CLI will ignore.
2807
2862
  function _syncThinkAvailability() {
@@ -2922,7 +2977,7 @@ function newTabState(sessionId = null, label = 'New chat', id = null) {
2922
2977
  };
2923
2978
  }
2924
2979
 
2925
- function createTab(sessionId = null, label = 'New chat', { autoSwitch = true, id = null } = {}) {
2980
+ function createTab(sessionId = null, label = 'New chat', { autoSwitch = true, id = null, deferHistory = false } = {}) {
2926
2981
  if (_tabs.length >= MAX_TABS) return null;
2927
2982
  const tab = newTabState(sessionId, label, id);
2928
2983
  // Create messages div — kept DETACHED until the tab activates. Inactive
@@ -2937,14 +2992,17 @@ function createTab(sessionId = null, label = 'New chat', { autoSwitch = true, id
2937
2992
  msgDiv.innerHTML = `<div class="cp-empty"><div class="cp-empty-logo">${CLAUDE_ICON}</div><span class="cp-empty-name">Claude</span><span class="cp-empty-hint">Send a message to start</span></div>`;
2938
2993
  tab.messagesEl = msgDiv;
2939
2994
  tab.scrollController = createStickyScrollController(msgDiv, { active: false });
2940
- // Connect WebSocket
2995
+ // Connect WebSocket (every tab, active or not: a restored tab reattaches to the background agents,
2996
+ // shells and wakeups its session kept on the server).
2941
2997
  connectTab(tab);
2942
2998
  // Create pill (rendered in shared tray, not inside panel)
2943
2999
  tab.pillEl = _createTrayPill(tab);
2944
3000
  _paintTemporary(tab);
2945
3001
  _tabs.push(tab);
2946
3002
  if (autoSwitch) switchTab(_tabs.length - 1);
2947
- if (sessionId) loadSessionHistory(sessionId, tab.messagesEl);
3003
+ // A restored background tab loads its transcript when it is first shown (switchTab), not all at once at panel open.
3004
+ if (sessionId && deferHistory) tab._historyDeferred = sessionId;
3005
+ if (sessionId && !deferHistory) loadSessionHistory(sessionId, tab.messagesEl);
2948
3006
  renderPills();
2949
3007
  if (autoSwitch) saveTabs();
2950
3008
  return tab;
@@ -3095,7 +3153,9 @@ function switchTab(idx) {
3095
3153
  const items = _models.map(m => ({ value: m.id, label: m.label }));
3096
3154
  const modelVal = _resolveModelValue(items, tab.model || _getDefaultModel());
3097
3155
  ddPopulateModels($model, items, modelVal);
3098
- if (modelVal && modelVal !== tab.model) { tab.model = modelVal; saveTabs(); }
3156
+ // The unverified fallback list (the CLI not asked yet: /api/claude/config answers at once) never
3157
+ // rewrites a tab's model; refreshModels() puts the real list in and settles it.
3158
+ if (modelVal && modelVal !== tab.model && _modelsSource !== 'fallback') { tab.model = modelVal; saveTabs(); }
3099
3159
  }
3100
3160
  if ($think && tab.effort) _setEffort($think, tab.effort);
3101
3161
  _syncThinkAvailability();
@@ -3117,6 +3177,10 @@ function switchTab(idx) {
3117
3177
  populateModeDropdown(_panel?.querySelector('#cp-mode'), tab);
3118
3178
  _renderAccountMenu();
3119
3179
  _paintTemporary(tab);
3180
+ if (tab._historyDeferred && tab._historyDeferred === tab.sessionId && !tab.automationRunId) {
3181
+ tab._historyDeferred = false;
3182
+ loadSessionHistory(tab.sessionId, tab.messagesEl);
3183
+ }
3120
3184
  saveTabs();
3121
3185
  }
3122
3186
 
@@ -3191,6 +3255,7 @@ function _createTrayPill(tab) {
3191
3255
  pill.className = 'term-minimized-pill cp-session-pill';
3192
3256
  pill.dataset.tabId = tab.id;
3193
3257
  pill.innerHTML = `
3258
+ <span class="cp-pill-ring" aria-hidden="true"><i></i></span>
3194
3259
  <span class="term-minimized-pill-icon">${_CP_ICON_SVG}</span>
3195
3260
  <span class="term-minimized-pill-label">${escH(tab.label)}</span>
3196
3261
  <button class="term-minimized-pill-close" data-tooltip="Close" data-tooltip-pos="top">&times;</button>
@@ -3414,8 +3479,23 @@ function _scrubStaleTabState(key) {
3414
3479
  }
3415
3480
  }
3416
3481
 
3482
+ // A restored tab that was working (a turn, a question, background agents or shells) can get live rows
3483
+ // as soon as its socket reattaches: its transcript loads at once, ahead of them. An idle one waits until
3484
+ // it is shown (switchTab), so opening the panel does not fetch and render every tab's transcript.
3485
+ function _loadHistoryIfWorking(tab, saved) {
3486
+ if (!tab._historyDeferred || tab._historyDeferred !== tab.sessionId) return;
3487
+ if (!(saved?.running || saved?.needsYou || Number(saved?.backgroundWork) > 0)) return;
3488
+ tab._historyDeferred = false;
3489
+ loadSessionHistory(tab.sessionId, tab.messagesEl);
3490
+ }
3491
+
3417
3492
  function restoreTabs() {
3418
3493
  _cleanStaleWindows();
3494
+ const pickRestoredTabIdx = (activeIdx) => {
3495
+ const pendingId = globalThis.__synabunPendingTab?.provider === 'claude' ? globalThis.__synabunPendingTab.tabId : '';
3496
+ const pendingIdx = pendingId ? _tabs.findIndex(t => t.id === pendingId || t.sessionId === pendingId) : -1;
3497
+ return pendingIdx >= 0 ? pendingIdx : Math.min(activeIdx || 0, _tabs.length - 1);
3498
+ };
3419
3499
  // Try window-scoped tabs first
3420
3500
  try {
3421
3501
  const raw = storage.getItem(STOR.tabs);
@@ -3424,7 +3504,7 @@ function restoreTabs() {
3424
3504
  if (data.tabs?.length) {
3425
3505
  for (const saved of data.tabs) {
3426
3506
  if (saved.automationActive && saved.automationOwnerId !== nativeLoopWindowId) continue;
3427
- const t = createTab(saved.automationRunId ? null : saved.sessionId, saved.label, { autoSwitch: false, id: saved.id });
3507
+ const t = createTab(saved.automationRunId ? null : saved.sessionId, saved.label, { autoSwitch: false, id: saved.id, deferHistory: true });
3428
3508
  if (t && saved.automationRunId) t.sessionId = saved.sessionId || null;
3429
3509
  if (t && saved.sessionCost) t.sessionCost = saved.sessionCost;
3430
3510
  if (t && saved.running && !saved.automationRunId) t._wasRunning = true; // trigger reattach on WS open
@@ -3448,9 +3528,10 @@ function restoreTabs() {
3448
3528
  if (t && saved.automationRunId) t.automationActive = !!saved.automationActive;
3449
3529
  if (t && saved.automationRunId) t.automationOwnerId = saved.automationOwnerId || null;
3450
3530
  if (t && saved.releasedRunId && !saved.automationRunId) t.releasedRunId = saved.releasedRunId;
3531
+ if (t) _loadHistoryIfWorking(t, saved);
3451
3532
  }
3452
3533
  if (_tabs.length) {
3453
- switchTab(Math.min(data.activeIdx || 0, _tabs.length - 1));
3534
+ switchTab(pickRestoredTabIdx(data.activeIdx));
3454
3535
  return;
3455
3536
  }
3456
3537
  }
@@ -3464,7 +3545,7 @@ function restoreTabs() {
3464
3545
  if (data.tabs?.length) {
3465
3546
  for (const saved of data.tabs) {
3466
3547
  if (saved.automationActive && saved.automationOwnerId !== nativeLoopWindowId) continue;
3467
- const t = createTab(saved.automationRunId ? null : saved.sessionId, saved.label, { autoSwitch: false, id: saved.id });
3548
+ const t = createTab(saved.automationRunId ? null : saved.sessionId, saved.label, { autoSwitch: false, id: saved.id, deferHistory: true });
3468
3549
  if (t && saved.automationRunId) t.sessionId = saved.sessionId || null;
3469
3550
  if (t && saved.sessionCost) t.sessionCost = saved.sessionCost;
3470
3551
  if (t && saved.running && !saved.automationRunId) t._wasRunning = true;
@@ -3475,9 +3556,10 @@ function restoreTabs() {
3475
3556
  if (t && saved.automationRunId) t.automationRunId = saved.automationRunId;
3476
3557
  if (t && saved.automationRunId) t.automationActive = !!saved.automationActive;
3477
3558
  if (t && saved.automationRunId) t.automationOwnerId = saved.automationOwnerId || null;
3559
+ if (t) _loadHistoryIfWorking(t, saved);
3478
3560
  }
3479
3561
  if (_tabs.length) {
3480
- switchTab(Math.min(data.activeIdx || 0, _tabs.length - 1));
3562
+ switchTab(pickRestoredTabIdx(data.activeIdx));
3481
3563
  // Don't remove legacy key — other windows may still need it during rollout
3482
3564
  return;
3483
3565
  }
@@ -4512,6 +4594,8 @@ function writeSessionSnapshot(tab) {
4512
4594
  const sid = tab?.sessionId || null;
4513
4595
  const $msgs = tab?.messagesEl || null;
4514
4596
  if (!sid || !$msgs) return;
4597
+ // Not loaded yet (a restored tab not shown so far), or half rebuilt: what is on screen is not the transcript.
4598
+ if (tab._historyDeferred || tab._historyRendering || tab._historyHold) return;
4515
4599
  const html = providerAuthSnapshot($msgs);
4516
4600
  if (!html.trim()) return;
4517
4601
  // Skip snapshot if the transcript only contains the empty-state placeholder
@@ -4686,9 +4770,14 @@ function _renderHistoryRow(tab, $msgs, m) {
4686
4770
  if (typeof t === 'object' && t.name) {
4687
4771
  // A question or a plan that was already answered: a record of it, not
4688
4772
  // the interactive card (and not a JSON dump).
4689
- if (t.name === 'AskUserQuestion') wrap.appendChild(buildHistoryAskCard(t));
4690
- else if (t.name === 'ExitPlanMode') wrap.appendChild(buildHistoryPlanCard(t));
4691
- else wrap.appendChild(buildTool(t, tab));
4773
+ let card;
4774
+ if (t.name === 'AskUserQuestion') card = buildHistoryAskCard(t);
4775
+ else if (t.name === 'ExitPlanMode') card = buildHistoryPlanCard(t);
4776
+ else card = buildTool(t, tab);
4777
+ if (card) {
4778
+ if (t.id && $msgs._replayCards instanceof Map) $msgs._replayCards.set(String(t.id), card);
4779
+ wrap.appendChild(card);
4780
+ }
4692
4781
  } else {
4693
4782
  // Legacy: tool name string only
4694
4783
  const tName = String(t);
@@ -4704,20 +4793,33 @@ function _renderHistoryRow(tab, $msgs, m) {
4704
4793
  }
4705
4794
  }
4706
4795
  el.appendChild(wrap); $msgs.appendChild(el);
4796
+ // Live results that arrived for these cards while the replay had not built them.
4797
+ if (live && tab._pendingResults instanceof Map && tab._pendingResults.size) {
4798
+ for (const t of m.tools || []) {
4799
+ const ev = t?.id ? takePendingResult(tab, t.id) : null;
4800
+ if (ev) { try { updateToolResult(tab, ev); } catch {} }
4801
+ }
4802
+ }
4707
4803
  return;
4708
4804
  }
4709
4805
  if (m.role === 'tool_result' && m.toolUseId) {
4710
4806
  if (fillHistoryCard($msgs, m)) return;
4807
+ const indexedCard = $msgs._replayCards instanceof Map ? $msgs._replayCards.get(String(m.toolUseId)) : null;
4711
4808
  if (live) {
4712
- updateToolResult(tab, { type: 'tool_result', tool_use_id: m.toolUseId, content: m.text || '', is_error: !!m.isError, tool_use_result: m.structured });
4809
+ // Marked as a replayed result: it is not kept as pending, and its todos are already in the precomputed list.
4810
+ tab._replayRow = true;
4811
+ try { updateToolResult(tab, { type: 'tool_result', tool_use_id: m.toolUseId, content: m.text || '', is_error: !!m.isError, tool_use_result: m.structured }); }
4812
+ finally { tab._replayRow = false; }
4713
4813
  // A subagent of a past session: its own transcript loads when the card is opened.
4714
4814
  const agentId = m.structured?.agentId;
4715
- const agentCard = agentId && sessionActionsAvailable(tab) ? $msgs.querySelector(`.cp-agent-card[data-tool-id="${CSS.escape(m.toolUseId)}"]`) : null;
4815
+ const agentCard = agentId && sessionActionsAvailable(tab)
4816
+ ? ((indexedCard?.classList?.contains('cp-agent-card') ? indexedCard : null) || $msgs.querySelector(`.cp-agent-card[data-tool-id="${CSS.escape(m.toolUseId)}"]`))
4817
+ : null;
4716
4818
  if (agentCard) _wireSubagentTranscript(tab, agentCard, String(agentId));
4717
4819
  return;
4718
4820
  }
4719
4821
  // No tab behind this container: plain text into the card.
4720
- const card = $msgs.querySelector(`.tool-card[data-tool-id="${CSS.escape(m.toolUseId)}"]`);
4822
+ const card = (indexedCard?.matches?.('.tool-card') ? indexedCard : null) || $msgs.querySelector(`.tool-card[data-tool-id="${CSS.escape(m.toolUseId)}"]`);
4721
4823
  const rLbl = card?.querySelector('.tool-result-label');
4722
4824
  const rSec = card?.querySelector('.tool-result-content');
4723
4825
  if (rSec && m.text) {
@@ -4822,12 +4924,19 @@ function _addHistoryPager(tab, $msgs, sid, project, data) {
4822
4924
  // Render at the end (the live renderers append), then move the block up.
4823
4925
  const tail = $msgs.lastElementChild;
4824
4926
  tab._replaying = (tab._replaying || 0) + 1;
4927
+ const replayCards = new Map();
4928
+ $msgs._replayCards = replayCards;
4825
4929
  try { for (const m of page.messages) _renderHistoryRow(tab, $msgs, m); }
4826
- finally { tab._replaying = Math.max(0, (tab._replaying || 1) - 1); }
4930
+ finally {
4931
+ if ($msgs._replayCards === replayCards) delete $msgs._replayCards;
4932
+ tab._replaying = Math.max(0, (tab._replaying || 1) - 1);
4933
+ }
4827
4934
  const added = [];
4828
4935
  for (let n = tail?.nextElementSibling; n; n = n.nextElementSibling) added.push(n);
4936
+ markHistoryRows(added, 'earlier'); // history: a later rebuild replaces it
4829
4937
  const anchor = note.nextElementSibling;
4830
4938
  for (const n of added) $msgs.insertBefore(n, anchor);
4939
+ try { collapseAdjacentMcpTools($msgs); } catch {}
4831
4940
  try { settleReplayedCards($msgs, tab); } catch {}
4832
4941
  // What the user asked to see must survive the next prune.
4833
4942
  $msgs._pruneCap = $msgs.childElementCount + MAX_MSG_CHILDREN;
@@ -4860,20 +4969,42 @@ function _dropSessionSnapshot(sid) {
4860
4969
  try { delete _sessionSnapshots[sid]; deleteBlob('claude-snapshots', sid); } catch {}
4861
4970
  }
4862
4971
 
4863
- async function loadSessionHistory(sid, $msgs) {
4972
+ // Loads of one container run one at a time, the newest asked for wins (queueHistoryLoad, cp/cp-restore.js).
4973
+ // The tab that owns the container is taken once, here: per-tab state (todos, usage, the snapshot) never goes
4974
+ // to another tab, and a tab closed meanwhile stops the load. While any load of a tab runs, its live
4975
+ // transcript messages wait in its buffer (historyGate in handleTabMsg) and play after the history.
4976
+ function loadSessionHistory(sid, $msgs) {
4864
4977
  if (!$msgs) $msgs = activeTab()?.messagesEl;
4865
- if (!$msgs) return;
4978
+ if (!$msgs) return Promise.resolve();
4979
+ const ownerTab = _tabs.find(t => t.messagesEl === $msgs) || null;
4980
+ if (ownerTab && ownerTab._historyDeferred === sid) ownerTab._historyDeferred = false;
4981
+ if (ownerTab) ownerTab._historyHold = (ownerTab._historyHold || 0) + 1;
4982
+ const job = queueHistoryLoad($msgs, (isStale, ticket) => {
4983
+ const stale = () => isStale() || !!(ownerTab && (ownerTab.closed || ownerTab.messagesEl !== $msgs));
4984
+ if (stale()) return;
4985
+ return _loadSessionHistoryNow(sid, $msgs, ownerTab, stale, ticket);
4986
+ });
4987
+ return job.finally(() => {
4988
+ if (!ownerTab) return;
4989
+ ownerTab._historyHold = Math.max(0, (ownerTab._historyHold || 1) - 1);
4990
+ if (!ownerTab._historyHold && ownerTab._msgBuffer?.length && !ownerTab.closed) _flushMsgBuffer(ownerTab);
4991
+ });
4992
+ }
4993
+
4994
+ async function _loadSessionHistoryNow(sid, $msgs, ownerTab, stale, ticket) {
4866
4995
  // Try local snapshot first — exact replay of the live render, lossless.
4867
4996
  // Snapshot hydration is async (blob store); wait for it so a fresh page
4868
4997
  // load doesn't miss its own snapshots and rebuild from JSONL needlessly.
4869
4998
  try { await _snapshotsReady; } catch {}
4999
+ if (stale()) return;
4870
5000
  const snap = getSessionSnapshot(sid);
4871
5001
  let restored = null; // the rows the snapshot put back, when a rebuild follows it
4872
5002
  if (snap && snap.html) {
4873
- const tab = _tabs.find(t => t.messagesEl === $msgs) || activeTab();
5003
+ const tab = ownerTab;
4874
5004
  $msgs._historySid = sid; // the session this transcript shows (a restored pager loads its earlier pages)
4875
5005
  if (renderStoredSession(snap, $msgs, tab)) {
4876
5006
  restored = [...$msgs.children];
5007
+ markHistoryRows(restored, ticket); // the next rebuild replaces them
4877
5008
  scrollEnd();
4878
5009
  // Background refresh: pull latest JSONL in case the session continued in
4879
5010
  // another window, or was rewound there. The probe reads what the owning
@@ -4889,6 +5020,7 @@ async function loadSessionHistory(sid, $msgs) {
4889
5020
  if (tab?.accountId) params.set('account', tab.accountId);
4890
5021
  const probe = await fetch(`/api/claude-code/sessions/${encodeURIComponent(sid)}/messages?${params}`);
4891
5022
  const probeData = await probe.json();
5023
+ if (stale()) return;
4892
5024
  // `visible` counts prompts and replies only (a server that predates it sends `total`).
4893
5025
  if ((probeData?.visible ?? probeData?.total ?? 0) > (snap.itemCount || 0) + 2) {
4894
5026
  // Upstream has materially more content — fall through to full rebuild
@@ -4904,8 +5036,8 @@ async function loadSessionHistory(sid, $msgs) {
4904
5036
  } catch { return; }
4905
5037
  }
4906
5038
  }
4907
- const accountTab = _tabs.find(t => t.messagesEl === $msgs);
4908
- let project = _historyProject(accountTab || activeTab()) || undefined;
5039
+ const accountTab = ownerTab;
5040
+ let project = _historyProject(ownerTab || activeTab()) || undefined;
4909
5041
  const readHistory = async (from) => {
4910
5042
  const params = new URLSearchParams({ limit: '500' });
4911
5043
  if (from) params.set('project', from);
@@ -4916,6 +5048,7 @@ async function loadSessionHistory(sid, $msgs) {
4916
5048
  };
4917
5049
  try {
4918
5050
  let data = await readHistory(project);
5051
+ if (stale()) return;
4919
5052
  // The tab's Claude account was removed in Settings: say so. Its transcripts
4920
5053
  // are not looked for under the default account (another identity's folder).
4921
5054
  if (data.code === 'account_unavailable') {
@@ -4932,6 +5065,7 @@ async function loadSessionHistory(sid, $msgs) {
4932
5065
  if (historyOutcome(data) === 'not_in_project') {
4933
5066
  const owner = await _transcriptOwner(sid, project, accountTab?.accountId || '');
4934
5067
  const moved = owner ? await readHistory(owner) : null;
5068
+ if (stale()) return;
4935
5069
  if (moved && historyOutcome(moved) !== 'not_in_project' && historyOutcome(moved) !== 'not_found' && historyOutcome(moved) !== 'refused') {
4936
5070
  data = moved;
4937
5071
  project = owner;
@@ -4963,29 +5097,66 @@ async function loadSessionHistory(sid, $msgs) {
4963
5097
  return;
4964
5098
  }
4965
5099
  // Chunked rebuild: rendering 500 messages in one task froze the UI during
4966
- // cold restore / orphan resync. The generation counter aborts a stale
4967
- // rebuild when a newer one starts (reattach resync, in-tab session switch,
4968
- // tab close) — the newer run owns the transcript.
4969
- const ownerTab = _tabs.find(t => t.messagesEl === $msgs) || activeTab();
4970
- const gen = ownerTab ? (ownerTab._historyGen = (ownerTab._historyGen || 0) + 1) : 0;
4971
- // The rebuild replaces the snapshot that was put back above (only those
4972
- // rows: what a live turn appended meanwhile stays).
5100
+ // cold restore / orphan resync. A newer load of this container (reattach resync,
5101
+ // in-tab session switch) or the tab closing stops this one at its next yield.
5102
+ // What an earlier rebuild of this container left (finished, stopped half way, or
5103
+ // the snapshot put back above) is taken out: never a second copy after it.
5104
+ for (const node of earlierHistoryRows($msgs.children, ticket)) node.remove();
4973
5105
  if (restored) for (const node of restored) node.remove();
5106
+ // Rows that came live before the load (the history goes before them: it is older).
5107
+ const live = liveRows($msgs.children);
5108
+ const liveAnchor = live[0] || null;
5109
+ const liveSet = new Set(live);
5110
+ const { head, tail } = !restored ? splitHistoryTail(data.messages, 40) : { head: [], tail: data.messages };
5111
+ // Renders rows at the end, marks them as this load's, and moves them before `before`.
5112
+ const renderRows = (rows, before) => renderHistoryRowsInto($msgs, rows, (m) => _renderHistoryRow(ownerTab, $msgs, m), { before, ticket, skip: liveSet });
4974
5113
  // Replay goes through the live renderers (tool cards, typed results), marked
4975
5114
  // so it leaves no activity behind: no hook-strip entries, no statusline verb.
4976
5115
  if (ownerTab) ownerTab._replaying = (ownerTab._replaying || 0) + 1;
5116
+ if (ownerTab) ownerTab._historyRendering = (ownerTab._historyRendering || 0) + 1;
5117
+ if (ownerTab && head.length) {
5118
+ const precomputedTodos = replayTodosFromMessages(data.messages, ownerTab.todos);
5119
+ if (precomputedTodos) ownerTab.todos = precomputedTodos;
5120
+ ownerTab._skipReplayTodos = true;
5121
+ }
5122
+ const replayCards = new Map();
5123
+ $msgs._replayCards = replayCards;
4977
5124
  try {
4978
- let processed = 0;
4979
- for (const m of data.messages) {
4980
- if (++processed % 40 === 0) {
5125
+ if (head.length) {
5126
+ const tailNodes = renderRows(tail, liveAnchor);
5127
+ const firstTailNode = tailNodes[0] || liveAnchor;
5128
+ scrollEnd();
5129
+ const CHUNK = 50;
5130
+ for (let offset = 0; offset < head.length; offset += CHUNK) {
4981
5131
  await new Promise(r => setTimeout(r, 0));
4982
- if (ownerTab && (ownerTab._historyGen !== gen || ownerTab.closed)) return;
5132
+ if (stale()) return;
5133
+ renderRows(head.slice(offset, offset + CHUNK), firstTailNode);
5134
+ // Following the bottom: stay there. Scrolled up: the browser's scroll anchoring keeps the rows in view.
5135
+ if (ownerTab === activeTab()) scrollEnd();
5136
+ }
5137
+ } else {
5138
+ const CHUNK = 40;
5139
+ for (let offset = 0; offset < data.messages.length; offset += CHUNK) {
5140
+ if (offset) {
5141
+ await new Promise(r => setTimeout(r, 0));
5142
+ if (stale()) return;
5143
+ }
5144
+ renderRows(data.messages.slice(offset, offset + CHUNK), liveAnchor);
4983
5145
  }
4984
- _renderHistoryRow(ownerTab, $msgs, m);
4985
5146
  }
4986
5147
  } finally {
4987
- if (ownerTab) ownerTab._replaying = Math.max(0, (ownerTab._replaying || 1) - 1);
5148
+ if ($msgs._replayCards === replayCards) delete $msgs._replayCards;
5149
+ if (ownerTab) {
5150
+ delete ownerTab._skipReplayTodos;
5151
+ ownerTab._historyRendering = Math.max(0, (ownerTab._historyRendering || 1) - 1);
5152
+ ownerTab._replaying = Math.max(0, (ownerTab._replaying || 1) - 1);
5153
+ // Live results kept for cards this page never built (cut by the page) are dropped.
5154
+ if (!ownerTab._historyRendering) ownerTab._pendingResults = null;
5155
+ }
4988
5156
  }
5157
+ try { collapseAdjacentMcpTools($msgs); } catch {}
5158
+ // Always for the tab on screen: a list the page emptied must clear the dock too.
5159
+ if (ownerTab && ownerTab === activeTab()) { try { renderTodoWidget(ownerTab); } catch {} }
4989
5160
  // Calls whose result is not in this page (interrupted, or cut by the page) stop looking busy.
4990
5161
  try { settleReplayedCards($msgs, ownerTab); } catch {}
4991
5162
  pruneMessages($msgs);
@@ -5001,6 +5172,8 @@ async function loadSessionHistory(sid, $msgs) {
5001
5172
  $msgs.appendChild(note);
5002
5173
  }
5003
5174
  if (ownerTab) ownerTab._historyResults = historyHasResultSupport(data);
5175
+ // The pager and the notes are this load's too (a later rebuild replaces them).
5176
+ markHistoryRows([...$msgs.children].filter(n => !liveSet.has(n) && n[HISTORY_ROW_KEY] == null), ticket);
5004
5177
  // Populate context gauge from session history usage
5005
5178
  const tab = ownerTab;
5006
5179
  if (tab && data.usage) {
@@ -5013,10 +5186,10 @@ async function loadSessionHistory(sid, $msgs) {
5013
5186
  renderGauge(tab);
5014
5187
  }
5015
5188
  scrollEnd();
5016
- // Cache the rebuilt transcript so the next resume on a fresh browser hits
5017
- // the snapshot path and avoids paying the JSONL → DOM cost again.
5018
- if (tab && tab.sessionId === sid) writeSessionSnapshot(tab);
5189
+ // Cache the rebuilt transcript (debounced so DOM serialization never blocks session open).
5190
+ if (tab && tab.sessionId === sid) scheduleSessionSnapshotSave(tab);
5019
5191
  } catch {
5192
+ if (stale()) return;
5020
5193
  $msgs.innerHTML = '<div class="msg-status">Failed to load history</div>';
5021
5194
  }
5022
5195
  }
@@ -5270,6 +5443,14 @@ function handleTabMsg(tab, msg) {
5270
5443
  // An MCP server's elicitation_complete resolves the URL card that is waiting
5271
5444
  // (bypassesPromptBuffer in cp/cp-events.js lists what passes).
5272
5445
  const _bufferBypass = bypassesPromptBuffer(msg);
5446
+ // A restored tab whose history is not loaded yet gets a live row (a wakeup, a background turn): its
5447
+ // history loads first and the message waits for it; any running history load holds them the same way.
5448
+ const gate = historyGate(tab, msg, _bufferBypass);
5449
+ if (gate === 'load') { tab._historyDeferred = false; loadSessionHistory(tab.sessionId, tab.messagesEl); }
5450
+ if (gate !== 'pass') {
5451
+ if (tab._msgBuffer.length < 500) tab._msgBuffer.push(msg);
5452
+ return;
5453
+ }
5273
5454
  // Also buffer while a chunked drain is in progress (or older messages still
5274
5455
  // queue) — without this, live events overtake buffered ones between slices.
5275
5456
  if ((tab._activePerm || tab.pendingAskRequestId || tab._draining || tab._msgBuffer.length) && !_bufferBypass) {
@@ -5295,10 +5476,11 @@ function _flushMsgBuffer(tab) {
5295
5476
  while (tab._msgBuffer.length && n++ < budget) {
5296
5477
  // A drained message can re-arm a permission/ask prompt — stop and let
5297
5478
  // that flow's resolution resume the drain (its handlers call us again).
5298
- if (tab._activePerm || tab.pendingAskRequestId) break;
5479
+ if (tab._activePerm || tab.pendingAskRequestId || tab._historyHold) break;
5299
5480
  _processTabMsg(tab, tab._msgBuffer.shift());
5300
5481
  }
5301
- if (tab._msgBuffer.length && !tab._activePerm && !tab.pendingAskRequestId) {
5482
+ // A history load holds the rest: it drains them when it is done.
5483
+ if (tab._msgBuffer.length && !tab._activePerm && !tab.pendingAskRequestId && !tab._historyHold) {
5302
5484
  setTimeout(step, 0);
5303
5485
  } else {
5304
5486
  tab._draining = false;
@@ -5413,6 +5595,11 @@ function _processTabMsg(tab, msg) {
5413
5595
  _sessionSaysMode(tab, msg.mode, 'reattach', msg);
5414
5596
  // Orphan buffer overflowed while detached — rebuild transcript from JSONL
5415
5597
  if (msg.resynced && tab.sessionId) {
5598
+ tab._historyDeferred = false;
5599
+ try { loadSessionHistory(tab.sessionId, tab.messagesEl); } catch {}
5600
+ } else if (tab._historyDeferred && tab._historyDeferred === tab.sessionId) {
5601
+ // A restored tab not shown yet whose session is still live on the server: its history first.
5602
+ tab._historyDeferred = false;
5416
5603
  try { loadSessionHistory(tab.sessionId, tab.messagesEl); } catch {}
5417
5604
  }
5418
5605
  if (msg.running) {
@@ -6688,6 +6875,8 @@ function renderAssistant(tab, msg) {
6688
6875
 
6689
6876
  /** Build an interactive AskUserQuestion card from a tool_use block */
6690
6877
  function buildAskFromToolUse(tab, block) {
6878
+ // A task worker’s broker owns the interactive card, even if this tool event arrives first.
6879
+ if (tab.automationRunId) { const placeholder = document.createElement('div'); placeholder.hidden = true; return placeholder; }
6691
6880
  const input = block.input || {};
6692
6881
  const toolUseId = block.id || '';
6693
6882
  tab.pendingAskToolUseId = toolUseId;
@@ -6879,7 +7068,7 @@ function buildAskFromToolUse(tab, block) {
6879
7068
  // Submit button — sends all answers as a batch
6880
7069
  submitBtn.addEventListener('click', () => {
6881
7070
  // An answer is shown as given only once it can be sent: until then the card stays as it is.
6882
- if (tab.ws?.readyState !== WebSocket.OPEN) { appendStatus(tab, ASK_ANSWER_UNSENT); return; }
7071
+ if (!tab.automationRunId && tab.ws?.readyState !== WebSocket.OPEN) { appendStatus(tab, ASK_ANSWER_UNSENT); return; }
6883
7072
  container.querySelectorAll('.ask-option').forEach(b => { b.disabled = true; });
6884
7073
  container.querySelectorAll('.ask-text-input, .ask-notes').forEach(i => { i.disabled = true; });
6885
7074
  submitBtn.disabled = true;
@@ -6897,6 +7086,21 @@ function buildAskFromToolUse(tab, block) {
6897
7086
 
6898
7087
  // Returns whether the answer left: false while the tab's socket is closed.
6899
7088
  function sendAskAnswer(tab, questions, answers, annotations = null) {
7089
+ if (tab.automationRunId) {
7090
+ const requestId = tab.pendingAskRequestId;
7091
+ if (!tab._workerQuestions?.has(requestId)) return false;
7092
+ answerWorkerQuestion(tab.automationRunId, requestId, { decision: 'answer', answers }).catch((error) => {
7093
+ appendStatus(tab, error.message);
7094
+ // Keep the native card retryable after a delivery failure.
7095
+ if (tab._workerQuestions?.has(requestId)) {
7096
+ const card = tab.messagesEl?.querySelector(`[data-worker-question-id="${CSS.escape(requestId)}"]`);
7097
+ card?.querySelectorAll('button, input, textarea').forEach((el) => { el.disabled = false; });
7098
+ const submit = card?.querySelector('.ask-submit');
7099
+ if (submit) submit.textContent = t('common.retry');
7100
+ }
7101
+ });
7102
+ return true;
7103
+ }
6900
7104
  if (!tab.ws || tab.ws.readyState !== WebSocket.OPEN) return false;
6901
7105
 
6902
7106
  const requestId = tab.pendingAskRequestId;
@@ -7348,9 +7552,15 @@ function updateToolResult(tab, ev) {
7348
7552
  if (!$msgs) return;
7349
7553
  // Background task output (BashOutput results) routes back to the origin card
7350
7554
  try { handleBgToolResult(ev, tab); } catch {}
7351
- const escapedId = CSS.escape(ev.tool_use_id);
7352
- const card = $msgs.querySelector(`.tool-card[data-tool-id="${escapedId}"]`) || $msgs.querySelector(`.plan-card[data-tool-id="${escapedId}"]`);
7353
- if (!card) return;
7555
+ // During a replay the cards it built are indexed by id (no selector scan of the transcript per result).
7556
+ const indexed = $msgs._replayCards instanceof Map ? $msgs._replayCards.get(String(ev.tool_use_id)) : null;
7557
+ let card = indexed?.matches?.('.tool-card, .plan-card') ? indexed : null;
7558
+ if (!card) {
7559
+ const escapedId = CSS.escape(ev.tool_use_id);
7560
+ card = $msgs.querySelector(`.tool-card[data-tool-id="${escapedId}"]`) || $msgs.querySelector(`.plan-card[data-tool-id="${escapedId}"]`);
7561
+ }
7562
+ // A live result for a card the tail-first replay has not built yet: applied when it is built.
7563
+ if (!card) { holdPendingResult(tab, ev); return; }
7354
7564
  // Synthetic PostToolUse hook event + SubagentStop
7355
7565
  const toolNm = card.dataset.toolName || '';
7356
7566
  recordHookEvent(tab, ev.is_error ? 'PostToolUseFailure' : 'PostToolUse', toolNm);
@@ -7375,8 +7585,10 @@ function updateToolResult(tab, ev) {
7375
7585
  finalizeDiffCard(card, ev, typed?.view);
7376
7586
  if (!ev.is_error) return; // success: stats in the header say it all — skip noisy RESULT text
7377
7587
  }
7378
- // Collapse adjacent repeated MCP tool calls (post-insert) — cheap, DOM-local
7379
- try { collapseAdjacentMcpTools($msgs); } catch {}
7588
+ // Collapse adjacent repeated MCP tool calls inside this turn (replays batch once at the end)
7589
+ if (!tab._replaying && toolNm.startsWith('mcp__')) {
7590
+ try { collapseAdjacentMcpTools(card.parentElement || $msgs); } catch {}
7591
+ }
7380
7592
  // Plan cards don't have result sections — just mark success/error via border
7381
7593
  if (card.classList.contains('plan-card')) {
7382
7594
  card.style.borderColor = ev.is_error ? 'rgba(255,82,82,0.3)' : 'rgba(100,200,120,0.3)';
@@ -7558,6 +7770,9 @@ function _applySessionOptions(tab, msg) {
7558
7770
  if (hasCapability(tab, 'tool_policy')) msg.toolPolicy = tab.toolPolicy || 'full';
7559
7771
  // The tab's session settings (/session): the bridge validates them again.
7560
7772
  if (hasCapability(tab, 'session_settings')) msg.session = normalizeSession(tab.session);
7773
+ // Think off on a model with effort levels is a pick of its own: sent as 'off'
7774
+ // so the bridge disables thinking, or the user's settings' effortLevel applies.
7775
+ if (!msg.effort && hasCapability(tab, 'effort_off') && _modelSupportsEffort(tab)) msg.effort = 'off';
7561
7776
  // The Claude account this tab runs under (the default account sends nothing).
7562
7777
  if (hasCapability(tab, 'accounts')) msg.accountId = tab.accountId || 'default';
7563
7778
  // A name typed before the first message becomes the new session's title.
@@ -8057,6 +8272,7 @@ function renderAskUserQuestion(tab, requestId, input) {
8057
8272
  const allQuestions = input.questions || questions;
8058
8273
  const el = document.createElement('div');
8059
8274
  el.className = 'msg msg-assistant';
8275
+ el.dataset.workerQuestionId = requestId;
8060
8276
 
8061
8277
  const avatar = document.createElement('div');
8062
8278
  avatar.className = 'msg-avatar';
@@ -8238,7 +8454,7 @@ function renderAskUserQuestion(tab, requestId, input) {
8238
8454
  // Submit button — sends all answers as a batch
8239
8455
  submitBtn.addEventListener('click', () => {
8240
8456
  // An answer is shown as given only once it can be sent: until then the card stays as it is.
8241
- if (tab.ws?.readyState !== WebSocket.OPEN) { appendStatus(tab, ASK_ANSWER_UNSENT); return; }
8457
+ if (!tab.automationRunId && tab.ws?.readyState !== WebSocket.OPEN) { appendStatus(tab, ASK_ANSWER_UNSENT); return; }
8242
8458
  wrap.querySelectorAll('.ask-option').forEach(b => { b.disabled = true; });
8243
8459
  wrap.querySelectorAll('.ask-text-input, .ask-notes').forEach(i => { i.disabled = true; });
8244
8460
  submitBtn.disabled = true;
@@ -8308,8 +8524,12 @@ function appendUser(tab, text, images, files) {
8308
8524
  function _stampUserMessageUuid(tab, uuid) {
8309
8525
  const $msgs = tab.messagesEl;
8310
8526
  if (!$msgs || !uuid) return;
8311
- const rows = [...$msgs.querySelectorAll('.msg-user')].reverse();
8312
- const row = rows.find(r => !r.dataset.uuid);
8527
+ const last = $msgs.lastElementChild;
8528
+ let row = (last?.classList?.contains('msg-user') && !last.dataset.uuid) ? last : null;
8529
+ if (!row) {
8530
+ const rows = [...$msgs.querySelectorAll('.msg-user')].reverse();
8531
+ row = rows.find(r => !r.dataset.uuid);
8532
+ }
8313
8533
  if (!row) return;
8314
8534
  row.dataset.uuid = uuid;
8315
8535
  if (!tab.sdkMode) return;
@@ -10349,29 +10569,36 @@ const _fileLinkOptions = {
10349
10569
  };
10350
10570
 
10351
10571
  // ── Public API ──
10572
+ let _readyPromise = null;
10352
10573
  async function _ensureClaudePanelReady() {
10574
+ if (_readyPromise) return _readyPromise;
10353
10575
  if (!_panel) {
10354
- injectStyles();
10355
- _panel = buildPanel();
10356
- document.body.appendChild(_panel);
10357
- registerSidepanel({
10358
- owner: PANEL_OWNER, provider: 'claude', element: _panel,
10359
- header: _panel.querySelector('.cp-header'), actions: _panel.querySelector('.cp-header-actions'),
10360
- buttonClass: 'cp-header-btn', dockHandle: _panel.querySelector('.cp-resize-handle'),
10361
- applyVisibility: applyClaudeVisibility,
10362
- });
10363
- wireEvents();
10364
- _panel.querySelector('#cp-messages-container')?.addEventListener('click', _onMessagesClick);
10365
- wireFileLinks(_panel.querySelector('#cp-messages-container'), _fileLinkOptions);
10366
- await loadConfig(); // must complete before restoreTabs — checks server boot ID
10367
- loadSkills();
10368
- restoreTabs();
10369
- loadMonthlyCost();
10370
- syncSessionCosts();
10371
- _wireBrowserSync();
10372
- _startHeartbeat();
10373
- window.dispatchEvent(new CustomEvent('sidepanel-tray:provider-loaded', { detail: { provider: 'claude' } }));
10374
- _ensureCliSubscription();
10576
+ _readyPromise = (async () => {
10577
+ injectStyles();
10578
+ _panel = buildPanel();
10579
+ document.body.appendChild(_panel);
10580
+ registerSidepanel({
10581
+ owner: PANEL_OWNER, provider: 'claude', element: _panel,
10582
+ header: _panel.querySelector('.cp-header'), actions: _panel.querySelector('.cp-header-actions'),
10583
+ buttonClass: 'cp-header-btn', dockHandle: _panel.querySelector('.cp-resize-handle'),
10584
+ applyVisibility: applyClaudeVisibility,
10585
+ });
10586
+ wireEvents();
10587
+ _panel.querySelector('#cp-messages-container')?.addEventListener('click', _onMessagesClick);
10588
+ wireFileLinks(_panel.querySelector('#cp-messages-container'), _fileLinkOptions);
10589
+ await loadConfig(); // must complete before restoreTabs — checks server boot ID
10590
+ loadSkills();
10591
+ restoreTabs();
10592
+ setTimeout(() => {
10593
+ loadMonthlyCost();
10594
+ syncSessionCosts();
10595
+ }, 400);
10596
+ _wireBrowserSync();
10597
+ _startHeartbeat();
10598
+ window.dispatchEvent(new CustomEvent('sidepanel-tray:provider-loaded', { detail: { provider: 'claude' } }));
10599
+ _ensureCliSubscription();
10600
+ })();
10601
+ return _readyPromise;
10375
10602
  }
10376
10603
  }
10377
10604
 
@@ -10495,6 +10722,19 @@ export async function attachClaudeAutomation(run, { focus = !!run?.focus, isCanc
10495
10722
  tab._automationLifecycleError = run.error;
10496
10723
  appendError(tab, run.error);
10497
10724
  }
10725
+ syncWorkerQuestionQueue(tab, run, {
10726
+ open: (id, pending) => handleControlRequest(tab, { request_id: id, request: { tool_name: 'AskUserQuestion', input: pending.input || { questions: pending.questions } } }),
10727
+ close: (id) => {
10728
+ const card = tab.messagesEl?.querySelector(`[data-worker-question-id="${CSS.escape(id)}"]`);
10729
+ card?.querySelectorAll('button, input, textarea').forEach((el) => { el.disabled = true; });
10730
+ const submit = card?.querySelector('.ask-submit');
10731
+ if (submit) submit.textContent = t('workerQuestion.resolved');
10732
+ tab.pendingAskRequestId = null; tab.pendingAskToolUseId = null;
10733
+ tab.pendingAskBufferedAnswer = null; tab.askRenderedViaControl = false;
10734
+ // The same provider turn resumes; release messages buffered while its question was open.
10735
+ _flushMsgBuffer(tab);
10736
+ },
10737
+ });
10498
10738
  trackWorkerEnd(run, { apply: (view) => showWorkerEnd(tab, view), busy: hasLiveBackgroundWork(tab) });
10499
10739
  // The mode control is hidden while the run is active and back when it ended.
10500
10740
  if (tab === activeTab()) populateModeDropdown(_panel?.querySelector('#cp-mode'), tab);
@@ -11997,11 +12237,11 @@ function wireEvents() {
11997
12237
  // Side panel browser launch removed — all automations use floating browsers only.
11998
12238
 
11999
12239
  // Show panel without toggling it closed (used by notification click-through)
12000
- on('claude-panel:show', (data) => {
12001
- if (!_visible) toggleClaudePanel();
12240
+ on('claude-panel:show', async (data) => {
12241
+ if (!_visible) await toggleClaudePanel();
12002
12242
  if (data?.tabId) {
12003
- const idx = _tabs.findIndex(t => t.id === data.tabId);
12004
- if (idx >= 0) switchTab(idx);
12243
+ const idx = _tabs.findIndex(t => t.id === data.tabId || t.sessionId === data.tabId);
12244
+ if (idx >= 0 && idx !== _activeTabIdx) switchTab(idx);
12005
12245
  }
12006
12246
  });
12007
12247