thursday-agent 0.1.2 → 0.2.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 (358) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/app-path-routes-manifest.json +1 -0
  3. package/.next/build-manifest.json +3 -3
  4. package/.next/prerender-manifest.json +3 -3
  5. package/.next/routes-manifest.json +6 -0
  6. package/.next/server/app/_global-error/page.js.nft.json +1 -1
  7. package/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  8. package/.next/server/app/_global-error.html +1 -1
  9. package/.next/server/app/_global-error.rsc +1 -1
  10. package/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  11. package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  12. package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  13. package/.next/server/app/_not-found/page.js.nft.json +1 -1
  14. package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  15. package/.next/server/app/_not-found.html +1 -1
  16. package/.next/server/app/_not-found.rsc +13 -13
  17. package/.next/server/app/_not-found.segments/_full.segment.rsc +13 -13
  18. package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +12 -12
  19. package/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  20. package/.next/server/app/api/artifact/route.js +1 -1
  21. package/.next/server/app/api/artifact/route.js.nft.json +1 -1
  22. package/.next/server/app/api/bot/notes/route/app-paths-manifest.json +3 -0
  23. package/.next/server/app/api/bot/notes/route/build-manifest.json +12 -0
  24. package/.next/server/app/api/bot/notes/route/server-reference-manifest.json +4 -0
  25. package/.next/server/app/api/bot/notes/route.js +14 -0
  26. package/.next/server/app/api/bot/notes/route.js.map +5 -0
  27. package/.next/server/app/api/bot/notes/route.js.nft.json +1 -0
  28. package/.next/server/app/api/bot/notes/route_client-reference-manifest.js +3 -0
  29. package/.next/server/app/api/bot/route.js +4 -4
  30. package/.next/server/app/api/bot/route.js.nft.json +1 -1
  31. package/.next/server/app/api/bot/task/[id]/route.js +6 -6
  32. package/.next/server/app/api/bot/task/[id]/route.js.nft.json +1 -1
  33. package/.next/server/app/api/bot/task/route.js +6 -6
  34. package/.next/server/app/api/bot/task/route.js.nft.json +1 -1
  35. package/.next/server/app/api/config/route.js +3 -3
  36. package/.next/server/app/api/config/route.js.nft.json +1 -1
  37. package/.next/server/app/api/events/route.js +1 -1
  38. package/.next/server/app/api/events/route.js.nft.json +1 -1
  39. package/.next/server/app/api/file/[...path]/route.js +1 -1
  40. package/.next/server/app/api/file/[...path]/route.js.nft.json +1 -1
  41. package/.next/server/app/api/llm-model/catalog/route.js +6 -6
  42. package/.next/server/app/api/llm-model/catalog/route.js.nft.json +1 -1
  43. package/.next/server/app/api/llm-model/route.js +3 -3
  44. package/.next/server/app/api/llm-model/route.js.nft.json +1 -1
  45. package/.next/server/app/api/mcp/[name]/route.js +3 -3
  46. package/.next/server/app/api/mcp/[name]/route.js.nft.json +1 -1
  47. package/.next/server/app/api/mcp/oauth/callback/route.js +5 -5
  48. package/.next/server/app/api/mcp/oauth/callback/route.js.nft.json +1 -1
  49. package/.next/server/app/api/mcp/route.js +3 -3
  50. package/.next/server/app/api/mcp/route.js.nft.json +1 -1
  51. package/.next/server/app/api/mcp/tools/route.js +3 -3
  52. package/.next/server/app/api/mcp/tools/route.js.nft.json +1 -1
  53. package/.next/server/app/api/memory/route.js +7 -7
  54. package/.next/server/app/api/memory/route.js.nft.json +1 -1
  55. package/.next/server/app/api/memory/tidy/route.js +10 -11
  56. package/.next/server/app/api/memory/tidy/route.js.nft.json +1 -1
  57. package/.next/server/app/api/skills/[source]/[name]/route.js +1 -1
  58. package/.next/server/app/api/skills/[source]/[name]/route.js.nft.json +1 -1
  59. package/.next/server/app/api/skills/route.js +1 -1
  60. package/.next/server/app/api/skills/route.js.nft.json +1 -1
  61. package/.next/server/app/api/thursday/call/route.js +8 -8
  62. package/.next/server/app/api/thursday/call/route.js.nft.json +1 -1
  63. package/.next/server/app/api/thursday/skills/route.js +8 -8
  64. package/.next/server/app/api/thursday/skills/route.js.nft.json +1 -1
  65. package/.next/server/app/api/thursday/tool-call/route.js +11 -12
  66. package/.next/server/app/api/thursday/tool-call/route.js.nft.json +1 -1
  67. package/.next/server/app/api/workspace/route.js +1 -1
  68. package/.next/server/app/api/workspace/route.js.nft.json +1 -1
  69. package/.next/server/app/artifact/[...path]/page/server-reference-manifest.json +1 -1
  70. package/.next/server/app/artifact/[...path]/page.js +3 -3
  71. package/.next/server/app/artifact/[...path]/page.js.nft.json +1 -1
  72. package/.next/server/app/artifact/[...path]/page_client-reference-manifest.js +1 -1
  73. package/.next/server/app/favicon.ico/route.js.nft.json +1 -1
  74. package/.next/server/app/page/server-reference-manifest.json +63 -41
  75. package/.next/server/app/page.js +7 -7
  76. package/.next/server/app/page.js.nft.json +1 -1
  77. package/.next/server/app/page_client-reference-manifest.js +1 -1
  78. package/.next/server/app-paths-manifest.json +1 -0
  79. package/.next/server/chunks/0d-b_next_dist_server_app-render_after-task-async-storage_external_0-exn7p.js +3 -0
  80. package/.next/server/chunks/{[root-of-the-server]__0t_dqaj._.js → [root-of-the-server]__0-npkh_._.js} +1 -1
  81. package/.next/server/chunks/[root-of-the-server]__005var5._.js +3 -0
  82. package/.next/server/chunks/[root-of-the-server]__019dx9f._.js +2 -2
  83. package/.next/server/chunks/[root-of-the-server]__02va5wz._.js +3 -0
  84. package/.next/server/chunks/{[root-of-the-server]__1mzp_el._.js → [root-of-the-server]__04_8spk._.js} +2 -2
  85. package/.next/server/chunks/[root-of-the-server]__06t77t4._.js +1 -1
  86. package/.next/server/chunks/[root-of-the-server]__0bed6hu._.js +4 -4
  87. package/.next/server/chunks/[root-of-the-server]__0cro2dz._.js +5 -0
  88. package/.next/server/chunks/[root-of-the-server]__0emr6go._.js +3 -0
  89. package/.next/server/chunks/[root-of-the-server]__0le4d-p._.js +1 -1
  90. package/.next/server/chunks/[root-of-the-server]__0m8ywyo._.js +5 -5
  91. package/.next/server/chunks/{[root-of-the-server]__113fqg5._.js → [root-of-the-server]__0p32jfz._.js} +3 -3
  92. package/.next/server/chunks/[root-of-the-server]__0phdgco._.js +4 -4
  93. package/.next/server/chunks/[root-of-the-server]__0wvgrsl._.js +1 -1
  94. package/.next/server/chunks/[root-of-the-server]__11j52t0._.js +5 -0
  95. package/.next/server/chunks/{[root-of-the-server]__0j_n5l3._.js → [root-of-the-server]__1692zrv._.js} +2 -2
  96. package/.next/server/chunks/[root-of-the-server]__16thabu._.js +1 -1
  97. package/.next/server/chunks/[root-of-the-server]__173nhwf._.js +3 -3
  98. package/.next/server/chunks/[root-of-the-server]__1bn73ih._.js +10 -0
  99. package/.next/server/chunks/[root-of-the-server]__1bqhuh9._.js +4 -4
  100. package/.next/server/chunks/[root-of-the-server]__1ccon1w._.js +10 -0
  101. package/.next/server/chunks/[root-of-the-server]__1cqizfq._.js +1 -1
  102. package/.next/server/chunks/{[root-of-the-server]__1u_qvc7._.js → [root-of-the-server]__1fboj2j._.js} +2 -2
  103. package/.next/server/chunks/{[root-of-the-server]__1dow0bz._.js → [root-of-the-server]__1oli73c._.js} +2 -2
  104. package/.next/server/chunks/[root-of-the-server]__1txedx6._.js +4 -4
  105. package/.next/server/chunks/[root-of-the-server]__1untqv8._.js +5 -0
  106. package/.next/server/chunks/_0-73of8._.js +55 -0
  107. package/.next/server/chunks/{_2050gwn._.js → _00_584r._.js} +66 -28
  108. package/.next/server/chunks/{_0y8is-s._.js → _03a0897._.js} +1 -1
  109. package/.next/server/chunks/_05h2cii._.js +1 -1
  110. package/.next/server/chunks/_06mxlgn._.js +1 -1
  111. package/.next/server/chunks/_0a351g7._.js +1 -1
  112. package/.next/server/chunks/{_0hin38_._.js → _0khihjk._.js} +5 -5
  113. package/.next/server/chunks/_0kjjmag._.js +1 -1
  114. package/.next/server/chunks/{_1t8fv7b._.js → _0n74zsf._.js} +5 -5
  115. package/.next/server/chunks/_0nn99o2._.js +3 -0
  116. package/.next/server/chunks/_0pppbb5._.js +10 -6
  117. package/.next/server/chunks/{_1icgbi8._.js → _0rnf-6n._.js} +5 -5
  118. package/.next/server/chunks/_0u10wmw._.js +55 -0
  119. package/.next/server/chunks/{_0kl37zl._.js → _0vnutv9._.js} +1 -1
  120. package/.next/server/chunks/_0wp4le3._.js +1 -1
  121. package/.next/server/chunks/_0y5fg-x._.js +1 -1
  122. package/.next/server/chunks/_106co2o._.js +3 -0
  123. package/.next/server/chunks/_12bbv5i._.js +10 -6
  124. package/.next/server/chunks/_15hq89o._.js +1 -1
  125. package/.next/server/chunks/_15q1pig._.js +1 -1
  126. package/.next/server/chunks/_18-1wcc._.js +55 -0
  127. package/.next/server/chunks/_19wg5yn._.js +138 -0
  128. package/.next/server/chunks/_1_5mnq4._.js +18 -0
  129. package/.next/server/chunks/_1bt95p9._.js +55 -0
  130. package/.next/server/chunks/_1cbk7ms._.js +9 -9
  131. package/.next/server/chunks/_1drgfx4._.js +18 -0
  132. package/.next/server/chunks/_1drv2et._.js +10 -6
  133. package/.next/server/chunks/{_10qam6o._.js → _1e48czp._.js} +5 -5
  134. package/.next/server/chunks/{_1skgy7b._.js → _1kg70wc._.js} +5 -5
  135. package/.next/server/chunks/_1php4eq._.js +1 -1
  136. package/.next/server/chunks/_1rysr-r._.js +10 -0
  137. package/.next/server/chunks/_1u3rwbr._.js +3 -0
  138. package/.next/server/chunks/_1x7p319._.js +1 -1
  139. package/.next/server/chunks/_1yuv4vp._.js +1 -1
  140. package/.next/server/chunks/_next-internal_server_app_api_bot_notes_route_actions_0qcsyu7.js +3 -0
  141. package/.next/server/chunks/config_ts_1g4sz5o._.js +1 -1
  142. package/.next/server/chunks/features_bot_task_query_ts_11xuzl3._.js +1 -1
  143. package/.next/server/chunks/{node_modules_consola_dist_chunks_prompt_mjs_0228f43._.js → node_modules_consola_dist_chunks_prompt_mjs_02s3hjt._.js} +1 -1
  144. package/.next/server/chunks/{node_modules_consola_dist_chunks_prompt_mjs_0ir-vo2._.js → node_modules_consola_dist_chunks_prompt_mjs_06067w6._.js} +1 -1
  145. package/.next/server/chunks/{node_modules_consola_dist_chunks_prompt_mjs_0otw8ef._.js → node_modules_consola_dist_chunks_prompt_mjs_096ais_._.js} +1 -1
  146. package/.next/server/chunks/{node_modules_consola_dist_chunks_prompt_mjs_0tf290y._.js → node_modules_consola_dist_chunks_prompt_mjs_0a1rd09._.js} +1 -1
  147. package/.next/server/chunks/node_modules_consola_dist_chunks_prompt_mjs_0gzvgfv._.js +3 -0
  148. package/.next/server/chunks/node_modules_consola_dist_chunks_prompt_mjs_0j61-ma._.js +3 -0
  149. package/.next/server/chunks/node_modules_consola_dist_chunks_prompt_mjs_0tvkfgh._.js +3 -0
  150. package/.next/server/chunks/node_modules_consola_dist_chunks_prompt_mjs_11zjo-1._.js +3 -0
  151. package/.next/server/chunks/node_modules_consola_dist_chunks_prompt_mjs_1pycqsq._.js +3 -0
  152. package/.next/server/chunks/node_modules_consola_dist_chunks_prompt_mjs_1tpvamf._.js +3 -0
  153. package/.next/server/chunks/node_modules_consola_dist_chunks_prompt_mjs_1y9-wcr._.js +3 -0
  154. package/.next/server/chunks/ssr/[externals]__0dizgek._.js +3 -0
  155. package/.next/server/chunks/ssr/[root-of-the-server]__0f6s48x._.js +1 -1
  156. package/.next/server/chunks/ssr/[root-of-the-server]__0m1b5ky._.js +3 -0
  157. package/.next/server/chunks/ssr/{[root-of-the-server]__0dfwvr4._.js → [root-of-the-server]__0un-erv._.js} +2 -2
  158. package/.next/server/chunks/ssr/[root-of-the-server]__1-mnhyd._.js +5 -3
  159. package/.next/server/chunks/ssr/[root-of-the-server]__1bww500._.js +3 -0
  160. package/.next/server/chunks/ssr/[root-of-the-server]__1mzi-ju._.js +1 -1
  161. package/.next/server/chunks/ssr/[root-of-the-server]__1qfqvmg._.js +3 -0
  162. package/.next/server/chunks/ssr/[root-of-the-server]__1vcdast._.js +3 -0
  163. package/.next/server/chunks/ssr/[root-of-the-server]__1xwnu-h._.js +3 -0
  164. package/.next/server/chunks/ssr/_03webdr._.js +3 -0
  165. package/.next/server/chunks/ssr/{_0gd4vaa._.js → _040sk32._.js} +71 -29
  166. package/.next/server/chunks/ssr/_077-g6s._.js +1 -1
  167. package/.next/server/chunks/ssr/{_0lc_7c8._.js → _09ja_w1._.js} +2 -2
  168. package/.next/server/chunks/ssr/{_1dcxzqe._.js → _0f_b_9s._.js} +2 -2
  169. package/.next/server/chunks/ssr/_0h7zdni._.js +1 -1
  170. package/.next/server/chunks/ssr/_0lu1n_x._.js +3 -0
  171. package/.next/server/chunks/ssr/_0sf1isi._.js +1 -1
  172. package/.next/server/chunks/ssr/_0su6_08._.js +3 -0
  173. package/.next/server/chunks/ssr/{_0nso8tp._.js → _0w5bgak._.js} +2 -2
  174. package/.next/server/chunks/ssr/{_19_o9d-._.js → _0x_nu5e._.js} +2 -2
  175. package/.next/server/chunks/ssr/_1-teee5._.js +3 -3
  176. package/.next/server/chunks/ssr/_13punpm._.js +1 -1
  177. package/.next/server/chunks/ssr/{_0ay6b01._.js → _1ciyoo3._.js} +2 -2
  178. package/.next/server/chunks/ssr/_1e-dgz0._.js +1 -1
  179. package/.next/server/chunks/ssr/_1fq7zgb._.js +1 -1
  180. package/.next/server/chunks/ssr/{_0cvjbuz._.js → _1g6l4uj._.js} +2 -2
  181. package/.next/server/chunks/ssr/{_0q_2f2x._.js → _1lip4th._.js} +2 -2
  182. package/.next/server/chunks/ssr/_1m3bsfr._.js +3 -0
  183. package/.next/server/chunks/ssr/_1mznrxk._.js +1 -1
  184. package/.next/server/chunks/ssr/_1ruzehw._.js +3 -0
  185. package/.next/server/chunks/ssr/_1vyd1ji._.js +3 -0
  186. package/.next/server/chunks/ssr/features_bot_components_bot-setting_tsx_01ajft6._.js +2 -2
  187. package/.next/server/chunks/ssr/features_config_components_14jqgy4._.js +1 -1
  188. package/.next/server/chunks/ssr/features_config_components_config-setting_tsx_0xfwqdg._.js +1 -1
  189. package/.next/server/chunks/ssr/features_connectors_components_mcp-setting_tsx_0jmbgvp._.js +1 -1
  190. package/.next/server/chunks/ssr/features_memory_components_memory-setting_tsx_0oa43sw._.js +1 -1
  191. package/.next/server/chunks/ssr/lib_realtime_12wg_dd._.js +27 -0
  192. package/.next/server/chunks/ssr/lib_realtime_15h_v1v._.js +27 -0
  193. package/.next/server/chunks/ssr/{node_modules_108wd2o._.js → node_modules_004qlr4._.js} +1 -1
  194. package/.next/server/chunks/ssr/{node_modules_1ub40gk._.js → node_modules_01qnv-a._.js} +1 -1
  195. package/.next/server/chunks/ssr/{node_modules_1y8_3vg._.js → node_modules_04n7mjs._.js} +1 -1
  196. package/.next/server/chunks/ssr/{node_modules_0_j7-g4._.js → node_modules_08d8bke._.js} +1 -1
  197. package/.next/server/chunks/ssr/{node_modules_0y2-av4._.js → node_modules_09xhur7._.js} +1 -1
  198. package/.next/server/chunks/ssr/{node_modules_043-236._.js → node_modules_0_n241r._.js} +1 -1
  199. package/.next/server/chunks/ssr/{node_modules_0ls96je._.js → node_modules_0iz1rzd._.js} +1 -1
  200. package/.next/server/chunks/ssr/{node_modules_0xwx4u-._.js → node_modules_0kl7-tn._.js} +1 -1
  201. package/.next/server/chunks/ssr/{node_modules_1jatz7w._.js → node_modules_0kxz4gt._.js} +1 -1
  202. package/.next/server/chunks/ssr/{node_modules_1t0y5jt._.js → node_modules_0lrjk5t._.js} +1 -1
  203. package/.next/server/chunks/ssr/{node_modules_13f2md6._.js → node_modules_0m93zeg._.js} +1 -1
  204. package/.next/server/chunks/ssr/{node_modules_1nhw9qg._.js → node_modules_0n1kj4p._.js} +1 -1
  205. package/.next/server/chunks/ssr/{node_modules_1k29j1j._.js → node_modules_0ndvxub._.js} +1 -1
  206. package/.next/server/chunks/ssr/{node_modules_16i4o_3._.js → node_modules_13r44jq._.js} +1 -1
  207. package/.next/server/chunks/ssr/{node_modules_0fbw5-u._.js → node_modules_17lrt3y._.js} +1 -1
  208. package/.next/server/chunks/ssr/{node_modules_1d-is1o._.js → node_modules_18_dk7_._.js} +1 -1
  209. package/.next/server/chunks/ssr/{node_modules_1hn_sd5._.js → node_modules_19dp7ko._.js} +1 -1
  210. package/.next/server/chunks/ssr/{node_modules_20ynonm._.js → node_modules_1dwguw_._.js} +1 -1
  211. package/.next/server/chunks/ssr/{node_modules_00d6675._.js → node_modules_1hd-5tv._.js} +1 -1
  212. package/.next/server/chunks/ssr/{node_modules_1ee8gav._.js → node_modules_1hn8z1f._.js} +1 -1
  213. package/.next/server/chunks/ssr/{node_modules_0bue6gy._.js → node_modules_1n5ygqe._.js} +1 -1
  214. package/.next/server/chunks/ssr/{node_modules_1_588_4._.js → node_modules_1q0p85z._.js} +1 -1
  215. package/.next/server/chunks/ssr/{node_modules_0ipn1ho._.js → node_modules_1toafc-._.js} +1 -1
  216. package/.next/server/chunks/ssr/{node_modules_0_fvxnt._.js → node_modules_1xyd7ov._.js} +1 -1
  217. package/.next/server/chunks/ssr/{node_modules_1v0077l._.js → node_modules_1zu988w._.js} +1 -1
  218. package/.next/server/chunks/ssr/{node_modules_06kvvuo._.js → node_modules_20yebtj._.js} +1 -1
  219. 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_0m9a4y0._.js} +1 -1
  220. 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_13w_wy9._.js} +1 -1
  221. package/.next/server/chunks/ssr/node_modules_consola_dist_chunks_prompt_mjs_05cwqv2._.js +47 -0
  222. package/.next/server/middleware-build-manifest.js +3 -3
  223. package/.next/server/pages/404.html +1 -1
  224. package/.next/server/pages/500.html +1 -1
  225. package/.next/server/server-reference-manifest.js +1 -1
  226. package/.next/server/server-reference-manifest.json +64 -42
  227. package/.next/static/chunks/05b6nx_o3_f_n.js +1 -0
  228. package/.next/static/chunks/{3aquirz5h71da.js → 07kl352c-lvcz.js} +1 -1
  229. package/.next/static/chunks/0dmns-gi0raa4.js +1 -0
  230. package/.next/static/chunks/0oc4_xoia77dg.css +1 -0
  231. package/.next/static/chunks/0pheo8m3a-rzi.js +1 -0
  232. package/.next/static/chunks/{2ihkf6l2jowo9.js → 18vhba44l1fc9.js} +3 -3
  233. package/.next/static/chunks/19i8h2t-5evjx.js +1 -0
  234. package/.next/static/chunks/{1gy3bbv7usnjo.js → 1c5fcj473025q.js} +1 -1
  235. package/.next/static/chunks/1pi_hbmjw-f-z.js +1 -0
  236. package/.next/static/chunks/{0tydnn2kybibk.js → 1twt6k09vg4np.js} +1 -1
  237. package/.next/static/chunks/{21rk8w7bda-su.js → 28gotuisldmom.js} +1 -1
  238. package/.next/static/chunks/{3xxa2j56-f3nm.js → 2998p0y7k1c8z.js} +1 -1
  239. package/.next/static/chunks/2l233xfycjpd8.js +1 -0
  240. package/.next/static/chunks/2p5n7pa3c40t-.js +2 -0
  241. package/.next/static/chunks/2ru1q132af7h7.js +1 -0
  242. package/.next/static/chunks/{2fq_86jvq9ymq.js → 2t8wqycwg3y04.js} +2 -2
  243. package/.next/static/chunks/32z_ypgtchw48.js +1 -0
  244. package/.next/static/chunks/{247d-r95j8xs0.js → 33pyjb3xd1iez.js} +1 -1
  245. package/.next/static/chunks/3786h5vayt80-.js +28 -0
  246. package/.next/static/chunks/3_h9txt3tq8t8.js +28 -0
  247. package/.next/static/chunks/{1khakdu_iuedc.js → 3ard_noqapnw1.js} +1 -1
  248. package/.next/static/chunks/{1fr5m75pwtyic.js → 3omud5xunrk1f.js} +1 -1
  249. package/.next/static/chunks/42dnrkbph9e-1.js +1 -0
  250. package/app/api/events/app-event.client.ts +24 -0
  251. package/app/api/events/app-event.server.ts +45 -0
  252. package/app/api/events/app-event.ts +44 -0
  253. package/app/api/llm-model/catalog/route.ts +12 -0
  254. package/app/api/llm-model/route.ts +21 -0
  255. package/app/api/query-key.ts +144 -0
  256. package/app/api/thursday/tool-call/route.ts +67 -0
  257. package/components/ui/button-group.tsx +45 -0
  258. package/components/ui/custom-icon.tsx +259 -0
  259. package/components/ui/dropdown-menu.tsx +271 -0
  260. package/components/ui/folded-text.tsx +68 -0
  261. package/components/ui/input-group.tsx +157 -0
  262. package/components/ui/json-view.tsx +240 -0
  263. package/components/ui/shiny-text.tsx +141 -0
  264. package/components/ui/text-type.tsx +213 -0
  265. package/components/ui/theme-sync.tsx +9 -0
  266. package/database/migrations/{20260908030742_rainy_marvel_zombies → 20260909060813_optimal_sabretooth}/migration.sql +7 -0
  267. package/database/migrations/{20260908030742_rainy_marvel_zombies → 20260909060813_optimal_sabretooth}/snapshot.json +54 -1
  268. package/database/migrations/20260909063821_bitter_tenebrous/migration.sql +1 -0
  269. package/database/migrations/20260909063821_bitter_tenebrous/snapshot.json +1285 -0
  270. package/docs/how-it-works.md +59 -0
  271. package/docs/images/keeps-going.png +0 -0
  272. package/docs/images/task-thread.png +0 -0
  273. package/docs/images/two-minds.png +0 -0
  274. package/features/ai/load-tools.ts +55 -5
  275. package/features/ai/prompts/bot.prompt.ts +121 -26
  276. package/features/ai/prompts/memory-tidy.prompt.ts +7 -1
  277. package/features/ai/prompts/prompt-helper.ts +31 -5
  278. package/features/ai/prompts/thursday.prompt.ts +2 -0
  279. package/features/ai/tools/bot.tool.ts +37 -18
  280. package/features/ai/tools/mcp.tool.ts +5 -5
  281. package/features/ai/tools/memory.tool.ts +17 -10
  282. package/features/ai/tools/studio.tool.ts +5 -5
  283. package/features/ai/tools/tool-name.ts +2 -0
  284. package/features/ai/tools/workspace.tool.ts +33 -5
  285. package/features/bot/bot.action.ts +31 -4
  286. package/features/bot/bot.notes.ts +120 -0
  287. package/features/bot/bot.query.ts +56 -1
  288. package/features/bot/bot.run.ts +43 -3
  289. package/features/bot/bot.runner.ts +94 -20
  290. package/features/bot/bot.schema.ts +17 -0
  291. package/features/bot/components/bot-room.tsx +405 -306
  292. package/features/bot/components/bot-setting.tsx +123 -20
  293. package/features/bot/task.query.ts +18 -5
  294. package/features/memory/components/memory-setting.tsx +11 -2
  295. package/features/memory/memory.query.ts +9 -2
  296. package/features/memory/memory.schema.ts +30 -0
  297. package/features/skills/components/skills-setting.tsx +9 -1
  298. package/features/workspace/workspace.ts +136 -3
  299. package/hooks/use-draft.ts +37 -0
  300. package/hooks/use-hotkey.ts +105 -0
  301. package/hooks/use-object-state.ts +51 -0
  302. package/hooks/use-on-visible.ts +72 -0
  303. package/hooks/use-theme.ts +106 -0
  304. package/hooks/use-wake-word.ts +262 -0
  305. package/lib/realtime/realtime.driver.ts +34 -1
  306. package/lib/realtime/realtime.protocol.ts +6 -0
  307. package/lib/sandbox.ts +9 -1
  308. package/lib/tokens.ts +19 -0
  309. package/package.json +1 -1
  310. package/.next/server/chunks/[root-of-the-server]__02kt8yn._.js +0 -10
  311. package/.next/server/chunks/[root-of-the-server]__0h59di-._.js +0 -5
  312. package/.next/server/chunks/[root-of-the-server]__0qjqtwo._.js +0 -5
  313. package/.next/server/chunks/[root-of-the-server]__0yk6s3w._.js +0 -5
  314. package/.next/server/chunks/[root-of-the-server]__0z3gelv._.js +0 -3
  315. package/.next/server/chunks/[root-of-the-server]__11w_7_3._.js +0 -3
  316. package/.next/server/chunks/[root-of-the-server]__1_0q_9w._.js +0 -3
  317. package/.next/server/chunks/[root-of-the-server]__1gmn1cs._.js +0 -10
  318. package/.next/server/chunks/_0fzqt2y._.js +0 -100
  319. package/.next/server/chunks/_0jgm3b8._.js +0 -55
  320. package/.next/server/chunks/_0ks6_sg._.js +0 -55
  321. package/.next/server/chunks/_0tzfesa._.js +0 -3
  322. package/.next/server/chunks/_0zmestz._.js +0 -55
  323. package/.next/server/chunks/_12nr4xc._.js +0 -3
  324. package/.next/server/chunks/_1hzrsvg._.js +0 -55
  325. package/.next/server/chunks/_1uj4_io._.js +0 -10
  326. package/.next/server/chunks/_1yldgo4._.js +0 -55
  327. package/.next/server/chunks/features_0scmr4i._.js +0 -3
  328. package/.next/server/chunks/features_bot_bot_query_ts_12mgkl2._.js +0 -3
  329. package/.next/server/chunks/node_modules_consola_dist_chunks_prompt_mjs_0ykklj0._.js +0 -3
  330. package/.next/server/chunks/node_modules_consola_dist_chunks_prompt_mjs_1ryg3wh._.js +0 -3
  331. package/.next/server/chunks/node_modules_consola_dist_chunks_prompt_mjs_1vwbnq_._.js +0 -3
  332. package/.next/server/chunks/ssr/[root-of-the-server]__01id-v3._.js +0 -3
  333. package/.next/server/chunks/ssr/[root-of-the-server]__0b-0i32._.js +0 -3
  334. package/.next/server/chunks/ssr/[root-of-the-server]__185grso._.js +0 -3
  335. package/.next/server/chunks/ssr/[root-of-the-server]__1minz8_._.js +0 -3
  336. package/.next/server/chunks/ssr/_0j4v8of._.js +0 -3
  337. package/.next/server/chunks/ssr/_0l81bzy._.js +0 -3
  338. package/.next/server/chunks/ssr/_10uvw3l._.js +0 -3
  339. package/.next/server/chunks/ssr/_1jbv7m1._.js +0 -3
  340. package/.next/server/chunks/ssr/lib_realtime_0aetuyp._.js +0 -24
  341. package/.next/server/chunks/ssr/lib_realtime_18k_aj-._.js +0 -24
  342. package/.next/server/chunks/ssr/node_modules_zod_v4_classic_errors_05xv7vp.js +0 -3
  343. package/.next/static/chunks/0yysv1gzin17t.js +0 -1
  344. package/.next/static/chunks/1keib_gt066gc.js +0 -1
  345. package/.next/static/chunks/28g42tw8tsvhd.js +0 -1
  346. package/.next/static/chunks/2cwjgkt_hoyhj.js +0 -22
  347. package/.next/static/chunks/2rbf1fpd8n80-.js +0 -22
  348. package/.next/static/chunks/2ukj8k69y89um.js +0 -1
  349. package/.next/static/chunks/2wngeetfnzxkp.js +0 -2
  350. package/.next/static/chunks/388-t2v72jh39.css +0 -1
  351. package/.next/static/chunks/3fqvc0cg-atq_.js +0 -1
  352. package/.next/static/chunks/3p4egvfjxpatb.js +0 -1
  353. package/.next/static/chunks/3qe1w1ei-huhl.js +0 -1
  354. package/.next/static/chunks/3y40riycdip-i.js +0 -1
  355. package/.next/static/chunks/45a6zbtkelph9.js +0 -1
  356. /package/.next/static/{X34BUd9bfdH27ripuwwBm → YR4NWcDDuzwLbTTU7U7Vr}/_buildManifest.js +0 -0
  357. /package/.next/static/{X34BUd9bfdH27ripuwwBm → YR4NWcDDuzwLbTTU7U7Vr}/_clientMiddlewareManifest.js +0 -0
  358. /package/.next/static/{X34BUd9bfdH27ripuwwBm → YR4NWcDDuzwLbTTU7U7Vr}/_ssgManifest.js +0 -0
@@ -34,6 +34,7 @@ import { BOT_RUN } from "@/config";
34
34
  import { markReportedAction, startTaskAction } from "@/features/bot/bot.action";
35
35
  import {
36
36
  type Bot,
37
+ type BotIcon,
37
38
  DEFAULT_BOT,
38
39
  isBudgetAsk,
39
40
  TASK_CONTINUE,
@@ -43,6 +44,7 @@ import { BotRoster } from "@/features/bot/components/bot-roster";
43
44
  import { PathChips } from "@/features/bot/components/path-chips";
44
45
  import { TaskReply, useAnswerTask } from "@/features/bot/components/task-reply";
45
46
  import { openSettings } from "@/features/settings/settings.store";
47
+ import { ThursdayMark } from "@/features/thursday/components/thursday-mark";
46
48
  import { FileViewer } from "@/features/workspace/components/file-view";
47
49
  import { shortAgo, toDate } from "@/lib/date-like";
48
50
  import { unwrapResult } from "@/lib/protocol/result";
@@ -78,10 +80,11 @@ import { BotTool } from "./bot-tool";
78
80
  * state.
79
81
  *
80
82
  * 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.
83
+ * on the chip itself, with its buttons; what a bot is doing right now sits beside its
84
+ * face, and what passed between two parties rides above one for a moment
85
+ * (useHandoffs). Opening the room is for reading a thread, never for answering a
86
+ * chevron that reveals the rows would put a click in front of the one thing this
87
+ * corner exists to remove.
85
88
  *
86
89
  * memo: the parent re-renders per transcript chunk and this reads only its store.
87
90
  */
@@ -99,12 +102,13 @@ export const BotRoom = memo(function BotRoom() {
99
102
  ? (newest.find((entry) => entry.id === picked) ?? null)
100
103
  : null;
101
104
 
102
- const [beat, announce] = useBeats();
105
+ const [bubbles, handoff] = useHandoffs();
106
+ const { crew, more } = useMemo(() => crewOf(bots, tasks), [bots, tasks]);
103
107
 
104
- // What each task was, and what each bot last said, at the previous sync. Only
105
- // changes since then are announced.
108
+ // What each task was, and which hand-offs had already landed, at the previous
109
+ // sync. Only what changed since then just happened.
106
110
  const known = useRef<Map<string, TaskViewStatus> | null>(null);
107
- const said = useRef(new Map<string, string>());
111
+ const passed = useRef(new Set<string>());
108
112
 
109
113
  useEffect(() => {
110
114
  if (!botTasks.primed()) return;
@@ -112,46 +116,67 @@ export const BotRoom = memo(function BotRoom() {
112
116
  const was = known.current;
113
117
  known.current = now;
114
118
 
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);
119
+ const seen = new Set<string>();
120
+ for (const task of tasks) {
121
+ for (const line of task.lines) {
122
+ if (line.kind === "ask" && line.to) seen.add(line.id);
123
+ }
118
124
  }
119
- const heard = said.current;
120
- said.current = lines;
125
+ const had = passed.current;
126
+ passed.current = seen;
121
127
 
122
- // The first list only seeds the two maps: nothing on it just happened.
128
+ // The first list only seeds the two: nothing on it just happened.
123
129
  if (!was) return;
124
130
 
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,
131
+ // Bot to bot. A giver handing work to several at once would put a bubble
132
+ // over each of them, 20px apart, so a whole round is drawn once over the
133
+ // giver and the faces that took the work are awake, which says who.
134
+ const rounds = new Map<
135
+ string,
136
+ { from: BotRef; to: BotRef; text: string }[]
137
+ >();
138
+ for (const task of tasks) {
139
+ for (const line of task.lines) {
140
+ if (line.kind !== "ask" || !line.to || had.has(line.id)) continue;
141
+ const round = rounds.get(line.bot.name) ?? [];
142
+ round.push({ from: line.bot, to: line.to, text: line.text });
143
+ rounds.set(line.bot.name, round);
144
+ }
145
+ }
146
+ for (const [giver, round] of rounds) {
147
+ if (round.length === 1) {
148
+ handoff({
149
+ at: round[0].to.name,
150
+ from: round[0].from,
151
+ text: clipWord(round[0].text),
152
+ });
153
+ continue;
154
+ }
155
+ handoff({
156
+ at: giver,
157
+ from: round[0].from,
158
+ text: `sent ${round.length} parts out`,
137
159
  });
138
160
  }
139
161
 
140
162
  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) {
163
+ if (was.get(task.id) === task.status || task.status !== "working")
146
164
  continue;
147
- }
148
- announce({
149
- bot: task.bot.name,
150
- label: task.label,
151
- ...beatFor(task, before === undefined),
152
- });
165
+ // A job that was never seen is one arriving; one that was is you having
166
+ // answered it. The mark says who gave it away — and a job typed into the
167
+ // compose box is Thursday's here too, because a task row does not record
168
+ // which of the two started it.
169
+ handoff(
170
+ was.has(task.id)
171
+ ? { at: task.bot.name, from: null, text: "picked it back up" }
172
+ : {
173
+ at: task.bot.name,
174
+ from: THURSDAY,
175
+ text: `took on \u201c${clipWord(task.label)}\u201d`,
176
+ },
177
+ );
153
178
  }
154
- }, [tasks, announce]);
179
+ }, [tasks, handoff]);
155
180
 
156
181
  // Opening a finished thread marks it reported. The key includes updatedAt: a
157
182
  // follow-up request finishes again and that result is unread. markReported does not
@@ -181,13 +206,6 @@ export const BotRoom = memo(function BotRoom() {
181
206
  const busy = tasks.filter((entry) => entry.status === "working").length;
182
207
  const attention = newest.filter(needsYou);
183
208
  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
- );
191
209
 
192
210
  const closeCompose = useCallback(() => setComposing(false), []);
193
211
 
@@ -241,13 +259,14 @@ export const BotRoom = memo(function BotRoom() {
241
259
  </div>
242
260
  ) : (
243
261
  <Chip
262
+ crew={crew}
263
+ more={more}
264
+ bubbles={bubbles}
244
265
  bots={bots}
245
266
  rows={attention}
246
- waiting={waiting}
247
267
  count={tasks.length}
248
268
  busy={busy}
249
269
  pending={pending}
250
- beat={beat}
251
270
  composing={composing}
252
271
  onCompose={() => setComposing(true)}
253
272
  onCloseCompose={closeCompose}
@@ -266,103 +285,160 @@ export const BotRoom = memo(function BotRoom() {
266
285
  );
267
286
  });
268
287
 
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;
288
+ /** Faces the row draws before the count takes over. */
289
+ const CREW_MAX = 10;
290
+ /** Stable identity for a row that can never have one up. */
291
+ const EMPTY_BUBBLES: Map<string, Handoff> = new Map();
292
+ /** How long a hand-off stays above a face, ms. */
293
+ const HANDOFF_MS = 3400;
294
+ /** A step is a glance, not a sentence. */
295
+ const WORD_MAX = 28;
296
+
297
+ /** One face in the row, and what it is doing. */
298
+ type CrewFace = {
299
+ name: string;
300
+ icon?: BotIcon | null;
301
+ /** Working or waiting: both are awake. */
302
+ awake: boolean;
303
+ /** Waiting on an answer; carries the dot. */
304
+ waiting: boolean;
305
+ /** The step it is on, in the model's own words. Null when it is not working. */
306
+ word: string | null;
307
+ /** A stand-in for an install with no bots; dimmed with the rest. */
308
+ standIn?: boolean;
309
+ };
273
310
 
274
311
  /**
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.
312
+ * Something passed between two parties. This is what a bubble is for, and the
313
+ * only thing: a step is the bot working alone and belongs beside its face.
278
314
  */
279
- type Beat = {
280
- bot: string;
281
- label: string;
282
- what: string;
283
- tone?: string;
284
- live?: boolean;
315
+ type Handoff = {
316
+ /** Whose face it points at. */
317
+ at: string;
318
+ /** Who gave it away. Null when it was the user, who has no mark. */
319
+ from: BotRef | null;
320
+ text: string;
321
+ };
322
+
323
+ const clipWord = (text: string) => {
324
+ const one = plainText(text).replace(/\s+/g, " ").trim();
325
+ return one.length > WORD_MAX ? `${one.slice(0, WORD_MAX - 1)}…` : one;
285
326
  };
286
327
 
287
328
  /**
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.
329
+ * Hand-offs currently up, one per face. A second one to the same face replaces
330
+ * it rather than queueing: the newest thing that landed there is the true one.
331
+ * Different faces hold their own at the same time the fan-out that would make
332
+ * two of them collide is folded into one before it gets here (BotRoom).
295
333
  */
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>[]>([]);
334
+ function useHandoffs(): [Map<string, Handoff>, (one: Handoff) => void] {
335
+ const [up, setUp] = useState<Map<string, Handoff>>(new Map());
336
+ const timers = useRef(new Map<string, ReturnType<typeof setTimeout>>());
301
337
 
302
338
  useEffect(
303
339
  () => () => {
304
- for (const timer of timers.current) clearTimeout(timer);
340
+ for (const timer of timers.current.values()) clearTimeout(timer);
305
341
  },
306
342
  [],
307
343
  );
308
344
 
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(
345
+ const show = useCallback((one: Handoff) => {
346
+ setUp((was) => new Map(was).set(one.at, one));
347
+ const held = timers.current.get(one.at);
348
+ if (held) clearTimeout(held);
349
+ timers.current.set(
350
+ one.at,
318
351
  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),
352
+ timers.current.delete(one.at);
353
+ setUp((was) => {
354
+ const next = new Map(was);
355
+ next.delete(one.at);
356
+ return next;
357
+ });
358
+ }, HANDOFF_MS),
327
359
  );
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
360
  }, []);
336
361
 
337
- return [now, announce] as const;
362
+ return [up, show];
363
+ }
364
+
365
+ /** The step a bot is on, as the model labelled it. */
366
+ function wordOf(line: Chatter | null): string | null {
367
+ if (!line) return null;
368
+ if (line.kind === "tool" && line.tool) {
369
+ // The model's own label when it wrote one, else the raw call.
370
+ return clipWord(line.tool.note ?? `${line.tool.name} · ${line.tool.input}`);
371
+ }
372
+ return line.kind === "say" ? clipWord(line.text) : null;
338
373
  }
339
374
 
340
375
  /**
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).
376
+ * Who is in the row, in the order it is drawn.
377
+ *
378
+ * Anyone moving sorts to the front, so the count at the tail only ever hides
379
+ * idle bots — a bot with something to say always has a face to say it from,
380
+ * which is what lets a hand-off point at one. A bot that spoke inside somebody
381
+ * else's job is in the room too, whether or not it owns a task (latestPerBot).
343
382
  */
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
- };
383
+ function crewOf(
384
+ bots: Bot[] | undefined,
385
+ tasks: TaskView[],
386
+ ): { crew: CrewFace[]; more: number } {
387
+ const live = new Map<string, { waiting: boolean; word: string | null }>();
388
+ for (const entry of latestPerBot(tasks)) {
389
+ const { task, bot, line } = entry;
390
+ // Waiting outranks working: a bot that stopped to ask is not on a step.
391
+ if (task.status === "waiting" && task.bot.name === bot.name) {
392
+ live.set(bot.name, { waiting: true, word: null });
393
+ continue;
394
+ }
395
+ if (task.status !== "working" || live.get(bot.name)?.waiting) continue;
396
+ live.set(bot.name, { waiting: false, word: wordOf(line) });
397
+ }
398
+
399
+ const named = new Map<string, CrewFace>();
400
+ for (const bot of bots ?? []) {
401
+ named.set(bot.name, {
402
+ name: bot.name,
403
+ icon: bot.icon,
404
+ awake: live.has(bot.name),
405
+ waiting: live.get(bot.name)?.waiting ?? false,
406
+ word: live.get(bot.name)?.word ?? null,
407
+ });
354
408
  }
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
- };
409
+ // A bot that is working but not on the roster (the row-less default) still
410
+ // has a face: the row is who is here, not who is configured.
411
+ for (const [name, doing] of live) {
412
+ if (named.has(name)) continue;
413
+ named.set(name, {
414
+ name,
415
+ icon: null,
416
+ awake: true,
417
+ waiting: doing.waiting,
418
+ word: doing.word,
419
+ });
362
420
  }
363
- return task.status === "failed"
364
- ? { what: `${who} could not finish`, tone: "text-destructive" }
365
- : { what: `${who} finished` };
421
+
422
+ // A fresh install has one worker and one silhouette says "one bot", which is
423
+ // the wrong thing to say about a room. Stand-ins fill it out and are dimmed.
424
+ const roster = [...named.values()];
425
+ if (!roster.length) return { crew: GHOSTS.slice(0, FLOOR), more: 0 };
426
+ for (const ghost of GHOSTS) {
427
+ if (roster.length >= FLOOR) break;
428
+ if (!named.has(ghost.name)) roster.push(ghost);
429
+ }
430
+
431
+ const rank = (face: CrewFace) =>
432
+ face.waiting ? 0 : face.awake ? 1 : face.standIn ? 3 : 2;
433
+ const sorted = roster
434
+ .map((face, at) => ({ face, at }))
435
+ .sort((a, b) => rank(a.face) - rank(b.face) || a.at - b.at)
436
+ .map((one) => one.face);
437
+
438
+ return {
439
+ crew: sorted.slice(0, CREW_MAX),
440
+ more: Math.max(0, sorted.length - CREW_MAX),
441
+ };
366
442
  }
367
443
 
368
444
  /** Blanks the question in the reply box when it already is the last thread line (budget stops). */
@@ -384,7 +460,13 @@ function askFor(task: TaskView): TaskView["ask"] {
384
460
  */
385
461
  const needsYou = (task: TaskView) => task.status === "waiting";
386
462
 
387
- /** What the chip says when nothing just happened, most urgent first. */
463
+ /**
464
+ * What the room itself is doing, and nothing else — the right side of the pill.
465
+ *
466
+ * Only `waiting on you` sweeps. A shine means something is happening right now,
467
+ * which is what the words beside the faces are for; the spinner already says a
468
+ * job is running, and two things sweeping in one pill read as a loading screen.
469
+ */
388
470
  function restingState({
389
471
  count,
390
472
  busy,
@@ -393,58 +475,66 @@ function restingState({
393
475
  count: number;
394
476
  busy: number;
395
477
  pending: number;
396
- }): { text: string; tone?: string; shine?: "amber" | "muted" } {
478
+ }): { text: string; tone: string; shine: boolean } {
397
479
  if (pending > 0)
398
480
  return {
399
481
  text: pending === 1 ? "waiting on you" : `${pending} waiting on you`,
400
482
  tone: WAITING_INK,
401
- shine: "amber",
483
+ shine: true,
402
484
  };
403
485
  if (busy > 0)
404
486
  return {
405
487
  text: busy === 1 ? "working" : `${busy} running`,
406
488
  tone: "text-muted-foreground",
407
- shine: "muted",
489
+ shine: false,
408
490
  };
409
- if (count > 0) return { text: "all done", tone: "text-muted-foreground" };
410
- return { text: "no jobs yet", tone: "text-muted-foreground" };
491
+ if (count > 0)
492
+ return { text: "all done", tone: "text-muted-foreground", shine: false };
493
+ return { text: "no jobs yet", tone: "text-muted-foreground", shine: false };
411
494
  }
412
495
 
413
496
  /**
414
497
  * The room folded into one object in the corner.
415
498
  *
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.
499
+ * The row carries two facts and they never take each other's place. On the
500
+ * **left**, who is here and what each of them is doing: a face, and the shiny
501
+ * text beside it. On the **right**, what the room itself is doing, with its
502
+ * glyph 6px away one fact, so nothing gets to come between them and nothing
503
+ * takes the glyph away. Anything that passes between two parties is neither, so
504
+ * it rides above a face in a bubble.
505
+ *
506
+ * While something is waiting on an answer its rows are grown in place above the
507
+ * row. Nothing here sits behind a disclosure: they appear because there is
508
+ * something to answer and leave when it is answered. The pill's own click opens
509
+ * the room, to read.
422
510
  *
423
511
  * The corner radius does not animate with the height: interpolating a pill radius
424
512
  * down to a card radius while the box is also resizing warps the corners in flight.
425
513
  */
426
514
  function Chip({
515
+ crew,
516
+ more,
517
+ bubbles,
427
518
  bots,
428
519
  rows,
429
- waiting,
430
520
  count,
431
521
  busy,
432
522
  pending,
433
- beat,
434
523
  composing,
435
524
  onCompose,
436
525
  onCloseCompose,
437
526
  onPick,
438
527
  onOpen,
439
528
  }: {
529
+ crew: CrewFace[];
530
+ more: number;
531
+ bubbles: Map<string, Handoff>;
440
532
  bots?: Bot[];
441
533
  /** Everything waiting on an answer, newest first. */
442
534
  rows: TaskView[];
443
- waiting: BotRef[];
444
535
  count: number;
445
536
  busy: number;
446
537
  pending: number;
447
- beat: Beat | null;
448
538
  composing: boolean;
449
539
  onCompose: () => void;
450
540
  onCloseCompose: () => void;
@@ -457,7 +547,9 @@ function Chip({
457
547
  return (
458
548
  <div
459
549
  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",
550
+ // Not `overflow-hidden`: a hand-off bubble stands above the row, outside
551
+ // this box. The growing part clips itself instead.
552
+ "pointer-events-auto w-fit max-w-full bg-background/78 ring-1 ring-border/50 backdrop-blur-md transition-shadow duration-300",
461
553
  grown
462
554
  ? "min-w-96 rounded-3xl shadow-lg shadow-black/8"
463
555
  : "rounded-full shadow-sm shadow-black/3",
@@ -466,7 +558,7 @@ function Chip({
466
558
  {/* One box, two heights: the rows grow out of nothing rather than appearing. */}
467
559
  <div
468
560
  className={cn(
469
- "grid transition-[grid-template-rows] duration-300 ease-out",
561
+ "grid overflow-hidden transition-[grid-template-rows] duration-300 ease-out",
470
562
  grown ? "grid-rows-[1fr]" : "grid-rows-[0fr]",
471
563
  )}
472
564
  >
@@ -501,79 +593,53 @@ function Chip({
501
593
  {/* The pill row. Same geometry either way, so the card shrinks into it. */}
502
594
  {/* px-3 is the chip's one rail: the faces here, the section line and every
503
595
  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")}>
596
+ is why the well carries no box of its own. */}
597
+ <div className="flex items-center gap-2 px-3 py-1.5">
508
598
  <button
509
599
  type="button"
510
600
  onClick={onOpen}
511
601
  aria-label={count ? `Tasks (${count})` : "Bots"}
512
602
  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
603
  >
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>
604
+ <Crew crew={crew} more={more} bubbles={bubbles} />
605
+
606
+ {/* The room's own state and its glyph: one group, and the only thing on
607
+ the right. `ml-auto` keeps it there when the crew says nothing. */}
608
+ <span className="ml-auto flex h-7 shrink-0 items-center gap-1.5">
609
+ {state.shine ? (
610
+ // Neither waiting nor running is a state at rest, but only one of
611
+ // them needs you. The colours are classes because they differ per
612
+ // theme, and ShinyText reads the variables they set.
613
+ <span className="block [--rest:var(--color-amber-700)] [--shine:var(--color-amber-400)] dark:[--rest:var(--color-amber-400)] dark:[--shine:var(--color-amber-100)]">
614
+ <ShinyText
615
+ text={state.text}
616
+ speed={2.6}
617
+ color="var(--rest)"
618
+ shineColor="var(--shine)"
619
+ className="truncate text-[14px] leading-5 tracking-[-0.15px]"
620
+ />
621
+ </span>
622
+ ) : (
623
+ <span
624
+ key={state.text}
625
+ className={cn(
626
+ "block animate-in truncate text-[14px] leading-5 tracking-[-0.15px] fade-in duration-300",
627
+ state.tone,
540
628
  )}
629
+ >
630
+ {state.text}
541
631
  </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>
632
+ )}
633
+ {/* Always 16px, empty or not: a glyph that comes and goes moves the
634
+ sentence's right end even when the sentence has not changed. */}
635
+ <span className="grid size-4 shrink-0 place-items-center">
636
+ {pending === 0 && busy > 0 && (
637
+ <Loader2 className="size-4 animate-spin text-muted-foreground/70" />
638
+ )}
639
+ {pending === 0 && busy === 0 && count > 0 && (
640
+ <Check className="size-4 text-muted-foreground/60" />
575
641
  )}
576
- </Lane>
642
+ </span>
577
643
  </span>
578
644
  </button>
579
645
 
@@ -582,36 +648,11 @@ function Chip({
582
648
  <X className="size-3.5" />
583
649
  </RoundButton>
584
650
  )}
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
651
  </div>
592
652
  </div>
593
653
  );
594
654
  }
595
655
 
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
656
  /** Starts a job. Lives in the header of whichever list is on screen. */
616
657
  function ComposeButton({ onClick }: { onClick: () => void }) {
617
658
  return (
@@ -775,94 +816,147 @@ function Compose({ bots, onDone }: { bots?: Bot[]; onDone: () => void }) {
775
816
  }
776
817
 
777
818
  /** 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
- }));
819
+ const GHOSTS: CrewFace[] = ["alto", "brio", "cinder", "delta"].map(
820
+ (seed, index) => ({
821
+ name: seed,
822
+ icon: {
823
+ color: MARK_PALETTE[Math.floor((index * MARK_PALETTE.length) / 4)],
824
+ shape: MARK_SHAPES[index % MARK_SHAPES.length],
825
+ },
826
+ awake: false,
827
+ waiting: false,
828
+ word: null,
829
+ standIn: true,
830
+ }),
831
+ );
785
832
 
786
833
  /** A single face reads as one worker, not as a crew; below this the row is padded out. */
787
- const CREW = 3;
834
+ const FLOOR = 3;
788
835
 
789
- /** The crew stacked behind each other; GHOSTS stand in when there are no bots. */
790
- function Faces({
791
- bots,
792
- waiting = [],
793
- only,
836
+ /**
837
+ * The crew, and what each of them is doing.
838
+ *
839
+ * Two things share this row and never take each other's place: a face says
840
+ * **who**, and the shiny text beside it says **what**, for as long as that bot
841
+ * is on that step. Both are derived — there is no queue and no cap, because
842
+ * every bot that is working is saying something true at the same time.
843
+ *
844
+ * A face that is moving is awake: lifted, swollen a little, and glowing in its
845
+ * own colour. That layer alone survives every collision — three bots at once
846
+ * are three awake faces, with no order to decide.
847
+ */
848
+ function Crew({
849
+ crew,
850
+ more,
851
+ bubbles,
794
852
  }: {
795
- bots?: Bot[];
796
- waiting?: BotRef[];
797
- /** While one bot is speaking the crowd narrows to it, then fans back out. */
798
- only?: string;
853
+ crew: CrewFace[];
854
+ more: number;
855
+ /** Hand-offs currently up, keyed by the face they point at. */
856
+ bubbles: Map<string, Handoff>;
799
857
  }) {
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
858
  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) => (
859
+ <span className="flex min-w-0 shrink items-center">
860
+ {crew.map((face, index) => {
861
+ const bubble = bubbles.get(face.name) ?? null;
862
+ return (
863
+ <Fragment key={face.name}>
864
+ <span
865
+ // Silhouettes overlapped, never ringed: a ring needs a circle and
866
+ // these are not circles. Earlier faces sit on top, so the dot on a
867
+ // waiting face is never covered by its neighbour.
868
+ className={cn(
869
+ "relative shrink-0 transition-[margin,transform] duration-500 ease-out",
870
+ // A word to the left has already broken the shingle.
871
+ index > 0 && !crew[index - 1].word && "-ml-2",
872
+ face.standIn && "opacity-35",
873
+ face.awake && "-translate-y-0.5 scale-110",
874
+ )}
875
+ style={{
876
+ zIndex: crew.length - index,
877
+ color: face.icon?.color ?? undefined,
878
+ // The glow is the bot's own colour, so who is moving reads before
879
+ // any word does. currentColor keeps it right when nobody chose one.
880
+ filter: face.awake
881
+ ? "drop-shadow(0 0 5px color-mix(in srgb, currentColor 55%, transparent))"
882
+ : undefined,
883
+ }}
884
+ >
885
+ <BotMark
886
+ size={28}
887
+ seed={face.name}
888
+ vary={face.name}
889
+ color={face.icon?.color}
890
+ shape={face.icon?.shape}
891
+ outline={face.icon?.outline}
892
+ notify={face.waiting}
893
+ />
894
+ {bubble && <HandoffBubble handoff={bubble} />}
895
+ </span>
896
+ {face.word && (
897
+ // No box: the shine is what says this is happening right now, so a
898
+ // capsule around it was drawing a second time what the sweep says.
899
+ <span className="mx-2.5 min-w-0 shrink truncate">
900
+ <ShinyText
901
+ text={face.word}
902
+ speed={2.6}
903
+ color="var(--muted-foreground)"
904
+ shineColor="var(--foreground)"
905
+ className="truncate text-[13px] leading-5 tracking-[-0.1px]"
906
+ />
907
+ </span>
908
+ )}
909
+ </Fragment>
910
+ );
911
+ })}
912
+ {more > 0 && (
913
+ // Past CREW_MAX the row stops growing. Only idle bots are ever behind it:
914
+ // anyone moving sorted to the front.
843
915
  <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
916
  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",
917
+ "grid size-7 shrink-0 place-items-center rounded-[9px] bg-muted font-mono text-[10px] text-muted-foreground ring-1 ring-border/50",
918
+ !crew.at(-1)?.word && "-ml-2",
852
919
  )}
853
- style={{ zIndex: crew.length - index }}
920
+ title={`${more} more`}
854
921
  >
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
- />
922
+ +{more}
864
923
  </span>
865
- ))}
924
+ )}
925
+ </span>
926
+ );
927
+ }
928
+
929
+ /**
930
+ * A hand-off, over the face it landed on. The mark at its head is whoever gave
931
+ * it away — Thursday, another bot, or nobody at all when it was you.
932
+ *
933
+ * It sits outside the pill's box on purpose, in space the corner is not using;
934
+ * the chip cannot clip its own children while one is up.
935
+ */
936
+ function HandoffBubble({ handoff }: { handoff: Handoff }) {
937
+ return (
938
+ <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">
939
+ <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">
940
+ {handoff.from?.name === THURSDAY.name ? (
941
+ <ThursdayMark size={14} className="shrink-0 opacity-75" />
942
+ ) : (
943
+ handoff.from && (
944
+ <BotMark
945
+ size={14}
946
+ seed={handoff.from.name}
947
+ vary={handoff.from.name}
948
+ color={handoff.from.icon?.color}
949
+ shape={handoff.from.icon?.shape}
950
+ outline={handoff.from.icon?.outline}
951
+ className="shrink-0 opacity-75"
952
+ />
953
+ )
954
+ )}
955
+ {handoff.text}
956
+ </span>
957
+ {/* Two triangles: the ring's, then the fill's a pixel over it. */}
958
+ <span className="-mt-px size-0 border-x-[5px] border-t-[6px] border-x-transparent border-t-border" />
959
+ <span className="-mt-[6.5px] size-0 border-x-[5px] border-t-[6px] border-x-transparent border-t-background" />
866
960
  </span>
867
961
  );
868
962
  }
@@ -1035,9 +1129,10 @@ function State({ task }: { task: TaskView }) {
1035
1129
  }
1036
1130
 
1037
1131
  function Empty({ bots }: { bots?: Bot[] }) {
1132
+ const { crew, more } = crewOf(bots, []);
1038
1133
  return (
1039
1134
  <div className="flex flex-col items-center gap-3 px-6 pt-3 pb-4 text-center">
1040
- <Faces bots={bots} />
1135
+ <Crew crew={crew} more={more} bubbles={EMPTY_BUBBLES} />
1041
1136
  {bots?.length ? (
1042
1137
  <p className="text-[12px] text-muted-foreground">
1043
1138
  Nothing handed over yet — ask for something that takes a while.
@@ -1369,7 +1464,11 @@ function Request({ task }: { task: TaskView }) {
1369
1464
  );
1370
1465
  }
1371
1466
 
1372
- /** Thursday's own face; she is not a bot and has no row to read one from. */
1467
+ /**
1468
+ * Thursday's own face; she is not a bot and has no row to read one from. Also
1469
+ * who a hand-off came from when it came from the call — her mark is drawn by
1470
+ * her own domain (features/thursday), so this only has to name her.
1471
+ */
1373
1472
  const THURSDAY: BotRef = { name: "Thursday" };
1374
1473
 
1375
1474
  /**