thursday-agent 0.5.0 → 0.7.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 (667) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/app-path-routes-manifest.json +3 -2
  3. package/.next/build-manifest.json +3 -3
  4. package/.next/prerender-manifest.json +3 -3
  5. package/.next/required-server-files.json +8 -1
  6. package/.next/routes-manifest.json +18 -6
  7. package/.next/server/app/_global-error/page.js.nft.json +1 -1
  8. package/.next/server/app/_global-error.html +1 -1
  9. package/.next/server/app/_global-error.rsc +1 -1
  10. package/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  11. package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  12. package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  13. package/.next/server/app/_not-found/page.js.nft.json +1 -1
  14. package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  15. package/.next/server/app/_not-found.html +1 -1
  16. package/.next/server/app/_not-found.rsc +13 -13
  17. package/.next/server/app/_not-found.segments/_full.segment.rsc +13 -13
  18. package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +12 -12
  19. package/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  20. package/.next/server/app/api/artifact/route.js +10 -6
  21. package/.next/server/app/api/artifact/route.js.nft.json +1 -1
  22. package/.next/server/app/api/bot/memory/files/route.js +5 -4
  23. package/.next/server/app/api/bot/memory/files/route.js.nft.json +1 -1
  24. package/.next/server/app/api/bot/memory/route.js +8 -8
  25. package/.next/server/app/api/bot/memory/route.js.nft.json +1 -1
  26. package/.next/server/app/api/bot/route.js +8 -8
  27. package/.next/server/app/api/bot/route.js.nft.json +1 -1
  28. package/.next/server/app/api/bot/thread/[id]/route/app-paths-manifest.json +3 -0
  29. package/.next/server/app/api/bot/thread/[id]/route.js +15 -0
  30. package/.next/server/app/api/bot/thread/[id]/route.js.nft.json +1 -0
  31. package/.next/server/app/api/bot/thread/[id]/route_client-reference-manifest.js +3 -0
  32. package/.next/server/app/api/bot/thread/route/app-paths-manifest.json +3 -0
  33. package/.next/server/app/api/bot/thread/route.js +15 -0
  34. package/.next/server/app/api/bot/thread/route.js.nft.json +1 -0
  35. package/.next/server/app/api/bot/thread/route_client-reference-manifest.js +3 -0
  36. package/.next/server/app/api/config/route.js +7 -7
  37. package/.next/server/app/api/config/route.js.nft.json +1 -1
  38. package/.next/server/app/api/events/route.js +3 -3
  39. package/.next/server/app/api/events/route.js.nft.json +1 -1
  40. package/.next/server/app/api/file/[...path]/route.js +4 -4
  41. package/.next/server/app/api/file/[...path]/route.js.nft.json +1 -1
  42. package/.next/server/app/api/llm-model/catalog/route.js +8 -8
  43. package/.next/server/app/api/llm-model/catalog/route.js.nft.json +1 -1
  44. package/.next/server/app/api/llm-model/credits/route.js +8 -8
  45. package/.next/server/app/api/llm-model/credits/route.js.nft.json +1 -1
  46. package/.next/server/app/api/llm-model/route.js +8 -8
  47. package/.next/server/app/api/llm-model/route.js.nft.json +1 -1
  48. package/.next/server/app/api/llm-model/subscription/route.js +8 -8
  49. package/.next/server/app/api/llm-model/subscription/route.js.nft.json +1 -1
  50. package/.next/server/app/api/mcp/[name]/route.js +6 -6
  51. package/.next/server/app/api/mcp/[name]/route.js.nft.json +1 -1
  52. package/.next/server/app/api/mcp/oauth/callback/route.js +4 -4
  53. package/.next/server/app/api/mcp/oauth/callback/route.js.nft.json +1 -1
  54. package/.next/server/app/api/mcp/route.js +6 -6
  55. package/.next/server/app/api/mcp/route.js.nft.json +1 -1
  56. package/.next/server/app/api/mcp/tools/route.js +6 -6
  57. package/.next/server/app/api/mcp/tools/route.js.nft.json +1 -1
  58. package/.next/server/app/api/memory/edit/route.js +15 -15
  59. package/.next/server/app/api/memory/edit/route.js.nft.json +1 -1
  60. package/.next/server/app/api/memory/route.js +7 -6
  61. package/.next/server/app/api/memory/route.js.nft.json +1 -1
  62. package/.next/server/app/api/skills/[source]/[name]/route.js +6 -6
  63. package/.next/server/app/api/skills/[source]/[name]/route.js.nft.json +1 -1
  64. package/.next/server/app/api/skills/route.js +6 -6
  65. package/.next/server/app/api/skills/route.js.nft.json +1 -1
  66. package/.next/server/app/api/thursday/call/route.js +10 -11
  67. package/.next/server/app/api/thursday/call/route.js.nft.json +1 -1
  68. package/.next/server/app/api/thursday/skills/route.js +9 -10
  69. package/.next/server/app/api/thursday/skills/route.js.nft.json +1 -1
  70. package/.next/server/app/api/thursday/tool-call/route.js +15 -15
  71. package/.next/server/app/api/thursday/tool-call/route.js.nft.json +1 -1
  72. package/.next/server/app/api/workspace/missing/route/app-paths-manifest.json +3 -0
  73. package/.next/server/app/api/workspace/missing/route/build-manifest.json +12 -0
  74. package/.next/server/app/api/workspace/missing/route/server-reference-manifest.json +4 -0
  75. package/.next/server/app/api/workspace/missing/route.js +12 -0
  76. package/.next/server/app/api/workspace/missing/route.js.map +5 -0
  77. package/.next/server/app/api/workspace/missing/route.js.nft.json +1 -0
  78. package/.next/server/app/api/workspace/missing/route_client-reference-manifest.js +3 -0
  79. package/.next/server/app/api/workspace/route.js +5 -5
  80. package/.next/server/app/api/workspace/route.js.nft.json +1 -1
  81. package/.next/server/app/artifact/[...path]/page/server-reference-manifest.json +1 -1
  82. package/.next/server/app/artifact/[...path]/page.js +4 -3
  83. package/.next/server/app/artifact/[...path]/page.js.nft.json +1 -1
  84. package/.next/server/app/artifact/[...path]/page_client-reference-manifest.js +1 -1
  85. package/.next/server/app/favicon.ico/route.js.nft.json +1 -1
  86. package/.next/server/app/page/server-reference-manifest.json +67 -45
  87. package/.next/server/app/page.js +11 -9
  88. package/.next/server/app/page.js.nft.json +1 -1
  89. package/.next/server/app/page_client-reference-manifest.js +1 -1
  90. package/.next/server/app-paths-manifest.json +3 -2
  91. package/.next/server/chunks/[root-of-the-server]__0-06t3f._.js +8 -0
  92. package/.next/server/chunks/[root-of-the-server]__02846fb._.js +8 -0
  93. package/.next/server/chunks/[root-of-the-server]__05luceg._.js +3 -0
  94. package/.next/server/chunks/[root-of-the-server]__06--3q3._.js +8 -0
  95. package/.next/server/chunks/[root-of-the-server]__06l05n_._.js +3 -0
  96. package/.next/server/chunks/[root-of-the-server]__07bnah_._.js +3 -0
  97. package/.next/server/chunks/[root-of-the-server]__08eae20._.js +8 -0
  98. package/.next/server/chunks/[root-of-the-server]__0_ue8oe._.js +3 -0
  99. package/.next/server/chunks/[root-of-the-server]__0btqrct._.js +3 -0
  100. package/.next/server/chunks/[root-of-the-server]__0c9fgca._.js +5 -0
  101. package/.next/server/chunks/[root-of-the-server]__0h_x6q6._.js +5 -0
  102. package/.next/server/chunks/{[root-of-the-server]__0pa2n68._.js → [root-of-the-server]__0kxrvxf._.js} +2 -2
  103. package/.next/server/chunks/{[root-of-the-server]__0q0vvt2._.js → [root-of-the-server]__0ov3iir._.js} +2 -2
  104. package/.next/server/chunks/[root-of-the-server]__0qwtlmx._.js +8 -0
  105. package/.next/server/chunks/[root-of-the-server]__0sm5xvm._.js +3 -0
  106. package/.next/server/chunks/[root-of-the-server]__0zp35ul._.js +5 -0
  107. package/.next/server/chunks/[root-of-the-server]__144u9d_._.js +3 -0
  108. package/.next/server/chunks/[root-of-the-server]__18-2tsq._.js +5 -0
  109. package/.next/server/chunks/[root-of-the-server]__1e-ci13._.js +3 -0
  110. package/.next/server/chunks/[root-of-the-server]__1e2365e._.js +8 -0
  111. package/.next/server/chunks/[root-of-the-server]__1fix932._.js +8 -0
  112. package/.next/server/chunks/{[root-of-the-server]__0ce8nf8._.js → [root-of-the-server]__1g3t7vn._.js} +3 -3
  113. package/.next/server/chunks/[root-of-the-server]__1kp-83u._.js +5 -0
  114. package/.next/server/chunks/[root-of-the-server]__1kv8p3e._.js +5 -0
  115. package/.next/server/chunks/[root-of-the-server]__1orn-5b._.js +2 -3
  116. package/.next/server/chunks/_02szwk8._.js +5 -0
  117. package/.next/server/chunks/_05h2cii._.js +1 -1
  118. package/.next/server/chunks/_06c9kie._.js +216 -0
  119. package/.next/server/chunks/{_1x6kd1r._.js → _07y2gj-._.js} +1 -1
  120. package/.next/server/chunks/_0a43a-0._.js +3 -0
  121. package/.next/server/chunks/_0bujjey._.js +5 -0
  122. package/.next/server/chunks/_0c9j6rb._.js +1 -1
  123. package/.next/server/chunks/_0dkd549._.js +3 -0
  124. package/.next/server/chunks/_0ej1kvv._.js +3 -0
  125. package/.next/server/chunks/_0f6ts7z._.js +3 -0
  126. package/.next/server/chunks/_0gm9hyv._.js +55 -0
  127. package/.next/server/chunks/_0i55en5._.js +25 -0
  128. package/.next/server/chunks/_0k50n27._.js +18 -0
  129. package/.next/server/chunks/_0khihjk._.js +3 -3
  130. package/.next/server/chunks/_0n74zsf._.js +5 -5
  131. package/.next/server/chunks/_0nvm9ef._.js +3 -0
  132. package/.next/server/chunks/_0p56fn0._.js +3 -0
  133. package/.next/server/chunks/_0recufk._.js +1 -1
  134. package/.next/server/chunks/_0xbkz10._.js +3 -0
  135. package/.next/server/chunks/_10tkr26._.js +3 -0
  136. package/.next/server/chunks/{_0ioz3nd._.js → _11u2kp7._.js} +1 -1
  137. package/.next/server/chunks/_11ujpya._.js +3 -0
  138. package/.next/server/chunks/_127nx_p._.js +5 -0
  139. package/.next/server/chunks/_17l5ygt._.js +1 -1
  140. package/.next/server/chunks/_19uauj6._.js +3 -0
  141. package/.next/server/chunks/_1_5mnq4._.js +5 -5
  142. package/.next/server/chunks/_1abl6t9._.js +55 -0
  143. package/.next/server/chunks/_1d0j961._.js +3 -3
  144. package/.next/server/chunks/_1dbixxz._.js +3 -0
  145. package/.next/server/chunks/_1dc_d1q._.js +98 -0
  146. package/.next/server/chunks/_1drgfx4._.js +5 -5
  147. package/.next/server/chunks/_1e48czp._.js +3 -3
  148. package/.next/server/chunks/_1f31u5x._.js +5 -0
  149. package/.next/server/chunks/_1gnjfli._.js +3 -0
  150. package/.next/server/chunks/_1kg70wc._.js +3 -3
  151. package/.next/server/chunks/_1klbcft._.js +5 -0
  152. package/.next/server/chunks/_1krd9rf._.js +4 -1
  153. package/.next/server/chunks/_1kwmzab._.js +3 -0
  154. package/.next/server/chunks/{_1pf4qe-._.js → _1oxj9fa._.js} +1 -1
  155. package/.next/server/chunks/{_1f46eie._.js → _1pcmdbb._.js} +1 -1
  156. package/.next/server/chunks/_1qgix-j._.js +3 -0
  157. package/.next/server/chunks/{lib_logger_ts_1exwjut._.js → _1rpchta._.js} +5 -5
  158. package/.next/server/chunks/_1vsej-s._.js +5 -0
  159. package/.next/server/chunks/_1yx0p_s._.js +5 -0
  160. package/.next/server/chunks/{_0rnf-6n._.js → _20fsr_z._.js} +6 -6
  161. package/.next/server/chunks/_2128hkj._.js +5 -0
  162. package/.next/server/chunks/_next-internal_server_app_api_bot_thread_[id]_route_actions_1fstjn2.js +3 -0
  163. package/.next/server/chunks/_next-internal_server_app_api_bot_thread_route_actions_003ccso.js +3 -0
  164. package/.next/server/chunks/_next-internal_server_app_api_workspace_missing_route_actions_1fidi6p.js +3 -0
  165. package/.next/server/chunks/config_ts_1g4sz5o._.js +1 -1
  166. package/.next/server/chunks/features_0-cnt0l._.js +4 -0
  167. package/.next/server/chunks/features_0hdaci0._.js +4 -0
  168. package/.next/server/chunks/features_1e39j7-._.js +8 -0
  169. package/.next/server/chunks/features_bot_bot_query_ts_12mgkl2._.js +1 -1
  170. package/.next/server/chunks/features_memory_1uqvksn._.js +1 -1
  171. package/.next/server/chunks/{node_modules_consola_dist_chunks_prompt_mjs_0v_7drq._.js → node_modules_consola_dist_chunks_prompt_mjs_0a7-pbw._.js} +1 -1
  172. package/.next/server/chunks/{node_modules_consola_dist_chunks_prompt_mjs_17k8irj._.js → node_modules_consola_dist_chunks_prompt_mjs_0gzvgfv._.js} +1 -1
  173. package/.next/server/chunks/{node_modules_consola_dist_chunks_prompt_mjs_0i7cvml._.js → node_modules_consola_dist_chunks_prompt_mjs_107pxfn._.js} +1 -1
  174. package/.next/server/chunks/{node_modules_consola_dist_chunks_prompt_mjs_0icqaqw._.js → node_modules_consola_dist_chunks_prompt_mjs_15hbel8._.js} +1 -1
  175. package/.next/server/chunks/node_modules_consola_dist_chunks_prompt_mjs_18i-23e._.js +3 -0
  176. package/.next/server/chunks/node_modules_consola_dist_chunks_prompt_mjs_1bewgf3._.js +3 -0
  177. package/.next/server/chunks/node_modules_consola_dist_chunks_prompt_mjs_1nlww1m._.js +3 -0
  178. package/.next/server/chunks/node_modules_consola_dist_chunks_prompt_mjs_1q77nfx._.js +3 -0
  179. package/.next/server/chunks/node_modules_consola_dist_chunks_prompt_mjs_1xqdzx_._.js +3 -0
  180. package/.next/server/chunks/{node_modules_zod_v4_1801ax7._.js → node_modules_zod_v4_0lkux5-._.js} +2 -2
  181. package/.next/server/chunks/{node_modules_zod_v4_1cha_bw._.js → node_modules_zod_v4_1jmr9cq._.js} +2 -2
  182. package/.next/server/chunks/{node_modules_zod_v4_0ubdflj._.js → node_modules_zod_v4_1yrxmek._.js} +2 -2
  183. package/.next/server/chunks/node_modules_zod_v4_classic_errors_0tp0y-q.js +3 -0
  184. package/.next/server/chunks/ssr/[root-of-the-server]__0-e011f._.js +3 -0
  185. package/.next/server/chunks/ssr/[root-of-the-server]__09q9med._.js +1 -1
  186. package/.next/server/chunks/ssr/[root-of-the-server]__0lyi8i2._.js +1 -1
  187. package/.next/server/chunks/ssr/[root-of-the-server]__0pd8yr7._.js +3 -0
  188. package/.next/server/chunks/ssr/[root-of-the-server]__16jc196._.js +1 -1
  189. package/.next/server/chunks/ssr/[root-of-the-server]__18dthbp._.js +3 -0
  190. package/.next/server/chunks/ssr/[root-of-the-server]__18e68w8._.js +3 -0
  191. package/.next/server/chunks/ssr/[root-of-the-server]__1mzi-ju._.js +1 -1
  192. package/.next/server/chunks/ssr/[root-of-the-server]__1nygfik._.js +5 -0
  193. package/.next/server/chunks/ssr/[root-of-the-server]__1sdphb0._.js +8 -0
  194. package/.next/server/chunks/ssr/[root-of-the-server]__1vdmfbe._.js +3 -0
  195. package/.next/server/chunks/ssr/[root-of-the-server]__1waa3rc._.js +85 -0
  196. package/.next/server/chunks/ssr/_0-2qsxo._.js +3 -0
  197. package/.next/server/chunks/ssr/_00rehl_._.js +3 -0
  198. package/.next/server/chunks/ssr/{_1lz_cq2._.js → _02oc_h5._.js} +76 -97
  199. package/.next/server/chunks/ssr/_05yghx6._.js +3 -0
  200. package/.next/server/chunks/ssr/_07nbwap._.js +3 -0
  201. package/.next/server/chunks/ssr/_08c4z-9._.js +3 -0
  202. package/.next/server/chunks/ssr/_0_qfdlw._.js +4 -0
  203. package/.next/server/chunks/ssr/{_0m2lsd1._.js → _0b6_jv-._.js} +2 -2
  204. package/.next/server/chunks/ssr/_0evkpto._.js +3 -0
  205. package/.next/server/chunks/ssr/_0geg212._.js +3 -0
  206. package/.next/server/chunks/ssr/{_1jdgjv9._.js → _0hxt2ul._.js} +2 -2
  207. package/.next/server/chunks/ssr/_0i_6q1w._.js +2 -2
  208. package/.next/server/chunks/ssr/_0lu1n_x._.js +1 -1
  209. package/.next/server/chunks/ssr/_0no7i1o._.js +3 -0
  210. package/.next/server/chunks/ssr/_0tzg--b._.js +3 -0
  211. package/.next/server/chunks/ssr/{_15j2_8f._.js → _0wzaht-._.js} +2 -2
  212. package/.next/server/chunks/ssr/_0z-g0uy._.js +3 -0
  213. package/.next/server/chunks/ssr/_114ylv7._.js +7 -0
  214. package/.next/server/chunks/ssr/_123y-ah._.js +3 -0
  215. package/.next/server/chunks/ssr/_12c-iwh._.js +3 -0
  216. package/.next/server/chunks/ssr/_12w42gi._.js +3 -0
  217. package/.next/server/chunks/ssr/_161kqav._.js +3 -0
  218. package/.next/server/chunks/ssr/_167hchk._.js +3 -0
  219. package/.next/server/chunks/ssr/{_0ap2fzn._.js → _16c6_xm._.js} +2 -2
  220. package/.next/server/chunks/ssr/_18opah7._.js +3 -0
  221. package/.next/server/chunks/ssr/_1_krfph._.js +3 -0
  222. package/.next/server/chunks/ssr/{_0rzucul._.js → _1_x5spz._.js} +3 -3
  223. package/.next/server/chunks/ssr/_1a61fc9._.js +3 -0
  224. package/.next/server/chunks/ssr/_1ey7qn-._.js +70 -0
  225. package/.next/server/chunks/ssr/_1g9vs7q._.js +3 -0
  226. package/.next/server/chunks/ssr/_1jmz33w._.js +3 -0
  227. package/.next/server/chunks/ssr/_1rgtxhr._.js +5 -0
  228. package/.next/server/chunks/ssr/_1slp25y._.js +3 -0
  229. package/.next/server/chunks/ssr/_1w_t7h7._.js +101 -0
  230. package/.next/server/chunks/ssr/{_0s99bgb._.js → _1wyfaq_._.js} +9 -3
  231. package/.next/server/chunks/ssr/_200gt-m._.js +102 -0
  232. package/.next/server/chunks/ssr/components_ui_tabs_tsx_0ej40b4._.js +3 -0
  233. package/.next/server/chunks/ssr/features_bot_thread_query_ts_023eklt._.js +4 -0
  234. package/.next/server/chunks/ssr/features_config_components_config-setting_tsx_0xfwqdg._.js +1 -1
  235. package/.next/server/chunks/ssr/features_connectors_components_mcp-setting_tsx_0jmbgvp._.js +1 -1
  236. package/.next/server/chunks/ssr/features_workspace_00jz2hu._.js +8 -0
  237. package/.next/server/chunks/ssr/{_1-teee5._.js → lib_logger_ts_0-olsze._.js} +2 -7
  238. package/.next/server/chunks/ssr/{node_modules_05-n6dz._.js → node_modules_02ai7br._.js} +1 -1
  239. package/.next/server/chunks/ssr/{node_modules_1chvq5f._.js → node_modules_02deexj._.js} +1 -1
  240. package/.next/server/chunks/ssr/{node_modules_0m383r2._.js → node_modules_07za262._.js} +1 -1
  241. package/.next/server/chunks/ssr/{node_modules_07dsgkx._.js → node_modules_0_s9get._.js} +1 -1
  242. package/.next/server/chunks/ssr/{node_modules_1a0kmjl._.js → node_modules_0n3c7h0._.js} +1 -1
  243. package/.next/server/chunks/ssr/{node_modules_0zeg3x_._.js → node_modules_0od6jwr._.js} +1 -1
  244. package/.next/server/chunks/ssr/{node_modules_1y79i83._.js → node_modules_0rdt7pk._.js} +1 -1
  245. package/.next/server/chunks/ssr/{node_modules_17bwo43._.js → node_modules_0tusx4d._.js} +3 -3
  246. package/.next/server/chunks/ssr/{node_modules_04sksn9._.js → node_modules_0w1z7kz._.js} +1 -1
  247. package/.next/server/chunks/ssr/{node_modules_1uyk91w._.js → node_modules_0wdupfz._.js} +1 -1
  248. package/.next/server/chunks/ssr/{node_modules_0ifp389._.js → node_modules_1720w0a._.js} +1 -1
  249. package/.next/server/chunks/ssr/{node_modules_0u9da1h._.js → node_modules_18lsg7d._.js} +1 -1
  250. package/.next/server/chunks/ssr/{node_modules_1k6n600._.js → node_modules_1b94fxf._.js} +1 -1
  251. package/.next/server/chunks/ssr/{node_modules_20c28js._.js → node_modules_1ecdo1s._.js} +1 -1
  252. package/.next/server/chunks/ssr/{node_modules_0fkeku3._.js → node_modules_1fa9ag7._.js} +1 -1
  253. package/.next/server/chunks/ssr/{node_modules_1j8j4pt._.js → node_modules_1fv-_si._.js} +1 -1
  254. package/.next/server/chunks/ssr/{node_modules_0-dr_o7._.js → node_modules_1hf461g._.js} +1 -1
  255. package/.next/server/chunks/ssr/{node_modules_1lz0oxc._.js → node_modules_1m71ta-._.js} +1 -1
  256. package/.next/server/chunks/ssr/{node_modules_1t77e4n._.js → node_modules_1n93n-g._.js} +1 -1
  257. package/.next/server/chunks/ssr/{node_modules_16mkxuo._.js → node_modules_1n9lqu9._.js} +1 -1
  258. package/.next/server/chunks/ssr/{node_modules_1k7z2j6._.js → node_modules_1npnsvd._.js} +1 -1
  259. package/.next/server/chunks/ssr/{node_modules_056qgf-._.js → node_modules_1rn2r0n._.js} +1 -1
  260. package/.next/server/chunks/ssr/{node_modules_0x3-ply._.js → node_modules_1seuurl._.js} +2 -2
  261. package/.next/server/chunks/ssr/{node_modules_0bvopy2._.js → node_modules_1vq-mfk._.js} +1 -1
  262. package/.next/server/chunks/ssr/{node_modules_0iur59s._.js → node_modules_1vzyw1t._.js} +1 -1
  263. package/.next/server/chunks/ssr/{node_modules_1k8j8n0._.js → node_modules_1w359w4._.js} +1 -1
  264. package/.next/server/chunks/ssr/{node_modules_1x0ztf4._.js → node_modules_1yajbl6._.js} +1 -1
  265. package/.next/server/chunks/ssr/{node_modules_1-y7nni._.js → node_modules_20eqhy8._.js} +1 -1
  266. package/.next/server/chunks/ssr/{node_modules_@mermaid-js_parser_dist_chunks_mermaid-parser_core_0bkwd0u._.js → node_modules_@mermaid-js_parser_dist_chunks_mermaid-parser_core_019sbis._.js} +1 -1
  267. package/.next/server/chunks/ssr/{node_modules_@mermaid-js_parser_dist_chunks_mermaid-parser_core_0p9a9su._.js → node_modules_@mermaid-js_parser_dist_chunks_mermaid-parser_core_0s6piq3._.js} +1 -1
  268. package/.next/server/chunks/ssr/{node_modules_@vercel_oidc_dist_1o553h-._.js → node_modules_@vercel_oidc_dist_05itbb1._.js} +1 -1
  269. package/.next/server/chunks/ssr/node_modules_lucide-react_dist_esm_icons_trash_mjs_0px2gzr._.js +3 -0
  270. package/.next/server/middleware-build-manifest.js +3 -3
  271. package/.next/server/pages/404.html +1 -1
  272. package/.next/server/pages/500.html +1 -1
  273. package/.next/server/server-reference-manifest.js +1 -1
  274. package/.next/server/server-reference-manifest.json +68 -46
  275. package/.next/static/chunks/07vqbk_9t4em4.js +2 -0
  276. package/.next/static/chunks/08krja1zx7nqg.js +1 -0
  277. package/.next/static/chunks/0g4b348l0pve7.js +1 -0
  278. package/.next/static/chunks/0qcuwdk132jjt.js +1 -0
  279. package/.next/static/chunks/0vlgzqo9ka90v.js +1 -0
  280. package/.next/static/chunks/{11r2yp_yh9_qn.js → 0z69gvysmave7.js} +1 -1
  281. package/.next/static/chunks/0zrupxgci5gqc.js +1 -0
  282. package/.next/static/chunks/12fvz1w5ows75.js +1 -0
  283. package/.next/static/chunks/14t03ned286iu.js +1 -0
  284. package/.next/static/chunks/156xxbkrz5lzu.js +18 -0
  285. package/.next/static/chunks/1fz14reod4lft.js +1 -0
  286. package/.next/static/chunks/1g23dbabl-vkr.js +1 -0
  287. package/.next/static/chunks/1ohslk4568tm3.js +1 -0
  288. package/.next/static/chunks/1qy_rc3oxfxok.js +1 -0
  289. package/.next/static/chunks/1sbmwswc9k2-1.js +1 -0
  290. package/.next/static/chunks/1vqezbhjm3goq.js +1 -0
  291. package/.next/static/chunks/1zea7qdl-7365.js +1 -0
  292. package/.next/static/chunks/1zvqumkg7kmo0.css +1 -0
  293. package/.next/static/chunks/26hpqt3y6bogl.js +12 -0
  294. package/.next/static/chunks/2_j5mqm6d-z18.js +99 -0
  295. package/.next/static/chunks/2dt6tu7a22svs.js +1 -0
  296. package/.next/static/chunks/2h5ls3d86kya6.js +1 -0
  297. package/.next/static/chunks/2wc6y1ht6dfuy.js +1 -0
  298. package/.next/static/chunks/{435atsnhs7ksu.js → 2xr0zpll7thyi.js} +2 -2
  299. package/.next/static/chunks/31jztc-yq30m7.js +1 -0
  300. package/.next/static/chunks/391af0i8eljzk.js +1 -0
  301. package/.next/static/chunks/3bfwpww3m3x4u.js +1 -0
  302. package/.next/static/chunks/{38gweqdo4jpfj.js → 3izg0a3k87q1-.js} +5 -5
  303. package/.next/static/chunks/3p2s2cjokx1aa.js +5 -0
  304. package/.next/static/chunks/{408ueu4hfvcg7.js → 41s3fml5oivsa.js} +1 -1
  305. package/README.md +45 -193
  306. package/app/api/events/app-event.ts +6 -4
  307. package/app/api/query-key.ts +25 -16
  308. package/bin/database.mjs +37 -0
  309. package/bin/thursday.mjs +44 -28
  310. package/components/ui/custom-icon.tsx +0 -60
  311. package/components/ui/radio-group.tsx +37 -0
  312. package/components/ui/shiny-text.tsx +33 -6
  313. package/database/migrations/{20260910173049_numerous_spirit → 20260916145701_clever_multiple_man}/migration.sql +80 -24
  314. package/database/migrations/{20260910173049_numerous_spirit → 20260916145701_clever_multiple_man}/snapshot.json +689 -110
  315. package/database/migrations/20260916160713_thread_cancelled/migration.sql +2 -0
  316. package/database/migrations/{20260911030320_opposite_jack_power → 20260916160713_thread_cancelled}/snapshot.json +706 -84
  317. package/docs/how-it-works.md +40 -35
  318. package/docs/images/keeps-going.png +0 -0
  319. package/docs/images/two-minds.png +0 -0
  320. package/docs/live-calls.md +236 -0
  321. package/docs/thread-rooms.md +71 -0
  322. package/features/ai/chatgpt.ts +1 -2
  323. package/features/ai/live.schema.ts +128 -0
  324. package/features/ai/load-tools.ts +152 -79
  325. package/features/ai/model.schema.ts +2 -120
  326. package/features/ai/model.ts +52 -88
  327. package/features/ai/prompts/bot.prompt.ts +91 -214
  328. package/features/ai/prompts/live.prompt.ts +169 -0
  329. package/features/ai/prompts/prompt-helper.ts +92 -32
  330. package/features/ai/prompts/thursday.prompt.ts +164 -190
  331. package/features/ai/tools/bot.tool.ts +38 -145
  332. package/features/ai/tools/call.tool.ts +33 -5
  333. package/features/ai/tools/connected.ts +5 -3
  334. package/features/ai/tools/mcp.tool.ts +7 -4
  335. package/features/ai/tools/memory.tool.ts +35 -31
  336. package/features/ai/tools/skills.tool.ts +11 -6
  337. package/features/ai/tools/studio.tool.ts +26 -21
  338. package/features/ai/tools/tool-name.ts +3 -5
  339. package/features/ai/tools/workspace.tool.ts +33 -10
  340. package/features/artifact/artifact.action.ts +2 -2
  341. package/features/artifact/artifact.query.ts +112 -61
  342. package/features/artifact/artifact.schema.ts +8 -6
  343. package/features/artifact/components/artifact-setting.tsx +83 -33
  344. package/features/bot/bot.action.ts +43 -36
  345. package/features/bot/bot.memory.ts +137 -20
  346. package/features/bot/bot.query.ts +28 -11
  347. package/features/bot/bot.run.ts +296 -744
  348. package/features/bot/bot.runner.ts +463 -864
  349. package/features/bot/bot.schema.ts +93 -82
  350. package/features/bot/bot.seed.ts +141 -64
  351. package/features/bot/components/bot-mark.tsx +324 -232
  352. package/features/bot/components/bot-room.tsx +1613 -697
  353. package/features/bot/components/bot-roster.tsx +8 -13
  354. package/features/bot/components/bot-setting.tsx +58 -42
  355. package/features/bot/components/bot-tool.tsx +55 -87
  356. package/features/bot/components/mark-palette.tsx +35 -32
  357. package/features/bot/components/path-chips.tsx +33 -3
  358. package/features/bot/components/thread-badge.tsx +34 -0
  359. package/features/bot/components/thread-reply.tsx +639 -0
  360. package/features/bot/components/thread-setting.tsx +674 -0
  361. package/features/bot/mark.const.ts +79 -31
  362. package/features/bot/room.query.ts +889 -0
  363. package/features/bot/room.schema.ts +64 -0
  364. package/features/bot/seed-bots.ts +2 -1
  365. package/features/bot/thread.query.ts +971 -0
  366. package/features/bot/thread.store.ts +587 -0
  367. package/features/config/components/voice-key.tsx +113 -88
  368. package/features/config/config.const.ts +5 -9
  369. package/features/connectors/mcp.schema.ts +0 -2
  370. package/features/intro/components/intro.tsx +169 -139
  371. package/features/memory/components/memory-setting.tsx +2 -7
  372. package/features/memory/memory.schema.ts +14 -23
  373. package/features/settings/components/setting-ui.tsx +0 -44
  374. package/features/settings/components/settings.tsx +12 -12
  375. package/features/settings/settings.alert.ts +3 -3
  376. package/features/settings/settings.store.ts +1 -1
  377. package/features/skills/skills.schema.ts +0 -2
  378. package/features/thursday/ascii.const.ts +74 -0
  379. package/features/thursday/components/ascii-field.tsx +24 -44
  380. package/features/thursday/components/ascii-orb.tsx +525 -381
  381. package/features/thursday/components/call-log.tsx +46 -44
  382. package/features/thursday/components/face.tsx +40 -10
  383. package/features/thursday/components/side-captions.tsx +273 -0
  384. package/features/thursday/components/tab-state.tsx +14 -6
  385. package/features/thursday/components/thursday-mark.tsx +1 -0
  386. package/features/thursday/components/thursday-setting.tsx +633 -333
  387. package/features/thursday/components/thursday.tsx +315 -274
  388. package/features/thursday/components/voice-picker.tsx +255 -0
  389. package/features/thursday/face.store.ts +5 -0
  390. package/features/thursday/thursday.action.ts +96 -72
  391. package/features/thursday/thursday.query.ts +49 -30
  392. package/features/thursday/thursday.schema.ts +37 -35
  393. package/features/thursday/thursday.store.ts +14 -7
  394. package/features/thursday/tool-call.ts +3 -3
  395. package/features/thursday/tool-line.ts +5 -3
  396. package/features/thursday/use-thursday.ts +801 -457
  397. package/features/workspace/file-kind.ts +9 -0
  398. package/features/workspace/workspace.query.ts +17 -0
  399. package/features/workspace/workspace.ts +82 -36
  400. package/hooks/use-hotkey.ts +3 -2
  401. package/lib/live/live.schema.ts +26 -0
  402. package/lib/live/live.server.ts +153 -0
  403. package/lib/live/live.session.ts +574 -0
  404. package/lib/{realtime/realtime.tap.ts → live/live.tap.ts} +41 -12
  405. package/lib/live/live.transport.ts +152 -0
  406. package/lib/protocol/use-server-pages.ts +9 -5
  407. package/lib/utils.ts +21 -20
  408. package/package.json +2 -2
  409. package/public/voices/alloy.ogg +0 -0
  410. package/public/voices/ash.ogg +0 -0
  411. package/public/voices/ballad.ogg +0 -0
  412. package/public/voices/beacon.ogg +0 -0
  413. package/public/voices/bossa.ogg +0 -0
  414. package/public/voices/cedar.ogg +0 -0
  415. package/public/voices/cinder.ogg +0 -0
  416. package/public/voices/coral.ogg +0 -0
  417. package/public/voices/delta.ogg +0 -0
  418. package/public/voices/echo.ogg +0 -0
  419. package/public/voices/gleam.ogg +0 -0
  420. package/public/voices/marin.ogg +0 -0
  421. package/public/voices/meridian.ogg +0 -0
  422. package/public/voices/quartz.ogg +0 -0
  423. package/public/voices/ripple.ogg +0 -0
  424. package/public/voices/sage.ogg +0 -0
  425. package/public/voices/shimmer.ogg +0 -0
  426. package/public/voices/stone.ogg +0 -0
  427. package/public/voices/tempo.ogg +0 -0
  428. package/public/voices/verse.ogg +0 -0
  429. package/public/voices/vesper.ogg +0 -0
  430. package/public/voices/willow.ogg +0 -0
  431. package/scripts/artifact-paths.test.mts +38 -0
  432. package/scripts/bot-context.test.mts +1408 -0
  433. package/scripts/live-history.test.mts +115 -0
  434. package/scripts/live-session.test.mts +938 -0
  435. package/server.js +1 -1
  436. package/skills/browser/SKILL.md +11 -1
  437. package/skills/computer/SKILL.md +1 -1
  438. package/skills/find-skills/SKILL.md +3 -8
  439. package/skills/interactive-page/SKILL.md +12 -7
  440. package/skills/interactive-page/references/diagram.md +89 -0
  441. package/skills/interactive-page/scripts/archify/LICENSE +22 -0
  442. package/skills/interactive-page/scripts/archify/README.md +25 -0
  443. package/skills/interactive-page/scripts/archify/assets/JetBrainsMono-OFL.txt +93 -0
  444. package/skills/interactive-page/scripts/archify/assets/template.html +14934 -0
  445. package/skills/interactive-page/scripts/archify/bin/archify.mjs +2126 -0
  446. package/skills/interactive-page/scripts/archify/bin/open-artifact.mjs +86 -0
  447. package/skills/interactive-page/scripts/archify/bin/preview.mjs +653 -0
  448. package/skills/interactive-page/scripts/archify/bin/visual-check.mjs +829 -0
  449. package/skills/interactive-page/scripts/archify/delta/architecture-delta.mjs +1223 -0
  450. package/skills/interactive-page/scripts/archify/examples/agent-run.lifecycle.json +60 -0
  451. package/skills/interactive-page/scripts/archify/examples/agent-tool-call.workflow.json +94 -0
  452. package/skills/interactive-page/scripts/archify/examples/async-job-roundtrip.sequence.json +61 -0
  453. package/skills/interactive-page/scripts/archify/examples/cache-miss-request.sequence.json +82 -0
  454. package/skills/interactive-page/scripts/archify/examples/checkout-platform.base.architecture.json +31 -0
  455. package/skills/interactive-page/scripts/archify/examples/checkout-platform.head.architecture.json +31 -0
  456. package/skills/interactive-page/scripts/archify/examples/deployment-release.lifecycle.json +49 -0
  457. package/skills/interactive-page/scripts/archify/examples/event-stream.dataflow.json +57 -0
  458. package/skills/interactive-page/scripts/archify/examples/incident-response.workflow.json +64 -0
  459. package/skills/interactive-page/scripts/archify/examples/product-analytics.dataflow.json +76 -0
  460. package/skills/interactive-page/scripts/archify/examples/production-deployment.architecture.json +71 -0
  461. package/skills/interactive-page/scripts/archify/examples/release-delivery.workflow.json +62 -0
  462. package/skills/interactive-page/scripts/archify/examples/web-app.architecture.json +46 -0
  463. package/skills/interactive-page/scripts/archify/migrations/workflow-v2.mjs +279 -0
  464. package/skills/interactive-page/scripts/archify/recipes/scenarios.mjs +284 -0
  465. package/skills/interactive-page/scripts/archify/references/authoring-contract.md +243 -0
  466. package/skills/interactive-page/scripts/archify/references/delivery-contract.md +120 -0
  467. package/skills/interactive-page/scripts/archify/references/viewer-runtime.md +45 -0
  468. package/skills/interactive-page/scripts/archify/renderers/architecture/grid.mjs +62 -0
  469. package/skills/interactive-page/scripts/archify/renderers/architecture/render-architecture.mjs +1078 -0
  470. package/skills/interactive-page/scripts/archify/renderers/dataflow/README.md +104 -0
  471. package/skills/interactive-page/scripts/archify/renderers/dataflow/render-dataflow.mjs +483 -0
  472. package/skills/interactive-page/scripts/archify/renderers/lifecycle/README.md +115 -0
  473. package/skills/interactive-page/scripts/archify/renderers/lifecycle/render-lifecycle.mjs +561 -0
  474. package/skills/interactive-page/scripts/archify/renderers/sequence/README.md +114 -0
  475. package/skills/interactive-page/scripts/archify/renderers/sequence/render-sequence.mjs +464 -0
  476. package/skills/interactive-page/scripts/archify/renderers/shared/brand-marks.mjs +563 -0
  477. package/skills/interactive-page/scripts/archify/renderers/shared/cli.mjs +218 -0
  478. package/skills/interactive-page/scripts/archify/renderers/shared/desktop-readability.mjs +26 -0
  479. package/skills/interactive-page/scripts/archify/renderers/shared/diagnostics.mjs +127 -0
  480. package/skills/interactive-page/scripts/archify/renderers/shared/engineering-profiles.mjs +157 -0
  481. package/skills/interactive-page/scripts/archify/renderers/shared/generated-brand-marks.mjs +3 -0
  482. package/skills/interactive-page/scripts/archify/renderers/shared/generated-validators.mjs +13 -0
  483. package/skills/interactive-page/scripts/archify/renderers/shared/geometry.mjs +1423 -0
  484. package/skills/interactive-page/scripts/archify/renderers/shared/i18n.mjs +595 -0
  485. package/skills/interactive-page/scripts/archify/renderers/shared/layout-report.mjs +40 -0
  486. package/skills/interactive-page/scripts/archify/renderers/shared/legend.mjs +217 -0
  487. package/skills/interactive-page/scripts/archify/renderers/shared/output-path.mjs +340 -0
  488. package/skills/interactive-page/scripts/archify/renderers/shared/repository-evidence.mjs +238 -0
  489. package/skills/interactive-page/scripts/archify/renderers/shared/repository-location.mjs +58 -0
  490. package/skills/interactive-page/scripts/archify/renderers/shared/text-fit.mjs +49 -0
  491. package/skills/interactive-page/scripts/archify/renderers/shared/utils.mjs +232 -0
  492. package/skills/interactive-page/scripts/archify/renderers/shared/validator.mjs +86 -0
  493. package/skills/interactive-page/scripts/archify/renderers/workflow/README.md +223 -0
  494. package/skills/interactive-page/scripts/archify/renderers/workflow/render-workflow.mjs +35 -0
  495. package/skills/interactive-page/scripts/archify/renderers/workflow/workflow-compiler.mjs +4400 -0
  496. package/skills/interactive-page/scripts/archify/renderers/workflow/workflow-migration-geometry.mjs +144 -0
  497. package/skills/interactive-page/scripts/archify/schemas/README.md +211 -0
  498. package/skills/interactive-page/scripts/archify/schemas/architecture.schema.json +178 -0
  499. package/skills/interactive-page/scripts/archify/schemas/common.schema.json +115 -0
  500. package/skills/interactive-page/scripts/archify/schemas/dataflow.schema.json +243 -0
  501. package/skills/interactive-page/scripts/archify/schemas/lifecycle.schema.json +266 -0
  502. package/skills/interactive-page/scripts/archify/schemas/sequence.schema.json +223 -0
  503. package/skills/interactive-page/scripts/archify/schemas/workflow.schema.json +428 -0
  504. package/skills/interactive-page/scripts/archify/scripts/check-render-output.mjs +916 -0
  505. package/skills/interactive-page/scripts/archify/scripts/render-examples.mjs +26 -0
  506. package/skills/interactive-page/scripts/bundle-artifact.sh +9 -8
  507. package/skills/skill-creator/SKILL.md +4 -5
  508. package/.next/server/app/api/bot/task/[id]/route/app-paths-manifest.json +0 -3
  509. package/.next/server/app/api/bot/task/[id]/route.js +0 -16
  510. package/.next/server/app/api/bot/task/[id]/route.js.nft.json +0 -1
  511. package/.next/server/app/api/bot/task/[id]/route_client-reference-manifest.js +0 -3
  512. package/.next/server/app/api/bot/task/route/app-paths-manifest.json +0 -3
  513. package/.next/server/app/api/bot/task/route.js +0 -16
  514. package/.next/server/app/api/bot/task/route.js.nft.json +0 -1
  515. package/.next/server/app/api/bot/task/route_client-reference-manifest.js +0 -3
  516. package/.next/server/chunks/[root-of-the-server]__026u77e._.js +0 -5
  517. package/.next/server/chunks/[root-of-the-server]__02va5wz._.js +0 -3
  518. package/.next/server/chunks/[root-of-the-server]__050zri4._.js +0 -3
  519. package/.next/server/chunks/[root-of-the-server]__0bg9710._.js +0 -3
  520. package/.next/server/chunks/[root-of-the-server]__0ffbg85._.js +0 -3
  521. package/.next/server/chunks/[root-of-the-server]__0in-d5s._.js +0 -3
  522. package/.next/server/chunks/[root-of-the-server]__0ncs9uu._.js +0 -3
  523. package/.next/server/chunks/[root-of-the-server]__0pa9k-f._.js +0 -8
  524. package/.next/server/chunks/[root-of-the-server]__0qi_m0e._.js +0 -8
  525. package/.next/server/chunks/[root-of-the-server]__0tq0m_k._.js +0 -3
  526. package/.next/server/chunks/[root-of-the-server]__0xep6j_._.js +0 -3
  527. package/.next/server/chunks/[root-of-the-server]__1-ga0kh._.js +0 -5
  528. package/.next/server/chunks/[root-of-the-server]__11n256_._.js +0 -5
  529. package/.next/server/chunks/[root-of-the-server]__144l7qt._.js +0 -8
  530. package/.next/server/chunks/[root-of-the-server]__1_04mcf._.js +0 -5
  531. package/.next/server/chunks/[root-of-the-server]__1anc1p9._.js +0 -3
  532. package/.next/server/chunks/[root-of-the-server]__1fizn31._.js +0 -3
  533. package/.next/server/chunks/[root-of-the-server]__1jg8t86._.js +0 -8
  534. package/.next/server/chunks/[root-of-the-server]__1lldzy9._.js +0 -3
  535. package/.next/server/chunks/[root-of-the-server]__1llj3-b._.js +0 -3
  536. package/.next/server/chunks/[root-of-the-server]__1p8m3f4._.js +0 -3
  537. package/.next/server/chunks/[root-of-the-server]__1peqvsi._.js +0 -3
  538. package/.next/server/chunks/[root-of-the-server]__1w8gm5h._.js +0 -10
  539. package/.next/server/chunks/[root-of-the-server]__1xt39nn._.js +0 -8
  540. package/.next/server/chunks/[root-of-the-server]__1zb8mfo._.js +0 -8
  541. package/.next/server/chunks/_0-73of8._.js +0 -55
  542. package/.next/server/chunks/_0-fjql4._.js +0 -31
  543. package/.next/server/chunks/_00xh_q0._.js +0 -25
  544. package/.next/server/chunks/_051qh8u._.js +0 -3
  545. package/.next/server/chunks/_05sovvj._.js +0 -3
  546. package/.next/server/chunks/_091bi8d._.js +0 -3
  547. package/.next/server/chunks/_0fzqt2y._.js +0 -142
  548. package/.next/server/chunks/_0lr92vd._.js +0 -3
  549. package/.next/server/chunks/_0mecv0f._.js +0 -31
  550. package/.next/server/chunks/_0mh8j4m._.js +0 -3
  551. package/.next/server/chunks/_0sb7tlz._.js +0 -3
  552. package/.next/server/chunks/_0si7xzn._.js +0 -3
  553. package/.next/server/chunks/_0u10wmw._.js +0 -55
  554. package/.next/server/chunks/_0wp4le3._.js +0 -3
  555. package/.next/server/chunks/_0y5fg-x._.js +0 -3
  556. package/.next/server/chunks/_1230wbx._.js +0 -10
  557. package/.next/server/chunks/_12nheja._.js +0 -3
  558. package/.next/server/chunks/_13510xh._.js +0 -3
  559. package/.next/server/chunks/_15hq89o._.js +0 -3
  560. package/.next/server/chunks/_18-1wcc._.js +0 -55
  561. package/.next/server/chunks/_18d7-5p._.js +0 -3
  562. package/.next/server/chunks/_1c78vw5._.js +0 -237
  563. package/.next/server/chunks/_1h5_8sk._.js +0 -3
  564. package/.next/server/chunks/_1hdnxb7._.js +0 -3
  565. package/.next/server/chunks/_1j8k_j5._.js +0 -3
  566. package/.next/server/chunks/_1nula3v._.js +0 -3
  567. package/.next/server/chunks/_1php4eq._.js +0 -3
  568. package/.next/server/chunks/_1yuv4vp._.js +0 -3
  569. package/.next/server/chunks/_1zkfbaa._.js +0 -3
  570. package/.next/server/chunks/_next-internal_server_app_api_bot_task_[id]_route_actions_1_k_8u3.js +0 -3
  571. package/.next/server/chunks/_next-internal_server_app_api_bot_task_route_actions_0xln6ra.js +0 -3
  572. package/.next/server/chunks/features_0rpfxbk._.js +0 -3
  573. package/.next/server/chunks/features_1qf3cei._.js +0 -8
  574. package/.next/server/chunks/features_bot_0zt92oh._.js +0 -3
  575. package/.next/server/chunks/node_modules_consola_dist_chunks_prompt_mjs_1jidylq._.js +0 -3
  576. package/.next/server/chunks/ssr/[externals]__0dizgek._.js +0 -3
  577. package/.next/server/chunks/ssr/[root-of-the-server]__0-nob52._.js +0 -4
  578. package/.next/server/chunks/ssr/[root-of-the-server]__092knlj._.js +0 -3
  579. package/.next/server/chunks/ssr/[root-of-the-server]__0mz33a7._.js +0 -3
  580. package/.next/server/chunks/ssr/[root-of-the-server]__0s4wb56._.js +0 -3
  581. package/.next/server/chunks/ssr/[root-of-the-server]__0s9dq5u._.js +0 -3
  582. package/.next/server/chunks/ssr/[root-of-the-server]__0sd3izg._.js +0 -3
  583. package/.next/server/chunks/ssr/[root-of-the-server]__0vnmv-s._.js +0 -3
  584. package/.next/server/chunks/ssr/[root-of-the-server]__1-13tcn._.js +0 -43
  585. package/.next/server/chunks/ssr/[root-of-the-server]__10-dz5o._.js +0 -61
  586. package/.next/server/chunks/ssr/[root-of-the-server]__1zidpfl._.js +0 -3
  587. package/.next/server/chunks/ssr/_001_ms1._.js +0 -33
  588. package/.next/server/chunks/ssr/_01n3o0m._.js +0 -3
  589. package/.next/server/chunks/ssr/_04z2c7b._.js +0 -3
  590. package/.next/server/chunks/ssr/_06tssji._.js +0 -3
  591. package/.next/server/chunks/ssr/_0fk4c7d._.js +0 -3
  592. package/.next/server/chunks/ssr/_0h42fze._.js +0 -3
  593. package/.next/server/chunks/ssr/_0h7zdni._.js +0 -3
  594. package/.next/server/chunks/ssr/_0mggveb._.js +0 -3
  595. package/.next/server/chunks/ssr/_0nbpc_8._.js +0 -3
  596. package/.next/server/chunks/ssr/_0q0zan6._.js +0 -70
  597. package/.next/server/chunks/ssr/_0zcjrbg._.js +0 -3
  598. package/.next/server/chunks/ssr/_0zhoc3r._.js +0 -3
  599. package/.next/server/chunks/ssr/_11j92tu._.js +0 -3
  600. package/.next/server/chunks/ssr/_13fke0x._.js +0 -3
  601. package/.next/server/chunks/ssr/_18cymkq._.js +0 -3
  602. package/.next/server/chunks/ssr/_1ebxla1._.js +0 -3
  603. package/.next/server/chunks/ssr/_1nj4niv._.js +0 -42
  604. package/.next/server/chunks/ssr/_1ov37if._.js +0 -3
  605. package/.next/server/chunks/ssr/_1ovmc9s._.js +0 -3
  606. package/.next/server/chunks/ssr/_1sji22n._.js +0 -3
  607. package/.next/server/chunks/ssr/_1vi_uo7._.js +0 -3
  608. package/.next/server/chunks/ssr/_1vyd1ji._.js +0 -3
  609. package/.next/server/chunks/ssr/_1xx4zlb._.js +0 -3
  610. package/.next/server/chunks/ssr/components_ui_1y1l7yj._.js +0 -3
  611. package/.next/server/chunks/ssr/features_bot_1s_-2vj._.js +0 -3
  612. package/.next/server/chunks/ssr/lib_realtime_12wg_dd._.js +0 -27
  613. package/.next/server/chunks/ssr/lib_realtime_15h_v1v._.js +0 -27
  614. package/.next/server/chunks/ssr/node_modules_lucide-react_dist_esm_icons_0npjf8-._.js +0 -3
  615. package/.next/static/chunks/0n3jle9kbqfr2.js +0 -40
  616. package/.next/static/chunks/0r5qcctujhxli.js +0 -1
  617. package/.next/static/chunks/10teww7wt-f6h.js +0 -1
  618. package/.next/static/chunks/126fj4pga_72k.js +0 -1
  619. package/.next/static/chunks/159udiexf6dom.js +0 -2
  620. package/.next/static/chunks/18a96rkhi4_yd.js +0 -1
  621. package/.next/static/chunks/18gi7o2ibrgh8.js +0 -3
  622. package/.next/static/chunks/1dkjg87pxk0n9.js +0 -28
  623. package/.next/static/chunks/1q9z1tohil7xn.js +0 -1
  624. package/.next/static/chunks/1sjgfets224re.js +0 -1
  625. package/.next/static/chunks/1wszsr83t01gh.js +0 -1
  626. package/.next/static/chunks/22_zkas21rpna.js +0 -1
  627. package/.next/static/chunks/2fh4-3z_q8b_w.js +0 -1
  628. package/.next/static/chunks/2h0sd2vumhb15.js +0 -1
  629. package/.next/static/chunks/2szintyibu0s5.js +0 -1
  630. package/.next/static/chunks/2t4vhi3m8ahya.js +0 -1
  631. package/.next/static/chunks/2tigdtz3dpcy8.js +0 -1
  632. package/.next/static/chunks/3-b6b7entqazs.js +0 -1
  633. package/.next/static/chunks/3786h5vayt80-.js +0 -28
  634. package/.next/static/chunks/3cq5g494yuke8.css +0 -1
  635. package/.next/static/chunks/3dmbhsv2u_8kj.js +0 -1
  636. package/.next/static/chunks/3hibbjnt85-k6.js +0 -1
  637. package/.next/static/chunks/3i0a5px-oufgc.js +0 -1
  638. package/.next/static/chunks/3wexk9qo8zuj3.js +0 -18
  639. package/.next/static/chunks/3xk7yqgomx7yq.js +0 -1
  640. package/.next/static/chunks/3xre8bu_pk1p_.js +0 -1
  641. package/.next/static/chunks/3zv6t3i77xhr_.js +0 -1
  642. package/database/migrations/20260911030320_opposite_jack_power/migration.sql +0 -1
  643. package/docs/images/task-thread.png +0 -0
  644. package/features/bot/components/task-badge.tsx +0 -42
  645. package/features/bot/components/task-reply.tsx +0 -328
  646. package/features/bot/components/task-setting.tsx +0 -524
  647. package/features/bot/task.query.ts +0 -1079
  648. package/features/bot/task.store.ts +0 -487
  649. package/lib/realtime/client-secret.ts +0 -86
  650. package/lib/realtime/open-session.ts +0 -31
  651. package/lib/realtime/openai.ts +0 -35
  652. package/lib/realtime/realtime.audio.ts +0 -196
  653. package/lib/realtime/realtime.driver.ts +0 -489
  654. package/lib/realtime/realtime.protocol.ts +0 -393
  655. package/lib/realtime/realtime.schema.ts +0 -37
  656. package/lib/realtime/realtime.session.ts +0 -106
  657. package/lib/realtime/realtime.transport.ts +0 -384
  658. package/lib/realtime/xai.ts +0 -39
  659. /package/.next/server/app/api/bot/{task → thread}/[id]/route/build-manifest.json +0 -0
  660. /package/.next/server/app/api/bot/{task → thread}/[id]/route/server-reference-manifest.json +0 -0
  661. /package/.next/server/app/api/bot/{task → thread}/[id]/route.js.map +0 -0
  662. /package/.next/server/app/api/bot/{task → thread}/route/build-manifest.json +0 -0
  663. /package/.next/server/app/api/bot/{task → thread}/route/server-reference-manifest.json +0 -0
  664. /package/.next/server/app/api/bot/{task → thread}/route.js.map +0 -0
  665. /package/.next/static/{G6-gNoyGDEcgAsq-L4PE1 → yyRVBc0xxYQsh3GpbQX9b}/_buildManifest.js +0 -0
  666. /package/.next/static/{G6-gNoyGDEcgAsq-L4PE1 → yyRVBc0xxYQsh3GpbQX9b}/_clientMiddlewareManifest.js +0 -0
  667. /package/.next/static/{G6-gNoyGDEcgAsq-L4PE1 → yyRVBc0xxYQsh3GpbQX9b}/_ssgManifest.js +0 -0
@@ -1,60 +1,61 @@
1
1
  "use client";
2
2
 
3
- import { useCallback, useEffect, useRef, useState } from "react";
3
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
4
4
  import { useAppEvent } from "@/app/api/events/app-event.client";
5
5
  import { queryKey } from "@/app/api/query-key";
6
6
  import { toast } from "@/components/ui/toast";
7
+ import { CALL_BACK, CALL_END, CALL_IDLE, CALL_RELAY } from "@/config";
7
8
  import { TOOL_NAMES } from "@/features/ai/tools/tool-name";
8
- import {
9
- type Bot,
10
- isAppStop,
11
- TASK_CONTINUE,
12
- type Task,
13
- } from "@/features/bot/bot.schema";
14
- import { MARK_BANDS } from "@/features/bot/mark.const";
15
- import { botTasks, screenActs } from "@/features/bot/task.store";
9
+ import { acceptThreadRelaysAction } from "@/features/bot/bot.action";
10
+ import { type Bot, isAppStop, type Thread } from "@/features/bot/bot.schema";
11
+ import { botThreads, screenActs } from "@/features/bot/thread.store";
16
12
  import { runRemoteTool } from "@/features/thursday/tool-call";
17
13
  import { isCombo, useHotkey } from "@/hooks/use-hotkey";
18
14
  import { useWakeWord } from "@/hooks/use-wake-word";
19
15
  import { toDate } from "@/lib/date-like";
20
- import { unwrapResult } from "@/lib/protocol/result";
16
+ import type { LiveClose } from "@/lib/live/live.schema";
17
+ import {
18
+ type LiveActivity,
19
+ type LiveSession,
20
+ type LiveToolCall,
21
+ openLiveSession,
22
+ } from "@/lib/live/live.session";
23
+ import {
24
+ type AudioTap,
25
+ createAudioTap,
26
+ SPECTRUM_BANDS,
27
+ } from "@/lib/live/live.tap";
28
+ import { type Result, unwrapResult } from "@/lib/protocol/result";
21
29
  import {
22
30
  revalidate,
23
31
  revalidateAll,
24
32
  useServerRoute,
25
33
  } from "@/lib/protocol/use-server-route";
26
34
  import { createOutbox, type Outbox } from "@/lib/queue";
27
- import { openRealtimeSession } from "@/lib/realtime/open-session";
28
- import type {
29
- RealtimeActivity,
30
- RealtimeSession,
31
- RealtimeToolCall,
32
- } from "@/lib/realtime/realtime.session";
33
- import { type AudioTap, createAudioTap } from "@/lib/realtime/realtime.tap";
34
35
  import { errorToString } from "@/lib/utils";
36
+ import { FACE_WORD_MAX, undrawable } from "./ascii.const";
35
37
  import {
36
38
  endCallAction,
37
39
  openCallAction,
40
+ saveThoughtAction,
38
41
  saveTurnsAction,
39
42
  } from "./thursday.action";
40
43
  import type {
44
+ CallBack,
41
45
  CallMessage,
42
46
  CallStatus,
43
- CallTurn,
47
+ FaceWord,
44
48
  LiveStatus,
45
49
  } from "./thursday.schema";
46
50
  import { thursdaySettings, useThursdayStore } from "./thursday.store";
47
51
  import { toolBot, toolLine } from "./tool-line";
48
52
 
49
53
  /**
50
- * One live call, plus the task inbox the app watches even with no call open.
51
- * The server picks the provider (openCallAction) and runs the tools (tool-call);
52
- * the page itself only hangs up.
54
+ * One live call, plus the thread inbox the app watches even with no call open.
55
+ * The server opens the Live session (openCallAction) and runs the tools
56
+ * (tool-call); the page itself only hangs up and puts a word on the face.
53
57
  */
54
58
 
55
- /** Time for a tool result to reach the model before the socket closes. */
56
- const HANG_UP_DELAY_MS = 400;
57
-
58
59
  /** Plays when the line opens. */
59
60
  const CONNECTED_SOUND = "/sounds/start_voice.ogg";
60
61
 
@@ -69,6 +70,13 @@ const TOOL_LINGER_MS = 2500;
69
70
  /** A relay from a bot carries more to read than a tool line. */
70
71
  const RELAY_LINGER_MS = 5000;
71
72
 
73
+ /**
74
+ * Once the backend's turn is over, how long the activity line keeps saying it is
75
+ * working while it waits for her voice. Her first word is what normally ends it;
76
+ * this is only for a turn that never reaches one.
77
+ */
78
+ const THINKING_TAIL_MS = 6000;
79
+
72
80
  /**
73
81
  * The face lags the activity line: only a tool held longer than this switches
74
82
  * to the working face. Shorter tools show on the line only.
@@ -81,127 +89,153 @@ const FACE_TOOL_MS = 800;
81
89
  */
82
90
  const FACE_SETTLE_MS = 600;
83
91
 
84
- /** Scrollback for the side-by-side layout; the call view shows at most three. */
85
- const KEEP_MESSAGES = 24;
86
-
87
- /** Safety net only; the event stream revalidates tasks as they change. */
88
- const TASK_POLL_FALLBACK_MS = 30_000;
89
-
90
- /** Notify once about failing saves, then stay quiet for the rest of the call. */
91
- const SAVE_FAILURE_LIMIT = 3;
92
-
93
- /**
94
- * With no user speech, agent speech, or tool for this long, the model is asked
95
- * to say goodbye and call end_call.
96
- */
97
- const IDLE_HANG_UP_MS = 60_000;
98
-
99
- /** If end_call does not follow the goodbye, the page hangs up. */
100
- const IDLE_GRACE_MS = 15_000;
101
-
102
- /** The countdown shows on screen inside this window. */
103
- const IDLE_WARN_MS = 15_000;
104
-
105
92
  /**
106
- * The user finished a turn and nothing came back — no words, no tool — for this
107
- * long. The line is up and the model is not on it, so there is no goodbye to
108
- * ask for: the page hangs up. Only armed while an answer is owed, so a quiet
109
- * line still ends the slow way (IDLE_HANG_UP_MS).
93
+ * How long the face says a call failed to open or dropped before it rests: long
94
+ * enough for the orb to spell its ERROR out. The toast carries the reason.
110
95
  */
111
- const AGENT_SILENT_MS = 30_000;
96
+ const FAILED_FACE_MS = 6000;
112
97
 
113
- const IDLE_LINE =
114
- "The user has said nothing for a minute. Say a one-line goodbye, then call end_call.";
98
+ /** Scrollback for the side-by-side layout; the call view shows at most three. */
99
+ const KEEP_MESSAGES = 24;
115
100
 
116
- /**
117
- * Upper bound on the mic staying muted for a relayed line the model never
118
- * voices (the response can be rejected).
119
- */
120
- const RELAY_MIC_MS = 15_000;
101
+ /** Safety net only; the event stream revalidates threads as they change. */
102
+ const THREAD_POLL_FALLBACK_MS = 30_000;
121
103
 
122
- /** A line queued for the model. */
123
- type Said = {
124
- line: string;
125
- /** Shown on the activity line when the line is sent. */
126
- show?: ToolRun;
127
- };
104
+ /** Failed saves of one kind before the call stops saving that kind and says so, once. */
105
+ const SAVE_FAILURE_LIMIT = 3;
128
106
 
129
107
  /**
130
- * A tool the model is using, as the activity line draws it. `line` is the
131
- * human-readable sentence (tool-line), null when none exists; `name` is the
108
+ * What the activity line draws: a tool the model is using, or a relay. `line` is
109
+ * the human-readable sentence (tool-line), null when none exists; `name` is the
132
110
  * tool that actually ran.
133
111
  */
134
- export type ToolRun = {
112
+ export type ActivityLine = {
135
113
  name: string;
136
114
  line: string | null;
137
115
  done: boolean;
138
- /** A relay from a bot (answer or question) rather than a tool; `name` is the task label. */
139
- kind?: "tool" | "relay";
116
+ /** The tool call this is, so one call finishing never ends another's line. */
117
+ id?: string;
118
+ /** A relay from a bot (answer or question) rather than a tool; `name` is the thread label. */
119
+ kind?: "relay";
140
120
  /** The bot this names, when it names one: the row draws its face instead of a glyph. */
141
121
  bot?: string | null;
142
122
  };
143
123
 
124
+ /** A call-back ringing, as the screen names it. */
125
+ export type Ringing = {
126
+ /** The bot whose work rang: the one asking, else the thread's own. */
127
+ bot: string;
128
+ kind: "question" | "done" | "stopped";
129
+ label: string;
130
+ /** Other threads ringing with it. */
131
+ more: number;
132
+ };
133
+
134
+ /**
135
+ * Why a call ended without the user hanging up: the line was quiet (CALL_IDLE),
136
+ * she hung up (`end_call`), Live closed the session, or the connection dropped.
137
+ */
138
+ export type CallEnd = "quiet" | "hungUp" | "closed" | "dropped";
139
+
144
140
  export function useThursday() {
145
- const session = useRef<RealtimeSession | null>(null);
141
+ const session = useRef<LiveSession | null>(null);
146
142
  // created on the first call: `new Audio()` cannot run during SSR
147
143
  const tap = useRef<AudioTap | null>(null);
148
144
  /** Created inside the call-starting click; hang-up may come from a tool or a disconnect with no gesture. */
149
145
  const farewell = useRef<HTMLAudioElement | null>(null);
150
146
 
151
147
  const [status, setStatus] = useState<CallStatus>("idle");
148
+ /** A call just failed to open or dropped; the face says so for FAILED_FACE_MS. */
149
+ const [failed, setFailed] = useState(false);
150
+ const failedFor = useRef<ReturnType<typeof setTimeout> | null>(null);
151
+ const showFailed = useCallback((on: boolean) => {
152
+ if (failedFor.current) clearTimeout(failedFor.current);
153
+ failedFor.current = on
154
+ ? setTimeout(() => setFailed(false), FAILED_FACE_MS)
155
+ : null;
156
+ setFailed(on);
157
+ }, []);
152
158
  const [messages, setMessages] = useState<CallMessage[]>([]);
153
- const [tool, setTool] = useState<ToolRun | null>(null);
159
+ const [tool, setTool] = useState<ActivityLine | null>(null);
154
160
  /** When the line opened (ms). */
155
161
  const [since, setSince] = useState<number | null>(null);
156
- /** Seconds until idle hang-up; set only inside IDLE_WARN_MS. */
162
+ /** Seconds until idle hang-up; set only inside CALL_IDLE.warnMs. */
157
163
  const [idleLeft, setIdleLeft] = useState<number | null>(null);
164
+ /** Why the last call ended, when the user did not end it; cleared by the next call. */
165
+ const [ended, setEnded] = useState<CallEnd | null>(null);
166
+ /** When the backend picked this turn up (ms); the activity line counts from it. */
167
+ const [thinkingSince, setThinkingSince] = useState<number | null>(null);
168
+ /** The title of the backend's latest reasoning summary in this stretch of work. */
169
+ const [thinkingTitle, setThinkingTitle] = useState<string | null>(null);
170
+ /** The word `emote` last put on the face. */
171
+ const [faceWord, setFaceWord] = useState<FaceWord | null>(null);
172
+ /** The same value where callbacks can read it, and the timer that ends it. */
173
+ const thinking = useRef<number | null>(null);
174
+ const thinkTail = useRef<ReturnType<typeof setTimeout> | null>(null);
158
175
  /**
159
- * Mic is muted while a tool runs and while a relayed line is being read.
160
- * The effect below applies it to the session.
176
+ * A relayed update is out and has not been voiced yet: nothing else goes in
177
+ * until her voice has started and stopped.
161
178
  */
162
- const [micOff, setMicOff] = useState(false);
163
- /** The two mute reasons, and whether the relayed line has produced audio yet. */
164
- const mic = useRef<{
165
- tool: boolean;
166
- reading: boolean;
179
+ const reading = useRef<{
180
+ on: boolean;
167
181
  spoke: boolean;
168
182
  giveUp: ReturnType<typeof setTimeout> | null;
169
- }>({ tool: false, reading: false, spoke: false, giveUp: null });
170
- /**
171
- * Last activity time, whether the goodbye was requested, and when an answer
172
- * started being owed (null once she answers or works).
173
- */
174
- const idle = useRef<{
175
- since: number;
176
- asked: boolean;
177
- owed: number | null;
178
- grace: ReturnType<typeof setTimeout> | null;
179
- }>({ since: 0, asked: false, owed: null, grace: null });
183
+ }>({ on: false, spoke: false, giveUp: null });
184
+ /** What `session.closed` confirmed for the call being ended; recorded on its row. */
185
+ const finalized = useRef<LiveClose | null>(null);
186
+ /** When the line last had new words from the user, her voice or backend work: the idle clock. */
187
+ const stirred = useRef(0);
180
188
  const linger = useRef<ReturnType<typeof setTimeout> | null>(null);
189
+ /** Whether the line holds a relay, so only a relay is finished by her voice. */
190
+ const relayOpen = useRef(false);
181
191
  // Every server-run tool in this call starts with this signal, so one abort
182
- // reaches running browser commands. Delegated tasks are server-owned and outlive the call
192
+ // reaches running browser commands. Delegated threads are server-owned and outlive the call
183
193
  const working = useRef<AbortController | null>(null);
184
194
  /** Row the turns are saved to; a ref so long-lived callbacks see it. */
185
195
  const callId = useRef<string | null>(null);
186
196
  /**
187
- * Relays sent this call, keyed by relayKey (id + updatedAt): an answered task
188
- * can stop to ask again, so the id alone would silence its second question.
197
+ * Open work put to her while this page has been open, by item key (openWork):
198
+ * each goes in once a call. A key carries its job's last change, so a job that
199
+ * asks or ends again is new work.
200
+ */
201
+ const told = useRef(new Set<string>());
202
+ /**
203
+ * The call-back rings only for threads that changed after this: the last call's
204
+ * end, the last ring, or the page opening.
189
205
  */
190
- const relayed = useRef(new Set<string>());
191
- /** When this call was placed. An ending from before it is already in her prompt. */
192
- const placedAt = useRef(0);
206
+ const ringAfter = useRef(Date.now());
207
+ /** The threads a call-back is ringing for, until it is answered, declined or rings out. */
208
+ const [ringingFor, setRingingFor] = useState<string[]>([]);
209
+ const isRinging = ringingFor.length > 0;
193
210
  /**
194
- * Lines for the model, including ones raised before the line opens. Held
195
- * until the session exists, which is also what lets a call-back queue its
196
- * line first.
211
+ * Keys put in this call that her voice has not carried yet: rejected, cut short
212
+ * by a hang-up, or never spoken. Hanging up takes them out of `told`, so the
213
+ * next call puts them in again; what she voiced stays out.
197
214
  */
198
- const outboxRef = useRef<Outbox<Said> | null>(null);
199
- const outbox = (outboxRef.current ??= createOutbox<Said>());
215
+ const unvoiced = useRef(new Set<string>());
216
+ /** The keys of the update on the line now, carried once her voice starts and stops on it. */
217
+ const onLine = useRef<string[]>([]);
218
+ /** When her voice was last heard, for letting a goodbye finish (CALL_END). */
219
+ const voiced = useRef(0);
220
+ /** The hang-up waiting on her goodbye once the backend called end_call. */
221
+ const leaving = useRef<ReturnType<typeof setInterval> | null>(null);
222
+ /** When either side's words were last transcribed: the quiet clock for relays. */
223
+ const heard = useRef(0);
200
224
  /**
201
- * Sends what piled up while she was reading the last line, as one line. Set
202
- * for the length of a session; null between calls.
225
+ * The call-back placed this call and its first relay has not gone in yet: that one
226
+ * goes in once she has voiced the opening rather than waiting for a quiet line.
203
227
  */
204
- const resumeRelay = useRef<(() => void) | null>(null);
228
+ const rang = useRef(false);
229
+ /** Backend work or a call tool is running, as the last activity said. */
230
+ const acting = useRef(false);
231
+ /** The inbox as last fetched, read by the relay clock. */
232
+ const latest = useRef<Thread[] | undefined>(undefined);
233
+ /**
234
+ * Context she need not say (screen acts), including what arrives before the
235
+ * line opens. Held until the session exists.
236
+ */
237
+ const outboxRef = useRef<Outbox<string> | null>(null);
238
+ const outbox = (outboxRef.current ??= createOutbox<string>());
205
239
  /** Wants a call, including while connecting. */
206
240
  const calling = useRef(false);
207
241
  /**
@@ -210,10 +244,18 @@ export function useThursday() {
210
244
  * in `call` or the hang-up press is swallowed too.
211
245
  */
212
246
  const opening = useRef(false);
247
+ const ending = useRef(false);
248
+ /**
249
+ * Which attempt owns the line. A hang-up during startup moves it on, so an
250
+ * attempt still connecting cannot come up after a newer call has been placed.
251
+ */
252
+ const attempt = useRef(0);
213
253
 
214
- const showTool = useCallback((call: RealtimeToolCall) => {
254
+ const showTool = useCallback((call: LiveToolCall) => {
215
255
  if (linger.current) clearTimeout(linger.current);
256
+ relayOpen.current = false;
216
257
  setTool({
258
+ id: call.id,
217
259
  name: call.name,
218
260
  line: toolLine(call.name, call.arguments),
219
261
  bot: toolBot(call.name, call.arguments),
@@ -221,21 +263,22 @@ export function useThursday() {
221
263
  });
222
264
  }, []);
223
265
 
224
- /** A relay takes the tool line and stays long enough to read. */
225
- const showRelay = useCallback((run: ToolRun) => {
266
+ /**
267
+ * A relay takes the tool line and runs there like a tool does: while it goes in,
268
+ * and while she voices it. Her voice finishing it is what stops the motion
269
+ * (doneReading); after that it stays long enough to read.
270
+ */
271
+ const showRelay = useCallback((run: ActivityLine) => {
226
272
  if (linger.current) clearTimeout(linger.current);
227
- setTool(run);
228
- linger.current = setTimeout(
229
- () => setTool((open) => (open?.kind === "relay" ? null : open)),
230
- RELAY_LINGER_MS,
231
- );
273
+ relayOpen.current = true;
274
+ setTool({ ...run, done: false });
232
275
  }, []);
233
276
 
234
277
  /** Tool finished; lingers, then clears. */
235
- const hideTool = useCallback((name: string) => {
278
+ const hideTool = useCallback((id: string) => {
236
279
  setTool((open) =>
237
280
  // a second tool already took the line; leave it
238
- open?.name === name ? { ...open, done: true } : open,
281
+ open?.id === id ? { ...open, done: true } : open,
239
282
  );
240
283
  if (linger.current) clearTimeout(linger.current);
241
284
  linger.current = setTimeout(
@@ -244,36 +287,141 @@ export function useThursday() {
244
287
  );
245
288
  }, []);
246
289
 
247
- /** The effect below is the only place that touches the session mute. */
248
- const syncMic = useCallback(() => {
249
- setMicOff(mic.current.tool || mic.current.reading);
290
+ /** The relayed update was voiced, or waited on long enough: the next can go in. */
291
+ const doneReading = useCallback(() => {
292
+ if (reading.current.giveUp) clearTimeout(reading.current.giveUp);
293
+ // Her voice started and stopped on it: carried
294
+ if (reading.current.spoke) {
295
+ for (const key of onLine.current) unvoiced.current.delete(key);
296
+ }
297
+ onLine.current = [];
298
+ reading.current = { on: false, spoke: false, giveUp: null };
299
+ if (!relayOpen.current) return;
300
+ relayOpen.current = false;
301
+ setTool((open) =>
302
+ open?.kind === "relay" ? { ...open, done: true } : open,
303
+ );
304
+ if (linger.current) clearTimeout(linger.current);
305
+ linger.current = setTimeout(
306
+ () => setTool((open) => (open?.kind === "relay" ? null : open)),
307
+ RELAY_LINGER_MS,
308
+ );
309
+ }, []);
310
+
311
+ /** An update is on the wire: nothing else goes in until she has voiced it. */
312
+ const readAloud = useCallback(() => {
313
+ if (reading.current.giveUp) clearTimeout(reading.current.giveUp);
314
+ reading.current = {
315
+ on: true,
316
+ spoke: false,
317
+ giveUp: setTimeout(doneReading, CALL_RELAY.readMs),
318
+ };
319
+ }, [doneReading]);
320
+
321
+ const setThinking = useCallback((at: number | null) => {
322
+ thinking.current = at;
323
+ setThinkingSince(at);
324
+ // a stretch of work that starts or ends has not said what it is about yet
325
+ setThinkingTitle(null);
250
326
  }, []);
251
327
 
252
- const openMic = useCallback(() => {
253
- if (mic.current.giveUp) clearTimeout(mic.current.giveUp);
254
- mic.current.giveUp = null;
255
- mic.current.reading = false;
256
- mic.current.spoke = false;
257
- syncMic();
258
- }, [syncMic]);
328
+ /**
329
+ * What the activity line says about the backend. It starts when the backend takes the
330
+ * turn and ends on her first word, not on the response: a turn that calls two
331
+ * tools settles for a moment between them, and the wait before she speaks is
332
+ * still the same stretch of work. Both would otherwise read as the call stalling.
333
+ */
334
+ const holdThinking = useCallback(
335
+ (busy: boolean, speaking: boolean) => {
336
+ const dropTail = () => {
337
+ if (thinkTail.current) clearTimeout(thinkTail.current);
338
+ thinkTail.current = null;
339
+ };
340
+ // Her voice always wins: it is the answer the work was for
341
+ if (speaking) {
342
+ dropTail();
343
+ if (thinking.current !== null) setThinking(null);
344
+ return;
345
+ }
346
+ if (busy) {
347
+ dropTail();
348
+ if (thinking.current === null) setThinking(Date.now());
349
+ return;
350
+ }
351
+ if (thinking.current !== null && !thinkTail.current) {
352
+ thinkTail.current = setTimeout(() => {
353
+ thinkTail.current = null;
354
+ setThinking(null);
355
+ }, THINKING_TAIL_MS);
356
+ }
357
+ },
358
+ [setThinking],
359
+ );
259
360
 
260
- /** A relayed line is on the wire; the mic stays muted until it is read. */
261
- const readAloud = useCallback(() => {
262
- if (mic.current.giveUp) clearTimeout(mic.current.giveUp);
263
- mic.current.reading = true;
264
- mic.current.spoke = false;
265
- mic.current.giveUp = setTimeout(() => {
266
- openMic();
267
- // a line she never voiced must not strand the ones queued behind it
268
- resumeRelay.current?.();
269
- }, RELAY_MIC_MS);
270
- syncMic();
271
- }, [openMic, syncMic]);
272
-
273
- // muting at the track level also clears the browser's recording indicator (realtime.audio setMuted)
274
- useEffect(() => {
275
- session.current?.mute(micOff);
276
- }, [micOff]);
361
+ /**
362
+ * Puts to her what background work still waits on the user, once neither
363
+ * side's words have been transcribed for CALL_RELAY.quietMs and nothing is
364
+ * running: questions not answered, jobs ended or stopped and not yet seen,
365
+ * and progress from jobs still running. Live never speaks unprompted, so
366
+ * nothing reaches the user unless this puts it in. Handling an item — an
367
+ * answer, `thread` `seen`, a click on screen — takes it off the list. Each
368
+ * item goes in once a call, and once her voice has carried it, not on later
369
+ * calls either (`told`, `unvoiced`). One append holds one kind, so an ending
370
+ * is never lost among questions. Nothing goes in once the call is ending.
371
+ */
372
+ const relayOpenWork = useCallback(() => {
373
+ const live = session.current;
374
+ const threads = latest.current;
375
+ if (!live || !threads || reading.current.on || acting.current) return;
376
+ if (leaving.current) return;
377
+ // On a call-back, why she called goes in once the opening is voiced: it is the
378
+ // first thing they ask, and a quiet line would come too late
379
+ const rung = rang.current;
380
+ rang.current = false;
381
+ if (!rung && Date.now() - heard.current < CALL_RELAY.quietMs) return;
382
+ const open = openWork(threads).filter(
383
+ (item) => !told.current.has(item.key),
384
+ );
385
+ const first = open[0];
386
+ if (!first) return;
387
+ const due = open
388
+ .filter((item) => item.kind === first.kind)
389
+ .slice(0, CALL_RELAY.perTurn);
390
+ const lines = due.map((item) => item.line);
391
+
392
+ // Told as it goes out, even if Live refuses it: a refusal is tried again on the next
393
+ // call (unvoiced), never in a loop on this one
394
+ for (const item of due) {
395
+ told.current.add(item.key);
396
+ unvoiced.current.add(item.key);
397
+ }
398
+ readAloud();
399
+ onLine.current = due.map((item) => item.key);
400
+ // On the line before it goes out, so it runs there for the whole wait
401
+ showRelay((due.at(-1) ?? first).show);
402
+ void live
403
+ .append(
404
+ "commentary",
405
+ lines.length === 1
406
+ ? lines.join("")
407
+ : `[${lines.length} updates.]\n\n${lines.join("\n\n")}`,
408
+ )
409
+ .then((delivered) => {
410
+ if (!delivered) {
411
+ doneReading();
412
+ return;
413
+ }
414
+ const ids = due.flatMap((item) => item.relayIds);
415
+ if (ids.length) return acceptThreadRelaysAction(ids).then(unwrapResult);
416
+ })
417
+ .catch((cause) =>
418
+ toast.add({
419
+ type: "error",
420
+ title: "Could not record relay delivery",
421
+ description: errorToString(cause),
422
+ }),
423
+ );
424
+ }, [readAloud, doneReading, showRelay]);
277
425
 
278
426
  /**
279
427
  * One timer; when it fires it wears the last wanted face, so a tool shorter
@@ -335,24 +483,22 @@ export function useThursday() {
335
483
  [restFace, wearFace],
336
484
  );
337
485
 
338
- const onCall = status !== "idle" && status !== "connecting";
486
+ const onCall =
487
+ status !== "idle" && status !== "connecting" && status !== "ending";
339
488
 
340
489
  // Inbox, with or without a call. Revalidated on server events; polling is the safety net
341
- const { data: tasks } = useServerRoute<Task[]>(queryKey.tasks, {
342
- refreshInterval: TASK_POLL_FALLBACK_MS,
490
+ const { data: threads } = useServerRoute<Thread[]>(queryKey.threads, {
491
+ refreshInterval: THREAD_POLL_FALLBACK_MS,
343
492
  });
344
493
 
345
494
  // Server event stream (app/api/events): signals revalidate their key
346
- const boot = useRef<string | null>(null);
347
495
  useAppEvent({
348
- hello: (event) => {
496
+ hello: () => {
349
497
  // Sent on every connect, so this is also every reconnect: nothing that
350
- // changed while the stream was down raised a signal anybody heard, and a
351
- // changed `boot` means the server restarted on top of that
352
- boot.current = event.boot;
498
+ // changed while the stream was down raised a signal anybody heard
353
499
  void revalidateAll();
354
500
  },
355
- tasks: () => void revalidate(queryKey.tasks),
501
+ threads: () => void revalidate(queryKey.threads),
356
502
  memory: () => void revalidate(queryKey.memory),
357
503
  mcp: () => {
358
504
  void revalidate(queryKey.mcp);
@@ -368,97 +514,93 @@ export function useThursday() {
368
514
  // faces come from the bot list
369
515
  const { data: bots } = useServerRoute<Bot[]>(queryKey.bot);
370
516
  useEffect(() => {
371
- if (tasks) botTasks.sync(tasks, bots);
372
- }, [tasks, bots]);
373
-
374
- // A question is relayed on every call, because it still needs an answer. An
375
- // ending is relayed only on the call it happened during: one from before is
376
- // already in her prompt, on the `delegate` line that opened it. Telling is not
377
- // seeing — the badge clears when the user opens the job. Sending while the
378
- // line is still opening is fine: the outbox holds the line. `status` is in
379
- // deps because the guard is a ref; without it a list that arrived before the
380
- // line opened would wait for the next tasks change
517
+ if (threads) botThreads.sync(threads, bots);
518
+ }, [threads, bots]);
519
+
381
520
  useEffect(() => {
382
- if (!tasks || !calling.current) return;
383
- for (const task of tasks) {
384
- if (task.status === "running") continue;
385
- // A stop the app picks back up by itself is not news: it runs again in a moment
386
- if (task.ask?.auto) continue;
387
- if (
388
- task.status !== "waiting" &&
389
- toDate(task.updatedAt).getTime() < placedAt.current
390
- )
391
- continue;
392
- const key = relayKey(task);
393
- if (relayed.current.has(key)) continue;
394
- relayed.current.add(key);
395
- outbox.send({ line: relayLine(task), show: relayShow(task) });
396
- }
397
- }, [tasks, outbox, status]);
521
+ latest.current = threads;
522
+ }, [threads]);
398
523
 
399
- // The user acted on a task on screen; the model may have just read that question and must not ask again
524
+ // The user acted on a thread on screen; she may have just read that question and must not ask again
400
525
  useEffect(
401
526
  () =>
402
527
  screenActs.subscribe((act) => {
403
528
  if (!calling.current) return;
404
- outbox.send({
405
- line:
406
- act.kind === "answered"
407
- ? `The user answered task "${act.label}" on screen: ${act.answer}. The bot is going on with that — do not ask again.`
408
- : `The user stopped task "${act.label}" on screen. It is not running any more — do not wait for it or say anything more about it.`,
409
- });
529
+ outbox.send(
530
+ act.kind === "answered"
531
+ ? `The user sent a message on screen to ${act.recipient ?? "the coordinator"} in thread "${act.label}" (${act.id})${act.replyTo ? `, replying to ${act.replyTo}` : ""}: ${act.answer}. It has reached that participant.`
532
+ : `The user stopped thread "${act.label}" on screen. It is no longer running.`,
533
+ );
410
534
  }),
411
535
  [outbox],
412
536
  );
413
537
 
414
- /** Closes the session and resets state. Turns were saved during the call. */
415
- const hangUp = useCallback(async () => {
416
- const live = session.current;
417
- const call = callId.current;
418
-
419
- session.current = null;
420
- callId.current = null;
421
- calling.current = false;
422
- opening.current = false;
423
- relayed.current.clear();
424
- // unsent lines go: a question is relayed again next call, an ending is in her prompt
425
- outbox.close();
426
- outbox.clear();
427
- // holds the closed session; the next call sets its own
428
- resumeRelay.current = null;
429
- working.current?.abort();
430
- working.current = null;
431
- mic.current.tool = false;
432
- openMic();
433
- if (linger.current) clearTimeout(linger.current);
434
- // a pending face timer must not fire after the call
435
- restFace();
436
- if (idle.current.grace) clearTimeout(idle.current.grace);
437
- idle.current = { since: 0, asked: false, owed: null, grace: null };
438
- setIdleLeft(null);
439
- setSince(null);
440
- setStatus("idle");
441
- setMessages([]);
442
- setTool(null);
443
- // the room is not cleared: tasks outlive the call
444
-
445
- // closing finalizes a mid-speech turn, so its save is queued first
446
- live?.close();
447
- if (call) void endCallAction(call);
448
- // only when there was a line to close
449
- if (live && farewell.current) {
450
- farewell.current.currentTime = 0;
451
- void farewell.current.play().catch(() => {});
452
- }
453
- }, [outbox, restFace, openMic]);
538
+ /** Closes the session and resets state. Turns were saved during the call. `why` is null when the user hung up. */
539
+ const hangUp = useCallback(
540
+ async (why: CallEnd | null = null) => {
541
+ if (ending.current) return;
542
+ ending.current = true;
543
+ if (leaving.current) clearInterval(leaving.current);
544
+ leaving.current = null;
545
+ attempt.current += 1;
546
+ const live = session.current;
547
+ const call = callId.current;
548
+
549
+ session.current = null;
550
+ callId.current = null;
551
+ calling.current = false;
552
+ opening.current = false;
553
+ rang.current = false;
554
+ // What she did not voice goes in again next call; unsent context goes with the session
555
+ for (const key of unvoiced.current) told.current.delete(key);
556
+ unvoiced.current.clear();
557
+ outbox.close();
558
+ outbox.clear();
559
+ working.current?.abort();
560
+ working.current = null;
561
+ doneReading();
562
+ if (linger.current) clearTimeout(linger.current);
563
+ // a pending face timer must not fire after the call
564
+ restFace();
565
+ // What came up on this call was this call's to tell; the call-back rings only for what comes after
566
+ ringAfter.current = Date.now();
567
+ setIdleLeft(null);
568
+ setSince(null);
569
+ setStatus(live ? "ending" : "idle");
570
+ setMessages([]);
571
+ setTool(null);
572
+ setThinking(null);
573
+ if (thinkTail.current) clearTimeout(thinkTail.current);
574
+ thinkTail.current = null;
575
+ relayOpen.current = false;
576
+ // the room is not cleared: threads outlive the call
577
+
578
+ // Waits (bounded) for session.closed, which saves the last turns and says
579
+ // what was billed; the row is ended with that, after the saves it queued
580
+ await live?.close();
581
+ const close = finalized.current;
582
+ finalized.current = null;
583
+ ending.current = false;
584
+ setStatus("idle");
585
+ if (live) setEnded(why);
586
+ if (call) void endCallAction(call, close);
587
+ // only when there was a line to close
588
+ if (live && farewell.current) {
589
+ farewell.current.currentTime = 0;
590
+ void farewell.current.play().catch(() => {});
591
+ }
592
+ },
593
+ [outbox, restFace, doneReading, setThinking],
594
+ );
454
595
 
455
596
  // Unmount during a call must release the mic and stop tools
456
597
  useEffect(() => {
457
598
  return () => {
458
- session.current?.close();
599
+ attempt.current += 1;
600
+ void session.current?.close();
459
601
  session.current = null;
460
602
  // Closing only the session would leave the row live (thursday.query
461
- // isAnyCallLive) and finished tasks unannounced (bot.runner); end the row too
603
+ // isAnyCallLive) and finished threads unannounced (bot.runner); end the row too
462
604
  const call = callId.current;
463
605
  callId.current = null;
464
606
  calling.current = false;
@@ -469,217 +611,278 @@ export function useThursday() {
469
611
  // clear timers so nothing sets state on an unmounted tree
470
612
  if (linger.current) clearTimeout(linger.current);
471
613
  if (faceIn.current) clearTimeout(faceIn.current);
472
- if (mic.current.giveUp) clearTimeout(mic.current.giveUp);
473
- if (idle.current.grace) clearTimeout(idle.current.grace);
614
+ if (reading.current.giveUp) clearTimeout(reading.current.giveUp);
615
+ if (thinkTail.current) clearTimeout(thinkTail.current);
616
+ if (failedFor.current) clearTimeout(failedFor.current);
617
+ if (leaving.current) clearInterval(leaving.current);
474
618
  };
475
619
  }, []);
476
620
 
477
621
  /**
478
- * Rewinds the idle clock. User activity also cancels a requested goodbye;
479
- * agent activity does not, so the goodbye cannot cancel itself.
622
+ * The backend ended the call: hang up once her goodbye is over. She says it while
623
+ * the backend works, so it may be done already, still going, or yet to start;
624
+ * CALL_END bounds each case. The tool's own result always gets `quietMs` to go out.
480
625
  */
481
- const stir = useCallback((who: "user" | "agent") => {
482
- const now = Date.now();
483
- idle.current.since = now;
484
- if (who !== "user") {
485
- // Words or a tool: she is on the line, so nothing is owed.
486
- idle.current.owed = null;
487
- return;
488
- }
489
- // Rewound by anything the user does, transcript deltas included, so a long
490
- // turn is never mistaken for a line that stopped answering; it runs from the
491
- // moment they stopped rather than from the moment they started.
492
- idle.current.owed = now;
493
- if (idle.current.grace) clearTimeout(idle.current.grace);
494
- idle.current.grace = null;
495
- idle.current.asked = false;
496
- }, []);
497
-
498
- // Idle clock: shows the countdown, then asks for a goodbye and grants a grace period
626
+ const leave = useCallback(() => {
627
+ if (leaving.current) return;
628
+ const asked = Date.now();
629
+ leaving.current = setInterval(() => {
630
+ const now = Date.now();
631
+ // A goodbye heard just before the backend ended the call counts as said
632
+ const said = voiced.current > asked - CALL_END.unsaidMs;
633
+ const over = said
634
+ ? now - Math.max(voiced.current, asked) >= CALL_END.quietMs
635
+ : now - asked >= CALL_END.unsaidMs;
636
+ if (over || now - asked >= CALL_END.maxMs) void hangUp("hungUp");
637
+ }, 100);
638
+ }, [hangUp]);
639
+
640
+ // Idle clock: shows the countdown, then hangs up. The page ends a quiet line itself;
641
+ // a goodbye would leave the ending to the model
499
642
  useEffect(() => {
500
643
  if (!onCall) return;
501
- stir("agent");
644
+ stirred.current = Date.now();
502
645
  const tick = setInterval(() => {
503
- const live = session.current;
504
- if (!live) return;
505
- const owed = idle.current.owed;
506
- if (owed !== null && Date.now() - owed >= AGENT_SILENT_MS) {
507
- // Nothing to say goodbye with; the model is what would have said it.
508
- toast.add({
509
- type: "error",
510
- title: "Call ended",
511
- description: "The model stopped answering.",
512
- });
513
- void hangUp();
514
- return;
515
- }
516
- const left = IDLE_HANG_UP_MS - (Date.now() - idle.current.since);
646
+ if (!session.current) return;
647
+ const left = CALL_IDLE.hangUpMs - (Date.now() - stirred.current);
517
648
  setIdleLeft(
518
- left <= IDLE_WARN_MS && !idle.current.asked
519
- ? Math.max(0, Math.ceil(left / 1000))
520
- : null,
649
+ left <= CALL_IDLE.warnMs ? Math.max(0, Math.ceil(left / 1000)) : null,
521
650
  );
522
- if (left > 0 || idle.current.asked) return;
523
-
524
- idle.current.asked = true;
525
- live.say(IDLE_LINE);
526
- idle.current.grace = setTimeout(() => void hangUp(), IDLE_GRACE_MS);
651
+ if (left <= 0) void hangUp("quiet");
527
652
  }, 1000);
528
653
  return () => clearInterval(tick);
529
- }, [onCall, stir, hangUp]);
654
+ }, [onCall, hangUp]);
655
+
656
+ // The relay clock: open work goes in only while the line is quiet
657
+ useEffect(() => {
658
+ if (!onCall) return;
659
+ const tick = setInterval(relayOpenWork, 1000);
660
+ return () => clearInterval(tick);
661
+ }, [onCall, relayOpenWork]);
530
662
 
531
663
  const call = useCallback(async () => {
664
+ // Every way in answers a ringing call-back: the face, the wake word, the hotkey
665
+ const calledBack = isRinging;
532
666
  // Guard with a ref, not `status`: three entry points (face, wake word,
533
667
  // hotkey) can fire in one frame and both see a stale "idle", opening two sessions
534
- if (opening.current) return;
668
+ if (opening.current || ending.current) return;
535
669
  // with a line open this press hangs up; `calling` covers the gap before re-render
536
670
  if (calling.current || status !== "idle") return hangUp();
671
+ setRingingFor([]);
537
672
 
538
673
  setStatus("connecting");
539
- // from here on this is a call; the outbox holds lines until the session exists
674
+ setEnded(null);
675
+ showFailed(false);
676
+ // from here on this is a call; the outbox holds updates until the session exists
540
677
  opening.current = true;
541
678
  calling.current = true;
542
- placedAt.current = Date.now();
679
+ finalized.current = null;
543
680
  outbox.clear();
681
+ attempt.current += 1;
682
+ const mine = attempt.current;
683
+ const current = () => calling.current && attempt.current === mine;
544
684
  try {
545
- const handshake = unwrapResult(await openCallAction(thursdaySettings()));
546
- callId.current = handshake.callId;
547
-
548
- const stop = new AbortController();
549
- working.current = stop;
550
- const saving = { failures: 0 };
551
-
685
+ // Inside the gesture, before anything awaits: an AudioContext created later
686
+ // starts suspended. Calls from the wake word or a call-back have no gesture;
687
+ // armAudioUnlock handles those
552
688
  tap.current ??= createAudioTap();
553
- // Inside the gesture: an AudioContext created elsewhere starts suspended.
554
- // Calls from the wake word or a call-back have no gesture; armAudioUnlock handles those
555
689
  armAudioUnlock(tap.current.open().context);
556
690
  const chime = new Audio(CONNECTED_SOUND);
557
691
  farewell.current ??= new Audio(HUNG_UP_SOUND);
558
692
 
559
- // one turn per seq; the session reports turns one at a time
560
- const turns: CallMessage[] = [];
561
-
562
- const live = await openRealtimeSession({
563
- provider: handshake.provider,
564
- credential: handshake.credential,
565
- setup: handshake.session,
693
+ const settings = thursdaySettings();
694
+ const stop = new AbortController();
695
+ working.current = stop;
696
+ const saving = { failures: 0 };
697
+ const thinkingSaves = { failures: 0 };
698
+ /** Filled in by the handshake, read by callbacks that only run after it. */
699
+ const line = { callId: "", opening: null as string | null };
700
+
701
+ // one turn per id; the session reports display groups one at a time
702
+ const turns = new Map<string, CallMessage & { seq: number }>();
703
+
704
+ const live = await openLiveSession({
705
+ initialize: async (sdp) => {
706
+ const handshake = unwrapResult(
707
+ await openCallAction(settings, sdp, calledBack),
708
+ );
709
+ if (!current()) {
710
+ void endCallAction(handshake.callId);
711
+ throw new Error("The call closed during startup.");
712
+ }
713
+ callId.current = handshake.callId;
714
+ line.callId = handshake.callId;
715
+ line.opening = handshake.opening;
716
+ return handshake.sdp;
717
+ },
566
718
  audio: tap.current,
567
719
  on: {
568
- // the session calls tools; the page forwards them to the server
720
+ // the backend calls tools; the page forwards them to the server
569
721
  runTool: async (call) => {
570
- // end_call is the page's only tool. Answer without awaiting: the socket
571
- // goes down with the call, so this reply must go first
722
+ // end_call and emote are the page's own tools. The line goes down once
723
+ // her goodbye is over (leave), so this reply reaches the model first
572
724
  if (call.name === TOOL_NAMES.end_call) {
573
- setTimeout(() => void hangUp(), HANG_UP_DELAY_MS);
725
+ leave();
574
726
  return "Ending the call.";
575
727
  }
728
+ // the face draws the word; nothing runs anywhere else
729
+ if (call.name === TOOL_NAMES.emote) {
730
+ const { word, reply } = readFaceWord(call.arguments);
731
+ if (word) setFaceWord({ text: word, at: Date.now() });
732
+ return reply;
733
+ }
576
734
  showTool(call);
577
735
  try {
578
- return await runRemoteTool(handshake.callId, call, stop.signal);
736
+ return await runRemoteTool(line.callId, call, stop.signal);
579
737
  } finally {
580
- hideTool(call.name);
738
+ hideTool(call.id);
581
739
  }
582
740
  },
741
+ reasoning: (part) => {
742
+ // A summary part opens with its title in bold; the activity line says what the work is about
743
+ const title = /^\s*\*\*(.+?)\*\*/.exec(part.text)?.[1]?.trim();
744
+ if (title) setThinkingTitle(title);
745
+ persist(
746
+ thinkingSaves,
747
+ () =>
748
+ saveThoughtAction(line.callId, {
749
+ ...part,
750
+ seq: Math.max(0, Math.round(part.seq)),
751
+ }),
752
+ "The backend's thinking is not being saved",
753
+ );
754
+ },
583
755
  turn: (turn) => {
584
- stir(turn.role === "user" ? "user" : "agent");
585
- // tool turns are saved but not shown
586
- if (turn.role !== "tool") {
587
- turns[turn.seq] = {
756
+ // New words, not a checkpoint of the same ones. A cough or a sigh is not words:
757
+ // it neither holds the line open nor holds an update back
758
+ const words = turn.role === "tool" ? "" : spokenWords(turn.text);
759
+ const fresh =
760
+ Boolean(words) &&
761
+ words !== spokenWords(turns.get(turn.id)?.text ?? "");
762
+ // New words from either side restart the relay's quiet clock
763
+ if (fresh) heard.current = Date.now();
764
+ // Her voice and backend work rewind the idle clock from activity, where an
765
+ // update she reads out is told apart; transcripts lag the audio and cannot
766
+ if (fresh && turn.role === "user") stirred.current = Date.now();
767
+ // Tool turns are saved but not shown. Hanging up clears the screen
768
+ // before `close()` checkpoints open groups, so only the live call draws.
769
+ if (turn.role !== "tool" && callId.current === line.callId) {
770
+ turns.set(turn.id, {
771
+ seq: turn.seq,
588
772
  id: turn.id,
589
773
  role: turn.role,
590
774
  text: turn.text,
591
- };
592
- // holes are turns whose text has not arrived
593
- setMessages(turns.filter(Boolean).slice(-KEEP_MESSAGES));
775
+ });
776
+ setMessages(
777
+ [...turns.values()]
778
+ .sort((a, b) => a.seq - b.seq)
779
+ .slice(-KEEP_MESSAGES),
780
+ );
594
781
  }
595
782
  if (turn.done) {
596
- const { id, role, tool, text, seq } = turn;
597
- persistTurn(
598
- handshake.callId,
599
- { id, role, tool, text, seq },
600
- saving,
783
+ persist(saving, () =>
784
+ saveTurnsAction(line.callId, [
785
+ {
786
+ id: turn.id,
787
+ role: turn.role,
788
+ tool: turn.tool,
789
+ text: turn.text,
790
+ // Live orders by audio milliseconds; the row keeps a whole number
791
+ seq: Math.max(0, Math.round(turn.seq)),
792
+ fragments: turn.fragments ?? null,
793
+ },
794
+ ]),
601
795
  );
602
796
  }
603
797
  },
604
798
  // session facts drive the face through showFace; the tool line is drawn by the tool itself
605
799
  activity: (activity) => {
606
800
  showFace(statusOf(activity));
607
- if (activity.hearing) stir("user");
608
- else if (activity.speaking || activity.tools.length) stir("agent");
609
- // Tool mute is the live fact. Relay mute ends once playback has started and
610
- // stopped: turn text is finalized before audio (realtime.session), so playback is what counts
611
- mic.current.tool = activity.tools.length > 0;
612
- if (mic.current.reading) {
613
- if (activity.speaking) mic.current.spoke = true;
614
- else if (mic.current.spoke) {
615
- mic.current.reading = false;
616
- // she has finished reading; whatever landed meanwhile goes now
617
- resumeRelay.current?.();
618
- }
801
+ const busy = activity.working || activity.tools.length > 0;
802
+ acting.current = busy;
803
+ // From the moment the backend picks the turn up until her first word
804
+ holdThinking(busy, activity.speaking);
805
+ if (activity.speaking) voiced.current = Date.now();
806
+ // Sound on the mic alone does not rewind the clock: a noisy room would
807
+ // keep a call open forever. Her words and backend work do, except an
808
+ // update she voices on her own: waiting results must not hold a call open.
809
+ if (
810
+ (activity.speaking && !reading.current.on) ||
811
+ activity.working ||
812
+ activity.tools.length
813
+ )
814
+ stirred.current = Date.now();
815
+ // An update counts as voiced once her voice has started and stopped
816
+ if (reading.current.on) {
817
+ if (activity.speaking) reading.current.spoke = true;
818
+ else if (reading.current.spoke) doneReading();
619
819
  }
620
- if (!mic.current.reading) openMic();
621
- else syncMic();
820
+ },
821
+ finalized: (close) => {
822
+ finalized.current = close;
622
823
  },
623
824
  warn: (description) =>
624
- toast.add({ type: "warning", title: "Call warning", description }),
825
+ toast.add({
826
+ type: "warning",
827
+ title: "Call warning",
828
+ description,
829
+ }),
625
830
  failed: (description) => {
626
831
  toast.add({ type: "error", title: "Call failed", description });
627
- void hangUp();
832
+ showFailed(true);
833
+ // Live says session.closed (finalized) when it closes the call itself
834
+ void hangUp(finalized.current ? "closed" : "dropped");
628
835
  },
629
836
  },
630
837
  });
631
838
 
632
- /**
633
- * One line at a time. Three jobs landing together used to be three
634
- * system items and three turns in a row, which the user hears
635
- * as her talking to herself; the box stays closed while she reads one,
636
- * and whatever arrived meanwhile goes out merged, as one thing to say.
637
- */
638
- const flush = (queued: Said[]) => {
639
- if (!queued.length) return;
640
- // not the user's turn while the model reads this
641
- readAloud();
642
- live.say(queued.length === 1 ? queued[0].line : mergedRelay(queued));
643
- const last = queued.at(-1)?.show;
644
- if (last) showRelay(last);
645
- outbox.close();
646
- };
839
+ if (!current()) {
840
+ // Hung up while the line was going up; hangUp already ended the row
841
+ await live.close();
842
+ return;
843
+ }
647
844
 
648
- resumeRelay.current = () => {
649
- const held = outbox.clear();
650
- outbox.open((one) => flush([one]));
651
- flush(held);
845
+ /** An opening that never landed leaves nothing to wait for. */
846
+ const unless = (delivered: boolean) => {
847
+ if (!delivered) doneReading();
652
848
  };
653
849
 
654
- // line open: flush held lines first
655
- outbox.open((one) => flush([one]));
656
-
657
- // An opening — a first call, or memory to tidy — makes the model speak first, on the same path as a relay
658
- if (handshake.opening) {
850
+ session.current = live;
851
+ opening.current = false;
852
+ rang.current = calledBack;
853
+ // Context is not gated: held lines first, then each as it comes
854
+ outbox.open((text) => void live.append("thinking", text));
855
+ // The quiet clock starts with the line, so nothing is put to her the moment it opens
856
+ heard.current = Date.now();
857
+
858
+ if (line.opening) {
859
+ // The greeting goes first; open work waits until she has said it
659
860
  readAloud();
660
- live.say(handshake.opening);
861
+ void live.append("instructions", line.opening).then(unless);
661
862
  }
662
863
 
663
- session.current = live;
664
- opening.current = false;
665
864
  wearFace("listening");
666
865
  setSince(Date.now());
667
866
  // a rejected chime is not worth a message
668
867
  void chime.play().catch(() => {});
669
868
  } catch (cause) {
670
- toast.add({
671
- type: "error",
672
- title: "Could not start the call",
673
- description: errorToString(cause),
674
- });
869
+ // Hanging up while the line was going up is not a failure to report
870
+ if (current()) {
871
+ toast.add({
872
+ type: "error",
873
+ title: "Could not start the call",
874
+ description: errorToString(cause),
875
+ });
876
+ showFailed(true);
877
+ }
878
+ if (attempt.current !== mine) return;
675
879
  // a call that never opened still has a row; close it
676
880
  if (callId.current) void endCallAction(callId.current);
677
881
  callId.current = null;
678
882
  opening.current = false;
679
883
  calling.current = false;
884
+ working.current = null;
680
885
  outbox.clear();
681
- // nothing was delivered; forget so the next call resends
682
- relayed.current.clear();
683
886
  setStatus("idle");
684
887
  }
685
888
  }, [
@@ -687,51 +890,87 @@ export function useThursday() {
687
890
  hangUp,
688
891
  showTool,
689
892
  hideTool,
690
- showRelay,
691
893
  showFace,
692
894
  wearFace,
693
- stir,
694
895
  outbox,
695
896
  readAloud,
696
- openMic,
697
- syncMic,
897
+ doneReading,
898
+ holdThinking,
899
+ showFailed,
900
+ leave,
901
+ isRinging,
698
902
  ]);
699
903
 
700
904
  /**
701
- * Call-back: opens a call when a task is waiting on the user and no line is
702
- * open. Tried once per task (`woke`), or a failed open would redial forever;
703
- * tasks present at app start count as already tried.
905
+ * Call-back: rings when a thread is waiting on the user and no line is open,
906
+ * only for threads that changed after `ringAfter`. What came up during a call
907
+ * was that call's to tell, and the next call tells what it left unsaid. Ringing
908
+ * never opens the line; answering does (`call`). A thread handled meanwhile, on
909
+ * screen or by the setting going off, stops ringing for itself.
704
910
  */
705
911
  const callBack = useThursdayStore((state) => state.callBack);
706
- const woke = useRef<Set<string> | null>(null);
707
912
  useEffect(() => {
708
- if (!tasks) return;
709
- const wants = tasks.filter(
710
- (task) =>
711
- !task.seen &&
712
- !task.ask?.auto &&
713
- (asksSomething(task) ||
714
- (callBack === "any" && task.status !== "running")),
913
+ if (!threads) return;
914
+ const wanted = new Set(
915
+ threads
916
+ .filter((thread) => ringsFor(thread, callBack))
917
+ .map((thread) => thread.id),
715
918
  );
919
+ setRingingFor((ids) =>
920
+ ids.every((id) => wanted.has(id))
921
+ ? ids
922
+ : ids.filter((id) => wanted.has(id)),
923
+ );
924
+ if (status !== "idle") return;
925
+ const after = ringAfter.current;
926
+ const fresh = threads.filter(
927
+ (thread) =>
928
+ wanted.has(thread.id) && toDate(thread.updatedAt).getTime() > after,
929
+ );
930
+ if (!fresh.length) return;
716
931
 
717
- if (!woke.current) {
718
- woke.current = new Set(wants.map((task) => task.id));
719
- return;
720
- }
721
-
722
- // forget tasks that stopped waiting; a later stop counts as fresh
723
- const open = new Set(wants.map((task) => task.id));
724
- for (const id of woke.current) {
725
- if (!open.has(id)) woke.current.delete(id);
726
- }
932
+ ringAfter.current = Date.now();
933
+ setRingingFor((ids) => [
934
+ ...ids,
935
+ ...fresh.map((thread) => thread.id).filter((id) => !ids.includes(id)),
936
+ ]);
937
+ }, [threads, callBack, status]);
727
938
 
728
- if (callBack === "off" || status !== "idle") return;
729
- const fresh = wants.filter((task) => !woke.current?.has(task.id));
730
- if (!fresh.length) return;
939
+ // While ringing: it stops by itself after CALL_BACK.ringMs, and Esc declines.
940
+ // A thread added to a ring already going does not restart the clock
941
+ useEffect(() => {
942
+ if (!isRinging) return;
943
+ const out = setTimeout(() => setRingingFor([]), CALL_BACK.ringMs);
944
+ const onKeyDown = (event: KeyboardEvent) => {
945
+ if (event.key === "Escape" && !event.defaultPrevented) setRingingFor([]);
946
+ };
947
+ window.addEventListener("keydown", onKeyDown);
948
+ return () => {
949
+ clearTimeout(out);
950
+ window.removeEventListener("keydown", onKeyDown);
951
+ };
952
+ }, [isRinging]);
731
953
 
732
- for (const task of wants) woke.current.add(task.id);
733
- void call();
734
- }, [tasks, callBack, status, call]);
954
+ const decline = useCallback(() => setRingingFor([]), []);
955
+ /** What the ringing screen names: the first thread that rang, and how many rang with it. */
956
+ const ringing = useMemo((): Ringing | null => {
957
+ const rung = ringingFor.flatMap(
958
+ (id) => threads?.find((thread) => thread.id === id) ?? [],
959
+ );
960
+ const first = rung[0];
961
+ if (!first) return null;
962
+ const question = first.room.questions[0];
963
+ return {
964
+ bot: question?.bot ?? first.bot,
965
+ kind: question
966
+ ? "question"
967
+ : first.status === "done"
968
+ ? "done"
969
+ : "stopped",
970
+ label: first.label,
971
+ more: rung.length - 1,
972
+ };
973
+ }, [ringingFor, threads]);
735
974
 
736
975
  /** Read by the face once per animation frame, outside React state. */
737
976
  const getSpectrum = useCallback(() => tap.current?.read() ?? EMPTY_BANDS, []);
@@ -748,12 +987,15 @@ export function useThursday() {
748
987
  enabled: status === "idle" && wake.enabled && !wakeBlocked,
749
988
  phrases: [wake.phrase],
750
989
  onWake: () => void call(),
751
- onError: () => setWakeBlocked(true),
990
+ onError: (reason) => {
991
+ setWakeBlocked(true);
992
+ toast.add({ type: "error", title: "Wake word off", description: reason });
993
+ },
752
994
  });
753
995
 
754
996
  // Hotkey presses `call` like the face tap, so it hangs up during a call; disabled while the line goes up or down
755
997
  const hotkey = useThursdayStore((state) => state.hotkey);
756
- const busy = status === "connecting";
998
+ const busy = status === "connecting" || status === "ending";
757
999
  useHotkey({
758
1000
  enabled: hotkey.enabled && !busy,
759
1001
  combo: hotkey.enabled && isCombo(hotkey.combo) ? hotkey.combo : null,
@@ -762,15 +1004,27 @@ export function useThursday() {
762
1004
 
763
1005
  return {
764
1006
  status,
1007
+ /** A call just failed to open or dropped; true for a few seconds after. */
1008
+ failed,
765
1009
  messages,
766
1010
  tool,
1011
+ /** Why the last call ended, when the user did not end it; null during a call. */
1012
+ ended,
1013
+ /** When the backend picked the turn up (ms); null when it is not working. */
1014
+ thinkingSince,
1015
+ /** What the backend's latest reasoning summary says it is doing; null before one arrives. */
1016
+ thinkingTitle,
1017
+ /** The word `emote` last put on the face; null before one. */
1018
+ faceWord,
767
1019
  /** Seconds until idle hang-up; null outside the warning window. */
768
1020
  idleLeft,
769
- /** Mic muted: a tool is running or a relay is being read. */
770
- micOff,
771
1021
  /** When the line opened (ms); null without a call. */
772
1022
  since,
773
1023
  call,
1024
+ /** A call-back ringing; null when none is. Any way of placing a call answers it. */
1025
+ ringing,
1026
+ /** Stops the ringing without answering. */
1027
+ decline,
774
1028
  getSpectrum,
775
1029
  getMicSpectrum,
776
1030
  /** null when the tap is the only entry point. */
@@ -780,11 +1034,37 @@ export function useThursday() {
780
1034
  };
781
1035
  }
782
1036
 
783
- const EMPTY_BANDS = new Array<number>(MARK_BANDS).fill(0);
1037
+ const EMPTY_BANDS = new Array<number>(SPECTRUM_BANDS).fill(0);
784
1038
 
785
- /** Relay dedup key: task id plus its last state change. */
786
- const relayKey = (task: Task) =>
787
- `${task.id}@${toDate(task.updatedAt).getTime()}`;
1039
+ /** What `emote` asked the face to show, and the line the model reads back; `word` is null when nothing is shown. */
1040
+ function readFaceWord(args: string): { word: string | null; reply: string } {
1041
+ let text = "";
1042
+ try {
1043
+ const parsed: unknown = JSON.parse(args);
1044
+ if (parsed && typeof parsed === "object" && "text" in parsed) {
1045
+ text = String(parsed.text ?? "").trim();
1046
+ }
1047
+ } catch {
1048
+ text = "";
1049
+ }
1050
+ const length = Array.from(text).length;
1051
+ if (!length)
1052
+ return { word: null, reply: "Nothing was shown: text is empty." };
1053
+ if (length > FACE_WORD_MAX) {
1054
+ return {
1055
+ word: null,
1056
+ reply: `Too long: ${length} characters, ${FACE_WORD_MAX} at most. Nothing was shown.`,
1057
+ };
1058
+ }
1059
+ const bad = undrawable(text);
1060
+ if (bad) {
1061
+ return {
1062
+ word: null,
1063
+ reply: `Cannot draw "${bad}"; use only the characters \`text\` lists. Nothing was shown.`,
1064
+ };
1065
+ }
1066
+ return { word: text.toUpperCase(), reply: "Shown." };
1067
+ }
788
1068
 
789
1069
  /**
790
1070
  * Calls opened without a gesture (wake word, call-back) get a suspended
@@ -816,95 +1096,159 @@ function armAudioUnlock(context: AudioContext) {
816
1096
  });
817
1097
  }
818
1098
 
819
- /** Tools win over speech; delegate gets its own face. */
820
- function statusOf(activity: RealtimeActivity): LiveStatus {
821
- if (activity.tools.length) {
1099
+ /** Voice wins over work; delegate gets its own face. */
1100
+ function statusOf(activity: LiveActivity): LiveStatus {
1101
+ if (activity.speaking) return "speaking";
1102
+ if (activity.working || activity.tools.length) {
822
1103
  return activity.tools.includes(TOOL_NAMES.delegate)
823
1104
  ? "delegating"
824
1105
  : "working";
825
1106
  }
826
- return activity.speaking ? "speaking" : "listening";
1107
+ return "listening";
827
1108
  }
828
1109
 
829
1110
  /**
830
- * Fire-and-forget save: one toast after SAVE_FAILURE_LIMIT failures, then
831
- * silence.
1111
+ * The words in a transcript, without the sounds Live writes in brackets ("[clear throat]",
1112
+ * "[sigh]"); a bracket may close in the next group ("] okay"). Empty when nothing was said.
832
1113
  */
833
- function persistTurn(
834
- callId: string,
835
- turn: CallTurn,
1114
+ function spokenWords(text: string) {
1115
+ const words = text.replace(/\[[^\]]*\]?|\]/g, "").trim();
1116
+ return /[\p{L}\p{N}]/u.test(words) ? words : "";
1117
+ }
1118
+
1119
+ /** Fire-and-forget save; after SAVE_FAILURE_LIMIT failures of this kind it stops and says so once. */
1120
+ function persist(
836
1121
  saving: { failures: number },
1122
+ save: () => Promise<Result<unknown>>,
1123
+ stopped = "This call is not being saved",
837
1124
  ) {
838
1125
  if (saving.failures >= SAVE_FAILURE_LIMIT) return;
839
- void saveTurnsAction(callId, [turn])
1126
+ void save()
840
1127
  .then((result) => unwrapResult(result))
841
1128
  .catch((cause) => {
842
1129
  saving.failures += 1;
843
1130
  if (saving.failures !== SAVE_FAILURE_LIMIT) return;
844
1131
  toast.add({
845
1132
  type: "warning",
846
- title: "This call is not being saved",
1133
+ title: stopped,
847
1134
  description: errorToString(cause),
848
1135
  });
849
1136
  });
850
1137
  }
851
1138
 
852
1139
  /**
853
- * Waiting on a real question. A job the app stopped waits in the inbox
854
- * instead of ringing.
1140
+ * Whether a thread is one the call-back rings for: a real question, or with
1141
+ * "any" every ending. A job the app stops and picks up by itself never rings.
855
1142
  */
856
- function asksSomething(task: Task) {
857
- return task.status === "waiting" && !isAppStop(task.ask);
858
- }
1143
+ const ringsFor = (thread: Thread, callBack: CallBack) =>
1144
+ callBack !== "off" &&
1145
+ !thread.seen &&
1146
+ !thread.ask?.auto &&
1147
+ (thread.room.questions.length > 0 ||
1148
+ (callBack === "any" && thread.status !== "running"));
1149
+
1150
+ /** One piece of background work waiting on the user, as the relay clock puts it to her. */
1151
+ type OpenWork = {
1152
+ key: string;
1153
+ /** What it is, so one append holds one kind. */
1154
+ kind: "question" | "ending" | "progress";
1155
+ /** The relay text. */
1156
+ line: string;
1157
+ /** Relay rows it covers, accepted once it lands. */
1158
+ relayIds: number[];
1159
+ /** The same item on the activity line, so the user sees where her words came from. */
1160
+ show: ActivityLine;
1161
+ };
859
1162
 
860
- /**
861
- * Sent as a system entry (realtime.driver say); the first sentence names
862
- * sender and recipient or the model answers it as user speech.
863
- */
864
- function relayLine(task: Task) {
865
- // The label travels with the answer: what the user says next about this job
866
- // is a word to it, and the task tool takes it by that name
867
- const from = `[${task.bot} → you, about "${task.label}" — not the user speaking; they have not heard this. Anything more on this job goes to the task tool as "${task.label}", never a new job.]`;
868
- switch (task.status) {
869
- case "waiting": {
870
- // a stop is told, not read as a question
871
- const options = task.ask?.options ?? [];
872
- const said = task.ask?.question ?? task.outcome ?? "";
873
- if (isAppStop(task.ask)) {
874
- return `${from} It stopped before finishing. Where it got to: ${said} Ask whether to keep going; if yes, send "${TASK_CONTINUE}" with the task tool.`;
875
- }
876
- const list = options.length ? ` Options: ${options.join(" / ")}.` : "";
877
- return `${from} It asks: ${said}${list} Answer with the task tool if you can; otherwise ask the user and send back what they say.`;
878
- }
879
- case "failed":
880
- return `${from} It could not finish: ${task.outcome ?? ""}`;
881
- default:
882
- return `${from} Done. Its answer: ${task.outcome ?? ""}`;
883
- }
884
- }
1163
+ const OPEN_RANK = {
1164
+ question: 0,
1165
+ stopped: 1,
1166
+ done: 2,
1167
+ progress: 3,
1168
+ } as const;
885
1169
 
886
1170
  /**
887
- * Lines that piled up while she was reading, as one system entry. The header
888
- * is what stops it becoming one turn per job: they arrived together and the
889
- * user has heard none of them.
1171
+ * Everything in the inbox still waiting on the user, most pressing first:
1172
+ * questions, then jobs stopped or finished and not yet seen, then
1173
+ * progress from jobs still running. Sent as commentary; the bracket carries
1174
+ * facts only — who, which thread, where an answer goes — because the backend
1175
+ * reads relays too and routes answers by them.
890
1176
  */
891
- function mergedRelay(queued: Said[]) {
892
- return `[${queued.length} jobs came back at once — not the user speaking; they have heard none of this. Tell them in one turn, not ${queued.length}.]
1177
+ function openWork(threads: Thread[]): OpenWork[] {
1178
+ const items: (OpenWork & { rank: number })[] = [];
1179
+ for (const thread of threads) {
1180
+ const { relays, questions } = thread.room;
1181
+ const changed = toDate(thread.updatedAt).getTime();
1182
+ const bracket = (from: string, kind: string, tail = "") =>
1183
+ `[${from} → Thursday, thread "${thread.label}" (${thread.id}), ${kind}.${tail}]`;
1184
+ const show = (bot: string, line: string): ActivityLine => ({
1185
+ kind: "relay",
1186
+ name: thread.label,
1187
+ line,
1188
+ done: true,
1189
+ bot,
1190
+ });
893
1191
 
894
- ${queued.map((one) => one.line).join("\n\n")}`;
895
- }
1192
+ for (const question of questions) {
1193
+ const options = question.options?.length
1194
+ ? ` Options: ${question.options.join(" / ")}.`
1195
+ : "";
1196
+ items.push({
1197
+ rank: OPEN_RANK.question,
1198
+ key: `question:${question.id}`,
1199
+ kind: "question",
1200
+ line: `${bracket(question.bot, "question", ` Its answer goes to thread ${thread.id}, recipient ${question.bot}, replyTo ${question.id}.`)}\n${question.text}${options}`,
1201
+ relayIds: relays
1202
+ .filter((relay) => relay.messageId === question.id)
1203
+ .map((relay) => relay.id),
1204
+ show: show(question.bot, `${question.bot}: question`),
1205
+ });
1206
+ }
896
1207
 
897
- /** The same relay on the activity line, so the user sees where the model's line came from. */
898
- function relayShow(task: Task): ToolRun {
899
- const line =
900
- task.status === "waiting"
901
- ? isAppStop(task.ask)
902
- ? `${task.bot} stopped`
903
- : `${task.bot} is asking`
904
- : task.status === "failed"
905
- ? `${task.bot} could not finish`
906
- : `Answer from ${task.bot}`;
907
- return { kind: "relay", name: task.label, line, done: true, bot: task.bot };
908
- }
1208
+ // Relay rows that belong to no open question
1209
+ const loose = relays.filter(
1210
+ (relay) => !questions.some((question) => question.id === relay.messageId),
1211
+ );
1212
+ // A cancel is the user's own and already seen; nothing about it is news
1213
+ const ended = thread.status === "done";
1214
+ // A stop the app picks back up by itself is not news: it runs again in a moment
1215
+ const stopped =
1216
+ thread.status === "waiting" && isAppStop(thread.ask) && !thread.ask?.auto;
1217
+
1218
+ if (ended || stopped) {
1219
+ if (thread.seen) continue;
1220
+ const kind = stopped ? "stopped" : "done";
1221
+ const said =
1222
+ kind === "stopped"
1223
+ ? `It stopped before finishing. Where it got to: ${thread.ask?.question ?? thread.outcome ?? ""}`
1224
+ : `Done. Its answer: ${thread.outcome ?? ""}`;
1225
+ items.push({
1226
+ rank: OPEN_RANK[kind],
1227
+ key: `${kind}:${thread.id}@${changed}`,
1228
+ kind: "ending",
1229
+ line: `${bracket(thread.bot, kind)}\n${said}`,
1230
+ // Its ending says what its progress messages said
1231
+ relayIds: loose.map((relay) => relay.id),
1232
+ show: show(
1233
+ thread.bot,
1234
+ kind === "stopped"
1235
+ ? `${thread.bot} stopped`
1236
+ : `Answer from ${thread.bot}`,
1237
+ ),
1238
+ });
1239
+ continue;
1240
+ }
909
1241
 
910
- export type { CallMessage, CallStatus };
1242
+ for (const relay of loose) {
1243
+ items.push({
1244
+ rank: OPEN_RANK.progress,
1245
+ key: `progress:${relay.id}`,
1246
+ kind: "progress",
1247
+ line: `${bracket(relay.bot, relay.kind)}\n${relay.text}`,
1248
+ relayIds: [relay.id],
1249
+ show: show(relay.bot, `${relay.bot}: ${relay.kind}`),
1250
+ });
1251
+ }
1252
+ }
1253
+ return items.sort((a, b) => a.rank - b.rank);
1254
+ }