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
@@ -1,6 +1,16 @@
1
1
  "use client";
2
2
 
3
- import { Check, History, Plus, Trash2, Wrench, X } from "lucide-react";
3
+ import {
4
+ Check,
5
+ ChevronRight,
6
+ CircleAlert,
7
+ FolderOpen,
8
+ History,
9
+ Plus,
10
+ Trash2,
11
+ Wrench,
12
+ X,
13
+ } from "lucide-react";
4
14
  import { type ReactNode, useState } from "react";
5
15
  import { queryKey } from "@/app/api/query-key";
6
16
  import { Button } from "@/components/ui/button";
@@ -13,43 +23,80 @@ import {
13
23
  CommandList,
14
24
  } from "@/components/ui/command";
15
25
  import { Input } from "@/components/ui/input";
26
+ import {
27
+ InputGroup,
28
+ InputGroupAddon,
29
+ InputGroupInput,
30
+ InputGroupText,
31
+ } from "@/components/ui/input-group";
16
32
  import { notify } from "@/components/ui/notify";
17
- import ShinyText from "@/components/ui/shiny-text";
18
- import { Swatch } from "@/components/ui/swatch";
33
+ import { ShinyText } from "@/components/ui/shiny-text";
34
+ import { Skeleton } from "@/components/ui/skeleton";
35
+ import { Switch } from "@/components/ui/switch";
19
36
  import { Textarea } from "@/components/ui/textarea";
37
+ import { APP_NAME, BOT_RUN, PROMPT_CROWDED } from "@/config";
20
38
  import { ModelPicker } from "@/features/ai/components/model-picker";
21
- import type { TextModelProviderId } from "@/features/ai/model.schema";
39
+ import {
40
+ compactAtFor,
41
+ contextWindowOf,
42
+ type GatewayModel,
43
+ type TextModelProviderId,
44
+ } from "@/features/ai/model.schema";
22
45
  import {
23
46
  createBotAction,
24
47
  createSeedBotsAction,
25
48
  deleteBotAction,
49
+ setBotMemoryOnAction,
26
50
  updateBotAction,
27
51
  } from "@/features/bot/bot.action";
28
52
  import {
29
53
  type Bot,
30
54
  type BotForm,
31
55
  type BotIcon,
32
- isBudgetAsk,
56
+ type BotMemory,
57
+ type BotMemoryFile,
33
58
  MAX_PINNED_TOOLS,
34
59
  randomBotIcon,
35
60
  type Task,
36
61
  } from "@/features/bot/bot.schema";
37
- import { BOT_SEEDS, type BotSeed } from "@/features/bot/bot.seed";
62
+ import {
63
+ BOT_SEEDS,
64
+ type BotSeed,
65
+ rollSeedColors,
66
+ } from "@/features/bot/bot.seed";
38
67
  import { BotMark } from "@/features/bot/components/bot-mark";
68
+ import { MarkPalette } from "@/features/bot/components/mark-palette";
69
+ import {
70
+ type ConfigStatus,
71
+ isConfigSet,
72
+ MEDIA_MODEL_KEYS,
73
+ mediaModelWords,
74
+ } from "@/features/config/config.const";
39
75
  import {
76
+ SettingDialogContent,
40
77
  SettingError,
78
+ SettingNote,
41
79
  SettingPanes,
42
80
  SettingPanesSkeleton,
43
81
  SettingRailNote,
44
82
  } from "@/features/settings/components/setting-ui";
45
83
  import { openSettings } from "@/features/settings/settings.store";
84
+ import {
85
+ FileBody,
86
+ useFileText,
87
+ } from "@/features/workspace/components/file-view";
88
+ import { viewKindOf } from "@/features/workspace/file-kind";
89
+ import {
90
+ deleteWorkspaceFileAction,
91
+ openFileAction,
92
+ } from "@/features/workspace/workspace.action";
46
93
  import { useObjectState } from "@/hooks/use-object-state";
47
94
  import { type DateLike, shortAgo, whenOf } from "@/lib/date-like";
48
95
  import { COMMON_VALIDATE } from "@/lib/limits";
49
96
  import { useServerAction } from "@/lib/protocol/use-server-action";
50
97
  import { revalidate, useServerRoute } from "@/lib/protocol/use-server-route";
51
98
  import { cn, formatCount, WAITING_INK } from "@/lib/utils";
52
- import { MARK_PALETTE, MARK_SHAPES, MARK_SYSTEM } from "../mark.const";
99
+ import { MARK_SHAPES, MARK_SYSTEM } from "../mark.const";
53
100
 
54
101
  /**
55
102
  * The bots Thursday hands background work to. Roster on the left, the picked bot's
@@ -64,35 +111,56 @@ export function BotSetting() {
64
111
  /** Picked roster entry: a bot name, NEW, or null for the first bot. */
65
112
  const [picked, setPicked] = useState<string | null>(null);
66
113
 
114
+ /**
115
+ * One roll for this screen, in BOT_SEEDS order. The faces on the invite, in the
116
+ * picker and on the bot that gets created are then the same colour, because
117
+ * `createSeedBotsAction` takes it (bot.seed rollSeedColors rolls per install so
118
+ * no two rosters look alike; a seed itself carries none).
119
+ */
120
+ const [inks] = useState(rollSeedColors);
121
+
67
122
  if (isLoading) return <SettingPanesSkeleton />;
68
123
  if (error) return <SettingError message={error.message} />;
69
124
 
70
125
  const drafting = picked === NEW;
126
+ const on = bots.filter((bot) => !bot.disabled).length;
71
127
  const have = new Set(bots.map((bot) => bot.name));
72
128
  const missing = BOT_SEEDS.filter((seed) => !have.has(seed.name));
73
- const current =
74
- drafting || picked === SEEDS
75
- ? null
76
- : (bots.find((bot) => bot.name === picked) ?? bots[0] ?? null);
129
+ const current = drafting
130
+ ? null
131
+ : (bots.find((bot) => bot.name === picked) ?? bots[0] ?? null);
77
132
 
78
133
  return (
79
134
  <SettingPanes
80
- footer={picked === SEEDS ? null : <BotRail bot={current} />}
135
+ footer={bots.length === 0 ? null : <BotRail bot={current} />}
81
136
  left={
82
137
  <div className="flex flex-col py-2">
83
- <button
84
- type="button"
85
- onClick={() => setPicked(NEW)}
86
- className={cn(
87
- "mx-2 mb-1 flex items-center gap-2 rounded-md px-2 py-1.5 text-left text-[13px] outline-none focus-visible:ring-3 focus-visible:ring-ring/50 focus-visible:ring-inset",
88
- drafting
89
- ? "bg-secondary text-foreground"
90
- : "text-muted-foreground hover:bg-muted/60 hover:text-foreground",
138
+ {/* The two ways to get a bot, on one line and apart from the roster
139
+ under it: a seed is not a bot you have */}
140
+ <div className="mx-2 mb-1 flex items-center gap-1">
141
+ <button
142
+ type="button"
143
+ onClick={() => setPicked(NEW)}
144
+ className={cn(
145
+ "flex flex-1 items-center gap-2 rounded-md px-2 py-1.5 text-left text-[13px] outline-none focus-visible:ring-3 focus-visible:ring-ring/50 focus-visible:ring-inset",
146
+ drafting
147
+ ? "bg-secondary text-foreground"
148
+ : "text-muted-foreground hover:bg-muted/60 hover:text-foreground",
149
+ )}
150
+ >
151
+ <Plus className="size-3.5 shrink-0" />
152
+ New bot
153
+ </button>
154
+
155
+ {bots.length > 0 && missing.length > 0 && (
156
+ <SeedInvite
157
+ missing={missing}
158
+ inks={inks}
159
+ have={have}
160
+ onDone={(name) => setPicked(name)}
161
+ />
91
162
  )}
92
- >
93
- <Plus className="size-3.5 shrink-0" />
94
- New bot
95
- </button>
163
+ </div>
96
164
 
97
165
  {bots.map((bot) => (
98
166
  <RosterRow
@@ -104,19 +172,17 @@ export function BotSetting() {
104
172
  />
105
173
  ))}
106
174
 
107
- {missing.length > 0 && (
108
- <SeedEntry
109
- missing={missing}
110
- active={picked === SEEDS}
111
- onPick={() => setPicked(SEEDS)}
112
- />
175
+ {/* Only bots that are on: a switched-off one is in no prompt to crowd. */}
176
+ {on > PROMPT_CROWDED.bots && (
177
+ <SettingNote className="mx-3 mt-2 leading-relaxed">
178
+ {on} bots are on. Each is a line in every prompt, and one more for
179
+ Thursday to choose between.
180
+ </SettingNote>
113
181
  )}
114
182
  </div>
115
183
  }
116
184
  right={
117
- picked === SEEDS ? (
118
- <SeedPackage have={have} onDone={(name) => setPicked(name)} />
119
- ) : drafting ? (
185
+ drafting ? (
120
186
  <BotPage
121
187
  key="new"
122
188
  jobs={[]}
@@ -133,6 +199,14 @@ export function BotSetting() {
133
199
  .slice(0, RECENT)}
134
200
  onDone={() => setPicked(null)}
135
201
  />
202
+ ) : missing.length > 0 ? (
203
+ // Nothing on the roster: the offer is the only thing this pane could
204
+ // hold, so it fills it rather than sitting in a dialog nobody opened
205
+ <SeedPackage
206
+ have={have}
207
+ inks={inks}
208
+ onDone={(name) => setPicked(name)}
209
+ />
136
210
  ) : (
137
211
  <div className="space-y-4 p-8">
138
212
  <p className="text-sm leading-relaxed text-muted-foreground">
@@ -153,8 +227,6 @@ export function BotSetting() {
153
227
 
154
228
  /** Roster selections that are not a bot; values no bot name can be. */
155
229
  const NEW = " new";
156
- const SEEDS = " seeds";
157
-
158
230
  /** Jobs shown under Recent on a bot's page. */
159
231
  const RECENT = 3;
160
232
 
@@ -171,7 +243,7 @@ function RosterRow({
171
243
  active: boolean;
172
244
  onPick: () => void;
173
245
  }) {
174
- const line = liveLine(job, bot.lastJobAt);
246
+ const line = liveLine(job, bot.lastJobAt, bot.disabled);
175
247
  return (
176
248
  <button
177
249
  type="button"
@@ -186,9 +258,14 @@ function RosterRow({
186
258
  {...markProps(bot.name, bot.icon)}
187
259
  state={job?.status === "running" ? "thinking" : "idle"}
188
260
  notify={job?.status === "waiting"}
189
- className="shrink-0"
261
+ className={cn("shrink-0", bot.disabled && "opacity-45")}
190
262
  />
191
- <span className="min-w-0 flex-1 space-y-0.5">
263
+ <span
264
+ className={cn(
265
+ "min-w-0 flex-1 space-y-0.5",
266
+ bot.disabled && "opacity-45",
267
+ )}
268
+ >
192
269
  <span
193
270
  className={cn(
194
271
  "block truncate text-[13px]",
@@ -198,23 +275,11 @@ function RosterRow({
198
275
  {bot.name}
199
276
  </span>
200
277
  {line.shine ? (
201
- // The colours come from CSS variables so they follow the theme.
202
- <span
203
- className={cn(
204
- "block",
205
- line.amber
206
- ? "[--rest:var(--color-amber-700)] [--shine:var(--color-amber-400)] dark:[--rest:var(--color-amber-400)] dark:[--shine:var(--color-amber-100)]"
207
- : "[--rest:var(--muted-foreground)] [--shine:var(--foreground)]",
208
- )}
209
- >
210
- <ShinyText
211
- text={line.text}
212
- speed={2.4}
213
- color="var(--rest)"
214
- shineColor="var(--shine)"
215
- className="truncate font-mono text-[11px] leading-4"
216
- />
217
- </span>
278
+ <ShinyText
279
+ text={line.text}
280
+ tone={line.amber ? "waiting" : "muted"}
281
+ className="block truncate font-mono text-[11px] leading-4"
282
+ />
218
283
  ) : (
219
284
  <span className="block truncate font-mono text-[11px] leading-4 text-muted-foreground">
220
285
  {line.text}
@@ -225,27 +290,35 @@ function RosterRow({
225
290
  );
226
291
  }
227
292
 
228
- /** What the bot is doing, most urgent first. A budget stop waits but is not amber (isBudgetAsk). */
293
+ /**
294
+ * What the bot is doing, most urgent first. Every wait is amber: the job moves
295
+ * again only when the user answers, whatever stopped it. Which kind of stop it
296
+ * was is the words' job, not the colour's.
297
+ */
229
298
  function liveLine(
230
299
  job: Task | null,
231
300
  lastJobAt: Bot["lastJobAt"],
301
+ disabled: boolean,
232
302
  ): {
233
303
  text: string;
234
304
  shine?: boolean;
235
305
  amber?: boolean;
236
306
  } {
307
+ if (job?.status === "waiting") {
308
+ return { text: "waiting on you", shine: true, amber: true };
309
+ }
310
+ if (job?.status === "running") {
311
+ return { text: `working · ${job.label}`, shine: true };
312
+ }
313
+ // Switched off, and no colour: off is the user's own choice, not something
314
+ // waiting on them, so the word carries it. It stands where the idle line
315
+ // would — the two cases above are a job it already had, which off never
316
+ // stopped, and hiding an ask behind "off" is how one goes unanswered.
317
+ if (disabled) return { text: "off" };
237
318
  if (!job) {
238
319
  if (!lastJobAt) return { text: "idle · no jobs yet" };
239
320
  return { text: `idle · last job ${sinceWord(lastJobAt)}` };
240
321
  }
241
- if (job.status === "waiting") {
242
- return isBudgetAsk(job.ask)
243
- ? { text: "out of steps" }
244
- : { text: "waiting on you", shine: true, amber: true };
245
- }
246
- if (job.status === "running") {
247
- return { text: `working · ${job.label}`, shine: true };
248
- }
249
322
  return { text: `idle · last job ${sinceWord(job.updatedAt)}` };
250
323
  }
251
324
 
@@ -254,86 +327,205 @@ function sinceWord(at: DateLike): string {
254
327
  const ago = shortAgo(at);
255
328
  return ago === "now" ? "just now" : `${ago} ago`;
256
329
  }
330
+ /**
331
+ * What a seed still needs before it can work, as one line, or null when it needs
332
+ * nothing. A missing studio model is not a fallback — the tool is absent — so the
333
+ * row says so before the bot finds out mid-job. Ticking is never blocked: the list
334
+ * states the cost, it does not cap it.
335
+ */
336
+ function unmetLine(
337
+ seed: BotSeed,
338
+ isSet: (key: string) => boolean,
339
+ ): string | null {
340
+ const unmet = (seed.requires ?? []).filter(
341
+ (kind) => !isSet(MEDIA_MODEL_KEYS[kind]),
342
+ );
343
+ if (!unmet.length) return null;
344
+ return `needs ${unmet.map(mediaModelWords).join(" and ")}`;
345
+ }
257
346
 
258
347
  /**
259
- * The ready-made bots (bot.seed) as one entry rather than three rows.
260
- *
261
- * They used to fill an empty roster, which made a roster with nothing in it look
262
- * full and made the row you clicked create rather than open. Here they sit behind
263
- * a single line carrying the faces of the ones you do not have, and it is shown
264
- * only while at least one is still on offer.
348
+ * The picker's state, shared by the two places the offer appears: the pane an
349
+ * empty roster opens on, and the dialog the roster's invite opens. Seeds are
350
+ * ticked already, because the set is the recommendation and unticking is the
351
+ * decision; one already on the roster is locked on and says so, since `createBot`
352
+ * would only answer "already exists", which is an error about something this
353
+ * screen already knows. One call creates all of them.
265
354
  */
266
- function SeedEntry({
267
- missing,
268
- active,
269
- onPick,
355
+ function useSeedPicks(
356
+ have: Set<string>,
357
+ inks: string[],
358
+ onDone: (name: string | null) => void,
359
+ ) {
360
+ const [off, setOff] = useState<Set<string>>(new Set());
361
+ const [add, adding] = useServerAction(createSeedBotsAction, {
362
+ onOk: (made) => {
363
+ revalidate(queryKey.bot);
364
+ onDone(made.created[0] ?? null);
365
+ },
366
+ });
367
+ // Same key the Models section and its badge read, so one fetch answers all three.
368
+ const { data: config } = useServerRoute<ConfigStatus[]>(queryKey.config);
369
+ const isSet = (key: string) => isConfigSet(config, key);
370
+
371
+ // Seeds not on the roster yet. `have` is the whole roster, so counting against
372
+ // its size goes negative the moment a bot nobody seeded is on it.
373
+ const addable = BOT_SEEDS.filter((seed) => !have.has(seed.name));
374
+ const wanted = addable.filter((seed) => !off.has(seed.name));
375
+
376
+ return {
377
+ isSet,
378
+ adding,
379
+ addable,
380
+ wanted,
381
+ ticked: (seed: BotSeed) => have.has(seed.name) || !off.has(seed.name),
382
+ toggle: (name: string) =>
383
+ setOff((was) => {
384
+ const next = new Set(was);
385
+ if (next.has(name)) next.delete(name);
386
+ else next.add(name);
387
+ return next;
388
+ }),
389
+ submit: () =>
390
+ add(
391
+ wanted.map((seed) => ({
392
+ name: seed.name,
393
+ color: inks[BOT_SEEDS.indexOf(seed)],
394
+ })),
395
+ ),
396
+ };
397
+ }
398
+
399
+ /** The rows themselves. Both wrappers draw these and supply their own chrome. */
400
+ function SeedRows({
401
+ have,
402
+ inks,
403
+ picks,
270
404
  }: {
271
- missing: BotSeed[];
272
- active: boolean;
273
- onPick: () => void;
405
+ have: Set<string>;
406
+ inks: string[];
407
+ picks: ReturnType<typeof useSeedPicks>;
274
408
  }) {
275
409
  return (
276
- <button
277
- type="button"
278
- onClick={onPick}
279
- className={cn(
280
- "mx-2 mt-1 flex items-center gap-2.5 rounded-md px-2 py-1.5 text-left outline-none focus-visible:ring-3 focus-visible:ring-ring/50 focus-visible:ring-inset",
281
- active ? "bg-secondary" : "hover:bg-muted/60",
282
- )}
283
- >
284
- <span className="flex shrink-0 items-center">
285
- {missing.map((seed, index) => (
286
- <span key={seed.name} className={cn("block", index > 0 && "-ml-1.5")}>
287
- <BotMark size={22} {...markProps(seed.name, seed.icon)} />
288
- </span>
289
- ))}
290
- </span>
291
- <span className="min-w-0 flex-1 space-y-0.5">
292
- <span
293
- className={cn(
294
- "block truncate text-[13px]",
295
- active ? "font-medium" : "text-foreground/90",
296
- )}
297
- >
298
- Ready-made
299
- </span>
300
- <span className="block truncate font-mono text-[10px] leading-4 text-muted-foreground">
301
- {missing.length === BOT_SEEDS.length
302
- ? `${missing.length} bots`
303
- : `${missing.length} left`}
304
- </span>
305
- </span>
306
- </button>
410
+ <div className="flex flex-col gap-2">
411
+ {BOT_SEEDS.map((seed, at) => {
412
+ const owned = have.has(seed.name);
413
+ const on = picks.ticked(seed);
414
+ const needs = unmetLine(seed, picks.isSet);
415
+ return (
416
+ <div
417
+ key={seed.name}
418
+ className={cn(
419
+ "rounded-xl ring-1 transition-colors",
420
+ owned
421
+ ? "opacity-45 ring-border/60"
422
+ : on
423
+ ? "ring-foreground"
424
+ : "ring-border/60",
425
+ )}
426
+ >
427
+ <button
428
+ type="button"
429
+ disabled={owned || picks.adding}
430
+ aria-pressed={on}
431
+ onClick={() => picks.toggle(seed.name)}
432
+ className="flex w-full items-center gap-3 rounded-xl px-3.5 py-2.5 text-left outline-none focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-default"
433
+ >
434
+ <BotMark
435
+ size={28}
436
+ {...markProps(seed.name, seed.icon)}
437
+ color={inks[at]}
438
+ className={cn(
439
+ "shrink-0 transition-opacity",
440
+ !on && "opacity-35",
441
+ )}
442
+ />
443
+ {/* The hint stays on one line: the list grows, and a wrapping
444
+ sentence per row is what made this read as a page of prose */}
445
+ <span className="min-w-0 flex-1 space-y-px">
446
+ <span className="block text-[14px] leading-[18px] font-medium">
447
+ {seed.name}
448
+ </span>
449
+ <span className="block truncate text-[12px] leading-[17px] text-muted-foreground">
450
+ {seed.hint}
451
+ </span>
452
+ </span>
453
+ {owned ? (
454
+ <span className="shrink-0 font-mono text-[11px] text-muted-foreground">
455
+ already added
456
+ </span>
457
+ ) : needs ? (
458
+ <span
459
+ className={cn(
460
+ "flex shrink-0 items-center gap-1.5 font-mono text-[11px]",
461
+ WAITING_INK,
462
+ )}
463
+ >
464
+ <CircleAlert className="size-3" />
465
+ {needs}
466
+ </span>
467
+ ) : null}
468
+ <span
469
+ className={cn(
470
+ "grid size-5 shrink-0 place-items-center rounded-full transition-colors",
471
+ on
472
+ ? "bg-foreground text-background"
473
+ : "ring-1 ring-border/60 ring-inset",
474
+ )}
475
+ >
476
+ {on && <Check className="size-3" />}
477
+ </span>
478
+ </button>
479
+ </div>
480
+ );
481
+ })}
482
+ </div>
483
+ );
484
+ }
485
+
486
+ /** Says what Add will do, so the count is checkable before the click. */
487
+ function SeedActions({
488
+ picks,
489
+ onCancel,
490
+ }: {
491
+ picks: ReturnType<typeof useSeedPicks>;
492
+ onCancel: () => void;
493
+ }) {
494
+ return (
495
+ <>
496
+ <Button variant="ghost" onClick={onCancel}>
497
+ Cancel
498
+ </Button>
499
+ <Button
500
+ loading={picks.adding}
501
+ disabled={picks.wanted.length === 0}
502
+ onClick={picks.submit}
503
+ >
504
+ <Plus />
505
+ {picks.wanted.length === 1
506
+ ? `Add ${picks.wanted[0].name}`
507
+ : `Add ${picks.wanted.length} bots`}
508
+ </Button>
509
+ </>
307
510
  );
308
511
  }
309
512
 
310
513
  /**
311
- * The package itself: the seeds ticked already, because the package is the
312
- * recommendation (`BotSeed.recommended`) and unticking is the decision. One
313
- * already on the roster is locked on and says so `createBot` would only answer
314
- * "already exists", which is an error about something this screen already knows.
315
- *
316
- * One call creates all of them; `createSeedBotsAction` has always taken an array.
514
+ * The offer as a pane: what an empty roster opens on, because nothing else could
515
+ * be there. A roster with bots on it gets `SeedInvite` and a dialog instead a
516
+ * row among the bots read as a bot the user already had.
317
517
  */
318
518
  function SeedPackage({
319
519
  have,
520
+ inks,
320
521
  onDone,
321
522
  }: {
322
523
  /** Names already on the roster. */
323
524
  have: Set<string>;
525
+ inks: string[];
324
526
  onDone: (name: string | null) => void;
325
527
  }) {
326
- const [off, setOff] = useState<Set<string>>(new Set());
327
- const [add, adding] = useServerAction(createSeedBotsAction, {
328
- onOk: (made) => {
329
- revalidate(queryKey.bot);
330
- onDone(made.created[0] ?? null);
331
- },
332
- });
333
-
334
- const wanted = BOT_SEEDS.filter(
335
- (seed) => !have.has(seed.name) && !off.has(seed.name),
336
- );
528
+ const picks = useSeedPicks(have, inks, onDone);
337
529
 
338
530
  return (
339
531
  <div className="flex min-h-full flex-col">
@@ -343,108 +535,115 @@ function SeedPackage({
343
535
 
344
536
  <div className="px-8 pt-7 pb-1">
345
537
  <h3 className="text-[17px] font-medium tracking-tight">
346
- {BOT_SEEDS.length === 1
347
- ? "One bot comes with her"
348
- : `${BOT_SEEDS.length === 2 ? "Two" : BOT_SEEDS.length} bots come with her`}
538
+ Bots you can add
349
539
  </h3>
350
540
  <p className="mt-1.5 max-w-lg text-[13px] leading-relaxed text-muted-foreground break-keep">
351
- They take the work that would leave the call silent, and report back.
352
- Untick anything you would rather not have; each runs on the app
353
- default model until you give it one of its own.
541
+ Each one is a starting point rename it, re-prompt it, give it a
542
+ model of its own. What a bot needs before it can work stands on its
543
+ row; until then it runs on the app default model.
354
544
  </p>
355
545
  </div>
356
546
 
357
- <div className="flex flex-col gap-2 px-8 pt-4">
358
- {BOT_SEEDS.map((seed) => {
359
- const owned = have.has(seed.name);
360
- const on = owned || !off.has(seed.name);
361
- return (
362
- <div
363
- key={seed.name}
364
- className={cn(
365
- "rounded-2xl ring-1 transition-colors",
366
- owned
367
- ? "opacity-45 ring-border/60"
368
- : on
369
- ? "ring-foreground"
370
- : "ring-border/60",
371
- )}
372
- >
373
- <button
374
- type="button"
375
- disabled={owned || adding}
376
- aria-pressed={on}
377
- onClick={() =>
378
- setOff((was) => {
379
- const next = new Set(was);
380
- if (next.has(seed.name)) next.delete(seed.name);
381
- else next.add(seed.name);
382
- return next;
383
- })
384
- }
385
- className="flex w-full items-center gap-3.5 rounded-2xl px-4 py-3 text-left outline-none focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-default"
386
- >
387
- <BotMark
388
- size={32}
389
- {...markProps(seed.name, seed.icon)}
390
- className={cn(
391
- "shrink-0 transition-opacity",
392
- !on && "opacity-35",
393
- )}
394
- />
395
- <span className="min-w-0 flex-1 space-y-0.5">
396
- <span className="block text-[14px] font-medium">
397
- {seed.name}
398
- </span>
399
- <span className="block text-[12px] leading-normal text-muted-foreground break-keep">
400
- {seed.description}
401
- </span>
402
- </span>
403
- {owned && (
404
- <span className="shrink-0 font-mono text-[10px] text-muted-foreground">
405
- already added
406
- </span>
407
- )}
408
- <span
409
- className={cn(
410
- "grid size-6 shrink-0 place-items-center rounded-full transition-colors",
411
- on
412
- ? "bg-foreground text-background"
413
- : "ring-1 ring-border/60 ring-inset",
414
- )}
415
- >
416
- {on && <Check className="size-3.5" />}
417
- </span>
418
- </button>
419
- </div>
420
- );
421
- })}
547
+ <div className="px-8 pt-4">
548
+ <SeedRows have={have} inks={inks} picks={picks} />
422
549
  </div>
423
550
 
424
551
  <div className="mt-auto flex items-center gap-3 border-t border-border/60 px-8 py-4">
425
552
  <span className="flex-1 font-mono text-[11px] text-muted-foreground">
426
- {wanted.length === 0
553
+ {picks.wanted.length === 0
427
554
  ? "nothing ticked"
428
- : `${wanted.length} of ${BOT_SEEDS.length - have.size} ticked`}
555
+ : `${picks.wanted.length} of ${picks.addable.length} ticked`}
429
556
  </span>
430
- <Button variant="ghost" onClick={() => onDone(null)}>
431
- Cancel
432
- </Button>
433
- {/* Says what it will do, so the count is checkable before the click. */}
434
- <Button
435
- loading={adding}
436
- disabled={wanted.length === 0}
437
- onClick={() => add(wanted.map((seed) => ({ name: seed.name })))}
438
- >
439
- {wanted.length === 1
440
- ? `Add ${wanted[0].name}`
441
- : `Add ${wanted.length} bots`}
442
- </Button>
557
+ <SeedActions picks={picks} onCancel={() => onDone(null)} />
443
558
  </div>
444
559
  </div>
445
560
  );
446
561
  }
447
562
 
563
+ /** The same offer in a dialog, which brings its own padding and its own footer. */
564
+ function SeedDialog({
565
+ have,
566
+ inks,
567
+ onDone,
568
+ }: {
569
+ have: Set<string>;
570
+ inks: string[];
571
+ onDone: (name: string | null) => void;
572
+ }) {
573
+ const picks = useSeedPicks(have, inks, onDone);
574
+
575
+ return (
576
+ <SettingDialogContent
577
+ title="Bots you can add"
578
+ description="Each one is a starting point — rename it, re-prompt it, give it a model of its own. What a bot needs before it can work stands on its row."
579
+ footer={<SeedActions picks={picks} onCancel={() => onDone(null)} />}
580
+ >
581
+ <SeedRows have={have} inks={inks} picks={picks} />
582
+ </SettingDialogContent>
583
+ );
584
+ }
585
+
586
+ /**
587
+ * The invite, on the New bot line rather than in the roster: both are ways to get
588
+ * a bot, and neither is a bot you have. It carries the faces of the ones still on
589
+ * offer, in the colours they would be created with, because a row of names would
590
+ * read as one more list and the faces are what says these are bots.
591
+ */
592
+ function SeedInvite({
593
+ missing,
594
+ inks,
595
+ have,
596
+ onDone,
597
+ }: {
598
+ missing: BotSeed[];
599
+ inks: string[];
600
+ have: Set<string>;
601
+ onDone: (name: string | null) => void;
602
+ }) {
603
+ return (
604
+ <button
605
+ type="button"
606
+ title="Ready-made bots"
607
+ aria-label={`Ready-made bots, ${missing.length} on offer`}
608
+ onClick={() =>
609
+ notify.component({
610
+ className: "sm:max-w-xl",
611
+ renderer: ({ close }) => (
612
+ <SeedDialog
613
+ have={have}
614
+ inks={inks}
615
+ onDone={(name) => {
616
+ close();
617
+ onDone(name);
618
+ }}
619
+ />
620
+ ),
621
+ })
622
+ }
623
+ className="flex shrink-0 items-center gap-1 rounded-md px-1.5 py-1 text-muted-foreground outline-none transition-colors hover:bg-muted/60 hover:text-foreground focus-visible:ring-3 focus-visible:ring-ring/50 focus-visible:ring-inset"
624
+ >
625
+ {/* Says the same thing the words next to it do, about a different set: these
626
+ are bots to add. It carries the affordance on its own when one seed is
627
+ left and the faces are a single dot */}
628
+ <Plus className="size-3 shrink-0" />
629
+ {missing.map((seed, at) => (
630
+ <span
631
+ key={seed.name}
632
+ className={cn("block", at > 0 && "-ml-1.5")}
633
+ style={{ zIndex: missing.length - at }}
634
+ >
635
+ <BotMark
636
+ size={20}
637
+ {...markProps(seed.name, seed.icon)}
638
+ color={inks[BOT_SEEDS.indexOf(seed)]}
639
+ notify={false}
640
+ />
641
+ </span>
642
+ ))}
643
+ </button>
644
+ );
645
+ }
646
+
448
647
  /**
449
648
  * The mark seed is the name, not the row id, so a new bot's preview is the face the
450
649
  * roster will show. An unpicked colour stays empty and means the theme ink.
@@ -486,10 +685,34 @@ function BotPage({
486
685
  icon: bot?.icon ?? randomBotIcon(),
487
686
  provider: (bot?.provider ?? null) as TextModelProviderId | null,
488
687
  model: bot?.model ?? "",
688
+ // In thousands of tokens, the unit the field is typed in
689
+ compactAt: bot?.compactAt ? String(bot.compactAt / 1000) : "",
489
690
  toolIds: bot?.tools.map((tool) => tool.id) ?? [],
490
691
  }));
491
- const { name, description, systemPrompt, icon, provider, model, toolIds } =
492
- fields;
692
+ const {
693
+ name,
694
+ description,
695
+ systemPrompt,
696
+ icon,
697
+ provider,
698
+ model,
699
+ compactAt,
700
+ toolIds,
701
+ } = fields;
702
+ // Typed by hand; otherwise the field shows what the picked model fills in
703
+ const [compactEdited, setCompactEdited] = useState(Boolean(bot?.compactAt));
704
+ // Read here as well as in the picker: a pick has to fill in from it the moment it happens
705
+ const catalog = useServerRoute<GatewayModel[]>(queryKey.modelCatalog);
706
+ const windowOf = (of: TextModelProviderId | null, id: string) =>
707
+ of && id.trim() ? contextWindowOf(of, id.trim(), catalog.data) : null;
708
+ const pickedWindow = windowOf(provider, model);
709
+ const filledK =
710
+ provider && model.trim() ? filledTokens(pickedWindow) / 1000 : null;
711
+ const shownK = compactEdited
712
+ ? compactAt
713
+ : filledK === null
714
+ ? ""
715
+ : String(filledK);
493
716
 
494
717
  const [save] = useServerAction(updateBotAction, {
495
718
  onOk: () => revalidate(queryKey.bot),
@@ -527,6 +750,9 @@ function BotPage({
527
750
  icon,
528
751
  provider,
529
752
  model: model.trim(),
753
+ compactAt: compactEdited
754
+ ? tokensFromK(compactAt)
755
+ : filledTokens(pickedWindow),
530
756
  toolIds,
531
757
  });
532
758
  };
@@ -549,53 +775,37 @@ function BotPage({
549
775
  {bot ? bot.name : "New bot"}
550
776
  </span>
551
777
  {bot && (
552
- <Button
553
- size="icon-sm"
554
- variant="ghost"
555
- loading={removing}
556
- aria-label="Delete this bot"
557
- onClick={confirmRemove}
558
- className="text-muted-foreground hover:text-destructive"
559
- >
560
- <Trash2 />
561
- </Button>
778
+ <>
779
+ {/* Only when off: on is the resting state and needs no word. */}
780
+ {bot.disabled && (
781
+ <span className="font-mono text-[10px] text-muted-foreground">
782
+ off
783
+ </span>
784
+ )}
785
+ <Switch
786
+ checked={!bot.disabled}
787
+ onCheckedChange={(on) => commit({ disabled: !on })}
788
+ aria-label={`${bot.name} on or off`}
789
+ /* mr-1 keeps it off Delete: one is a setting, the other is not. */
790
+ className="mr-1 shrink-0"
791
+ />
792
+ <Button
793
+ size="icon-sm"
794
+ variant="ghost"
795
+ loading={removing}
796
+ aria-label="Delete this bot"
797
+ onClick={confirmRemove}
798
+ className="text-muted-foreground hover:text-destructive"
799
+ >
800
+ <Trash2 />
801
+ </Button>
802
+ </>
562
803
  )}
563
804
  </div>
564
805
 
565
806
  <div className="flex-1 space-y-5 p-6">
566
- <div className="flex items-start gap-4">
567
- <BotMark size={72} {...markProps(name, icon)} className="shrink-0" />
568
- <div className="min-w-0 flex-1 space-y-2.5">
569
- {bot ? (
570
- <p className="truncate text-lg leading-8 font-medium">
571
- {bot.name}
572
- </p>
573
- ) : (
574
- <Input
575
- value={name}
576
- onChange={(event) => patch({ name: event.target.value })}
577
- placeholder="researcher"
578
- spellCheck={false}
579
- maxLength={COMMON_VALIDATE.name.max}
580
- autoFocus
581
- />
582
- )}
583
- <Input
584
- value={description}
585
- onChange={(event) => patch({ description: event.target.value })}
586
- onBlur={() => {
587
- const next = description.trim();
588
- if (bot && next && next !== bot.description) {
589
- commit({ description: next });
590
- }
591
- }}
592
- placeholder="Searches the web and reports back"
593
- maxLength={COMMON_VALIDATE.description.max}
594
- />
595
- </div>
596
- </div>
597
-
598
807
  <MarkPicker
808
+ name={name}
599
809
  icon={icon}
600
810
  onChange={(next) => {
601
811
  patch({ icon: next });
@@ -603,18 +813,100 @@ function BotPage({
603
813
  }}
604
814
  />
605
815
 
816
+ <Row label="Name">
817
+ {bot ? (
818
+ <p className="truncate text-sm leading-8 font-medium">{bot.name}</p>
819
+ ) : (
820
+ <Input
821
+ value={name}
822
+ onChange={(event) => patch({ name: event.target.value })}
823
+ placeholder="researcher"
824
+ spellCheck={false}
825
+ maxLength={COMMON_VALIDATE.name.max}
826
+ autoFocus
827
+ />
828
+ )}
829
+ {/* Renaming is delete and recreate (bot.query updateBot), so say it before the name is typed */}
830
+ <p className="text-xs text-muted-foreground">
831
+ {bot
832
+ ? `What ${APP_NAME} calls it when she hands it work. Fixed once the bot is made.`
833
+ : `What ${APP_NAME} calls it when she hands it work. Up to ${COMMON_VALIDATE.name.max} characters, and it can’t be renamed later.`}
834
+ </p>
835
+ </Row>
836
+
837
+ <Row label="Description">
838
+ <Input
839
+ value={description}
840
+ onChange={(event) => patch({ description: event.target.value })}
841
+ onBlur={() => {
842
+ const next = description.trim();
843
+ if (bot && next && next !== bot.description) {
844
+ commit({ description: next });
845
+ }
846
+ }}
847
+ placeholder="Searches the web and answers"
848
+ maxLength={COMMON_VALIDATE.description.max}
849
+ />
850
+ <p className="text-xs text-muted-foreground">
851
+ The one line {APP_NAME} and the other bots read when they decide who
852
+ gets a job.
853
+ </p>
854
+ </Row>
855
+
606
856
  <Row label="Runs on">
607
857
  <ModelPicker
608
858
  provider={provider}
609
859
  model={model}
610
860
  onChange={(next) => {
611
861
  patch({ provider: next.provider, model: next.model });
862
+ // A new model fills in its own point; a number typed for the old one no longer fits
863
+ setCompactEdited(false);
612
864
  // A provider without a model is not a model; save once both are picked.
613
- if (next.model) commit(next);
865
+ if (next.model) {
866
+ commit({
867
+ ...next,
868
+ compactAt: filledTokens(windowOf(next.provider, next.model)),
869
+ });
870
+ }
614
871
  }}
615
872
  />
616
873
  </Row>
617
874
 
875
+ <Row label="Compacts at">
876
+ <InputGroup>
877
+ <InputGroupInput
878
+ inputMode="decimal"
879
+ value={shownK}
880
+ placeholder={model.trim() ? "" : "Pick a model first"}
881
+ onChange={(event) => {
882
+ setCompactEdited(true);
883
+ patch({ compactAt: event.target.value });
884
+ }}
885
+ onBlur={() => {
886
+ if (!compactEdited) return;
887
+ // Typing is a draft; only a settled field is a value. Emptied, the field goes back to the model's own
888
+ const typed = tokensFromK(compactAt);
889
+ if (typed === null) setCompactEdited(false);
890
+ else patch({ compactAt: String(typed / 1000) });
891
+ commit({
892
+ compactAt:
893
+ typed ?? (model.trim() ? filledTokens(pickedWindow) : null),
894
+ });
895
+ }}
896
+ />
897
+ <InputGroupAddon align="inline-end">
898
+ <InputGroupText>k tokens</InputGroupText>
899
+ </InputGroupAddon>
900
+ </InputGroup>
901
+ <p className="text-xs text-muted-foreground">
902
+ {compactNote({
903
+ model: model.trim(),
904
+ window: pickedWindow,
905
+ shown: tokensFromK(shownK),
906
+ })}
907
+ </p>
908
+ </Row>
909
+
618
910
  <Row label="Tools">
619
911
  <ToolPicker
620
912
  selected={toolIds}
@@ -647,6 +939,8 @@ function BotPage({
647
939
  )}
648
940
  </Row>
649
941
 
942
+ {bot && <Memory bot={bot.name} />}
943
+
650
944
  {bot && <Recent jobs={jobs} />}
651
945
 
652
946
  {!bot && (
@@ -668,32 +962,234 @@ function BotPage({
668
962
  );
669
963
  }
670
964
 
671
- /** What the roster's pick is, in the section's rail. */
672
- function BotRail({ bot }: { bot: Bot | null }) {
673
- if (!bot)
965
+ /**
966
+ * What this bot keeps for its next jobs (bot.memory), drawn the way Recent is: one
967
+ * line per file, newest first. A row opens in place. Deleting is the one thing done
968
+ * to a file from here — writing one is the bot's.
969
+ */
970
+ function Memory({ bot }: { bot: string }) {
971
+ const key = queryKey.botMemoryFiles(bot);
972
+ const { data, isLoading } = useServerRoute<BotMemory>(key);
973
+ const [open, setOpen] = useState<string | null>(null);
974
+ const [reveal] = useServerAction(openFileAction);
975
+ const [remove, removing] = useServerAction(deleteWorkspaceFileAction, {
976
+ okMessage: "File deleted",
977
+ onOk: () => {
978
+ setOpen(null);
979
+ revalidate(key);
980
+ revalidate(queryKey.workspace);
981
+ },
982
+ });
983
+
984
+ const confirmRemove = async (file: BotMemoryFile) => {
985
+ const confirmed = await notify.confirm({
986
+ title: `Delete ${file.file}?`,
987
+ description: `It is deleted from disk for good, and ${bot}'s next job starts without it.`,
988
+ okText: "Delete",
989
+ destructive: true,
990
+ });
991
+ if (confirmed) remove(file.path);
992
+ };
993
+
994
+ return (
995
+ <div className="pt-1">
996
+ <div className="flex h-6 items-center">
997
+ <span className="font-mono text-xs text-muted-foreground">Memory</span>
998
+ <span className="flex-1" />
999
+ {data && data.total > 0 && (
1000
+ <>
1001
+ <span className="pr-1 font-mono text-[11px] text-muted-foreground tabular-nums">
1002
+ {data.total === 1 ? "1 file" : `${data.total} files`}
1003
+ </span>
1004
+ <button
1005
+ type="button"
1006
+ title="Show in the file manager"
1007
+ onClick={() => reveal(data.folder)}
1008
+ className="flex items-center gap-1 rounded-md px-1.5 py-1 font-mono text-[11px] text-muted-foreground outline-none transition-colors hover:text-foreground focus-visible:ring-3 focus-visible:ring-ring/50"
1009
+ >
1010
+ <FolderOpen className="size-3" />
1011
+ Folder
1012
+ </button>
1013
+ </>
1014
+ )}
1015
+ </div>
1016
+ {isLoading ? (
1017
+ <div className="flex h-9 items-center border-t border-border/60">
1018
+ <Skeleton className="h-3 w-56" />
1019
+ </div>
1020
+ ) : !data?.entries.length ? (
1021
+ <p className="py-2 text-[13px] text-muted-foreground">
1022
+ Nothing kept yet.
1023
+ </p>
1024
+ ) : (
1025
+ data.entries.map((file) => {
1026
+ const isOpen = open === file.path;
1027
+ return (
1028
+ <div key={file.path} className="border-t border-border/60">
1029
+ <button
1030
+ type="button"
1031
+ aria-expanded={isOpen}
1032
+ onClick={() => setOpen(isOpen ? null : file.path)}
1033
+ className="flex h-9 w-full items-center gap-3 text-left text-[13px] outline-none focus-visible:ring-3 focus-visible:ring-ring/50"
1034
+ >
1035
+ <span
1036
+ className={cn(
1037
+ "min-w-0 flex-1 truncate",
1038
+ isOpen && "font-medium",
1039
+ )}
1040
+ >
1041
+ {file.line || file.file}
1042
+ </span>
1043
+ <span className="w-36 shrink-0 truncate font-mono text-[11px] text-muted-foreground">
1044
+ {file.file}
1045
+ </span>
1046
+ <span className="w-28 shrink-0 text-right font-mono text-[11px] text-muted-foreground tabular-nums">
1047
+ {whenOf(file.at)}
1048
+ </span>
1049
+ <ChevronRight
1050
+ className={cn(
1051
+ "size-3 shrink-0 text-muted-foreground transition-transform",
1052
+ isOpen && "rotate-90",
1053
+ )}
1054
+ />
1055
+ </button>
1056
+ {isOpen && (
1057
+ <div className="flex items-start gap-3">
1058
+ <div className="max-h-96 min-w-0 flex-1 overflow-y-auto">
1059
+ <MemoryText path={file.path} />
1060
+ </div>
1061
+ <Button
1062
+ size="icon-sm"
1063
+ variant="ghost"
1064
+ aria-label={`Delete ${file.file}`}
1065
+ loading={removing}
1066
+ onClick={() => confirmRemove(file)}
1067
+ className="text-muted-foreground hover:text-destructive"
1068
+ >
1069
+ <Trash2 />
1070
+ </Button>
1071
+ </div>
1072
+ )}
1073
+ </div>
1074
+ );
1075
+ })
1076
+ )}
1077
+ </div>
1078
+ );
1079
+ }
1080
+
1081
+ /** An opened memory file, read and capped the way Workspace reads one (file-view useFileText). */
1082
+ function MemoryText({ path }: { path: string }) {
1083
+ const { content, failure, truncated } = useFileText(path);
1084
+ if (failure) {
1085
+ return <p className="pb-3 font-mono text-xs text-destructive">{failure}</p>;
1086
+ }
1087
+ if (content === null) {
674
1088
  return (
675
- <SettingRailNote>
676
- Becomes a bot once it has a name and a model
677
- </SettingRailNote>
1089
+ <div className="space-y-2 pb-3">
1090
+ <Skeleton className="h-4 w-5/6" />
1091
+ <Skeleton className="h-4 w-2/3" />
1092
+ </div>
678
1093
  );
679
- const tokens = bot.tokens.input + bot.tokens.output;
1094
+ }
1095
+ const kind = viewKindOf(path);
680
1096
  return (
681
- <SettingRailNote>
682
- <span className="font-mono text-[11px]">
683
- {bot.name}
684
- <span className="px-1.5 opacity-50">·</span>
685
- <span
686
- title={`in ${formatCount(bot.tokens.input)} · out ${formatCount(bot.tokens.output)}`}
687
- >
688
- {tokens > 0 ? `${formatCount(tokens)} tokens` : "no tokens yet"}
689
- </span>
690
- <span className="px-1.5 opacity-50">·</span>
691
- since {whenOf(bot.createdAt)}
1097
+ <FileBody
1098
+ kind={kind}
1099
+ content={kind === "markdown" ? underItsRow(content) : content}
1100
+ truncated={truncated}
1101
+ where="inline"
1102
+ />
1103
+ );
1104
+ }
1105
+
1106
+ /**
1107
+ * A markdown memory file as it reads under its row: the row already carries the
1108
+ * file's first line, so frontmatter and a leading heading are not drawn again.
1109
+ */
1110
+ const underItsRow = (content: string) =>
1111
+ content.replace(/^---\n[\s\S]*?\n---\n/, "").replace(/^\s*#[^\n]*\n?/, "");
1112
+
1113
+ /**
1114
+ * What the roster's pick is, and the one setting that is the whole set's rather
1115
+ * than any bot's: whether bots keep their own memory at all.
1116
+ */
1117
+ function BotRail({ bot }: { bot: Bot | null }) {
1118
+ const { data: memoryOn, mutate } = useServerRoute<boolean>(
1119
+ queryKey.botMemory,
1120
+ );
1121
+ const [setMemoryOn] = useServerAction(setBotMemoryOnAction, {
1122
+ onOk: () => revalidate(queryKey.botMemory),
1123
+ });
1124
+
1125
+ const tokens = bot ? bot.tokens.input + bot.tokens.output : 0;
1126
+ return (
1127
+ <>
1128
+ <SettingRailNote>
1129
+ {bot ? (
1130
+ <span className="font-mono text-[11px]">
1131
+ {bot.name}
1132
+ {bot.disabled && (
1133
+ <>
1134
+ <span className="px-1.5 opacity-50">·</span>off
1135
+ </>
1136
+ )}
1137
+ <span className="px-1.5 opacity-50">·</span>
1138
+ <span
1139
+ title={`in ${formatCount(bot.tokens.input)} · out ${formatCount(bot.tokens.output)}`}
1140
+ >
1141
+ {tokens > 0 ? `${formatCount(tokens)} tokens` : "no tokens yet"}
1142
+ </span>
1143
+ <span className="px-1.5 opacity-50">·</span>
1144
+ since {whenOf(bot.createdAt)}
1145
+ </span>
1146
+ ) : (
1147
+ "Becomes a bot once it has a name and a model"
1148
+ )}
1149
+ </SettingRailNote>
1150
+ <span className="shrink-0 text-xs text-muted-foreground">
1151
+ Bots keep their own memory
692
1152
  </span>
693
- </SettingRailNote>
1153
+ <Switch
1154
+ checked={memoryOn ?? true}
1155
+ disabled={memoryOn === undefined}
1156
+ onCheckedChange={(on) => {
1157
+ void mutate(on, false);
1158
+ setMemoryOn(on);
1159
+ }}
1160
+ aria-label="Bots keep their own memory"
1161
+ />
1162
+ </>
694
1163
  );
695
1164
  }
696
1165
 
1166
+ /** "800", "800k" or "1.2" → tokens (800,000 / 1,200); null when there is no number. */
1167
+ function tokensFromK(text: string): number | null {
1168
+ const k = Number(text.replace(/[^0-9.]/g, ""));
1169
+ return Number.isFinite(k) && k > 0 ? Math.round(k * 1000) : null;
1170
+ }
1171
+
1172
+ /** What a model fills in, rounded to the field's unit so the saved number is the one shown (model.schema compactAtFor). */
1173
+ const filledTokens = (window: number | null) =>
1174
+ Math.round(compactAtFor(window) / 1000) * 1000;
1175
+
1176
+ /** The line under the field: where the number came from. */
1177
+ function compactNote(input: {
1178
+ model: string;
1179
+ window: number | null;
1180
+ shown: number | null;
1181
+ }): string {
1182
+ if (!input.model) return "Filled in from the model once one is picked.";
1183
+ const filled = filledTokens(input.window);
1184
+ const summarize = "A job summarizes itself here and carries on.";
1185
+ if (input.shown !== null && input.shown !== filled) {
1186
+ return `Set by hand. Picking a model fills in its own again. ${summarize}`;
1187
+ }
1188
+ return input.window
1189
+ ? `${Math.round(BOT_RUN.compactHeadroom * 100)}% of this model's ${(input.window / 1000).toLocaleString()}k context window. ${summarize}`
1190
+ : `This model's context window is unknown, so the default is filled in. ${summarize}`;
1191
+ }
1192
+
697
1193
  function Row({ label, children }: { label: string; children: ReactNode }) {
698
1194
  return (
699
1195
  <div className="flex gap-3">
@@ -734,11 +1230,18 @@ function Recent({ jobs }: { jobs: Task[] }) {
734
1230
  className="flex h-9 items-center gap-3 border-t border-border/60 text-[13px]"
735
1231
  >
736
1232
  <span className="min-w-0 flex-1 truncate">{job.label}</span>
737
- <span
738
- className={cn("shrink-0 font-mono text-[11px]", state.tone)}
739
- >
740
- {state.text}
741
- </span>
1233
+ {job.status === "running" ? (
1234
+ <ShinyText
1235
+ text={state.text}
1236
+ className="shrink-0 font-mono text-[11px]"
1237
+ />
1238
+ ) : (
1239
+ <span
1240
+ className={cn("shrink-0 font-mono text-[11px]", state.tone)}
1241
+ >
1242
+ {state.text}
1243
+ </span>
1244
+ )}
742
1245
  <span className="w-28 shrink-0 text-right font-mono text-[11px] text-muted-foreground tabular-nums">
743
1246
  {whenOf(job.updatedAt)}
744
1247
  </span>
@@ -753,12 +1256,7 @@ function Recent({ jobs }: { jobs: Task[] }) {
753
1256
  function jobState(job: Task): { text: string; tone: string } {
754
1257
  switch (job.status) {
755
1258
  case "waiting":
756
- return isBudgetAsk(job.ask)
757
- ? { text: "out of steps", tone: "text-muted-foreground" }
758
- : {
759
- text: "waiting on you",
760
- tone: WAITING_INK,
761
- };
1259
+ return { text: "waiting on you", tone: WAITING_INK };
762
1260
  case "running":
763
1261
  return { text: "working", tone: "text-muted-foreground" };
764
1262
  case "failed":
@@ -768,81 +1266,74 @@ function jobState(job: Task): { text: string; tone: string } {
768
1266
  }
769
1267
  }
770
1268
 
771
- /** Palette swatch and silhouette for a BotMark; the caller draws the face. */
1269
+ /**
1270
+ * The face, with what paints it right under it: the palette, then the
1271
+ * silhouette. Laid out like Thursday's own face picker, so both faces are
1272
+ * chosen the same way.
1273
+ */
772
1274
  function MarkPicker({
1275
+ name,
773
1276
  icon,
774
1277
  onChange,
775
1278
  }: {
1279
+ name: string;
776
1280
  icon: BotIcon;
777
1281
  onChange: (icon: BotIcon) => void;
778
1282
  }) {
779
1283
  return (
780
- <>
781
- <Row label="Color">
782
- <div className="flex flex-wrap items-center gap-1.5">
783
- {/* MARK_SYSTEM is an explicit "follow the theme ink", distinct from no colour. */}
784
- <Swatch
785
- color={null}
786
- picked={icon.color === MARK_SYSTEM}
787
- onPick={() => onChange({ ...icon, color: MARK_SYSTEM })}
788
- />
789
- {MARK_PALETTE.map((color) => (
790
- <Swatch
791
- key={color}
792
- color={color}
793
- picked={icon.color === color}
794
- onPick={() =>
795
- onChange({
796
- ...icon,
797
- color: icon.color === color ? undefined : color,
798
- })
799
- }
800
- />
801
- ))}
802
- </div>
803
- </Row>
1284
+ <div className="flex flex-col items-center gap-4 pt-3 pb-2">
1285
+ <BotMark size={112} {...markProps(name, icon)} className="shrink-0" />
1286
+
1287
+ {/* MARK_SYSTEM is an explicit "follow the theme ink", distinct from no
1288
+ colour; picking the colour already on it goes back to none. */}
1289
+ <MarkPalette
1290
+ color={icon.color}
1291
+ themePicked={icon.color === MARK_SYSTEM}
1292
+ onTheme={() => onChange({ ...icon, color: MARK_SYSTEM })}
1293
+ onPick={(color) =>
1294
+ onChange({ ...icon, color: icon.color === color ? undefined : color })
1295
+ }
1296
+ />
804
1297
 
805
1298
  {/* Every chip is a toggle: unpicked means "whatever the name seeds", not a value */}
806
- <Row label="Face">
807
- <div className="flex gap-1">
808
- {MARK_SHAPES.map((shape) => (
809
- <button
810
- key={shape}
811
- type="button"
812
- onClick={() =>
813
- onChange({
814
- ...icon,
815
- shape: icon.shape === shape ? undefined : shape,
816
- })
817
- }
818
- className={cn(
819
- "rounded-md px-2 py-1 font-mono text-[11px] capitalize transition-colors",
820
- icon.shape === shape
821
- ? "bg-foreground text-background"
822
- : "bg-muted text-muted-foreground hover:text-foreground",
823
- )}
824
- >
825
- {shape}
826
- </button>
827
- ))}
828
- <span className="mx-1 w-px self-stretch bg-border/60" />
1299
+ <div className="flex gap-1">
1300
+ {MARK_SHAPES.map((shape) => (
829
1301
  <button
1302
+ key={shape}
830
1303
  type="button"
831
1304
  onClick={() =>
832
- onChange({ ...icon, outline: icon.outline ? undefined : true })
1305
+ onChange({
1306
+ ...icon,
1307
+ shape: icon.shape === shape ? undefined : shape,
1308
+ })
833
1309
  }
834
1310
  className={cn(
835
- "rounded-md px-2 py-1 font-mono text-[11px] transition-colors",
836
- icon.outline
1311
+ "rounded-md px-2 py-1 font-mono text-[11px] capitalize transition-colors",
1312
+ icon.shape === shape
837
1313
  ? "bg-foreground text-background"
838
1314
  : "bg-muted text-muted-foreground hover:text-foreground",
839
1315
  )}
840
1316
  >
841
- outline
1317
+ {shape}
842
1318
  </button>
843
- </div>
844
- </Row>
845
- </>
1319
+ ))}
1320
+ <span className="mx-1 w-px self-stretch bg-border/60" />
1321
+ <button
1322
+ type="button"
1323
+ onClick={() =>
1324
+ onChange({ ...icon, outline: icon.outline ? undefined : true })
1325
+ }
1326
+ className={cn(
1327
+ "rounded-md px-2 py-1 font-mono text-[11px] transition-colors",
1328
+ icon.outline
1329
+ ? "bg-foreground text-background"
1330
+ : "bg-muted text-muted-foreground hover:text-foreground",
1331
+ )}
1332
+ >
1333
+ outline
1334
+ </button>
1335
+ </div>
1336
+ </div>
846
1337
  );
847
1338
  }
848
1339