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
@@ -0,0 +1,339 @@
1
+ // ═══════════════════════════════════════════
2
+ // SynaBun Neural Interface — File links in agent messages: the decisions
3
+ // ═══════════════════════════════════════════
4
+ //
5
+ // A link in an agent's message can point at a file on this computer: a path,
6
+ // a `file:` URL, with or without a place in the file (`:line[:column]`,
7
+ // `#Lline[Ccolumn]`). This module reads such a target and says which paths to
8
+ // try, and decides what a click on one does. ui-file-links.js gives that
9
+ // decision the page (the bus, the server, the toast). The four side panels
10
+ // share both.
11
+ //
12
+ // The text is a model's: nothing here trusts it. A target only ever becomes a
13
+ // path handed to /api/file-content, which keeps its own checks.
14
+ // No imports and no `document`: the click handler only touches the event and
15
+ // the link it is handed. Loaded by `node --test` as it is, and by the Markdown
16
+ // sanitiser.
17
+
18
+ /** The English of `fileLinks.*` in i18n/en.json (a test keeps them equal). */
19
+ export const FILE_LINK_COPY = Object.freeze({
20
+ open: 'Open in editor',
21
+ openAtLine: 'Open in editor at line {line}',
22
+ openAtLineColumn: 'Open in editor at line {line}, column {column}',
23
+ missing: '{name} was not found, or it is a folder.',
24
+ notText: '{name} is not a text file the editor can open.',
25
+ tooLarge: '{name} is too large to open in the editor.',
26
+ outside: '{name} is outside your registered projects. Add its folder as a project to open it here.',
27
+ failed: 'Could not reach SynaBun to open {name}. Check that the server is running.',
28
+ });
29
+
30
+ /** Longer than any path: a target past this is not read at all. */
31
+ export const MAX_TARGET_LENGTH = 2048;
32
+
33
+ const DRIVE_RE = /^[a-z]:[\\/]/i;
34
+ const SCHEME_RE = /^([a-z][a-z0-9+.-]*):/i;
35
+ // `name.ext:12` and `name.ext:12:3` read as a scheme to a URL parser; they are a file and a place in it.
36
+ const PLACE_ONLY_RE = /^\d+(?::\d+)?(?:#.*)?$/;
37
+ const LINE_SUFFIX_RE = /:(\d+)(?::(\d+))?$/;
38
+ const LINE_HASH_RE = /^#L(\d+)(?:C(\d+))?(?:-L?\d+(?:C\d+)?)?$/i;
39
+
40
+ const slashes = (path) => String(path ?? '').replace(/\\/g, '/');
41
+
42
+ /** How a target reads: a `path`, a `file` URL, a `web` address (http, https), or null for anything else. */
43
+ function kindOf(value) {
44
+ if (!value || value.length > MAX_TARGET_LENGTH) return null;
45
+ if (value.startsWith('#') || value.startsWith('?') || value.startsWith('//')) return null;
46
+ if (DRIVE_RE.test(value)) return 'path';
47
+ const scheme = SCHEME_RE.exec(value);
48
+ if (!scheme) return 'path';
49
+ const name = scheme[1].toLowerCase();
50
+ if (name === 'file') return 'file';
51
+ if (name === 'http' || name === 'https') return 'web';
52
+ const rest = value.slice(scheme[0].length);
53
+ if (rest.startsWith('//')) return null;
54
+ return PLACE_ONLY_RE.test(rest) ? 'path' : null;
55
+ }
56
+
57
+ /**
58
+ * A link target as written, when it reads as a file on this computer rather than an address.
59
+ * The Markdown sanitiser keeps it beside the link (`data-file-target`): the `href` it writes is resolved
60
+ * against the page, which loses a relative path, `~`, a `file:` URL and `name.ext:12`.
61
+ * @param {string} raw the `href` an agent wrote
62
+ * @returns {string | null}
63
+ */
64
+ export function localFileHref(raw) {
65
+ const value = String(raw ?? '').trim();
66
+ const kind = kindOf(value);
67
+ return kind === 'path' || kind === 'file' ? value : null;
68
+ }
69
+
70
+ /** A path with `.`, `..` and doubled slashes resolved, and no slash at its end. */
71
+ export function normalizeFilePath(path) {
72
+ const value = slashes(path);
73
+ const drive = /^[a-z]:/i.exec(value)?.[0] || '';
74
+ const rest = value.slice(drive.length);
75
+ const rooted = rest.startsWith('/');
76
+ const parts = [];
77
+ for (const part of rest.split('/')) {
78
+ if (!part || part === '.') continue;
79
+ if (part === '..') { if (parts.length && parts[parts.length - 1] !== '..') parts.pop(); else if (!rooted) parts.push('..'); continue; }
80
+ parts.push(part);
81
+ }
82
+ return `${drive}${rooted ? '/' : ''}${parts.join('/')}`;
83
+ }
84
+
85
+ const isAbsolute = (path) => path.startsWith('/') || DRIVE_RE.test(path);
86
+
87
+ function decoded(text) {
88
+ try { return decodeURIComponent(text); } catch { return text; }
89
+ }
90
+
91
+ /**
92
+ * Read a link target.
93
+ * @param {string} raw the target as written (an `href`, or the text of a path a panel made clickable)
94
+ * @param {{ cwd?: string, home?: string, origin?: string }} [where] `cwd`: the working directory of the tab or
95
+ * session the message belongs to (a relative path is resolved against it); `home`: the user's home folder
96
+ * (for `~`); `origin`: the page's own origin (an address on it is read as the path it holds, which is how a
97
+ * link sanitised before `data-file-target` existed looks)
98
+ * @returns {{ path: string, candidates: string[], line: number | null, column: number | null,
99
+ * needsHome: boolean, sameOrigin: boolean } | null} null when the target is not a file's. `candidates` are
100
+ * the paths to try, in order; `path` is the first. `needsHome`: the target starts with `~` and no home was
101
+ * given, so there is nothing to try yet. `sameOrigin`: the target was an address of this app.
102
+ */
103
+ export function parseFileLinkTarget(raw, { cwd = '', home = '', origin = '' } = {}) {
104
+ const value = String(raw ?? '').trim();
105
+ const kind = kindOf(value);
106
+ if (!kind) return null;
107
+
108
+ let written;
109
+ let hash = '';
110
+ let sameOrigin = false;
111
+ if (kind === 'path') {
112
+ const at = value.indexOf('#');
113
+ written = at < 0 ? value : value.slice(0, at);
114
+ hash = at < 0 ? '' : value.slice(at);
115
+ } else {
116
+ let url;
117
+ try { url = new URL(value); } catch { return null; }
118
+ if (kind === 'web') {
119
+ if (!origin || url.origin !== origin) return null;
120
+ sameOrigin = true;
121
+ } else if (url.host && url.host !== 'localhost') {
122
+ return null; // a file on another machine
123
+ }
124
+ written = url.pathname;
125
+ hash = url.hash;
126
+ }
127
+
128
+ // An agent may have escaped the path (a link with a space in it has to be); it may also hold a real "%".
129
+ const spellings = kind === 'path' ? [...new Set([decoded(written), written])] : [decoded(written)];
130
+ // A working directory that is not a whole path would be read from the server's own folder: it is not used.
131
+ const root = isAbsolute(slashes(cwd)) ? normalizeFilePath(cwd) : '';
132
+ const candidates = [];
133
+ let line = null;
134
+ let column = null;
135
+ let needsHome = false;
136
+ for (const spelling of spellings) {
137
+ let path = slashes(spelling);
138
+ const place = LINE_SUFFIX_RE.exec(path);
139
+ if (place) {
140
+ line ??= Number(place[1]) || null;
141
+ column ??= Number(place[2]) || null;
142
+ path = path.slice(0, place.index);
143
+ }
144
+ if (/^\/[a-z]:\//i.test(path)) path = path.slice(1); // file:///C:/…
145
+ if (path === '~' || path.startsWith('~/')) {
146
+ if (!home) { needsHome = true; continue; }
147
+ path = `${slashes(home).replace(/\/+$/, '')}${path.slice(1)}`;
148
+ }
149
+ if (!path) continue;
150
+ if (isAbsolute(path)) {
151
+ candidates.push(normalizeFilePath(path));
152
+ // The sanitiser used to turn `src/a.js` into `<origin>/src/a.js`: the same link, read from its project.
153
+ if (sameOrigin && root) candidates.push(normalizeFilePath(`${root}/${path}`));
154
+ } else if (root) {
155
+ candidates.push(normalizeFilePath(`${root}/${path}`));
156
+ }
157
+ }
158
+
159
+ const place = LINE_HASH_RE.exec(hash);
160
+ if (place) {
161
+ line = Number(place[1]) || line;
162
+ column = Number(place[2]) || (Number(place[1]) ? null : column);
163
+ }
164
+
165
+ const unique = [...new Set(candidates.filter((path) => path && path !== '/'))];
166
+ if (!unique.length && !needsHome) return null;
167
+ return { path: unique[0] || '', candidates: unique, line, column, needsHome: needsHome && !unique.length, sameOrigin };
168
+ }
169
+
170
+ /** Whether `path` is one of `roots` or inside one. */
171
+ export function isInsideRoots(path, roots = []) {
172
+ const target = normalizeFilePath(path);
173
+ if (!isAbsolute(target)) return false;
174
+ for (const root of roots) {
175
+ const base = root ? normalizeFilePath(root) : '';
176
+ if (!base || !isAbsolute(base)) continue;
177
+ if (target === base || target.startsWith(base.endsWith('/') ? base : `${base}/`)) return true;
178
+ }
179
+ return false;
180
+ }
181
+
182
+ /** The last part of a path: the name a notice uses. */
183
+ export function fileLinkName(path) {
184
+ const value = slashes(path).replace(/\/+$/, '');
185
+ return value.slice(value.lastIndexOf('/') + 1) || value;
186
+ }
187
+
188
+ /** A path as a panel copies it: without the place in the file. */
189
+ export function withoutPlace(text) {
190
+ return String(text ?? '').trim().replace(/:\d+(?::\d+)?$/, '');
191
+ }
192
+
193
+ /**
194
+ * The tooltip of a link the editor may open.
195
+ * @param {{ line?: number | null, column?: number | null }} target from parseFileLinkTarget()
196
+ * @returns {{ key: 'open' | 'openAtLine' | 'openAtLineColumn', params: object }}
197
+ */
198
+ export function fileLinkTitle(target) {
199
+ if (!target?.line) return { key: 'open', params: {} };
200
+ return target.column
201
+ ? { key: 'openAtLineColumn', params: { line: target.line, column: target.column } }
202
+ : { key: 'openAtLine', params: { line: target.line } };
203
+ }
204
+
205
+ /**
206
+ * What `GET /api/file-content` said about a path: the editor's own answer to "can this be opened".
207
+ * @param {{ status: number }} answer
208
+ * @returns {{ ok: true } | { ok: false, reason: 'missing' | 'notText' | 'tooLarge' | 'outside' | 'failed' }}
209
+ */
210
+ export function describeProbe({ status } = {}) {
211
+ if (status === 200) return { ok: true };
212
+ if (status === 415) return { ok: false, reason: 'notText' };
213
+ if (status === 413) return { ok: false, reason: 'tooLarge' };
214
+ if (status === 403) return { ok: false, reason: 'outside' };
215
+ if (status === 400 || status === 404) return { ok: false, reason: 'missing' };
216
+ return { ok: false, reason: 'failed' };
217
+ }
218
+
219
+ // A file that is there but cannot be edited says more than one that is not there at all.
220
+ const TELLING = ['failed', 'outside', 'missing', 'tooLarge', 'notText'];
221
+
222
+ /** Of two reasons a path was not opened, the one worth telling the user. */
223
+ export function moreTelling(a, b) {
224
+ return TELLING.indexOf(b) > TELLING.indexOf(a) ? b : a;
225
+ }
226
+
227
+ /** A `FILE_LINK_COPY` sentence with its `{params}` filled in. */
228
+ export function fillFileLinkCopy(key, params = {}) {
229
+ return String(FILE_LINK_COPY[key] ?? key).replace(/\{(\w+)\}/g, (whole, name) => (params[name] != null ? params[name] : whole));
230
+ }
231
+
232
+ // ── The click ──
233
+
234
+ /**
235
+ * What a link points at, as the agent wrote it.
236
+ * @param {Element} anchor
237
+ * @param {{ textLinks?: string }} [options] `textLinks`: the selector of the panel's own path links, the ones
238
+ * it makes out of a path in plain text (their text is the target, their `href` is "#")
239
+ * @returns {{ raw: string, text: boolean } | null}
240
+ */
241
+ export function readFileLink(anchor, { textLinks = '' } = {}) {
242
+ const kept = anchor.getAttribute('data-file-target');
243
+ if (kept) return { raw: kept, text: false };
244
+ const href = anchor.getAttribute('href');
245
+ if (textLinks && anchor.matches(textLinks) && (!href || href === '#')) return { raw: anchor.textContent || '', text: true };
246
+ return href ? { raw: href, text: false } : null;
247
+ }
248
+
249
+ /**
250
+ * The click and hover handlers of the links in a panel's messages.
251
+ * @param {object} page what the handlers need from the page
252
+ * @param {(event: string, data: object) => void} page.emit the bus (`open-file-editor`)
253
+ * @param {(path: string) => Promise<number>} page.probe the status GET /api/file-content answers for a path
254
+ * @param {() => Promise<string>} page.home the user's home folder ('' when it cannot be told)
255
+ * @param {() => string} page.origin the page's origin
256
+ * @param {(key: string, params?: object) => string} page.say a `fileLinks.*` sentence
257
+ * @param {(message: string) => void} page.notify shows a short message
258
+ * @param {(anchor: Element) => void} page.follow goes where a link of this app leads, as the browser would
259
+ */
260
+ export function createFileLinkHandlers(page) {
261
+ const whereOf = (anchor, options) => {
262
+ let cwd = '';
263
+ try { cwd = options.cwd?.(anchor) || ''; } catch { cwd = ''; }
264
+ return { cwd, origin: page.origin() };
265
+ };
266
+
267
+ async function openOrFallBack(anchor, link, where, target, options) {
268
+ let found = target;
269
+ if (found.needsHome) found = parseFileLinkTarget(link.raw, { ...where, home: await page.home() });
270
+
271
+ let reason = null;
272
+ for (const path of found?.candidates || []) {
273
+ let answer;
274
+ try { answer = describeProbe({ status: await page.probe(path) }); } catch { answer = { ok: false, reason: 'failed' }; }
275
+ if (answer.ok) {
276
+ page.emit('open-file-editor', { filePath: path, line: found.line || undefined, column: found.column || undefined });
277
+ return 'opened';
278
+ }
279
+ reason = reason ? moreTelling(reason, answer.reason) : answer.reason;
280
+ }
281
+ reason ||= 'missing'; // nothing to try: `~` with no home folder to read it from
282
+
283
+ // Not a file the editor opens. The link keeps what it did before, or the reason is said.
284
+ if (link.text && typeof options.copyPath === 'function') {
285
+ try { await options.copyPath(anchor, withoutPlace(link.raw)); return 'copied'; } catch { /* could not copy: say why it did not open */ }
286
+ }
287
+ if (target.sameOrigin) { page.follow(anchor); return 'followed'; }
288
+ (options.notify || page.notify)(page.say(reason, { name: fileLinkName(found?.path || withoutPlace(link.raw)) }));
289
+ return reason;
290
+ }
291
+
292
+ /**
293
+ * Take a click on a link in an agent's message when the link points at a file.
294
+ * @param {MouseEvent} event
295
+ * @param {object} [options]
296
+ * @param {Element} [options.within] only links inside this element are taken
297
+ * @param {(anchor: Element) => string} [options.cwd] the working directory of the tab or session the link's
298
+ * message belongs to: a relative path is a path in it
299
+ * @param {string} [options.textLinks] selector of the panel's own path links (see readFileLink)
300
+ * @param {(anchor: Element, path: string) => any} [options.copyPath] what a click on one of those did
301
+ * before: it runs when the path is not a file the editor opens
302
+ * @param {(message: string) => void} [options.notify] shows the reason a link was not opened
303
+ * @returns {Promise<string> | null} null when the click is not this handler's (the browser follows the
304
+ * link as before); else the click is taken and the promise says how it ended: 'opened', 'copied',
305
+ * 'followed', or the reason the user was told
306
+ */
307
+ function handleClick(event, options = {}) {
308
+ if (event.defaultPrevented || event.button > 0) return null;
309
+ const anchor = event.target?.closest?.('a');
310
+ if (!anchor || (options.within && !options.within.contains(anchor))) return null;
311
+ if (anchor.hasAttribute?.('download')) return null; // a file the app itself offers
312
+ const link = readFileLink(anchor, options);
313
+ if (!link) return null;
314
+ const where = whereOf(anchor, options);
315
+ const target = parseFileLinkTarget(link.raw, where);
316
+ if (!target) {
317
+ if (!link.text || typeof options.copyPath !== 'function') return null;
318
+ event.preventDefault();
319
+ try { options.copyPath(anchor, withoutPlace(link.raw)); } catch { /* nothing to copy to */ }
320
+ return Promise.resolve('copied');
321
+ }
322
+ event.preventDefault();
323
+ return openOrFallBack(anchor, link, where, target, options);
324
+ }
325
+
326
+ /** A link the editor may open says so when it is pointed at, unless it already says something. */
327
+ function label(event, options = {}) {
328
+ const anchor = event.target?.closest?.('a');
329
+ if (!anchor || anchor.title || (options.within && !options.within.contains(anchor))) return;
330
+ const link = readFileLink(anchor, options);
331
+ if (!link || link.text) return;
332
+ const target = parseFileLinkTarget(link.raw, whereOf(anchor, options));
333
+ if (!target || target.sameOrigin) return;
334
+ const { key, params } = fileLinkTitle(target);
335
+ anchor.title = page.say(key, params);
336
+ }
337
+
338
+ return { handleClick, label };
339
+ }
@@ -0,0 +1,145 @@
1
+ // ═══════════════════════════════════════════
2
+ // SynaBun Neural Interface — Git Bash for Claude Code: what Settings says
3
+ // ═══════════════════════════════════════════
4
+ //
5
+ // Settings → AI connections → Executable paths has a row for the Git Bash
6
+ // that every Claude Code started by SynaBun gets on Windows (the server's
7
+ // lib/git-bash/, GET /api/cli/git-bash): the one on the PC, SynaBun's own copy
8
+ // of Git for Windows (installed with the app, or downloaded on an npm
9
+ // install), its download while it runs, why it failed, and the button that
10
+ // tries again. This module reads an answer of the server into what the row
11
+ // shows; ui-git-bash.js draws it.
12
+ //
13
+ // No imports and no DOM: `node --test` loads this file as it is
14
+ // (tests/git-bash-view.test.mjs). GIT_BASH_COPY is the English of every
15
+ // sentence; `gitBash.*` in i18n/en.json must equal it and i18n/pt-BR.json must
16
+ // have the same keys.
17
+ //
18
+ // macOS and Linux have nothing to show (Claude Code runs the system's shell
19
+ // there): the row stays hidden, as it does for a server from before this,
20
+ // which has no such route.
21
+
22
+ export const GIT_BASH_COPY = Object.freeze({
23
+ label: 'Git Bash for Claude Code (Windows)',
24
+ help: 'Claude Code on Windows runs its Bash tool and its hooks in Git Bash. SynaBun uses the Git for Windows of this PC, or its own copy when the PC has none.',
25
+ badgeSystem: 'On this PC',
26
+ badgeReady: 'SynaBun\'s copy',
27
+ badgeMissing: 'Not found',
28
+ badgeDownloading: 'Downloading',
29
+ badgeFailed: 'Not installed',
30
+ badgeOff: 'Off',
31
+ sourceEnv: 'CLAUDE_CODE_GIT_BASH_PATH names {path}.',
32
+ sourceSettings: 'Your Claude Code settings name {path}.',
33
+ sourceSystem: 'Git for Windows on this PC: {path}',
34
+ sourceSystemPassed: 'Git for Windows on this PC: {path}. Claude Code would not find it by itself, so SynaBun names it.',
35
+ sourceBundled: 'Git for Windows {version}, installed with SynaBun: {path}',
36
+ sourceDownloaded: 'Git for Windows {version}, downloaded by SynaBun: {path}',
37
+ missing: 'No Git Bash on this PC. SynaBun downloads its own copy of Git for Windows {version} ({size} MB) and checks it against its published SHA-256.',
38
+ stepDownload: 'Downloading Git for Windows {version}: {done} of {total} MB',
39
+ stepExtract: 'Unpacking Git for Windows {version}: {percent}%',
40
+ stepVerify: 'Checking every file against the pinned release…',
41
+ stepPlace: 'Putting it in place…',
42
+ failed: 'Git for Windows could not be put in place. {reason}',
43
+ off: 'Turned off: SYNABUN_GIT_BASH=off is set, so SynaBun downloads nothing and names no Git Bash.',
44
+ unsupported: 'No Git for Windows release is pinned for this processor. Install Git for Windows from git-scm.com.',
45
+ settingsNamed: 'A claude you start in another terminal uses it too: your Claude Code settings name it.',
46
+ settingsKept: 'Your Claude Code settings name a Git Bash that is not there ({value}). SynaBun left that value as it is.',
47
+ settingsError: 'Your Claude Code settings could not be updated: {message}',
48
+ download: 'Download',
49
+ retry: 'Try again',
50
+ recheck: 'Re-check',
51
+ checking: 'Checking…',
52
+ errorOffline: 'The download server could not be reached. Check the connection and try again.',
53
+ errorHttp: 'The download server answered with an error. Try again in a moment.',
54
+ errorInterrupted: 'The download stopped before it was complete. Nothing was kept.',
55
+ errorTimeout: 'Nothing arrived from the download server for too long.',
56
+ errorChecksum: 'What arrived is not the published release, so it was removed and never run.',
57
+ errorUnpack: 'The download could not be unpacked, so it was removed.',
58
+ errorDisk: 'The files could not be written. Check free space (about 550 MB) and permissions.',
59
+ errorCancelled: 'The download was stopped.',
60
+ errorUnknown: 'The download did not finish.',
61
+ errorRequest: 'SynaBun did not answer: {message}',
62
+ });
63
+
64
+ const ERROR_COPY = Object.freeze({
65
+ offline: 'errorOffline', http: 'errorHttp', interrupted: 'errorInterrupted', timeout: 'errorTimeout',
66
+ checksum: 'errorChecksum', unpack: 'errorUnpack', disk: 'errorDisk', cancelled: 'errorCancelled',
67
+ });
68
+
69
+ const STEP_COPY = Object.freeze({ download: 'stepDownload', extract: 'stepExtract', verify: 'stepVerify', place: 'stepPlace' });
70
+
71
+ /** A GIT_BASH_COPY sentence with its `{params}` filled in: the translator when the page gives none. */
72
+ export function fillGitBashCopy(key, params = {}) {
73
+ return String(GIT_BASH_COPY[key] ?? key).replace(/\{(\w+)\}/g, (whole, name) => (params[name] != null ? params[name] : whole));
74
+ }
75
+
76
+ let _translate = null;
77
+
78
+ /** The page's translator: `(key, params) => t('gitBash.' + key, params)`; an empty answer or the key itself falls back to the English. */
79
+ export function setGitBashTranslator(fn) {
80
+ _translate = typeof fn === 'function' ? fn : null;
81
+ }
82
+
83
+ /** A sentence in the page's language. */
84
+ export function gitBashText(key, params = {}) {
85
+ if (_translate) {
86
+ try {
87
+ const text = _translate(key, params);
88
+ if (typeof text === 'string' && text && text !== key && text !== `gitBash.${key}`) return text;
89
+ } catch { /* the English below */ }
90
+ }
91
+ return fillGitBashCopy(key, params);
92
+ }
93
+
94
+ /** Whole megabytes (the release file is about a hundred of them). */
95
+ const mb = (bytes) => String(Number(bytes) > 0 ? Math.round(Number(bytes) / 1024 / 1024) : 0);
96
+
97
+ /** What the user's settings note adds to a row that shows SynaBun's copy in use, or null. */
98
+ function settingsNote(status) {
99
+ const result = status.settings;
100
+ if (!result || typeof result !== 'object') return null;
101
+ if (status.state === 'ready' && (result.state === 'written' || result.state === 'unchanged') && result.value === status.path) return gitBashText('settingsNamed');
102
+ if (result.state === 'kept-user-value' && result.value) return gitBashText('settingsKept', { value: result.value });
103
+ if (result.state === 'error') return gitBashText('settingsError', { message: result.message || '' });
104
+ return null;
105
+ }
106
+
107
+ /**
108
+ * The row for one answer of GET /api/cli/git-bash.
109
+ * @returns {{ visible: boolean, badge: { text: string, tone: 'detected' | 'not-found' | 'custom' | null } | null,
110
+ * line: string, note: string | null, action: 'download' | 'retry' | null, busy: boolean, recheck: boolean }}
111
+ */
112
+ export function gitBashView(status) {
113
+ const hidden = { visible: false, badge: null, line: '', note: null, action: null, busy: false, recheck: false };
114
+ if (!status || typeof status !== 'object' || status.supported !== true) return hidden;
115
+ const version = status.pinned?.version || '';
116
+ const path = status.path || '';
117
+ const row = (badge, tone, line, extra = {}) => ({ ...hidden, visible: true, badge: { text: gitBashText(badge), tone }, line, recheck: true, ...extra });
118
+ switch (status.state) {
119
+ case 'off':
120
+ return row('badgeOff', null, gitBashText('off'), { recheck: false });
121
+ case 'unsupported':
122
+ return row('badgeMissing', 'not-found', gitBashText('unsupported'));
123
+ case 'system': {
124
+ const key = status.source === 'env' ? 'sourceEnv' : status.source === 'settings' ? 'sourceSettings' : status.passToClaude ? 'sourceSystemPassed' : 'sourceSystem';
125
+ return row('badgeSystem', 'detected', gitBashText(key, { path }), { note: settingsNote(status) });
126
+ }
127
+ case 'ready':
128
+ return row('badgeReady', 'detected', gitBashText(status.source === 'bundled' ? 'sourceBundled' : 'sourceDownloaded', { version, path }), { note: settingsNote(status) });
129
+ case 'missing':
130
+ return row('badgeMissing', 'not-found', gitBashText('missing', { version, size: mb(status.pinned?.size) }), { action: 'download' });
131
+ case 'downloading': {
132
+ const step = STEP_COPY[status.step] || 'stepDownload';
133
+ const progress = status.progress || {};
134
+ const params = { version, done: mb(progress.done), total: mb(progress.total || status.pinned?.size), percent: Number(progress.percent) || 0 };
135
+ return row('badgeDownloading', 'custom', gitBashText(step, params), { busy: true, recheck: false });
136
+ }
137
+ case 'failed': {
138
+ const code = status.error?.code;
139
+ const reason = ERROR_COPY[code] ? gitBashText(ERROR_COPY[code]) : (status.error?.message || gitBashText('errorUnknown'));
140
+ return row('badgeFailed', 'not-found', gitBashText('failed', { reason }), { action: 'retry', note: settingsNote(status) });
141
+ }
142
+ default:
143
+ return hidden;
144
+ }
145
+ }
@@ -0,0 +1,178 @@
1
+ // ═══════════════════════════════════════════
2
+ // SynaBun — Greeting: the card every surface mounts
3
+ // ═══════════════════════════════════════════
4
+ //
5
+ // One renderer for the greeting: the Assistant and the Claude Code, Codex and
6
+ // OpenCode panels mount it inside the empty state they already have, Settings
7
+ // shows the same card as its preview. greeting-view.js decides the words; this
8
+ // file asks the server (`POST /api/greeting/open`) and draws.
9
+ //
10
+ // Everything is text (never markup): a greeting is built from records and may
11
+ // have been written by a model. A server older than this page (404) or an
12
+ // engine that did not start (503) simply shows nothing, and is asked again a
13
+ // minute later at the earliest.
14
+
15
+ import { greetingView, readGreetingResponse } from './greeting-view.js';
16
+
17
+ const STYLE_ID = 'sb-greet-styles';
18
+ const OPEN_URL = '/api/greeting/open';
19
+ const RETRY_MS = 60_000;
20
+ const TIMEOUT_MS = 4000;
21
+
22
+ // The app's own tokens (styles.css), so the card reads like the panel it sits in; a panel with
23
+ // tokens of its own maps them here.
24
+ const STYLES = `
25
+ .sb-greet { --sbg-text: var(--t-bright, rgba(255,255,255,0.9)); --sbg-2: var(--t-secondary, rgba(255,255,255,0.5)); --sbg-3: var(--t-muted, rgba(255,255,255,0.3)); --sbg-line: var(--b-subtle, rgba(255,255,255,0.06)); --sbg-fill: var(--s-subtle, rgba(255,255,255,0.04));
26
+ display: flex; flex-direction: column; align-items: center; gap: 4px; width: 100%; max-width: 440px; margin: 6px auto 0; padding: 0 12px; box-sizing: border-box; text-align: center; pointer-events: auto; }
27
+ .asst-empty .sb-greet { --sbg-text: var(--asst-text, var(--t-bright)); --sbg-2: var(--asst-text-2, var(--t-secondary)); --sbg-3: var(--asst-text-3, var(--t-muted)); margin-top: 0; }
28
+ .sb-greet[hidden] { display: none; }
29
+ .sb-greet-when { font-size: 12px; line-height: 1.45; color: var(--sbg-3); }
30
+ .sb-greet-text { margin: 0; font-size: 13px; line-height: 1.5; font-weight: 400; color: var(--sbg-text); overflow-wrap: anywhere; text-wrap: pretty; user-select: text; }
31
+ .sb-greet-more { appearance: none; display: inline-flex; align-items: center; gap: 4px; margin-top: 2px; padding: 2px 6px; border: 0; border-radius: var(--r-small, 6px); background: none; color: var(--sbg-3); font: inherit; font-size: 12px; line-height: 1.45; cursor: pointer; }
32
+ .sb-greet-more:is(:hover, :focus-visible) { color: var(--sbg-2); background: var(--sbg-fill); }
33
+ .sb-greet-more:focus-visible { outline: 1px solid var(--b-visible, rgba(255,255,255,0.22)); outline-offset: 1px; }
34
+ .sb-greet-chev { display: inline-block; transition: transform .12s ease; }
35
+ .sb-greet-more[aria-expanded="true"] .sb-greet-chev { transform: rotate(90deg); }
36
+ .sb-greet-basis { display: flex; flex-direction: column; gap: 4px; width: 100%; box-sizing: border-box; margin-top: 2px; padding: 8px 10px; border: 1px solid var(--sbg-line); border-radius: var(--r-card, 8px); background: var(--sbg-fill); text-align: left; }
37
+ .sb-greet-basis[hidden] { display: none; }
38
+ .sb-greet-row { display: grid; grid-template-columns: minmax(84px, 30%) minmax(0, 1fr) auto; gap: 8px; align-items: baseline; font-size: 12px; line-height: 1.45; }
39
+ .sb-greet-kind { color: var(--sbg-3); }
40
+ .sb-greet-label { color: var(--sbg-2); overflow-wrap: anywhere; user-select: text; }
41
+ .sb-greet-link { appearance: none; padding: 0 2px; border: 0; background: none; color: var(--sbg-3); font-family: var(--ff-mono, ui-monospace, 'JetBrains Mono', monospace); font-size: 11px; text-decoration: underline dotted; text-underline-offset: 2px; cursor: pointer; }
42
+ .sb-greet-link:is(:hover, :focus-visible) { color: var(--sbg-text); }
43
+ .sb-greet-cost { margin-top: 2px; padding-top: 6px; border-top: 1px solid var(--sbg-line); font-size: 12px; line-height: 1.45; color: var(--sbg-3); }
44
+ @media (prefers-reduced-motion: reduce) { .sb-greet-chev { transition: none; } }
45
+ `;
46
+
47
+ let uid = 0;
48
+ const cache = new Map(); // `${surface}:${session}` → the greeting this conversation opened with (or null)
49
+ let unavailableUntil = 0; // an older server or an engine that did not start: not asked again before this
50
+
51
+ /** The card's styles, once per document. */
52
+ export function ensureGreetingStyles(doc = globalThis.document) {
53
+ if (!doc?.head || doc.getElementById(STYLE_ID)) return;
54
+ const style = doc.createElement('style');
55
+ style.id = STYLE_ID;
56
+ style.textContent = STYLES;
57
+ doc.head.appendChild(style);
58
+ }
59
+
60
+ function node(doc, tag, className, text) {
61
+ const el = doc.createElement(tag);
62
+ if (className) el.className = className;
63
+ if (text != null) el.textContent = text;
64
+ return el;
65
+ }
66
+
67
+ /**
68
+ * Draw a greeting inside `host` (a panel's empty state), replacing one already there.
69
+ * → the card element, or null when there is nothing to draw.
70
+ * opts: { t, now, onOpenMemory(id), open } (`open`: the disclosure starts open, Settings' preview)
71
+ */
72
+ export function renderGreeting(host, greeting, { t = null, now = Date.now(), onOpenMemory = null, open = false } = {}) {
73
+ const doc = host?.ownerDocument || globalThis.document;
74
+ if (!host || !doc) return null;
75
+ for (const old of host.querySelectorAll(':scope > .sb-greet')) old.remove();
76
+ const view = greetingView(greeting, { now, t });
77
+ if (!view) return null;
78
+ ensureGreetingStyles(doc);
79
+ const card = node(doc, 'div', 'sb-greet');
80
+ card.setAttribute('role', 'note');
81
+ card.setAttribute('aria-label', view.label);
82
+ if (view.id) card.dataset.greetingId = view.id;
83
+ if (view.when) card.appendChild(node(doc, 'div', 'sb-greet-when', view.when));
84
+ card.appendChild(node(doc, 'p', 'sb-greet-text', view.text));
85
+
86
+ const basisId = `sb-greet-basis-${++uid}`;
87
+ const more = node(doc, 'button', 'sb-greet-more');
88
+ more.type = 'button';
89
+ more.setAttribute('aria-controls', basisId);
90
+ more.appendChild(node(doc, 'span', 'sb-greet-more-text', view.toggle));
91
+ const chev = node(doc, 'span', 'sb-greet-chev', '›');
92
+ chev.setAttribute('aria-hidden', 'true');
93
+ more.appendChild(chev);
94
+ const basis = node(doc, 'div', 'sb-greet-basis');
95
+ basis.id = basisId;
96
+ for (const row of view.rows) {
97
+ const line = node(doc, 'div', 'sb-greet-row');
98
+ line.dataset.kind = row.kind;
99
+ line.appendChild(node(doc, 'span', 'sb-greet-kind', row.kindLabel));
100
+ line.appendChild(node(doc, 'span', 'sb-greet-label', row.label));
101
+ if (row.id && typeof onOpenMemory === 'function') {
102
+ const link = node(doc, 'button', 'sb-greet-link', row.id.slice(0, 8));
103
+ link.type = 'button';
104
+ link.title = `${row.openLabel} ${row.id}`;
105
+ link.setAttribute('aria-label', link.title);
106
+ link.addEventListener('click', (event) => { event.stopPropagation(); try { onOpenMemory(row.id); } catch { /* the host decides */ } });
107
+ line.appendChild(link);
108
+ }
109
+ basis.appendChild(line);
110
+ }
111
+ basis.appendChild(node(doc, 'div', 'sb-greet-cost', view.cost));
112
+ const setOpen = (value) => { more.setAttribute('aria-expanded', value ? 'true' : 'false'); basis.hidden = !value; };
113
+ more.addEventListener('click', (event) => { event.stopPropagation(); setOpen(more.getAttribute('aria-expanded') !== 'true'); });
114
+ setOpen(open === true);
115
+ card.appendChild(more);
116
+ card.appendChild(basis);
117
+ host.appendChild(card);
118
+ return card;
119
+ }
120
+
121
+ /** Remove the card from a host. */
122
+ export function clearGreeting(host) {
123
+ if (!host) return;
124
+ for (const old of host.querySelectorAll(':scope > .sb-greet')) old.remove();
125
+ }
126
+
127
+ /**
128
+ * Ask whether this fresh conversation is greeted. One request per conversation: the answer is
129
+ * kept, so a redraw of the empty state costs nothing. → the greeting, or null.
130
+ * `session`: the surface's own name for the conversation (the server gives the same one the same answer).
131
+ */
132
+ export async function fetchGreeting({ surface, cwd = null, project = null, session = null, fetchImpl = globalThis.fetch, now = Date.now } = {}) {
133
+ if (!surface || typeof fetchImpl !== 'function') return null;
134
+ const key = session ? `${surface}:${session}` : null;
135
+ if (key && cache.has(key)) return cache.get(key);
136
+ if (now() < unavailableUntil) return null;
137
+ const controller = typeof AbortController === 'function' ? new AbortController() : null;
138
+ const timer = controller ? setTimeout(() => controller.abort(), TIMEOUT_MS) : null;
139
+ try {
140
+ const response = await fetchImpl(OPEN_URL, {
141
+ method: 'POST', headers: { 'Content-Type': 'application/json' }, signal: controller?.signal,
142
+ body: JSON.stringify({ surface, ...(project ? { project } : {}), ...(cwd ? { cwd } : {}), ...(session ? { session } : {}) }),
143
+ });
144
+ let body = null;
145
+ try { body = await response.json(); } catch { body = null; }
146
+ const answer = readGreetingResponse(response.status, body);
147
+ if (answer.state === 'old-server' || answer.state === 'failed') { unavailableUntil = now() + RETRY_MS; return null; }
148
+ if (answer.state === 'unreadable') return null;
149
+ if (key) { cache.set(key, answer.greeting); while (cache.size > 200) cache.delete(cache.keys().next().value); }
150
+ return answer.greeting;
151
+ } catch { return null; } finally { if (timer) clearTimeout(timer); }
152
+ }
153
+
154
+ /** The greeting a conversation opened with, if it was asked already (no request). */
155
+ export function knownGreeting(surface, session) {
156
+ return session ? cache.get(`${surface}:${session}`) || null : null;
157
+ }
158
+
159
+ /** The conversation sent its first message: its greeting is not drawn again. */
160
+ export function forgetGreeting(surface, session) {
161
+ if (session) cache.set(`${surface}:${session}`, null);
162
+ }
163
+
164
+ /**
165
+ * Ask and draw. `host`: the empty state element; `isCurrent()`: false when the conversation is no
166
+ * longer the one on screen (the answer arrived late). → the greeting drawn, or null.
167
+ */
168
+ export async function mountGreeting(host, { surface, cwd = null, project = null, session = null, t = null, onOpenMemory = null, isCurrent = null, fetchImpl = globalThis.fetch } = {}) {
169
+ if (!host) return null;
170
+ const greeting = await fetchGreeting({ surface, cwd, project, session, fetchImpl });
171
+ // (A host that is not in the document yet is fine: a panel may draw a tab before it shows it.)
172
+ if (!greeting) return null;
173
+ if (typeof isCurrent === 'function') { try { if (isCurrent() === false) return greeting; } catch { return greeting; } }
174
+ renderGreeting(host, greeting, { t, onOpenMemory });
175
+ return greeting;
176
+ }
177
+
178
+ export const _test = { reset() { cache.clear(); unavailableUntil = 0; } };