synabun 2.0.10 → 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 (517) hide show
  1. package/CHANGELOG.md +86 -0
  2. package/README.md +25 -2
  3. package/THIRD-PARTY-LICENSES.md +22 -0
  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/lib/claude-hooks.js +172 -18
  10. package/lib/client-config-repair.js +17 -8
  11. package/lib/desktop-app.js +7 -1
  12. package/lib/external-tools.js +1 -1
  13. package/lib/first-launch-protection.js +57 -12
  14. package/lib/mcp-dist.js +176 -0
  15. package/lib/release-npm-ci.mjs +138 -0
  16. package/lib/release-publish.mjs +91 -0
  17. package/lib/release-scan.mjs +50 -0
  18. package/lib/server-crash-record.js +123 -0
  19. package/lib/server-exit.js +318 -0
  20. package/lib/server-supervisor.js +508 -0
  21. package/lib/start-launcher.js +37 -4
  22. package/lib/tools-cli.js +45 -0
  23. package/lib/update-snapshot.js +228 -0
  24. package/lib/virustotal.mjs +192 -0
  25. package/lib/windows-crash-event.js +269 -0
  26. package/mcp-server/dist/http.d.ts +20 -1
  27. package/mcp-server/dist/http.d.ts.map +1 -1
  28. package/mcp-server/dist/http.js +59 -12
  29. package/mcp-server/dist/http.js.map +1 -1
  30. package/mcp-server/dist/index.d.ts +33 -27
  31. package/mcp-server/dist/index.d.ts.map +1 -1
  32. package/mcp-server/dist/index.js +128 -143
  33. package/mcp-server/dist/index.js.map +1 -1
  34. package/mcp-server/dist/preload.js +1 -1
  35. package/mcp-server/dist/preload.js.map +1 -1
  36. package/mcp-server/dist/services/codex-config-heal.d.ts +6 -2
  37. package/mcp-server/dist/services/codex-config-heal.d.ts.map +1 -1
  38. package/mcp-server/dist/services/codex-config-heal.js +81 -28
  39. package/mcp-server/dist/services/codex-config-heal.js.map +1 -1
  40. package/mcp-server/dist/services/memory-judge-backfill.d.ts +6 -0
  41. package/mcp-server/dist/services/memory-judge-backfill.d.ts.map +1 -1
  42. package/mcp-server/dist/services/memory-judge-backfill.js +7 -1
  43. package/mcp-server/dist/services/memory-judge-backfill.js.map +1 -1
  44. package/mcp-server/dist/services/neural-interface.d.ts +16 -10
  45. package/mcp-server/dist/services/neural-interface.d.ts.map +1 -1
  46. package/mcp-server/dist/services/neural-interface.js +67 -11
  47. package/mcp-server/dist/services/neural-interface.js.map +1 -1
  48. package/mcp-server/dist/services/server-instructions.d.ts +7 -2
  49. package/mcp-server/dist/services/server-instructions.d.ts.map +1 -1
  50. package/mcp-server/dist/services/server-instructions.js +20 -62
  51. package/mcp-server/dist/services/server-instructions.js.map +1 -1
  52. package/mcp-server/dist/services/tool-catalog.d.ts +120 -0
  53. package/mcp-server/dist/services/tool-catalog.d.ts.map +1 -0
  54. package/mcp-server/dist/services/tool-catalog.js +339 -0
  55. package/mcp-server/dist/services/tool-catalog.js.map +1 -0
  56. package/mcp-server/dist/services/tool-list-refresh-barrier.d.ts +12 -0
  57. package/mcp-server/dist/services/tool-list-refresh-barrier.d.ts.map +1 -0
  58. package/mcp-server/dist/services/tool-list-refresh-barrier.js +40 -0
  59. package/mcp-server/dist/services/tool-list-refresh-barrier.js.map +1 -0
  60. package/mcp-server/dist/services/tool-list.d.ts +108 -0
  61. package/mcp-server/dist/services/tool-list.d.ts.map +1 -0
  62. package/mcp-server/dist/services/tool-list.js +334 -0
  63. package/mcp-server/dist/services/tool-list.js.map +1 -0
  64. package/mcp-server/dist/services/tool-runtime.d.ts +161 -0
  65. package/mcp-server/dist/services/tool-runtime.d.ts.map +1 -0
  66. package/mcp-server/dist/services/tool-runtime.js +400 -0
  67. package/mcp-server/dist/services/tool-runtime.js.map +1 -0
  68. package/mcp-server/dist/services/whiteboard-geometry.d.ts +26 -2
  69. package/mcp-server/dist/services/whiteboard-geometry.d.ts.map +1 -1
  70. package/mcp-server/dist/services/whiteboard-geometry.js +91 -8
  71. package/mcp-server/dist/services/whiteboard-geometry.js.map +1 -1
  72. package/mcp-server/dist/tools/agents.d.ts +10 -10
  73. package/mcp-server/dist/tools/agents.d.ts.map +1 -1
  74. package/mcp-server/dist/tools/agents.js +22 -13
  75. package/mcp-server/dist/tools/agents.js.map +1 -1
  76. package/mcp-server/dist/tools/browser-batch.d.ts +3 -3
  77. package/mcp-server/dist/tools/image-tools.d.ts +1 -1
  78. package/mcp-server/dist/tools/image-tools.js +1 -1
  79. package/mcp-server/dist/tools/skill-tools.d.ts +505 -0
  80. package/mcp-server/dist/tools/skill-tools.d.ts.map +1 -0
  81. package/mcp-server/dist/tools/skill-tools.js +639 -0
  82. package/mcp-server/dist/tools/skill-tools.js.map +1 -0
  83. package/mcp-server/dist/tools/skill.d.ts +7 -0
  84. package/mcp-server/dist/tools/skill.d.ts.map +1 -0
  85. package/mcp-server/dist/tools/skill.js +11 -0
  86. package/mcp-server/dist/tools/skill.js.map +1 -0
  87. package/mcp-server/dist/tools/tool-groups.d.ts +68 -0
  88. package/mcp-server/dist/tools/tool-groups.d.ts.map +1 -0
  89. package/mcp-server/dist/tools/tool-groups.js +147 -0
  90. package/mcp-server/dist/tools/tool-groups.js.map +1 -0
  91. package/mcp-server/dist/tools/whiteboard-format.d.ts.map +1 -1
  92. package/mcp-server/dist/tools/whiteboard-format.js +7 -4
  93. package/mcp-server/dist/tools/whiteboard-format.js.map +1 -1
  94. package/mcp-server/dist/tools/whiteboard-tools.d.ts +64 -13
  95. package/mcp-server/dist/tools/whiteboard-tools.d.ts.map +1 -1
  96. package/mcp-server/dist/tools/whiteboard-tools.js +30 -5
  97. package/mcp-server/dist/tools/whiteboard-tools.js.map +1 -1
  98. package/mcp-server/package-lock.json +2 -2
  99. package/mcp-server/package.json +1 -1
  100. package/mcp-server/src/http.ts +75 -12
  101. package/mcp-server/src/index.ts +147 -154
  102. package/mcp-server/src/preload.ts +12 -12
  103. package/mcp-server/src/services/codex-config-heal.ts +80 -29
  104. package/mcp-server/src/services/memory-judge-backfill.ts +7 -1
  105. package/mcp-server/src/services/neural-interface.ts +79 -20
  106. package/mcp-server/src/services/server-instructions.ts +27 -63
  107. package/mcp-server/src/services/tool-catalog.ts +403 -0
  108. package/mcp-server/src/services/tool-list-refresh-barrier.ts +33 -0
  109. package/mcp-server/src/services/tool-list.ts +410 -0
  110. package/mcp-server/src/services/tool-runtime.ts +457 -0
  111. package/mcp-server/src/services/whiteboard-geometry.ts +94 -9
  112. package/mcp-server/src/tools/agents.ts +22 -14
  113. package/mcp-server/src/tools/image-tools.ts +1 -1
  114. package/mcp-server/src/tools/skill-tools.ts +589 -0
  115. package/mcp-server/src/tools/skill.ts +13 -0
  116. package/mcp-server/src/tools/tool-groups.ts +164 -0
  117. package/mcp-server/src/tools/whiteboard-format.ts +9 -3
  118. package/mcp-server/src/tools/whiteboard-tools.ts +39 -5
  119. package/neural-interface/i18n/en.json +3463 -200
  120. package/neural-interface/i18n/pt-BR.json +3411 -151
  121. package/neural-interface/lib/assistant-api.js +127 -28
  122. package/neural-interface/lib/assistant-brains/claude.js +4 -3
  123. package/neural-interface/lib/assistant-brains/codex-browser-policy-hook.mjs +12 -2
  124. package/neural-interface/lib/assistant-brains/codex-route-gate-hook.mjs +32 -9
  125. package/neural-interface/lib/assistant-brains/codex.js +9 -5
  126. package/neural-interface/lib/assistant-brains/opencode.js +120 -54
  127. package/neural-interface/lib/assistant-brains/opencode2-browser-policy/index.js +28 -13
  128. package/neural-interface/lib/assistant-brains/opencode2-route-gate/index.js +59 -19
  129. package/neural-interface/lib/assistant-catalog.js +6 -7
  130. package/neural-interface/lib/assistant-dispatch.js +871 -156
  131. package/neural-interface/lib/assistant-envelope.js +3 -1
  132. package/neural-interface/lib/assistant-persona.js +80 -11
  133. package/neural-interface/lib/assistant-plan-permissions.js +10 -9
  134. package/neural-interface/lib/assistant-route-gate-codex.js +3 -0
  135. package/neural-interface/lib/assistant-route-gate.js +6 -4
  136. package/neural-interface/lib/assistant-router.js +13 -8
  137. package/neural-interface/lib/assistant-runtime.js +56 -18
  138. package/neural-interface/lib/assistant-task-prompt.js +78 -14
  139. package/neural-interface/lib/assistant-tools.js +51 -0
  140. package/neural-interface/lib/backup-service.js +620 -3
  141. package/neural-interface/lib/backup-zip-reader.js +10 -5
  142. package/neural-interface/lib/backup-zip-writer.js +257 -0
  143. package/neural-interface/lib/browser-execution.js +10 -2
  144. package/neural-interface/lib/browser-guard-cdp.js +83 -0
  145. package/neural-interface/lib/browser-network-guard.js +284 -0
  146. package/neural-interface/lib/browser-policy-roots.js +64 -0
  147. package/neural-interface/lib/browser-tool-policy.js +186 -31
  148. package/neural-interface/lib/claude-agent-bridge.js +4 -2
  149. package/neural-interface/lib/claude-hooks-status.js +103 -0
  150. package/neural-interface/lib/claude-temporary.js +5 -11
  151. package/neural-interface/lib/cli-install.js +18 -138
  152. package/neural-interface/lib/cli-resolver.js +4 -1
  153. package/neural-interface/lib/cli-update-planner.js +32 -31
  154. package/neural-interface/lib/cloudflared/tunnel.js +42 -2
  155. package/neural-interface/lib/codex-mcp-approvals.js +11 -0
  156. package/neural-interface/lib/codex-worker-app-server.js +172 -0
  157. package/neural-interface/lib/confined-fs.js +30 -3
  158. package/neural-interface/lib/crash-notice-api.js +98 -0
  159. package/neural-interface/lib/editor-completion-opencode.js +68 -68
  160. package/neural-interface/lib/exec-loop-command.js +4 -8
  161. package/neural-interface/lib/http-guards.js +8 -2
  162. package/neural-interface/lib/install-details.js +4 -1
  163. package/neural-interface/lib/mcp-agent-servers.js +67 -0
  164. package/neural-interface/lib/mcp-client-config.js +0 -29
  165. package/neural-interface/lib/mcp-external-servers.js +323 -0
  166. package/neural-interface/lib/{mcp-profile-refresh-coordinator.js → mcp-tools-refresh-coordinator.js} +4 -4
  167. package/neural-interface/lib/morelogin.js +30 -0
  168. package/neural-interface/lib/native-loop-providers.js +547 -47
  169. package/neural-interface/lib/native-loop-runtime.js +268 -41
  170. package/neural-interface/lib/opencode-config.js +557 -0
  171. package/neural-interface/lib/opencode-db-schema.js +17 -34
  172. package/neural-interface/lib/opencode-history.js +48 -218
  173. package/neural-interface/lib/opencode-mcp-ready.js +108 -0
  174. package/neural-interface/lib/opencode-native-boundary.js +44 -0
  175. package/neural-interface/lib/opencode-protocol.js +249 -112
  176. package/neural-interface/lib/opencode-runtime-config.js +32 -22
  177. package/neural-interface/lib/opencode-serve-records.js +39 -21
  178. package/neural-interface/lib/opencode-v2-client.js +48 -153
  179. package/neural-interface/lib/opencode-v2-ws-requests.js +36 -210
  180. package/neural-interface/lib/opencode-worker-data.js +27 -0
  181. package/neural-interface/lib/opencode2-client.js +73 -37
  182. package/neural-interface/lib/opencode2-proxy.js +23 -63
  183. package/neural-interface/lib/opencode2-translate.js +51 -2
  184. package/neural-interface/lib/owner-authority.js +249 -0
  185. package/neural-interface/lib/page-session.js +275 -0
  186. package/neural-interface/lib/peer-identity.js +763 -0
  187. package/neural-interface/lib/request-gate.js +17 -2
  188. package/neural-interface/lib/rulesets/api.js +1 -1
  189. package/neural-interface/lib/rulesets/fs-safe.js +1 -1
  190. package/neural-interface/lib/rulesets/installer.js +92 -27
  191. package/neural-interface/lib/rulesets/json-edit.js +1 -1
  192. package/neural-interface/lib/session-cache-builder.js +17 -30
  193. package/neural-interface/lib/session-title-generator.js +58 -36
  194. package/neural-interface/lib/skills/api-v2.js +495 -0
  195. package/neural-interface/lib/skills/bundle.js +418 -0
  196. package/neural-interface/lib/skills/catalog.js +294 -0
  197. package/neural-interface/lib/skills/creation-api.js +191 -0
  198. package/neural-interface/lib/skills/creation-brief.js +247 -0
  199. package/neural-interface/lib/skills/creation-sources.js +267 -0
  200. package/neural-interface/lib/skills/creation.js +843 -0
  201. package/neural-interface/lib/skills/deploy.js +564 -0
  202. package/neural-interface/lib/skills/digest.js +119 -0
  203. package/neural-interface/lib/skills/discover.js +111 -0
  204. package/neural-interface/lib/skills/draft-confinement.js +214 -0
  205. package/neural-interface/lib/skills/eval-confinement.js +397 -0
  206. package/neural-interface/lib/skills/eval-sandbox.js +280 -0
  207. package/neural-interface/lib/skills/eval-trace.js +119 -0
  208. package/neural-interface/lib/skills/evals-api.js +212 -0
  209. package/neural-interface/lib/skills/evals.js +1573 -0
  210. package/neural-interface/lib/skills/export.js +200 -0
  211. package/neural-interface/lib/skills/frontmatter.js +64 -0
  212. package/neural-interface/lib/skills/health.js +75 -0
  213. package/neural-interface/lib/skills/json-edit.js +165 -0
  214. package/neural-interface/lib/skills/known-bridge-digests.json +387 -0
  215. package/neural-interface/lib/skills/known-bridges.js +96 -0
  216. package/neural-interface/lib/skills/library.js +823 -0
  217. package/neural-interface/lib/skills/local-store.js +21 -0
  218. package/neural-interface/lib/skills/migration.js +433 -0
  219. package/neural-interface/lib/skills/operations.js +45 -0
  220. package/neural-interface/lib/skills/ownership.js +427 -0
  221. package/neural-interface/lib/skills/packages.js +88 -0
  222. package/neural-interface/lib/skills/reviews.js +212 -0
  223. package/neural-interface/lib/skills/revision.js +80 -0
  224. package/neural-interface/lib/skills/roots.js +262 -0
  225. package/neural-interface/lib/skills/runtime-checks.js +355 -0
  226. package/neural-interface/lib/skills/scan-assets.js +252 -0
  227. package/neural-interface/lib/skills/scan-context.js +26 -0
  228. package/neural-interface/lib/skills/scan-shell.js +324 -0
  229. package/neural-interface/lib/skills/scan.js +364 -0
  230. package/neural-interface/lib/skills/scanners.js +54 -0
  231. package/neural-interface/lib/skills/service.js +522 -0
  232. package/neural-interface/lib/skills/telemetry.js +113 -0
  233. package/neural-interface/lib/sqlite-integrity-worker.js +22 -0
  234. package/neural-interface/lib/tool-lists.js +124 -0
  235. package/neural-interface/lib/whatsapp/bridge.js +6 -3
  236. package/neural-interface/lib/worker-confinement.js +1192 -0
  237. package/neural-interface/lib/worker-guard-setting.js +56 -0
  238. package/neural-interface/lib/worker-mcp-channel.js +175 -0
  239. package/neural-interface/lib/worker-relay-ports.js +227 -0
  240. package/neural-interface/lib/worker-resume.js +229 -0
  241. package/neural-interface/lib/worker-sandbox.js +299 -0
  242. package/neural-interface/package-lock.json +18 -12
  243. package/neural-interface/package.json +2 -2
  244. package/neural-interface/public/index.html +3 -1
  245. package/neural-interface/public/index2d.html +19 -11
  246. package/neural-interface/public/onboarding.html +2 -2
  247. package/neural-interface/public/shared/account-menu.js +8 -5
  248. package/neural-interface/public/shared/agent-runtime-options.js +35 -19
  249. package/neural-interface/public/shared/api.js +1527 -1336
  250. package/neural-interface/public/shared/app-frame.js +192 -0
  251. package/neural-interface/public/shared/assistant/asst-agents-dock.js +33 -2
  252. package/neural-interface/public/shared/assistant/asst-ask-styles.js +270 -0
  253. package/neural-interface/public/shared/assistant/asst-ask-tail.js +118 -0
  254. package/neural-interface/public/shared/assistant/asst-ask.js +397 -0
  255. package/neural-interface/public/shared/assistant/asst-brain-picker.js +49 -35
  256. package/neural-interface/public/shared/assistant/asst-control.js +157 -26
  257. package/neural-interface/public/shared/assistant/asst-followups-card.js +383 -0
  258. package/neural-interface/public/shared/assistant/asst-followups.js +417 -0
  259. package/neural-interface/public/shared/assistant/asst-limit-dock.js +40 -0
  260. package/neural-interface/public/shared/assistant/asst-limit-wait.js +294 -0
  261. package/neural-interface/public/shared/assistant/asst-limit.js +168 -0
  262. package/neural-interface/public/shared/assistant/asst-mascot.js +49 -12
  263. package/neural-interface/public/shared/assistant/asst-menu.js +7 -6
  264. package/neural-interface/public/shared/assistant/asst-panel.js +106 -11
  265. package/neural-interface/public/shared/assistant/asst-rack-styles.js +66 -49
  266. package/neural-interface/public/shared/assistant/asst-render.js +401 -77
  267. package/neural-interface/public/shared/assistant/asst-route.js +83 -12
  268. package/neural-interface/public/shared/assistant/asst-scenes.js +2 -0
  269. package/neural-interface/public/shared/assistant/asst-sidepanel.js +67 -4
  270. package/neural-interface/public/shared/assistant/asst-slash.js +9 -7
  271. package/neural-interface/public/shared/assistant/asst-state.js +11 -15
  272. package/neural-interface/public/shared/assistant/asst-stop.js +127 -0
  273. package/neural-interface/public/shared/assistant/asst-styles.js +59 -21
  274. package/neural-interface/public/shared/assistant/asst-tool-kinds.js +156 -16
  275. package/neural-interface/public/shared/automation-context.js +5 -2
  276. package/neural-interface/public/shared/cc-dropdown.js +89 -19
  277. package/neural-interface/public/shared/cdx/cdx-context-menu.js +10 -9
  278. package/neural-interface/public/shared/cdx/cdx-copy.js +16 -0
  279. package/neural-interface/public/shared/cdx/cdx-output-model.js +22 -1
  280. package/neural-interface/public/shared/cdx/cdx-output.js +8 -7
  281. package/neural-interface/public/shared/cdx/cdx-panel.js +45 -149
  282. package/neural-interface/public/shared/cdx/cdx-protocol.js +28 -1
  283. package/neural-interface/public/shared/cdx/cdx-render.js +47 -29
  284. package/neural-interface/public/shared/cdx/cdx-requests.js +3 -3
  285. package/neural-interface/public/shared/cdx/cdx-skill-hints.js +54 -0
  286. package/neural-interface/public/shared/cdx/cdx-styles.js +11 -3
  287. package/neural-interface/public/shared/cdx/cdx-tabs.js +52 -39
  288. package/neural-interface/public/shared/cdx/cdx-ws.js +0 -1
  289. package/neural-interface/public/shared/cli-install-view.js +4 -8
  290. package/neural-interface/public/shared/cli-status.js +2 -2
  291. package/neural-interface/public/shared/constants.js +2 -2
  292. package/neural-interface/public/shared/context-menu-areas.js +34 -0
  293. package/neural-interface/public/shared/context-menu-logic.js +243 -0
  294. package/neural-interface/public/shared/context-menu-model.js +366 -0
  295. package/neural-interface/public/shared/context-menu.js +372 -0
  296. package/neural-interface/public/shared/cp/cp-context-menu.js +10 -9
  297. package/neural-interface/public/shared/crash-notice-view.js +168 -0
  298. package/neural-interface/public/shared/desktop-update-view.js +20 -0
  299. package/neural-interface/public/shared/editor/editor-panel-logic.js +2 -2
  300. package/neural-interface/public/shared/html-shell.js +9 -11
  301. package/neural-interface/public/shared/i18n.js +2 -0
  302. package/neural-interface/public/shared/ocp-v2/ocp-v2-approvals.js +7 -7
  303. package/neural-interface/public/shared/ocp-v2/ocp-v2-binding.js +4 -4
  304. package/neural-interface/public/shared/ocp-v2/ocp-v2-changes-logic.js +3 -101
  305. package/neural-interface/public/shared/ocp-v2/ocp-v2-childpanel.js +2 -0
  306. package/neural-interface/public/shared/ocp-v2/ocp-v2-compact-button.js +1 -3
  307. package/neural-interface/public/shared/ocp-v2/ocp-v2-composer-logic.js +2 -6
  308. package/neural-interface/public/shared/ocp-v2/ocp-v2-context-menu.js +11 -11
  309. package/neural-interface/public/shared/ocp-v2/ocp-v2-context-model.js +0 -3
  310. package/neural-interface/public/shared/ocp-v2/ocp-v2-events.js +8 -4
  311. package/neural-interface/public/shared/ocp-v2/ocp-v2-manager.js +7 -7
  312. package/neural-interface/public/shared/ocp-v2/ocp-v2-outcome-notifications.js +16 -0
  313. package/neural-interface/public/shared/ocp-v2/ocp-v2-panel.js +47 -252
  314. package/neural-interface/public/shared/ocp-v2/ocp-v2-plan.js +1 -2
  315. package/neural-interface/public/shared/ocp-v2/ocp-v2-projectbar.js +95 -202
  316. package/neural-interface/public/shared/ocp-v2/ocp-v2-render.js +6 -21
  317. package/neural-interface/public/shared/ocp-v2/ocp-v2-send.js +4 -4
  318. package/neural-interface/public/shared/ocp-v2/ocp-v2-session-actions.js +8 -61
  319. package/neural-interface/public/shared/ocp-v2/ocp-v2-sessions-logic.js +2 -91
  320. package/neural-interface/public/shared/ocp-v2/ocp-v2-slash-hints.js +16 -15
  321. package/neural-interface/public/shared/ocp-v2/ocp-v2-state.js +8 -21
  322. package/neural-interface/public/shared/ocp-v2/ocp-v2-status-logic.js +1 -99
  323. package/neural-interface/public/shared/ocp-v2/ocp-v2-status.js +5 -35
  324. package/neural-interface/public/shared/ocp-v2/ocp-v2-styles.js +0 -21
  325. package/neural-interface/public/shared/ocp-v2/{ocp-v2-profile-inheritance.js → ocp-v2-tools-inheritance.js} +16 -16
  326. package/neural-interface/public/shared/ocp-v2/ocp-v2-ws.js +14 -47
  327. package/neural-interface/public/shared/page-session.js +96 -0
  328. package/neural-interface/public/shared/registry.js +5 -3
  329. package/neural-interface/public/shared/retired-2d.js +3 -0
  330. package/neural-interface/public/shared/schedules-view.js +195 -61
  331. package/neural-interface/public/shared/settings/settings-claude-hooks.js +135 -0
  332. package/neural-interface/public/shared/settings/settings-data.js +3 -1
  333. package/neural-interface/public/shared/settings/settings-ia.js +6 -6
  334. package/neural-interface/public/shared/settings/settings-opencode.js +89 -0
  335. package/neural-interface/public/shared/settings/settings-skill-folders-pane.js +265 -0
  336. package/neural-interface/public/shared/settings/settings-skill-folders.js +130 -0
  337. package/neural-interface/public/shared/settings/settings-worker-guard.js +72 -0
  338. package/neural-interface/public/shared/skills/ss-args.js +96 -0
  339. package/neural-interface/public/shared/skills/ss-creation.js +219 -0
  340. package/neural-interface/public/shared/skills/ss-dialog.js +130 -0
  341. package/neural-interface/public/shared/skills/ss-doc.js +173 -0
  342. package/neural-interface/public/shared/skills/ss-eval-adapters.js +269 -0
  343. package/neural-interface/public/shared/skills/ss-eval-grade.js +359 -0
  344. package/neural-interface/public/shared/skills/ss-eval-optimise.js +153 -0
  345. package/neural-interface/public/shared/skills/ss-eval-plan.js +315 -0
  346. package/neural-interface/public/shared/skills/ss-evals.js +417 -0
  347. package/neural-interface/public/shared/skills/ss-form.js +159 -0
  348. package/neural-interface/public/shared/skills/ss-frontmatter.js +376 -0
  349. package/neural-interface/public/shared/skills/ss-ids.js +32 -0
  350. package/neural-interface/public/shared/skills/ss-lint.js +141 -0
  351. package/neural-interface/public/shared/skills/ss-presets.js +117 -0
  352. package/neural-interface/public/shared/skills/ss-profiles.js +475 -0
  353. package/neural-interface/public/shared/skills/ss-resources.js +165 -0
  354. package/neural-interface/public/shared/skills/ss-review-view.js +190 -0
  355. package/neural-interface/public/shared/skills/ss-tabs.js +79 -0
  356. package/neural-interface/public/shared/skills/ss-unicode.js +135 -0
  357. package/neural-interface/public/shared/skills/studio/sx-actions.js +222 -0
  358. package/neural-interface/public/shared/skills/studio/sx-cm.js +101 -0
  359. package/neural-interface/public/shared/skills/studio/sx-create-ai-view.js +749 -0
  360. package/neural-interface/public/shared/skills/studio/sx-create-ai.js +236 -0
  361. package/neural-interface/public/shared/skills/studio/sx-create-view.js +254 -0
  362. package/neural-interface/public/shared/skills/studio/sx-create.js +126 -0
  363. package/neural-interface/public/shared/skills/studio/sx-diff-view.js +43 -0
  364. package/neural-interface/public/shared/skills/studio/sx-diff.js +133 -0
  365. package/neural-interface/public/shared/skills/studio/sx-docs.js +480 -0
  366. package/neural-interface/public/shared/skills/studio/sx-dom.js +229 -0
  367. package/neural-interface/public/shared/skills/studio/sx-edit-view.js +848 -0
  368. package/neural-interface/public/shared/skills/studio/sx-editor-model.js +193 -0
  369. package/neural-interface/public/shared/skills/studio/sx-errors.js +22 -0
  370. package/neural-interface/public/shared/skills/studio/sx-geometry.js +82 -0
  371. package/neural-interface/public/shared/skills/studio/sx-import.js +262 -0
  372. package/neural-interface/public/shared/skills/studio/sx-inspector.js +308 -0
  373. package/neural-interface/public/shared/skills/studio/sx-install-flow.js +279 -0
  374. package/neural-interface/public/shared/skills/studio/sx-install.js +296 -0
  375. package/neural-interface/public/shared/skills/studio/sx-library-view.js +639 -0
  376. package/neural-interface/public/shared/skills/studio/sx-library.js +475 -0
  377. package/neural-interface/public/shared/skills/studio/sx-main.js +681 -0
  378. package/neural-interface/public/shared/skills/studio/sx-operations-view.js +98 -0
  379. package/neural-interface/public/shared/skills/studio/sx-operations.js +11 -0
  380. package/neural-interface/public/shared/skills/studio/sx-plan.js +158 -0
  381. package/neural-interface/public/shared/skills/studio/sx-publish-view.js +621 -0
  382. package/neural-interface/public/shared/skills/studio/sx-review-view.js +369 -0
  383. package/neural-interface/public/shared/skills/studio/sx-review.js +148 -0
  384. package/neural-interface/public/shared/skills/studio/sx-scan.js +35 -0
  385. package/neural-interface/public/shared/skills/studio/sx-snippets.js +96 -0
  386. package/neural-interface/public/shared/skills/studio/sx-subject.js +248 -0
  387. package/neural-interface/public/shared/skills/studio/sx-test-view.js +994 -0
  388. package/neural-interface/public/shared/skills/studio/sx-test.js +328 -0
  389. package/neural-interface/public/shared/skills/studio/sx-versions-view.js +241 -0
  390. package/neural-interface/public/shared/skills/studio/sx-workspace-view.js +425 -0
  391. package/neural-interface/public/shared/styleguide/sg-panel.js +4 -2
  392. package/neural-interface/public/shared/styles.css +1525 -1779
  393. package/neural-interface/public/shared/synabun-mascot.js +1115 -9
  394. package/neural-interface/public/shared/tool-list-picker.js +55 -0
  395. package/neural-interface/public/shared/tool-list-storage.js +24 -0
  396. package/neural-interface/public/shared/tool-list-view.js +112 -0
  397. package/neural-interface/public/shared/tour/tour-actor.js +268 -0
  398. package/neural-interface/public/shared/tour/tour-beats.js +336 -0
  399. package/neural-interface/public/shared/tour/tour-engine.js +884 -0
  400. package/neural-interface/public/shared/tour/tour-ink.js +390 -0
  401. package/neural-interface/public/shared/tour/tour-stage.js +440 -0
  402. package/neural-interface/public/shared/tour/tour-state.js +40 -0
  403. package/neural-interface/public/shared/tour/tour-tips.js +99 -0
  404. package/neural-interface/public/shared/tour/tour.css +144 -0
  405. package/neural-interface/public/shared/ui-automation-studio.js +91 -128
  406. package/neural-interface/public/shared/ui-claude-panel.js +77 -5
  407. package/neural-interface/public/shared/ui-command-runner.js +6 -3
  408. package/neural-interface/public/shared/ui-context-menu.js +295 -0
  409. package/neural-interface/public/shared/ui-cost-widget.js +57 -59
  410. package/neural-interface/public/shared/ui-crash-notice.js +187 -0
  411. package/neural-interface/public/shared/ui-detail.js +15 -3
  412. package/neural-interface/public/shared/ui-image-gallery.js +50 -47
  413. package/neural-interface/public/shared/ui-keybinds.js +26 -1
  414. package/neural-interface/public/shared/ui-menubar.js +8 -18
  415. package/neural-interface/public/shared/ui-native-loop-router-state.js +5 -0
  416. package/neural-interface/public/shared/ui-native-loop-router.js +7 -1
  417. package/neural-interface/public/shared/ui-navbar.js +4 -24
  418. package/neural-interface/public/shared/ui-panels.js +96 -65
  419. package/neural-interface/public/shared/ui-schedules-studio.js +507 -423
  420. package/neural-interface/public/shared/ui-sessions.js +5 -3
  421. package/neural-interface/public/shared/ui-settings.js +594 -462
  422. package/neural-interface/public/shared/ui-sidepanel-sessions.js +1 -1
  423. package/neural-interface/public/shared/ui-sidepanel-state.js +23 -12
  424. package/neural-interface/public/shared/ui-sidepanel-windows.js +16 -11
  425. package/neural-interface/public/shared/ui-skills.js +33 -2830
  426. package/neural-interface/public/shared/ui-styleguide.js +5 -2
  427. package/neural-interface/public/shared/ui-sync.js +15 -5
  428. package/neural-interface/public/shared/ui-terminal.js +124 -180
  429. package/neural-interface/public/shared/ui-tool-updates.js +3 -1
  430. package/neural-interface/public/shared/ui-tooltip.js +10 -5
  431. package/neural-interface/public/shared/ui-trash.js +6 -4
  432. package/neural-interface/public/shared/ui-tutorial-layout.js +3 -117
  433. package/neural-interface/public/shared/ui-tutorial-preview.js +103 -0
  434. package/neural-interface/public/shared/ui-tutorial-steps.js +3 -65
  435. package/neural-interface/public/shared/ui-tutorial.css +47 -100
  436. package/neural-interface/public/shared/ui-tutorial.js +285 -509
  437. package/neural-interface/public/shared/ui-update.js +38 -2
  438. package/neural-interface/public/shared/ui-whiteboard.js +311 -58
  439. package/neural-interface/public/shared/ui-worker-end.js +4 -0
  440. package/neural-interface/public/shared/ui-workspaces.js +3 -2
  441. package/neural-interface/public/shared/view-variant.js +66 -0
  442. package/neural-interface/public/shared/whatsapp/wa-view.js +1 -1
  443. package/neural-interface/public/shared/whiteboard-list.js +172 -0
  444. package/neural-interface/public/shared/worker-end.js +10 -3
  445. package/neural-interface/public/shared/worker-restart.js +12 -0
  446. package/neural-interface/public/sidepanel-session.html +1 -1
  447. package/neural-interface/public/variant/2d/background.js +2 -1
  448. package/neural-interface/public/variant/2d/context-menu.js +8 -2
  449. package/neural-interface/public/variant/2d/graph.js +42 -20
  450. package/neural-interface/public/variant/2d/hulls.js +14 -9
  451. package/neural-interface/public/variant/2d/lasso.js +11 -7
  452. package/neural-interface/public/variant/2d/main.js +32 -1
  453. package/neural-interface/public/variant/2d/minimap.js +11 -13
  454. package/neural-interface/public/variant/3d/main.js +69 -6
  455. package/neural-interface/public/variant/3d/map-controls.js +24 -9
  456. package/neural-interface/public/vendor/caveat/OFL.txt +93 -0
  457. package/neural-interface/public/vendor/caveat/caveat-latin-400-normal.woff2 +0 -0
  458. package/neural-interface/public/vendor/caveat/caveat-latin-700-normal.woff2 +0 -0
  459. package/neural-interface/public/vendor/caveat/caveat.css +17 -0
  460. package/neural-interface/public/vendor/caveat/manifest.json +12 -0
  461. package/neural-interface/public/vendor/yaml/LICENSE +13 -0
  462. package/neural-interface/public/vendor/yaml/manifest.json +4 -0
  463. package/neural-interface/public/vendor/yaml/yaml-2.9.1.min.js +137 -0
  464. package/neural-interface/scripts/build-vendor.mjs +32 -0
  465. package/neural-interface/server.js +3934 -2454
  466. package/neural-interface/templates/rulesets/core.md +1 -1
  467. package/neural-interface/templates/rulesets/legacy-hashes.json +18 -0
  468. package/neural-interface/templates/rulesets/manifest.json +8 -8
  469. package/npm-shrinkwrap.json +18 -12
  470. package/package.json +16 -3
  471. package/postinstall.js +9 -4
  472. package/preuninstall.js +1 -1
  473. package/setup.js +97 -322
  474. package/skills/skill-forge/SKILL.md +177 -0
  475. package/skills/skill-forge/evals/evals.json +54 -0
  476. package/skills/skill-forge/references/package.md +105 -0
  477. package/skills/skill-forge/references/review-and-recovery.md +63 -0
  478. package/skills/skill-forge/references/runtime-profiles.md +20 -0
  479. package/skills/skill-forge/references/sources-and-capabilities.md +51 -0
  480. package/skills/synabun/SKILL.md +10 -7
  481. package/skills/synabun/modules/audit.md +1 -5
  482. package/skills/synabun/modules/changelog.md +1 -5
  483. package/skills/synabun/modules/idea.md +1 -1
  484. package/skills/synabun/modules/memorize.md +6 -9
  485. package/skills/synabun/modules/schedule.md +4 -4
  486. package/mcp-server/dist/services/profiles.d.ts +0 -120
  487. package/mcp-server/dist/services/profiles.d.ts.map +0 -1
  488. package/mcp-server/dist/services/profiles.js +0 -387
  489. package/mcp-server/dist/services/profiles.js.map +0 -1
  490. package/mcp-server/dist/tools/leonardo-browser-tools.d.ts +0 -97
  491. package/mcp-server/dist/tools/leonardo-browser-tools.d.ts.map +0 -1
  492. package/mcp-server/dist/tools/leonardo-browser-tools.js +0 -228
  493. package/mcp-server/dist/tools/leonardo-browser-tools.js.map +0 -1
  494. package/mcp-server/dist/tools/leonardo.d.ts +0 -13
  495. package/mcp-server/dist/tools/leonardo.d.ts.map +0 -1
  496. package/mcp-server/dist/tools/leonardo.js +0 -21
  497. package/mcp-server/dist/tools/leonardo.js.map +0 -1
  498. package/mcp-server/dist/tools/profile.d.ts +0 -17
  499. package/mcp-server/dist/tools/profile.d.ts.map +0 -1
  500. package/mcp-server/dist/tools/profile.js +0 -114
  501. package/mcp-server/dist/tools/profile.js.map +0 -1
  502. package/mcp-server/src/services/profiles.ts +0 -428
  503. package/mcp-server/src/tools/leonardo-browser-tools.ts +0 -291
  504. package/mcp-server/src/tools/leonardo.ts +0 -30
  505. package/mcp-server/src/tools/profile.ts +0 -122
  506. package/neural-interface/lib/assistant-brains/opencode-browser-policy.js +0 -47
  507. package/neural-interface/lib/assistant-brains/opencode-route-gate.js +0 -62
  508. package/neural-interface/lib/mcp-profile-servers.js +0 -120
  509. package/neural-interface/public/shared/ui-tutorial-draw.js +0 -311
  510. package/skills/leonardo/SKILL.md +0 -168
  511. package/skills/leonardo/icon.svg +0 -1
  512. package/skills/leonardo/modules/browser-guide.md +0 -378
  513. package/skills/leonardo/modules/image-prompter.md +0 -283
  514. package/skills/leonardo/modules/model-advisor.md +0 -100
  515. package/skills/leonardo/modules/prompt-library.md +0 -110
  516. package/skills/leonardo/modules/style-guide.md +0 -186
  517. package/skills/leonardo/modules/video-prompter.md +0 -317
@@ -30,6 +30,19 @@
30
30
  // conversation opened again learns its route from the journal like any other
31
31
  // event (no rack of the history moves: none is live) and lends it to the
32
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.
33
46
 
34
47
  import { assistantEventKey, fmtCost, fmtElapsed, isRemovableRun, isTerminalRunStatus, modelShortName, providerShortLabel, routeTargetLabel, runTone, summarizeMailbox } from './asst-state.js';
35
48
  import { createRouteLine } from './asst-route.js';
@@ -37,7 +50,7 @@ import { closeMenu, isMenuOpen } from './asst-menu.js';
37
50
  import { computerToolName, describeComputerAction, frameUrls, markerPercent, openFrameLightbox, parseComputerResult, swapImage } from './asst-computer.js';
38
51
  import * as mascotLib from '../synabun-mascot.js';
39
52
  import { isReducedMotion, subscribe } from '../synabun-ticker.js';
40
- import { KIND_SPECS, callOutcome, commandOf, editDiff, editPaths, fmtDuration, groupSubject, joinCut, 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';
41
54
  import { injectRackStyles } from './asst-rack-styles.js';
42
55
  import { injectSceneStyles } from './asst-scene-styles.js';
43
56
  import { activityScene, compactActivity, isLivePhase, runChip, sceneOfClass, sceneSvg, stagePhase } from './asst-scenes.js';
@@ -45,6 +58,12 @@ import { STAGE_TIMING, createWorkerStage } from './asst-run-stage.js';
45
58
  import { formatRunTokens } from './asst-usage.js';
46
59
  import { sanitizeHtmlString, sanitizeInto } from './asst-sanitize.js';
47
60
  import { automationView } from './asst-automation.js';
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';
48
67
 
49
68
  // ── Markdown (lazy marked, the vendored build the sidepanels use too) ───────────────
50
69
  let _marked = null;
@@ -259,12 +278,15 @@ export function updateRunCard(card, run, opts = {}) {
259
278
  const readBtn = card.querySelector('[data-action="read"]');
260
279
  if (readBtn) readBtn.disabled = run.status === 'queued';
261
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
+
262
284
  // Escalation offer ("Escalate to ‹label›", or "Retry on ‹label›" after a
263
285
  // temporary failure) when the server proposes one; a note without a button
264
286
  // when a stronger model would hit the same wall (access, the user's input).
265
287
  let esc8 = card.querySelector('.asst-run-escalate');
266
288
  const escalationRaw = run.escalation && typeof run.escalation === 'object' ? run.escalation : null;
267
- const escalation = escalationRaw?.to ? escalationRaw : null;
289
+ const escalation = escalationRaw?.to && !(limitShown && escalationRaw.kind === 'retry') ? escalationRaw : null;
268
290
  const needsNote = escalationRaw && escalationRaw.kind === 'none' ? escalationRaw : null;
269
291
  const onEscalate = opts.onEscalate || card._runOpts?.onEscalate;
270
292
  if (esc8 && esc8.dataset.kind !== (needsNote ? 'note' : 'offer')) { esc8.remove(); esc8 = null; card._escalation = null; }
@@ -322,6 +344,49 @@ export function updateRunCard(card, run, opts = {}) {
322
344
  updateRunMedia(card, run, { t, lightboxT: opts.t || card._runOpts?.t });
323
345
  }
324
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
+
325
390
  const MAX_RUN_MEDIA = 8;
326
391
 
327
392
  /**
@@ -424,7 +489,7 @@ const PREVIEW_LINES = 5;
424
489
  const THINK_PREVIEW_LINES = 10;
425
490
  const MAX_DIFF_LINES = 400; // an edit's mini diff draws this many lines; "Show full input" draws the rest
426
491
  // The empty state's suggestions (asst-panel.js ids) and the kind each one is about.
427
- 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' };
428
493
 
429
494
  /**
430
495
  * createRenderer(transcriptEl, hooks)
@@ -433,7 +498,11 @@ const SUGGESTION_KINDS = { computer: 'computer', web: 'web', fix: 'edit', schedu
433
498
  * emptySuggestions() → [{ id, label, template }], onSuggestion(template),
434
499
  * brainLabel() → string, onChangeRoutes(), onRouteEvent(ev) → bool (true = handled by an open card),
435
500
  * heroOrigin() → DOMRect | null (where the empty state's character flies in from: the host's placeholder),
436
- * 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`) }
437
506
  */
438
507
  export function createRenderer(transcriptEl, hooks = {}) {
439
508
  loadMarked();
@@ -449,6 +518,9 @@ export function createRenderer(transcriptEl, hooks = {}) {
449
518
  const autoRows = new Map(); // traceId → the turn's automation trace row
450
519
  let working = null;
451
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) });
452
524
  let followTail = true;
453
525
  let emptyEl = null;
454
526
  let hero = null; // the empty state's character: { rig, el, box, parked, leaving, held, dock, egg, target }
@@ -470,6 +542,15 @@ export function createRenderer(transcriptEl, hooks = {}) {
470
542
  let lastDirect = null; // the last one approved since the user's prompt: a "do it here" continuation turn takes it again
471
543
  let turnSeq = 0; // turns this renderer has seen start (showWorking)
472
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
473
554
 
474
555
  // ── Activity racks ──
475
556
  // Every burst of agent work (tool calls, thinking, short narration) is one
@@ -539,6 +620,7 @@ export function createRenderer(transcriptEl, hooks = {}) {
539
620
  if (step.ghost && step.type === 'call' && !Object.keys(step.input || {}).length) { say.text = '…'; say.detail = '…'; say.segments = null; say.mono = false; say.meta = []; }
540
621
  return say;
541
622
  };
623
+ const limitDock = createLimitDock($msgs, { t });
542
624
  const nowMs = () => (clockAt ?? Date.now());
543
625
  const isTimed = () => !muted || clockAt != null;
544
626
  const locale = () => (typeof document !== 'undefined' && document.documentElement?.lang) || 'en';
@@ -739,12 +821,46 @@ export function createRenderer(transcriptEl, hooks = {}) {
739
821
  return best;
740
822
  }
741
823
 
742
- /** 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. */
743
825
  function visibleMascot() {
744
826
  if (hero?.rig) return hero.target;
827
+ const ask = liveAsk();
828
+ if (ask?.presenter?.isLive?.()) return ask.presenter.target;
745
829
  return newestStage()?.target || workingFace?.target || null;
746
830
  }
747
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
+
748
864
  function mascotChanged() {
749
865
  activateRigs();
750
866
  try { hooks.onMascotChange?.(); } catch { /* the director is gone */ }
@@ -755,12 +871,12 @@ export function createRenderer(transcriptEl, hooks = {}) {
755
871
  while (rows.length > MAX_MESSAGES && rows[0] !== working) rows[0].remove();
756
872
  }
757
873
 
758
- /** 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. */
759
875
  function append(node, scope = rootScope, { park = false } = {}) {
760
876
  hideEmpty({ park });
761
- if (scope.container === $msgs && working && working.parentNode === $msgs) $msgs.insertBefore(node, working);
877
+ if (scope.container === $msgs) askTail.insert(node);
762
878
  else scope.container.appendChild(node);
763
- if (scope === rootScope) prune();
879
+ if (scope === rootScope) { prune(); askTail.sync(); }
764
880
  // Text, a card, a notice…: the parked hero would sit on it. The prompt and a rack leave it to settleHero().
765
881
  if (hero?.parked && scope === rootScope && !node._rack && !node.classList.contains('msg-user')) dismissHero();
766
882
  if (racks.size) syncRacks();
@@ -825,6 +941,99 @@ export function createRenderer(transcriptEl, hooks = {}) {
825
941
  linkify(body);
826
942
  }
827
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
+
828
1037
  function copyText(text) {
829
1038
  const value = String(text ?? '');
830
1039
  if (!value) return;
@@ -857,10 +1066,18 @@ export function createRenderer(transcriptEl, hooks = {}) {
857
1066
  if (!s) return;
858
1067
  if (s.mdTimer) { clearTimeout(s.mdTimer); s.mdTimer = null; }
859
1068
  if (s.thinkTimer) { clearTimeout(s.thinkTimer); s.thinkTimer = null; }
860
- 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 });
861
1070
  if (s.think) refreshStep(s.think);
862
1071
  }
863
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
+
864
1081
  /** The stream's text row goes (the final message renders the text again). */
865
1082
  function dropStreamText(scope) {
866
1083
  const s = scope.stream;
@@ -930,10 +1147,12 @@ export function createRenderer(transcriptEl, hooks = {}) {
930
1147
  if (!s.thinkTimer) s.thinkTimer = setTimeout(() => { s.thinkTimer = null; refreshStep(step); if (step.rack.live) paintStage(step.rack); }, STREAM_TICK_MS);
931
1148
  } else if (delta.type === 'text_delta' && delta.text != null) {
932
1149
  s.textBuf += delta.text;
933
- 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()) {
934
1152
  const { row, wrap } = assistantRow(scope, { _text: s.textBuf, _msgId: s.msgId, _claude: s.claude });
935
1153
  row.classList.add('streaming');
936
1154
  noteAnswer(scope, row);
1155
+ if (scope === rootScope) lastTextRow = row;
937
1156
  s.row = row;
938
1157
  s.wrap = wrap;
939
1158
  s.bodyEl = el('div', 'asst-md');
@@ -944,7 +1163,7 @@ export function createRenderer(transcriptEl, hooks = {}) {
944
1163
  s.mdTimer = setTimeout(() => {
945
1164
  s.mdTimer = null;
946
1165
  if (!s.bodyEl) return;
947
- renderMarkdownInto(s.bodyEl, s.textBuf);
1166
+ paintMessage(s.row, s.bodyEl, s.textBuf, { streaming: true, scope });
948
1167
  scrollEnd();
949
1168
  }, STREAM_TICK_MS);
950
1169
  }
@@ -953,7 +1172,7 @@ export function createRenderer(transcriptEl, hooks = {}) {
953
1172
  }
954
1173
  if (type === 'content_block_stop') {
955
1174
  const s = scope.stream;
956
- 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 }); }
957
1176
  if (s?.think && s.thinkTimer) { clearTimeout(s.thinkTimer); s.thinkTimer = null; refreshStep(s.think); }
958
1177
  }
959
1178
  }
@@ -1023,9 +1242,10 @@ export function createRenderer(transcriptEl, hooks = {}) {
1023
1242
  function textRow(scope, text, ctx = {}) {
1024
1243
  const { row, wrap } = assistantRow(scope, { _text: text, _msgId: ctx.msgId || null, _claude: !!ctx.claude });
1025
1244
  const body = el('div', 'asst-md');
1026
- renderMarkdownInto(body, text);
1027
1245
  wrap.appendChild(body);
1246
+ paintMessage(row, body, text, { scope });
1028
1247
  noteAnswer(scope, row);
1248
+ if (scope === rootScope) lastTextRow = row;
1029
1249
  return row;
1030
1250
  }
1031
1251
 
@@ -1052,7 +1272,7 @@ export function createRenderer(transcriptEl, hooks = {}) {
1052
1272
  else if (s.row) {
1053
1273
  s.row.classList.remove('streaming');
1054
1274
  if (!s.wrap.children.length) s.row.remove();
1055
- 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); }
1056
1276
  }
1057
1277
  if (streamedThink && !thinks.length) finishThink(streamedThink);
1058
1278
  scope.stream = null;
@@ -1213,10 +1433,10 @@ export function createRenderer(transcriptEl, hooks = {}) {
1213
1433
  return prev;
1214
1434
  }
1215
1435
 
1216
- /** 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. */
1217
1437
  function tailRow(scope) {
1218
1438
  let node = scope.container.lastElementChild;
1219
- 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;
1220
1440
  return node;
1221
1441
  }
1222
1442
 
@@ -1326,7 +1546,7 @@ export function createRenderer(transcriptEl, hooks = {}) {
1326
1546
  row.innerHTML = `<div class="asst-rack-stage">
1327
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>
1328
1548
  <div class="asst-rack-caption"></div>
1329
- <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>
1330
1550
  <div class="asst-rack-sweep" aria-hidden="true"></div>
1331
1551
  </div>
1332
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>
@@ -1340,7 +1560,7 @@ export function createRenderer(transcriptEl, hooks = {}) {
1340
1560
  // The direct route's class whose scene the stage shows: set here, or once when a reopened conversation's route
1341
1561
  // resumes (lendRouteScene); a rack never loses its scene.
1342
1562
  sceneClass: nested ? '' : directSceneClass(), sceneId: '', sceneAt: -Infinity,
1343
- 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'), 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') },
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') },
1344
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 },
1345
1565
  ledger: q('.asst-rack-ledger'), statusEl: q('.asst-rack-status'), alertEl: q('.asst-rack-alert'), timelineEl: null, timelineSig: '',
1346
1566
  rig: null, target: null, held: false, holdUntil: 0, holdTimer: 0, pose: null, awake: null, awakeKey: null,
@@ -1631,6 +1851,7 @@ export function createRenderer(transcriptEl, hooks = {}) {
1631
1851
  s.text.textContent = '';
1632
1852
  s.subject.removeAttribute('title');
1633
1853
  s.subject.className = 'asst-bubble-subject';
1854
+ s.subject.hidden = false;
1634
1855
  s.subject.setAttribute('aria-expanded', 'false');
1635
1856
  s.meta.textContent = '';
1636
1857
  s.err.textContent = '';
@@ -1640,15 +1861,21 @@ export function createRenderer(transcriptEl, hooks = {}) {
1640
1861
  }
1641
1862
 
1642
1863
  /**
1643
- * One rig of this transcript animates: the hero, else the newest stage, else
1644
- * the "Thinking" row's. The rule ends at this renderer: every other panel
1645
- * 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.
1646
1869
  */
1647
1870
  function activateRigs() {
1648
- 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();
1649
1876
  for (const rack of racks) if (rack.rig) rack.rig.setActive?.(rack === newest);
1650
1877
  hero?.rig?.setActive?.(true);
1651
- workingFace?.rig?.setActive?.(!hero?.rig && !newest);
1878
+ workingFace?.rig?.setActive?.(!hero?.rig && !asking && !newest);
1652
1879
  }
1653
1880
 
1654
1881
  /** The bubble's call: the awake one, else the newest; it holds 900 ms (3 s for thinking) before the next wins. */
@@ -1758,29 +1985,38 @@ export function createRenderer(transcriptEl, hooks = {}) {
1758
1985
  const held = rack.waiting && step === waitingStep(rack);
1759
1986
  const verb = held ? t('assistant.rack.waiting', 'Waiting for you') : verbText(say.verb, running, t);
1760
1987
  const grouped = groupedSubject(rack, step);
1761
- 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;
1762
1993
  const literal = !grouped && say.full ? say.full : full; // the command as written: the title and the opened bubble
1763
1994
  const open = rack.bubble.open;
1764
- // Narrow stages cut from the head, so the file name (the tail) survives. A small budget keeps a
1765
- // shorter tail (24 chars of it would not fit beside the verb).
1766
- const cut = subjectCut(rack);
1767
- const cutTo = (n) => (open ? literal : cut === 'head' ? headEllipsis(full, n) : middleEllipsis(full, n, Math.min(24, Math.max(8, n - 6))));
1768
- const shownText = cutTo(subjectBudget(rack, verb, cut));
1769
- const key = `${step.id}|${verb}|${shownText}|${say.muted ? 1 : 0}`;
1770
1995
  s.bubble.hidden = !full && !verb;
1771
1996
  s.bubble.dataset.state = held ? 'waiting' : running ? 'running' : step.state;
1772
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}`;
1773
2007
  if (key !== rack.bubble.key) {
1774
2008
  const changedStep = !rack.bubble.key.startsWith(`${step.id}|`);
1775
2009
  rack.bubble.key = key;
1776
2010
  const write = () => {
1777
2011
  s.verb.textContent = verb;
1778
2012
  s.subject.className = `asst-bubble-subject${say.mono ? ' is-mono' : ''}${say.muted ? ' is-muted' : ''}`;
2013
+ s.subject.hidden = bare;
1779
2014
  const segments = grouped || open ? null : say.segments;
1780
2015
  writeSubject(s.text, segments, shownText);
1781
2016
  // The budget is an estimate (a long token wraps whole to the next line): what the two-line
1782
2017
  // clamp would hide is cut here instead, so the subject is never cut a second time.
1783
- const fitted = open ? shownText : fitSubject(s, shownText, cutTo, segments);
2018
+ const fitted = open || bare ? shownText : fitSubject(s, shownText, cutTo, segments);
2019
+ s.subject.classList.toggle('is-cut', fitted.startsWith('…'));
1784
2020
  if (literal !== fitted || open) s.subject.title = literal; else s.subject.removeAttribute('title');
1785
2021
  };
1786
2022
  if (swap && changedStep && !calm() && typeof s.subject.animate === 'function' && s.text.textContent) {
@@ -1793,7 +2029,6 @@ export function createRenderer(transcriptEl, hooks = {}) {
1793
2029
  };
1794
2030
  } else write();
1795
2031
  }
1796
- paintBubbleMeta(rack, step, say, now);
1797
2032
  const err = step.state === 'error' ? (step.detail || exitText(step)) : '';
1798
2033
  s.err.hidden = !err;
1799
2034
  if (s.err.textContent !== err) s.err.textContent = err;
@@ -1804,10 +2039,11 @@ export function createRenderer(transcriptEl, hooks = {}) {
1804
2039
  }
1805
2040
 
1806
2041
  /**
1807
- * How many characters of subject fit the bubble: two lines of the column
1808
- * beside the verb, measured on the laid-out stage. The text is cut in the
1809
- * middle so its tail (the file, the last argument) stays; on a narrow stage
1810
- * 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).
1811
2047
  */
1812
2048
  function subjectBudget(rack, verb, cut = 'middle') {
1813
2049
  const s = rack.stage;
@@ -1817,11 +2053,18 @@ export function createRenderer(transcriptEl, hooks = {}) {
1817
2053
  const bubble = s.bubble.hidden ? null : s.bubble.getBoundingClientRect();
1818
2054
  const beside = !bubble || bubble.left >= face.right - 1;
1819
2055
  const verbPx = Math.max(s.verb.getBoundingClientRect().width, verb.length * 7);
1820
- 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);
1821
2061
  const perLine = Math.max(12, Math.floor(column / 7));
1822
- // Two lines, less what wrapping at token boundaries wastes (a head cut wraps anywhere: two full lines).
1823
- const lines = getComputedStyle(s.subject).whiteSpace === 'nowrap' ? 1 : cut === 'head' ? 2 : 1.7;
1824
- 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));
1825
2068
  }
1826
2069
 
1827
2070
  /** 'head' where the stylesheet says so (≤320 px: keep the tail), else 'middle'. */
@@ -1830,30 +2073,53 @@ export function createRenderer(transcriptEl, hooks = {}) {
1830
2073
  }
1831
2074
 
1832
2075
  /**
1833
- * "…ender-events.browser.mjs": the last `max` chars, from a token boundary when one is near. One "…":
1834
- * a token the subject already shortened at the start of that tail goes with the cut ("……/x.js").
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.
1835
2080
  */
1836
- function headEllipsis(text, max) {
1837
- const s = String(text ?? '');
1838
- if (s.length <= max) return s;
1839
- let tail = s.slice(s.length - Math.max(8, max - 1));
1840
- const cut = tail.search(/[\s/]/);
1841
- if (cut > 0 && cut < tail.length * 0.4) tail = tail.slice(tail[cut] === '/' ? cut : cut + 1);
1842
- return joinCut('', tail);
1843
- }
1844
-
1845
2081
  function writeSubject(node, segments, text) {
1846
2082
  node.textContent = '';
1847
- if (Array.isArray(segments) && segments.length && segments.map(x => x.text).join('') === text) {
1848
- for (const seg of segments) node.appendChild(el('span', seg.cls ? `is-${seg.cls}` : '', seg.text));
1849
- } 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;
1850
2113
  }
1851
2114
 
1852
- /** The bubble's subject cut shorter (`cutTo`, six steps at most) until its clamp hides none of it; the text it shows. */
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
+ */
1853
2119
  function fitSubject(s, text, cutTo, segments) {
1854
2120
  let shown = text;
1855
2121
  let budget = shown.length;
1856
- for (let i = 0; i < 6 && budget > 12 && s.subject.scrollHeight > s.subject.clientHeight + 1; i += 1) {
2122
+ for (let i = 0; i < 6 && budget > 12 && (s.line.scrollHeight > s.line.clientHeight + 1 || pieceOverflows(s)); i += 1) {
1857
2123
  budget = Math.floor(budget * 0.85);
1858
2124
  shown = cutTo(budget);
1859
2125
  writeSubject(s.text, segments, shown);
@@ -1882,7 +2148,7 @@ export function createRenderer(transcriptEl, hooks = {}) {
1882
2148
  // A subagent's bubble follows its child: the step the agent is on now.
1883
2149
  if (step.kind === 'subagent' && step.children?.length) {
1884
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);
1885
- 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)]);
1886
2152
  }
1887
2153
  // ×N only for the same target again (a re-run, a re-read): a number the ledger agrees with.
1888
2154
  // Other calls in the same 800 ms burst read as a sentence instead.
@@ -2176,10 +2442,10 @@ export function createRenderer(transcriptEl, hooks = {}) {
2176
2442
  return t('assistant.rack.state.done', 'done');
2177
2443
  }
2178
2444
 
2179
- /** "Running npm test" / "Ran npm test". */
2180
- 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 } = {}) {
2181
2447
  const say = sayOf(step);
2182
- 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();
2183
2449
  }
2184
2450
 
2185
2451
  /** The receipt: one call says what it did; a burst says it in one sentence. */
@@ -2681,6 +2947,9 @@ export function createRenderer(transcriptEl, hooks = {}) {
2681
2947
  }
2682
2948
 
2683
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);
2684
2953
  turnFrames = [];
2685
2954
  directRoute = null; // a new prompt is a new task: its own route decides
2686
2955
  lastDirect = null;
@@ -2720,17 +2989,21 @@ export function createRenderer(transcriptEl, hooks = {}) {
2720
2989
  }
2721
2990
 
2722
2991
  function appendError(text) {
2723
- 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;
2724
2995
  }
2725
2996
 
2726
2997
  function appendDivider(label) {
2727
2998
  return append(el('div', 'asst-divider', label || ''));
2728
2999
  }
2729
3000
 
2730
- /** 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. */
2731
3002
  function appendNode(node) {
2732
3003
  if (!node) return null;
2733
- return append(node);
3004
+ const row = append(node);
3005
+ if (node._ask) registerAsk(node._ask);
3006
+ return row;
2734
3007
  }
2735
3008
 
2736
3009
  function showWorking(label) {
@@ -2744,6 +3017,9 @@ export function createRenderer(transcriptEl, hooks = {}) {
2744
3017
  hideEmpty({ park: true });
2745
3018
  $msgs.appendChild(working);
2746
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);
2747
3023
  turn = { calls: 0, last: null, errored: false, answerRow: null };
2748
3024
  turnSeq += 1;
2749
3025
  if (directRoute && directRoute.turn == null) { // a route picked while no turn ran, or one the journal left running, is this turn's
@@ -2831,6 +3107,12 @@ export function createRenderer(transcriptEl, hooks = {}) {
2831
3107
  onStop: hooks.onStopRun,
2832
3108
  onRead: hooks.onReadRun,
2833
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),
2834
3116
  result,
2835
3117
  keepSummary: true,
2836
3118
  };
@@ -2876,7 +3158,7 @@ export function createRenderer(transcriptEl, hooks = {}) {
2876
3158
  if (!run?.runId) return null;
2877
3159
  noteRun(run);
2878
3160
  const card = runCards.get(run.runId);
2879
- if (!card?.isConnected) return null;
3161
+ if (!card?.isConnected) { limitDock.set(`run:${run.runId}`, null); return null; }
2880
3162
  updateRunCard(card, run, runCardOpts()); // fresh options: the run's own lastResult, never a result the card was built with
2881
3163
  syncStage(run.runId, card, run);
2882
3164
  return card;
@@ -3216,8 +3498,8 @@ export function createRenderer(transcriptEl, hooks = {}) {
3216
3498
  // Under the reply once the turn has ended; a later snapshot (learning) never moves it again.
3217
3499
  const ended = ev.phase !== 'sent';
3218
3500
  if (fresh || (ended && !row.atEnd)) {
3219
- // Placed like any row (before "Working…"), but a status line of the harness never dismisses the hero.
3220
- 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);
3221
3503
  else $msgs.appendChild(row.el);
3222
3504
  if (ended) row.atEnd = true;
3223
3505
  prune();
@@ -3231,13 +3513,14 @@ export function createRenderer(transcriptEl, hooks = {}) {
3231
3513
  function showEmpty() {
3232
3514
  if (emptyEl || $msgs.querySelector(':scope > :not(.asst-working)')) return;
3233
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.
3234
3518
  emptyEl.innerHTML = `
3235
3519
  <div class="asst-empty-mascot" aria-hidden="true"></div>
3236
3520
  <div class="asst-empty-title" role="heading" aria-level="2">${esc(t('assistant.empty.heading', 'What should we do?'))}</div>
3237
- <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>
3238
3521
  <div class="asst-empty-greet"></div>
3239
- <div class="asst-empty-project" hidden></div>
3240
- <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>`;
3241
3524
  const box = emptyEl.querySelector('.asst-empty-mascot');
3242
3525
  try {
3243
3526
  const rig = mascotLib.createMascot(box, { width: 120, height: 60 });
@@ -3245,8 +3528,8 @@ export function createRenderer(transcriptEl, hooks = {}) {
3245
3528
  hero.target = heroTarget(hero);
3246
3529
  wireEgg(hero);
3247
3530
  } catch { hero = null; }
3248
- // The project the conversation works in, under the lead: the host draws the picker (the brain
3249
- // 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.
3250
3533
  const projectSlot = emptyEl.querySelector('.asst-empty-project');
3251
3534
  let picker = null;
3252
3535
  try { picker = typeof hooks.mountEmptyProject === 'function' ? hooks.mountEmptyProject(projectSlot) : null; } catch { picker = null; }
@@ -3263,8 +3546,8 @@ export function createRenderer(transcriptEl, hooks = {}) {
3263
3546
  }
3264
3547
 
3265
3548
  /**
3266
- * The greeting SynaBun prepared for this return (public/shared/greeting/) takes the lead's place.
3267
- * 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.
3268
3551
  */
3269
3552
  function offerGreeting() {
3270
3553
  const owner = emptyEl;
@@ -3282,12 +3565,16 @@ export function createRenderer(transcriptEl, hooks = {}) {
3282
3565
  host.innerHTML = '';
3283
3566
  let list = [];
3284
3567
  try { list = typeof hooks.emptySuggestions === 'function' ? (hooks.emptySuggestions() || []) : []; } catch { list = []; }
3568
+ let i = 0;
3285
3569
  for (const s of list) {
3286
- // 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.
3287
3572
  const chip = el('button', 'asst-chip asst-suggestion');
3288
3573
  chip.type = 'button';
3289
3574
  chip.setAttribute('role', 'listitem');
3290
3575
  chip.dataset.suggestion = s.id || '';
3576
+ chip.style.setProperty('--i', String(i + 1));
3577
+ i += 1;
3291
3578
  const kind = s.kind || SUGGESTION_KINDS[s.id] || null;
3292
3579
  if (kind) {
3293
3580
  chip.dataset.kind = kind;
@@ -3301,10 +3588,28 @@ export function createRenderer(transcriptEl, hooks = {}) {
3301
3588
  chip.addEventListener('blur', () => previewHero(null));
3302
3589
  }
3303
3590
  chip.appendChild(el('span', 'asst-chip-text', s.label));
3304
- 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
+ });
3305
3601
  host.appendChild(chip);
3306
3602
  }
3307
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');
3308
3613
  }
3309
3614
 
3310
3615
  /** The empty state's character tries a suggestion on (null: back to idle); never over a pose the director holds. */
@@ -3509,6 +3814,7 @@ export function createRenderer(transcriptEl, hooks = {}) {
3509
3814
  const content = Array.isArray(ev.message.content) ? ev.message.content : [];
3510
3815
  lastErrorText = typeof ev.message.content === 'string' ? ev.message.content.trim()
3511
3816
  : content.filter((b) => b?.type === 'text').map((b) => b.text).join('\n').trim();
3817
+ if (!parentId) offerLimitWait(lastErrorText, { brainError: true });
3512
3818
  }
3513
3819
  return true;
3514
3820
  }
@@ -3536,6 +3842,8 @@ export function createRenderer(transcriptEl, hooks = {}) {
3536
3842
  if (!parentId) endBursts(rootScope);
3537
3843
  // A journal's or a replay's turn ended here (live, finishTurn ends it): so did the route it ran.
3538
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();
3539
3847
  hooks.onResult?.(ev);
3540
3848
  if (errorText && errorText !== lastErrorText) appendError(errorText);
3541
3849
  if (!parentId) { lastErrorText = ''; rootScope.kept = null; releaseGrace(); }
@@ -3642,10 +3950,14 @@ export function createRenderer(transcriptEl, hooks = {}) {
3642
3950
  /**
3643
3951
  * The turn ended. `holdMs`: the panel plays a one-shot (success, error) on
3644
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.
3645
3954
  */
3646
- function finishTurn({ holdMs = 0 } = {}) {
3955
+ function finishTurn({ holdMs = 0, guess = true } = {}) {
3647
3956
  flushStream(rootScope);
3957
+ sealStreamRow(rootScope);
3648
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();
3649
3961
  for (const scope of scopes.values()) flushStream(scope);
3650
3962
  closeStreamThinks();
3651
3963
  hideWorking();
@@ -3686,11 +3998,14 @@ export function createRenderer(transcriptEl, hooks = {}) {
3686
3998
  }
3687
3999
  if (failed) console.warn(`[assistant] ${failed} of ${list.length} transcript entries could not be drawn`, firstError);
3688
4000
  flushStream(rootScope);
4001
+ sealStreamRow(rootScope);
3689
4002
  if (rootScope.stream?.row) rootScope.stream.row.classList.remove('streaming');
3690
4003
  for (const scope of scopes.values()) flushStream(scope);
3691
4004
  hideWorking();
3692
4005
  turnActive = false;
3693
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();
3694
4009
  scrollEnd(true);
3695
4010
  }
3696
4011
 
@@ -3733,6 +4048,12 @@ export function createRenderer(transcriptEl, hooks = {}) {
3733
4048
  if (rootScope.stream?.thinkTimer) clearTimeout(rootScope.stream.thinkTimer);
3734
4049
  scopes.clear();
3735
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 */ } }
3736
4057
  runActivity.clear();
3737
4058
  runCards.clear();
3738
4059
  routeLines.clear();
@@ -3764,7 +4085,7 @@ export function createRenderer(transcriptEl, hooks = {}) {
3764
4085
  return {
3765
4086
  el: $msgs,
3766
4087
  clear,
3767
- destroy: () => { hideWorking(); dropHero(); dropAll(); dropStages(); },
4088
+ destroy: () => { clear(); limitDock.destroy(); },
3768
4089
  appendUser,
3769
4090
  appendAssistantMarkdown,
3770
4091
  appendStatus,
@@ -3803,6 +4124,8 @@ export function createRenderer(transcriptEl, hooks = {}) {
3803
4124
  forgetSeen: () => { seen.clear(); snapshot = null; },
3804
4125
  /** A card waits on the user: `toolUseId` names the call it holds (the newest live stage when unknown); false clears it. */
3805
4126
  setWaiting,
4127
+ /** The session's usage-limit report changed: this turn's waiting card counts to its reset time. */
4128
+ noteLimit,
3806
4129
  finishTurn,
3807
4130
  renderTranscript,
3808
4131
  showWorking,
@@ -3811,6 +4134,7 @@ export function createRenderer(transcriptEl, hooks = {}) {
3811
4134
  showEmpty,
3812
4135
  offerGreeting,
3813
4136
  refreshEmpty: renderSuggestions,
4137
+ setDrafting,
3814
4138
  isEmpty: () => !!emptyEl,
3815
4139
  scrollEnd,
3816
4140
  isNearBottom,