synabun 2.0.0 → 2.0.7

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 (507) hide show
  1. package/CHANGELOG.md +97 -0
  2. package/NOTICE +5 -0
  3. package/README.md +2 -1
  4. package/THIRD-PARTY-LICENSES.md +59 -0
  5. package/benchmarks/fixtures/browser/README.md +1 -1
  6. package/benchmarks/fixtures/desktop/README.md +1 -1
  7. package/docs/whatsapp.md +153 -18
  8. package/hooks/claude-code/pre-compact.mjs +11 -0
  9. package/hooks/claude-code/prompt-submit.mjs +60 -184
  10. package/hooks/claude-code/session-start.mjs +73 -16
  11. package/hooks/claude-code/shared.mjs +26 -3
  12. package/hooks/claude-code/stop.mjs +5 -5
  13. package/lib/child-path.js +303 -0
  14. package/lib/claude-hooks.js +2 -1
  15. package/lib/data-home-migration.js +2 -0
  16. package/lib/desktop-app.js +265 -0
  17. package/lib/external-tools.js +7 -3
  18. package/lib/log-redact.js +102 -0
  19. package/lib/packaged-runtime.js +138 -11
  20. package/lib/start-launcher.js +88 -1
  21. package/mcp-server/dist/http.d.ts +3 -1
  22. package/mcp-server/dist/http.d.ts.map +1 -1
  23. package/mcp-server/dist/http.js +15 -4
  24. package/mcp-server/dist/http.js.map +1 -1
  25. package/mcp-server/dist/index.d.ts.map +1 -1
  26. package/mcp-server/dist/index.js +9 -6
  27. package/mcp-server/dist/index.js.map +1 -1
  28. package/mcp-server/dist/services/budget-judgments.d.ts +108 -0
  29. package/mcp-server/dist/services/budget-judgments.d.ts.map +1 -0
  30. package/mcp-server/dist/services/budget-judgments.js +153 -0
  31. package/mcp-server/dist/services/budget-judgments.js.map +1 -0
  32. package/mcp-server/dist/services/codex-config-heal.d.ts +5 -1
  33. package/mcp-server/dist/services/codex-config-heal.d.ts.map +1 -1
  34. package/mcp-server/dist/services/codex-config-heal.js +41 -4
  35. package/mcp-server/dist/services/codex-config-heal.js.map +1 -1
  36. package/mcp-server/dist/services/identity.d.ts +11 -0
  37. package/mcp-server/dist/services/identity.d.ts.map +1 -1
  38. package/mcp-server/dist/services/identity.js +24 -0
  39. package/mcp-server/dist/services/identity.js.map +1 -1
  40. package/mcp-server/dist/services/neural-interface.d.ts +13 -0
  41. package/mcp-server/dist/services/neural-interface.d.ts.map +1 -1
  42. package/mcp-server/dist/services/neural-interface.js +34 -2
  43. package/mcp-server/dist/services/neural-interface.js.map +1 -1
  44. package/mcp-server/dist/services/typesafe-config.d.ts +1 -1
  45. package/mcp-server/dist/services/typesafe-config.d.ts.map +1 -1
  46. package/mcp-server/dist/services/typesafe-config.js +4 -1
  47. package/mcp-server/dist/services/typesafe-config.js.map +1 -1
  48. package/mcp-server/dist/tools/agents.d.ts +10 -7
  49. package/mcp-server/dist/tools/agents.d.ts.map +1 -1
  50. package/mcp-server/dist/tools/agents.js +34 -13
  51. package/mcp-server/dist/tools/agents.js.map +1 -1
  52. package/mcp-server/dist/tools/whatsapp-send-file.d.ts +17 -0
  53. package/mcp-server/dist/tools/whatsapp-send-file.d.ts.map +1 -0
  54. package/mcp-server/dist/tools/whatsapp-send-file.js +44 -0
  55. package/mcp-server/dist/tools/whatsapp-send-file.js.map +1 -0
  56. package/mcp-server/dist/tui.js +2 -1
  57. package/mcp-server/dist/tui.js.map +1 -1
  58. package/mcp-server/dist/version.d.ts +5 -0
  59. package/mcp-server/dist/version.d.ts.map +1 -0
  60. package/mcp-server/dist/version.js +21 -0
  61. package/mcp-server/dist/version.js.map +1 -0
  62. package/mcp-server/package-lock.json +2 -2
  63. package/mcp-server/package.json +1 -3
  64. package/mcp-server/run.mjs +15 -9
  65. package/mcp-server/src/http.ts +18 -3
  66. package/mcp-server/src/index.ts +9 -6
  67. package/mcp-server/src/services/budget-judgments.ts +183 -0
  68. package/mcp-server/src/services/codex-config-heal.ts +35 -4
  69. package/mcp-server/src/services/identity.ts +27 -0
  70. package/mcp-server/src/services/neural-interface.ts +34 -2
  71. package/mcp-server/src/services/typesafe-config.ts +4 -1
  72. package/mcp-server/src/tools/agents.ts +33 -14
  73. package/mcp-server/src/tools/whatsapp-send-file.ts +51 -0
  74. package/mcp-server/src/tui.ts +2 -1
  75. package/mcp-server/src/version.ts +22 -0
  76. package/neural-interface/i18n/en.json +2126 -209
  77. package/neural-interface/i18n/pt-BR.json +2123 -207
  78. package/neural-interface/lib/account-usage.js +163 -0
  79. package/neural-interface/lib/assistant-api.js +316 -57
  80. package/neural-interface/lib/assistant-brains/claude.js +19 -2
  81. package/neural-interface/lib/assistant-brains/codex.js +3 -0
  82. package/neural-interface/lib/assistant-brains/opencode.js +17 -4
  83. package/neural-interface/lib/assistant-budget-planner.js +390 -0
  84. package/neural-interface/lib/assistant-budget-split.js +408 -0
  85. package/neural-interface/lib/assistant-budget.js +94 -95
  86. package/neural-interface/lib/assistant-catalog.js +179 -27
  87. package/neural-interface/lib/assistant-config.js +91 -14
  88. package/neural-interface/lib/assistant-dispatch.js +494 -138
  89. package/neural-interface/lib/assistant-hidden-models.js +393 -66
  90. package/neural-interface/lib/assistant-jev.js +26 -3
  91. package/neural-interface/lib/assistant-memory.js +24 -10
  92. package/neural-interface/lib/assistant-persona.js +73 -12
  93. package/neural-interface/lib/assistant-plan-permissions.js +8 -1
  94. package/neural-interface/lib/assistant-pricing.js +21 -6
  95. package/neural-interface/lib/assistant-route-accounts.js +14 -0
  96. package/neural-interface/lib/assistant-route-gate.js +19 -3
  97. package/neural-interface/lib/assistant-router.js +373 -42
  98. package/neural-interface/lib/assistant-runtime.js +488 -59
  99. package/neural-interface/lib/assistant-task-prompt.js +13 -3
  100. package/neural-interface/lib/assistant-test-density.js +142 -0
  101. package/neural-interface/lib/assistant-user-context-budgets.js +123 -0
  102. package/neural-interface/lib/assistant-user-context.js +908 -0
  103. package/neural-interface/lib/assistant-user-knowledge.js +328 -0
  104. package/neural-interface/lib/assistant-user-learn.js +404 -0
  105. package/neural-interface/lib/assistant-user-threads.js +226 -0
  106. package/neural-interface/lib/assistant-user-writer.js +183 -0
  107. package/neural-interface/lib/augmented-path.js +317 -53
  108. package/neural-interface/lib/browser-background-tab.js +134 -0
  109. package/neural-interface/lib/browser-screenshot.js +678 -61
  110. package/neural-interface/lib/claude-accounts.js +342 -31
  111. package/neural-interface/lib/claude-agent-bridge.js +123 -9
  112. package/neural-interface/lib/claude-executable.js +6 -1
  113. package/neural-interface/lib/claude-model-catalog.js +33 -5
  114. package/neural-interface/lib/claude-panel-auth.js +221 -0
  115. package/neural-interface/lib/claude-profile-api-key.js +62 -0
  116. package/neural-interface/lib/claude-temporary.js +46 -7
  117. package/neural-interface/lib/cli-install-api.js +137 -0
  118. package/neural-interface/lib/cli-install.js +797 -0
  119. package/neural-interface/lib/cli-path.js +130 -0
  120. package/neural-interface/lib/cli-resolver.js +744 -0
  121. package/neural-interface/lib/cli-update-planner.js +143 -7
  122. package/neural-interface/lib/cloudflared/install.js +533 -0
  123. package/neural-interface/lib/cloudflared/manager.js +545 -0
  124. package/neural-interface/lib/cloudflared/pins.js +115 -0
  125. package/neural-interface/lib/cloudflared/pins.json +63 -0
  126. package/neural-interface/lib/cloudflared/tunnel.js +265 -0
  127. package/neural-interface/lib/codex-account-auth.js +178 -0
  128. package/neural-interface/lib/codex-accounts.js +489 -0
  129. package/neural-interface/lib/codex-capabilities.js +2 -1
  130. package/neural-interface/lib/codex-compat-rpc.js +8 -2
  131. package/neural-interface/lib/codex-mcp-approvals.js +5 -1
  132. package/neural-interface/lib/codex-panel-auth.js +62 -0
  133. package/neural-interface/lib/codex-session-settings.js +1 -1
  134. package/neural-interface/lib/codex-settings-schema.js +120 -0
  135. package/neural-interface/lib/desktop/config.js +3 -0
  136. package/neural-interface/lib/editor-completion-endpoint.js +264 -0
  137. package/neural-interface/lib/editor-completion-opencode.js +436 -0
  138. package/neural-interface/lib/editor-completion.js +714 -0
  139. package/neural-interface/lib/editor-config.js +149 -0
  140. package/neural-interface/lib/editor-files-api.js +268 -0
  141. package/neural-interface/lib/editor-git-api.js +134 -0
  142. package/neural-interface/lib/file-explorer-api.js +1039 -0
  143. package/neural-interface/lib/file-explorer-system.js +355 -0
  144. package/neural-interface/lib/file-explorer-trash.ps1 +85 -0
  145. package/neural-interface/lib/file-explorer-watch.js +239 -0
  146. package/neural-interface/lib/git-bash/bzip2.js +339 -0
  147. package/neural-interface/lib/git-bash/claude-settings.js +73 -0
  148. package/neural-interface/lib/git-bash/extract-worker.js +34 -0
  149. package/neural-interface/lib/git-bash/install.js +529 -0
  150. package/neural-interface/lib/git-bash/licenses/COPYING +360 -0
  151. package/neural-interface/lib/git-bash/licenses/LGPL-2.1 +511 -0
  152. package/neural-interface/lib/git-bash/manager.js +268 -0
  153. package/neural-interface/lib/git-bash/pins.js +206 -0
  154. package/neural-interface/lib/git-bash/pins.json +34 -0
  155. package/neural-interface/lib/git-bash/resolver.js +194 -0
  156. package/neural-interface/lib/git-diff-api.js +103 -0
  157. package/neural-interface/lib/git-read.js +234 -0
  158. package/neural-interface/lib/greeting/api.js +89 -0
  159. package/neural-interface/lib/greeting/budgets.js +90 -0
  160. package/neural-interface/lib/greeting/engine.js +461 -0
  161. package/neural-interface/lib/greeting/facts.js +320 -0
  162. package/neural-interface/lib/greeting/sources.js +252 -0
  163. package/neural-interface/lib/greeting/writer.js +139 -0
  164. package/neural-interface/lib/http-guards.js +189 -13
  165. package/neural-interface/lib/install-details.js +145 -0
  166. package/neural-interface/lib/machine-names.js +88 -0
  167. package/neural-interface/lib/mcp-installer.js +14 -2
  168. package/neural-interface/lib/memory-api.js +31 -7
  169. package/neural-interface/lib/native-loop-providers.js +26 -8
  170. package/neural-interface/lib/native-loop-runtime.js +9 -4
  171. package/neural-interface/lib/os-path.js +319 -0
  172. package/neural-interface/lib/plugin-installer.js +4 -2
  173. package/neural-interface/lib/project-storage.js +5 -7
  174. package/neural-interface/lib/provider-panel-api-key-auth.js +92 -0
  175. package/neural-interface/lib/provider-panel-terminal-auth.js +95 -0
  176. package/neural-interface/lib/pty-host/core.js +2 -1
  177. package/neural-interface/lib/pty-host/manager.js +27 -2
  178. package/neural-interface/lib/remote-policy.js +20 -11
  179. package/neural-interface/lib/request-gate.js +589 -0
  180. package/neural-interface/lib/session-title-generator.js +4 -3
  181. package/neural-interface/lib/system-about.js +340 -0
  182. package/neural-interface/lib/system-activity.js +66 -0
  183. package/neural-interface/lib/terminal-env.js +11 -3
  184. package/neural-interface/lib/terminal-path.js +55 -0
  185. package/neural-interface/lib/ui-blob-api.js +108 -0
  186. package/neural-interface/lib/ui-blob-store.js +68 -0
  187. package/neural-interface/lib/virtual-socket.js +12 -1
  188. package/neural-interface/lib/whatsapp/baileys-adapter.js +165 -45
  189. package/neural-interface/lib/whatsapp/bridge.js +78 -34
  190. package/neural-interface/lib/whatsapp/commands.js +13 -14
  191. package/neural-interface/lib/whatsapp/config.js +134 -20
  192. package/neural-interface/lib/whatsapp/fake-baileys.js +99 -14
  193. package/neural-interface/lib/whatsapp/host-core.js +168 -35
  194. package/neural-interface/lib/whatsapp/inbound.js +5 -3
  195. package/neural-interface/lib/whatsapp/local-file.js +524 -0
  196. package/neural-interface/lib/whatsapp/manager.js +92 -32
  197. package/neural-interface/lib/whatsapp/paths.js +11 -1
  198. package/neural-interface/lib/whatsapp/protocol.js +65 -5
  199. package/neural-interface/lib/whatsapp/redact.js +63 -0
  200. package/neural-interface/lib/whatsapp/service.js +343 -91
  201. package/neural-interface/lib/whatsapp/transcription.js +463 -0
  202. package/neural-interface/package-lock.json +701 -2
  203. package/neural-interface/package.json +13 -2
  204. package/neural-interface/public/index.html +2 -0
  205. package/neural-interface/public/index2d.html +2 -0
  206. package/neural-interface/public/install-app.html +9 -6
  207. package/neural-interface/public/offline.html +19 -3
  208. package/neural-interface/public/onboarding.html +107 -2
  209. package/neural-interface/public/shared/account-i18n.js +7 -0
  210. package/neural-interface/public/shared/account-menu.js +539 -0
  211. package/neural-interface/public/shared/account-usage.js +19 -0
  212. package/neural-interface/public/shared/account-view.js +335 -0
  213. package/neural-interface/public/shared/agent-runtime-options.js +50 -14
  214. package/neural-interface/public/shared/assistant/asst-accounts.js +182 -184
  215. package/neural-interface/public/shared/assistant/asst-api.js +32 -2
  216. package/neural-interface/public/shared/assistant/asst-automation.js +286 -0
  217. package/neural-interface/public/shared/assistant/asst-brain-picker.js +430 -81
  218. package/neural-interface/public/shared/assistant/asst-budget.js +259 -303
  219. package/neural-interface/public/shared/assistant/asst-composer.js +11 -1
  220. package/neural-interface/public/shared/assistant/asst-mascot.js +140 -61
  221. package/neural-interface/public/shared/assistant/asst-menu.js +27 -11
  222. package/neural-interface/public/shared/assistant/asst-panel.js +198 -42
  223. package/neural-interface/public/shared/assistant/asst-render.js +197 -23
  224. package/neural-interface/public/shared/assistant/asst-route-models.js +108 -0
  225. package/neural-interface/public/shared/assistant/asst-route.js +95 -343
  226. package/neural-interface/public/shared/assistant/asst-routes-editor.js +534 -0
  227. package/neural-interface/public/shared/assistant/asst-sanitize.js +8 -1
  228. package/neural-interface/public/shared/assistant/asst-sidepanel.js +31 -9
  229. package/neural-interface/public/shared/assistant/asst-slash.js +1 -1
  230. package/neural-interface/public/shared/assistant/asst-state.js +186 -2
  231. package/neural-interface/public/shared/assistant/asst-styles.js +247 -11
  232. package/neural-interface/public/shared/blob-store.js +111 -22
  233. package/neural-interface/public/shared/cdx/cdx-commands.js +4 -1
  234. package/neural-interface/public/shared/cdx/cdx-features.js +22 -14
  235. package/neural-interface/public/shared/cdx/cdx-icons.js +2 -0
  236. package/neural-interface/public/shared/cdx/cdx-panel.js +80 -118
  237. package/neural-interface/public/shared/cdx/cdx-render.js +25 -89
  238. package/neural-interface/public/shared/cdx/cdx-settings.js +231 -0
  239. package/neural-interface/public/shared/cdx/cdx-styles.js +164 -107
  240. package/neural-interface/public/shared/cdx/cdx-tabs.js +348 -668
  241. package/neural-interface/public/shared/cdx/cdx-ws.js +2 -0
  242. package/neural-interface/public/shared/claude-account-view.js +3 -0
  243. package/neural-interface/public/shared/cli-install-view.js +392 -0
  244. package/neural-interface/public/shared/cli-status.js +282 -22
  245. package/neural-interface/public/shared/constants.js +6 -1
  246. package/neural-interface/public/shared/cp/cp-events.js +2 -1
  247. package/neural-interface/public/shared/cp/cp-rehydrate.js +2 -1
  248. package/neural-interface/public/shared/cp/cp-styles.js +61 -0
  249. package/neural-interface/public/shared/desktop-bridge.js +39 -0
  250. package/neural-interface/public/shared/desktop-titlebar-page.js +59 -0
  251. package/neural-interface/public/shared/desktop-titlebar-view.js +122 -0
  252. package/neural-interface/public/shared/desktop-titlebar.js +153 -0
  253. package/neural-interface/public/shared/desktop-update-view.js +462 -0
  254. package/neural-interface/public/shared/editor/editor-ai-logic.js +409 -0
  255. package/neural-interface/public/shared/editor/editor-ai.js +619 -0
  256. package/neural-interface/public/shared/editor/editor-commands.js +212 -0
  257. package/neural-interface/public/shared/editor/editor-compare.js +103 -0
  258. package/neural-interface/public/shared/editor/editor-completion-list.js +172 -0
  259. package/neural-interface/public/shared/editor/editor-doc.js +257 -0
  260. package/neural-interface/public/shared/editor/editor-git-logic.js +131 -0
  261. package/neural-interface/public/shared/editor/editor-git.js +95 -0
  262. package/neural-interface/public/shared/editor/editor-languages.js +156 -0
  263. package/neural-interface/public/shared/editor/editor-lint.js +229 -0
  264. package/neural-interface/public/shared/editor/editor-loader.js +51 -0
  265. package/neural-interface/public/shared/editor/editor-markdown.js +90 -0
  266. package/neural-interface/public/shared/editor/editor-menu.js +233 -0
  267. package/neural-interface/public/shared/editor/editor-minimap.js +371 -0
  268. package/neural-interface/public/shared/editor/editor-palette-logic.js +192 -0
  269. package/neural-interface/public/shared/editor/editor-palette.js +125 -0
  270. package/neural-interface/public/shared/editor/editor-panel-logic.js +288 -0
  271. package/neural-interface/public/shared/editor/editor-panel.js +2099 -0
  272. package/neural-interface/public/shared/editor/editor-prefs.js +224 -0
  273. package/neural-interface/public/shared/editor/editor-safety.js +500 -0
  274. package/neural-interface/public/shared/editor/editor-search.js +352 -0
  275. package/neural-interface/public/shared/editor/editor-statusbar.js +144 -0
  276. package/neural-interface/public/shared/editor/editor-tabs-logic.js +331 -0
  277. package/neural-interface/public/shared/editor/editor-tabs.js +233 -0
  278. package/neural-interface/public/shared/editor/editor-theme.js +244 -0
  279. package/neural-interface/public/shared/editor/editor-view.js +332 -0
  280. package/neural-interface/public/shared/explorer/fx-api.js +216 -0
  281. package/neural-interface/public/shared/explorer/fx-links.js +204 -0
  282. package/neural-interface/public/shared/explorer/fx-logic.js +537 -0
  283. package/neural-interface/public/shared/explorer/fx-main.js +865 -0
  284. package/neural-interface/public/shared/explorer/fx-names.js +87 -0
  285. package/neural-interface/public/shared/explorer/fx-ops.js +370 -0
  286. package/neural-interface/public/shared/explorer/fx-popups.js +490 -0
  287. package/neural-interface/public/shared/explorer/fx-tree.js +790 -0
  288. package/neural-interface/public/shared/file-links.js +339 -0
  289. package/neural-interface/public/shared/git-bash-view.js +145 -0
  290. package/neural-interface/public/shared/greeting/greeting-card.js +178 -0
  291. package/neural-interface/public/shared/greeting/greeting-view.js +148 -0
  292. package/neural-interface/public/shared/html-shell.js +161 -165
  293. package/neural-interface/public/shared/i18n.js +19 -1
  294. package/neural-interface/public/shared/install-details-view.js +86 -0
  295. package/neural-interface/public/shared/ocp-v2/ocp-v2-childpanel.js +4 -0
  296. package/neural-interface/public/shared/ocp-v2/ocp-v2-greeting.js +47 -0
  297. package/neural-interface/public/shared/ocp-v2/ocp-v2-panel.js +131 -17
  298. package/neural-interface/public/shared/ocp-v2/ocp-v2-render.js +7 -1
  299. package/neural-interface/public/shared/ocp-v2/ocp-v2-send.js +22 -0
  300. package/neural-interface/public/shared/ocp-v2/ocp-v2-styles.js +158 -0
  301. package/neural-interface/public/shared/ocp-v2/ocp-v2-ws.js +3 -2
  302. package/neural-interface/public/shared/pwa-install-page.js +32 -2
  303. package/neural-interface/public/shared/pwa-install.css +3 -3
  304. package/neural-interface/public/shared/pwa-install.js +100 -1
  305. package/neural-interface/public/shared/settings/settings-about-pane.js +271 -0
  306. package/neural-interface/public/shared/settings/settings-about.js +254 -0
  307. package/neural-interface/public/shared/settings/settings-code-editor-pane.js +412 -0
  308. package/neural-interface/public/shared/settings/settings-code-editor.js +328 -0
  309. package/neural-interface/public/shared/settings/settings-greeting.js +133 -0
  310. package/neural-interface/public/shared/settings/settings-ia.js +33 -4
  311. package/neural-interface/public/shared/settings/settings-user-context.js +154 -0
  312. package/neural-interface/public/shared/showcase-privacy.js +489 -0
  313. package/neural-interface/public/shared/start-bridge.js +238 -19
  314. package/neural-interface/public/shared/storage.js +30 -3
  315. package/neural-interface/public/shared/styles.css +1710 -770
  316. package/neural-interface/public/shared/synabun-mascot.js +68 -58
  317. package/neural-interface/public/shared/tunnel-view.js +167 -0
  318. package/neural-interface/public/shared/ui-claude-panel.js +369 -229
  319. package/neural-interface/public/shared/ui-cli-banner.js +355 -0
  320. package/neural-interface/public/shared/ui-detail.js +15 -0
  321. package/neural-interface/public/shared/ui-explorer.js +22 -0
  322. package/neural-interface/public/shared/ui-file-explorer.js +481 -2496
  323. package/neural-interface/public/shared/ui-file-links.js +103 -0
  324. package/neural-interface/public/shared/ui-git-bash.js +124 -0
  325. package/neural-interface/public/shared/ui-help.js +26 -0
  326. package/neural-interface/public/shared/ui-invite.js +126 -116
  327. package/neural-interface/public/shared/ui-judgments.js +10 -1
  328. package/neural-interface/public/shared/ui-keybinds.js +4 -0
  329. package/neural-interface/public/shared/ui-loading.js +187 -19
  330. package/neural-interface/public/shared/ui-menubar.js +25 -33
  331. package/neural-interface/public/shared/ui-native-loop-router-state.js +11 -0
  332. package/neural-interface/public/shared/ui-native-loop-router.js +15 -2
  333. package/neural-interface/public/shared/ui-navbar.js +48 -22
  334. package/neural-interface/public/shared/ui-provider-auth.js +104 -0
  335. package/neural-interface/public/shared/ui-provider-terminal-auth.js +223 -0
  336. package/neural-interface/public/shared/ui-sessions.js +9 -3
  337. package/neural-interface/public/shared/ui-settings.js +509 -490
  338. package/neural-interface/public/shared/ui-sidepanel-runtime.js +2 -1
  339. package/neural-interface/public/shared/ui-sidepanel-sessions.js +3 -1
  340. package/neural-interface/public/shared/ui-sync.js +22 -0
  341. package/neural-interface/public/shared/ui-terminal.js +40 -5
  342. package/neural-interface/public/shared/ui-tool-updates.js +5 -0
  343. package/neural-interface/public/shared/ui-tunnel.js +130 -0
  344. package/neural-interface/public/shared/ui-tutorial-steps.js +6 -2
  345. package/neural-interface/public/shared/ui-update.js +594 -5
  346. package/neural-interface/public/shared/ui-whatsapp.js +21 -6
  347. package/neural-interface/public/shared/ui-whiteboard.js +12 -0
  348. package/neural-interface/public/shared/ui-worker-end.js +245 -0
  349. package/neural-interface/public/shared/ui-workspaces.js +41 -27
  350. package/neural-interface/public/shared/whatsapp/wa-view.js +51 -8
  351. package/neural-interface/public/shared/worker-end.js +253 -0
  352. package/neural-interface/public/sw.js +2 -2
  353. package/neural-interface/public/variant/2d/main.js +10 -1
  354. package/neural-interface/public/variant/3d/main.js +10 -1
  355. package/neural-interface/public/vendor/codemirror/chunks/apl-CSDKT72Z.js +1 -0
  356. package/neural-interface/public/vendor/codemirror/chunks/asciiarmor-JYYPU5BM.js +1 -0
  357. package/neural-interface/public/vendor/codemirror/chunks/asn1-NRWCZDA6.js +1 -0
  358. package/neural-interface/public/vendor/codemirror/chunks/asterisk-5OU6D4ZQ.js +1 -0
  359. package/neural-interface/public/vendor/codemirror/chunks/brainfuck-ID4N4SYC.js +1 -0
  360. package/neural-interface/public/vendor/codemirror/chunks/chunk-4RZWGCS2.js +1 -0
  361. package/neural-interface/public/vendor/codemirror/chunks/chunk-ACTJM3JR.js +11 -0
  362. package/neural-interface/public/vendor/codemirror/chunks/chunk-S6BMAXLK.js +1 -0
  363. package/neural-interface/public/vendor/codemirror/chunks/chunk-TO2RDOHQ.js +3 -0
  364. package/neural-interface/public/vendor/codemirror/chunks/chunk-US2GMZZQ.js +13 -0
  365. package/neural-interface/public/vendor/codemirror/chunks/chunk-WYYO5ZAJ.js +1 -0
  366. package/neural-interface/public/vendor/codemirror/chunks/chunk-XALZTDJC.js +1 -0
  367. package/neural-interface/public/vendor/codemirror/chunks/chunk-YRNUTRDV.js +1 -0
  368. package/neural-interface/public/vendor/codemirror/chunks/chunk-Z3QOKH62.js +1 -0
  369. package/neural-interface/public/vendor/codemirror/chunks/clike-OWQBSOGL.js +1 -0
  370. package/neural-interface/public/vendor/codemirror/chunks/clojure-KV75UQWC.js +1 -0
  371. package/neural-interface/public/vendor/codemirror/chunks/cmake-GTEP5Y3C.js +1 -0
  372. package/neural-interface/public/vendor/codemirror/chunks/cobol-WCZ7QEGJ.js +1 -0
  373. package/neural-interface/public/vendor/codemirror/chunks/coffeescript-TGLBOT6B.js +1 -0
  374. package/neural-interface/public/vendor/codemirror/chunks/commonlisp-WVVI432V.js +1 -0
  375. package/neural-interface/public/vendor/codemirror/chunks/crystal-S6R6HLKQ.js +1 -0
  376. package/neural-interface/public/vendor/codemirror/chunks/css-PDA2RINP.js +1 -0
  377. package/neural-interface/public/vendor/codemirror/chunks/cypher-HL5QPL4C.js +1 -0
  378. package/neural-interface/public/vendor/codemirror/chunks/d-HHJH2HAJ.js +1 -0
  379. package/neural-interface/public/vendor/codemirror/chunks/diff-MN3A35BY.js +1 -0
  380. package/neural-interface/public/vendor/codemirror/chunks/dist-25SQLBVS.js +3 -0
  381. package/neural-interface/public/vendor/codemirror/chunks/dist-27ZGGJJS.js +1 -0
  382. package/neural-interface/public/vendor/codemirror/chunks/dist-4E52OJIF.js +1 -0
  383. package/neural-interface/public/vendor/codemirror/chunks/dist-6FT73XDC.js +2 -0
  384. package/neural-interface/public/vendor/codemirror/chunks/dist-BE6YGRMW.js +1 -0
  385. package/neural-interface/public/vendor/codemirror/chunks/dist-CJSGKW2O.js +1 -0
  386. package/neural-interface/public/vendor/codemirror/chunks/dist-CRPH2WLI.js +1 -0
  387. package/neural-interface/public/vendor/codemirror/chunks/dist-CTG7JEQN.js +1 -0
  388. package/neural-interface/public/vendor/codemirror/chunks/dist-I3FEVELX.js +1 -0
  389. package/neural-interface/public/vendor/codemirror/chunks/dist-IM5BBSWZ.js +1 -0
  390. package/neural-interface/public/vendor/codemirror/chunks/dist-KERUKTIF.js +1 -0
  391. package/neural-interface/public/vendor/codemirror/chunks/dist-LXTMWZAM.js +1 -0
  392. package/neural-interface/public/vendor/codemirror/chunks/dist-MABASSET.js +1 -0
  393. package/neural-interface/public/vendor/codemirror/chunks/dist-MJVBUZLL.js +3 -0
  394. package/neural-interface/public/vendor/codemirror/chunks/dist-MNB3WZVU.js +1 -0
  395. package/neural-interface/public/vendor/codemirror/chunks/dist-QYWXOE3U.js +7 -0
  396. package/neural-interface/public/vendor/codemirror/chunks/dist-TEBOFWLZ.js +1 -0
  397. package/neural-interface/public/vendor/codemirror/chunks/dist-UNXFROM3.js +1 -0
  398. package/neural-interface/public/vendor/codemirror/chunks/dist-XL2SLQO6.js +1 -0
  399. package/neural-interface/public/vendor/codemirror/chunks/dist-XO42Y3AP.js +6 -0
  400. package/neural-interface/public/vendor/codemirror/chunks/dist-Y2WWDWJK.js +1 -0
  401. package/neural-interface/public/vendor/codemirror/chunks/dist-YFEZKWMU.js +1 -0
  402. package/neural-interface/public/vendor/codemirror/chunks/dist-ZY7SWQO6.js +1 -0
  403. package/neural-interface/public/vendor/codemirror/chunks/dockerfile-3Y4R4NCH.js +1 -0
  404. package/neural-interface/public/vendor/codemirror/chunks/dtd-5343GJ4P.js +1 -0
  405. package/neural-interface/public/vendor/codemirror/chunks/dylan-FI2CXSW2.js +1 -0
  406. package/neural-interface/public/vendor/codemirror/chunks/ebnf-GNQ77YWA.js +1 -0
  407. package/neural-interface/public/vendor/codemirror/chunks/ecl-MRPUAE2L.js +1 -0
  408. package/neural-interface/public/vendor/codemirror/chunks/eiffel-DZROES27.js +1 -0
  409. package/neural-interface/public/vendor/codemirror/chunks/elm-4KICRFKQ.js +1 -0
  410. package/neural-interface/public/vendor/codemirror/chunks/erlang-3IHYC263.js +1 -0
  411. package/neural-interface/public/vendor/codemirror/chunks/factor-C6D3E5UK.js +1 -0
  412. package/neural-interface/public/vendor/codemirror/chunks/fcl-VYTWUJMI.js +1 -0
  413. package/neural-interface/public/vendor/codemirror/chunks/forth-ZBO2EH5M.js +1 -0
  414. package/neural-interface/public/vendor/codemirror/chunks/fortran-5NDCRWIK.js +1 -0
  415. package/neural-interface/public/vendor/codemirror/chunks/gas-SKRMOXFP.js +1 -0
  416. package/neural-interface/public/vendor/codemirror/chunks/gherkin-L45YXZLW.js +1 -0
  417. package/neural-interface/public/vendor/codemirror/chunks/groovy-GLPTLN44.js +1 -0
  418. package/neural-interface/public/vendor/codemirror/chunks/haskell-E7NF7P4Y.js +1 -0
  419. package/neural-interface/public/vendor/codemirror/chunks/haxe-K2NVDOB6.js +1 -0
  420. package/neural-interface/public/vendor/codemirror/chunks/http-Q6DRQIJB.js +1 -0
  421. package/neural-interface/public/vendor/codemirror/chunks/idl-MZYNJVLN.js +1 -0
  422. package/neural-interface/public/vendor/codemirror/chunks/javascript-6YRYK2HR.js +1 -0
  423. package/neural-interface/public/vendor/codemirror/chunks/julia-HXEL2T5G.js +1 -0
  424. package/neural-interface/public/vendor/codemirror/chunks/livescript-MV7CI2JZ.js +1 -0
  425. package/neural-interface/public/vendor/codemirror/chunks/lua-E36NHOOD.js +1 -0
  426. package/neural-interface/public/vendor/codemirror/chunks/mathematica-3XOZZCMC.js +1 -0
  427. package/neural-interface/public/vendor/codemirror/chunks/mbox-ODYGFKS7.js +1 -0
  428. package/neural-interface/public/vendor/codemirror/chunks/mirc-WB43AWF5.js +1 -0
  429. package/neural-interface/public/vendor/codemirror/chunks/mllike-ZT6ET7ZH.js +1 -0
  430. package/neural-interface/public/vendor/codemirror/chunks/modelica-LLMZPFGA.js +1 -0
  431. package/neural-interface/public/vendor/codemirror/chunks/mscgen-2OPNNYFH.js +1 -0
  432. package/neural-interface/public/vendor/codemirror/chunks/mumps-6P3HWJH6.js +1 -0
  433. package/neural-interface/public/vendor/codemirror/chunks/nginx-TCGKQTRW.js +1 -0
  434. package/neural-interface/public/vendor/codemirror/chunks/nsis-ATIGIXTJ.js +1 -0
  435. package/neural-interface/public/vendor/codemirror/chunks/ntriples-5PLBI5OV.js +1 -0
  436. package/neural-interface/public/vendor/codemirror/chunks/octave-QF2S3NL6.js +1 -0
  437. package/neural-interface/public/vendor/codemirror/chunks/oz-JFIVFFH2.js +1 -0
  438. package/neural-interface/public/vendor/codemirror/chunks/pascal-JG6SW76V.js +1 -0
  439. package/neural-interface/public/vendor/codemirror/chunks/perl-4B3IWVVO.js +1 -0
  440. package/neural-interface/public/vendor/codemirror/chunks/pig-ABEBH4MI.js +1 -0
  441. package/neural-interface/public/vendor/codemirror/chunks/powershell-H32XE7TL.js +1 -0
  442. package/neural-interface/public/vendor/codemirror/chunks/properties-NN644MMA.js +1 -0
  443. package/neural-interface/public/vendor/codemirror/chunks/protobuf-YKWQK2J3.js +1 -0
  444. package/neural-interface/public/vendor/codemirror/chunks/pug-RIYXEKFW.js +1 -0
  445. package/neural-interface/public/vendor/codemirror/chunks/puppet-JZQ5WPEV.js +1 -0
  446. package/neural-interface/public/vendor/codemirror/chunks/python-EBCGGHAC.js +1 -0
  447. package/neural-interface/public/vendor/codemirror/chunks/q-5UMH2YQY.js +1 -0
  448. package/neural-interface/public/vendor/codemirror/chunks/r-7XMGG453.js +1 -0
  449. package/neural-interface/public/vendor/codemirror/chunks/rpm-IEV7TSLR.js +1 -0
  450. package/neural-interface/public/vendor/codemirror/chunks/ruby-T4SPFPNM.js +1 -0
  451. package/neural-interface/public/vendor/codemirror/chunks/sas-GEDX67ET.js +1 -0
  452. package/neural-interface/public/vendor/codemirror/chunks/scheme-FTLOWKPC.js +1 -0
  453. package/neural-interface/public/vendor/codemirror/chunks/shell-HUFRKFSJ.js +1 -0
  454. package/neural-interface/public/vendor/codemirror/chunks/sieve-P5QUDVYO.js +1 -0
  455. package/neural-interface/public/vendor/codemirror/chunks/smalltalk-2SORJNQI.js +1 -0
  456. package/neural-interface/public/vendor/codemirror/chunks/solr-M22TGAR5.js +1 -0
  457. package/neural-interface/public/vendor/codemirror/chunks/sparql-4M3KL752.js +1 -0
  458. package/neural-interface/public/vendor/codemirror/chunks/spreadsheet-3K23LA4N.js +1 -0
  459. package/neural-interface/public/vendor/codemirror/chunks/sql-VHRFMUBI.js +1 -0
  460. package/neural-interface/public/vendor/codemirror/chunks/stex-QZDAQUY2.js +1 -0
  461. package/neural-interface/public/vendor/codemirror/chunks/stylus-7ZDKAKBD.js +1 -0
  462. package/neural-interface/public/vendor/codemirror/chunks/swift-WCLCLGMR.js +1 -0
  463. package/neural-interface/public/vendor/codemirror/chunks/tcl-JLMU6WOK.js +1 -0
  464. package/neural-interface/public/vendor/codemirror/chunks/textile-4CHDKZ4T.js +1 -0
  465. package/neural-interface/public/vendor/codemirror/chunks/tiddlywiki-DKMCZZQ4.js +1 -0
  466. package/neural-interface/public/vendor/codemirror/chunks/tiki-F4JIG36Q.js +1 -0
  467. package/neural-interface/public/vendor/codemirror/chunks/toml-BHMHTIM2.js +1 -0
  468. package/neural-interface/public/vendor/codemirror/chunks/troff-YQ2KV5NT.js +1 -0
  469. package/neural-interface/public/vendor/codemirror/chunks/ttcn-ZHRCVFJY.js +1 -0
  470. package/neural-interface/public/vendor/codemirror/chunks/ttcn-cfg-KJOVTY4X.js +1 -0
  471. package/neural-interface/public/vendor/codemirror/chunks/turtle-535P5N6I.js +1 -0
  472. package/neural-interface/public/vendor/codemirror/chunks/vb-76UMQHOT.js +1 -0
  473. package/neural-interface/public/vendor/codemirror/chunks/vbscript-CGNDTK6Z.js +1 -0
  474. package/neural-interface/public/vendor/codemirror/chunks/velocity-VSJDO5T5.js +1 -0
  475. package/neural-interface/public/vendor/codemirror/chunks/verilog-4T6ETXIP.js +1 -0
  476. package/neural-interface/public/vendor/codemirror/chunks/vhdl-T7FNAXUN.js +1 -0
  477. package/neural-interface/public/vendor/codemirror/chunks/webidl-P6MJSJ6J.js +1 -0
  478. package/neural-interface/public/vendor/codemirror/chunks/xquery-UOVILRGN.js +1 -0
  479. package/neural-interface/public/vendor/codemirror/chunks/yacas-CV2APROL.js +1 -0
  480. package/neural-interface/public/vendor/codemirror/chunks/z80-3KFNVM4M.js +1 -0
  481. package/neural-interface/public/vendor/codemirror/codemirror-6.43.14-75f05e67.min.js +2 -0
  482. package/neural-interface/public/vendor/codemirror/manifest.json +60 -0
  483. package/neural-interface/scripts/build-vendor.mjs +78 -2
  484. package/neural-interface/scripts/vendor-codemirror-entry.js +121 -0
  485. package/neural-interface/server.js +2085 -1629
  486. package/neural-interface/templates/rulesets/README.md +1 -1
  487. package/package.json +5 -8
  488. package/setup.js +53 -15
  489. package/benchmarks/browser-live-results.json +0 -79
  490. package/benchmarks/browser-v2-results.json +0 -258
  491. package/neural-interface/lib/codex-capabilities.d.ts +0 -41
  492. package/neural-interface/public/category-logos/claude.svg +0 -1
  493. package/neural-interface/public/category-logos/gemini-color.svg +0 -1
  494. package/neural-interface/public/category-logos/openai.svg +0 -1
  495. package/neural-interface/public/category-logos/synabun.png +0 -0
  496. package/neural-interface/public/claude.svg +0 -1
  497. package/neural-interface/public/gallery.svg +0 -6
  498. package/neural-interface/public/leonardoai.svg +0 -1
  499. package/neural-interface/public/mcp.svg +0 -4
  500. package/neural-interface/public/opencode-logo-dark-square.svg +0 -1
  501. package/neural-interface/public/pin-svgrepo-com.svg +0 -25
  502. package/neural-interface/public/sessionicon.svg +0 -8
  503. package/neural-interface/public/toolbar-album.svg +0 -8
  504. package/neural-interface/public/toolbar-question.svg +0 -6
  505. package/neural-interface/public/toolbar-sessions.svg +0 -6
  506. package/neural-interface/scripts/codex-protocol-fixture.mjs +0 -83
  507. package/skills/synabun/synabunicon.png +0 -0
@@ -29,11 +29,16 @@ import { emit, on, state as appState } from '../state.js';
29
29
  import { storage } from '../storage.js';
30
30
  import { KEYS } from '../constants.js';
31
31
  import { t } from '../i18n.js';
32
+ // The account words of the brain picker and the accounts manager (account-view.js) in the page's language.
33
+ // Here, not in those modules: node tests load them, and i18n.js reads the page's storage.
34
+ import '../account-i18n.js';
35
+ import { forgetGreeting, mountGreeting } from '../greeting/greeting-card.js';
32
36
  import { notify, NOTIF_TYPE } from '../ui-notifications.js';
33
37
  import { getProviderMeta } from '../provider-icons.js';
34
38
  import { isGuest, hasPermission } from '../ui-sync.js';
35
39
  import { getWhiteboardElementById } from '../ui-whiteboard.js';
36
40
  import { focusNativeLoopRun } from '../ui-native-loop-router.js';
41
+ import { wireFileLinks } from '../ui-file-links.js';
37
42
  import { injectAssistantStyles } from './asst-styles.js';
38
43
  import { createAssistantSocket } from './asst-ws.js';
39
44
  import { createRenderer, ICON_ASSISTANT, mdInto } from './asst-render.js';
@@ -42,19 +47,19 @@ import { lastInputAt, onKey, onPointer } from '../synabun-ticker.js';
42
47
  import { buildControlResponse, closedControlState, normalizeControlRequest, renderControlCard } from './asst-control.js';
43
48
  import { buildPromptText, createComposer } from './asst-composer.js';
44
49
  import { buildDispatchSpec, parseSlashCommand, slashHelpMarkdown } from './asst-slash.js';
45
- import { createBrainPicker } from './asst-brain-picker.js';
50
+ import { cachedProjects, createBrainPicker, testDensityLabel } from './asst-brain-picker.js';
46
51
  import { createRovingToolbar } from './asst-roving.js';
47
52
  import { openAccountsManager } from './asst-accounts.js';
48
53
  import { createAgentsDock } from './asst-agents-dock.js';
49
54
  import { createUsageGauge } from './asst-usage.js';
50
55
  import { closeMenu, isMenuOpen, openMenu } from './asst-menu.js';
51
56
  import { createRouteModeControl, openModelsManager, openRoutesEditor, refreshModelsManager, renderRouteCard, routeModeLabel } from './asst-route.js';
52
- import { budgetChipText, budgetTone, openBudgetEditor } from './asst-budget.js';
57
+ import { budgetChipText, budgetSpendText, budgetTone, closeBudgetEditor, createBudgetController, mountBudgetChip, openBudgetEditor } from './asst-budget.js';
53
58
  import { computerRemoteView, createComputerPanel, createComputerToggle, normalizeComputerRemote, openFrameLightbox, closeFrameLightbox, requestDesktopStop } from './asst-computer.js';
54
59
  import {
55
- applyLimit, applyLimitPacket, applyUsagePacket, brainLabel, createTabState, DEFAULT_ROUTE_MODE, deserializeTabState, fmtCost, fmtRunLine, limitExpiresIn, limitNoticeText, modelShortName,
56
- brainModePatch, normalizeBrain, normalizeComputerMode, permissionModeLabel, normalizeRouteMode, providerShortLabel, readStoredBrain, runFromPayload,
57
- serializeTabState, tabStorageKey,
60
+ applyLimit, applyLimitPacket, applyUsagePacket, brainLabel, createRenameWriter, createTabState, DEFAULT_ROUTE_MODE, deserializeTabState, fmtCost, fmtRunLine, limitExpiresIn, limitNoticeText, modelShortName,
61
+ brainModePatch, normalizeBrain, normalizeComputerMode, permissionModeLabel, normalizeRouteMode, normalizeTestDensity, projectLabel, providerShortLabel, readStoredBrain, runFromPayload,
62
+ serializeTabState, tabStorageKey, userSessionTitle,
58
63
  } from './asst-state.js';
59
64
  import {
60
65
  closeAssistantSession, dispatchAssistantRun, escalateAssistantRun, focusAssistantRun, getAssistantRun,
@@ -93,10 +98,6 @@ function num(value) {
93
98
  return value != null && value !== '' && Number.isFinite(n) ? n : null;
94
99
  }
95
100
 
96
- function basename(path) {
97
- return String(path || '').split(/[\\/]/).filter(Boolean).pop() || String(path || '');
98
- }
99
-
100
101
  function relativeTime(value) {
101
102
  const ms = new Date(value || 0).getTime();
102
103
  if (!ms) return '';
@@ -109,6 +110,10 @@ function relativeTime(value) {
109
110
  return `${Math.floor(hrs / 24)}d`;
110
111
  }
111
112
 
113
+ // Renames on their way to a session record. One writer for the page, not one per tab: a write
114
+ // outlives the tab that asked for it, and every Sessions menu waits for all of them.
115
+ const renames = createRenameWriter({ patch: patchAssistantSession });
116
+
112
117
  function clipCopy(text) {
113
118
  if (navigator.clipboard?.writeText) navigator.clipboard.writeText(text).catch(() => {});
114
119
  }
@@ -116,17 +121,21 @@ function clipCopy(text) {
116
121
  /**
117
122
  * mountAssistant(viewport, { sessionId, brain, host, session })
118
123
  * host: { id: 'terminal'|'sidepanel', notifySource: 'cli'|'panel',
119
- * setLabel(text), setStatus('idle'|'working'|'action'|'done'), setBrain(brain),
124
+ * setLabel(text), setName?(text, renamed) (a rename that was not saved: the tab's name and its
125
+ * renamed flag go back, saved label included), setStatus('idle'|'working'|'action'|'done'), setBrain(brain),
120
126
  * isVisible() → bool, toolbarSlot?: Element (where the toolbar goes; default the root),
121
127
  * takeHeroOrigin?() → DOMRect | null (a character the host showed while starting: the empty state's flies in from it) }
122
128
  * The hooks may fire during mount, before this returns.
123
- * The tab label is always "SynaBun"; a user rename arrives via controller.setTitle.
129
+ * The tab label is always "SynaBun"; a user rename arrives via controller.setTitle
130
+ * and is written to the session record (saveRename), where every session list reads it.
124
131
  */
125
132
  export function mountAssistant(viewport, { sessionId, brain, host = {}, session = null } = {}) {
126
133
  injectAssistantStyles();
127
134
  const saved = deserializeTabState(storage.getItem(tabStorageKey(sessionId)));
135
+ let greetingId = null; // the greeting this conversation opened with, until its first prompt carries it
136
+ const defaultTitle = tt('assistant.defaultTitle', 'SynaBun');
128
137
  const st = createTabState(sessionId, brain || session?.brain || saved?.brain || readStoredBrain(storage), {
129
- title: tt('assistant.defaultTitle', 'SynaBun'),
138
+ title: defaultTitle,
130
139
  // The agents tray starts collapsed; it only expands on request.
131
140
  dockOpen: saved ? saved.dockOpen : storage.getItem(KEYS.ASSISTANT_DOCK_OPEN) === '1',
132
141
  dockShowAll: saved ? saved.dockShowAll : storage.getItem(KEYS.ASSISTANT_DOCK_SHOW_ALL) === '1',
@@ -135,6 +144,7 @@ export function mountAssistant(viewport, { sessionId, brain, host = {}, session
135
144
  providerSessionId: session?.providerSessionId || null,
136
145
  routeMode: saved?.routeMode ?? null,
137
146
  computerUse: saved?.computerUse ?? null,
147
+ testDensity: saved?.testDensity ?? null,
138
148
  usageView: saved?.usageView || null,
139
149
  });
140
150
  Object.assign(st, {
@@ -144,7 +154,8 @@ export function mountAssistant(viewport, { sessionId, brain, host = {}, session
144
154
  engine: '',
145
155
  sdkVersion: '',
146
156
  routing: null, // server view { mode, effectiveMode, defaultMode, askBelow }
147
- features: { routing: null, computer: null }, // null = unknown yet
157
+ features: { routing: null, computer: null, testDensity: null, rename: null }, // null = unknown yet
158
+ testDensityEffective: null, // server view: the level in effect
148
159
  brainCapabilities: null,
149
160
  computerEffective: st.computerUse === true,
150
161
  desktopSupported: null,
@@ -152,8 +163,9 @@ export function mountAssistant(viewport, { sessionId, brain, host = {}, session
152
163
  desktopActive: false,
153
164
  desktopProbe: false,
154
165
  routingProbe: false,
155
- sessionTitle: String(session?.title || ''), // the server's name (first prompt): history + notifications, never the tab
166
+ sessionTitle: String(session?.title || ''), // the server's name (the user's rename, else the first prompt): history + notifications
156
167
  });
168
+ let recordName = userSessionTitle(session); // the name the session record holds as the user's ('' = never renamed)
157
169
  const controls = new Map(); // requestId → { card, origin, kind }
158
170
  // Cards answered here (requestId → card): one the server closed before the answer
159
171
  // arrived (control_cancelled after it) says so instead of keeping "Allowed".
@@ -253,6 +265,45 @@ export function mountAssistant(viewport, { sessionId, brain, host = {}, session
253
265
  try { host.setLabel?.(st.title); } catch { /* ignore */ }
254
266
  }
255
267
 
268
+ /** What the session record says about its name (a session view: the rename's answer, a broadcast, an attach). */
269
+ function applyRecordName(meta) {
270
+ if (!meta || typeof meta !== 'object' || !meta.title || (meta.id && meta.id !== sessionId)) return;
271
+ st.sessionTitle = String(meta.title);
272
+ recordName = userSessionTitle(meta);
273
+ }
274
+
275
+ /**
276
+ * A rename names the session, not this tab: it goes to the session record, which the Sessions
277
+ * menu and every other list read, through the page's one writer (`renames`). A host also calls
278
+ * setTitle when it mounts a renamed tab (`restored`): the name the record already holds is not
279
+ * written again, one it lacks (a rename made before the record kept names) is, and it never
280
+ * replaces a name on its way. "SynaBun" is no name: it is written only to take a rename back.
281
+ * `from` is the name the tab had before: a write that fails puts it back (renameFailed).
282
+ */
283
+ function saveRename(from, restored = false) {
284
+ if (destroyed || (restored && renames.pending(sessionId))) return;
285
+ renames.save(sessionId, st.title, {
286
+ from, recordName, recordTitle: st.sessionTitle, keepsRenames: st.features.rename === true, defaultTitle,
287
+ onSaved: applyRecordName,
288
+ onFailed: renameFailed,
289
+ });
290
+ }
291
+
292
+ /**
293
+ * A rename that did not reach the record: the tab goes back to the name the record answers to,
294
+ * in the host and in the label it saved, and the transcript says so. A name with nothing to go
295
+ * back to (a restored one, or the name the tab already shows) stays: the next mount sends it again.
296
+ */
297
+ function renameFailed({ name, back }) {
298
+ const previous = back || defaultTitle;
299
+ if (destroyed || previous === st.title) return;
300
+ st.userRenamed = previous !== defaultTitle;
301
+ st.title = previous;
302
+ try { host.setName?.(previous, st.userRenamed); } catch { /* ignore */ }
303
+ persist();
304
+ renderer.appendStatus(tt('assistant.status.renameFailed', 'Could not save the name “{name}”. The session is still called “{previous}”.', { name, previous }), 'warn');
305
+ }
306
+
256
307
  /** Notifications say which conversation: the rename, else the session's first-prompt title. */
257
308
  function notifyLabel() {
258
309
  return st.userRenamed ? st.title : (st.sessionTitle || st.title);
@@ -317,11 +368,11 @@ export function mountAssistant(viewport, { sessionId, brain, host = {}, session
317
368
  }
318
369
 
319
370
  function renderCost() {
320
- // The server's budget view (brain + agents, the caps) when it sends one.
321
- const budget = st.budget && Number.isFinite(Number(st.budget.totalUsd)) ? st.budget : null;
322
- const total = budget ? Number(budget.totalUsd) : totalCost();
323
- costEl.textContent = fmtCost(total);
324
- costEl.hidden = !(total > 0);
371
+ // The confirmed SessionBudget includes both brain and worker spend.
372
+ const budget = st.budget && Number.isFinite(Number(st.budget.spentUsd)) ? st.budget : null;
373
+ const total = budget ? Number(budget.spentUsd) : totalCost();
374
+ costEl.textContent = budget ? budgetSpendText(budget, { t }) : fmtCost(total);
375
+ costEl.hidden = !budget && !(total > 0);
325
376
  const tone = budgetTone(budget);
326
377
  if (tone && tone !== 'ok') costEl.dataset.tone = tone;
327
378
  else delete costEl.dataset.tone;
@@ -331,6 +382,7 @@ export function mountAssistant(viewport, { sessionId, brain, host = {}, session
331
382
  ? tt('assistant.topbar.costSplit', 'Session {session} · agents {agents}', { session: fmtCost(st.costUsd || 0), agents: fmtCost(st.dispatchUsd) })
332
383
  : tt('assistant.topbar.cost', 'Session cost');
333
384
  costEl.setAttribute('data-tooltip', tip);
385
+ costEl.setAttribute('aria-label', `${tt('assistant.budget.title', 'Budget')}: ${tip}`);
334
386
  }
335
387
 
336
388
  function brainModelLabel() {
@@ -378,7 +430,27 @@ export function mountAssistant(viewport, { sessionId, brain, host = {}, session
378
430
  return st.features.routing === true;
379
431
  }
380
432
 
433
+ // ── File links ──
434
+ // A link to a file the code editor can open opens it there (shared with the other panels): one listener
435
+ // for the transcript, whatever drew the message. A relative path is a path in the brain's project.
436
+ wireFileLinks(transcriptEl, {
437
+ cwd: () => st.brain?.project || '',
438
+ textLinks: 'a.file-link',
439
+ // A path the editor does not open keeps what a click on it did: it is copied.
440
+ copyPath: (link) => {
441
+ clipCopy((link.textContent || '').replace(/:\d+$/, ''));
442
+ link.style.opacity = '0.5';
443
+ setTimeout(() => { link.style.opacity = ''; }, 300);
444
+ },
445
+ });
446
+
381
447
  // ── Renderer ──
448
+ let emptyBudgetChip = null;
449
+ const budgetController = createBudgetController({ t, sessionId, onSaved: (data) => {
450
+ if (destroyed) return;
451
+ if (data?.session) st.budget = data.session;
452
+ renderCost();
453
+ } });
382
454
  const renderer = createRenderer(transcriptEl, {
383
455
  t,
384
456
  providerIcon,
@@ -389,10 +461,28 @@ export function mountAssistant(viewport, { sessionId, brain, host = {}, session
389
461
  onReadRun: (runId) => readRun(runId),
390
462
  onEscalateRun: (runId, target) => escalateRun(runId, target),
391
463
  onMemoryClick: (id) => { if (id) emit('memory:open', { id }); },
464
+ // The greeting this conversation opens with (lib/greeting/ decides whether there is one): its id
465
+ // goes back with the first prompt, so the brain is told once what was said.
466
+ greeting: (slot) => (guestBlocked() ? null : mountGreeting(slot, {
467
+ surface: 'assistant', session: sessionId, cwd: st.brain?.project || null, t,
468
+ onOpenMemory: (id) => { if (id) emit('memory:open', { id }); },
469
+ }).then((greeting) => { greetingId = greeting?.id || null; return greeting; })),
392
470
  brainLabel: () => brainModelLabel(),
393
471
  onChangeRoutes: () => openRoutes(),
394
472
  onRouteEvent: (ev) => handleRouteCardEvent(ev),
395
473
  emptySuggestions: () => emptySuggestions(),
474
+ // "Choose a project" in the empty state is the toolbar's Working project field (asst-brain-picker.js).
475
+ mountEmptyProject: (slot) => picker.mountEmptyProject(slot),
476
+ mountEmptyBudget: (slot) => {
477
+ emptyBudgetChip?.destroy();
478
+ emptyBudgetChip = mountBudgetChip(slot, budgetController, { t, mascot: () => director });
479
+ return emptyBudgetChip.el;
480
+ },
481
+ unmountEmptyControls: () => {
482
+ picker.mountEmptyProject(null);
483
+ emptyBudgetChip?.destroy();
484
+ emptyBudgetChip = null;
485
+ },
396
486
  onSuggestion: (template) => composer.fillTemplate(template),
397
487
  heroOrigin: () => { try { return host.takeHeroOrigin?.() || null; } catch { return null; } },
398
488
  onMascotChange: () => director?.retarget(),
@@ -424,7 +514,7 @@ export function mountAssistant(viewport, { sessionId, brain, host = {}, session
424
514
  });
425
515
 
426
516
  function emptySuggestions() {
427
- const project = st.brain.project ? basename(st.brain.project) : tt('assistant.empty.thisProject', 'this project');
517
+ const project = projectLabel(st.brain.project, cachedProjects()) || tt('assistant.empty.thisProject', 'this project');
428
518
  const list = [];
429
519
  if (computerAvailable()) list.push({ id: 'computer', label: tt('assistant.empty.suggestComputer', 'Use my Mac to…'), template: tt('assistant.empty.templateComputer', 'Use my Mac to {caret}') });
430
520
  list.push({ id: 'web', label: tt('assistant.empty.suggestWeb', 'Look up … on the web'), template: tt('assistant.empty.templateWeb', 'Look up {caret} on the web') });
@@ -455,8 +545,6 @@ export function mountAssistant(viewport, { sessionId, brain, host = {}, session
455
545
  director = createMascotDirector({
456
546
  getTarget: () => renderer.mascot(),
457
547
  caret: () => composer.caretPoint(),
458
- setEnabled: (on) => renderer.setRigsEnabled(on),
459
- isVisible: () => visible(),
460
548
  env: {
461
549
  input: (fn) => { const offPointer = onPointer(fn); const offKey = onKey(fn); return () => { offPointer(); offKey(); }; },
462
550
  lastInputAt,
@@ -481,7 +569,9 @@ export function mountAssistant(viewport, { sessionId, brain, host = {}, session
481
569
  // Model · effort · permission mode · account sit in the composer row next to attach; project and MCP in the toolbar.
482
570
  const picker = createBrainPicker({ brain: composer.el.querySelector('.asst-composer-brain'), context: barGroups.context }, {
483
571
  t,
572
+ mascot: () => director,
484
573
  getBrain: () => st.brain,
574
+ getRunning: () => st.running,
485
575
  onChange: (next, meta) => applyBrain(next, meta),
486
576
  onAddAccount: (provider) => openAccountsManager({ provider, ...accountHooks() }).openAdd(),
487
577
  onManageAccounts: (provider) => openAccountsManager({ provider, ...accountHooks() }),
@@ -490,6 +580,11 @@ export function mountAssistant(viewport, { sessionId, brain, host = {}, session
490
580
  getMenuAnchor: () => moreBtn,
491
581
  // Composer fields open upward, toolbar fields downward.
492
582
  menuPlacement: 'auto',
583
+ // Test density sits right after the permission mode; hidden on a server that does not know it.
584
+ getTestDensity: () => testDensityView(),
585
+ onTestDensity: (level) => setTestDensity(level),
586
+ // The registered projects arrived: their labels name the project in the fields and the "Fix … in" suggestion.
587
+ onProjects: () => { picker.render(); renderer.refreshEmpty(); },
493
588
  });
494
589
 
495
590
  const computerToggle = createComputerToggle(barGroups.computer, {
@@ -552,7 +647,10 @@ export function mountAssistant(viewport, { sessionId, brain, host = {}, session
552
647
  function fetchUsage() {
553
648
  const seq = ++usageFetchSeq;
554
649
  getAssistantSessionUsage(sessionId).then((data) => {
555
- if (!destroyed && seq === usageFetchSeq && data?.ok && data.usage) applyUsage(data.usage);
650
+ if (!destroyed && seq === usageFetchSeq && data?.ok) {
651
+ if (data.budget) budgetController.receiveSession(data.budget);
652
+ if (data.usage) applyUsage(data.usage);
653
+ }
556
654
  }).catch(() => { /* older servers do not expose usage */ });
557
655
  }
558
656
  function applyUsage(packet) {
@@ -740,6 +838,9 @@ export function mountAssistant(viewport, { sessionId, brain, host = {}, session
740
838
  case 'assistant:mailbox':
741
839
  renderer.appendMailbox(packet.items || packet.events || packet.mailbox || []);
742
840
  return;
841
+ case 'assistant:budget':
842
+ budgetController.receiveSession(packet.budget);
843
+ return;
743
844
  case 'assistant:cost':
744
845
  applyCost(packet);
745
846
  return;
@@ -812,7 +913,7 @@ export function mountAssistant(viewport, { sessionId, brain, host = {}, session
812
913
  if (session != null) st.costUsd = session;
813
914
  else if (!packet?.runId && num(packet?.costUsd) != null) st.costUsd = num(packet.costUsd);
814
915
  if (num(packet?.dispatchUsd) != null) st.dispatchUsd = num(packet.dispatchUsd);
815
- if (packet?.budget && typeof packet.budget === 'object') st.budget = packet.budget;
916
+ if (packet?.budget && typeof packet.budget === 'object') budgetController.receiveSession(packet.budget);
816
917
  if (typeof packet?.brainMetered === 'boolean') st.brainMetered = packet.brainMetered;
817
918
  if (packet?.runId && num(packet.costUsd) != null) dock.patchRun(packet.runId, { costUsd: num(packet.costUsd) });
818
919
  if (session != null) st.serverCost = true;
@@ -823,8 +924,8 @@ export function mountAssistant(viewport, { sessionId, brain, host = {}, session
823
924
  function applySessionMeta(meta) {
824
925
  if (!meta || typeof meta !== 'object') return;
825
926
  if (meta.id && meta.id !== sessionId) return;
826
- // The server's title names the conversation (history, notifications), never the tab.
827
- if (meta.title) st.sessionTitle = String(meta.title);
927
+ // The server's title names the conversation (history, notifications); the tab's name is the host's.
928
+ applyRecordName(meta);
828
929
  if (meta.brain && typeof meta.brain === 'object') {
829
930
  const next = normalizeBrain({ ...st.brain, ...meta.brain, provider: meta.brain.provider || meta.brain.brain || st.brain.provider });
830
931
  if (JSON.stringify(next) !== JSON.stringify(st.brain)) { st.brain = next; picker.render(); host.setBrain?.(next); persist(); }
@@ -832,7 +933,7 @@ export function mountAssistant(viewport, { sessionId, brain, host = {}, session
832
933
  const next = normalizeBrain({ ...st.brain, provider: meta.brain, model: meta.model ?? st.brain.model, effort: meta.effort ?? st.brain.effort, accountId: meta.accountId ?? st.brain.accountId, mcpProfile: meta.mcpProfile ?? st.brain.mcpProfile, project: meta.cwd ?? st.brain.project });
833
934
  if (JSON.stringify(next) !== JSON.stringify(st.brain)) { st.brain = next; picker.render(); host.setBrain?.(next); persist(); }
834
935
  }
835
- if (meta.budget && typeof meta.budget === 'object') st.budget = meta.budget;
936
+ if (meta.budget && typeof meta.budget === 'object') budgetController.receiveSession(meta.budget);
836
937
  if (Number.isFinite(Number(meta.costUsd))) { st.costUsd = Number(meta.costUsd); renderCost(); }
837
938
  if (meta.providerSessionId) st.providerSessionId = meta.providerSessionId;
838
939
  if (meta.routing && typeof meta.routing === 'object') {
@@ -847,7 +948,11 @@ export function mountAssistant(viewport, { sessionId, brain, host = {}, session
847
948
  if (meta.features && typeof meta.features === 'object') {
848
949
  if (typeof meta.features.routing === 'boolean') st.features.routing = meta.features.routing;
849
950
  if (typeof meta.features.computer === 'boolean') st.features.computer = meta.features.computer;
951
+ if (typeof meta.features.testDensity === 'boolean') st.features.testDensity = meta.features.testDensity;
952
+ if (typeof meta.features.rename === 'boolean') st.features.rename = meta.features.rename;
850
953
  }
954
+ if ('testDensityExplicit' in meta) st.testDensity = normalizeTestDensity(meta.testDensityExplicit, null);
955
+ if (meta.testDensity) st.testDensityEffective = normalizeTestDensity(meta.testDensity);
851
956
  if (typeof meta.computerUse === 'boolean') st.computerEffective = meta.computerUse;
852
957
  if ('computerUseExplicit' in meta) st.computerUse = normalizeComputerMode(meta.computerUseExplicit, null);
853
958
  // A WhatsApp conversation: what is in effect and why (not a per-conversation switch). null on the desktop.
@@ -872,6 +977,7 @@ export function mountAssistant(viewport, { sessionId, brain, host = {}, session
872
977
  if (destroyed) return;
873
978
  routeChip.setVisible(routingAvailable());
874
979
  routeChip.render();
980
+ picker.render();
875
981
  const showComputer = st.features.computer === true || st.desktopSupported === true;
876
982
  computerToggle.set({
877
983
  visible: showComputer,
@@ -920,7 +1026,7 @@ export function mountAssistant(viewport, { sessionId, brain, host = {}, session
920
1026
 
921
1027
  function routeModeView() {
922
1028
  const r = st.routing || {};
923
- return { mode: normalizeRouteMode(st.routeMode, null) || r.mode || null, effectiveMode: r.effectiveMode, defaultMode: r.defaultMode };
1029
+ return { mode: normalizeRouteMode(st.routeMode, null) || r.mode || null, effectiveMode: r.effectiveMode, defaultMode: r.defaultMode, askBelow: r.askBelow };
924
1030
  }
925
1031
 
926
1032
  function setRouteMode(mode, { quiet = false } = {}) {
@@ -943,6 +1049,31 @@ export function mountAssistant(viewport, { sessionId, brain, host = {}, session
943
1049
  if (last && last !== st.routing.effectiveMode) setRouteMode(last, { quiet: true });
944
1050
  }
945
1051
 
1052
+ /** The level the composer shows: this session's, else the server's; null while the server does not know the setting. */
1053
+ function testDensityView() {
1054
+ if (st.features.testDensity !== true) return null;
1055
+ return normalizeTestDensity(st.testDensity || st.testDensityEffective);
1056
+ }
1057
+
1058
+ function setTestDensity(level, { quiet = false } = {}) {
1059
+ const next = normalizeTestDensity(level);
1060
+ st.testDensity = next;
1061
+ st.testDensityEffective = next;
1062
+ picker.render();
1063
+ persist();
1064
+ sock.send({ type: 'set_test_density', level: next });
1065
+ try { storage.setItem(KEYS.ASSISTANT_TEST_DENSITY, next); } catch { /* storage unavailable */ }
1066
+ if (!quiet) renderer.appendStatus(tt('assistant.status.testDensity', 'Test density: {level}', { level: testDensityLabel(next, t) }));
1067
+ }
1068
+
1069
+ /** Fresh tabs inherit the last-used test density (like the route mode). */
1070
+ function applyLastUsedTestDensity() {
1071
+ if (saved || st.features.testDensity !== true || st.testDensity != null) return;
1072
+ let last = null;
1073
+ try { last = normalizeTestDensity(storage.getItem(KEYS.ASSISTANT_TEST_DENSITY), null); } catch { last = null; }
1074
+ if (last && last !== testDensityView()) setTestDensity(last, { quiet: true });
1075
+ }
1076
+
946
1077
  /** A WhatsApp conversation's Computer switch says why it is what it is (the status line of the transcript). */
947
1078
  function explainRemoteComputer() {
948
1079
  const view = computerRemoteView(st.computerRemote, t);
@@ -1170,6 +1301,10 @@ export function mountAssistant(viewport, { sessionId, brain, host = {}, session
1170
1301
  request_id: `q-${Date.now().toString(36)}-${(querySeq += 1)}`,
1171
1302
  };
1172
1303
  if (payload.images?.length) msg.images = payload.images.map(i => ({ base64: i.base64, mediaType: i.mediaType }));
1304
+ // The typed words apart from long pastes (asst-composer.js): what the server may learn about the user from.
1305
+ if (typeof payload.typed === 'string') msg.typed = payload.typed;
1306
+ // The greeting shown when this conversation opened: echoed once, with the first prompt.
1307
+ if (greetingId) { msg.greetingId = greetingId; greetingId = null; forgetGreeting('assistant', sessionId); }
1173
1308
  const row = renderer.appendUser(payload.display ?? payload.text, { images: payload.images, files: [...(payload.files || []), ...(payload.uploads || [])], memories: payload.memories });
1174
1309
  director.sent();
1175
1310
  sentQueries.set(msg.request_id, { payload, row });
@@ -1178,7 +1313,7 @@ export function mountAssistant(viewport, { sessionId, brain, host = {}, session
1178
1313
  setRunning(true);
1179
1314
  sock.send(msg);
1180
1315
  // Name the conversation like the server will (its first prompt) — not the tab.
1181
- if (!st.sessionTitle || /^(?:SynaBun|Assistant(?: \d+)?)$/.test(st.sessionTitle)) {
1316
+ if (!st.userRenamed && (!st.sessionTitle || /^(?:SynaBun|Assistant(?: \d+)?)$/.test(st.sessionTitle))) {
1182
1317
  const guess = String(payload.text || '').split('\n')[0].trim().slice(0, 60);
1183
1318
  if (guess) st.sessionTitle = guess;
1184
1319
  }
@@ -1222,7 +1357,7 @@ export function mountAssistant(viewport, { sessionId, brain, host = {}, session
1222
1357
  return true;
1223
1358
  case 'resume':
1224
1359
  if (parsed.sessionId) resume(parsed.sessionId);
1225
- else sock.send({ type: 'list_sessions' });
1360
+ else renames.settled().then(() => { if (!destroyed) sock.send({ type: 'list_sessions' }); }); // a rename on its way is in the list
1226
1361
  return true;
1227
1362
  case 'stop':
1228
1363
  if (parsed.all) await stopAll();
@@ -1357,23 +1492,24 @@ export function mountAssistant(viewport, { sessionId, brain, host = {}, session
1357
1492
  picker.render();
1358
1493
  host.setBrain?.(st.brain);
1359
1494
  persist();
1495
+ if (before.project !== st.brain.project) renderer.refreshEmpty();
1360
1496
  if (meta.silent) return;
1361
1497
  if (meta.requiresSwitch) {
1362
1498
  const b = st.brain;
1363
- sock.send({ type: 'switch_brain', brain: b.provider, model: b.model || undefined, effort: b.effort || undefined, accountId: b.accountId || undefined, mcpProfile: b.mcpProfile || undefined, cwd: b.project || undefined, agent: b.agent || undefined, permissionMode: b.permissionMode, planMode: b.planMode === true });
1499
+ if (!meta.restartBrain) sock.send({ type: 'switch_brain', brain: b.provider, model: b.model || undefined, effort: b.effort || undefined, accountId: b.accountId || undefined, mcpProfile: b.mcpProfile || undefined, cwd: b.project || undefined, agent: b.agent || undefined, permissionMode: b.permissionMode, planMode: b.planMode === true });
1364
1500
  renderer.appendDivider(`${tt('assistant.status.brain', 'brain')} → ${brainLabel(b)}`);
1365
1501
  renderer.forgetSeen(); // the new brain's ids start over
1366
1502
  } else if (before.permissionMode !== st.brain.permissionMode || !!before.planMode !== !!st.brain.planMode) {
1367
1503
  sock.send({ type: 'set_permission_mode', mode: st.brain.permissionMode, planMode: st.brain.planMode === true });
1368
1504
  }
1369
- if (before.project !== st.brain.project) renderer.refreshEmpty();
1370
- patchAssistantSession(sessionId, { brain: st.brain }).catch((err) => {
1505
+ patchAssistantSession(sessionId, { brain: st.brain, ...(meta.restartBrain ? { restartBrain: true } : {}) }).catch((err) => {
1371
1506
  // Refused (a model disabled in Assistant → Models): undo, so the refused
1372
1507
  // brain is neither shown nor saved as the last used one — the next start
1373
1508
  // would ask for it again. A brain switch already got the error on the socket.
1374
1509
  if (err?.code !== 'MODEL_DISABLED' || destroyed || st.brain !== applied) return;
1375
1510
  st.brain = before;
1376
1511
  picker.render();
1512
+ if (applied.project !== before.project) renderer.refreshEmpty();
1377
1513
  host.setBrain?.(before);
1378
1514
  persist();
1379
1515
  if (!meta.requiresSwitch) renderer.appendError(err.message || String(err));
@@ -1384,7 +1520,8 @@ export function mountAssistant(viewport, { sessionId, brain, host = {}, session
1384
1520
  async function focusRun(runId) {
1385
1521
  if (!runId) return;
1386
1522
  focusAssistantRun(runId).catch(() => {});
1387
- try { await focusNativeLoopRun(runId); } catch { /* router not ready */ }
1523
+ // The user's own Focus: a run whose tab was closed opens again.
1524
+ try { await focusNativeLoopRun(runId, { reopen: true }); } catch { /* router not ready */ }
1388
1525
  }
1389
1526
 
1390
1527
  async function stopRun(runId) {
@@ -1544,7 +1681,7 @@ export function mountAssistant(viewport, { sessionId, brain, host = {}, session
1544
1681
  items.push({ id: 'compact', label: tt('assistant.topbar.compact', 'Compact context'), onSelect: () => compact() });
1545
1682
  if (routingAvailable()) items.push({ id: 'routes', label: tt('assistant.routes.editor', 'Model routes…'), icon: ICON_ROUTE, onSelect: () => openRoutes() });
1546
1683
  items.push({ id: 'models', label: tt('assistant.models.menu', 'Models…'), onSelect: () => openModels() });
1547
- items.push({ id: 'budget', label: tt('assistant.budget.menu', 'Budget…'), desc: st.budget ? `${fmtCost(st.budget.totalUsd) || '$0.00'} / ${fmtCost(st.budget.hardUsd)}` : '', onSelect: () => openBudget() });
1684
+ items.push({ id: 'budget', label: tt('assistant.budget.menu', 'Budget…'), desc: budgetSpendText(st.budget, { t }), onSelect: () => openBudget() });
1548
1685
  items.push({ id: 'clear', label: tt('assistant.topbar.clear', 'Clear transcript'), onSelect: () => clearTranscript() });
1549
1686
  items.push({ id: 'help', label: tt('assistant.topbar.helpShortcuts', 'Help & shortcuts'), onSelect: () => openHelp() });
1550
1687
  items.push({ kind: 'separator' });
@@ -1598,12 +1735,12 @@ export function mountAssistant(viewport, { sessionId, brain, host = {}, session
1598
1735
  }
1599
1736
  }
1600
1737
 
1601
- // Assistant → Budget: the money caps (applied live by the server).
1738
+ // Assistant → Budget: one saved amount plus the current session's live split.
1602
1739
  function openBudget() {
1603
1740
  openBudgetEditor({
1604
1741
  t,
1605
1742
  sessionId,
1606
- brainMetered: st.brainMetered !== false && st.brain?.provider !== 'codex',
1743
+ controller: budgetController,
1607
1744
  onToast: toast,
1608
1745
  onSaved: (res) => { if (res?.session) { st.budget = res.session; renderCost(); } },
1609
1746
  }).catch(() => {});
@@ -1615,6 +1752,7 @@ export function mountAssistant(viewport, { sessionId, brain, host = {}, session
1615
1752
  providerIcon,
1616
1753
  getBrain: () => st.brain,
1617
1754
  brainName: () => brainModelLabel(),
1755
+ getRouting: () => st.routing,
1618
1756
  onToast: toast,
1619
1757
  onSaved: (routing) => {
1620
1758
  if (routing?.defaultMode) {
@@ -1667,7 +1805,8 @@ export function mountAssistant(viewport, { sessionId, brain, host = {}, session
1667
1805
  e.stopPropagation();
1668
1806
  if (isMenuOpen(sessionsBtn)) { closeMenu(); return; }
1669
1807
  let list = [];
1670
- try { list = await listAssistantSessions(); } catch { /* server not ready */ }
1808
+ // A rename still on its way to a record is in the list this opens: this tab's, another tab's, a closed one's.
1809
+ try { await renames.settled(); list = await listAssistantSessions(); } catch { /* server not ready */ }
1671
1810
  if (!destroyed) showSessionsMenu(list);
1672
1811
  });
1673
1812
  function toggleDock() {
@@ -1710,6 +1849,8 @@ export function mountAssistant(viewport, { sessionId, brain, host = {}, session
1710
1849
 
1711
1850
  // ── Bus ──
1712
1851
  unsubs.push(on('sync:assistant:session-updated', (msg) => { const s = msg?.session || msg; if (s?.id === sessionId) applySessionMeta(s); }));
1852
+ unsubs.push(on('sync:assistant:budget', (msg) => { if ((msg?.assistantSessionId || msg?.sessionId) === sessionId) budgetController.receiveSession(msg.budget); }));
1853
+ unsubs.push(on('sync:assistant:budget-changed', () => budgetController.load()));
1713
1854
  unsubs.push(on('sync:assistant:cost', (msg) => { if (!msg?.assistantSessionId || msg.assistantSessionId === sessionId) applyCost(msg); }));
1714
1855
  unsubs.push(on('sync:assistant:mailbox', (msg) => { if (msg?.assistantSessionId === sessionId) renderer.appendMailbox(msg.items || msg.events || []); }));
1715
1856
  unsubs.push(on('sync:assistant:permission-request', (msg) => {
@@ -1755,6 +1896,8 @@ export function mountAssistant(viewport, { sessionId, brain, host = {}, session
1755
1896
  if (force) renderer.clear();
1756
1897
  if (Array.isArray(transcript) && transcript.length) renderer.renderTranscript(transcript);
1757
1898
  renderer.showEmpty(); // no-op once the transcript has rows
1899
+ // A conversation with nothing in it yet may be greeted; a restored one never is.
1900
+ if (Array.isArray(transcript) && !transcript.length && !(Number(meta?.turns) > 0)) renderer.offerGreeting();
1758
1901
  for (const run of (Array.isArray(data?.runs) ? data.runs : [])) dock.upsertRun(run);
1759
1902
  } catch (err) {
1760
1903
  console.warn('[assistant] could not restore the conversation', err);
@@ -1774,9 +1917,11 @@ export function mountAssistant(viewport, { sessionId, brain, host = {}, session
1774
1917
  if (destroyed) return;
1775
1918
  probeFeatures();
1776
1919
  applyLastUsedRouteMode();
1920
+ applyLastUsedTestDensity();
1777
1921
  sock.connect();
1778
1922
  });
1779
1923
  fetchUsage();
1924
+ budgetController.load();
1780
1925
 
1781
1926
  const controller = {
1782
1927
  sessionId,
@@ -1784,7 +1929,15 @@ export function mountAssistant(viewport, { sessionId, brain, host = {}, session
1784
1929
  getBrain: () => st.brain,
1785
1930
  setBrain: (b, meta) => applyBrain(b, meta || { requiresSwitch: false, field: 'external' }),
1786
1931
  getTitle: () => st.title,
1787
- setTitle: (text) => { st.userRenamed = true; setTitle(text); },
1932
+ /** A rename (the host's tab, float header or ✎), or with `restored` the name a host puts back when it mounts a renamed tab. */
1933
+ setTitle: (text, { restored = false } = {}) => {
1934
+ const next = String(text || '').trim();
1935
+ if (!next) return;
1936
+ const from = restored ? next : st.title; // a restored name has none to go back to
1937
+ st.userRenamed = next !== defaultTitle; // the default name is no rename
1938
+ setTitle(next);
1939
+ saveRename(from, restored);
1940
+ },
1788
1941
  isRunning: () => st.running,
1789
1942
  isConnected: () => st.connected,
1790
1943
  send: (text, extra = {}) => submit({ text: String(text ?? ''), images: extra.images || [], files: extra.files || [], uploads: extra.uploads || [], memories: extra.memories || [] }),
@@ -1799,12 +1952,12 @@ export function mountAssistant(viewport, { sessionId, brain, host = {}, session
1799
1952
  insertText: (text) => composer.insertText(text),
1800
1953
  /** A file path from the explorer ("send to panel") → the composer, as inline code. */
1801
1954
  attachPath: (path) => { if (path) { composer.insertText(`\`${path}\` `); composer.focus(); } },
1802
- /** The server's name for the conversation (first prompt) — tooltips, never the tab label. */
1955
+ /** The server's name for the conversation (the user's rename, else the first prompt): tooltips. */
1803
1956
  getConversationTitle: () => st.sessionTitle || '',
1804
1957
  getStatus: () => st.status || 'idle',
1805
1958
  focus: () => composer.focus(),
1806
- onShown: () => { syncTabLabel(); renderer.scrollEnd(true); if (visible()) composer.focus(); director.touch(); director.visibilityChanged(); },
1807
- onHidden: () => { closeMenu(); picker.close(); director.stopTyping(); director.visibilityChanged(); },
1959
+ onShown: () => { syncTabLabel(); renderer.scrollEnd(true); if (visible()) composer.focus(); director.touch(); },
1960
+ onHidden: () => { closeMenu(); picker.close(); director.stopTyping(); },
1808
1961
  counts: () => dock.counts(),
1809
1962
  destroy: ({ closeServer = false } = {}) => {
1810
1963
  if (destroyed) return;
@@ -1812,6 +1965,9 @@ export function mountAssistant(viewport, { sessionId, brain, host = {}, session
1812
1965
  if (persistTimer) { clearTimeout(persistTimer); persistTimer = null; }
1813
1966
  clearTimeout(announceTimer);
1814
1967
  if (replay) { clearTimeout(replay.timer); replay = null; }
1968
+ closeBudgetEditor(budgetController);
1969
+ emptyBudgetChip?.destroy();
1970
+ budgetController.destroy();
1815
1971
  director.destroy();
1816
1972
  usage.destroy();
1817
1973
  clearTimeout(limitTimer);