thursday-agent 0.1.2 → 0.3.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 (573) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/app-path-routes-manifest.json +5 -1
  3. package/.next/build-manifest.json +3 -3
  4. package/.next/prerender-manifest.json +5 -5
  5. package/.next/routes-manifest.json +27 -3
  6. package/.next/server/app/_global-error/page.js +2 -2
  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 +2 -2
  10. package/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  11. package/.next/server/app/_global-error.segments/_full.segment.rsc +2 -2
  12. package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  13. package/.next/server/app/_not-found/page.js +2 -2
  14. package/.next/server/app/_not-found/page.js.nft.json +1 -1
  15. package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  16. package/.next/server/app/_not-found.html +1 -1
  17. package/.next/server/app/_not-found.rsc +14 -14
  18. package/.next/server/app/_not-found.segments/_full.segment.rsc +14 -14
  19. package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +12 -12
  20. package/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  21. package/.next/server/app/api/artifact/route.js +3 -3
  22. package/.next/server/app/api/artifact/route.js.nft.json +1 -1
  23. package/.next/server/app/api/bot/memory/files/route/app-paths-manifest.json +3 -0
  24. package/.next/server/app/api/bot/memory/files/route.js +10 -0
  25. package/.next/server/app/api/bot/memory/files/route.js.nft.json +1 -0
  26. package/.next/server/app/api/bot/memory/files/route_client-reference-manifest.js +3 -0
  27. package/.next/server/app/api/bot/memory/route/app-paths-manifest.json +3 -0
  28. package/.next/server/app/api/bot/memory/route/build-manifest.json +12 -0
  29. package/.next/server/app/api/bot/memory/route/server-reference-manifest.json +4 -0
  30. package/.next/server/app/api/bot/memory/route.js +14 -0
  31. package/.next/server/app/api/bot/memory/route.js.map +5 -0
  32. package/.next/server/app/api/bot/memory/route.js.nft.json +1 -0
  33. package/.next/server/app/api/bot/memory/route_client-reference-manifest.js +3 -0
  34. package/.next/server/app/api/bot/route.js +7 -7
  35. package/.next/server/app/api/bot/route.js.nft.json +1 -1
  36. package/.next/server/app/api/bot/task/[id]/route.js +9 -9
  37. package/.next/server/app/api/bot/task/[id]/route.js.nft.json +1 -1
  38. package/.next/server/app/api/bot/task/route.js +8 -8
  39. package/.next/server/app/api/bot/task/route.js.nft.json +1 -1
  40. package/.next/server/app/api/config/route.js +6 -6
  41. package/.next/server/app/api/config/route.js.nft.json +1 -1
  42. package/.next/server/app/api/events/route.js +4 -5
  43. package/.next/server/app/api/events/route.js.nft.json +1 -1
  44. package/.next/server/app/api/file/[...path]/route.js +3 -3
  45. package/.next/server/app/api/file/[...path]/route.js.nft.json +1 -1
  46. package/.next/server/app/api/llm-model/catalog/route.js +10 -9
  47. package/.next/server/app/api/llm-model/catalog/route.js.nft.json +1 -1
  48. package/.next/server/app/api/llm-model/credits/route/app-paths-manifest.json +3 -0
  49. package/.next/server/app/api/llm-model/credits/route/build-manifest.json +12 -0
  50. package/.next/server/app/api/llm-model/credits/route/server-reference-manifest.json +4 -0
  51. package/.next/server/app/api/llm-model/credits/route.js +16 -0
  52. package/.next/server/app/api/llm-model/credits/route.js.map +5 -0
  53. package/.next/server/app/api/llm-model/credits/route.js.nft.json +1 -0
  54. package/.next/server/app/api/llm-model/credits/route_client-reference-manifest.js +3 -0
  55. package/.next/server/app/api/llm-model/route.js +10 -8
  56. package/.next/server/app/api/llm-model/route.js.nft.json +1 -1
  57. package/.next/server/app/api/llm-model/subscription/route/app-paths-manifest.json +3 -0
  58. package/.next/server/app/api/llm-model/subscription/route/build-manifest.json +12 -0
  59. package/.next/server/app/api/llm-model/subscription/route/server-reference-manifest.json +4 -0
  60. package/.next/server/app/api/llm-model/subscription/route.js +15 -0
  61. package/.next/server/app/api/llm-model/subscription/route.js.map +5 -0
  62. package/.next/server/app/api/llm-model/subscription/route.js.nft.json +1 -0
  63. package/.next/server/app/api/llm-model/subscription/route_client-reference-manifest.js +3 -0
  64. package/.next/server/app/api/mcp/[name]/route.js +5 -5
  65. package/.next/server/app/api/mcp/[name]/route.js.nft.json +1 -1
  66. package/.next/server/app/api/mcp/oauth/callback/route.js +6 -6
  67. package/.next/server/app/api/mcp/oauth/callback/route.js.nft.json +1 -1
  68. package/.next/server/app/api/mcp/route.js +5 -5
  69. package/.next/server/app/api/mcp/route.js.nft.json +1 -1
  70. package/.next/server/app/api/mcp/tools/route.js +5 -5
  71. package/.next/server/app/api/mcp/tools/route.js.nft.json +1 -1
  72. package/.next/server/app/api/memory/edit/route/app-paths-manifest.json +3 -0
  73. package/.next/server/app/api/memory/edit/route/build-manifest.json +12 -0
  74. package/.next/server/app/api/memory/edit/route/server-reference-manifest.json +4 -0
  75. package/.next/server/app/api/memory/edit/route.js +24 -0
  76. package/.next/server/app/api/memory/edit/route.js.map +5 -0
  77. package/.next/server/app/api/memory/edit/route.js.nft.json +1 -0
  78. package/.next/server/app/api/memory/{tidy → edit}/route_client-reference-manifest.js +1 -1
  79. package/.next/server/app/api/memory/route.js +7 -8
  80. package/.next/server/app/api/memory/route.js.nft.json +1 -1
  81. package/.next/server/app/api/skills/[source]/[name]/route.js +3 -3
  82. package/.next/server/app/api/skills/[source]/[name]/route.js.nft.json +1 -1
  83. package/.next/server/app/api/skills/route.js +3 -3
  84. package/.next/server/app/api/skills/route.js.nft.json +1 -1
  85. package/.next/server/app/api/thursday/call/route.js +9 -7
  86. package/.next/server/app/api/thursday/call/route.js.nft.json +1 -1
  87. package/.next/server/app/api/thursday/skills/route.js +9 -7
  88. package/.next/server/app/api/thursday/skills/route.js.nft.json +1 -1
  89. package/.next/server/app/api/thursday/tool-call/route.js +16 -15
  90. package/.next/server/app/api/thursday/tool-call/route.js.nft.json +1 -1
  91. package/.next/server/app/api/workspace/route.js +4 -5
  92. package/.next/server/app/api/workspace/route.js.nft.json +1 -1
  93. package/.next/server/app/artifact/[...path]/page/server-reference-manifest.json +1 -1
  94. package/.next/server/app/artifact/[...path]/page.js +5 -6
  95. package/.next/server/app/artifact/[...path]/page.js.nft.json +1 -1
  96. package/.next/server/app/artifact/[...path]/page_client-reference-manifest.js +1 -1
  97. package/.next/server/app/favicon.ico/route.js +3 -4
  98. package/.next/server/app/favicon.ico/route.js.nft.json +1 -1
  99. package/.next/server/app/favicon.ico.body +0 -0
  100. package/.next/server/app/page/server-reference-manifest.json +66 -77
  101. package/.next/server/app/page.js +11 -11
  102. package/.next/server/app/page.js.nft.json +1 -1
  103. package/.next/server/app/page_client-reference-manifest.js +1 -1
  104. package/.next/server/app-paths-manifest.json +5 -1
  105. package/.next/server/chunks/[externals]__0dzuk6_._.js +3 -0
  106. package/.next/server/chunks/[root-of-the-server]__01dkp4a._.js +3 -0
  107. package/.next/server/chunks/[root-of-the-server]__02va5wz._.js +3 -0
  108. package/.next/server/chunks/[root-of-the-server]__02xjoto._.js +3 -0
  109. package/.next/server/chunks/[root-of-the-server]__04q--0f._.js +8 -0
  110. package/.next/server/chunks/[root-of-the-server]__0bl-e63._.js +3 -0
  111. package/.next/server/chunks/[root-of-the-server]__0ce8nf8._.js +5 -0
  112. package/.next/server/chunks/[root-of-the-server]__0ffbg85._.js +3 -0
  113. package/.next/server/chunks/[root-of-the-server]__0j7z0r3._.js +12 -0
  114. package/.next/server/chunks/[root-of-the-server]__0jq4ohy._.js +8 -0
  115. package/.next/server/chunks/[root-of-the-server]__0ls6_8y._.js +8 -0
  116. package/.next/server/chunks/[root-of-the-server]__0ncs9uu._.js +3 -0
  117. package/.next/server/chunks/[root-of-the-server]__0pa2n68._.js +3 -0
  118. package/.next/server/chunks/[root-of-the-server]__0q0vvt2._.js +5 -0
  119. package/.next/server/chunks/[root-of-the-server]__1-ga0kh._.js +5 -0
  120. package/.next/server/chunks/[root-of-the-server]__11n256_._.js +5 -0
  121. package/.next/server/chunks/[root-of-the-server]__12cabg9._.js +3 -0
  122. package/.next/server/chunks/[root-of-the-server]__1339jnv._.js +8 -0
  123. package/.next/server/chunks/[root-of-the-server]__15bjnu5._.js +5 -0
  124. package/.next/server/chunks/[root-of-the-server]__18_teo8._.js +8 -0
  125. package/.next/server/chunks/[root-of-the-server]__1_04mcf._.js +5 -0
  126. package/.next/server/chunks/[root-of-the-server]__1anc1p9._.js +3 -0
  127. package/.next/server/chunks/[root-of-the-server]__1ccon1w._.js +10 -0
  128. package/.next/server/chunks/[root-of-the-server]__1fizn31._.js +3 -0
  129. package/.next/server/chunks/[root-of-the-server]__1llj3-b._.js +3 -0
  130. package/.next/server/chunks/[root-of-the-server]__1orn-5b._.js +11 -0
  131. package/.next/server/chunks/[root-of-the-server]__1p8m3f4._.js +3 -0
  132. package/.next/server/chunks/[root-of-the-server]__1wa1k7r._.js +8 -0
  133. package/.next/server/chunks/{_1hzrsvg._.js → _0-73of8._.js} +6 -6
  134. package/.next/server/chunks/{_1cbk7ms._.js → _0-fjql4._.js} +10 -10
  135. package/.next/server/chunks/_00xh_q0._.js +27 -0
  136. package/.next/server/chunks/{_0y8is-s._.js → _03a0897._.js} +1 -1
  137. package/.next/server/chunks/{_0pppbb5._.js → _03kp0jp._.js} +166 -35
  138. package/.next/server/chunks/_051qh8u._.js +3 -0
  139. package/.next/server/chunks/{node_modules_next_04q-o58._.js → _05dla15._.js} +2 -2
  140. package/.next/server/chunks/_05h2cii._.js +1 -1
  141. package/.next/server/chunks/_0c9j6rb._.js +49 -0
  142. package/.next/server/chunks/_0fzqt2y._.js +74 -44
  143. package/.next/server/chunks/{_0kl37zl._.js → _0ioz3nd._.js} +1 -1
  144. package/.next/server/chunks/_0iwfby5._.js +3 -0
  145. package/.next/server/chunks/{_0hin38_._.js → _0khihjk._.js} +5 -5
  146. package/.next/server/chunks/_0mecv0f._.js +26 -0
  147. package/.next/server/chunks/_0mh8j4m._.js +3 -0
  148. package/.next/server/chunks/{_1icgbi8._.js → _0n74zsf._.js} +2 -2
  149. package/.next/server/chunks/_0obojvv._.js +3 -0
  150. package/.next/server/chunks/_0recufk._.js +49 -0
  151. package/.next/server/chunks/{_1t8fv7b._.js → _0rnf-6n._.js} +2 -2
  152. package/.next/server/chunks/_0sb7tlz._.js +3 -0
  153. package/.next/server/chunks/{_0jgm3b8._.js → _0u10wmw._.js} +6 -6
  154. package/.next/server/chunks/{_12nr4xc._.js → _0u4aiy2._.js} +2 -2
  155. package/.next/server/chunks/_0wp4le3._.js +1 -1
  156. package/.next/server/chunks/_0y5fg-x._.js +1 -1
  157. package/.next/server/chunks/_1230wbx._.js +10 -0
  158. package/.next/server/chunks/_12nheja._.js +3 -0
  159. package/.next/server/chunks/_13510xh._.js +3 -0
  160. package/.next/server/chunks/_15hq89o._.js +1 -1
  161. package/.next/server/chunks/_17l5ygt._.js +49 -0
  162. package/.next/server/chunks/{_0ks6_sg._.js → _18-1wcc._.js} +6 -6
  163. package/.next/server/chunks/_1_5mnq4._.js +18 -0
  164. package/.next/server/chunks/{_0tzfesa._.js → _1_9mjx-._.js} +1 -1
  165. package/.next/server/chunks/_1d0j961._.js +6 -0
  166. package/.next/server/chunks/_1d5zmy8._.js +1 -1
  167. package/.next/server/chunks/_1drgfx4._.js +18 -0
  168. package/.next/server/chunks/{_1skgy7b._.js → _1e48czp._.js} +5 -5
  169. package/.next/server/chunks/_1f46eie._.js +3 -0
  170. package/.next/server/chunks/_1h5_8sk._.js +3 -0
  171. package/.next/server/chunks/_1hdnxb7._.js +3 -0
  172. package/.next/server/chunks/_1j8k_j5._.js +3 -0
  173. package/.next/server/chunks/{_10qam6o._.js → _1kg70wc._.js} +5 -5
  174. package/.next/server/chunks/_1krd9rf._.js +3 -0
  175. package/.next/server/chunks/_1nc0eui._.js +11 -0
  176. package/.next/server/chunks/_1nula3v._.js +3 -0
  177. package/.next/server/chunks/_1pf4qe-._.js +3 -0
  178. package/.next/server/chunks/_1php4eq._.js +1 -1
  179. package/.next/server/chunks/_1qxheoa._.js +3 -0
  180. package/.next/server/chunks/_1x6kd1r._.js +3 -0
  181. package/.next/server/chunks/_1yuv4vp._.js +1 -1
  182. package/.next/server/chunks/_1zjrdag._.js +3 -0
  183. package/.next/server/chunks/_next-internal_server_app_api_bot_memory_files_route_actions_1_xd_8f.js +3 -0
  184. package/.next/server/chunks/_next-internal_server_app_api_bot_memory_route_actions_1a8xfr7.js +3 -0
  185. package/.next/server/chunks/_next-internal_server_app_api_llm-model_credits_route_actions_0v424pb.js +3 -0
  186. package/.next/server/chunks/_next-internal_server_app_api_llm-model_subscription_route_actions_0-udcm5.js +3 -0
  187. package/.next/server/chunks/_next-internal_server_app_api_memory_edit_route_actions_02sfnmr.js +3 -0
  188. package/.next/server/chunks/config_ts_1g4sz5o._.js +1 -1
  189. package/.next/server/chunks/features_0rpfxbk._.js +3 -0
  190. package/.next/server/chunks/features_1qf3cei._.js +3 -0
  191. package/.next/server/chunks/features_bot_0zt92oh._.js +3 -0
  192. package/.next/server/chunks/features_bot_bot_query_ts_12mgkl2._.js +1 -1
  193. package/.next/server/chunks/features_memory_1uqvksn._.js +6 -0
  194. package/.next/server/chunks/{node_modules_consola_dist_chunks_prompt_mjs_0tf290y._.js → node_modules_consola_dist_chunks_prompt_mjs_0i7cvml._.js} +1 -1
  195. package/.next/server/chunks/{node_modules_consola_dist_chunks_prompt_mjs_0228f43._.js → node_modules_consola_dist_chunks_prompt_mjs_0icqaqw._.js} +1 -1
  196. package/.next/server/chunks/{node_modules_consola_dist_chunks_prompt_mjs_0ir-vo2._.js → node_modules_consola_dist_chunks_prompt_mjs_0j61-ma._.js} +1 -1
  197. package/.next/server/chunks/{node_modules_consola_dist_chunks_prompt_mjs_0otw8ef._.js → node_modules_consola_dist_chunks_prompt_mjs_0r28-sm._.js} +1 -1
  198. package/.next/server/chunks/node_modules_consola_dist_chunks_prompt_mjs_0v_7drq._.js +3 -0
  199. package/.next/server/chunks/node_modules_consola_dist_chunks_prompt_mjs_11zjo-1._.js +3 -0
  200. package/.next/server/chunks/node_modules_consola_dist_chunks_prompt_mjs_1pycqsq._.js +3 -0
  201. package/.next/server/chunks/node_modules_consola_dist_chunks_prompt_mjs_1xoq2rl._.js +3 -0
  202. package/.next/server/chunks/node_modules_consola_dist_chunks_prompt_mjs_1y49572._.js +3 -0
  203. package/.next/server/chunks/ssr/[externals]__0dizgek._.js +3 -0
  204. package/.next/server/chunks/ssr/[root-of-the-server]__09q9med._.js +1 -1
  205. package/.next/server/chunks/ssr/[root-of-the-server]__0_69wqr._.js +3 -0
  206. package/.next/server/chunks/ssr/[root-of-the-server]__0hg5gib._.js +1 -1
  207. package/.next/server/chunks/ssr/[root-of-the-server]__0i8ie7f._.js +3 -0
  208. package/.next/server/chunks/ssr/{[root-of-the-server]__0jsmmb-._.js → [root-of-the-server]__0mz33a7._.js} +1 -1
  209. package/.next/server/chunks/ssr/[root-of-the-server]__0tbkdlx._.js +3 -0
  210. package/.next/server/chunks/ssr/[root-of-the-server]__0vnmv-s._.js +3 -0
  211. package/.next/server/chunks/ssr/{[root-of-the-server]__0f6s48x._.js → [root-of-the-server]__0xpcv9w._.js} +3 -3
  212. package/.next/server/chunks/ssr/[root-of-the-server]__0xtssj3._.js +1 -1
  213. package/.next/server/chunks/ssr/[root-of-the-server]__0zkv7z_._.js +3 -0
  214. package/.next/server/chunks/ssr/[root-of-the-server]__10-dz5o._.js +57 -0
  215. package/.next/server/chunks/ssr/[root-of-the-server]__15_uhip._.js +3 -0
  216. package/.next/server/chunks/ssr/[root-of-the-server]__16jc196._.js +1 -1
  217. package/.next/server/chunks/ssr/[root-of-the-server]__1cyjywu._.js +3 -0
  218. package/.next/server/chunks/ssr/[root-of-the-server]__1eh7hid._.js +3 -0
  219. package/.next/server/chunks/ssr/[root-of-the-server]__1gtlw6v._.js +43 -0
  220. package/.next/server/chunks/ssr/[root-of-the-server]__1mzi-ju._.js +1 -1
  221. package/.next/server/chunks/ssr/[root-of-the-server]__1ox7hbk._.js +4 -0
  222. package/.next/server/chunks/ssr/[root-of-the-server]__1pduf5n._.js +3 -0
  223. package/.next/server/chunks/ssr/{_1mznrxk._.js → _001_ms1._.js} +3 -3
  224. package/.next/server/chunks/ssr/_01_6m32._.js +3 -0
  225. package/.next/server/chunks/ssr/_01n3o0m._.js +3 -0
  226. package/.next/server/chunks/ssr/_07uchnc._.js +3 -0
  227. package/.next/server/chunks/ssr/_0ap2fzn._.js +3 -0
  228. package/.next/server/chunks/ssr/_0c-ql2m._.js +3 -0
  229. package/.next/server/chunks/ssr/_0h42fze._.js +3 -0
  230. package/.next/server/chunks/ssr/_0h7zdni._.js +1 -1
  231. package/.next/server/chunks/ssr/_0i_6q1w._.js +41 -0
  232. package/.next/server/chunks/ssr/_0kbkmar._.js +3 -0
  233. package/.next/server/chunks/ssr/_0lu1n_x._.js +3 -0
  234. package/.next/server/chunks/ssr/{_077-g6s._.js → _0mb8zc8._.js} +3 -3
  235. package/.next/server/chunks/ssr/_0mo2w06._.js +3 -0
  236. package/.next/server/chunks/ssr/{_13punpm._.js → _0q0zan6._.js} +2 -2
  237. package/.next/server/chunks/ssr/{_0gd4vaa._.js → _0u2jv_x._.js} +94 -58
  238. package/.next/server/chunks/ssr/_0y0tiht._.js +4 -0
  239. package/.next/server/chunks/ssr/_0zhoc3r._.js +3 -0
  240. package/.next/server/chunks/ssr/_1-teee5._.js +3 -3
  241. package/.next/server/chunks/ssr/_13fke0x._.js +3 -0
  242. package/.next/server/chunks/ssr/{_0q_2f2x._.js → _13z3f85._.js} +2 -2
  243. package/.next/server/chunks/ssr/_14_lw10._.js +3 -0
  244. package/.next/server/chunks/ssr/_15j2_8f._.js +3 -0
  245. package/.next/server/chunks/ssr/{_0lc_7c8._.js → _1eoqg_g._.js} +2 -2
  246. package/.next/server/chunks/ssr/_1sji22n._.js +3 -0
  247. package/.next/server/chunks/ssr/_1spzm8z._.js +3 -0
  248. package/.next/server/chunks/ssr/{_19_o9d-._.js → _1vi_uo7._.js} +2 -2
  249. package/.next/server/chunks/ssr/_1vyd1ji._.js +3 -0
  250. package/.next/server/chunks/ssr/_1xvgn77._.js +3 -0
  251. package/.next/server/chunks/ssr/_20kd_n0._.js +42 -0
  252. package/.next/server/chunks/ssr/features_bot_1s_-2vj._.js +3 -0
  253. package/.next/server/chunks/ssr/features_config_components_config-setting_tsx_0xfwqdg._.js +1 -1
  254. package/.next/server/chunks/ssr/features_connectors_components_mcp-setting_tsx_0jmbgvp._.js +1 -1
  255. package/.next/server/chunks/ssr/lib_date-like_ts_02is0pw._.js +3 -0
  256. package/.next/server/chunks/ssr/lib_realtime_12wg_dd._.js +27 -0
  257. package/.next/server/chunks/ssr/lib_realtime_15h_v1v._.js +27 -0
  258. package/.next/server/chunks/ssr/{node_modules_1d-is1o._.js → node_modules_063xwpp._.js} +1 -1
  259. package/.next/server/chunks/ssr/{node_modules_1ub40gk._.js → node_modules_0bvraul._.js} +1 -1
  260. package/.next/server/chunks/ssr/{node_modules_1nhw9qg._.js → node_modules_0eu1va6._.js} +1 -1
  261. package/.next/server/chunks/ssr/{node_modules_0xwx4u-._.js → node_modules_0h50b8n._.js} +1 -1
  262. package/.next/server/chunks/ssr/{node_modules_043-236._.js → node_modules_0h5hnr1._.js} +1 -1
  263. package/.next/server/chunks/ssr/{node_modules_1t0y5jt._.js → node_modules_0h7u60n._.js} +1 -1
  264. package/.next/server/chunks/ssr/{node_modules_1k29j1j._.js → node_modules_0lm7_vm._.js} +1 -1
  265. package/.next/server/chunks/ssr/{node_modules_16i4o_3._.js → node_modules_0mwym0g._.js} +1 -1
  266. package/.next/server/chunks/ssr/{node_modules_1y8_3vg._.js → node_modules_0rjg6t1._.js} +1 -1
  267. package/.next/server/chunks/ssr/node_modules_0x3-ply._.js +3 -0
  268. package/.next/server/chunks/ssr/{node_modules_108wd2o._.js → node_modules_0y_cded._.js} +1 -1
  269. package/.next/server/chunks/ssr/{node_modules_1jatz7w._.js → node_modules_0z5tsu8._.js} +1 -1
  270. package/.next/server/chunks/ssr/{node_modules_0ls96je._.js → node_modules_0zp9r92._.js} +1 -1
  271. package/.next/server/chunks/ssr/{node_modules_06kvvuo._.js → node_modules_1-hc6uv._.js} +1 -1
  272. package/.next/server/chunks/ssr/{node_modules_1_588_4._.js → node_modules_113tz0l._.js} +1 -1
  273. package/.next/server/chunks/ssr/{node_modules_0ipn1ho._.js → node_modules_124bue5._.js} +1 -1
  274. package/.next/server/chunks/ssr/node_modules_17bwo43._.js +55 -0
  275. package/.next/server/chunks/ssr/{node_modules_0_j7-g4._.js → node_modules_1_n3ztw._.js} +1 -1
  276. package/.next/server/chunks/ssr/{node_modules_20ynonm._.js → node_modules_1bc7gu8._.js} +1 -1
  277. package/.next/server/chunks/ssr/{node_modules_1hn_sd5._.js → node_modules_1bgb6-f._.js} +1 -1
  278. package/.next/server/chunks/ssr/{node_modules_0bue6gy._.js → node_modules_1h4a4pp._.js} +1 -1
  279. package/.next/server/chunks/ssr/{node_modules_0fbw5-u._.js → node_modules_1i7u1ia._.js} +1 -1
  280. package/.next/server/chunks/ssr/{node_modules_1ee8gav._.js → node_modules_1juq3hs._.js} +1 -1
  281. package/.next/server/chunks/ssr/{node_modules_0_fvxnt._.js → node_modules_1l0qtdu._.js} +1 -1
  282. package/.next/server/chunks/ssr/{node_modules_13f2md6._.js → node_modules_1t9cyee._.js} +1 -1
  283. package/.next/server/chunks/ssr/{node_modules_0y2-av4._.js → node_modules_1uuj9x1._.js} +1 -1
  284. package/.next/server/chunks/ssr/{node_modules_00d6675._.js → node_modules_1vqb6po._.js} +1 -1
  285. package/.next/server/chunks/ssr/{node_modules_1v0077l._.js → node_modules_1xx28h1._.js} +1 -1
  286. package/.next/server/chunks/ssr/{node_modules_@mermaid-js_parser_dist_chunks_mermaid-parser_core_092s8nx._.js → node_modules_@mermaid-js_parser_dist_chunks_mermaid-parser_core_0d3skpl._.js} +1 -1
  287. package/.next/server/chunks/ssr/{node_modules_@mermaid-js_parser_dist_chunks_mermaid-parser_core_1nvexgu._.js → node_modules_@mermaid-js_parser_dist_chunks_mermaid-parser_core_0i44kk5._.js} +1 -1
  288. package/.next/server/chunks/ssr/node_modules_@vercel_oidc_dist_0qe30a_._.js +3 -0
  289. package/.next/server/chunks/ssr/node_modules_consola_dist_chunks_prompt_mjs_05cwqv2._.js +47 -0
  290. package/.next/server/chunks/ssr/{node_modules_next_dist_0w6mzq5._.js → node_modules_next_dist_0drixxt._.js} +4 -4
  291. package/.next/server/chunks/ssr/node_modules_next_dist_1n3w9lb._.js +2 -2
  292. package/.next/server/instrumentation.js +1 -1
  293. package/.next/server/middleware-build-manifest.js +3 -3
  294. package/.next/server/pages/404.html +1 -1
  295. package/.next/server/pages/500.html +1 -1
  296. package/.next/server/server-reference-manifest.js +1 -1
  297. package/.next/server/server-reference-manifest.json +67 -78
  298. package/.next/static/chunks/{31ttnjt6g35a0.js → 0ev4hyghq0mbb.js} +1 -1
  299. package/.next/static/chunks/0r5qcctujhxli.js +1 -0
  300. package/.next/static/chunks/0tmgrnlje14-s.js +3 -0
  301. package/.next/static/chunks/0x79tx04dxbpp.js +1 -0
  302. package/.next/static/chunks/0xzgvc0fkdb6t.js +1 -0
  303. package/.next/static/chunks/10teww7wt-f6h.js +1 -0
  304. package/.next/static/chunks/14hgtsjg-kael.css +1 -0
  305. package/.next/static/chunks/17donp69y4do7.js +40 -0
  306. package/.next/static/chunks/1dkjg87pxk0n9.js +28 -0
  307. package/.next/static/chunks/1ekc7ul556kbl.js +1 -0
  308. package/.next/static/chunks/{1khakdu_iuedc.js → 1grhjo7a5a0_-.js} +1 -1
  309. package/.next/static/chunks/1kq8h8wi_e0bc.js +53 -0
  310. package/.next/static/chunks/1okqiww3pnwka.js +1 -0
  311. package/.next/static/chunks/26-pr2c9ro222.js +1 -0
  312. package/.next/static/chunks/{2ihkf6l2jowo9.js → 280t0_e99i276.js} +3 -3
  313. package/.next/static/chunks/28cizcdaabd4h.js +1 -0
  314. package/.next/static/chunks/2a0k6jlq0_v1u.js +2 -0
  315. package/.next/static/chunks/2fzs9fm8yarvq.js +1 -0
  316. package/.next/static/chunks/2h0sd2vumhb15.js +1 -0
  317. package/.next/static/chunks/2hw2f6zmt7fmt.js +1 -0
  318. package/.next/static/chunks/2t4vhi3m8ahya.js +1 -0
  319. package/.next/static/chunks/2tigdtz3dpcy8.js +1 -0
  320. package/.next/static/chunks/3111pj5wnjgos.js +1 -0
  321. package/.next/static/chunks/3786h5vayt80-.js +28 -0
  322. package/.next/static/chunks/{3aquirz5h71da.js → 3agpgkwaa321p.js} +1 -1
  323. package/.next/static/chunks/3cfi_q6d_wtci.js +1 -0
  324. package/.next/static/chunks/3ok9_x74rip3v.js +1 -0
  325. package/.next/static/chunks/3xk7yqgomx7yq.js +1 -0
  326. package/.next/static/chunks/3xpbkl66ssib5.js +38 -0
  327. package/.next/static/media/favicon.37o1pjldwi2uc.ico +0 -0
  328. package/README.md +6 -4
  329. package/app/api/bot/memory/files/route.ts +14 -0
  330. package/app/api/bot/memory/route.ts +5 -0
  331. package/app/api/events/app-event.client.ts +24 -0
  332. package/app/api/events/app-event.server.ts +45 -0
  333. package/app/api/events/app-event.ts +42 -0
  334. package/app/api/llm-model/catalog/route.ts +12 -0
  335. package/app/api/llm-model/credits/route.ts +11 -0
  336. package/app/api/llm-model/route.ts +26 -0
  337. package/app/api/llm-model/subscription/route.ts +11 -0
  338. package/app/api/memory/edit/route.ts +7 -0
  339. package/app/api/memory/route.ts +21 -0
  340. package/app/api/query-key.ts +159 -0
  341. package/app/api/thursday/tool-call/route.ts +67 -0
  342. package/bin/thursday.mjs +3 -0
  343. package/components/ui/button-group.tsx +45 -0
  344. package/components/ui/custom-icon.tsx +259 -0
  345. package/components/ui/dropdown-menu.tsx +271 -0
  346. package/components/ui/folded-text.tsx +68 -0
  347. package/components/ui/input-group.tsx +157 -0
  348. package/components/ui/json-view.tsx +240 -0
  349. package/components/ui/shiny-text.tsx +55 -0
  350. package/components/ui/text-type.tsx +213 -0
  351. package/components/ui/theme-sync.tsx +9 -0
  352. package/database/migrations/{20260908030742_rainy_marvel_zombies → 20260910173049_numerous_spirit}/migration.sql +30 -33
  353. package/database/migrations/{20260908030742_rainy_marvel_zombies → 20260910173049_numerous_spirit}/snapshot.json +124 -145
  354. package/database/migrations/20260911030320_opposite_jack_power/migration.sql +1 -0
  355. package/database/migrations/20260911030320_opposite_jack_power/snapshot.json +1158 -0
  356. package/docs/how-it-works.md +59 -0
  357. package/docs/images/keeps-going.png +0 -0
  358. package/docs/images/task-thread.png +0 -0
  359. package/docs/images/two-minds.png +0 -0
  360. package/features/ai/chatgpt.ts +617 -0
  361. package/features/ai/components/chatgpt-sign-in.tsx +66 -0
  362. package/features/ai/components/model-picker.tsx +17 -1
  363. package/features/ai/components/provider-icon.tsx +1 -0
  364. package/features/ai/load-tools.ts +105 -39
  365. package/features/ai/model.schema.ts +232 -34
  366. package/features/ai/model.ts +169 -24
  367. package/features/ai/prompts/bot.prompt.ts +245 -59
  368. package/features/ai/prompts/memory-edit.prompt.ts +33 -0
  369. package/features/ai/prompts/prompt-helper.ts +75 -30
  370. package/features/ai/prompts/thursday.prompt.ts +42 -17
  371. package/features/ai/tools/bot.tool.ts +32 -30
  372. package/features/ai/tools/connected.ts +17 -4
  373. package/features/ai/tools/mcp.tool.ts +5 -5
  374. package/features/ai/tools/memory.tool.ts +142 -74
  375. package/features/ai/tools/search.tool.ts +136 -47
  376. package/features/ai/tools/studio.tool.ts +5 -5
  377. package/features/ai/tools/tool-name.ts +2 -3
  378. package/features/ai/tools/workspace.tool.ts +33 -4
  379. package/features/bot/bot.action.ts +24 -7
  380. package/features/bot/bot.memory.ts +89 -0
  381. package/features/bot/bot.query.ts +41 -6
  382. package/features/bot/bot.run.ts +516 -210
  383. package/features/bot/bot.runner.ts +463 -93
  384. package/features/bot/bot.schema.ts +84 -7
  385. package/features/bot/bot.seed.ts +89 -16
  386. package/features/bot/components/bot-mark.tsx +122 -20
  387. package/features/bot/components/bot-room.tsx +519 -391
  388. package/features/bot/components/bot-setting.tsx +832 -341
  389. package/features/bot/components/bot-tool.tsx +2 -6
  390. package/features/bot/components/mark-palette.tsx +54 -0
  391. package/features/bot/components/path-chips.tsx +1 -1
  392. package/features/bot/components/task-badge.tsx +26 -6
  393. package/features/bot/components/task-reply.tsx +22 -33
  394. package/features/bot/components/task-setting.tsx +20 -17
  395. package/features/bot/task.query.ts +233 -33
  396. package/features/bot/task.store.ts +53 -9
  397. package/features/config/components/config-setting.tsx +265 -34
  398. package/features/config/components/models-badge.tsx +42 -0
  399. package/features/config/config.action.ts +12 -0
  400. package/features/config/config.const.ts +38 -2
  401. package/features/connectors/mcp.manager.ts +10 -2
  402. package/features/intro/components/intro.tsx +47 -46
  403. package/features/memory/components/memory-edit.tsx +338 -0
  404. package/features/memory/components/memory-setting.tsx +86 -79
  405. package/features/memory/memory.action.ts +3 -43
  406. package/features/memory/memory.edit.ts +87 -0
  407. package/features/memory/memory.query.ts +33 -44
  408. package/features/memory/memory.schema.ts +50 -97
  409. package/features/settings/components/settings.tsx +2 -0
  410. package/features/settings/settings.alert.ts +3 -1
  411. package/features/skills/components/skills-setting.tsx +9 -1
  412. package/features/thursday/components/ascii-orb.tsx +106 -90
  413. package/features/thursday/components/boot.tsx +7 -0
  414. package/features/thursday/components/call-log.tsx +219 -92
  415. package/features/thursday/components/face.tsx +6 -4
  416. package/features/thursday/components/tab-state.tsx +97 -0
  417. package/features/thursday/components/thursday-mark.tsx +4 -1
  418. package/features/thursday/components/thursday-setting.tsx +17 -47
  419. package/features/thursday/components/thursday.tsx +208 -95
  420. package/features/thursday/thursday.action.ts +12 -17
  421. package/features/thursday/thursday.query.ts +61 -70
  422. package/features/thursday/thursday.schema.ts +20 -7
  423. package/features/thursday/tool-line.ts +85 -11
  424. package/features/thursday/use-thursday.ts +86 -59
  425. package/features/workspace/components/file-view.tsx +4 -2
  426. package/features/workspace/workspace.ts +230 -13
  427. package/hooks/use-draft.ts +37 -0
  428. package/hooks/use-hotkey.ts +105 -0
  429. package/hooks/use-object-state.ts +51 -0
  430. package/hooks/use-on-visible.ts +72 -0
  431. package/hooks/use-theme.ts +106 -0
  432. package/hooks/use-wake-word.ts +262 -0
  433. package/lib/limits.ts +1 -1
  434. package/lib/oauth-page.ts +27 -0
  435. package/lib/realtime/openai.ts +2 -2
  436. package/lib/realtime/realtime.driver.ts +34 -1
  437. package/lib/realtime/realtime.protocol.ts +7 -1
  438. package/lib/realtime/realtime.tap.ts +122 -0
  439. package/lib/sandbox.ts +60 -29
  440. package/lib/tokens.ts +19 -0
  441. package/package.json +1 -1
  442. package/skills/browser/SKILL.md +55 -34
  443. package/.next/server/app/api/memory/tidy/route/app-paths-manifest.json +0 -3
  444. package/.next/server/app/api/memory/tidy/route.js +0 -23
  445. package/.next/server/app/api/memory/tidy/route.js.nft.json +0 -1
  446. package/.next/server/chunks/[root-of-the-server]__019dx9f._.js +0 -5
  447. package/.next/server/chunks/[root-of-the-server]__02_2sm-._.js +0 -5
  448. package/.next/server/chunks/[root-of-the-server]__02kt8yn._.js +0 -10
  449. package/.next/server/chunks/[root-of-the-server]__05luceg._.js +0 -3
  450. package/.next/server/chunks/[root-of-the-server]__06t77t4._.js +0 -11
  451. package/.next/server/chunks/[root-of-the-server]__0bed6hu._.js +0 -8
  452. package/.next/server/chunks/[root-of-the-server]__0dupqt7._.js +0 -5
  453. package/.next/server/chunks/[root-of-the-server]__0h59di-._.js +0 -5
  454. package/.next/server/chunks/[root-of-the-server]__0j_n5l3._.js +0 -3
  455. package/.next/server/chunks/[root-of-the-server]__0le4d-p._.js +0 -3
  456. package/.next/server/chunks/[root-of-the-server]__0m8ywyo._.js +0 -10
  457. package/.next/server/chunks/[root-of-the-server]__0nfuugi._.js +0 -5
  458. package/.next/server/chunks/[root-of-the-server]__0phdgco._.js +0 -8
  459. package/.next/server/chunks/[root-of-the-server]__0qjqtwo._.js +0 -5
  460. package/.next/server/chunks/[root-of-the-server]__0t_dqaj._.js +0 -3
  461. package/.next/server/chunks/[root-of-the-server]__0wvgrsl._.js +0 -3
  462. package/.next/server/chunks/[root-of-the-server]__0yk6s3w._.js +0 -5
  463. package/.next/server/chunks/[root-of-the-server]__0z3gelv._.js +0 -3
  464. package/.next/server/chunks/[root-of-the-server]__10qshki._.js +0 -3
  465. package/.next/server/chunks/[root-of-the-server]__113fqg5._.js +0 -12
  466. package/.next/server/chunks/[root-of-the-server]__11w_7_3._.js +0 -3
  467. package/.next/server/chunks/[root-of-the-server]__16thabu._.js +0 -3
  468. package/.next/server/chunks/[root-of-the-server]__173nhwf._.js +0 -8
  469. package/.next/server/chunks/[root-of-the-server]__1_0q_9w._.js +0 -3
  470. package/.next/server/chunks/[root-of-the-server]__1_aoyr9._.js +0 -3
  471. package/.next/server/chunks/[root-of-the-server]__1bqhuh9._.js +0 -8
  472. package/.next/server/chunks/[root-of-the-server]__1cqizfq._.js +0 -5
  473. package/.next/server/chunks/[root-of-the-server]__1dow0bz._.js +0 -3
  474. package/.next/server/chunks/[root-of-the-server]__1e-ci13._.js +0 -3
  475. package/.next/server/chunks/[root-of-the-server]__1gmn1cs._.js +0 -10
  476. package/.next/server/chunks/[root-of-the-server]__1lr7j9t._.js +0 -5
  477. package/.next/server/chunks/[root-of-the-server]__1mzp_el._.js +0 -3
  478. package/.next/server/chunks/[root-of-the-server]__1txedx6._.js +0 -8
  479. package/.next/server/chunks/[root-of-the-server]__1u_qvc7._.js +0 -3
  480. package/.next/server/chunks/[root-of-the-server]__1zgpz1w._.js +0 -3
  481. package/.next/server/chunks/_012yhfm._.js +0 -39
  482. package/.next/server/chunks/_06mxlgn._.js +0 -3
  483. package/.next/server/chunks/_07rtlxv._.js +0 -3
  484. package/.next/server/chunks/_0a351g7._.js +0 -6
  485. package/.next/server/chunks/_0kjjmag._.js +0 -3
  486. package/.next/server/chunks/_0zmestz._.js +0 -55
  487. package/.next/server/chunks/_0zuzmt_._.js +0 -39
  488. package/.next/server/chunks/_12bbv5i._.js +0 -96
  489. package/.next/server/chunks/_15q1pig._.js +0 -3
  490. package/.next/server/chunks/_1drv2et._.js +0 -36
  491. package/.next/server/chunks/_1ic3bew._.js +0 -5
  492. package/.next/server/chunks/_1mxn_lb._.js +0 -3
  493. package/.next/server/chunks/_1uj4_io._.js +0 -10
  494. package/.next/server/chunks/_1x7p319._.js +0 -6
  495. package/.next/server/chunks/_1yldgo4._.js +0 -55
  496. package/.next/server/chunks/_2050gwn._.js +0 -255
  497. package/.next/server/chunks/_next-internal_server_app_api_memory_tidy_route_actions_0_cfuom.js +0 -3
  498. package/.next/server/chunks/features_0scmr4i._.js +0 -3
  499. package/.next/server/chunks/features_bot_task_query_ts_11xuzl3._.js +0 -3
  500. package/.next/server/chunks/node_modules_consola_dist_chunks_prompt_mjs_0vorg6w._.js +0 -3
  501. package/.next/server/chunks/node_modules_consola_dist_chunks_prompt_mjs_0ykklj0._.js +0 -3
  502. package/.next/server/chunks/node_modules_consola_dist_chunks_prompt_mjs_1ryg3wh._.js +0 -3
  503. package/.next/server/chunks/node_modules_consola_dist_chunks_prompt_mjs_1vmaq9f._.js +0 -3
  504. package/.next/server/chunks/node_modules_consola_dist_chunks_prompt_mjs_1vwbnq_._.js +0 -3
  505. package/.next/server/chunks/ssr/[externals]_node_crypto_1kn79-7._.js +0 -3
  506. package/.next/server/chunks/ssr/[root-of-the-server]__01id-v3._.js +0 -3
  507. package/.next/server/chunks/ssr/[root-of-the-server]__0b-0i32._.js +0 -3
  508. package/.next/server/chunks/ssr/[root-of-the-server]__0dfwvr4._.js +0 -4
  509. package/.next/server/chunks/ssr/[root-of-the-server]__0iuoa-r._.js +0 -3
  510. package/.next/server/chunks/ssr/[root-of-the-server]__1-mnhyd._.js +0 -90
  511. package/.next/server/chunks/ssr/[root-of-the-server]__185grso._.js +0 -3
  512. package/.next/server/chunks/ssr/[root-of-the-server]__1minz8_._.js +0 -3
  513. package/.next/server/chunks/ssr/_0-mc7ro._.js +0 -3
  514. package/.next/server/chunks/ssr/_01p5r6-._.js +0 -3
  515. package/.next/server/chunks/ssr/_09-0k3z._.js +0 -3
  516. package/.next/server/chunks/ssr/_0ay6b01._.js +0 -3
  517. package/.next/server/chunks/ssr/_0cvjbuz._.js +0 -3
  518. package/.next/server/chunks/ssr/_0fafgvl._.js +0 -3
  519. package/.next/server/chunks/ssr/_0j4v8of._.js +0 -3
  520. package/.next/server/chunks/ssr/_0l81bzy._.js +0 -3
  521. package/.next/server/chunks/ssr/_0nso8tp._.js +0 -3
  522. package/.next/server/chunks/ssr/_0sf1isi._.js +0 -3
  523. package/.next/server/chunks/ssr/_0ti51xc._.js +0 -3
  524. package/.next/server/chunks/ssr/_0y84qo6._.js +0 -3
  525. package/.next/server/chunks/ssr/_10uvw3l._.js +0 -3
  526. package/.next/server/chunks/ssr/_1dcxzqe._.js +0 -3
  527. package/.next/server/chunks/ssr/_1e-dgz0._.js +0 -4
  528. package/.next/server/chunks/ssr/_1fq7zgb._.js +0 -3
  529. package/.next/server/chunks/ssr/_1jbv7m1._.js +0 -3
  530. package/.next/server/chunks/ssr/features_bot_components_bot-setting_tsx_01ajft6._.js +0 -4
  531. package/.next/server/chunks/ssr/features_config_components_14jqgy4._.js +0 -3
  532. package/.next/server/chunks/ssr/features_memory_components_memory-setting_tsx_0oa43sw._.js +0 -4
  533. package/.next/server/chunks/ssr/lib_realtime_0aetuyp._.js +0 -24
  534. package/.next/server/chunks/ssr/lib_realtime_18k_aj-._.js +0 -24
  535. package/.next/server/chunks/ssr/node_modules_zod_v4_classic_errors_05xv7vp.js +0 -3
  536. package/.next/server/chunks/ssr/node_modules_zod_v4_classic_external_15fmmz3.js +0 -55
  537. package/.next/static/chunks/0f9sq-7q_yxyl.js +0 -1
  538. package/.next/static/chunks/0tydnn2kybibk.js +0 -1
  539. package/.next/static/chunks/0yysv1gzin17t.js +0 -1
  540. package/.next/static/chunks/1fr5m75pwtyic.js +0 -1
  541. package/.next/static/chunks/1gy3bbv7usnjo.js +0 -1
  542. package/.next/static/chunks/1keib_gt066gc.js +0 -1
  543. package/.next/static/chunks/1ytnip8wdng93.js +0 -1
  544. package/.next/static/chunks/21rk8w7bda-su.js +0 -1
  545. package/.next/static/chunks/247d-r95j8xs0.js +0 -1
  546. package/.next/static/chunks/28g42tw8tsvhd.js +0 -1
  547. package/.next/static/chunks/29gyb3nubj51j.js +0 -53
  548. package/.next/static/chunks/2cwjgkt_hoyhj.js +0 -22
  549. package/.next/static/chunks/2fq_86jvq9ymq.js +0 -2
  550. package/.next/static/chunks/2rbf1fpd8n80-.js +0 -22
  551. package/.next/static/chunks/2ukj8k69y89um.js +0 -1
  552. package/.next/static/chunks/2uxum7qbyeyxd.js +0 -1
  553. package/.next/static/chunks/2wngeetfnzxkp.js +0 -2
  554. package/.next/static/chunks/388-t2v72jh39.css +0 -1
  555. package/.next/static/chunks/3fqvc0cg-atq_.js +0 -1
  556. package/.next/static/chunks/3p4egvfjxpatb.js +0 -1
  557. package/.next/static/chunks/3qe1w1ei-huhl.js +0 -1
  558. package/.next/static/chunks/3xxa2j56-f3nm.js +0 -3
  559. package/.next/static/chunks/3y40riycdip-i.js +0 -1
  560. package/.next/static/chunks/45a6zbtkelph9.js +0 -1
  561. package/.next/static/media/favicon.2vob68tjqpejf.ico +0 -0
  562. package/features/ai/prompts/memory-tidy.prompt.ts +0 -123
  563. package/features/ai/tools/tidy.tool.ts +0 -22
  564. package/features/memory/components/memory-tidy.tsx +0 -395
  565. package/features/memory/memory.tidy.ts +0 -332
  566. package/features/memory/tidy.query.ts +0 -135
  567. package/features/thursday/use-call-title.ts +0 -31
  568. /package/.next/server/app/api/{memory/tidy → bot/memory/files}/route/build-manifest.json +0 -0
  569. /package/.next/server/app/api/{memory/tidy → bot/memory/files}/route/server-reference-manifest.json +0 -0
  570. /package/.next/server/app/api/{memory/tidy → bot/memory/files}/route.js.map +0 -0
  571. /package/.next/static/{X34BUd9bfdH27ripuwwBm → x1cCqY0kWpR_VcYx_lWO5}/_buildManifest.js +0 -0
  572. /package/.next/static/{X34BUd9bfdH27ripuwwBm → x1cCqY0kWpR_VcYx_lWO5}/_clientMiddlewareManifest.js +0 -0
  573. /package/.next/static/{X34BUd9bfdH27ripuwwBm → x1cCqY0kWpR_VcYx_lWO5}/_ssgManifest.js +0 -0
@@ -27,15 +27,15 @@ import { Bubble, BubbleContent } from "@/components/ui/bubble";
27
27
  import { Button } from "@/components/ui/button";
28
28
  import { FoldedText } from "@/components/ui/folded-text";
29
29
  import { Markdown } from "@/components/ui/markdown";
30
- import ShinyText from "@/components/ui/shiny-text";
30
+ import { ShinyText, type ShinyTone } from "@/components/ui/shiny-text";
31
31
  import { Textarea } from "@/components/ui/textarea";
32
32
  import { toast } from "@/components/ui/toast";
33
- import { BOT_RUN } from "@/config";
34
- import { markReportedAction, startTaskAction } from "@/features/bot/bot.action";
33
+ import { startTaskAction } from "@/features/bot/bot.action";
35
34
  import {
36
35
  type Bot,
36
+ type BotIcon,
37
37
  DEFAULT_BOT,
38
- isBudgetAsk,
38
+ isAppStop,
39
39
  TASK_CONTINUE,
40
40
  } from "@/features/bot/bot.schema";
41
41
  import { BotMark } from "@/features/bot/components/bot-mark";
@@ -43,9 +43,9 @@ import { BotRoster } from "@/features/bot/components/bot-roster";
43
43
  import { PathChips } from "@/features/bot/components/path-chips";
44
44
  import { TaskReply, useAnswerTask } from "@/features/bot/components/task-reply";
45
45
  import { openSettings } from "@/features/settings/settings.store";
46
+ import { ThursdayMark } from "@/features/thursday/components/thursday-mark";
46
47
  import { FileViewer } from "@/features/workspace/components/file-view";
47
- import { shortAgo, toDate } from "@/lib/date-like";
48
- import { unwrapResult } from "@/lib/protocol/result";
48
+ import { shortAgo } from "@/lib/date-like";
49
49
  import { useServerAction } from "@/lib/protocol/use-server-action";
50
50
  import { revalidate, useServerRoute } from "@/lib/protocol/use-server-route";
51
51
  import {
@@ -69,6 +69,7 @@ import {
69
69
  type TaskViewStatus,
70
70
  threadItems,
71
71
  useBotTasks,
72
+ useSeenOnDetail,
72
73
  } from "../task.store";
73
74
  import { BotTool } from "./bot-tool";
74
75
 
@@ -78,10 +79,11 @@ import { BotTool } from "./bot-tool";
78
79
  * state.
79
80
  *
80
81
  * Folded, it is not a badge you have to open. Anything waiting on an answer is drawn
81
- * on the chip itself, with its buttons, and anything that merely happened borrows the
82
- * chip's one sentence for three seconds (useBeats). Opening the room is for reading a
83
- * thread, never for answering — a chevron that reveals the rows would put a click in
84
- * front of the one thing this corner exists to remove.
82
+ * on the chip itself, with its buttons; what a bot is doing right now sits beside its
83
+ * face, and what passed between two parties rides above one for a moment
84
+ * (useHandoffs). Opening the room is for reading a thread, never for answering — a
85
+ * chevron that reveals the rows would put a click in front of the one thing this
86
+ * corner exists to remove.
85
87
  *
86
88
  * memo: the parent re-renders per transcript chunk and this reads only its store.
87
89
  */
@@ -99,12 +101,13 @@ export const BotRoom = memo(function BotRoom() {
99
101
  ? (newest.find((entry) => entry.id === picked) ?? null)
100
102
  : null;
101
103
 
102
- const [beat, announce] = useBeats();
104
+ const [bubbles, handoff] = useHandoffs();
105
+ const { crew, more } = useMemo(() => crewOf(bots, tasks), [bots, tasks]);
103
106
 
104
- // What each task was, and what each bot last said, at the previous sync. Only
105
- // changes since then are announced.
107
+ // What each task was, and which hand-offs had already landed, at the previous
108
+ // sync. Only what changed since then just happened.
106
109
  const known = useRef<Map<string, TaskViewStatus> | null>(null);
107
- const said = useRef(new Map<string, string>());
110
+ const passed = useRef(new Set<string>());
108
111
 
109
112
  useEffect(() => {
110
113
  if (!botTasks.primed()) return;
@@ -112,89 +115,85 @@ export const BotRoom = memo(function BotRoom() {
112
115
  const was = known.current;
113
116
  known.current = now;
114
117
 
115
- const lines = new Map<string, string>();
116
- for (const entry of latestPerBot(tasks)) {
117
- if (entry.line) lines.set(entry.bot.name, entry.line.id);
118
+ const seen = new Set<string>();
119
+ for (const task of tasks) {
120
+ for (const line of task.lines) {
121
+ if (line.kind === "ask" && line.to) seen.add(line.id);
122
+ }
118
123
  }
119
- const heard = said.current;
120
- said.current = lines;
124
+ const had = passed.current;
125
+ passed.current = seen;
121
126
 
122
- // The first list only seeds the two maps: nothing on it just happened.
127
+ // The first list only seeds the two: nothing on it just happened.
123
128
  if (!was) return;
124
129
 
125
- // A bot holds one seat in the queue, so the last push for a bot wins. Steps
126
- // go first and a change of status overwrites them: that a job stopped to ask
127
- // outranks whatever tool it called on the way there.
128
- for (const entry of latestPerBot(tasks)) {
129
- if (entry.task.status !== "working") continue;
130
- const at = entry.line?.id;
131
- if (!at || heard.get(entry.bot.name) === at) continue;
132
- announce({
133
- bot: entry.bot.name,
134
- label: entry.task.label,
135
- what: secondLine(entry.task).text,
136
- live: true,
130
+ // Bot to bot. A giver handing work to several at once would put a bubble
131
+ // over each of them, 20px apart, so a whole round is drawn once — over the
132
+ // giver — and the faces that took the work are awake, which says who.
133
+ const rounds = new Map<
134
+ string,
135
+ { from: BotRef; to: BotRef; text: string }[]
136
+ >();
137
+ for (const task of tasks) {
138
+ for (const line of task.lines) {
139
+ if (line.kind !== "ask" || !line.to || had.has(line.id)) continue;
140
+ const round = rounds.get(line.bot.name) ?? [];
141
+ round.push({ from: line.bot, to: line.to, text: line.text });
142
+ rounds.set(line.bot.name, round);
143
+ }
144
+ }
145
+ for (const [giver, round] of rounds) {
146
+ if (round.length === 1) {
147
+ handoff({
148
+ at: round[0].to.name,
149
+ from: round[0].from,
150
+ text: clipWord(round[0].text),
151
+ });
152
+ continue;
153
+ }
154
+ handoff({
155
+ at: giver,
156
+ from: round[0].from,
157
+ text: `sent ${round.length} parts out`,
137
158
  });
138
159
  }
139
160
 
140
161
  for (const task of newest) {
141
- const before = was.get(task.id);
142
- if (before === task.status) continue;
143
- // First seen already finished — polling skipped the whole run. Say it only
144
- // if nobody has heard it yet.
145
- if (before === undefined && task.status !== "working" && task.relayed) {
162
+ if (was.get(task.id) === task.status || task.status !== "working")
146
163
  continue;
147
- }
148
- announce({
149
- bot: task.bot.name,
150
- label: task.label,
151
- ...beatFor(task, before === undefined),
152
- });
164
+ // A job that was never seen is one arriving; one that was is you having
165
+ // answered it. The mark says who gave it away — and a job typed into the
166
+ // compose box is Thursday's here too, because a task row does not record
167
+ // which of the two started it.
168
+ handoff(
169
+ was.has(task.id)
170
+ ? { at: task.bot.name, from: null, text: "picked it back up" }
171
+ : {
172
+ at: task.bot.name,
173
+ from: THURSDAY,
174
+ text: `took on \u201c${clipWord(task.label)}\u201d`,
175
+ },
176
+ );
153
177
  }
154
- }, [tasks, announce]);
178
+ }, [tasks, handoff]);
155
179
 
156
- // Opening a finished thread marks it reported. The key includes updatedAt: a
157
- // follow-up request finishes again and that result is unread. markReported does not
158
- // move `updatedAt`, so the set only bridges the gap until the next poll.
159
- const marked = useRef(new Set<string>());
160
- useEffect(() => {
161
- if (!current || current.relayed) return;
162
- if (current.status !== "done" && current.status !== "failed") return;
163
- const key = `${current.id}@${toDate(current.updatedAt).getTime()}`;
164
- if (marked.current.has(key)) return;
165
- marked.current.add(key);
166
- // unwrapResult folds Result failures and rejections into one path; on failure
167
- // release the key so the task can be marked again.
168
- void markReportedAction([current.id])
169
- .then(unwrapResult)
170
- .then(() => revalidate(queryKey.tasks))
171
- .catch((cause) => {
172
- marked.current.delete(key);
173
- toast.add({
174
- type: "error",
175
- title: "Could not mark the task as read",
176
- description: errorToString(cause),
177
- });
178
- });
179
- }, [current]);
180
+ // Reading a thread is reading its ending; that is what clears its dot.
181
+ useSeenOnDetail(open ? current : null);
180
182
 
181
183
  const busy = tasks.filter((entry) => entry.status === "working").length;
182
184
  const attention = newest.filter(needsYou);
183
185
  const pending = attention.length;
184
- // Bots with a task waiting on the user; the chip shows them in front.
185
- const waiting = attention
186
- .map((entry) => entry.bot)
187
- .filter(
188
- (bot, at, all) =>
189
- all.findIndex((other) => other.name === bot.name) === at,
190
- );
186
+ const unread = newest.filter(isUnread);
187
+ const failed = unread.filter((entry) => entry.status === "failed").length;
191
188
 
192
189
  const closeCompose = useCallback(() => setComposing(false), []);
193
190
 
194
191
  return (
195
- <div className="pointer-events-none absolute right-5 bottom-5 z-10 flex w-132 max-w-[calc(100vw-2.5rem)] flex-col items-end gap-2">
192
+ // As wide as the resting pill may grow: 80% of the window. The open room
193
+ // keeps its own 33rem inside it.
194
+ <div className="pointer-events-none absolute right-5 bottom-5 z-10 flex w-[min(80vw,calc(100vw-2.5rem))] flex-col items-end gap-2">
196
195
  {open ? (
197
- <div className="pointer-events-auto flex max-h-[min(44rem,78vh)] w-full animate-in flex-col overflow-hidden rounded-3xl bg-background/75 shadow-2xl shadow-black/6 ring-1 ring-border/50 backdrop-blur-xl fade-in slide-in-from-bottom-1 duration-200">
196
+ <div className="pointer-events-auto flex max-h-[min(44rem,78vh)] w-132 max-w-full animate-in flex-col overflow-hidden rounded-3xl bg-background/75 shadow-2xl shadow-black/6 ring-1 ring-border/50 backdrop-blur-xl fade-in slide-in-from-bottom-1 duration-200">
198
197
  {current ? (
199
198
  <>
200
199
  <ThreadHeader
@@ -241,13 +240,16 @@ export const BotRoom = memo(function BotRoom() {
241
240
  </div>
242
241
  ) : (
243
242
  <Chip
243
+ crew={crew}
244
+ more={more}
245
+ bubbles={bubbles}
244
246
  bots={bots}
245
247
  rows={attention}
246
- waiting={waiting}
247
248
  count={tasks.length}
248
249
  busy={busy}
249
250
  pending={pending}
250
- beat={beat}
251
+ unread={unread.length}
252
+ failed={failed}
251
253
  composing={composing}
252
254
  onCompose={() => setComposing(true)}
253
255
  onCloseCompose={closeCompose}
@@ -256,8 +258,10 @@ export const BotRoom = memo(function BotRoom() {
256
258
  setOpen(true);
257
259
  }}
258
260
  onOpen={() => {
259
- // A single waiting task opens straight into its thread.
260
- if (!picked && attention.length === 1) setPicked(attention[0].id);
261
+ // One thing to look at opens straight into its thread: a single
262
+ // question, or with none waiting, a single answer nobody has read.
263
+ const only = attention.length ? attention : unread;
264
+ if (!picked && only.length === 1) setPicked(only[0].id);
261
265
  setOpen(true);
262
266
  }}
263
267
  />
@@ -266,106 +270,163 @@ export const BotRoom = memo(function BotRoom() {
266
270
  );
267
271
  });
268
272
 
269
- /** How long one announcement holds the chip's sentence, ms. */
270
- const BEAT_MS = 3000;
271
- /** The sentence goes back to rest between two of them, so they never blur into one. */
272
- const BEAT_GAP_MS = 260;
273
+ /** Faces the row draws before the count takes over. */
274
+ const CREW_MAX = 10;
275
+ /** Stable identity for a row that can never have one up. */
276
+ const EMPTY_BUBBLES: Map<string, Handoff> = new Map();
277
+ /** How long a hand-off stays above a face, ms. */
278
+ const HANDOFF_MS = 3400;
279
+ /** A step is a glance, not a sentence. */
280
+ const WORD_MAX = 28;
281
+
282
+ /** One face in the row, and what it is doing. */
283
+ type CrewFace = {
284
+ name: string;
285
+ icon?: BotIcon | null;
286
+ /** Working or waiting: both are awake. */
287
+ awake: boolean;
288
+ /** Waiting on an answer; carries the dot. */
289
+ waiting: boolean;
290
+ /** The step it is on, in the model's own words. Null when it is not working. */
291
+ word: string | null;
292
+ /** A stand-in for an install with no bots; dimmed with the rest. */
293
+ standIn?: boolean;
294
+ };
273
295
 
274
296
  /**
275
- * What the chip is saying for a moment: who moved, on what, and what they did.
276
- * `live` is the difference between a step and an ending — a step is still going
277
- * while the sentence is up, so it shines; "finished" does not.
297
+ * Something passed between two parties. This is what a bubble is for, and the
298
+ * only thing: a step is the bot working alone and belongs beside its face.
278
299
  */
279
- type Beat = {
280
- bot: string;
281
- label: string;
282
- what: string;
283
- tone?: string;
284
- live?: boolean;
300
+ type Handoff = {
301
+ /** Whose face it points at. */
302
+ at: string;
303
+ /** Who gave it away. Null when it was the user, who has no mark. */
304
+ from: BotRef | null;
305
+ text: string;
306
+ };
307
+
308
+ const clipWord = (text: string) => {
309
+ const one = plainText(text).replace(/\s+/g, " ").trim();
310
+ return one.length > WORD_MAX ? `${one.slice(0, WORD_MAX - 1)}…` : one;
285
311
  };
286
312
 
287
313
  /**
288
- * The one queue anything announces itself through.
289
- *
290
- * A beat holds the sentence for BEAT_MS and they play one at a time, so a busy
291
- * minute reads as a list rather than a flicker. **A bot holds one seat**: a second
292
- * beat from the same bot overwrites its place instead of joining the back of the
293
- * queue, so three bots stepping at once still take turns and the newest thing a
294
- * given bot did is the one you see.
314
+ * Hand-offs currently up, one per face. A second one to the same face replaces
315
+ * it rather than queueing: the newest thing that landed there is the true one.
316
+ * Different faces hold their own at the same time — the fan-out that would make
317
+ * two of them collide is folded into one before it gets here (BotRoom).
295
318
  */
296
- function useBeats() {
297
- const [now, setNow] = useState<Beat | null>(null);
298
- const queue = useRef<Beat[]>([]);
299
- const holding = useRef(false);
300
- const timers = useRef<ReturnType<typeof setTimeout>[]>([]);
319
+ function useHandoffs(): [Map<string, Handoff>, (one: Handoff) => void] {
320
+ const [up, setUp] = useState<Map<string, Handoff>>(new Map());
321
+ const timers = useRef(new Map<string, ReturnType<typeof setTimeout>>());
301
322
 
302
323
  useEffect(
303
324
  () => () => {
304
- for (const timer of timers.current) clearTimeout(timer);
325
+ for (const timer of timers.current.values()) clearTimeout(timer);
305
326
  },
306
327
  [],
307
328
  );
308
329
 
309
- // Held in a ref because it calls itself to drain the queue.
310
- const pump = useRef(() => {});
311
- pump.current = () => {
312
- if (holding.current) return;
313
- const next = queue.current.shift();
314
- if (!next) return;
315
- holding.current = true;
316
- setNow(next);
317
- timers.current.push(
330
+ const show = useCallback((one: Handoff) => {
331
+ setUp((was) => new Map(was).set(one.at, one));
332
+ const held = timers.current.get(one.at);
333
+ if (held) clearTimeout(held);
334
+ timers.current.set(
335
+ one.at,
318
336
  setTimeout(() => {
319
- setNow(null);
320
- timers.current.push(
321
- setTimeout(() => {
322
- holding.current = false;
323
- pump.current();
324
- }, BEAT_GAP_MS),
325
- );
326
- }, BEAT_MS),
337
+ timers.current.delete(one.at);
338
+ setUp((was) => {
339
+ const next = new Map(was);
340
+ next.delete(one.at);
341
+ return next;
342
+ });
343
+ }, HANDOFF_MS),
327
344
  );
328
- };
329
-
330
- const announce = useCallback((beat: Beat) => {
331
- const at = queue.current.findIndex((one) => one.bot === beat.bot);
332
- if (at >= 0) queue.current[at] = beat;
333
- else queue.current.push(beat);
334
- pump.current();
335
345
  }, []);
336
346
 
337
- return [now, announce] as const;
347
+ return [up, show];
348
+ }
349
+
350
+ /** The step a bot is on, as the model labelled it. */
351
+ function wordOf(line: Chatter | null): string | null {
352
+ if (!line) return null;
353
+ if (line.kind === "tool" && line.tool) {
354
+ // The model's own label when it wrote one, else the raw call.
355
+ return clipWord(line.tool.note ?? `${line.tool.name} · ${line.tool.input}`);
356
+ }
357
+ return line.kind === "say" ? clipWord(line.text) : null;
338
358
  }
339
359
 
340
360
  /**
341
- * What a change of status says. Budget stops are worded apart from questions so a
342
- * job that is really blocked is not buried among them (bot.schema isBudgetAsk).
361
+ * Who is in the row, in the order it is drawn.
362
+ *
363
+ * Anyone moving sorts to the front, so the count at the tail only ever hides
364
+ * idle bots — a bot with something to say always has a face to say it from,
365
+ * which is what lets a hand-off point at one. A bot that spoke inside somebody
366
+ * else's job is in the room too, whether or not it owns a task (latestPerBot).
343
367
  */
344
- function beatFor(
345
- task: TaskView,
346
- first: boolean,
347
- ): Pick<Beat, "what" | "tone" | "live"> {
348
- const who = task.bot.name;
349
- if (task.status === "working") {
350
- return {
351
- what: first ? `${who} took this on` : `${who} picked it back up`,
352
- live: true,
353
- };
368
+ function crewOf(
369
+ bots: Bot[] | undefined,
370
+ tasks: TaskView[],
371
+ ): { crew: CrewFace[]; more: number } {
372
+ const live = new Map<string, { waiting: boolean; word: string | null }>();
373
+ for (const entry of latestPerBot(tasks)) {
374
+ const { task, bot, line } = entry;
375
+ // Waiting outranks working: a bot that stopped to ask is not on a step.
376
+ if (task.status === "waiting" && task.bot.name === bot.name) {
377
+ live.set(bot.name, { waiting: true, word: null });
378
+ continue;
379
+ }
380
+ if (task.status !== "working" || live.get(bot.name)?.waiting) continue;
381
+ live.set(bot.name, { waiting: false, word: wordOf(line) });
354
382
  }
355
- if (task.status === "waiting") {
356
- return isBudgetAsk(task.ask)
357
- ? { what: `${who} is out of steps` }
358
- : {
359
- what: `${who} is asking`,
360
- tone: WAITING_INK,
361
- };
383
+
384
+ const named = new Map<string, CrewFace>();
385
+ for (const bot of bots ?? []) {
386
+ named.set(bot.name, {
387
+ name: bot.name,
388
+ icon: bot.icon,
389
+ awake: live.has(bot.name),
390
+ waiting: live.get(bot.name)?.waiting ?? false,
391
+ word: live.get(bot.name)?.word ?? null,
392
+ });
393
+ }
394
+ // A bot that is working but not on the roster (the row-less default) still
395
+ // has a face: the row is who is here, not who is configured.
396
+ for (const [name, doing] of live) {
397
+ if (named.has(name)) continue;
398
+ named.set(name, {
399
+ name,
400
+ icon: null,
401
+ awake: true,
402
+ waiting: doing.waiting,
403
+ word: doing.word,
404
+ });
405
+ }
406
+
407
+ // A fresh install has one worker and one silhouette says "one bot", which is
408
+ // the wrong thing to say about a room. Stand-ins fill it out and are dimmed.
409
+ const roster = [...named.values()];
410
+ if (!roster.length) return { crew: GHOSTS.slice(0, FLOOR), more: 0 };
411
+ for (const ghost of GHOSTS) {
412
+ if (roster.length >= FLOOR) break;
413
+ if (!named.has(ghost.name)) roster.push(ghost);
362
414
  }
363
- return task.status === "failed"
364
- ? { what: `${who} could not finish`, tone: "text-destructive" }
365
- : { what: `${who} finished` };
415
+
416
+ const rank = (face: CrewFace) =>
417
+ face.waiting ? 0 : face.awake ? 1 : face.standIn ? 3 : 2;
418
+ const sorted = roster
419
+ .map((face, at) => ({ face, at }))
420
+ .sort((a, b) => rank(a.face) - rank(b.face) || a.at - b.at)
421
+ .map((one) => one.face);
422
+
423
+ return {
424
+ crew: sorted.slice(0, CREW_MAX),
425
+ more: Math.max(0, sorted.length - CREW_MAX),
426
+ };
366
427
  }
367
428
 
368
- /** Blanks the question in the reply box when it already is the last thread line (budget stops). */
429
+ /** Blanks the question in the reply box when it already is the last thread line (stops the app made). */
369
430
  function askFor(task: TaskView): TaskView["ask"] {
370
431
  if (!task.ask?.question) return task.ask;
371
432
  const last = task.lines.at(-1);
@@ -375,30 +436,59 @@ function askFor(task: TaskView): TaskView["ask"] {
375
436
  }
376
437
 
377
438
  /**
378
- * A task that cannot move until the user answers: a question, or a budget stop.
439
+ * A task that cannot move until the user answers: a question, or a stop the app made.
379
440
  *
380
441
  * A finished job is not on this list. It has nothing for the user to do, and
381
- * counting it here made the chip hold itself open over a report nobody had to
382
- * act on. A report says itself once, in the sentence, and then it is a row in
442
+ * counting it here made the chip hold itself open over an answer nobody had to
443
+ * act on. An answer says itself once, in the sentence, and then it is a row in
383
444
  * the room like every other one.
384
445
  */
385
446
  const needsYou = (task: TaskView) => task.status === "waiting";
386
447
 
387
- /** What the chip says when nothing just happened, most urgent first. */
448
+ /** An ending nobody has opened. It needs the user too, to read rather than to answer. */
449
+ const isUnread = (task: TaskView) =>
450
+ (task.status === "done" || task.status === "failed") && !task.seen;
451
+
452
+ /**
453
+ * What the room itself is doing, and nothing else — the right side of the pill.
454
+ *
455
+ * What is still moving shines, like every other running line: a wait in amber,
456
+ * running jobs in muted ink beside the spinner. An ending does not — an answer
457
+ * nobody has opened is something to read, not something happening.
458
+ */
388
459
  function restingState({
389
460
  count,
390
461
  busy,
391
462
  pending,
463
+ unread,
464
+ failed,
392
465
  }: {
393
466
  count: number;
394
467
  busy: number;
395
468
  pending: number;
396
- }): { text: string; tone?: string; shine?: "amber" | "muted" } {
469
+ /** Endings nobody has opened, failures included. */
470
+ unread: number;
471
+ failed: number;
472
+ }): { text: string; tone: string; shine: ShinyTone | null } {
397
473
  if (pending > 0)
398
474
  return {
399
475
  text: pending === 1 ? "waiting on you" : `${pending} waiting on you`,
400
476
  tone: WAITING_INK,
401
- shine: "amber",
477
+ shine: "waiting",
478
+ };
479
+ // Unread outranks running: a job still going will say so again, and an answer
480
+ // left unopened will not.
481
+ if (failed > 0)
482
+ return {
483
+ text: failed === 1 ? "1 failed" : `${failed} failed`,
484
+ tone: "text-destructive",
485
+ shine: null,
486
+ };
487
+ if (unread > 0)
488
+ return {
489
+ text: unread === 1 ? "1 new answer" : `${unread} new answers`,
490
+ tone: WAITING_INK,
491
+ shine: null,
402
492
  };
403
493
  if (busy > 0)
404
494
  return {
@@ -406,67 +496,83 @@ function restingState({
406
496
  tone: "text-muted-foreground",
407
497
  shine: "muted",
408
498
  };
409
- if (count > 0) return { text: "all done", tone: "text-muted-foreground" };
410
- return { text: "no jobs yet", tone: "text-muted-foreground" };
499
+ if (count > 0)
500
+ return { text: "all done", tone: "text-muted-foreground", shine: null };
501
+ return { text: "no jobs yet", tone: "text-muted-foreground", shine: null };
411
502
  }
412
503
 
413
504
  /**
414
505
  * The room folded into one object in the corner.
415
506
  *
416
- * Two parts, and they are one object: a pill that always says what is true right
417
- * now, and — while something is actually waiting on an answer — the rows for it,
418
- * grown in place above the pill. Nothing here sits behind a disclosure. The rows
419
- * appear because there is something to answer and leave when it is answered; a
420
- * chevron that revealed them would put a click in front of the one thing this
421
- * corner exists to remove. The pill's own click opens the room, to read.
507
+ * The row carries two facts and they never take each other's place. On the
508
+ * **left**, who is here and what each of them is doing: a face, and the shiny
509
+ * text beside it. On the **right**, what the room itself is doing, with its
510
+ * glyph 6px away — one fact, so nothing gets to come between them and nothing
511
+ * takes the glyph away. Anything that passes between two parties is neither, so
512
+ * it rides above a face in a bubble.
513
+ *
514
+ * While something is waiting on an answer its rows are grown in place above the
515
+ * row. Nothing here sits behind a disclosure: they appear because there is
516
+ * something to answer and leave when it is answered. The pill's own click opens
517
+ * the room, to read.
422
518
  *
423
519
  * The corner radius does not animate with the height: interpolating a pill radius
424
520
  * down to a card radius while the box is also resizing warps the corners in flight.
425
521
  */
426
522
  function Chip({
523
+ crew,
524
+ more,
525
+ bubbles,
427
526
  bots,
428
527
  rows,
429
- waiting,
430
528
  count,
431
529
  busy,
432
530
  pending,
433
- beat,
531
+ unread,
532
+ failed,
434
533
  composing,
435
534
  onCompose,
436
535
  onCloseCompose,
437
536
  onPick,
438
537
  onOpen,
439
538
  }: {
539
+ crew: CrewFace[];
540
+ more: number;
541
+ bubbles: Map<string, Handoff>;
440
542
  bots?: Bot[];
441
543
  /** Everything waiting on an answer, newest first. */
442
544
  rows: TaskView[];
443
- waiting: BotRef[];
444
545
  count: number;
445
546
  busy: number;
446
547
  pending: number;
447
- beat: Beat | null;
548
+ unread: number;
549
+ failed: number;
448
550
  composing: boolean;
449
551
  onCompose: () => void;
450
552
  onCloseCompose: () => void;
451
553
  onPick: (id: string) => void;
452
554
  onOpen: () => void;
453
555
  }) {
454
- const state = restingState({ count, busy, pending });
556
+ const state = restingState({ count, busy, pending, unread, failed });
455
557
  const grown = composing || pending > 0;
456
558
 
457
559
  return (
458
560
  <div
459
561
  className={cn(
460
- "pointer-events-auto w-fit max-w-full overflow-hidden bg-background/78 ring-1 ring-border/50 backdrop-blur-md transition-shadow duration-300",
562
+ // Not `overflow-hidden`: a hand-off bubble stands above the row, outside
563
+ // this box. The growing part clips itself instead.
564
+ "pointer-events-auto w-fit max-w-full bg-background/78 ring-1 ring-border/50 backdrop-blur-md transition-shadow duration-300",
565
+ // A card with questions in it keeps the room's width: only the resting
566
+ // row grows with the corner, and a reply's long lines would stretch it.
461
567
  grown
462
- ? "min-w-96 rounded-3xl shadow-lg shadow-black/8"
568
+ ? "max-w-[min(33rem,100%)] min-w-96 rounded-3xl shadow-lg shadow-black/8"
463
569
  : "rounded-full shadow-sm shadow-black/3",
464
570
  )}
465
571
  >
466
572
  {/* One box, two heights: the rows grow out of nothing rather than appearing. */}
467
573
  <div
468
574
  className={cn(
469
- "grid transition-[grid-template-rows] duration-300 ease-out",
575
+ "grid overflow-hidden transition-[grid-template-rows] duration-300 ease-out",
470
576
  grown ? "grid-rows-[1fr]" : "grid-rows-[0fr]",
471
577
  )}
472
578
  >
@@ -501,79 +607,49 @@ function Chip({
501
607
  {/* The pill row. Same geometry either way, so the card shrinks into it. */}
502
608
  {/* px-3 is the chip's one rail: the faces here, the section line and every
503
609
  row's mark all start at 12px, and the trailing glyph ends at 12px — which
504
- is why a resting glyph carries no box. A box would centre it and leave
505
- its ink 5px short of the rail the faces start on. Rows reach the same rail
506
- as their own 8px inside a 4px list. */}
507
- <div className={cn("flex items-center gap-2 px-3 py-1.5")}>
610
+ is why the well carries no box of its own. */}
611
+ <div className="flex items-center gap-2 px-3 py-1.5">
508
612
  <button
509
613
  type="button"
510
614
  onClick={onOpen}
511
615
  aria-label={count ? `Tasks (${count})` : "Bots"}
512
616
  className="flex min-w-0 flex-1 items-center gap-2 rounded-full text-left outline-none focus-visible:ring-3 focus-visible:ring-ring/50"
513
617
  >
514
- <Faces bots={bots} waiting={waiting} only={beat?.bot} />
515
-
516
- {/* One slot, two things in it: what is true, and what just happened. */}
517
- <span className="flex h-7 min-w-0 flex-1 items-center">
518
- <Lane on={!!beat}>
519
- <span className="flex items-center gap-1.5 whitespace-nowrap">
520
- <span className="shrink-0 font-mono text-[11px] text-muted-foreground/70">
521
- {beat?.label}
522
- </span>
523
- {beat?.live ? (
524
- <ShinyText
525
- text={beat.what}
526
- speed={2.4}
527
- color="var(--muted-foreground)"
528
- shineColor="var(--foreground)"
529
- className="min-w-0 truncate text-[14px] leading-5 tracking-[-0.15px]"
530
- />
531
- ) : (
532
- <span
533
- className={cn(
534
- "min-w-0 truncate text-[14px] leading-5 tracking-[-0.15px]",
535
- beat?.tone ?? "text-foreground",
536
- )}
537
- >
538
- {beat?.what}
539
- </span>
618
+ <Crew crew={crew} more={more} bubbles={bubbles} />
619
+
620
+ {/* The room's own state and its glyph: one group, and the only thing on
621
+ the right. `ml-auto` keeps it there when the crew says nothing. */}
622
+ <span className="ml-auto flex h-7 shrink-0 items-center gap-1.5">
623
+ {state.shine ? (
624
+ <ShinyText
625
+ text={state.text}
626
+ tone={state.shine}
627
+ speed={2.6}
628
+ className="block truncate text-[14px] leading-5 tracking-[-0.15px]"
629
+ />
630
+ ) : (
631
+ <span
632
+ key={state.text}
633
+ className={cn(
634
+ "block animate-in truncate text-[14px] leading-5 tracking-[-0.15px] fade-in duration-300",
635
+ state.tone,
540
636
  )}
637
+ >
638
+ {state.text}
541
639
  </span>
542
- </Lane>
543
- <Lane on={!beat}>
544
- {state.shine ? (
545
- // Neither waiting nor running is a state at rest: one is held open
546
- // towards you, the other is moving. Slowly lit text says both better
547
- // than a glyph does. The colours are classes because they differ per
548
- // theme, and ShinyText reads the variables they set.
549
- <span
550
- className={cn(
551
- "block",
552
- state.shine === "amber"
553
- ? "[--rest:var(--color-amber-700)] [--shine:var(--color-amber-400)] dark:[--rest:var(--color-amber-400)] dark:[--shine:var(--color-amber-100)]"
554
- : "[--rest:var(--muted-foreground)] [--shine:var(--foreground)]",
555
- )}
556
- >
557
- <ShinyText
558
- text={state.text}
559
- speed={2.6}
560
- color="var(--rest)"
561
- shineColor="var(--shine)"
562
- className="truncate text-[14px] leading-5 tracking-[-0.15px]"
563
- />
564
- </span>
565
- ) : (
566
- <span
567
- key={state.text}
568
- className={cn(
569
- "block animate-in truncate text-[14px] leading-5 tracking-[-0.15px] fade-in duration-300",
570
- state.tone,
571
- )}
572
- >
573
- {state.text}
574
- </span>
640
+ )}
641
+ {/* Always 16px, empty or not: a glyph that comes and goes moves the
642
+ sentence's right end even when the sentence has not changed. */}
643
+ <span className="grid size-4 shrink-0 place-items-center">
644
+ {pending === 0 && unread === 0 && busy > 0 && (
645
+ <Loader2 className="size-4 animate-spin text-muted-foreground/70" />
575
646
  )}
576
- </Lane>
647
+ {pending === 0 &&
648
+ failed === 0 &&
649
+ (unread > 0 || (busy === 0 && count > 0)) && (
650
+ <Check className="size-4 text-muted-foreground/60" />
651
+ )}
652
+ </span>
577
653
  </span>
578
654
  </button>
579
655
 
@@ -582,36 +658,11 @@ function Chip({
582
658
  <X className="size-3.5" />
583
659
  </RoundButton>
584
660
  )}
585
- {!composing && pending === 0 && busy > 0 && (
586
- <Loader2 className="size-4 shrink-0 animate-spin text-muted-foreground/70" />
587
- )}
588
- {!composing && pending === 0 && busy === 0 && count > 0 && (
589
- <Check className="size-4 shrink-0 text-muted-foreground/60" />
590
- )}
591
661
  </div>
592
662
  </div>
593
663
  );
594
664
  }
595
665
 
596
- /**
597
- * Two things sharing one line: the one that is on takes the width and the other
598
- * folds to nothing. Both stay mounted, so the swap is a slide and not a jump.
599
- */
600
- function Lane({ on, children }: { on: boolean; children: ReactNode }) {
601
- return (
602
- <span
603
- className={cn(
604
- // The width is the only thing that moves, and it moves fast: at half a
605
- // second the letters arrive one by one and it reads as typing.
606
- "grid min-w-0 transition-[grid-template-columns,opacity] duration-200 ease-out",
607
- on ? "grid-cols-[1fr] opacity-100" : "grid-cols-[0fr] opacity-0",
608
- )}
609
- >
610
- <span className="min-w-0 overflow-hidden">{children}</span>
611
- </span>
612
- );
613
- }
614
-
615
666
  /** Starts a job. Lives in the header of whichever list is on screen. */
616
667
  function ComposeButton({ onClick }: { onClick: () => void }) {
617
668
  return (
@@ -661,8 +712,13 @@ function Compose({ bots, onDone }: { bots?: Bot[]; onDone: () => void }) {
661
712
  });
662
713
 
663
714
  // A fresh install has no rows and still has a worker (bot.schema DEFAULT_BOT).
715
+ // Switched-off bots are left out for the same reason no model is shown one —
716
+ // but the fallback answers "no rows", not "every row off", so switching them
717
+ // all off leaves nothing to pick rather than conjuring a worker.
664
718
  const roster: BotRef[] = bots?.length
665
- ? bots.map((bot) => ({ name: bot.name, icon: bot.icon }))
719
+ ? bots
720
+ .filter((bot) => !bot.disabled)
721
+ .map((bot) => ({ name: bot.name, icon: bot.icon }))
666
722
  : [{ name: DEFAULT_BOT.name, icon: DEFAULT_BOT.icon }];
667
723
 
668
724
  const send = () => {
@@ -775,94 +831,159 @@ function Compose({ bots, onDone }: { bots?: Bot[]; onDone: () => void }) {
775
831
  }
776
832
 
777
833
  /** Stand-in faces for an install with no bots. Fixed, not random: this renders on the server too and Math.random would break hydration. */
778
- const GHOSTS = ["alto", "brio", "cinder", "delta"].map((seed, index) => ({
779
- key: seed,
780
- seed,
781
- color: MARK_PALETTE[Math.floor((index * MARK_PALETTE.length) / 4)],
782
- shape: MARK_SHAPES[index % MARK_SHAPES.length],
783
- outline: undefined as boolean | undefined,
784
- }));
834
+ const GHOSTS: CrewFace[] = ["alto", "brio", "cinder", "delta"].map(
835
+ (seed, index) => ({
836
+ name: seed,
837
+ icon: {
838
+ color: MARK_PALETTE[Math.floor((index * MARK_PALETTE.length) / 4)],
839
+ shape: MARK_SHAPES[index % MARK_SHAPES.length],
840
+ },
841
+ awake: false,
842
+ waiting: false,
843
+ word: null,
844
+ standIn: true,
845
+ }),
846
+ );
785
847
 
786
848
  /** A single face reads as one worker, not as a crew; below this the row is padded out. */
787
- const CREW = 3;
849
+ const FLOOR = 3;
788
850
 
789
- /** The crew stacked behind each other; GHOSTS stand in when there are no bots. */
790
- function Faces({
791
- bots,
792
- waiting = [],
793
- only,
851
+ /** The awake halo, in the mark's own view-box units: about 2px of blur at 28px. */
852
+ const AWAKE_GLOW = 20;
853
+
854
+ /**
855
+ * The crew, and what each of them is doing.
856
+ *
857
+ * Two things share this row and never take each other's place: a face says
858
+ * **who**, and the shiny text beside it says **what**, for as long as that bot
859
+ * is on that step. Both are derived — there is no queue and no cap, because
860
+ * every bot that is working is saying something true at the same time.
861
+ *
862
+ * A face that is moving is awake: lifted, swollen a little, and glowing in its
863
+ * own colour. That layer alone survives every collision — three bots at once
864
+ * are three awake faces, with no order to decide.
865
+ */
866
+ function Crew({
867
+ crew,
868
+ more,
869
+ bubbles,
794
870
  }: {
795
- bots?: Bot[];
796
- waiting?: BotRef[];
797
- /** While one bot is speaking the crowd narrows to it, then fans back out. */
798
- only?: string;
871
+ crew: CrewFace[];
872
+ more: number;
873
+ /** Hand-offs currently up, keyed by the face they point at. */
874
+ bubbles: Map<string, Handoff>;
799
875
  }) {
800
- // Faces with a task waiting go in front and are the only ones with the dot.
801
- const roster = bots?.length
802
- ? bots.map((bot) => ({
803
- key: bot.name,
804
- seed: bot.name,
805
- color: bot.icon?.color,
806
- shape: bot.icon?.shape,
807
- outline: bot.icon?.outline,
808
- notify: false,
809
- }))
810
- : GHOSTS.map((ghost) => ({ ...ghost, notify: false }));
811
-
812
- const front = waiting.map((bot) => ({
813
- key: bot.name,
814
- seed: bot.name,
815
- color: bot.icon?.color,
816
- shape: bot.icon?.shape,
817
- outline: bot.icon?.outline,
818
- notify: true,
819
- }));
820
-
821
- // A fresh install has one worker (DEFAULT_BOT) and one silhouette says "one bot",
822
- // which is the wrong thing to say about a room. Stand-ins fill the row out to CREW
823
- // and are dimmed with it, so the corner reads as a crew before it holds one.
824
- const named = [
825
- ...front,
826
- ...roster.filter((face) => !front.some((one) => one.key === face.key)),
827
- ].map((face) => ({ ...face, standIn: false }));
828
- const filler = GHOSTS.filter(
829
- (ghost) => !named.some((face) => face.key === ghost.key),
830
- )
831
- .slice(0, Math.max(0, CREW - named.length))
832
- .map((ghost) => ({ ...ghost, notify: false, standIn: true }));
833
- const crew = [...named, ...filler].slice(0, 4);
834
-
835
876
  return (
836
- <span
837
- className={cn(
838
- "flex shrink-0 items-center",
839
- !bots?.length && !front.length && "opacity-50",
840
- )}
841
- >
842
- {crew.map((face, index) => (
877
+ <span className="flex min-w-0 shrink items-center">
878
+ {crew.map((face, index) => {
879
+ const bubble = bubbles.get(face.name) ?? null;
880
+ return (
881
+ <Fragment key={face.name}>
882
+ <span
883
+ // Silhouettes overlapped, never ringed: a ring needs a circle and
884
+ // these are not circles. Earlier faces sit on top, so the dot on a
885
+ // waiting face is never covered by its neighbour.
886
+ className={cn(
887
+ "relative shrink-0 transition-[margin,transform] duration-500 ease-out",
888
+ // A word to the left has already broken the shingle.
889
+ index > 0 && !crew[index - 1].word && "-ml-2",
890
+ face.standIn && "opacity-35",
891
+ face.awake && "-translate-y-0.5 scale-110",
892
+ )}
893
+ style={{ zIndex: crew.length - index }}
894
+ >
895
+ {/* The halo is the mark's own (bot-mark `glow`), not a filter on
896
+ this box: a filter here also lands on the bubble above, which
897
+ came out tinted in the bot's colour and wearing its blur. */}
898
+ <CrewMark face={face} />
899
+ {bubble && <HandoffBubble handoff={bubble} />}
900
+ </span>
901
+ {face.word && (
902
+ // No box: the shine is what says this is happening right now, so a
903
+ // capsule around it was drawing a second time what the sweep says.
904
+ <span className="mx-2.5 min-w-0 shrink truncate">
905
+ <ShinyText
906
+ text={face.word}
907
+ speed={2.6}
908
+ className="truncate text-[13px] leading-5 tracking-[-0.1px]"
909
+ />
910
+ </span>
911
+ )}
912
+ </Fragment>
913
+ );
914
+ })}
915
+ {more > 0 && (
916
+ // Past CREW_MAX the row stops growing. Only idle bots are ever behind it:
917
+ // anyone moving sorted to the front.
843
918
  <span
844
- key={face.key}
845
- // Silhouettes overlapped, never ringed: a ring needs a circle and these
846
- // are not circles. One speaking bot pulls the rest in behind it.
847
919
  className={cn(
848
- "origin-right transition-[margin,opacity,transform] duration-300 ease-out",
849
- index > 0 && "-ml-2",
850
- face.standIn && "opacity-35",
851
- only && face.key !== only && "-ml-[28px] scale-50 opacity-0",
920
+ "grid size-7 shrink-0 place-items-center rounded-[9px] bg-muted font-mono text-[10px] text-muted-foreground ring-1 ring-border/50",
921
+ !crew.at(-1)?.word && "-ml-2",
852
922
  )}
853
- style={{ zIndex: crew.length - index }}
923
+ title={`${more} more`}
854
924
  >
855
- <BotMark
856
- size={28}
857
- seed={face.seed}
858
- vary={face.seed}
859
- color={face.color}
860
- shape={face.shape}
861
- outline={face.outline}
862
- notify={face.notify}
863
- />
925
+ +{more}
864
926
  </span>
865
- ))}
927
+ )}
928
+ </span>
929
+ );
930
+ }
931
+
932
+ /**
933
+ * One crew face. Awake, it wears a halo in its own colour, so who is moving
934
+ * reads before any word does; the options object is memoized because a new one
935
+ * every render re-derives the silhouette behind it.
936
+ */
937
+ function CrewMark({ face }: { face: CrewFace }) {
938
+ const glow = useMemo(
939
+ () => (face.awake ? { glow: AWAKE_GLOW } : undefined),
940
+ [face.awake],
941
+ );
942
+ return (
943
+ <BotMark
944
+ size={28}
945
+ seed={face.name}
946
+ vary={face.name}
947
+ color={face.icon?.color}
948
+ shape={face.icon?.shape}
949
+ outline={face.icon?.outline}
950
+ notify={face.waiting}
951
+ options={glow}
952
+ />
953
+ );
954
+ }
955
+
956
+ /**
957
+ * A hand-off, over the face it landed on. The mark at its head is whoever gave
958
+ * it away — Thursday, another bot, or nobody at all when it was you.
959
+ *
960
+ * It sits outside the pill's box on purpose, in space the corner is not using;
961
+ * the chip cannot clip its own children while one is up.
962
+ */
963
+ function HandoffBubble({ handoff }: { handoff: Handoff }) {
964
+ return (
965
+ <span className="pointer-events-none absolute bottom-[calc(100%+9px)] left-1/2 flex -translate-x-1/2 animate-in flex-col items-center whitespace-nowrap fade-in zoom-in-95 duration-200">
966
+ <span className="flex items-center gap-1.5 rounded-full bg-background px-2.5 py-1 text-[12.5px] leading-4 tracking-[-0.1px] shadow-lg shadow-black/10 ring-1 ring-border">
967
+ {handoff.from?.name === THURSDAY.name ? (
968
+ <ThursdayMark size={14} className="shrink-0 opacity-75" />
969
+ ) : (
970
+ handoff.from && (
971
+ <BotMark
972
+ size={14}
973
+ seed={handoff.from.name}
974
+ vary={handoff.from.name}
975
+ color={handoff.from.icon?.color}
976
+ shape={handoff.from.icon?.shape}
977
+ outline={handoff.from.icon?.outline}
978
+ className="shrink-0 opacity-75"
979
+ />
980
+ )
981
+ )}
982
+ {handoff.text}
983
+ </span>
984
+ {/* Two triangles: the ring's, then the fill's a pixel over it. */}
985
+ <span className="-mt-px size-0 border-x-[5px] border-t-[6px] border-x-transparent border-t-border" />
986
+ <span className="-mt-[6.5px] size-0 border-x-[5px] border-t-[6px] border-x-transparent border-t-background" />
866
987
  </span>
867
988
  );
868
989
  }
@@ -1006,7 +1127,7 @@ const STATE_LOOK: Record<TaskViewStatus, string> = {
1006
1127
 
1007
1128
  function State({ task }: { task: TaskView }) {
1008
1129
  const look =
1009
- task.status === "done" && task.relayed
1130
+ task.status === "done" && task.seen
1010
1131
  ? "text-muted-foreground"
1011
1132
  : STATE_LOOK[task.status];
1012
1133
 
@@ -1015,8 +1136,6 @@ function State({ task }: { task: TaskView }) {
1015
1136
  <ShinyText
1016
1137
  text="working"
1017
1138
  speed={2.2}
1018
- color="var(--muted-foreground)"
1019
- shineColor="var(--foreground)"
1020
1139
  className="shrink-0 font-mono text-[10px]"
1021
1140
  />
1022
1141
  );
@@ -1035,9 +1154,10 @@ function State({ task }: { task: TaskView }) {
1035
1154
  }
1036
1155
 
1037
1156
  function Empty({ bots }: { bots?: Bot[] }) {
1157
+ const { crew, more } = crewOf(bots, []);
1038
1158
  return (
1039
1159
  <div className="flex flex-col items-center gap-3 px-6 pt-3 pb-4 text-center">
1040
- <Faces bots={bots} />
1160
+ <Crew crew={crew} more={more} bubbles={EMPTY_BUBBLES} />
1041
1161
  {bots?.length ? (
1042
1162
  <p className="text-[12px] text-muted-foreground">
1043
1163
  Nothing handed over yet — ask for something that takes a while.
@@ -1129,18 +1249,17 @@ function TaskList({
1129
1249
 
1130
1250
  function TaskRow({ task, onPick }: { task: TaskView; onPick: () => void }) {
1131
1251
  const attention = needsYou(task);
1132
- // secondLine runs plainText over the whole report. Every sync rebuilds each TaskView
1252
+ // secondLine runs plainText over the whole answer. Every sync rebuilds each TaskView
1133
1253
  // (task.store), so depend on the fields that change the line, not on `task`.
1134
1254
  const last = task.lines.at(-1);
1135
1255
  const line = useMemo(
1136
1256
  () => secondLine(task),
1137
- [task.status, task.outcome, task.relayed, task.ask, last?.id],
1257
+ [task.status, task.outcome, task.seen, task.ask, last?.id],
1138
1258
  );
1139
1259
  const [answer, answering] = useAnswerTask();
1140
1260
  const [sending, setSending] = useState<string | null>(null);
1141
1261
  // Options are answered inline, without opening the thread.
1142
1262
  const options = task.status === "waiting" ? (task.ask?.options ?? []) : [];
1143
- const budget = isBudgetAsk(task.ask);
1144
1263
 
1145
1264
  return (
1146
1265
  // The row is not itself a button: the option buttons cannot nest inside one.
@@ -1192,8 +1311,6 @@ function TaskRow({ task, onPick }: { task: TaskView; onPick: () => void }) {
1192
1311
  <ShinyText
1193
1312
  text={line.text}
1194
1313
  speed={2.2}
1195
- color="var(--muted-foreground)"
1196
- shineColor="var(--foreground)"
1197
1314
  className={cn(
1198
1315
  "min-w-0 flex-1 truncate text-[12px] leading-4",
1199
1316
  line.tone,
@@ -1227,21 +1344,12 @@ function TaskRow({ task, onPick }: { task: TaskView; onPick: () => void }) {
1227
1344
  await answer(task, option);
1228
1345
  setSending(null);
1229
1346
  }}
1230
- className={cn(
1231
- "h-7 gap-1.5 rounded-full bg-background px-3 text-[12px]",
1232
- // A budget stop is not a question, so it does not take the waiting colour.
1233
- !budget && "border-amber-500/35",
1234
- )}
1347
+ className="h-7 gap-1.5 rounded-full border-amber-500/35 bg-background px-3 text-[12px]"
1235
1348
  >
1236
- {budget && option === TASK_CONTINUE && (
1349
+ {option === TASK_CONTINUE && (
1237
1350
  <ChevronsRight className="size-3.5 text-muted-foreground" />
1238
1351
  )}
1239
1352
  {option}
1240
- {budget && option === TASK_CONTINUE && (
1241
- <span className="font-mono text-[10px] text-muted-foreground">
1242
- +{BOT_RUN.steps} steps
1243
- </span>
1244
- )}
1245
1353
  </Button>
1246
1354
  ))}
1247
1355
  </div>
@@ -1253,29 +1361,28 @@ function TaskRow({ task, onPick }: { task: TaskView; onPick: () => void }) {
1253
1361
  /** Second row of a task line: the question, the outcome, or the bot's last step. */
1254
1362
  function secondLine(task: TaskView): { text: string; tone: string } {
1255
1363
  if (task.status === "waiting" && task.ask) {
1256
- // Budget stops are not questions; keep them out of the waiting colour.
1257
- if (isBudgetAsk(task.ask)) {
1258
- return {
1259
- text: plainText(task.outcome ?? task.ask.question),
1260
- tone: "text-muted-foreground",
1261
- };
1262
- }
1364
+ // A budget stop is not a question, but it waits on the user exactly as one
1365
+ // does, so it carries the waiting colour too; only the words differ.
1263
1366
  return {
1264
- text: task.ask.question,
1367
+ text: isAppStop(task.ask)
1368
+ ? plainText(task.outcome ?? task.ask.question)
1369
+ : task.ask.question,
1265
1370
  tone: WAITING_INK,
1266
1371
  };
1267
1372
  }
1268
1373
  // Reports are markdown; keep only the text.
1374
+ // An ending the user has opened steps back; red stays red, only quieter.
1375
+ const had = task.seen;
1269
1376
  if (task.status === "failed") {
1270
1377
  return {
1271
1378
  text: plainText(task.outcome ?? "Failed"),
1272
- tone: "text-destructive",
1379
+ tone: had ? "text-destructive/70" : "text-destructive",
1273
1380
  };
1274
1381
  }
1275
1382
  if (task.status === "done") {
1276
1383
  return {
1277
1384
  text: plainText(task.outcome ?? "Done"),
1278
- tone: task.relayed ? "text-muted-foreground" : "text-foreground",
1385
+ tone: had ? "text-muted-foreground" : "text-foreground",
1279
1386
  };
1280
1387
  }
1281
1388
  const last = lastSaid(task);
@@ -1339,20 +1446,37 @@ export function Conversation({
1339
1446
  <Group key={item.key} group={item.group} task={task} />
1340
1447
  ),
1341
1448
  )}
1342
- {task.status === "working" && task.lines.length === 0 && (
1343
- <ShinyText
1344
- text={`${task.bot.name} is taking it on…`}
1345
- speed={2.2}
1346
- color="var(--muted-foreground)"
1347
- shineColor="var(--foreground)"
1348
- className="block px-1 font-mono text-[10px]"
1349
- />
1350
- )}
1449
+ {task.status === "working" && <NextMove task={task} />}
1351
1450
  </div>
1352
1451
  </FileViewer>
1353
1452
  );
1354
1453
  }
1355
1454
 
1455
+ /**
1456
+ * The foot of a running thread while nothing on it is moving: the bot is
1457
+ * writing its next step. A tool mid-call already shines on its own row, so this
1458
+ * steps aside for it.
1459
+ */
1460
+ function NextMove({ task }: { task: TaskView }) {
1461
+ if (task.lines.some((line) => line.tool && line.tool.results === undefined)) {
1462
+ return null;
1463
+ }
1464
+ const last = task.lines.at(-1);
1465
+ // After a hand-off, in either direction, the move is the receiver's
1466
+ const who = last?.kind === "ask" && last.to ? last.to : last?.bot;
1467
+ return (
1468
+ <ShinyText
1469
+ text={
1470
+ who
1471
+ ? `${who.name} is on the next step…`
1472
+ : `${task.bot.name} is taking it on…`
1473
+ }
1474
+ speed={2.2}
1475
+ className="block px-1 font-mono text-[10px]"
1476
+ />
1477
+ );
1478
+ }
1479
+
1356
1480
  /** The delegated request, folded to three lines (FoldedText). */
1357
1481
  function Request({ task }: { task: TaskView }) {
1358
1482
  return (
@@ -1369,7 +1493,11 @@ function Request({ task }: { task: TaskView }) {
1369
1493
  );
1370
1494
  }
1371
1495
 
1372
- /** Thursday's own face; she is not a bot and has no row to read one from. */
1496
+ /**
1497
+ * Thursday's own face; she is not a bot and has no row to read one from. Also
1498
+ * who a hand-off came from when it came from the call — her mark is drawn by
1499
+ * her own domain (features/thursday), so this only has to name her.
1500
+ */
1373
1501
  const THURSDAY: BotRef = { name: "Thursday" };
1374
1502
 
1375
1503
  /**
@@ -1445,7 +1573,7 @@ function Turn({
1445
1573
  return (
1446
1574
  <div className={cn("flex gap-2.5", side === "end" && "flex-row-reverse")}>
1447
1575
  {mark}
1448
- {/* One cap on the turn's column, not one per block inside it: a report and
1576
+ {/* One cap on the turn's column, not one per block inside it: an answer and
1449
1577
  a one-line remark from the same bot then end on the same edge. The
1450
1578
  answering side hugs that edge, so the two sides face each other. */}
1451
1579
  <div
@@ -1537,7 +1665,7 @@ function Line({ line, taskId }: { line: Chatter; taskId: string }) {
1537
1665
  );
1538
1666
  }
1539
1667
 
1540
- // Passing remarks are muted; the report is the one thing here at full weight.
1668
+ // Passing remarks are muted; the answer is the one thing here at full weight.
1541
1669
  if (!isOutcome(line)) {
1542
1670
  return (
1543
1671
  <p className="px-1 text-[12.5px] leading-relaxed break-keep text-muted-foreground">
@@ -1570,7 +1698,7 @@ function Line({ line, taskId }: { line: Chatter; taskId: string }) {
1570
1698
  const failed = line.kind === "error";
1571
1699
 
1572
1700
  return (
1573
- // The report is not a card. It sits in a thread that is already in a box,
1701
+ // The answer is not a card. It sits in a thread that is already in a box,
1574
1702
  // in a section that is another: a fourth border reads as a second chat
1575
1703
  // window. What tells it from a passing remark is that it is the only prose
1576
1704
  // here at foreground weight, plus the files it names — and its copy button
@@ -1591,7 +1719,7 @@ function Line({ line, taskId }: { line: Chatter; taskId: string }) {
1591
1719
  {line.text}
1592
1720
  </Markdown>
1593
1721
  {/* The files it names, and the copy — one row, because the end of the
1594
- report is where a reader is when they want either. */}
1722
+ answer is where a reader is when they want either. */}
1595
1723
  <div className="mt-2 flex items-center gap-2">
1596
1724
  <PathChips text={line.text} className="min-w-0" />
1597
1725
  {!failed && (
@@ -1604,7 +1732,7 @@ function Line({ line, taskId }: { line: Chatter; taskId: string }) {
1604
1732
  );
1605
1733
  }
1606
1734
 
1607
- /** Copies the report. The icon confirms; a toast only reports failure. */
1735
+ /** Copies the answer. The icon confirms; a toast only reports failure. */
1608
1736
  function CopyReport({ text }: { text: string }) {
1609
1737
  const [copied, setCopied] = useState(false);
1610
1738
  const back = useRef<ReturnType<typeof setTimeout> | null>(null);
@@ -1619,7 +1747,7 @@ function CopyReport({ text }: { text: string }) {
1619
1747
  return (
1620
1748
  <button
1621
1749
  type="button"
1622
- aria-label="Copy the report"
1750
+ aria-label="Copy the answer"
1623
1751
  onClick={async () => {
1624
1752
  try {
1625
1753
  await navigator.clipboard.writeText(text);
@@ -1629,7 +1757,7 @@ function CopyReport({ text }: { text: string }) {
1629
1757
  } catch (cause) {
1630
1758
  toast.add({
1631
1759
  type: "error",
1632
- title: "Could not copy the report",
1760
+ title: "Could not copy the answer",
1633
1761
  description: errorToString(cause),
1634
1762
  });
1635
1763
  }