synabun 2.0.9 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (679) hide show
  1. package/CHANGELOG.md +115 -0
  2. package/README.md +25 -2
  3. package/THIRD-PARTY-LICENSES.md +68 -4
  4. package/bin/synabun.js +71 -24
  5. package/docs/whatsapp.md +2 -2
  6. package/hooks/claude-code/pre-websearch.mjs +12 -1
  7. package/hooks/claude-code/shared.mjs +5 -0
  8. package/hooks/claude-code/stop.mjs +1 -1
  9. package/launcher.mjs +9 -3
  10. package/lib/claude-hooks.js +172 -18
  11. package/lib/client-config-repair.js +17 -8
  12. package/lib/desktop-app.js +7 -1
  13. package/lib/external-tools.js +1 -1
  14. package/lib/first-launch-protection.js +57 -12
  15. package/lib/mcp-dist.js +176 -0
  16. package/lib/release-npm-ci.mjs +138 -0
  17. package/lib/release-publish.mjs +91 -0
  18. package/lib/release-scan.mjs +50 -0
  19. package/lib/server-crash-record.js +123 -0
  20. package/lib/server-exit.js +318 -0
  21. package/lib/server-supervisor.js +508 -0
  22. package/lib/start-launcher.js +43 -5
  23. package/lib/tools-cli.js +45 -0
  24. package/lib/update-snapshot.js +228 -0
  25. package/lib/virustotal.mjs +192 -0
  26. package/lib/windows-crash-event.js +269 -0
  27. package/mcp-server/dist/http.d.ts +20 -1
  28. package/mcp-server/dist/http.d.ts.map +1 -1
  29. package/mcp-server/dist/http.js +59 -12
  30. package/mcp-server/dist/http.js.map +1 -1
  31. package/mcp-server/dist/index.d.ts +33 -27
  32. package/mcp-server/dist/index.d.ts.map +1 -1
  33. package/mcp-server/dist/index.js +128 -143
  34. package/mcp-server/dist/index.js.map +1 -1
  35. package/mcp-server/dist/preload.js +1 -1
  36. package/mcp-server/dist/preload.js.map +1 -1
  37. package/mcp-server/dist/services/codex-config-heal.d.ts +6 -2
  38. package/mcp-server/dist/services/codex-config-heal.d.ts.map +1 -1
  39. package/mcp-server/dist/services/codex-config-heal.js +81 -28
  40. package/mcp-server/dist/services/codex-config-heal.js.map +1 -1
  41. package/mcp-server/dist/services/memory-judge-backfill.d.ts +6 -0
  42. package/mcp-server/dist/services/memory-judge-backfill.d.ts.map +1 -1
  43. package/mcp-server/dist/services/memory-judge-backfill.js +7 -1
  44. package/mcp-server/dist/services/memory-judge-backfill.js.map +1 -1
  45. package/mcp-server/dist/services/neural-interface.d.ts +16 -10
  46. package/mcp-server/dist/services/neural-interface.d.ts.map +1 -1
  47. package/mcp-server/dist/services/neural-interface.js +67 -11
  48. package/mcp-server/dist/services/neural-interface.js.map +1 -1
  49. package/mcp-server/dist/services/server-instructions.d.ts +7 -2
  50. package/mcp-server/dist/services/server-instructions.d.ts.map +1 -1
  51. package/mcp-server/dist/services/server-instructions.js +20 -62
  52. package/mcp-server/dist/services/server-instructions.js.map +1 -1
  53. package/mcp-server/dist/services/tool-catalog.d.ts +120 -0
  54. package/mcp-server/dist/services/tool-catalog.d.ts.map +1 -0
  55. package/mcp-server/dist/services/tool-catalog.js +339 -0
  56. package/mcp-server/dist/services/tool-catalog.js.map +1 -0
  57. package/mcp-server/dist/services/tool-list-refresh-barrier.d.ts +12 -0
  58. package/mcp-server/dist/services/tool-list-refresh-barrier.d.ts.map +1 -0
  59. package/mcp-server/dist/services/tool-list-refresh-barrier.js +40 -0
  60. package/mcp-server/dist/services/tool-list-refresh-barrier.js.map +1 -0
  61. package/mcp-server/dist/services/tool-list.d.ts +108 -0
  62. package/mcp-server/dist/services/tool-list.d.ts.map +1 -0
  63. package/mcp-server/dist/services/tool-list.js +334 -0
  64. package/mcp-server/dist/services/tool-list.js.map +1 -0
  65. package/mcp-server/dist/services/tool-runtime.d.ts +161 -0
  66. package/mcp-server/dist/services/tool-runtime.d.ts.map +1 -0
  67. package/mcp-server/dist/services/tool-runtime.js +400 -0
  68. package/mcp-server/dist/services/tool-runtime.js.map +1 -0
  69. package/mcp-server/dist/services/whiteboard-geometry.d.ts +26 -2
  70. package/mcp-server/dist/services/whiteboard-geometry.d.ts.map +1 -1
  71. package/mcp-server/dist/services/whiteboard-geometry.js +91 -8
  72. package/mcp-server/dist/services/whiteboard-geometry.js.map +1 -1
  73. package/mcp-server/dist/tools/agents.d.ts +10 -10
  74. package/mcp-server/dist/tools/agents.d.ts.map +1 -1
  75. package/mcp-server/dist/tools/agents.js +22 -13
  76. package/mcp-server/dist/tools/agents.js.map +1 -1
  77. package/mcp-server/dist/tools/browser-batch.d.ts +3 -3
  78. package/mcp-server/dist/tools/image-tools.d.ts +1 -1
  79. package/mcp-server/dist/tools/image-tools.js +1 -1
  80. package/mcp-server/dist/tools/skill-tools.d.ts +505 -0
  81. package/mcp-server/dist/tools/skill-tools.d.ts.map +1 -0
  82. package/mcp-server/dist/tools/skill-tools.js +639 -0
  83. package/mcp-server/dist/tools/skill-tools.js.map +1 -0
  84. package/mcp-server/dist/tools/skill.d.ts +7 -0
  85. package/mcp-server/dist/tools/skill.d.ts.map +1 -0
  86. package/mcp-server/dist/tools/skill.js +11 -0
  87. package/mcp-server/dist/tools/skill.js.map +1 -0
  88. package/mcp-server/dist/tools/tool-groups.d.ts +68 -0
  89. package/mcp-server/dist/tools/tool-groups.d.ts.map +1 -0
  90. package/mcp-server/dist/tools/tool-groups.js +147 -0
  91. package/mcp-server/dist/tools/tool-groups.js.map +1 -0
  92. package/mcp-server/dist/tools/whiteboard-format.d.ts.map +1 -1
  93. package/mcp-server/dist/tools/whiteboard-format.js +7 -4
  94. package/mcp-server/dist/tools/whiteboard-format.js.map +1 -1
  95. package/mcp-server/dist/tools/whiteboard-tools.d.ts +64 -13
  96. package/mcp-server/dist/tools/whiteboard-tools.d.ts.map +1 -1
  97. package/mcp-server/dist/tools/whiteboard-tools.js +30 -5
  98. package/mcp-server/dist/tools/whiteboard-tools.js.map +1 -1
  99. package/mcp-server/package-lock.json +2 -2
  100. package/mcp-server/package.json +1 -1
  101. package/mcp-server/src/http.ts +75 -12
  102. package/mcp-server/src/index.ts +147 -154
  103. package/mcp-server/src/preload.ts +12 -12
  104. package/mcp-server/src/services/codex-config-heal.ts +80 -29
  105. package/mcp-server/src/services/memory-judge-backfill.ts +7 -1
  106. package/mcp-server/src/services/neural-interface.ts +79 -20
  107. package/mcp-server/src/services/server-instructions.ts +27 -63
  108. package/mcp-server/src/services/tool-catalog.ts +403 -0
  109. package/mcp-server/src/services/tool-list-refresh-barrier.ts +33 -0
  110. package/mcp-server/src/services/tool-list.ts +410 -0
  111. package/mcp-server/src/services/tool-runtime.ts +457 -0
  112. package/mcp-server/src/services/whiteboard-geometry.ts +94 -9
  113. package/mcp-server/src/tools/agents.ts +22 -14
  114. package/mcp-server/src/tools/image-tools.ts +1 -1
  115. package/mcp-server/src/tools/skill-tools.ts +589 -0
  116. package/mcp-server/src/tools/skill.ts +13 -0
  117. package/mcp-server/src/tools/tool-groups.ts +164 -0
  118. package/mcp-server/src/tools/whiteboard-format.ts +9 -3
  119. package/mcp-server/src/tools/whiteboard-tools.ts +39 -5
  120. package/neural-interface/i18n/en.json +3680 -206
  121. package/neural-interface/i18n/pt-BR.json +3628 -157
  122. package/neural-interface/lib/account-usage.js +5 -3
  123. package/neural-interface/lib/app-controls-api.js +161 -0
  124. package/neural-interface/lib/assistant-api.js +148 -35
  125. package/neural-interface/lib/assistant-brains/claude.js +4 -3
  126. package/neural-interface/lib/assistant-brains/codex-browser-policy-hook.mjs +12 -2
  127. package/neural-interface/lib/assistant-brains/codex-route-gate-hook.mjs +32 -9
  128. package/neural-interface/lib/assistant-brains/codex.js +9 -5
  129. package/neural-interface/lib/assistant-brains/opencode.js +122 -44
  130. package/neural-interface/lib/assistant-brains/opencode2-browser-policy/index.js +59 -0
  131. package/neural-interface/lib/assistant-brains/opencode2-route-gate/index.js +97 -0
  132. package/neural-interface/lib/assistant-catalog.js +23 -9
  133. package/neural-interface/lib/assistant-dispatch.js +874 -156
  134. package/neural-interface/lib/assistant-envelope.js +22 -2
  135. package/neural-interface/lib/assistant-persona.js +80 -11
  136. package/neural-interface/lib/assistant-plan-permissions.js +10 -9
  137. package/neural-interface/lib/assistant-route-gate-codex.js +3 -0
  138. package/neural-interface/lib/assistant-route-gate.js +6 -4
  139. package/neural-interface/lib/assistant-router.js +13 -8
  140. package/neural-interface/lib/assistant-runtime.js +183 -26
  141. package/neural-interface/lib/assistant-task-prompt.js +78 -14
  142. package/neural-interface/lib/assistant-tools.js +51 -0
  143. package/neural-interface/lib/backup-service.js +620 -3
  144. package/neural-interface/lib/backup-zip-reader.js +10 -5
  145. package/neural-interface/lib/backup-zip-writer.js +257 -0
  146. package/neural-interface/lib/browser-execution.js +10 -2
  147. package/neural-interface/lib/browser-guard-cdp.js +83 -0
  148. package/neural-interface/lib/browser-network-guard.js +284 -0
  149. package/neural-interface/lib/browser-policy-roots.js +64 -0
  150. package/neural-interface/lib/browser-tool-policy.js +186 -31
  151. package/neural-interface/lib/claude-agent-bridge.js +4 -2
  152. package/neural-interface/lib/claude-hooks-status.js +103 -0
  153. package/neural-interface/lib/claude-model-catalog.js +64 -14
  154. package/neural-interface/lib/claude-temporary.js +5 -11
  155. package/neural-interface/lib/cli-install.js +18 -138
  156. package/neural-interface/lib/cli-resolver.js +4 -1
  157. package/neural-interface/lib/cli-update-planner.js +32 -31
  158. package/neural-interface/lib/cloudflared/tunnel.js +42 -2
  159. package/neural-interface/lib/codex-mcp-approvals.js +11 -0
  160. package/neural-interface/lib/codex-settings-schema.js +109 -47
  161. package/neural-interface/lib/codex-worker-app-server.js +172 -0
  162. package/neural-interface/lib/confined-fs.js +30 -3
  163. package/neural-interface/lib/crash-notice-api.js +98 -0
  164. package/neural-interface/lib/editor-completion-opencode.js +164 -67
  165. package/neural-interface/lib/exec-loop-command.js +4 -1
  166. package/neural-interface/lib/http-guards.js +8 -2
  167. package/neural-interface/lib/install-details.js +4 -1
  168. package/neural-interface/lib/launch-cache.js +293 -0
  169. package/neural-interface/lib/login-start.js +524 -0
  170. package/neural-interface/lib/mcp-agent-servers.js +67 -0
  171. package/neural-interface/lib/mcp-client-config.js +0 -29
  172. package/neural-interface/lib/mcp-external-servers.js +323 -0
  173. package/neural-interface/lib/{mcp-profile-refresh-coordinator.js → mcp-tools-refresh-coordinator.js} +4 -4
  174. package/neural-interface/lib/morelogin.js +30 -0
  175. package/neural-interface/lib/native-loop-providers.js +565 -50
  176. package/neural-interface/lib/native-loop-runtime.js +287 -41
  177. package/neural-interface/lib/opencode-config.js +557 -0
  178. package/neural-interface/lib/opencode-db-schema.js +58 -0
  179. package/neural-interface/lib/opencode-history.js +49 -204
  180. package/neural-interface/lib/opencode-mcp-ready.js +108 -0
  181. package/neural-interface/lib/opencode-native-boundary.js +44 -0
  182. package/neural-interface/lib/opencode-protocol.js +310 -0
  183. package/neural-interface/lib/opencode-provider-view.js +81 -0
  184. package/neural-interface/lib/opencode-runtime-config.js +32 -15
  185. package/neural-interface/lib/opencode-serve-records.js +624 -0
  186. package/neural-interface/lib/opencode-v2-client.js +53 -131
  187. package/neural-interface/lib/opencode-v2-ws-requests.js +35 -195
  188. package/neural-interface/lib/opencode-worker-activity.js +229 -0
  189. package/neural-interface/lib/opencode-worker-data.js +27 -0
  190. package/neural-interface/lib/opencode2-client.js +796 -0
  191. package/neural-interface/lib/opencode2-proxy.js +76 -0
  192. package/neural-interface/lib/opencode2-translate.js +1057 -0
  193. package/neural-interface/lib/owner-authority.js +249 -0
  194. package/neural-interface/lib/page-session.js +275 -0
  195. package/neural-interface/lib/peer-identity.js +763 -0
  196. package/neural-interface/lib/process-identity.js +361 -0
  197. package/neural-interface/lib/request-gate.js +17 -2
  198. package/neural-interface/lib/rulesets/api.js +1 -1
  199. package/neural-interface/lib/rulesets/fs-safe.js +1 -1
  200. package/neural-interface/lib/rulesets/installer.js +92 -27
  201. package/neural-interface/lib/rulesets/json-edit.js +1 -1
  202. package/neural-interface/lib/session-cache-builder.js +18 -10
  203. package/neural-interface/lib/session-title-generator.js +58 -18
  204. package/neural-interface/lib/skills/api-v2.js +495 -0
  205. package/neural-interface/lib/skills/bundle.js +418 -0
  206. package/neural-interface/lib/skills/catalog.js +294 -0
  207. package/neural-interface/lib/skills/creation-api.js +191 -0
  208. package/neural-interface/lib/skills/creation-brief.js +247 -0
  209. package/neural-interface/lib/skills/creation-sources.js +267 -0
  210. package/neural-interface/lib/skills/creation.js +843 -0
  211. package/neural-interface/lib/skills/deploy.js +564 -0
  212. package/neural-interface/lib/skills/digest.js +119 -0
  213. package/neural-interface/lib/skills/discover.js +111 -0
  214. package/neural-interface/lib/skills/draft-confinement.js +214 -0
  215. package/neural-interface/lib/skills/eval-confinement.js +397 -0
  216. package/neural-interface/lib/skills/eval-sandbox.js +280 -0
  217. package/neural-interface/lib/skills/eval-trace.js +119 -0
  218. package/neural-interface/lib/skills/evals-api.js +212 -0
  219. package/neural-interface/lib/skills/evals.js +1573 -0
  220. package/neural-interface/lib/skills/export.js +200 -0
  221. package/neural-interface/lib/skills/frontmatter.js +64 -0
  222. package/neural-interface/lib/skills/health.js +75 -0
  223. package/neural-interface/lib/skills/json-edit.js +165 -0
  224. package/neural-interface/lib/skills/known-bridge-digests.json +387 -0
  225. package/neural-interface/lib/skills/known-bridges.js +96 -0
  226. package/neural-interface/lib/skills/library.js +823 -0
  227. package/neural-interface/lib/skills/local-store.js +21 -0
  228. package/neural-interface/lib/skills/migration.js +433 -0
  229. package/neural-interface/lib/skills/operations.js +45 -0
  230. package/neural-interface/lib/skills/ownership.js +427 -0
  231. package/neural-interface/lib/skills/packages.js +88 -0
  232. package/neural-interface/lib/skills/reviews.js +212 -0
  233. package/neural-interface/lib/skills/revision.js +80 -0
  234. package/neural-interface/lib/skills/roots.js +262 -0
  235. package/neural-interface/lib/skills/runtime-checks.js +355 -0
  236. package/neural-interface/lib/skills/scan-assets.js +252 -0
  237. package/neural-interface/lib/skills/scan-context.js +26 -0
  238. package/neural-interface/lib/skills/scan-shell.js +324 -0
  239. package/neural-interface/lib/skills/scan.js +364 -0
  240. package/neural-interface/lib/skills/scanners.js +54 -0
  241. package/neural-interface/lib/skills/service.js +522 -0
  242. package/neural-interface/lib/skills/telemetry.js +113 -0
  243. package/neural-interface/lib/sqlite-integrity-worker.js +22 -0
  244. package/neural-interface/lib/style-guide/api.js +4 -3
  245. package/neural-interface/lib/tool-lists.js +124 -0
  246. package/neural-interface/lib/untrusted-file-headers.js +27 -0
  247. package/neural-interface/lib/whatsapp/bridge.js +6 -3
  248. package/neural-interface/lib/worker-confinement.js +1192 -0
  249. package/neural-interface/lib/worker-guard-setting.js +56 -0
  250. package/neural-interface/lib/worker-mcp-channel.js +175 -0
  251. package/neural-interface/lib/worker-relay-ports.js +227 -0
  252. package/neural-interface/lib/worker-resume.js +229 -0
  253. package/neural-interface/lib/worker-sandbox.js +299 -0
  254. package/neural-interface/package-lock.json +1354 -50
  255. package/neural-interface/package.json +6 -2
  256. package/neural-interface/public/index.html +5 -3
  257. package/neural-interface/public/index2d.html +19 -11
  258. package/neural-interface/public/onboarding.html +2 -2
  259. package/neural-interface/public/shared/account-menu.js +8 -5
  260. package/neural-interface/public/shared/agent-runtime-options.js +76 -16
  261. package/neural-interface/public/shared/api.js +1527 -1336
  262. package/neural-interface/public/shared/app-frame.js +192 -0
  263. package/neural-interface/public/shared/assistant/asst-agents-dock.js +205 -19
  264. package/neural-interface/public/shared/assistant/asst-api.js +11 -3
  265. package/neural-interface/public/shared/assistant/asst-ask-styles.js +270 -0
  266. package/neural-interface/public/shared/assistant/asst-ask-tail.js +118 -0
  267. package/neural-interface/public/shared/assistant/asst-ask.js +397 -0
  268. package/neural-interface/public/shared/assistant/asst-brain-picker.js +241 -112
  269. package/neural-interface/public/shared/assistant/asst-composer.js +1 -1
  270. package/neural-interface/public/shared/assistant/asst-control.js +157 -26
  271. package/neural-interface/public/shared/assistant/asst-followups-card.js +383 -0
  272. package/neural-interface/public/shared/assistant/asst-followups.js +417 -0
  273. package/neural-interface/public/shared/assistant/asst-hosts.js +49 -2
  274. package/neural-interface/public/shared/assistant/asst-limit-dock.js +40 -0
  275. package/neural-interface/public/shared/assistant/asst-limit-wait.js +294 -0
  276. package/neural-interface/public/shared/assistant/asst-limit.js +168 -0
  277. package/neural-interface/public/shared/assistant/asst-mascot.js +49 -12
  278. package/neural-interface/public/shared/assistant/asst-menu.js +84 -12
  279. package/neural-interface/public/shared/assistant/asst-panel.js +303 -44
  280. package/neural-interface/public/shared/assistant/asst-rack-styles.js +93 -58
  281. package/neural-interface/public/shared/assistant/asst-redact.js +186 -0
  282. package/neural-interface/public/shared/assistant/asst-render.js +770 -100
  283. package/neural-interface/public/shared/assistant/asst-route.js +83 -12
  284. package/neural-interface/public/shared/assistant/asst-run-stage.js +231 -0
  285. package/neural-interface/public/shared/assistant/asst-sanitize.js +193 -3
  286. package/neural-interface/public/shared/assistant/asst-scene-styles.js +273 -0
  287. package/neural-interface/public/shared/assistant/asst-scenes.js +530 -0
  288. package/neural-interface/public/shared/assistant/asst-sidepanel-styles.js +3 -15
  289. package/neural-interface/public/shared/assistant/asst-sidepanel.js +67 -4
  290. package/neural-interface/public/shared/assistant/asst-slash.js +9 -7
  291. package/neural-interface/public/shared/assistant/asst-state.js +25 -15
  292. package/neural-interface/public/shared/assistant/asst-stop.js +127 -0
  293. package/neural-interface/public/shared/assistant/asst-styles.js +102 -21
  294. package/neural-interface/public/shared/assistant/asst-tool-kinds.js +165 -13
  295. package/neural-interface/public/shared/automation-context.js +5 -2
  296. package/neural-interface/public/shared/cc-dropdown.js +550 -0
  297. package/neural-interface/public/shared/cdx/cdx-context-menu.js +10 -9
  298. package/neural-interface/public/shared/cdx/cdx-copy.js +16 -0
  299. package/neural-interface/public/shared/cdx/cdx-output-model.js +22 -1
  300. package/neural-interface/public/shared/cdx/cdx-output.js +8 -7
  301. package/neural-interface/public/shared/cdx/cdx-panel.js +58 -150
  302. package/neural-interface/public/shared/cdx/cdx-protocol.js +28 -1
  303. package/neural-interface/public/shared/cdx/cdx-render.js +62 -32
  304. package/neural-interface/public/shared/cdx/cdx-requests.js +3 -3
  305. package/neural-interface/public/shared/cdx/cdx-skill-hints.js +54 -0
  306. package/neural-interface/public/shared/cdx/cdx-styles.js +15 -3
  307. package/neural-interface/public/shared/cdx/cdx-tabs.js +201 -74
  308. package/neural-interface/public/shared/cdx/cdx-ws.js +0 -1
  309. package/neural-interface/public/shared/cli-install-view.js +4 -8
  310. package/neural-interface/public/shared/cli-status.js +2 -2
  311. package/neural-interface/public/shared/constants.js +2 -2
  312. package/neural-interface/public/shared/context-menu-areas.js +34 -0
  313. package/neural-interface/public/shared/context-menu-logic.js +243 -0
  314. package/neural-interface/public/shared/context-menu-model.js +366 -0
  315. package/neural-interface/public/shared/context-menu.js +372 -0
  316. package/neural-interface/public/shared/cp/cp-context-menu.js +10 -9
  317. package/neural-interface/public/shared/cp/cp-markdown.js +4 -57
  318. package/neural-interface/public/shared/crash-notice-view.js +168 -0
  319. package/neural-interface/public/shared/data-cache.js +223 -0
  320. package/neural-interface/public/shared/desktop-bridge.js +11 -0
  321. package/neural-interface/public/shared/desktop-titlebar-view.js +78 -1
  322. package/neural-interface/public/shared/desktop-titlebar.js +214 -28
  323. package/neural-interface/public/shared/desktop-update-view.js +35 -5
  324. package/neural-interface/public/shared/editor/editor-panel-logic.js +2 -2
  325. package/neural-interface/public/shared/file-links.js +73 -8
  326. package/neural-interface/public/shared/html-shell.js +78 -86
  327. package/neural-interface/public/shared/i18n.js +2 -0
  328. package/neural-interface/public/shared/launch-prefetch.js +59 -0
  329. package/neural-interface/public/shared/ocp-v2/ocp-v2-approvals.js +7 -7
  330. package/neural-interface/public/shared/ocp-v2/ocp-v2-binding.js +4 -4
  331. package/neural-interface/public/shared/ocp-v2/ocp-v2-caps.js +3 -2
  332. package/neural-interface/public/shared/ocp-v2/ocp-v2-changes-logic.js +3 -101
  333. package/neural-interface/public/shared/ocp-v2/ocp-v2-childpanel.js +9 -2
  334. package/neural-interface/public/shared/ocp-v2/ocp-v2-compact-button.js +1 -3
  335. package/neural-interface/public/shared/ocp-v2/ocp-v2-composer-logic.js +2 -6
  336. package/neural-interface/public/shared/ocp-v2/ocp-v2-context-menu.js +11 -11
  337. package/neural-interface/public/shared/ocp-v2/ocp-v2-context-model.js +0 -3
  338. package/neural-interface/public/shared/ocp-v2/ocp-v2-events.js +8 -4
  339. package/neural-interface/public/shared/ocp-v2/ocp-v2-manager.js +7 -7
  340. package/neural-interface/public/shared/ocp-v2/ocp-v2-modelpicker.js +54 -17
  341. package/neural-interface/public/shared/ocp-v2/ocp-v2-outcome-notifications.js +16 -0
  342. package/neural-interface/public/shared/ocp-v2/ocp-v2-panel.js +140 -254
  343. package/neural-interface/public/shared/ocp-v2/ocp-v2-plan.js +1 -2
  344. package/neural-interface/public/shared/ocp-v2/ocp-v2-projectbar.js +97 -203
  345. package/neural-interface/public/shared/ocp-v2/ocp-v2-providers.js +11 -9
  346. package/neural-interface/public/shared/ocp-v2/ocp-v2-rehydrate.js +15 -4
  347. package/neural-interface/public/shared/ocp-v2/ocp-v2-render.js +7 -22
  348. package/neural-interface/public/shared/ocp-v2/ocp-v2-send-logic.js +401 -0
  349. package/neural-interface/public/shared/ocp-v2/ocp-v2-send.js +20 -13
  350. package/neural-interface/public/shared/ocp-v2/ocp-v2-session-actions.js +8 -61
  351. package/neural-interface/public/shared/ocp-v2/ocp-v2-sessions-logic.js +53 -82
  352. package/neural-interface/public/shared/ocp-v2/ocp-v2-slash-hints.js +16 -15
  353. package/neural-interface/public/shared/ocp-v2/ocp-v2-state.js +8 -21
  354. package/neural-interface/public/shared/ocp-v2/ocp-v2-status-logic.js +1 -99
  355. package/neural-interface/public/shared/ocp-v2/ocp-v2-status.js +5 -35
  356. package/neural-interface/public/shared/ocp-v2/ocp-v2-styles.js +48 -40
  357. package/neural-interface/public/shared/ocp-v2/{ocp-v2-profile-inheritance.js → ocp-v2-tools-inheritance.js} +16 -16
  358. package/neural-interface/public/shared/ocp-v2/ocp-v2-ws.js +205 -62
  359. package/neural-interface/public/shared/opencode-providers.js +39 -0
  360. package/neural-interface/public/shared/page-session.js +96 -0
  361. package/neural-interface/public/shared/registry.js +5 -3
  362. package/neural-interface/public/shared/retired-2d.js +3 -0
  363. package/neural-interface/public/shared/schedules-view.js +452 -0
  364. package/neural-interface/public/shared/settings/settings-app-pane.js +335 -0
  365. package/neural-interface/public/shared/settings/settings-app.js +243 -0
  366. package/neural-interface/public/shared/settings/settings-claude-hooks.js +135 -0
  367. package/neural-interface/public/shared/settings/settings-data.js +90 -0
  368. package/neural-interface/public/shared/settings/settings-ia.js +23 -6
  369. package/neural-interface/public/shared/settings/settings-opencode.js +89 -0
  370. package/neural-interface/public/shared/settings/settings-skill-folders-pane.js +265 -0
  371. package/neural-interface/public/shared/settings/settings-skill-folders.js +130 -0
  372. package/neural-interface/public/shared/settings/settings-worker-guard.js +72 -0
  373. package/neural-interface/public/shared/sidepanel-session-status.js +49 -0
  374. package/neural-interface/public/shared/skills/ss-args.js +96 -0
  375. package/neural-interface/public/shared/skills/ss-creation.js +219 -0
  376. package/neural-interface/public/shared/skills/ss-dialog.js +130 -0
  377. package/neural-interface/public/shared/skills/ss-doc.js +173 -0
  378. package/neural-interface/public/shared/skills/ss-eval-adapters.js +269 -0
  379. package/neural-interface/public/shared/skills/ss-eval-grade.js +359 -0
  380. package/neural-interface/public/shared/skills/ss-eval-optimise.js +153 -0
  381. package/neural-interface/public/shared/skills/ss-eval-plan.js +315 -0
  382. package/neural-interface/public/shared/skills/ss-evals.js +417 -0
  383. package/neural-interface/public/shared/skills/ss-form.js +159 -0
  384. package/neural-interface/public/shared/skills/ss-frontmatter.js +376 -0
  385. package/neural-interface/public/shared/skills/ss-ids.js +32 -0
  386. package/neural-interface/public/shared/skills/ss-lint.js +141 -0
  387. package/neural-interface/public/shared/skills/ss-presets.js +117 -0
  388. package/neural-interface/public/shared/skills/ss-profiles.js +475 -0
  389. package/neural-interface/public/shared/skills/ss-resources.js +165 -0
  390. package/neural-interface/public/shared/skills/ss-review-view.js +190 -0
  391. package/neural-interface/public/shared/skills/ss-tabs.js +79 -0
  392. package/neural-interface/public/shared/skills/ss-unicode.js +135 -0
  393. package/neural-interface/public/shared/skills/studio/sx-actions.js +222 -0
  394. package/neural-interface/public/shared/skills/studio/sx-cm.js +101 -0
  395. package/neural-interface/public/shared/skills/studio/sx-create-ai-view.js +749 -0
  396. package/neural-interface/public/shared/skills/studio/sx-create-ai.js +236 -0
  397. package/neural-interface/public/shared/skills/studio/sx-create-view.js +254 -0
  398. package/neural-interface/public/shared/skills/studio/sx-create.js +126 -0
  399. package/neural-interface/public/shared/skills/studio/sx-diff-view.js +43 -0
  400. package/neural-interface/public/shared/skills/studio/sx-diff.js +133 -0
  401. package/neural-interface/public/shared/skills/studio/sx-docs.js +480 -0
  402. package/neural-interface/public/shared/skills/studio/sx-dom.js +229 -0
  403. package/neural-interface/public/shared/skills/studio/sx-edit-view.js +848 -0
  404. package/neural-interface/public/shared/skills/studio/sx-editor-model.js +193 -0
  405. package/neural-interface/public/shared/skills/studio/sx-errors.js +22 -0
  406. package/neural-interface/public/shared/skills/studio/sx-geometry.js +82 -0
  407. package/neural-interface/public/shared/skills/studio/sx-import.js +262 -0
  408. package/neural-interface/public/shared/skills/studio/sx-inspector.js +308 -0
  409. package/neural-interface/public/shared/skills/studio/sx-install-flow.js +279 -0
  410. package/neural-interface/public/shared/skills/studio/sx-install.js +296 -0
  411. package/neural-interface/public/shared/skills/studio/sx-library-view.js +639 -0
  412. package/neural-interface/public/shared/skills/studio/sx-library.js +475 -0
  413. package/neural-interface/public/shared/skills/studio/sx-main.js +681 -0
  414. package/neural-interface/public/shared/skills/studio/sx-operations-view.js +98 -0
  415. package/neural-interface/public/shared/skills/studio/sx-operations.js +11 -0
  416. package/neural-interface/public/shared/skills/studio/sx-plan.js +158 -0
  417. package/neural-interface/public/shared/skills/studio/sx-publish-view.js +621 -0
  418. package/neural-interface/public/shared/skills/studio/sx-review-view.js +369 -0
  419. package/neural-interface/public/shared/skills/studio/sx-review.js +148 -0
  420. package/neural-interface/public/shared/skills/studio/sx-scan.js +35 -0
  421. package/neural-interface/public/shared/skills/studio/sx-snippets.js +96 -0
  422. package/neural-interface/public/shared/skills/studio/sx-subject.js +248 -0
  423. package/neural-interface/public/shared/skills/studio/sx-test-view.js +994 -0
  424. package/neural-interface/public/shared/skills/studio/sx-test.js +328 -0
  425. package/neural-interface/public/shared/skills/studio/sx-versions-view.js +241 -0
  426. package/neural-interface/public/shared/skills/studio/sx-workspace-view.js +425 -0
  427. package/neural-interface/public/shared/styleguide/sg-panel.js +4 -2
  428. package/neural-interface/public/shared/styles.css +2065 -2150
  429. package/neural-interface/public/shared/synabun-mascot.js +1115 -9
  430. package/neural-interface/public/shared/tool-list-picker.js +55 -0
  431. package/neural-interface/public/shared/tool-list-storage.js +24 -0
  432. package/neural-interface/public/shared/tool-list-view.js +112 -0
  433. package/neural-interface/public/shared/tour/tour-actor.js +268 -0
  434. package/neural-interface/public/shared/tour/tour-beats.js +336 -0
  435. package/neural-interface/public/shared/tour/tour-engine.js +884 -0
  436. package/neural-interface/public/shared/tour/tour-ink.js +390 -0
  437. package/neural-interface/public/shared/tour/tour-stage.js +440 -0
  438. package/neural-interface/public/shared/tour/tour-state.js +40 -0
  439. package/neural-interface/public/shared/tour/tour-tips.js +99 -0
  440. package/neural-interface/public/shared/tour/tour.css +144 -0
  441. package/neural-interface/public/shared/ui-assistant.js +27 -5
  442. package/neural-interface/public/shared/ui-automation-studio.js +155 -152
  443. package/neural-interface/public/shared/ui-bookmarks.js +2 -1
  444. package/neural-interface/public/shared/ui-claude-panel.js +153 -13
  445. package/neural-interface/public/shared/ui-codex-panel.js +1 -0
  446. package/neural-interface/public/shared/ui-command-runner.js +6 -3
  447. package/neural-interface/public/shared/ui-context-menu.js +295 -0
  448. package/neural-interface/public/shared/ui-cost-widget.js +57 -59
  449. package/neural-interface/public/shared/ui-crash-notice.js +187 -0
  450. package/neural-interface/public/shared/ui-detail.js +21 -9
  451. package/neural-interface/public/shared/ui-file-links.js +1 -0
  452. package/neural-interface/public/shared/ui-image-gallery.js +56 -52
  453. package/neural-interface/public/shared/ui-invite.js +18 -2
  454. package/neural-interface/public/shared/ui-keybinds.js +26 -1
  455. package/neural-interface/public/shared/ui-link.js +9 -9
  456. package/neural-interface/public/shared/ui-menubar.js +229 -40
  457. package/neural-interface/public/shared/ui-native-loop-router-state.js +5 -0
  458. package/neural-interface/public/shared/ui-native-loop-router.js +9 -1
  459. package/neural-interface/public/shared/ui-navbar.js +4 -24
  460. package/neural-interface/public/shared/ui-opencode-panel-v2.js +1 -0
  461. package/neural-interface/public/shared/ui-panels.js +96 -65
  462. package/neural-interface/public/shared/ui-schedules-studio.js +2267 -953
  463. package/neural-interface/public/shared/ui-sessions.js +20 -7
  464. package/neural-interface/public/shared/ui-settings.js +679 -496
  465. package/neural-interface/public/shared/ui-sidebar.js +4 -4
  466. package/neural-interface/public/shared/ui-sidepanel-session-entry.js +13 -0
  467. package/neural-interface/public/shared/ui-sidepanel-sessions.js +56 -5
  468. package/neural-interface/public/shared/ui-sidepanel-state.js +23 -12
  469. package/neural-interface/public/shared/ui-sidepanel-tray.js +84 -8
  470. package/neural-interface/public/shared/ui-sidepanel-windows.js +16 -11
  471. package/neural-interface/public/shared/ui-skills.js +42 -2781
  472. package/neural-interface/public/shared/ui-styleguide.js +5 -2
  473. package/neural-interface/public/shared/ui-sync.js +22 -5
  474. package/neural-interface/public/shared/ui-terminal.js +159 -199
  475. package/neural-interface/public/shared/ui-tool-updates.js +3 -1
  476. package/neural-interface/public/shared/ui-tooltip.js +10 -5
  477. package/neural-interface/public/shared/ui-trash.js +14 -5
  478. package/neural-interface/public/shared/ui-tutorial-layout.js +3 -117
  479. package/neural-interface/public/shared/ui-tutorial-preview.js +103 -0
  480. package/neural-interface/public/shared/ui-tutorial-steps.js +3 -61
  481. package/neural-interface/public/shared/ui-tutorial.css +47 -100
  482. package/neural-interface/public/shared/ui-tutorial.js +285 -499
  483. package/neural-interface/public/shared/ui-update.js +121 -16
  484. package/neural-interface/public/shared/ui-whiteboard.js +313 -59
  485. package/neural-interface/public/shared/ui-worker-end.js +4 -0
  486. package/neural-interface/public/shared/ui-workspaces.js +3 -2
  487. package/neural-interface/public/shared/view-variant.js +66 -0
  488. package/neural-interface/public/shared/whatsapp/wa-view.js +1 -1
  489. package/neural-interface/public/shared/whiteboard-list.js +172 -0
  490. package/neural-interface/public/shared/worker-end.js +10 -3
  491. package/neural-interface/public/shared/worker-restart.js +12 -0
  492. package/neural-interface/public/sidepanel-session.html +1 -1
  493. package/neural-interface/public/variant/2d/background.js +2 -1
  494. package/neural-interface/public/variant/2d/context-menu.js +8 -2
  495. package/neural-interface/public/variant/2d/graph.js +42 -20
  496. package/neural-interface/public/variant/2d/hulls.js +14 -9
  497. package/neural-interface/public/variant/2d/lasso.js +11 -7
  498. package/neural-interface/public/variant/2d/main.js +36 -2
  499. package/neural-interface/public/variant/2d/minimap.js +11 -13
  500. package/neural-interface/public/variant/3d/main.js +72 -6
  501. package/neural-interface/public/variant/3d/map-controls.js +24 -9
  502. package/neural-interface/public/vendor/caveat/OFL.txt +93 -0
  503. package/neural-interface/public/vendor/caveat/caveat-latin-400-normal.woff2 +0 -0
  504. package/neural-interface/public/vendor/caveat/caveat-latin-700-normal.woff2 +0 -0
  505. package/neural-interface/public/vendor/caveat/caveat.css +17 -0
  506. package/neural-interface/public/vendor/caveat/manifest.json +12 -0
  507. package/neural-interface/public/vendor/highlight/LICENSE +29 -0
  508. package/neural-interface/public/vendor/highlight/github-dark-dimmed-11.12.0.min.css +9 -0
  509. package/neural-interface/public/vendor/highlight/highlight-11.12.0.min.js +1262 -0
  510. package/neural-interface/public/vendor/highlight/manifest.json +5 -0
  511. package/neural-interface/public/vendor/html2canvas/LICENSE +22 -0
  512. package/neural-interface/public/vendor/html2canvas/html2canvas-1.4.1.min.js +20 -0
  513. package/neural-interface/public/vendor/html2canvas/manifest.json +4 -0
  514. package/neural-interface/public/vendor/marked/LICENSE.md +44 -0
  515. package/neural-interface/public/vendor/marked/manifest.json +4 -0
  516. package/neural-interface/public/vendor/marked/marked-14.1.4.esm.js +2502 -0
  517. package/neural-interface/public/vendor/mermaid/LICENSE +21 -0
  518. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/abnfDiagram-PAR5IXJI.mjs +1 -0
  519. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/architecture-7GRP2DOG-M7G3SWCO.mjs +1 -0
  520. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/architectureDiagram-2PNWDENW.mjs +36 -0
  521. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/blockDiagram-PUWGUBOA.mjs +129 -0
  522. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/c4Diagram-3NLNHZP5.mjs +38 -0
  523. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-2NMSUPJH.mjs +1 -0
  524. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-32MCUW2P.mjs +206 -0
  525. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-3NPNU5GP.mjs +1 -0
  526. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-3SMGFRGC.mjs +1 -0
  527. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-4CAJECRU.mjs +14 -0
  528. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-4OADUZ5Q.mjs +10 -0
  529. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-5IMINLNL.mjs +1 -0
  530. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-5IQCK5RY.mjs +3 -0
  531. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-5MVFYWFU.mjs +1 -0
  532. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-AQ6EADP3.mjs +1 -0
  533. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-AZZRMDJM.mjs +15 -0
  534. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-CLS4B6BI.mjs +168 -0
  535. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-DZP67EKU.mjs +70 -0
  536. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-EICP7K5X.mjs +1 -0
  537. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-GDY2UAB4.mjs +1 -0
  538. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-H7VHZCWX.mjs +1 -0
  539. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-IJBDOHL6.mjs +1 -0
  540. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-JFFVMJIA.mjs +1 -0
  541. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-JQRUD6KW.mjs +1 -0
  542. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-JZGDI5JK.mjs +1 -0
  543. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-KD6SPIPN.mjs +2 -0
  544. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-NF366YI6.mjs +231 -0
  545. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-NUDPKBEF.mjs +32 -0
  546. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-OJINMHTS.mjs +1 -0
  547. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-Q6NGPIAH.mjs +2 -0
  548. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-QJSWEUOL.mjs +132 -0
  549. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-R7JDMUKV.mjs +1 -0
  550. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-RCXAM2GK.mjs +1 -0
  551. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-RT4MAWR3.mjs +1 -0
  552. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-SFMINX4W.mjs +1 -0
  553. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-SLDQ463B.mjs +88 -0
  554. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-STOV2HOB.mjs +1 -0
  555. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-T5OCTHI4.mjs +1 -0
  556. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-TCEHX7R3.mjs +1 -0
  557. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-VQC2EELJ.mjs +1 -0
  558. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-VRZKL3EH.mjs +1 -0
  559. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-VTHDQ7BV.mjs +1 -0
  560. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-VY2OSTJR.mjs +2 -0
  561. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-WUBWJARI.mjs +1 -0
  562. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-YBMQ5JGJ.mjs +2 -0
  563. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-YDMLL4PJ.mjs +1 -0
  564. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-Z6V44ARX.mjs +62 -0
  565. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-ZJJ7VQJP.mjs +339 -0
  566. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-ZNLK7HRR.mjs +1 -0
  567. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-ZQN7UHIL.mjs +1 -0
  568. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-ZS6VBONO.mjs +174 -0
  569. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-ZUNWM646.mjs +1 -0
  570. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/classDiagram-FGAMPOII.mjs +1 -0
  571. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/classDiagram-v2-AI5RJEK4.mjs +1 -0
  572. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/cose-bilkent-Q72C2OJ3.mjs +1 -0
  573. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/cynefin-OW5HDTMX-6OCTWOGG.mjs +1 -0
  574. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/cynefinDiagram-2OOWUY4M.mjs +62 -0
  575. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/dagre-MPVFI544.mjs +4 -0
  576. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/diagram-AE4NWXFW.mjs +24 -0
  577. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/diagram-BUJID5UG.mjs +41 -0
  578. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/diagram-C44CZ5NI.mjs +24 -0
  579. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/diagram-EYOLEXXS.mjs +30 -0
  580. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/diagram-P7B6PWOJ.mjs +3 -0
  581. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/ebnfDiagram-FTPZ3PV6.mjs +1 -0
  582. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/erDiagram-H3ERR26V.mjs +99 -0
  583. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/eventmodeling-NTZA5JFV-WKSWQHFS.mjs +1 -0
  584. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/flowDiagram-YHGXBVSY.mjs +1 -0
  585. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/ganttDiagram-RWFTB7ED.mjs +292 -0
  586. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/gitGraph-4MIJSDKK-JH2HKYUO.mjs +1 -0
  587. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/gitGraphDiagram-MAR6GKOP.mjs +106 -0
  588. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/info-A6RAGUB7-3TX3OFWW.mjs +1 -0
  589. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/infoDiagram-LV547AFH.mjs +2 -0
  590. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/ishikawaDiagram-PUJQGNDS.mjs +70 -0
  591. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/journeyDiagram-EW5QJT4W.mjs +139 -0
  592. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/kanban-definition-JSQERAZB.mjs +89 -0
  593. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/katex-CZ4GXH2S.mjs +257 -0
  594. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/mindmap-definition-VBJCLQLM.mjs +96 -0
  595. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/packet-AYTQ26CC-MKII6P3Y.mjs +1 -0
  596. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/pegDiagram-7WILOQTJ.mjs +1 -0
  597. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/pie-WAS4IAKB-6B4OPMJ5.mjs +1 -0
  598. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/pieDiagram-EFA7CBSB.mjs +39 -0
  599. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/quadrantDiagram-WARVYA7P.mjs +7 -0
  600. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/radar-RG4KPBEZ-UPTR3XUL.mjs +1 -0
  601. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/railroad-74A4TZTK-MEFUPFMS.mjs +1 -0
  602. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/railroad-abnf-HS5TGJTU-R5YG2S5D.mjs +1 -0
  603. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/railroad-ebnf-LZEXJU2U-G5QEX2L6.mjs +1 -0
  604. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/railroad-peg-WCYAUIDC-H5I2ZRY2.mjs +1 -0
  605. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/railroadDiagram-5NIQVNLQ.mjs +1 -0
  606. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/requirementDiagram-SXREGOKZ.mjs +84 -0
  607. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/sankeyDiagram-VV74M2VP.mjs +40 -0
  608. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/sequenceDiagram-52ZFIFDL.mjs +162 -0
  609. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/sizeCapture-FGA5AGYP.mjs +1 -0
  610. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/stateDiagram-3QAKPIVI.mjs +1 -0
  611. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/stateDiagram-v2-HN2P4RDZ.mjs +1 -0
  612. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/swimlanes-QLVP42QD.mjs +1 -0
  613. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/swimlanesDiagram-JKPDBFCN.mjs +8 -0
  614. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/timeline-definition-GS45XAXW.mjs +120 -0
  615. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/treeView-Q6P3EWNA-ZRJGANNQ.mjs +1 -0
  616. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/treemap-WGGIJYW6-K64VN5AS.mjs +1 -0
  617. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/vennDiagram-QIQU2ZRJ.mjs +34 -0
  618. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/wardley-WFR3VGLG-D67OWCCH.mjs +1 -0
  619. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/wardleyDiagram-U6YNM66N.mjs +78 -0
  620. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/xychartDiagram-XJAZEDNM.mjs +7 -0
  621. package/neural-interface/public/vendor/mermaid/manifest.json +4 -0
  622. package/neural-interface/public/vendor/mermaid/mermaid-11.17.2.esm.min.mjs +14 -0
  623. package/neural-interface/public/vendor/yaml/LICENSE +13 -0
  624. package/neural-interface/public/vendor/yaml/manifest.json +4 -0
  625. package/neural-interface/public/vendor/yaml/yaml-2.9.1.min.js +137 -0
  626. package/neural-interface/scripts/build-vendor.mjs +116 -3
  627. package/neural-interface/server.js +4834 -2541
  628. package/neural-interface/templates/rulesets/core.md +1 -1
  629. package/neural-interface/templates/rulesets/legacy-hashes.json +18 -0
  630. package/neural-interface/templates/rulesets/manifest.json +8 -8
  631. package/npm-shrinkwrap.json +18 -12
  632. package/package.json +16 -3
  633. package/postinstall.js +9 -4
  634. package/preuninstall.js +1 -1
  635. package/setup.js +97 -322
  636. package/skills/skill-forge/SKILL.md +177 -0
  637. package/skills/skill-forge/evals/evals.json +54 -0
  638. package/skills/skill-forge/references/package.md +105 -0
  639. package/skills/skill-forge/references/review-and-recovery.md +63 -0
  640. package/skills/skill-forge/references/runtime-profiles.md +20 -0
  641. package/skills/skill-forge/references/sources-and-capabilities.md +51 -0
  642. package/skills/synabun/SKILL.md +10 -7
  643. package/skills/synabun/modules/audit.md +1 -5
  644. package/skills/synabun/modules/changelog.md +1 -5
  645. package/skills/synabun/modules/idea.md +1 -1
  646. package/skills/synabun/modules/memorize.md +6 -9
  647. package/skills/synabun/modules/schedule.md +5 -5
  648. package/mcp-server/dist/services/profiles.d.ts +0 -120
  649. package/mcp-server/dist/services/profiles.d.ts.map +0 -1
  650. package/mcp-server/dist/services/profiles.js +0 -387
  651. package/mcp-server/dist/services/profiles.js.map +0 -1
  652. package/mcp-server/dist/tools/leonardo-browser-tools.d.ts +0 -97
  653. package/mcp-server/dist/tools/leonardo-browser-tools.d.ts.map +0 -1
  654. package/mcp-server/dist/tools/leonardo-browser-tools.js +0 -228
  655. package/mcp-server/dist/tools/leonardo-browser-tools.js.map +0 -1
  656. package/mcp-server/dist/tools/leonardo.d.ts +0 -13
  657. package/mcp-server/dist/tools/leonardo.d.ts.map +0 -1
  658. package/mcp-server/dist/tools/leonardo.js +0 -21
  659. package/mcp-server/dist/tools/leonardo.js.map +0 -1
  660. package/mcp-server/dist/tools/profile.d.ts +0 -17
  661. package/mcp-server/dist/tools/profile.d.ts.map +0 -1
  662. package/mcp-server/dist/tools/profile.js +0 -114
  663. package/mcp-server/dist/tools/profile.js.map +0 -1
  664. package/mcp-server/src/services/profiles.ts +0 -428
  665. package/mcp-server/src/tools/leonardo-browser-tools.ts +0 -291
  666. package/mcp-server/src/tools/leonardo.ts +0 -30
  667. package/mcp-server/src/tools/profile.ts +0 -122
  668. package/neural-interface/lib/assistant-brains/opencode-browser-policy.js +0 -47
  669. package/neural-interface/lib/assistant-brains/opencode-route-gate.js +0 -62
  670. package/neural-interface/lib/mcp-profile-servers.js +0 -120
  671. package/neural-interface/public/shared/ui-tutorial-draw.js +0 -311
  672. package/skills/leonardo/SKILL.md +0 -168
  673. package/skills/leonardo/icon.svg +0 -1
  674. package/skills/leonardo/modules/browser-guide.md +0 -378
  675. package/skills/leonardo/modules/image-prompter.md +0 -283
  676. package/skills/leonardo/modules/model-advisor.md +0 -100
  677. package/skills/leonardo/modules/prompt-library.md +0 -110
  678. package/skills/leonardo/modules/style-guide.md +0 -186
  679. package/skills/leonardo/modules/video-prompter.md +0 -317
@@ -17,25 +17,60 @@
17
17
  // out. mascot() names the character on screen for the panel's director
18
18
  // (asst-mascot.js), which may hold its pose; stills are painted SVGs, never
19
19
  // rigs.
20
-
21
- import { assistantEventKey, fmtCost, fmtElapsed, isRemovableRun, isRunIdle, isTerminalRunStatus, modelShortName, providerShortLabel, routeTargetLabel, runTone, summarizeMailbox } from './asst-state.js';
20
+ // A dispatched run's card opens a stage while its worker works
21
+ // (asst-run-stage.js): the run's own mascot and scene, independent of the
22
+ // Assistant's turn, so a worker that outlives the turn keeps moving. Run
23
+ // cards are not journaled: a reload restores them from the runs the agents
24
+ // tray lists (restoreRuns), next to the call that dispatched them.
25
+ // The Assistant's own work on a direct route (an approved "do it here" route
26
+ // of a task class: research, browser, quick…) uses the same scenes: its
27
+ // racks' stages show the class's scene under the mascot, and the scene the
28
+ // bubble's call implies (a browser tool the browser, a web search research),
29
+ // for the turn the route runs in (noteDirectRoute, paintRackScene). A
30
+ // conversation opened again learns its route from the journal like any other
31
+ // event (no rack of the history moves: none is live) and lends it to the
32
+ // resumed work once its turn runs (lendRouteScene).
33
+ // What a turn leaves for the user to click sits under the row it belongs to:
34
+ // the follow-ups a message offers (its ```synabun-followups block, or best
35
+ // effort its prose: asst-followups.js) and, under an error that is a usage
36
+ // limit, the waiting card with its countdown (asst-limit-wait.js); a limited
37
+ // run's card shows the same waiting card. The user's next prompt answers
38
+ // them or leaves them stale, and so does a new turn (settleOffers). A card
39
+ // that asks and is still open is lifted to the transcript's bottom while it
40
+ // waits (asst-ask-tail.js), so text and tool output streaming in after it
41
+ // never bury it; once answered it goes back under its row.
42
+ // A follow-ups card and the question cards the panel appends (appendNode with
43
+ // an `_ask`) are presented by the mascot (asst-ask.js): the newest one still
44
+ // open is the character on screen, its face a live rig (liveAsk,
45
+ // activateRigs); every other card's face is a painted still.
46
+
47
+ import { assistantEventKey, fmtCost, fmtElapsed, isRemovableRun, isTerminalRunStatus, modelShortName, providerShortLabel, routeTargetLabel, runTone, summarizeMailbox } from './asst-state.js';
22
48
  import { createRouteLine } from './asst-route.js';
23
49
  import { closeMenu, isMenuOpen } from './asst-menu.js';
24
50
  import { computerToolName, describeComputerAction, frameUrls, markerPercent, openFrameLightbox, parseComputerResult, swapImage } from './asst-computer.js';
25
51
  import * as mascotLib from '../synabun-mascot.js';
26
52
  import { isReducedMotion, subscribe } from '../synabun-ticker.js';
27
- import { KIND_SPECS, callOutcome, commandOf, editDiff, editPaths, fmtDuration, groupSubject, kindOf, maskEcho, mcpServerOf, middleEllipsis, planState, plural, prettyServer, receiptSentence, redactInput, resultDetail, sayCall, shouldFoldNarration, stationAggregate, stationKey, targetKey, toolWords, verbText } from './asst-tool-kinds.js';
53
+ import { KIND_SPECS, callOutcome, commandOf, editDiff, editPaths, fmtDuration, groupSubject, headEllipsis, kindOf, maskEcho, mcpServerOf, middleEllipsis, planState, plural, prettyServer, receiptSentence, redactInput, resultDetail, sayCall, shouldFoldNarration, stationAggregate, stationKey, targetKey, toolWords, verbText, wrapRuns } from './asst-tool-kinds.js';
28
54
  import { injectRackStyles } from './asst-rack-styles.js';
55
+ import { injectSceneStyles } from './asst-scene-styles.js';
56
+ import { activityScene, compactActivity, isLivePhase, runChip, sceneOfClass, sceneSvg, stagePhase } from './asst-scenes.js';
57
+ import { STAGE_TIMING, createWorkerStage } from './asst-run-stage.js';
29
58
  import { formatRunTokens } from './asst-usage.js';
30
59
  import { sanitizeHtmlString, sanitizeInto } from './asst-sanitize.js';
31
60
  import { automationView } from './asst-automation.js';
32
-
33
- // ── Markdown (lazy marked, same CDN build as the sidepanels) ───────────────
61
+ import { guessFollowups, splitFollowups } from './asst-followups.js';
62
+ import { createFollowupsCard } from './asst-followups-card.js';
63
+ import { createAskTail } from './asst-ask-tail.js';
64
+ import { limitFromText } from './asst-limit.js';
65
+ import { createLimitDock } from './asst-limit-dock.js';
66
+ import { createLimitWait } from './asst-limit-wait.js';
67
+
68
+ // ── Markdown (lazy marked, the vendored build the sidepanels use too) ───────────────
34
69
  let _marked = null;
35
70
  let _markedLoading = null;
36
71
  function loadMarked() {
37
72
  if (_marked || _markedLoading) return _markedLoading;
38
- _markedLoading = import('https://cdn.jsdelivr.net/npm/marked@14/lib/marked.esm.js')
73
+ _markedLoading = import('/vendor/marked/marked-14.1.4.esm.js')
39
74
  .then((m) => {
40
75
  _marked = m?.marked || m?.default || null;
41
76
  if (_marked?.setOptions) _marked.setOptions({ breaks: true, gfm: true });
@@ -126,10 +161,6 @@ function memoryTitle(memory) {
126
161
 
127
162
  // ── Run cards (shared with the agents dock) ────────────────────────────────
128
163
 
129
- function runWaitingForRoute(run) {
130
- return run?.state === 'awaiting_route' || run?.turnState === 'awaiting_route' || run?.route?.status === 'pending';
131
- }
132
-
133
164
  /**
134
165
  * buildRunCard(run, { t, providerIcon, providerColor, onFocus, onStop, onRead, onEscalate, compact, result, index })
135
166
  * Returns a card element; call updateRunCard(el, run, opts) to refresh it in place.
@@ -191,20 +222,13 @@ export function updateRunCard(card, run, opts = {}) {
191
222
  const title = card.querySelector('.asst-run-title');
192
223
  const label = run.title || run.task || run.runId || '';
193
224
  if (title) { title.textContent = label; title.title = label; }
194
- const waitingRoute = runWaitingForRoute(run);
195
225
  const pill = card.querySelector('.asst-pill');
196
226
  if (pill) {
197
- const tone = waitingRoute ? 'action' : runTone(run);
198
- pill.dataset.tone = tone;
199
- (pill.querySelector('.asst-pill-label') || pill).textContent = waitingRoute ? t('assistant.run.awaitingRoute', 'waiting for your model choice')
200
- : run.turnState === 'awaiting_permission' ? t('assistant.run.needsApproval', 'needs approval')
201
- : run.turnState === 'needs_input' ? t('assistant.run.needsInput', 'needs input')
202
- : run.queued && run.status !== 'running' ? t('assistant.run.queued', 'queued')
203
- // A warm worker between turns reports what its last turn ended with, not "running".
204
- : isRunIdle(run) ? (run.outcome || run.lastResult?.status
205
- ? t('assistant.run.idleWith', '{outcome} · idle', { outcome: run.outcome || run.lastResult.status })
206
- : t('assistant.run.idle', 'idle'))
207
- : (run.status || tone);
227
+ // What it waits on (a route card, your answer: the stage's own words) > its approval > queued > what a warm
228
+ // worker's last turn ended with > its status (asst-scenes.js runChip).
229
+ const chip = runChip(run, t);
230
+ pill.dataset.tone = chip.tone;
231
+ (pill.querySelector('.asst-pill-label') || pill).textContent = chip.label;
208
232
  }
209
233
  const meta = card.querySelector('.asst-run-meta');
210
234
  if (meta) {
@@ -254,12 +278,15 @@ export function updateRunCard(card, run, opts = {}) {
254
278
  const readBtn = card.querySelector('[data-action="read"]');
255
279
  if (readBtn) readBtn.disabled = run.status === 'queued';
256
280
 
281
+ // A provider's usage limit stopped it: the waiting card, whose Retry now stands in for "Retry on" the same model.
282
+ const limitShown = syncRunLimit(card, run, opts, t);
283
+
257
284
  // Escalation offer ("Escalate to ‹label›", or "Retry on ‹label›" after a
258
285
  // temporary failure) when the server proposes one; a note without a button
259
286
  // when a stronger model would hit the same wall (access, the user's input).
260
287
  let esc8 = card.querySelector('.asst-run-escalate');
261
288
  const escalationRaw = run.escalation && typeof run.escalation === 'object' ? run.escalation : null;
262
- const escalation = escalationRaw?.to ? escalationRaw : null;
289
+ const escalation = escalationRaw?.to && !(limitShown && escalationRaw.kind === 'retry') ? escalationRaw : null;
263
290
  const needsNote = escalationRaw && escalationRaw.kind === 'none' ? escalationRaw : null;
264
291
  const onEscalate = opts.onEscalate || card._runOpts?.onEscalate;
265
292
  if (esc8 && esc8.dataset.kind !== (needsNote ? 'note' : 'offer')) { esc8.remove(); esc8 = null; card._escalation = null; }
@@ -317,6 +344,49 @@ export function updateRunCard(card, run, opts = {}) {
317
344
  updateRunMedia(card, run, { t, lightboxT: opts.t || card._runOpts?.t });
318
345
  }
319
346
 
347
+ /** What Retry now runs a limited run on: the server's same-model retry, else the run's own provider and model. */
348
+ function limitRetryTarget(run) {
349
+ const esc = run?.escalation && typeof run.escalation === 'object' ? run.escalation : null;
350
+ if (esc?.kind === 'retry' && esc.to?.provider) return esc.to;
351
+ return { provider: run.provider || 'claude-code', model: run.model || null, effort: run.effort || null, accountId: run.accountId || null };
352
+ }
353
+
354
+ /**
355
+ * A run a provider's usage limit stopped (run.limit, lib/assistant-dispatch.js): the waiting card
356
+ * is registered with the conversation's single bottom dock; a standalone card can own its wait.
357
+ * Retry now runs it again on the same model. Never while running or in the compact agents tray.
358
+ */
359
+ function syncRunLimit(card, run, opts, t) {
360
+ const limit = run.limit && typeof run.limit === 'object' ? run.limit : null;
361
+ const tone = runTone(run);
362
+ const show = !opts.compact && !!limit && tone !== 'running' && tone !== 'queued' && !run.removed;
363
+ let wait = card._limitWait || null;
364
+ if (!show) {
365
+ (opts.onLimitWait || card._runOpts?.onLimitWait)?.(run, null);
366
+ if (wait) { wait.destroy(); wait.el.remove(); card._limitWait = null; }
367
+ return false;
368
+ }
369
+ const shared = opts.onLimitWait || card._runOpts?.onLimitWait;
370
+ if (shared) { shared(run, show && !limit.retriedAt && !run.escalation?.started ? limit : null); return show; }
371
+ if (!wait || !wait.el.isConnected) {
372
+ const onEscalate = opts.onEscalate || card._runOpts?.onEscalate;
373
+ wait = createLimitWait({
374
+ t,
375
+ resetsAt: limit.resetsAt || null,
376
+ since: Date.parse(limit.detectedAt || run.completedAt || '') || Date.now(),
377
+ provider: providerShortLabel(run.provider || 'claude-code'),
378
+ onRetry: typeof onEscalate === 'function' ? () => onEscalate(card.dataset.runId, limitRetryTarget(card._usageRun || run)) : null,
379
+ });
380
+ card._limitWait = wait;
381
+ const meta = card.querySelector('.asst-run-meta');
382
+ if (meta) meta.after(wait.el); else card.appendChild(wait.el);
383
+ } else if (limit.resetsAt) {
384
+ wait.update({ resetsAt: limit.resetsAt });
385
+ }
386
+ if (limit.retriedAt || run.escalation?.started) wait.retried();
387
+ return true;
388
+ }
389
+
320
390
  const MAX_RUN_MEDIA = 8;
321
391
 
322
392
  /**
@@ -419,7 +489,7 @@ const PREVIEW_LINES = 5;
419
489
  const THINK_PREVIEW_LINES = 10;
420
490
  const MAX_DIFF_LINES = 400; // an edit's mini diff draws this many lines; "Show full input" draws the rest
421
491
  // The empty state's suggestions (asst-panel.js ids) and the kind each one is about.
422
- const SUGGESTION_KINDS = { computer: 'computer', web: 'web', fix: 'edit', schedule: 'plan', remember: 'memory' };
492
+ const SUGGESTION_KINDS = { computer: 'computer', web: 'web', fix: 'edit', skill: 'skill', schedule: 'plan', remember: 'memory' };
423
493
 
424
494
  /**
425
495
  * createRenderer(transcriptEl, hooks)
@@ -428,18 +498,29 @@ const SUGGESTION_KINDS = { computer: 'computer', web: 'web', fix: 'edit', schedu
428
498
  * emptySuggestions() → [{ id, label, template }], onSuggestion(template),
429
499
  * brainLabel() → string, onChangeRoutes(), onRouteEvent(ev) → bool (true = handled by an open card),
430
500
  * heroOrigin() → DOMRect | null (where the empty state's character flies in from: the host's placeholder),
431
- * onMascotChange() (the character on screen changed: mascot()) }
501
+ * onMascotChange() (the character on screen changed: mascot()),
502
+ * onFollowup(text, { set, indices }) (a follow-up was clicked: send `text` as the user's message),
503
+ * onFollowupOther() ("Something else…": the composer), limitInfo() → the session's limit notice
504
+ * ({ status, resetsAt } | null), brainProvider() → the brain's provider id, onLimitRetry(text)
505
+ * (Retry now on the Assistant's own limit card: send `text`) }
432
506
  */
433
507
  export function createRenderer(transcriptEl, hooks = {}) {
434
508
  loadMarked();
435
509
  injectRackStyles();
510
+ injectSceneStyles();
436
511
  const $msgs = transcriptEl;
437
512
  const scopes = new Map(); // parent_tool_use_id → { container, stream, key }
438
513
  const runCards = new Map(); // runId → card element
514
+ const stages = new Map(); // runId → its card's worker stage (asst-run-stage.js)
515
+ const runActivity = new Map(); // runId → what its worker does now, compact ({ kind, name, step, at }), while the run lasts
516
+ let unsubStageTick = null;
439
517
  const routeLines = new Map(); // routeId → line handle
440
518
  const autoRows = new Map(); // traceId → the turn's automation trace row
441
519
  let working = null;
442
520
  let workingFace = null; // the "Thinking" row's character: { still, rig?, el, target? } (syncWorkingFace)
521
+ // The open card's slot under every row (asst-ask-tail.js): a card that waits on the user is drawn
522
+ // there, above the "Working…" row, until it is answered; rows appended meanwhile go above it.
523
+ const askTail = createAskTail($msgs, { anchor: () => (working?.parentNode === $msgs ? working : null) });
443
524
  let followTail = true;
444
525
  let emptyEl = null;
445
526
  let hero = null; // the empty state's character: { rig, el, box, parked, leaving, held, dock, egg, target }
@@ -451,6 +532,25 @@ export function createRenderer(transcriptEl, hooks = {}) {
451
532
  let turn = { calls: 0, last: null, errored: false, answerRow: null }; // what the turn did: the mascot's verdict, the announced answer
452
533
  let grace = null; // the stage on screen when the result arrived: it waits for the turn's verdict (finishTurn)
453
534
  let waiting = null; // a card waits on the user: { id: the tool_use id it holds or null, since } (setWaiting)
535
+ // An approved direct route (synabun.route auto / decided, target.kind 'direct', a class with a scene) lends its
536
+ // class's scene to the racks of the turn it runs in (noteDirectRoute): { routeId, taskClass, turn, rackAt } with
537
+ // `turn` the turnSeq it belongs to, null until a turn takes it (a pick made while no turn runs, or a route the
538
+ // journal's last turn was still running), and `rackAt` the last rack made before it (its racks come after).
539
+ // A route the journal records (`journal`) belongs to the journal's last turn only: the turn the reattach finds
540
+ // running takes it; with none running, that turn is over (it ended with no result) and nothing later takes it.
541
+ let directRoute = null;
542
+ let lastDirect = null; // the last one approved since the user's prompt: a "do it here" continuation turn takes it again
543
+ let turnSeq = 0; // turns this renderer has seen start (showWorking)
544
+ let reattachRunning = null; // what the last reattach said: a turn runs (true), none (false), not asked yet (null)
545
+ // What a turn left for the user to click: follow-up cards under its messages (asst-followups-card.js).
546
+ // The user's next prompt answers them or they go
547
+ // stale, and so does a new turn; live and from the journal alike, so a reload draws the same states.
548
+ const offers = new Set(); // { el, set, settle(text) → answered, stale() }
549
+ // The cards the mascot presents (asst-ask.js): follow-ups cards and the question cards the panel appends.
550
+ // The newest one still open is the character on screen (above any stage); see liveAsk / activateRigs.
551
+ const askCards = new Set(); // { el, presenter, isOpen(), onChange, seq }
552
+ let askSeq = 0;
553
+ let lastTextRow = null; // the newest root message since the user's last prompt: the one a guess reads
454
554
 
455
555
  // ── Activity racks ──
456
556
  // Every burst of agent work (tool calls, thinking, short narration) is one
@@ -463,6 +563,27 @@ export function createRenderer(transcriptEl, hooks = {}) {
463
563
  let rackSeq = 0;
464
564
  let stepSeq = 0;
465
565
  let unsubTick = null;
566
+ // A staged rack's bubble is cut to its stage's width (subjectBudget, fitSubject): when that width
567
+ // changes, the bubble is cut again once it has held still (one pass when a drag ends, never one per
568
+ // frame of it), instead of on the next tick. Heights are not watched.
569
+ const RECUT_SETTLE_MS = 120;
570
+ const stageWidths = new WeakMap(); // stage element → { rack, width }
571
+ const toRecut = new Set();
572
+ let recutTimer = 0;
573
+ const stageResizes = typeof ResizeObserver === 'function' ? new ResizeObserver((entries) => {
574
+ for (const entry of entries) {
575
+ const seen = stageWidths.get(entry.target);
576
+ if (!seen) continue;
577
+ const width = Math.round(entry.contentRect.width);
578
+ if (seen.width === null) { seen.width = width; continue; } // the first report: the width it was cut at
579
+ if (width === seen.width) continue;
580
+ seen.width = width;
581
+ toRecut.add(seen.rack);
582
+ }
583
+ if (!toRecut.size) return;
584
+ clearTimeout(recutTimer);
585
+ recutTimer = setTimeout(recutBubbles, RECUT_SETTLE_MS);
586
+ }) : null;
466
587
  let muted = false; // inside handleEvent(…, { silent: true }): no motion, no announcements
467
588
  let replayDraw = false; // inside handleEvent(…, { replay: true }): a reattach's missed packets (drawn like the journal)
468
589
  let clockAt = null; // the journal `at` of the entry being replayed
@@ -499,6 +620,7 @@ export function createRenderer(transcriptEl, hooks = {}) {
499
620
  if (step.ghost && step.type === 'call' && !Object.keys(step.input || {}).length) { say.text = '…'; say.detail = '…'; say.segments = null; say.mono = false; say.meta = []; }
500
621
  return say;
501
622
  };
623
+ const limitDock = createLimitDock($msgs, { t });
502
624
  const nowMs = () => (clockAt ?? Date.now());
503
625
  const isTimed = () => !muted || clockAt != null;
504
626
  const locale = () => (typeof document !== 'undefined' && document.documentElement?.lang) || 'en';
@@ -699,12 +821,46 @@ export function createRenderer(transcriptEl, hooks = {}) {
699
821
  return best;
700
822
  }
701
823
 
702
- /** The mascot on screen: the hero, else the newest stage, else the "Thinking" row's, else none. */
824
+ /** The mascot on screen: the hero, else the newest open card's, else the newest stage, else the "Thinking" row's, else none. */
703
825
  function visibleMascot() {
704
826
  if (hero?.rig) return hero.target;
827
+ const ask = liveAsk();
828
+ if (ask?.presenter?.isLive?.()) return ask.presenter.target;
705
829
  return newestStage()?.target || workingFace?.target || null;
706
830
  }
707
831
 
832
+ // ── Cards the mascot presents ──
833
+ /**
834
+ * A follow-ups card or a question card (its `_ask`): its face may be the character on screen while it is
835
+ * open. Every ask — a route card too, which has no face — waits at the transcript's bottom until answered.
836
+ */
837
+ function registerAsk(handle) {
838
+ if (!handle?.el || askCards.has(handle)) return;
839
+ handle.seq = (askSeq += 1);
840
+ handle.onChange = () => { askTail.sync(); mascotChanged(); };
841
+ askCards.add(handle);
842
+ askTail.pin(handle);
843
+ mascotChanged();
844
+ }
845
+
846
+ /** The newest card with a face that is still open (answers can still be picked): the one whose face is live. */
847
+ function liveAsk() {
848
+ let best = null;
849
+ for (const handle of [...askCards]) {
850
+ if (!handle.el?.isConnected) { askCards.delete(handle); handle.presenter?.destroy?.(); continue; }
851
+ // A route card asks, but has no face of its own: it waits at the bottom without taking the character.
852
+ if (!handle.presenter || !handle.isOpen() || (best && best.seq > handle.seq)) continue;
853
+ best = handle;
854
+ }
855
+ return best;
856
+ }
857
+
858
+ function dropAsks() {
859
+ for (const handle of askCards) { try { handle.presenter?.destroy?.(); } catch { /* gone */ } }
860
+ askCards.clear();
861
+ askTail.clear();
862
+ }
863
+
708
864
  function mascotChanged() {
709
865
  activateRigs();
710
866
  try { hooks.onMascotChange?.(); } catch { /* the director is gone */ }
@@ -715,12 +871,12 @@ export function createRenderer(transcriptEl, hooks = {}) {
715
871
  while (rows.length > MAX_MESSAGES && rows[0] !== working) rows[0].remove();
716
872
  }
717
873
 
718
- /** Insert a row before "Working…" so the indicator stays the last row. */
874
+ /** Insert a row before the open cards (if any) and before "Working…", so the indicator stays the last row. */
719
875
  function append(node, scope = rootScope, { park = false } = {}) {
720
876
  hideEmpty({ park });
721
- if (scope.container === $msgs && working && working.parentNode === $msgs) $msgs.insertBefore(node, working);
877
+ if (scope.container === $msgs) askTail.insert(node);
722
878
  else scope.container.appendChild(node);
723
- if (scope === rootScope) prune();
879
+ if (scope === rootScope) { prune(); askTail.sync(); }
724
880
  // Text, a card, a notice…: the parked hero would sit on it. The prompt and a rack leave it to settleHero().
725
881
  if (hero?.parked && scope === rootScope && !node._rack && !node.classList.contains('msg-user')) dismissHero();
726
882
  if (racks.size) syncRacks();
@@ -785,6 +941,99 @@ export function createRenderer(transcriptEl, hooks = {}) {
785
941
  linkify(body);
786
942
  }
787
943
 
944
+ /**
945
+ * A message's text without its follow-ups block (asst-followups.js). While it streams, a block
946
+ * being typed stays hidden; once the text is final, the block becomes the card under the row
947
+ * (root messages only: a subagent's offers are not the user's to answer).
948
+ */
949
+ function paintMessage(row, body, text, { streaming = false, scope = rootScope } = {}) {
950
+ const split = splitFollowups(text, { streaming });
951
+ renderMarkdownInto(body, split.body);
952
+ if (!streaming && split.block && scope === rootScope) attachFollowups(row, split.block);
953
+ return split;
954
+ }
955
+ /** What a streaming text shows: '' while it is nothing but the start of a block. */
956
+ const shownText = (text) => splitFollowups(text, { streaming: true }).body;
957
+
958
+ // ── Offers ──
959
+ /**
960
+ * The follow-ups card under a message row: once per row, registered as an open offer, and presented by
961
+ * the mascot (its face is live while it is the newest open card: activateRigs).
962
+ */
963
+ function attachFollowups(row, set) {
964
+ if (!row || !set?.items?.length) return null;
965
+ if (row._followups?.el.isConnected) return row._followups;
966
+ const card = createFollowupsCard(set, {
967
+ t,
968
+ // A card drawn from the journal comes in quietly; a live one with a nod.
969
+ entrance: calm() ? null : 'nod',
970
+ onSend: (text, meta) => hooks.onFollowup?.(text, meta),
971
+ onOther: () => hooks.onFollowupOther?.(),
972
+ onFocusComposer: () => hooks.onFollowupOther?.(),
973
+ });
974
+ row._followups = card;
975
+ (row.querySelector('.asst-msg-content') || row).appendChild(card.el);
976
+ offers.add(card);
977
+ registerAsk(card);
978
+ enterRow(card.el);
979
+ scrollEnd();
980
+ return card;
981
+ }
982
+
983
+ /**
984
+ * A finished turn whose last message still ends in a list of options or an "I can X, or Y" offer,
985
+ * with no block: best-effort items read from the prose. Once per message.
986
+ */
987
+ function guessOffer() {
988
+ const row = lastTextRow;
989
+ if (!row?.isConnected || row._followups || row._guessed || row.classList.contains('asst-msg-error')) return;
990
+ row._guessed = true;
991
+ const set = guessFollowups(row._text || '');
992
+ if (set) attachFollowups(row, set);
993
+ }
994
+
995
+ /**
996
+ * The user's next prompt reached the transcript (`text`), or a new turn started (no text): every open
997
+ * offer is answered by it or goes stale. A guessed set nobody took leaves: its message says it in words.
998
+ */
999
+ function settleOffers(text = null) {
1000
+ for (const offer of [...offers]) {
1001
+ offers.delete(offer);
1002
+ if (!offer.el?.isConnected) continue;
1003
+ const answered = text !== null ? offer.settle(text) : (offer.stale(), false);
1004
+ if (!answered && offer.set?.source === 'heuristic') offer.el.remove();
1005
+ }
1006
+ if (text !== null) lastTextRow = null;
1007
+ }
1008
+
1009
+ const sameText = (a, b) => String(a ?? '').replace(/\s+/g, ' ').trim().toLowerCase() === String(b ?? '').replace(/\s+/g, ' ').trim().toLowerCase();
1010
+
1011
+ /**
1012
+ * A provider's usage limit (asst-limit.js) updates the conversation's single bottom countdown.
1013
+ * The reset time is the session's own limit report while live (hooks.limitInfo: the Claude SDK's
1014
+ * rate_limit_info), else the one the message names, read from when it was written. Retry now sends one
1015
+ * message that retries the blocked work.
1016
+ */
1017
+ function offerLimitWait(text, { brainError = false, limit = null } = {}) {
1018
+ const read = limitFromText(text, { now: nowMs() });
1019
+ let info = limit;
1020
+ if (!info && !muted) { try { info = hooks.limitInfo?.() || null; } catch { info = null; } }
1021
+ const reported = info?.status === 'rejected';
1022
+ // The brain's own error while the session's report says the limit holds is that limit, whatever its words.
1023
+ if (!read && !(brainError && reported)) return null;
1024
+ const resetsAt = reported ? info.resetsAt : read?.resetsAt;
1025
+ const retryText = t('assistant.limitWait.continue', 'The usage limit has reset. Retry what was blocked and continue where you left off.');
1026
+ let provider = '';
1027
+ try { provider = providerShortLabel(hooks.brainProvider?.() || ''); } catch { /* unknown */ }
1028
+ return limitDock.set('brain', { resetsAt, since: nowMs(), provider, onRetry: () => { limitDock.set('brain', null); hooks.onLimitRetry?.(retryText); } });
1029
+ }
1030
+
1031
+ /** The session's limit report changed (assistant:limit): this turn's waiting card counts to its reset time. */
1032
+ function noteLimit(limit) {
1033
+ if (limit?.status === 'rejected') offerLimitWait('', { brainError: true, limit });
1034
+ else if (limit?.status && limit.status !== 'rejected') limitDock.set('brain', null);
1035
+ }
1036
+
788
1037
  function copyText(text) {
789
1038
  const value = String(text ?? '');
790
1039
  if (!value) return;
@@ -817,10 +1066,18 @@ export function createRenderer(transcriptEl, hooks = {}) {
817
1066
  if (!s) return;
818
1067
  if (s.mdTimer) { clearTimeout(s.mdTimer); s.mdTimer = null; }
819
1068
  if (s.thinkTimer) { clearTimeout(s.thinkTimer); s.thinkTimer = null; }
820
- if (s.bodyEl && s.textBuf.trim()) renderMarkdownInto(s.bodyEl, s.textBuf);
1069
+ if (s.bodyEl && s.textBuf.trim()) paintMessage(s.row, s.bodyEl, s.textBuf, { streaming: true, scope });
821
1070
  if (s.think) refreshStep(s.think);
822
1071
  }
823
1072
 
1073
+ /** A streamed row that stays as the message (no final message replaced it): its text is final now. */
1074
+ function sealStreamRow(scope) {
1075
+ const s = scope.stream;
1076
+ if (!s?.row || !s.bodyEl || !s.textBuf.trim()) return;
1077
+ s.row.classList.remove('streaming');
1078
+ paintMessage(s.row, s.bodyEl, s.textBuf, { scope });
1079
+ }
1080
+
824
1081
  /** The stream's text row goes (the final message renders the text again). */
825
1082
  function dropStreamText(scope) {
826
1083
  const s = scope.stream;
@@ -890,10 +1147,12 @@ export function createRenderer(transcriptEl, hooks = {}) {
890
1147
  if (!s.thinkTimer) s.thinkTimer = setTimeout(() => { s.thinkTimer = null; refreshStep(step); if (step.rack.live) paintStage(step.rack); }, STREAM_TICK_MS);
891
1148
  } else if (delta.type === 'text_delta' && delta.text != null) {
892
1149
  s.textBuf += delta.text;
893
- if (!s.bodyEl && s.textBuf.trim()) {
1150
+ // A row once there is something to show: a follow-ups block on its own is the card, later.
1151
+ if (!s.bodyEl && shownText(s.textBuf).trim()) {
894
1152
  const { row, wrap } = assistantRow(scope, { _text: s.textBuf, _msgId: s.msgId, _claude: s.claude });
895
1153
  row.classList.add('streaming');
896
1154
  noteAnswer(scope, row);
1155
+ if (scope === rootScope) lastTextRow = row;
897
1156
  s.row = row;
898
1157
  s.wrap = wrap;
899
1158
  s.bodyEl = el('div', 'asst-md');
@@ -904,7 +1163,7 @@ export function createRenderer(transcriptEl, hooks = {}) {
904
1163
  s.mdTimer = setTimeout(() => {
905
1164
  s.mdTimer = null;
906
1165
  if (!s.bodyEl) return;
907
- renderMarkdownInto(s.bodyEl, s.textBuf);
1166
+ paintMessage(s.row, s.bodyEl, s.textBuf, { streaming: true, scope });
908
1167
  scrollEnd();
909
1168
  }, STREAM_TICK_MS);
910
1169
  }
@@ -913,7 +1172,7 @@ export function createRenderer(transcriptEl, hooks = {}) {
913
1172
  }
914
1173
  if (type === 'content_block_stop') {
915
1174
  const s = scope.stream;
916
- if (s?.bodyEl && s.mdTimer) { clearTimeout(s.mdTimer); s.mdTimer = null; renderMarkdownInto(s.bodyEl, s.textBuf); }
1175
+ if (s?.bodyEl && s.mdTimer) { clearTimeout(s.mdTimer); s.mdTimer = null; paintMessage(s.row, s.bodyEl, s.textBuf, { streaming: true, scope }); }
917
1176
  if (s?.think && s.thinkTimer) { clearTimeout(s.thinkTimer); s.thinkTimer = null; refreshStep(s.think); }
918
1177
  }
919
1178
  }
@@ -983,9 +1242,10 @@ export function createRenderer(transcriptEl, hooks = {}) {
983
1242
  function textRow(scope, text, ctx = {}) {
984
1243
  const { row, wrap } = assistantRow(scope, { _text: text, _msgId: ctx.msgId || null, _claude: !!ctx.claude });
985
1244
  const body = el('div', 'asst-md');
986
- renderMarkdownInto(body, text);
987
1245
  wrap.appendChild(body);
1246
+ paintMessage(row, body, text, { scope });
988
1247
  noteAnswer(scope, row);
1248
+ if (scope === rootScope) lastTextRow = row;
989
1249
  return row;
990
1250
  }
991
1251
 
@@ -1012,7 +1272,7 @@ export function createRenderer(transcriptEl, hooks = {}) {
1012
1272
  else if (s.row) {
1013
1273
  s.row.classList.remove('streaming');
1014
1274
  if (!s.wrap.children.length) s.row.remove();
1015
- else if (s.textBuf.trim()) (scope.kept ||= new Set()).add(textKey(s.textBuf));
1275
+ else if (s.textBuf.trim()) { (scope.kept ||= new Set()).add(textKey(s.textBuf)); sealStreamRow(scope); }
1016
1276
  }
1017
1277
  if (streamedThink && !thinks.length) finishThink(streamedThink);
1018
1278
  scope.stream = null;
@@ -1173,10 +1433,10 @@ export function createRenderer(transcriptEl, hooks = {}) {
1173
1433
  return prev;
1174
1434
  }
1175
1435
 
1176
- /** The scope's last row, skipping "Working…" and rows folding into a rack. */
1436
+ /** The scope's last row, skipping "Working…", the open card's slot and rows folding into a rack. */
1177
1437
  function tailRow(scope) {
1178
1438
  let node = scope.container.lastElementChild;
1179
- while (node && (node === working || node.classList.contains('asst-folding'))) node = node.previousElementSibling;
1439
+ while (node && (node === working || node === askTail.slot || node.classList.contains('asst-folding'))) node = node.previousElementSibling;
1180
1440
  return node;
1181
1441
  }
1182
1442
 
@@ -1256,6 +1516,25 @@ export function createRenderer(transcriptEl, hooks = {}) {
1256
1516
  }
1257
1517
 
1258
1518
  // ── Racks ──
1519
+ /**
1520
+ * The class whose scene a new rack of this turn shows: an approved direct route this turn runs, else ''. Live, or
1521
+ * a reattach's replay of the running turn; never the journal (no turn runs while it is drawn).
1522
+ */
1523
+ function directSceneClass() {
1524
+ return (!muted || replayDraw) && turnActive && directRoute && directRoute.turn === turnSeq ? directRoute.taskClass : '';
1525
+ }
1526
+
1527
+ /**
1528
+ * A route a turn takes when it starts (a conversation opened again mid-turn, a pick made while no turn ran): the
1529
+ * open racks made after it are that turn's work, and show its scene from now on. Settled racks never move.
1530
+ */
1531
+ function lendRouteScene(route) {
1532
+ for (const rack of racks) {
1533
+ if (rack.nested || rack.closed || rack.sceneClass || !(rack.id > (route.rackAt ?? Infinity))) continue;
1534
+ rack.sceneClass = route.taskClass;
1535
+ }
1536
+ }
1537
+
1259
1538
  function createRack(scope) {
1260
1539
  const id = ++rackSeq;
1261
1540
  const nested = scope.container !== $msgs;
@@ -1265,9 +1544,9 @@ export function createRenderer(transcriptEl, hooks = {}) {
1265
1544
  row.setAttribute('role', 'group');
1266
1545
  const ledgerId = `asst-rack-${id}-ledger`;
1267
1546
  row.innerHTML = `<div class="asst-rack-stage">
1268
- <div class="asst-rack-face" aria-hidden="true"></div>
1547
+ <div class="asst-rack-set"><div class="asst-rack-face" aria-hidden="true"></div><div class="asst-rack-scene" aria-hidden="true"></div></div>
1269
1548
  <div class="asst-rack-caption"></div>
1270
- <div class="asst-bubble" aria-live="off" hidden><span class="asst-bubble-verb"></span><span class="asst-bubble-subject" role="button" tabindex="-1" aria-expanded="false"><span class="asst-bubble-text"></span><span class="asst-bubble-caret" aria-hidden="true"></span></span><span class="asst-bubble-meta"></span><span class="asst-bubble-err" hidden></span></div>
1549
+ <div class="asst-bubble" aria-live="off" hidden><span class="asst-bubble-line"><span class="asst-bubble-verb"></span><span class="asst-bubble-subject" role="button" tabindex="-1" aria-expanded="false"><span class="asst-bubble-text"></span></span><span class="asst-bubble-meta"></span></span><span class="asst-bubble-err" hidden></span></div>
1271
1550
  <div class="asst-rack-sweep" aria-hidden="true"></div>
1272
1551
  </div>
1273
1552
  <button type="button" class="asst-rack-receipt" aria-expanded="false" aria-controls="${ledgerId}"><span class="asst-rack-receipt-face" aria-hidden="true"></span><span class="asst-rack-receipt-text"></span><span class="asst-rack-receipt-failed" hidden></span><span class="asst-rack-receipt-clock"></span><span class="asst-rack-chev" aria-hidden="true">&#x203A;</span></button>
@@ -1278,7 +1557,10 @@ export function createRenderer(transcriptEl, hooks = {}) {
1278
1557
  const rack = {
1279
1558
  id, row, scope, nested, closed: false, live: false, staged: false, open: false, view: 'kind', everAwake: false,
1280
1559
  stations: new Map(), steps: [], timeline: [], said: [], firstAt: 0,
1281
- stage: { el: q('.asst-rack-stage'), face: q('.asst-rack-face'), caption: q('.asst-rack-caption'), bubble: q('.asst-bubble'), verb: q('.asst-bubble-verb'), subject: q('.asst-bubble-subject'), text: q('.asst-bubble-text'), meta: q('.asst-bubble-meta'), err: q('.asst-bubble-err') },
1560
+ // The direct route's class whose scene the stage shows: set here, or once when a reopened conversation's route
1561
+ // resumes (lendRouteScene); a rack never loses its scene.
1562
+ sceneClass: nested ? '' : directSceneClass(), sceneId: '', sceneAt: -Infinity,
1563
+ stage: { el: q('.asst-rack-stage'), set: q('.asst-rack-set'), face: q('.asst-rack-face'), scene: q('.asst-rack-scene'), caption: q('.asst-rack-caption'), bubble: q('.asst-bubble'), line: q('.asst-bubble-line'), verb: q('.asst-bubble-verb'), subject: q('.asst-bubble-subject'), text: q('.asst-bubble-text'), meta: q('.asst-bubble-meta'), err: q('.asst-bubble-err') },
1282
1564
  receipt: { btn: q('.asst-rack-receipt'), face: q('.asst-rack-receipt-face'), text: q('.asst-rack-receipt-text'), failed: q('.asst-rack-receipt-failed'), clock: q('.asst-rack-receipt-clock'), faceBuilt: false, pose: '', svg: null },
1283
1565
  ledger: q('.asst-rack-ledger'), statusEl: q('.asst-rack-status'), alertEl: q('.asst-rack-alert'), timelineEl: null, timelineSig: '',
1284
1566
  rig: null, target: null, held: false, holdUntil: 0, holdTimer: 0, pose: null, awake: null, awakeKey: null,
@@ -1454,6 +1736,8 @@ export function createRenderer(transcriptEl, hooks = {}) {
1454
1736
  renderRackLabel(rack, sentence);
1455
1737
  if (live) { if (rack.staged) paintStage(rack, now); scheduleStage(rack, now); needTicker(); }
1456
1738
  else if (was) settle(rack);
1739
+ // The scene moves while the rack is live, as its sweep does: never while a card holds it, nor once it settled.
1740
+ rack.stage.scene.firstElementChild?.toggleAttribute('data-live', live && !rack.waiting);
1457
1741
  if (rack.open && rack.view === 'time') renderTimeline(rack);
1458
1742
  if (working?.isConnected) syncWorkingFace();
1459
1743
  }
@@ -1506,6 +1790,7 @@ export function createRenderer(transcriptEl, hooks = {}) {
1506
1790
  clearTimeout(rack.clearTimer);
1507
1791
  rack.clearTimer = 0;
1508
1792
  rack.row.classList.add('is-staged');
1793
+ watchStageWidth(rack, true);
1509
1794
  if (!rack.rig) {
1510
1795
  try { rack.rig = mascotLib.createMascot(rack.stage.face, { width: 72, height: 36, rangeX: 10, rangeY: 7, pose: 'idle' }); } catch { rack.rig = null; }
1511
1796
  rack.pose = null;
@@ -1544,6 +1829,7 @@ export function createRenderer(transcriptEl, hooks = {}) {
1544
1829
  /** A folded stage keeps nothing: its rig goes, its texts empty (a settled rack is the same live or replayed). */
1545
1830
  function clearStage(rack) {
1546
1831
  if (rack.staged) return;
1832
+ watchStageWidth(rack, false);
1547
1833
  const had = !!rack.rig;
1548
1834
  rack.rig?.destroy?.();
1549
1835
  rack.rig = null;
@@ -1553,6 +1839,10 @@ export function createRenderer(transcriptEl, hooks = {}) {
1553
1839
  if (had) mascotChanged();
1554
1840
  const s = rack.stage;
1555
1841
  s.face.textContent = '';
1842
+ s.scene.textContent = '';
1843
+ delete s.el.dataset.scene;
1844
+ rack.sceneId = '';
1845
+ rack.sceneAt = -Infinity;
1556
1846
  s.caption.textContent = '';
1557
1847
  s.bubble.hidden = true;
1558
1848
  delete s.bubble.dataset.state;
@@ -1561,6 +1851,7 @@ export function createRenderer(transcriptEl, hooks = {}) {
1561
1851
  s.text.textContent = '';
1562
1852
  s.subject.removeAttribute('title');
1563
1853
  s.subject.className = 'asst-bubble-subject';
1854
+ s.subject.hidden = false;
1564
1855
  s.subject.setAttribute('aria-expanded', 'false');
1565
1856
  s.meta.textContent = '';
1566
1857
  s.err.textContent = '';
@@ -1570,15 +1861,21 @@ export function createRenderer(transcriptEl, hooks = {}) {
1570
1861
  }
1571
1862
 
1572
1863
  /**
1573
- * One rig of this transcript animates: the hero, else the newest stage, else
1574
- * the "Thinking" row's. The rule ends at this renderer: every other panel
1575
- * runs its own, so N live sessions show N moving characters.
1864
+ * One rig of this transcript animates: the hero, else the newest open card
1865
+ * the mascot presents (a follow-ups card, a question card: its face is a rig
1866
+ * only then, and never while calm), else the newest stage, else the
1867
+ * "Thinking" row's. The rule ends at this renderer: every other panel runs
1868
+ * its own, so N live sessions show N moving characters.
1576
1869
  */
1577
1870
  function activateRigs() {
1578
- const newest = hero?.rig ? null : newestStage();
1871
+ const ask = hero?.rig ? null : liveAsk();
1872
+ const askLive = !!ask && !calm();
1873
+ for (const handle of askCards) handle.presenter?.setLive?.(askLive && handle === ask);
1874
+ const asking = askLive && !!ask.presenter?.isLive?.();
1875
+ const newest = hero?.rig || asking ? null : newestStage();
1579
1876
  for (const rack of racks) if (rack.rig) rack.rig.setActive?.(rack === newest);
1580
1877
  hero?.rig?.setActive?.(true);
1581
- workingFace?.rig?.setActive?.(!hero?.rig && !newest);
1878
+ workingFace?.rig?.setActive?.(!hero?.rig && !asking && !newest);
1582
1879
  }
1583
1880
 
1584
1881
  /** The bubble's call: the awake one, else the newest; it holds 900 ms (3 s for thinking) before the next wins. */
@@ -1638,11 +1935,32 @@ export function createRenderer(transcriptEl, hooks = {}) {
1638
1935
  if (!rack.waiting && shown && shown.rack === rack && now - rack.bubble.since < hold) rack.bubble.pendingAt = rack.bubble.since + hold;
1639
1936
  else { rack.bubble.step = target; rack.bubble.since = now; }
1640
1937
  }
1938
+ paintRackScene(rack, now); // before the bubble: the set it sits beside takes the scene's width
1641
1939
  poseStage(rack);
1642
1940
  paintBubble(rack, now, { swap: true });
1643
1941
  paintCaption(rack);
1644
1942
  }
1645
1943
 
1944
+ /**
1945
+ * A direct route's rack: the scene under the mascot is the route class's
1946
+ * (asst-scenes.js), or the one the bubble's call implies (a browser tool the
1947
+ * browser, a computer tool the computer, a web search or a page read
1948
+ * research); a scene a call picked holds STAGE_TIMING.sceneDwellMs, as on a
1949
+ * run's stage (tickRacks tries again). Nothing for a rack without a route.
1950
+ */
1951
+ function paintRackScene(rack, now) {
1952
+ if (!rack.sceneClass || !rack.staged) return;
1953
+ const step = rack.bubble.step;
1954
+ const call = step?.type === 'call' ? { kind: step.kind, name: step.name } : null;
1955
+ const want = activityScene(call, rack.sceneClass) || sceneOfClass(rack.sceneClass);
1956
+ if (want === rack.sceneId) return;
1957
+ if (rack.sceneId && now - rack.sceneAt < STAGE_TIMING.sceneDwellMs) return;
1958
+ rack.sceneId = want;
1959
+ rack.sceneAt = now;
1960
+ rack.stage.scene.innerHTML = sceneSvg(want, { live: rack.live && !rack.waiting });
1961
+ rack.stage.el.dataset.scene = want;
1962
+ }
1963
+
1646
1964
  function paintCaption(rack) {
1647
1965
  const step = rack.bubble.step;
1648
1966
  const say = step ? sayOf(step) : null;
@@ -1667,25 +1985,39 @@ export function createRenderer(transcriptEl, hooks = {}) {
1667
1985
  const held = rack.waiting && step === waitingStep(rack);
1668
1986
  const verb = held ? t('assistant.rack.waiting', 'Waiting for you') : verbText(say.verb, running, t);
1669
1987
  const grouped = groupedSubject(rack, step);
1670
- const full = grouped || say.text;
1988
+ // A thought with no words yet says only "Thinking" (the mascot's thought dots are the one busy sign):
1989
+ // the subject's "…" placeholder stays for a call whose input is still streaming.
1990
+ const bare = step.type === 'think' && say.text === '…';
1991
+ // A command as the stage says it: one "…" however many paths its summary shortens (say.stage).
1992
+ const full = bare ? '' : grouped || say.stage || say.text;
1671
1993
  const literal = !grouped && say.full ? say.full : full; // the command as written: the title and the opened bubble
1672
1994
  const open = rack.bubble.open;
1673
- // Narrow stages cut from the head, so the file name (the tail) survives.
1674
- const cut = subjectCut(rack);
1675
- const budget = subjectBudget(rack, verb, cut);
1676
- const shownText = open ? literal : cut === 'head' ? headEllipsis(full, budget) : middleEllipsis(full, budget, 24);
1677
- const key = `${step.id}|${verb}|${shownText}|${say.muted ? 1 : 0}`;
1678
1995
  s.bubble.hidden = !full && !verb;
1679
1996
  s.bubble.dataset.state = held ? 'waiting' : running ? 'running' : step.state;
1680
1997
  s.bubble.dataset.kind = step.kind;
1998
+ // The numbers first: they share the subject's line, so the cut below measures the room they leave.
1999
+ paintBubbleMeta(rack, step, say, now);
2000
+ // Narrow stages cut from the head, so the file name (the tail) survives. A small budget keeps a
2001
+ // shorter tail (24 chars of it would not fit beside the verb). A subject already cut from its head
2002
+ // (a streamed thought's tail) is cut from the head again: one "…", never two.
2003
+ const cut = full.startsWith('…') ? 'head' : subjectCut(rack);
2004
+ const cutTo = (n) => (open ? literal : cut === 'head' ? headEllipsis(full, n) : middleEllipsis(full, n, Math.min(24, Math.max(8, n - 6))));
2005
+ const shownText = bare ? '' : cutTo(subjectBudget(rack, verb, cut));
2006
+ const key = `${step.id}|${verb}|${shownText}|${say.muted ? 1 : 0}|${bare ? 1 : 0}`;
1681
2007
  if (key !== rack.bubble.key) {
1682
2008
  const changedStep = !rack.bubble.key.startsWith(`${step.id}|`);
1683
2009
  rack.bubble.key = key;
1684
2010
  const write = () => {
1685
2011
  s.verb.textContent = verb;
1686
2012
  s.subject.className = `asst-bubble-subject${say.mono ? ' is-mono' : ''}${say.muted ? ' is-muted' : ''}`;
1687
- writeSubject(s.text, grouped || open ? null : say.segments, shownText);
1688
- if (literal !== shownText || open) s.subject.title = literal; else s.subject.removeAttribute('title');
2013
+ s.subject.hidden = bare;
2014
+ const segments = grouped || open ? null : say.segments;
2015
+ writeSubject(s.text, segments, shownText);
2016
+ // The budget is an estimate (a long token wraps whole to the next line): what the two-line
2017
+ // clamp would hide is cut here instead, so the subject is never cut a second time.
2018
+ const fitted = open || bare ? shownText : fitSubject(s, shownText, cutTo, segments);
2019
+ s.subject.classList.toggle('is-cut', fitted.startsWith('…'));
2020
+ if (literal !== fitted || open) s.subject.title = literal; else s.subject.removeAttribute('title');
1689
2021
  };
1690
2022
  if (swap && changedStep && !calm() && typeof s.subject.animate === 'function' && s.text.textContent) {
1691
2023
  s.subject.getAnimations?.().forEach(a => a.cancel());
@@ -1697,7 +2029,6 @@ export function createRenderer(transcriptEl, hooks = {}) {
1697
2029
  };
1698
2030
  } else write();
1699
2031
  }
1700
- paintBubbleMeta(rack, step, say, now);
1701
2032
  const err = step.state === 'error' ? (step.detail || exitText(step)) : '';
1702
2033
  s.err.hidden = !err;
1703
2034
  if (s.err.textContent !== err) s.err.textContent = err;
@@ -1708,24 +2039,32 @@ export function createRenderer(transcriptEl, hooks = {}) {
1708
2039
  }
1709
2040
 
1710
2041
  /**
1711
- * How many characters of subject fit the bubble: two lines of the column
1712
- * beside the verb, measured on the laid-out stage. The text is cut in the
1713
- * middle so its tail (the file, the last argument) stays; on a narrow stage
1714
- * from the head (subjectCut).
2042
+ * How many characters of subject fit the bubble: two lines of its one
2043
+ * text flow, less the verb at the head and the numbers at the end,
2044
+ * measured on the laid-out stage. The text is cut in the middle so its
2045
+ * tail (the file, the last argument) stays; on a narrow stage from the
2046
+ * head (subjectCut).
1715
2047
  */
1716
2048
  function subjectBudget(rack, verb, cut = 'middle') {
1717
2049
  const s = rack.stage;
1718
2050
  const width = s.el.clientWidth;
1719
2051
  if (!width) return 96;
1720
- const face = s.face.getBoundingClientRect();
2052
+ const face = (s.set || s.face).getBoundingClientRect(); // the mascot's column (with a scene under it, as wide as the scene)
1721
2053
  const bubble = s.bubble.hidden ? null : s.bubble.getBoundingClientRect();
1722
2054
  const beside = !bubble || bubble.left >= face.right - 1;
1723
2055
  const verbPx = Math.max(s.verb.getBoundingClientRect().width, verb.length * 7);
1724
- const column = width - 28 - 22 - (beside ? face.width + 8 : 0) - verbPx - 7;
2056
+ // The numbers at the end of the line (painted first): a running call keeps a clock's room before it shows.
2057
+ const metaPx = s.meta.textContent ? s.meta.getBoundingClientRect().width : 0;
2058
+ const numbersPx = Math.max(metaPx, rack.bubble.step?.state === 'running' ? 28 : 0);
2059
+ // The bubble's text column: the stage's 14px sides, the notch's 8px, the bubble's 10px sides.
2060
+ const column = width - 28 - 8 - 20 - (beside ? face.width + 8 : 0);
1725
2061
  const perLine = Math.max(12, Math.floor(column / 7));
1726
- // Two lines, less what wrapping at token boundaries wastes (a head cut wraps anywhere: two full lines).
1727
- const lines = getComputedStyle(s.subject).whiteSpace === 'nowrap' ? 1 : cut === 'head' ? 2 : 1.7;
1728
- return Math.max(24, Math.min(240, Math.floor(perLine * lines) - 1));
2062
+ // Two lines, less what wrapping at token boundaries wastes (a head cut is one tail: close to two full
2063
+ // lines, a path's pieces wrapping at its separators); a guess on the generous side, since fitSubject
2064
+ // cuts what the clamp would hide or a piece would push past the edge.
2065
+ const lines = getComputedStyle(s.line).whiteSpace === 'nowrap' ? 1 : cut === 'head' ? 2 : 1.85;
2066
+ const taken = Math.ceil((verbPx + 7 + (numbersPx ? numbersPx + 7 : 0)) / 7);
2067
+ return Math.max(24, Math.min(240, Math.floor(perLine * lines) - taken - 1));
1729
2068
  }
1730
2069
 
1731
2070
  /** 'head' where the stylesheet says so (≤320 px: keep the tail), else 'middle'. */
@@ -1733,21 +2072,59 @@ export function createRenderer(transcriptEl, hooks = {}) {
1733
2072
  try { return getComputedStyle(rack.stage.subject).getPropertyValue('--rk-cut').trim() === 'head' ? 'head' : 'middle'; } catch { return 'middle'; }
1734
2073
  }
1735
2074
 
1736
- /** "…ender-events.browser.mjs": the last `max` chars, from a token boundary when one is near. */
1737
- function headEllipsis(text, max) {
1738
- const s = String(text ?? '');
1739
- if (s.length <= max) return s;
1740
- let tail = s.slice(s.length - Math.max(8, max - 1));
1741
- const cut = tail.search(/[\s/]/);
1742
- if (cut > 0 && cut < tail.length * 0.4) tail = tail.slice(tail[cut] === '/' ? cut : cut + 1);
1743
- return `…${tail}`;
1744
- }
1745
-
2075
+ /**
2076
+ * The subject's text, in its segments' spans (a read's dim folder, its
2077
+ * bold name). A URL or path in it is one `.asst-bubble-piece` (nowrap) per
2078
+ * piece with a <wbr> between them (wrapRuns): it wraps after "/" and at
2079
+ * its other separators, never mid-word.
2080
+ */
1746
2081
  function writeSubject(node, segments, text) {
1747
2082
  node.textContent = '';
1748
- if (Array.isArray(segments) && segments.length && segments.map(x => x.text).join('') === text) {
1749
- for (const seg of segments) node.appendChild(el('span', seg.cls ? `is-${seg.cls}` : '', seg.text));
1750
- } else node.textContent = text;
2083
+ const segs = Array.isArray(segments) && segments.length && segments.map(x => x.text).join('') === text ? segments : null;
2084
+ let end = 0;
2085
+ const bounds = segs ? segs.map(seg => [end += seg.text.length, seg.cls]) : [[text.length, null]];
2086
+ // text[from, to) into `host`, one span per segment it crosses.
2087
+ const put = (host, from, to) => {
2088
+ for (const [stop, cls] of bounds) {
2089
+ if (from >= to) break;
2090
+ if (stop <= from) continue;
2091
+ const slice = text.slice(from, Math.min(stop, to));
2092
+ host.appendChild(segs ? el('span', cls ? `is-${cls}` : '', slice) : document.createTextNode(slice));
2093
+ from += slice.length;
2094
+ }
2095
+ };
2096
+ let at = 0;
2097
+ for (const run of wrapRuns(text)) {
2098
+ if (!run.pieces) { put(node, at, at += run.text.length); continue; }
2099
+ run.pieces.forEach((piece, i) => {
2100
+ if (i) node.appendChild(document.createElement('wbr'));
2101
+ put(node.appendChild(el('span', 'asst-bubble-piece')), at, at += piece.length);
2102
+ });
2103
+ }
2104
+ }
2105
+
2106
+ /** A path piece wider than what is left of its line: it runs past the line's edge, where the line's overflow hides it. */
2107
+ function pieceOverflows(s) {
2108
+ const pieces = s.text.getElementsByClassName('asst-bubble-piece');
2109
+ if (!pieces.length) return false;
2110
+ const right = s.line.getBoundingClientRect().right + 0.5;
2111
+ for (const piece of pieces) for (const r of piece.getClientRects()) if (r.right > right) return true;
2112
+ return false;
2113
+ }
2114
+
2115
+ /**
2116
+ * The bubble's subject cut shorter (`cutTo`, six steps at most) until the line's clamp hides none of it
2117
+ * (nor the numbers after it) and no path piece runs past its edge; the text it shows.
2118
+ */
2119
+ function fitSubject(s, text, cutTo, segments) {
2120
+ let shown = text;
2121
+ let budget = shown.length;
2122
+ for (let i = 0; i < 6 && budget > 12 && (s.line.scrollHeight > s.line.clientHeight + 1 || pieceOverflows(s)); i += 1) {
2123
+ budget = Math.floor(budget * 0.85);
2124
+ shown = cutTo(budget);
2125
+ writeSubject(s.text, segments, shown);
2126
+ }
2127
+ return shown;
1751
2128
  }
1752
2129
 
1753
2130
  /** How many calls of the rack act on the bubble call's target (itself included): a re-run or re-read reads ×N. */
@@ -1771,7 +2148,7 @@ export function createRenderer(transcriptEl, hooks = {}) {
1771
2148
  // A subagent's bubble follows its child: the step the agent is on now.
1772
2149
  if (step.kind === 'subagent' && step.children?.length) {
1773
2150
  const child = step.children.filter(c => c.type === 'call' || (c.state === 'running' && !c.stale)).reduce((a, b) => (!a || b.startedAt >= a.startedAt ? b : a), null);
1774
- if (child) parts.push(['m', middleEllipsis(stepSentence(child, child.state === 'running' && !child.stale), 60, 24)]);
2151
+ if (child) parts.push(['m', middleEllipsis(stepSentence(child, child.state === 'running' && !child.stale, { stage: true }), 60, 24)]);
1775
2152
  }
1776
2153
  // ×N only for the same target again (a re-run, a re-read): a number the ledger agrees with.
1777
2154
  // Other calls in the same 800 ms burst read as a sentence instead.
@@ -2065,10 +2442,10 @@ export function createRenderer(transcriptEl, hooks = {}) {
2065
2442
  return t('assistant.rack.state.done', 'done');
2066
2443
  }
2067
2444
 
2068
- /** "Running npm test" / "Ran npm test". */
2069
- function stepSentence(step, running) {
2445
+ /** "Running npm test" / "Ran npm test"; `stage`: as the stage bubble says it (say.stage). */
2446
+ function stepSentence(step, running, { stage = false } = {}) {
2070
2447
  const say = sayOf(step);
2071
- return `${verbText(say.verb, running, t)} ${String(say.text || '').replace(/^\$ /, '')}`.trim();
2448
+ return `${verbText(say.verb, running, t)} ${String((stage && say.stage) || say.text || '').replace(/^\$ /, '')}`.trim();
2072
2449
  }
2073
2450
 
2074
2451
  /** The receipt: one call says what it did; a burst says it in one sentence. */
@@ -2462,6 +2839,30 @@ export function createRenderer(transcriptEl, hooks = {}) {
2462
2839
  unsubTick = null;
2463
2840
  }
2464
2841
 
2842
+ /** The bubbles whose stage changed width: cut and fit again (the same text may fit differently now). */
2843
+ function recutBubbles() {
2844
+ recutTimer = 0;
2845
+ for (const rack of toRecut) {
2846
+ if (!rack.staged || !rack.bubble.step || !rack.row.isConnected) continue;
2847
+ rack.bubble.key = '';
2848
+ paintBubble(rack, Date.now(), { swap: false });
2849
+ }
2850
+ toRecut.clear();
2851
+ }
2852
+
2853
+ function watchStageWidth(rack, on) {
2854
+ if (!stageResizes) return;
2855
+ const stage = rack.stage.el;
2856
+ if (on && !stageWidths.has(stage)) {
2857
+ stageWidths.set(stage, { rack, width: null });
2858
+ stageResizes.observe(stage);
2859
+ } else if (!on && stageWidths.has(stage)) {
2860
+ stageWidths.delete(stage);
2861
+ stageResizes.unobserve(stage);
2862
+ toRecut.delete(rack);
2863
+ }
2864
+ }
2865
+
2465
2866
  /** Once a second while a rack is live: its clocks (and any wake-up the timers have not caught). */
2466
2867
  function tickRacks() {
2467
2868
  let any = false;
@@ -2470,7 +2871,8 @@ export function createRenderer(transcriptEl, hooks = {}) {
2470
2871
  if (!rack.live) continue;
2471
2872
  any = true;
2472
2873
  const now = Date.now();
2473
- if (rack.staged && rack.bubble.step) paintBubble(rack, now, { swap: false }); // clocks, and the cut after a resize
2874
+ if (rack.staged && rack.sceneClass) paintRackScene(rack, now); // a scene swap the dwell held back
2875
+ if (rack.staged && rack.bubble.step) paintBubble(rack, now, { swap: false }); // its clocks (a resize re-cuts it: recutBubbles)
2474
2876
  for (const st of rack.stations.values()) { const time = stationTime(st, true, rackNow(rack)); if (st.time.textContent !== time) st.time.textContent = time; }
2475
2877
  for (const step of rack.steps) if (step.state === 'running' && step.type === 'call' && step.progress >= 1) refreshStep(step);
2476
2878
  }
@@ -2478,6 +2880,7 @@ export function createRenderer(transcriptEl, hooks = {}) {
2478
2880
  }
2479
2881
 
2480
2882
  function dropRack(rack) {
2883
+ watchStageWidth(rack, false);
2481
2884
  clearTimeout(rack.stageTimer);
2482
2885
  clearTimeout(rack.clearTimer);
2483
2886
  clearTimeout(rack.sr.timer);
@@ -2544,7 +2947,12 @@ export function createRenderer(transcriptEl, hooks = {}) {
2544
2947
  }
2545
2948
 
2546
2949
  function appendUser(text, { images = [], files = [], memories = [], origin = null, imageCount = 0 } = {}) {
2950
+ // The prompt answers what the last turn offered (one of its follow-ups, a retry), or it leaves it behind.
2951
+ settleOffers(String(text ?? ''));
2952
+ limitDock.set('brain', null);
2547
2953
  turnFrames = [];
2954
+ directRoute = null; // a new prompt is a new task: its own route decides
2955
+ lastDirect = null;
2548
2956
  rootScope.kept = null;
2549
2957
  lastErrorText = '';
2550
2958
  const row = el('div', 'asst-msg msg-user');
@@ -2581,17 +2989,21 @@ export function createRenderer(transcriptEl, hooks = {}) {
2581
2989
  }
2582
2990
 
2583
2991
  function appendError(text) {
2584
- return appendStatus(text || t('assistant.errorGeneric', 'Something went wrong'), 'error');
2992
+ const row = appendStatus(text || t('assistant.errorGeneric', 'Something went wrong'), 'error');
2993
+ if (text) offerLimitWait(text); // a usage limit: the waiting card under it
2994
+ return row;
2585
2995
  }
2586
2996
 
2587
2997
  function appendDivider(label) {
2588
2998
  return append(el('div', 'asst-divider', label || ''));
2589
2999
  }
2590
3000
 
2591
- /** Place an arbitrary node (control / route card) as a transcript row. */
3001
+ /** Place an arbitrary node (control / route card) as a transcript row; a card that asks is presented by the mascot. */
2592
3002
  function appendNode(node) {
2593
3003
  if (!node) return null;
2594
- return append(node);
3004
+ const row = append(node);
3005
+ if (node._ask) registerAsk(node._ask);
3006
+ return row;
2595
3007
  }
2596
3008
 
2597
3009
  function showWorking(label) {
@@ -2604,7 +3016,17 @@ export function createRenderer(transcriptEl, hooks = {}) {
2604
3016
  working.querySelector('.asst-working-label').textContent = label || t('assistant.rack.verb.think.now', 'Thinking');
2605
3017
  hideEmpty({ park: true });
2606
3018
  $msgs.appendChild(working);
2607
- if (!turnActive) turn = { calls: 0, last: null, errored: false, answerRow: null };
3019
+ if (!turnActive) {
3020
+ // A new turn: what the last one offered and nobody took is no longer clickable.
3021
+ settleOffers();
3022
+ limitDock.set('brain', null);
3023
+ turn = { calls: 0, last: null, errored: false, answerRow: null };
3024
+ turnSeq += 1;
3025
+ if (directRoute && directRoute.turn == null) { // a route picked while no turn ran, or one the journal left running, is this turn's
3026
+ directRoute.turn = turnSeq;
3027
+ lendRouteScene(directRoute);
3028
+ }
3029
+ }
2608
3030
  turnActive = true;
2609
3031
  if (racks.size) syncRacks();
2610
3032
  syncWorkingFace();
@@ -2685,33 +3107,178 @@ export function createRenderer(transcriptEl, hooks = {}) {
2685
3107
  onStop: hooks.onStopRun,
2686
3108
  onRead: hooks.onReadRun,
2687
3109
  onEscalate: hooks.onEscalateRun,
3110
+ onLimitWait: (run, limit) => limitDock.set(`run:${run.runId}`, limit ? {
3111
+ resetsAt: limit.resetsAt || null,
3112
+ since: Date.parse(limit.detectedAt || run.completedAt || '') || nowMs(),
3113
+ provider: providerShortLabel(run.provider || 'claude-code'),
3114
+ onRetry: () => { limitDock.set(`run:${run.runId}`, null); hooks.onEscalateRun?.(run.runId, limitRetryTarget(run)); },
3115
+ } : null),
2688
3116
  result,
2689
3117
  keepSummary: true,
2690
3118
  };
2691
3119
  }
2692
3120
 
2693
- function upsertRunCard(run, { result = null } = {}) {
2694
- if (!run?.runId) return null;
3121
+ /** What the renderer knows of a run (titles for the stage bubble, the route line's live state). */
3122
+ function noteRun(run) {
2695
3123
  const prev = runInfo.get(run.runId);
2696
3124
  const status = run.status || prev?.state;
2697
3125
  const tone = status ? runTone({ ...run, status }) : '';
2698
3126
  const routeId = run.route?.routeId || prev?.routeId || '';
2699
3127
  runInfo.set(run.runId, { ...prev, title: run.title || run.task || prev?.title, state: status, provider: run.provider, routeId, working: tone === 'running' || tone === 'queued' });
3128
+ if (isTerminalRunStatus(status)) runActivity.delete(run.runId); // the run is over: so is what it was doing
2700
3129
  if (routeId) syncRouteLive(routeId);
3130
+ }
3131
+
3132
+ function upsertRunCard(run, { result = null } = {}) {
3133
+ if (!run?.runId) return null;
3134
+ noteRun(run);
2701
3135
  const cardOpts = runCardOpts(result);
2702
3136
  let card = runCards.get(run.runId);
2703
3137
  if (card && card.isConnected) {
2704
3138
  updateRunCard(card, run, cardOpts);
3139
+ syncStage(run.runId, card, run);
2705
3140
  for (const rack of racks) if (rack.live) renderRack(rack); // "Waiting on <title>" learns the title
2706
3141
  return card;
2707
3142
  }
3143
+ dropStage(run.runId); // a card that left the transcript took its stage with it
2708
3144
  card = buildRunCard(run, cardOpts);
2709
3145
  runCards.set(run.runId, card);
2710
3146
  const { wrap } = assistantRow(rootScope);
2711
3147
  wrap.appendChild(card);
3148
+ syncStage(run.runId, card, run);
2712
3149
  return card;
2713
3150
  }
2714
3151
 
3152
+ /**
3153
+ * A run update from anywhere (the agents tray's sources: the sync bus, its
3154
+ * fetch): the card and its stage update in place, the route line learns
3155
+ * whether its run works. Never adds a card.
3156
+ */
3157
+ function syncRun(run) {
3158
+ if (!run?.runId) return null;
3159
+ noteRun(run);
3160
+ const card = runCards.get(run.runId);
3161
+ if (!card?.isConnected) { limitDock.set(`run:${run.runId}`, null); return null; }
3162
+ updateRunCard(card, run, runCardOpts()); // fresh options: the run's own lastResult, never a result the card was built with
3163
+ syncStage(run.runId, card, run);
3164
+ return card;
3165
+ }
3166
+
3167
+ // ── Worker stages (asst-run-stage.js) ──
3168
+ /** The card's stage: made when the run first works, then updated in place for as long as the card lives. */
3169
+ function syncStage(runId, card, run) {
3170
+ let stage = stages.get(runId);
3171
+ if (!stage) {
3172
+ if (!isLivePhase(stagePhase(run))) return;
3173
+ stage = createWorkerStage(card, { t, calm: () => reducedMotion() });
3174
+ stages.set(runId, stage);
3175
+ stage.setActivity(runActivity.get(runId) || null);
3176
+ }
3177
+ stage.update(run);
3178
+ if (stage.live()) needStageTicker();
3179
+ }
3180
+
3181
+ function dropStage(runId) {
3182
+ const stage = stages.get(runId);
3183
+ if (!stage) return;
3184
+ stages.delete(runId);
3185
+ try { stage.destroy(); } catch { /* gone */ }
3186
+ }
3187
+
3188
+ function needStageTicker() {
3189
+ if (unsubStageTick) return;
3190
+ try { unsubStageTick = subscribe(tickStages, { fps: 1 }); } catch { unsubStageTick = null; }
3191
+ }
3192
+
3193
+ function stopStageTicker() {
3194
+ if (!unsubStageTick) return;
3195
+ try { unsubStageTick(); } catch { /* already gone */ }
3196
+ unsubStageTick = null;
3197
+ }
3198
+
3199
+ /** Once a second while a stage is open: its beat, and the card's elapsed time (the run's one clock: the details line). */
3200
+ function tickStages() {
3201
+ let any = false;
3202
+ const now = Date.now();
3203
+ for (const [runId, stage] of [...stages]) {
3204
+ const card = runCards.get(runId);
3205
+ if (!card?.isConnected) { dropStage(runId); continue; }
3206
+ if (!stage.live()) continue;
3207
+ any = true;
3208
+ stage.tick(now);
3209
+ if (card._usageRun) tickRunCard(card, card._usageRun);
3210
+ }
3211
+ if (!any) stopStageTicker();
3212
+ }
3213
+
3214
+ /** What a run's worker does now (its provider events: Claude and Codex workers). Kept compact, and only while the run lasts. */
3215
+ function setRunActivity(runId, activity) {
3216
+ if (!runId) return;
3217
+ const next = isTerminalRunStatus(runInfo.get(runId)?.state) ? null : compactActivity(activity, t);
3218
+ if (next) runActivity.set(runId, next); else runActivity.delete(runId);
3219
+ while (runActivity.size > 200) runActivity.delete(runActivity.keys().next().value);
3220
+ stages.get(runId)?.setActivity(next);
3221
+ }
3222
+
3223
+ /** The row a restored card sits after: its dispatch call's rack, else its route line; null when neither is drawn. */
3224
+ function dispatchAnchor(run) {
3225
+ const routeId = run.route?.routeId || '';
3226
+ let byRoute = null;
3227
+ for (const step of stepsById.values()) {
3228
+ if (step.type !== 'call' || step.kind !== 'runs' || step.rack.nested || !/agent_dispatch$/i.test(String(step.name))) continue;
3229
+ if (step.result && step.result.includes(run.runId)) return step.rack.row;
3230
+ if (!byRoute && routeId && (step.input?.route_id === routeId || step.input?.routeId === routeId)) byRoute = step.rack.row;
3231
+ }
3232
+ if (byRoute?.isConnected) return byRoute;
3233
+ const line = routeId ? routeLines.get(routeId) : null;
3234
+ return line?.el?.isConnected ? line.el : null;
3235
+ }
3236
+
3237
+ const startMs = (run) => new Date(run.startedAt || run.createdAt || 0).getTime() || 0;
3238
+
3239
+ /**
3240
+ * After a reload (run cards are not journaled): each run's card comes back
3241
+ * next to the call that dispatched it (runs of one call in the order they
3242
+ * started), live when its worker still works. A finished run with nothing
3243
+ * in the transcript to sit by is left to the tray; one still working is
3244
+ * added at the end. A card already drawn is only updated.
3245
+ */
3246
+ function restoreRuns(list) {
3247
+ const placed = new Map(); // anchor row → the last card row put after it
3248
+ const runs = (Array.isArray(list) ? list : []).filter(run => run?.runId).sort((a, b) => startMs(a) - startMs(b));
3249
+ for (const run of runs) {
3250
+ if (runCards.get(run.runId)?.isConnected) { syncRun(run); continue; }
3251
+ const anchor = dispatchAnchor(run);
3252
+ if (!anchor && isTerminalRunStatus(run.status)) { noteRun(run); continue; }
3253
+ const row = upsertRunCard(run)?.closest('.asst-msg');
3254
+ if (!row || !anchor?.isConnected || anchor === row) continue;
3255
+ const after = placed.get(anchor);
3256
+ (after?.isConnected ? after : anchor).after(row);
3257
+ placed.set(anchor, row);
3258
+ }
3259
+ }
3260
+
3261
+ /** Bring a run's card into view and hand it focus (the agents tray's mini scenes and lanes). */
3262
+ function revealRun(runId) {
3263
+ const card = runCards.get(runId);
3264
+ if (!card?.isConnected) return false;
3265
+ const top = card.getBoundingClientRect().top - $msgs.getBoundingClientRect().top + $msgs.scrollTop - 16;
3266
+ try { $msgs.scrollTo({ top: Math.max(0, top), behavior: calm() ? 'auto' : 'smooth' }); } catch { $msgs.scrollTop = Math.max(0, top); }
3267
+ card.classList.remove('is-revealed');
3268
+ void card.offsetWidth; // restart the highlight
3269
+ card.classList.add('is-revealed');
3270
+ clearTimeout(card._revealTimer);
3271
+ card._revealTimer = setTimeout(() => card.classList.remove('is-revealed'), 1600);
3272
+ if (!card.hasAttribute('tabindex')) card.tabIndex = -1;
3273
+ try { card.focus({ preventScroll: true }); } catch { /* not focusable */ }
3274
+ return true;
3275
+ }
3276
+
3277
+ function dropStages() {
3278
+ for (const runId of [...stages.keys()]) dropStage(runId);
3279
+ stopStageTicker();
3280
+ }
3281
+
2715
3282
  function patchRunUsage(runId, tokens, fidelity) {
2716
3283
  const card = runCards.get(runId);
2717
3284
  if (!card?.isConnected || !card._usageRun) return;
@@ -2749,6 +3316,43 @@ export function createRenderer(transcriptEl, hooks = {}) {
2749
3316
  }
2750
3317
 
2751
3318
  // ── Route lines ──
3319
+ /**
3320
+ * A decided route says what the Assistant's own racks show. A direct one
3321
+ * ("do it here") whose class has a scene lends it to the racks of the turn
3322
+ * it runs in: this turn, or the next one when it was picked while no turn
3323
+ * ran (a late card answer). A dispatch or chat route takes it back, and so
3324
+ * does the end of its card (declined, expired, cancelled). The scene goes
3325
+ * with its turn (finishTurn; in a journal or a replay, the turn's `result`) and with a new prompt (appendUser).
3326
+ */
3327
+ function noteDirectRoute(route, phase) {
3328
+ const id = String(route?.routeId || '');
3329
+ if (phase !== 'auto' && phase !== 'decided') {
3330
+ if (directRoute && directRoute.routeId === id && (phase === 'declined' || phase === 'expired' || phase === 'cancelled')) directRoute = null;
3331
+ return;
3332
+ }
3333
+ const target = route?.target && typeof route.target === 'object' ? route.target : null;
3334
+ const cls = sceneOfClass(route?.taskClass);
3335
+ const direct = !!target && String(target.kind || 'direct') === 'direct' && cls !== 'generic';
3336
+ // Drawn from the journal (not a live event, not a reattach's replay of the running turn): its turn is the journal's last.
3337
+ const journal = muted && !replayDraw;
3338
+ // That turn is known to be over (the reattach found nothing running, e.g. the journal was drawn again after it):
3339
+ // nothing takes its route.
3340
+ const over = journal && !turnActive && reattachRunning === false;
3341
+ directRoute = direct && !over ? { routeId: id, taskClass: cls, turn: turnActive ? turnSeq : null, rackAt: rackSeq, journal } : null;
3342
+ lastDirect = direct ? { routeId: id, taskClass: cls } : null;
3343
+ }
3344
+
3345
+ /**
3346
+ * What a reattach said (asst-panel.js reattach_result). With no turn running, a route the journal left without its
3347
+ * turn's result belonged to a turn that is over: a later turn (one the CLI starts on its own, a mailbox turn) is
3348
+ * something else and never takes its scene. With one running, that turn is the journal's last: it takes the route
3349
+ * when it starts (showWorking).
3350
+ */
3351
+ function reattached(running) {
3352
+ reattachRunning = !!running;
3353
+ if (!running && directRoute?.journal && directRoute.turn == null) directRoute = null;
3354
+ }
3355
+
2752
3356
  function routeLineOpts(phase) {
2753
3357
  return { phase, t: hooks.t, providerIcon: hooks.providerIcon, brainLabel: hooks.brainLabel, onChangeRoutes: hooks.onChangeRoutes };
2754
3358
  }
@@ -2894,8 +3498,8 @@ export function createRenderer(transcriptEl, hooks = {}) {
2894
3498
  // Under the reply once the turn has ended; a later snapshot (learning) never moves it again.
2895
3499
  const ended = ev.phase !== 'sent';
2896
3500
  if (fresh || (ended && !row.atEnd)) {
2897
- // Placed like any row (before "Working…"), but a status line of the harness never dismisses the hero.
2898
- if (working && working.parentNode === $msgs) $msgs.insertBefore(row.el, working);
3501
+ // Placed like any row (above the open cards and before "Working…"), but a status line of the harness never dismisses the hero.
3502
+ if (askTail.slot || (working && working.parentNode === $msgs)) askTail.insert(row.el);
2899
3503
  else $msgs.appendChild(row.el);
2900
3504
  if (ended) row.atEnd = true;
2901
3505
  prune();
@@ -2909,13 +3513,14 @@ export function createRenderer(transcriptEl, hooks = {}) {
2909
3513
  function showEmpty() {
2910
3514
  if (emptyEl || $msgs.querySelector(':scope > :not(.asst-working)')) return;
2911
3515
  emptyEl = el('div', 'asst-empty');
3516
+ // One column, one hierarchy: the character and the heading, the greeting when there is one,
3517
+ // the starters, then the context line (Project · Budget). No lead: the starters say what it can do.
2912
3518
  emptyEl.innerHTML = `
2913
3519
  <div class="asst-empty-mascot" aria-hidden="true"></div>
2914
3520
  <div class="asst-empty-title" role="heading" aria-level="2">${esc(t('assistant.empty.heading', 'What should we do?'))}</div>
2915
- <div class="asst-empty-body">${esc(t('assistant.empty.lead', 'I can use your Mac, look things up on the web, edit files and run commands, and remember or schedule things.'))}</div>
2916
3521
  <div class="asst-empty-greet"></div>
2917
- <div class="asst-empty-project" hidden></div>
2918
- <div class="asst-suggestions" role="list"></div>`;
3522
+ <div class="asst-suggestions" role="list" aria-label="${esc(t('assistant.empty.starters', 'Starters'))}"></div>
3523
+ <div class="asst-empty-project" role="group" aria-label="${esc(t('assistant.empty.context', 'Context'))}" hidden></div>`;
2919
3524
  const box = emptyEl.querySelector('.asst-empty-mascot');
2920
3525
  try {
2921
3526
  const rig = mascotLib.createMascot(box, { width: 120, height: 60 });
@@ -2923,8 +3528,8 @@ export function createRenderer(transcriptEl, hooks = {}) {
2923
3528
  hero.target = heroTarget(hero);
2924
3529
  wireEgg(hero);
2925
3530
  } catch { hero = null; }
2926
- // The project the conversation works in, under the lead: the host draws the picker (the brain
2927
- // picker's project field), so it and the toolbar's Working project are one control.
3531
+ // The project the conversation works in, on the context line under the starters: the host draws
3532
+ // the picker (the brain picker's project field), so it and the toolbar's Working project are one control.
2928
3533
  const projectSlot = emptyEl.querySelector('.asst-empty-project');
2929
3534
  let picker = null;
2930
3535
  try { picker = typeof hooks.mountEmptyProject === 'function' ? hooks.mountEmptyProject(projectSlot) : null; } catch { picker = null; }
@@ -2941,8 +3546,8 @@ export function createRenderer(transcriptEl, hooks = {}) {
2941
3546
  }
2942
3547
 
2943
3548
  /**
2944
- * The greeting SynaBun prepared for this return (public/shared/greeting/) takes the lead's place.
2945
- * The panel calls it once it knows the conversation is new: a restored one is never greeted.
3549
+ * The greeting SynaBun prepared for this return (public/shared/greeting/) sits between the heading
3550
+ * and the starters. The panel calls it once it knows the conversation is new: a restored one is never greeted.
2946
3551
  */
2947
3552
  function offerGreeting() {
2948
3553
  const owner = emptyEl;
@@ -2960,12 +3565,16 @@ export function createRenderer(transcriptEl, hooks = {}) {
2960
3565
  host.innerHTML = '';
2961
3566
  let list = [];
2962
3567
  try { list = typeof hooks.emptySuggestions === 'function' ? (hooks.emptySuggestions() || []) : []; } catch { list = []; }
3568
+ let i = 0;
2963
3569
  for (const s of list) {
2964
- // glyph | seam | label: the kind's prop, and the character acts it out while the suggestion is hovered or focused.
3570
+ // A ghost row: the kind's prop and the label (no capsule, no seam); it comes in staggered by --i,
3571
+ // and the character acts the kind out while the suggestion is hovered or focused.
2965
3572
  const chip = el('button', 'asst-chip asst-suggestion');
2966
3573
  chip.type = 'button';
2967
3574
  chip.setAttribute('role', 'listitem');
2968
3575
  chip.dataset.suggestion = s.id || '';
3576
+ chip.style.setProperty('--i', String(i + 1));
3577
+ i += 1;
2969
3578
  const kind = s.kind || SUGGESTION_KINDS[s.id] || null;
2970
3579
  if (kind) {
2971
3580
  chip.dataset.kind = kind;
@@ -2979,10 +3588,28 @@ export function createRenderer(transcriptEl, hooks = {}) {
2979
3588
  chip.addEventListener('blur', () => previewHero(null));
2980
3589
  }
2981
3590
  chip.appendChild(el('span', 'asst-chip-text', s.label));
2982
- chip.addEventListener('click', () => hooks.onSuggestion?.(s.template, s));
3591
+ if (s.meta) chip.appendChild(el('span', 'asst-chip-meta', s.meta));
3592
+ chip.addEventListener('click', () => {
3593
+ // The picked starter keeps the gold on-state while its text is the draft (setDrafting clears it);
3594
+ // a row with an action (the Workspace tour) fills nothing, so it is never "picked".
3595
+ if (!s.action) {
3596
+ for (const other of host.querySelectorAll('.is-picked')) other.classList.remove('is-picked');
3597
+ chip.classList.add('is-picked');
3598
+ }
3599
+ hooks.onSuggestion?.(s.template, s);
3600
+ });
2983
3601
  host.appendChild(chip);
2984
3602
  }
2985
3603
  host.hidden = !list.length;
3604
+ // The context line comes in after the last starter.
3605
+ emptyEl.querySelector('.asst-empty-project')?.style.setProperty('--i', String(list.length + 1));
3606
+ }
3607
+
3608
+ /** The composer holds a draft: the starters recede (the picked one stays lit) until it is cleared. */
3609
+ function setDrafting(on) {
3610
+ if (!emptyEl) return;
3611
+ emptyEl.classList.toggle('is-drafting', !!on);
3612
+ if (!on) for (const chip of emptyEl.querySelectorAll('.asst-suggestion.is-picked')) chip.classList.remove('is-picked');
2986
3613
  }
2987
3614
 
2988
3615
  /** The empty state's character tries a suggestion on (null: back to idle); never over a pose the director holds. */
@@ -3187,6 +3814,7 @@ export function createRenderer(transcriptEl, hooks = {}) {
3187
3814
  const content = Array.isArray(ev.message.content) ? ev.message.content : [];
3188
3815
  lastErrorText = typeof ev.message.content === 'string' ? ev.message.content.trim()
3189
3816
  : content.filter((b) => b?.type === 'text').map((b) => b.text).join('\n').trim();
3817
+ if (!parentId) offerLimitWait(lastErrorText, { brainError: true });
3190
3818
  }
3191
3819
  return true;
3192
3820
  }
@@ -3212,6 +3840,10 @@ export function createRenderer(transcriptEl, hooks = {}) {
3212
3840
  if (!parentId && !muted) grace = newestStage();
3213
3841
  if (grace) grace.holdUntil = Infinity;
3214
3842
  if (!parentId) endBursts(rootScope);
3843
+ // A journal's or a replay's turn ended here (live, finishTurn ends it): so did the route it ran.
3844
+ if (!parentId && muted) directRoute = null;
3845
+ // A turn that ended well: its last message's options in prose become items (no block of its own).
3846
+ if (!parentId && !ev.is_error && !String(ev.subtype || '').startsWith('error')) guessOffer();
3215
3847
  hooks.onResult?.(ev);
3216
3848
  if (errorText && errorText !== lastErrorText) appendError(errorText);
3217
3849
  if (!parentId) { lastErrorText = ''; rootScope.kept = null; releaseGrace(); }
@@ -3270,11 +3902,16 @@ export function createRenderer(transcriptEl, hooks = {}) {
3270
3902
  return true;
3271
3903
  case 'synabun.route': {
3272
3904
  const phase = ev.phase || 'decided';
3905
+ noteDirectRoute(ev.route, phase); // the journal's too: a conversation opened again keeps the route of its last turn
3273
3906
  if (hooks.onRouteEvent?.(ev)) return true; // an open card represents it
3274
3907
  if (phase === 'card' && silent) return true; // replayed card: the pending control re-sends it
3275
3908
  upsertRouteLine(ev.route || {}, phase);
3276
3909
  return true;
3277
3910
  }
3911
+ case 'synabun.continuation':
3912
+ // A "do it here" pick on another model runs as a turn of its own: the route it approved goes on there.
3913
+ if (lastDirect) directRoute = { ...lastDirect, turn: turnActive ? turnSeq : null, rackAt: rackSeq };
3914
+ return true;
3278
3915
  case 'synabun.clarify':
3279
3916
  // The card itself arrives as a control_request; its outcome is a line (the panel's card shows it live).
3280
3917
  if (ev.phase !== 'card') appendStatus(clarifyLine(ev), 'info');
@@ -3313,14 +3950,19 @@ export function createRenderer(transcriptEl, hooks = {}) {
3313
3950
  /**
3314
3951
  * The turn ended. `holdMs`: the panel plays a one-shot (success, error) on
3315
3952
  * the character on screen, so its stage stays up that long before it folds.
3953
+ * `guess`: the turn ended well, so options its last message offers in prose may become items.
3316
3954
  */
3317
- function finishTurn({ holdMs = 0 } = {}) {
3955
+ function finishTurn({ holdMs = 0, guess = true } = {}) {
3318
3956
  flushStream(rootScope);
3957
+ sealStreamRow(rootScope);
3319
3958
  if (rootScope.stream?.row) rootScope.stream.row.classList.remove('streaming');
3959
+ // The `result` usually guessed already; a turn that ended well without one gets it here (never a stopped or failed one).
3960
+ if (guess && !turn.errored) guessOffer();
3320
3961
  for (const scope of scopes.values()) flushStream(scope);
3321
3962
  closeStreamThinks();
3322
3963
  hideWorking();
3323
3964
  turnActive = false;
3965
+ if (directRoute && directRoute.turn === turnSeq) directRoute = null; // its turn is over
3324
3966
  const stage = grace || newestStage();
3325
3967
  grace = null;
3326
3968
  const hold = holdMs > 0 && !calm();
@@ -3356,11 +3998,14 @@ export function createRenderer(transcriptEl, hooks = {}) {
3356
3998
  }
3357
3999
  if (failed) console.warn(`[assistant] ${failed} of ${list.length} transcript entries could not be drawn`, firstError);
3358
4000
  flushStream(rootScope);
4001
+ sealStreamRow(rootScope);
3359
4002
  if (rootScope.stream?.row) rootScope.stream.row.classList.remove('streaming');
3360
4003
  for (const scope of scopes.values()) flushStream(scope);
3361
4004
  hideWorking();
3362
4005
  turnActive = false;
3363
4006
  for (const rack of [...racks]) renderRack(rack);
4007
+ // Drawn quietly: a card the history leaves open (nobody answered it yet) comes to life now.
4008
+ mascotChanged();
3364
4009
  scrollEnd(true);
3365
4010
  }
3366
4011
 
@@ -3402,6 +4047,14 @@ export function createRenderer(transcriptEl, hooks = {}) {
3402
4047
  if (rootScope.stream?.mdTimer) clearTimeout(rootScope.stream.mdTimer);
3403
4048
  if (rootScope.stream?.thinkTimer) clearTimeout(rootScope.stream.thinkTimer);
3404
4049
  scopes.clear();
4050
+ dropStages();
4051
+ limitDock.clear();
4052
+ for (const offer of offers) { try { offer.wait?.destroy(); } catch { /* gone */ } }
4053
+ offers.clear();
4054
+ dropAsks();
4055
+ lastTextRow = null;
4056
+ for (const card of runCards.values()) { try { card._limitWait?.destroy(); } catch { /* gone */ } }
4057
+ runActivity.clear();
3405
4058
  runCards.clear();
3406
4059
  routeLines.clear();
3407
4060
  autoRows.clear();
@@ -3417,6 +4070,8 @@ export function createRenderer(transcriptEl, hooks = {}) {
3417
4070
  dropHero();
3418
4071
  grace = null;
3419
4072
  turn = { calls: 0, last: null, errored: false, answerRow: null };
4073
+ directRoute = null;
4074
+ lastDirect = null;
3420
4075
  mcpWarned.clear();
3421
4076
  seen.clear();
3422
4077
  snapshot = null;
@@ -3430,7 +4085,7 @@ export function createRenderer(transcriptEl, hooks = {}) {
3430
4085
  return {
3431
4086
  el: $msgs,
3432
4087
  clear,
3433
- destroy: () => { hideWorking(); dropHero(); dropAll(); },
4088
+ destroy: () => { clear(); limitDock.destroy(); },
3434
4089
  appendUser,
3435
4090
  appendAssistantMarkdown,
3436
4091
  appendStatus,
@@ -3441,6 +4096,16 @@ export function createRenderer(transcriptEl, hooks = {}) {
3441
4096
  appendMemorySaved,
3442
4097
  appendMailbox,
3443
4098
  upsertRunCard,
4099
+ /** A run update that never adds a card (the tray's sources): the card, its stage and the route line follow it. */
4100
+ syncRun,
4101
+ /** After a reload: these runs' cards again, next to the calls that dispatched them (see restoreRuns). */
4102
+ restoreRuns,
4103
+ /** What a run's worker does now: its stage's scene and step follow it. */
4104
+ setRunActivity,
4105
+ /** Scroll a run's card into view and focus it. */
4106
+ revealRun,
4107
+ /** For tests and devtools: a run card's stage ({ open, phase, scene, final, pose, step, label }) or null. */
4108
+ stageOf: (runId) => stages.get(runId)?.state() || null,
3444
4109
  patchRunUsage,
3445
4110
  upsertRouteLine,
3446
4111
  registerRouteLine,
@@ -3453,10 +4118,14 @@ export function createRenderer(transcriptEl, hooks = {}) {
3453
4118
  claimEvent,
3454
4119
  /** A reattach's buffered packets minus what is on screen (see planReplay). */
3455
4120
  planReplay,
4121
+ /** What the reattach said (a turn runs or not): a route the journal left open goes only to that turn (see reattached). */
4122
+ reattached,
3456
4123
  /** Forget what was drawn (a brain switch starts a new id space). */
3457
4124
  forgetSeen: () => { seen.clear(); snapshot = null; },
3458
4125
  /** A card waits on the user: `toolUseId` names the call it holds (the newest live stage when unknown); false clears it. */
3459
4126
  setWaiting,
4127
+ /** The session's usage-limit report changed: this turn's waiting card counts to its reset time. */
4128
+ noteLimit,
3460
4129
  finishTurn,
3461
4130
  renderTranscript,
3462
4131
  showWorking,
@@ -3465,6 +4134,7 @@ export function createRenderer(transcriptEl, hooks = {}) {
3465
4134
  showEmpty,
3466
4135
  offerGreeting,
3467
4136
  refreshEmpty: renderSuggestions,
4137
+ setDrafting,
3468
4138
  isEmpty: () => !!emptyEl,
3469
4139
  scrollEnd,
3470
4140
  isNearBottom,