thursday-agent 0.2.0 → 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 (535) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/app-path-routes-manifest.json +5 -2
  3. package/.next/build-manifest.json +3 -3
  4. package/.next/prerender-manifest.json +5 -5
  5. package/.next/routes-manifest.json +24 -6
  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/page_client-reference-manifest.js +1 -1
  9. package/.next/server/app/_global-error.html +1 -1
  10. package/.next/server/app/_global-error.rsc +2 -2
  11. package/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  12. package/.next/server/app/_global-error.segments/_full.segment.rsc +2 -2
  13. package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  14. package/.next/server/app/_not-found/page.js +2 -2
  15. package/.next/server/app/_not-found/page.js.nft.json +1 -1
  16. package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  17. package/.next/server/app/_not-found.html +1 -1
  18. package/.next/server/app/_not-found.rsc +14 -14
  19. package/.next/server/app/_not-found.segments/_full.segment.rsc +14 -14
  20. package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +12 -12
  21. package/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  22. package/.next/server/app/api/artifact/route.js +3 -3
  23. package/.next/server/app/api/artifact/route.js.nft.json +1 -1
  24. package/.next/server/app/api/bot/memory/files/route/app-paths-manifest.json +3 -0
  25. package/.next/server/app/api/bot/memory/files/route.js +10 -0
  26. package/.next/server/app/api/bot/memory/files/route.js.nft.json +1 -0
  27. package/.next/server/app/api/bot/memory/files/route_client-reference-manifest.js +3 -0
  28. package/.next/server/app/api/bot/memory/route/app-paths-manifest.json +3 -0
  29. package/.next/server/app/api/bot/memory/route.js +14 -0
  30. package/.next/server/app/api/bot/{notes → memory}/route.js.nft.json +1 -1
  31. package/.next/server/app/api/bot/memory/route_client-reference-manifest.js +3 -0
  32. package/.next/server/app/api/bot/route.js +6 -6
  33. package/.next/server/app/api/bot/route.js.nft.json +1 -1
  34. package/.next/server/app/api/bot/task/[id]/route.js +8 -8
  35. package/.next/server/app/api/bot/task/[id]/route.js.nft.json +1 -1
  36. package/.next/server/app/api/bot/task/route.js +8 -8
  37. package/.next/server/app/api/bot/task/route.js.nft.json +1 -1
  38. package/.next/server/app/api/config/route.js +5 -5
  39. package/.next/server/app/api/config/route.js.nft.json +1 -1
  40. package/.next/server/app/api/events/route.js +4 -5
  41. package/.next/server/app/api/events/route.js.nft.json +1 -1
  42. package/.next/server/app/api/file/[...path]/route.js +3 -3
  43. package/.next/server/app/api/file/[...path]/route.js.nft.json +1 -1
  44. package/.next/server/app/api/llm-model/catalog/route.js +9 -8
  45. package/.next/server/app/api/llm-model/catalog/route.js.nft.json +1 -1
  46. package/.next/server/app/api/llm-model/credits/route/app-paths-manifest.json +3 -0
  47. package/.next/server/app/api/llm-model/credits/route/build-manifest.json +12 -0
  48. package/.next/server/app/api/llm-model/credits/route/server-reference-manifest.json +4 -0
  49. package/.next/server/app/api/llm-model/credits/route.js +16 -0
  50. package/.next/server/app/api/llm-model/credits/route.js.map +5 -0
  51. package/.next/server/app/api/llm-model/credits/route.js.nft.json +1 -0
  52. package/.next/server/app/api/llm-model/credits/route_client-reference-manifest.js +3 -0
  53. package/.next/server/app/api/llm-model/route.js +10 -8
  54. package/.next/server/app/api/llm-model/route.js.nft.json +1 -1
  55. package/.next/server/app/api/llm-model/subscription/route/app-paths-manifest.json +3 -0
  56. package/.next/server/app/api/llm-model/subscription/route/build-manifest.json +12 -0
  57. package/.next/server/app/api/llm-model/subscription/route/server-reference-manifest.json +4 -0
  58. package/.next/server/app/api/llm-model/subscription/route.js +15 -0
  59. package/.next/server/app/api/llm-model/subscription/route.js.map +5 -0
  60. package/.next/server/app/api/llm-model/subscription/route.js.nft.json +1 -0
  61. package/.next/server/app/api/llm-model/subscription/route_client-reference-manifest.js +3 -0
  62. package/.next/server/app/api/mcp/[name]/route.js +4 -4
  63. package/.next/server/app/api/mcp/[name]/route.js.nft.json +1 -1
  64. package/.next/server/app/api/mcp/oauth/callback/route.js +5 -5
  65. package/.next/server/app/api/mcp/oauth/callback/route.js.nft.json +1 -1
  66. package/.next/server/app/api/mcp/route.js +4 -4
  67. package/.next/server/app/api/mcp/route.js.nft.json +1 -1
  68. package/.next/server/app/api/mcp/tools/route.js +4 -4
  69. package/.next/server/app/api/mcp/tools/route.js.nft.json +1 -1
  70. package/.next/server/app/api/memory/edit/route/app-paths-manifest.json +3 -0
  71. package/.next/server/app/api/memory/edit/route/build-manifest.json +12 -0
  72. package/.next/server/app/api/memory/edit/route/server-reference-manifest.json +4 -0
  73. package/.next/server/app/api/memory/edit/route.js +24 -0
  74. package/.next/server/app/api/memory/edit/route.js.map +5 -0
  75. package/.next/server/app/api/memory/edit/route.js.nft.json +1 -0
  76. package/.next/server/app/api/memory/{tidy → edit}/route_client-reference-manifest.js +1 -1
  77. package/.next/server/app/api/memory/route.js +6 -7
  78. package/.next/server/app/api/memory/route.js.nft.json +1 -1
  79. package/.next/server/app/api/skills/[source]/[name]/route.js +3 -3
  80. package/.next/server/app/api/skills/[source]/[name]/route.js.nft.json +1 -1
  81. package/.next/server/app/api/skills/route.js +3 -3
  82. package/.next/server/app/api/skills/route.js.nft.json +1 -1
  83. package/.next/server/app/api/thursday/call/route.js +9 -7
  84. package/.next/server/app/api/thursday/call/route.js.nft.json +1 -1
  85. package/.next/server/app/api/thursday/skills/route.js +9 -7
  86. package/.next/server/app/api/thursday/skills/route.js.nft.json +1 -1
  87. package/.next/server/app/api/thursday/tool-call/route.js +17 -15
  88. package/.next/server/app/api/thursday/tool-call/route.js.nft.json +1 -1
  89. package/.next/server/app/api/workspace/route.js +4 -5
  90. package/.next/server/app/api/workspace/route.js.nft.json +1 -1
  91. package/.next/server/app/artifact/[...path]/page/server-reference-manifest.json +1 -1
  92. package/.next/server/app/artifact/[...path]/page.js +5 -6
  93. package/.next/server/app/artifact/[...path]/page.js.nft.json +1 -1
  94. package/.next/server/app/artifact/[...path]/page_client-reference-manifest.js +1 -1
  95. package/.next/server/app/favicon.ico/route.js +3 -4
  96. package/.next/server/app/favicon.ico/route.js.nft.json +1 -1
  97. package/.next/server/app/favicon.ico.body +0 -0
  98. package/.next/server/app/page/server-reference-manifest.json +57 -90
  99. package/.next/server/app/page.js +10 -10
  100. package/.next/server/app/page.js.nft.json +1 -1
  101. package/.next/server/app/page_client-reference-manifest.js +1 -1
  102. package/.next/server/app-paths-manifest.json +5 -2
  103. package/.next/server/chunks/[externals]__0dzuk6_._.js +3 -0
  104. package/.next/server/chunks/[root-of-the-server]__01dkp4a._.js +3 -0
  105. package/.next/server/chunks/[root-of-the-server]__02va5wz._.js +1 -1
  106. package/.next/server/chunks/[root-of-the-server]__02xjoto._.js +3 -0
  107. package/.next/server/chunks/[root-of-the-server]__04q--0f._.js +8 -0
  108. package/.next/server/chunks/[root-of-the-server]__0bl-e63._.js +3 -0
  109. package/.next/server/chunks/[root-of-the-server]__0ce8nf8._.js +5 -0
  110. package/.next/server/chunks/[root-of-the-server]__0ffbg85._.js +3 -0
  111. package/.next/server/chunks/[root-of-the-server]__0j7z0r3._.js +12 -0
  112. package/.next/server/chunks/[root-of-the-server]__0jq4ohy._.js +8 -0
  113. package/.next/server/chunks/[root-of-the-server]__0ls6_8y._.js +8 -0
  114. package/.next/server/chunks/[root-of-the-server]__0ncs9uu._.js +3 -0
  115. package/.next/server/chunks/[root-of-the-server]__0pa2n68._.js +3 -0
  116. package/.next/server/chunks/[root-of-the-server]__0q0vvt2._.js +5 -0
  117. package/.next/server/chunks/[root-of-the-server]__1-ga0kh._.js +5 -0
  118. package/.next/server/chunks/[root-of-the-server]__11n256_._.js +5 -0
  119. package/.next/server/chunks/[root-of-the-server]__12cabg9._.js +3 -0
  120. package/.next/server/chunks/[root-of-the-server]__1339jnv._.js +8 -0
  121. package/.next/server/chunks/[root-of-the-server]__15bjnu5._.js +5 -0
  122. package/.next/server/chunks/[root-of-the-server]__18_teo8._.js +8 -0
  123. package/.next/server/chunks/[root-of-the-server]__1_04mcf._.js +5 -0
  124. package/.next/server/chunks/[root-of-the-server]__1anc1p9._.js +3 -0
  125. package/.next/server/chunks/[root-of-the-server]__1ccon1w._.js +5 -5
  126. package/.next/server/chunks/[root-of-the-server]__1fizn31._.js +3 -0
  127. package/.next/server/chunks/[root-of-the-server]__1llj3-b._.js +3 -0
  128. package/.next/server/chunks/[root-of-the-server]__1orn-5b._.js +11 -0
  129. package/.next/server/chunks/[root-of-the-server]__1p8m3f4._.js +3 -0
  130. package/.next/server/chunks/[root-of-the-server]__1wa1k7r._.js +8 -0
  131. package/.next/server/chunks/_0-73of8._.js +5 -5
  132. package/.next/server/chunks/_0-fjql4._.js +26 -0
  133. package/.next/server/chunks/_00xh_q0._.js +27 -0
  134. package/.next/server/chunks/{_0pppbb5._.js → _03kp0jp._.js} +163 -36
  135. package/.next/server/chunks/_051qh8u._.js +3 -0
  136. package/.next/server/chunks/{node_modules_next_04q-o58._.js → _05dla15._.js} +2 -2
  137. package/.next/server/chunks/_05h2cii._.js +1 -1
  138. package/.next/server/chunks/_0c9j6rb._.js +49 -0
  139. package/.next/server/chunks/_0fzqt2y._.js +130 -0
  140. package/.next/server/chunks/{_0vnutv9._.js → _0ioz3nd._.js} +1 -1
  141. package/.next/server/chunks/_0iwfby5._.js +3 -0
  142. package/.next/server/chunks/_0mecv0f._.js +26 -0
  143. package/.next/server/chunks/_0mh8j4m._.js +3 -0
  144. package/.next/server/chunks/_0n74zsf._.js +4 -4
  145. package/.next/server/chunks/_0obojvv._.js +3 -0
  146. package/.next/server/chunks/_0recufk._.js +49 -0
  147. package/.next/server/chunks/_0rnf-6n._.js +4 -4
  148. package/.next/server/chunks/_0sb7tlz._.js +3 -0
  149. package/.next/server/chunks/_0u10wmw._.js +5 -5
  150. package/.next/server/chunks/_0u4aiy2._.js +3 -0
  151. package/.next/server/chunks/_0wp4le3._.js +1 -1
  152. package/.next/server/chunks/_0y5fg-x._.js +1 -1
  153. package/.next/server/chunks/_1230wbx._.js +10 -0
  154. package/.next/server/chunks/_12nheja._.js +3 -0
  155. package/.next/server/chunks/_13510xh._.js +3 -0
  156. package/.next/server/chunks/_15hq89o._.js +1 -1
  157. package/.next/server/chunks/_17l5ygt._.js +49 -0
  158. package/.next/server/chunks/_18-1wcc._.js +5 -5
  159. package/.next/server/chunks/_1_5mnq4._.js +4 -4
  160. package/.next/server/chunks/_1_9mjx-._.js +3 -0
  161. package/.next/server/chunks/_1d0j961._.js +6 -0
  162. package/.next/server/chunks/_1d5zmy8._.js +1 -1
  163. package/.next/server/chunks/_1drgfx4._.js +4 -4
  164. package/.next/server/chunks/_1f46eie._.js +3 -0
  165. package/.next/server/chunks/_1h5_8sk._.js +3 -0
  166. package/.next/server/chunks/_1hdnxb7._.js +3 -0
  167. package/.next/server/chunks/_1j8k_j5._.js +3 -0
  168. package/.next/server/chunks/_1krd9rf._.js +3 -0
  169. package/.next/server/chunks/_1nc0eui._.js +11 -0
  170. package/.next/server/chunks/_1nula3v._.js +3 -0
  171. package/.next/server/chunks/_1pf4qe-._.js +3 -0
  172. package/.next/server/chunks/_1php4eq._.js +1 -1
  173. package/.next/server/chunks/_1qxheoa._.js +3 -0
  174. package/.next/server/chunks/_1x6kd1r._.js +3 -0
  175. package/.next/server/chunks/_1yuv4vp._.js +1 -1
  176. package/.next/server/chunks/_1zjrdag._.js +3 -0
  177. package/.next/server/chunks/_next-internal_server_app_api_bot_memory_files_route_actions_1_xd_8f.js +3 -0
  178. package/.next/server/chunks/_next-internal_server_app_api_bot_memory_route_actions_1a8xfr7.js +3 -0
  179. package/.next/server/chunks/_next-internal_server_app_api_llm-model_credits_route_actions_0v424pb.js +3 -0
  180. package/.next/server/chunks/_next-internal_server_app_api_llm-model_subscription_route_actions_0-udcm5.js +3 -0
  181. package/.next/server/chunks/_next-internal_server_app_api_memory_edit_route_actions_02sfnmr.js +3 -0
  182. package/.next/server/chunks/config_ts_1g4sz5o._.js +1 -1
  183. package/.next/server/chunks/features_0rpfxbk._.js +3 -0
  184. package/.next/server/chunks/features_1qf3cei._.js +3 -0
  185. package/.next/server/chunks/features_bot_0zt92oh._.js +3 -0
  186. package/.next/server/chunks/features_bot_bot_query_ts_12mgkl2._.js +3 -0
  187. package/.next/server/chunks/features_memory_1uqvksn._.js +6 -0
  188. package/.next/server/chunks/{node_modules_consola_dist_chunks_prompt_mjs_0a1rd09._.js → node_modules_consola_dist_chunks_prompt_mjs_0i7cvml._.js} +1 -1
  189. package/.next/server/chunks/{node_modules_consola_dist_chunks_prompt_mjs_06067w6._.js → node_modules_consola_dist_chunks_prompt_mjs_0icqaqw._.js} +1 -1
  190. package/.next/server/chunks/{node_modules_consola_dist_chunks_prompt_mjs_096ais_._.js → node_modules_consola_dist_chunks_prompt_mjs_0r28-sm._.js} +1 -1
  191. package/.next/server/chunks/{node_modules_consola_dist_chunks_prompt_mjs_02s3hjt._.js → node_modules_consola_dist_chunks_prompt_mjs_0v_7drq._.js} +1 -1
  192. package/.next/server/chunks/node_modules_consola_dist_chunks_prompt_mjs_1xoq2rl._.js +3 -0
  193. package/.next/server/chunks/{node_modules_consola_dist_chunks_prompt_mjs_0tvkfgh._.js → node_modules_consola_dist_chunks_prompt_mjs_1y49572._.js} +1 -1
  194. package/.next/server/chunks/ssr/[root-of-the-server]__09q9med._.js +1 -1
  195. package/.next/server/chunks/ssr/{[root-of-the-server]__1qfqvmg._.js → [root-of-the-server]__0_69wqr._.js} +2 -2
  196. package/.next/server/chunks/ssr/[root-of-the-server]__0hg5gib._.js +1 -1
  197. package/.next/server/chunks/ssr/[root-of-the-server]__0i8ie7f._.js +3 -0
  198. package/.next/server/chunks/ssr/{[root-of-the-server]__0jsmmb-._.js → [root-of-the-server]__0mz33a7._.js} +1 -1
  199. package/.next/server/chunks/ssr/[root-of-the-server]__0tbkdlx._.js +3 -0
  200. package/.next/server/chunks/ssr/[root-of-the-server]__0vnmv-s._.js +3 -0
  201. package/.next/server/chunks/ssr/{[root-of-the-server]__0f6s48x._.js → [root-of-the-server]__0xpcv9w._.js} +3 -3
  202. package/.next/server/chunks/ssr/[root-of-the-server]__0xtssj3._.js +1 -1
  203. package/.next/server/chunks/ssr/[root-of-the-server]__0zkv7z_._.js +3 -0
  204. package/.next/server/chunks/ssr/[root-of-the-server]__10-dz5o._.js +57 -0
  205. package/.next/server/chunks/ssr/[root-of-the-server]__15_uhip._.js +3 -0
  206. package/.next/server/chunks/ssr/[root-of-the-server]__16jc196._.js +1 -1
  207. package/.next/server/chunks/ssr/{[root-of-the-server]__0m1b5ky._.js → [root-of-the-server]__1cyjywu._.js} +1 -1
  208. package/.next/server/chunks/ssr/[root-of-the-server]__1eh7hid._.js +3 -0
  209. package/.next/server/chunks/ssr/[root-of-the-server]__1gtlw6v._.js +43 -0
  210. package/.next/server/chunks/ssr/[root-of-the-server]__1mzi-ju._.js +1 -1
  211. package/.next/server/chunks/ssr/[root-of-the-server]__1ox7hbk._.js +4 -0
  212. package/.next/server/chunks/ssr/[root-of-the-server]__1pduf5n._.js +3 -0
  213. package/.next/server/chunks/ssr/{_1mznrxk._.js → _001_ms1._.js} +3 -3
  214. package/.next/server/chunks/ssr/_01_6m32._.js +3 -0
  215. package/.next/server/chunks/ssr/_01n3o0m._.js +3 -0
  216. package/.next/server/chunks/ssr/_07uchnc._.js +3 -0
  217. package/.next/server/chunks/ssr/_0ap2fzn._.js +3 -0
  218. package/.next/server/chunks/ssr/_0c-ql2m._.js +3 -0
  219. package/.next/server/chunks/ssr/_0h42fze._.js +3 -0
  220. package/.next/server/chunks/ssr/_0h7zdni._.js +1 -1
  221. package/.next/server/chunks/ssr/_0i_6q1w._.js +41 -0
  222. package/.next/server/chunks/ssr/_0kbkmar._.js +3 -0
  223. package/.next/server/chunks/ssr/{_077-g6s._.js → _0mb8zc8._.js} +3 -3
  224. package/.next/server/chunks/ssr/_0mo2w06._.js +3 -0
  225. package/.next/server/chunks/ssr/{_13punpm._.js → _0q0zan6._.js} +2 -2
  226. package/.next/server/chunks/ssr/{_040sk32._.js → _0u2jv_x._.js} +76 -82
  227. package/.next/server/chunks/ssr/_0y0tiht._.js +4 -0
  228. package/.next/server/chunks/ssr/_0zhoc3r._.js +3 -0
  229. package/.next/server/chunks/ssr/_1-teee5._.js +3 -3
  230. package/.next/server/chunks/ssr/_13fke0x._.js +3 -0
  231. package/.next/server/chunks/ssr/{_1lip4th._.js → _13z3f85._.js} +2 -2
  232. package/.next/server/chunks/ssr/_14_lw10._.js +3 -0
  233. package/.next/server/chunks/ssr/_15j2_8f._.js +3 -0
  234. package/.next/server/chunks/ssr/{_09ja_w1._.js → _1eoqg_g._.js} +2 -2
  235. package/.next/server/chunks/ssr/_1sji22n._.js +3 -0
  236. package/.next/server/chunks/ssr/_1spzm8z._.js +3 -0
  237. package/.next/server/chunks/ssr/{_0x_nu5e._.js → _1vi_uo7._.js} +2 -2
  238. package/.next/server/chunks/ssr/_1vyd1ji._.js +1 -1
  239. package/.next/server/chunks/ssr/{_1m3bsfr._.js → _1xvgn77._.js} +2 -2
  240. package/.next/server/chunks/ssr/_20kd_n0._.js +42 -0
  241. package/.next/server/chunks/ssr/features_bot_1s_-2vj._.js +3 -0
  242. package/.next/server/chunks/ssr/features_config_components_config-setting_tsx_0xfwqdg._.js +1 -1
  243. package/.next/server/chunks/ssr/features_connectors_components_mcp-setting_tsx_0jmbgvp._.js +1 -1
  244. package/.next/server/chunks/ssr/lib_date-like_ts_02is0pw._.js +3 -0
  245. package/.next/server/chunks/ssr/{node_modules_18_dk7_._.js → node_modules_063xwpp._.js} +1 -1
  246. package/.next/server/chunks/ssr/{node_modules_01qnv-a._.js → node_modules_0bvraul._.js} +1 -1
  247. package/.next/server/chunks/ssr/{node_modules_0n1kj4p._.js → node_modules_0eu1va6._.js} +1 -1
  248. package/.next/server/chunks/ssr/{node_modules_0kl7-tn._.js → node_modules_0h50b8n._.js} +1 -1
  249. package/.next/server/chunks/ssr/{node_modules_0_n241r._.js → node_modules_0h5hnr1._.js} +1 -1
  250. package/.next/server/chunks/ssr/{node_modules_0lrjk5t._.js → node_modules_0h7u60n._.js} +1 -1
  251. package/.next/server/chunks/ssr/{node_modules_0ndvxub._.js → node_modules_0lm7_vm._.js} +1 -1
  252. package/.next/server/chunks/ssr/{node_modules_13r44jq._.js → node_modules_0mwym0g._.js} +1 -1
  253. package/.next/server/chunks/ssr/{node_modules_17lrt3y._.js → node_modules_0rjg6t1._.js} +1 -1
  254. package/.next/server/chunks/ssr/node_modules_0x3-ply._.js +3 -0
  255. package/.next/server/chunks/ssr/{node_modules_004qlr4._.js → node_modules_0y_cded._.js} +1 -1
  256. package/.next/server/chunks/ssr/{node_modules_0kxz4gt._.js → node_modules_0z5tsu8._.js} +1 -1
  257. package/.next/server/chunks/ssr/{node_modules_0iz1rzd._.js → node_modules_0zp9r92._.js} +1 -1
  258. package/.next/server/chunks/ssr/{node_modules_19dp7ko._.js → node_modules_1-hc6uv._.js} +1 -1
  259. package/.next/server/chunks/ssr/{node_modules_1q0p85z._.js → node_modules_113tz0l._.js} +1 -1
  260. package/.next/server/chunks/ssr/{node_modules_1toafc-._.js → node_modules_124bue5._.js} +1 -1
  261. package/.next/server/chunks/ssr/node_modules_17bwo43._.js +55 -0
  262. package/.next/server/chunks/ssr/{node_modules_08d8bke._.js → node_modules_1_n3ztw._.js} +1 -1
  263. package/.next/server/chunks/ssr/{node_modules_1dwguw_._.js → node_modules_1bc7gu8._.js} +1 -1
  264. package/.next/server/chunks/ssr/{node_modules_20yebtj._.js → node_modules_1bgb6-f._.js} +1 -1
  265. package/.next/server/chunks/ssr/{node_modules_1n5ygqe._.js → node_modules_1h4a4pp._.js} +1 -1
  266. package/.next/server/chunks/ssr/{node_modules_04n7mjs._.js → node_modules_1i7u1ia._.js} +1 -1
  267. package/.next/server/chunks/ssr/{node_modules_1hn8z1f._.js → node_modules_1juq3hs._.js} +1 -1
  268. package/.next/server/chunks/ssr/{node_modules_1xyd7ov._.js → node_modules_1l0qtdu._.js} +1 -1
  269. package/.next/server/chunks/ssr/{node_modules_0m93zeg._.js → node_modules_1t9cyee._.js} +1 -1
  270. package/.next/server/chunks/ssr/{node_modules_09xhur7._.js → node_modules_1uuj9x1._.js} +1 -1
  271. package/.next/server/chunks/ssr/{node_modules_1hd-5tv._.js → node_modules_1vqb6po._.js} +1 -1
  272. package/.next/server/chunks/ssr/{node_modules_1zu988w._.js → node_modules_1xx28h1._.js} +1 -1
  273. package/.next/server/chunks/ssr/{node_modules_@mermaid-js_parser_dist_chunks_mermaid-parser_core_0m9a4y0._.js → node_modules_@mermaid-js_parser_dist_chunks_mermaid-parser_core_0d3skpl._.js} +1 -1
  274. package/.next/server/chunks/ssr/{node_modules_@mermaid-js_parser_dist_chunks_mermaid-parser_core_13w_wy9._.js → node_modules_@mermaid-js_parser_dist_chunks_mermaid-parser_core_0i44kk5._.js} +1 -1
  275. package/.next/server/chunks/ssr/node_modules_@vercel_oidc_dist_0qe30a_._.js +3 -0
  276. package/.next/server/chunks/ssr/{node_modules_next_dist_0w6mzq5._.js → node_modules_next_dist_0drixxt._.js} +4 -4
  277. package/.next/server/chunks/ssr/node_modules_next_dist_1n3w9lb._.js +2 -2
  278. package/.next/server/instrumentation.js +1 -1
  279. package/.next/server/middleware-build-manifest.js +3 -3
  280. package/.next/server/pages/404.html +1 -1
  281. package/.next/server/pages/500.html +1 -1
  282. package/.next/server/server-reference-manifest.js +1 -1
  283. package/.next/server/server-reference-manifest.json +58 -91
  284. package/.next/static/chunks/{31ttnjt6g35a0.js → 0ev4hyghq0mbb.js} +1 -1
  285. package/.next/static/chunks/0r5qcctujhxli.js +1 -0
  286. package/.next/static/chunks/0tmgrnlje14-s.js +3 -0
  287. package/.next/static/chunks/0x79tx04dxbpp.js +1 -0
  288. package/.next/static/chunks/0xzgvc0fkdb6t.js +1 -0
  289. package/.next/static/chunks/10teww7wt-f6h.js +1 -0
  290. package/.next/static/chunks/14hgtsjg-kael.css +1 -0
  291. package/.next/static/chunks/17donp69y4do7.js +40 -0
  292. package/.next/static/chunks/{3_h9txt3tq8t8.js → 1dkjg87pxk0n9.js} +1 -1
  293. package/.next/static/chunks/1ekc7ul556kbl.js +1 -0
  294. package/.next/static/chunks/{3ard_noqapnw1.js → 1grhjo7a5a0_-.js} +1 -1
  295. package/.next/static/chunks/1kq8h8wi_e0bc.js +53 -0
  296. package/.next/static/chunks/1okqiww3pnwka.js +1 -0
  297. package/.next/static/chunks/26-pr2c9ro222.js +1 -0
  298. package/.next/static/chunks/{18vhba44l1fc9.js → 280t0_e99i276.js} +1 -1
  299. package/.next/static/chunks/28cizcdaabd4h.js +1 -0
  300. package/.next/static/chunks/2a0k6jlq0_v1u.js +2 -0
  301. package/.next/static/chunks/2fzs9fm8yarvq.js +1 -0
  302. package/.next/static/chunks/2h0sd2vumhb15.js +1 -0
  303. package/.next/static/chunks/2hw2f6zmt7fmt.js +1 -0
  304. package/.next/static/chunks/2t4vhi3m8ahya.js +1 -0
  305. package/.next/static/chunks/2tigdtz3dpcy8.js +1 -0
  306. package/.next/static/chunks/3111pj5wnjgos.js +1 -0
  307. package/.next/static/chunks/{07kl352c-lvcz.js → 3agpgkwaa321p.js} +1 -1
  308. package/.next/static/chunks/3cfi_q6d_wtci.js +1 -0
  309. package/.next/static/chunks/3ok9_x74rip3v.js +1 -0
  310. package/.next/static/chunks/3xk7yqgomx7yq.js +1 -0
  311. package/.next/static/chunks/3xpbkl66ssib5.js +38 -0
  312. package/.next/static/media/favicon.37o1pjldwi2uc.ico +0 -0
  313. package/README.md +6 -4
  314. package/app/api/bot/memory/files/route.ts +14 -0
  315. package/app/api/bot/memory/route.ts +5 -0
  316. package/app/api/events/app-event.ts +3 -5
  317. package/app/api/llm-model/credits/route.ts +11 -0
  318. package/app/api/llm-model/route.ts +6 -1
  319. package/app/api/llm-model/subscription/route.ts +11 -0
  320. package/app/api/memory/edit/route.ts +7 -0
  321. package/app/api/memory/route.ts +21 -0
  322. package/app/api/query-key.ts +19 -4
  323. package/bin/thursday.mjs +3 -0
  324. package/components/ui/shiny-text.tsx +48 -134
  325. package/database/migrations/{20260909060813_optimal_sabretooth → 20260910173049_numerous_spirit}/migration.sql +24 -34
  326. package/database/migrations/{20260909060813_optimal_sabretooth → 20260910173049_numerous_spirit}/snapshot.json +71 -145
  327. package/database/migrations/20260911030320_opposite_jack_power/migration.sql +1 -0
  328. package/database/migrations/{20260909063821_bitter_tenebrous → 20260911030320_opposite_jack_power}/snapshot.json +57 -184
  329. package/features/ai/chatgpt.ts +617 -0
  330. package/features/ai/components/chatgpt-sign-in.tsx +66 -0
  331. package/features/ai/components/model-picker.tsx +17 -1
  332. package/features/ai/components/provider-icon.tsx +1 -0
  333. package/features/ai/load-tools.ts +58 -42
  334. package/features/ai/model.schema.ts +232 -34
  335. package/features/ai/model.ts +169 -24
  336. package/features/ai/prompts/bot.prompt.ts +156 -65
  337. package/features/ai/prompts/memory-edit.prompt.ts +33 -0
  338. package/features/ai/prompts/prompt-helper.ts +45 -26
  339. package/features/ai/prompts/thursday.prompt.ts +40 -17
  340. package/features/ai/tools/bot.tool.ts +35 -52
  341. package/features/ai/tools/connected.ts +17 -4
  342. package/features/ai/tools/memory.tool.ts +133 -72
  343. package/features/ai/tools/search.tool.ts +136 -47
  344. package/features/ai/tools/tool-name.ts +2 -5
  345. package/features/ai/tools/workspace.tool.ts +2 -1
  346. package/features/bot/bot.action.ts +8 -18
  347. package/features/bot/bot.memory.ts +89 -0
  348. package/features/bot/bot.query.ts +37 -57
  349. package/features/bot/bot.run.ts +507 -241
  350. package/features/bot/bot.runner.ts +410 -114
  351. package/features/bot/bot.schema.ts +79 -19
  352. package/features/bot/bot.seed.ts +89 -16
  353. package/features/bot/components/bot-mark.tsx +122 -20
  354. package/features/bot/components/bot-room.tsx +159 -130
  355. package/features/bot/components/bot-setting.tsx +790 -402
  356. package/features/bot/components/bot-tool.tsx +2 -6
  357. package/features/bot/components/mark-palette.tsx +54 -0
  358. package/features/bot/components/path-chips.tsx +1 -1
  359. package/features/bot/components/task-badge.tsx +26 -6
  360. package/features/bot/components/task-reply.tsx +22 -33
  361. package/features/bot/components/task-setting.tsx +20 -17
  362. package/features/bot/task.query.ts +215 -28
  363. package/features/bot/task.store.ts +53 -9
  364. package/features/config/components/config-setting.tsx +265 -34
  365. package/features/config/components/models-badge.tsx +42 -0
  366. package/features/config/config.action.ts +12 -0
  367. package/features/config/config.const.ts +38 -2
  368. package/features/connectors/mcp.manager.ts +10 -2
  369. package/features/intro/components/intro.tsx +47 -46
  370. package/features/memory/components/memory-edit.tsx +338 -0
  371. package/features/memory/components/memory-setting.tsx +75 -77
  372. package/features/memory/memory.action.ts +3 -43
  373. package/features/memory/memory.edit.ts +87 -0
  374. package/features/memory/memory.query.ts +24 -42
  375. package/features/memory/memory.schema.ts +29 -106
  376. package/features/settings/components/settings.tsx +2 -0
  377. package/features/settings/settings.alert.ts +3 -1
  378. package/features/thursday/components/ascii-orb.tsx +106 -90
  379. package/features/thursday/components/boot.tsx +7 -0
  380. package/features/thursday/components/call-log.tsx +219 -92
  381. package/features/thursday/components/face.tsx +6 -4
  382. package/features/thursday/components/tab-state.tsx +97 -0
  383. package/features/thursday/components/thursday-mark.tsx +4 -1
  384. package/features/thursday/components/thursday-setting.tsx +17 -47
  385. package/features/thursday/components/thursday.tsx +208 -95
  386. package/features/thursday/thursday.action.ts +12 -17
  387. package/features/thursday/thursday.query.ts +61 -70
  388. package/features/thursday/thursday.schema.ts +20 -7
  389. package/features/thursday/tool-line.ts +85 -11
  390. package/features/thursday/use-thursday.ts +86 -59
  391. package/features/workspace/components/file-view.tsx +4 -2
  392. package/features/workspace/workspace.ts +110 -26
  393. package/lib/limits.ts +1 -1
  394. package/lib/oauth-page.ts +27 -0
  395. package/lib/realtime/openai.ts +2 -2
  396. package/lib/realtime/realtime.protocol.ts +1 -1
  397. package/lib/realtime/realtime.tap.ts +122 -0
  398. package/lib/sandbox.ts +59 -36
  399. package/package.json +1 -1
  400. package/skills/browser/SKILL.md +55 -34
  401. package/.next/server/app/api/bot/notes/route/app-paths-manifest.json +0 -3
  402. package/.next/server/app/api/bot/notes/route.js +0 -14
  403. package/.next/server/app/api/bot/notes/route_client-reference-manifest.js +0 -3
  404. package/.next/server/app/api/memory/tidy/route/app-paths-manifest.json +0 -3
  405. package/.next/server/app/api/memory/tidy/route.js +0 -22
  406. package/.next/server/app/api/memory/tidy/route.js.nft.json +0 -1
  407. package/.next/server/chunks/0d-b_next_dist_server_app-render_after-task-async-storage_external_0-exn7p.js +0 -3
  408. package/.next/server/chunks/[root-of-the-server]__0-npkh_._.js +0 -3
  409. package/.next/server/chunks/[root-of-the-server]__005var5._.js +0 -3
  410. package/.next/server/chunks/[root-of-the-server]__019dx9f._.js +0 -5
  411. package/.next/server/chunks/[root-of-the-server]__02_2sm-._.js +0 -5
  412. package/.next/server/chunks/[root-of-the-server]__04_8spk._.js +0 -3
  413. package/.next/server/chunks/[root-of-the-server]__05luceg._.js +0 -3
  414. package/.next/server/chunks/[root-of-the-server]__06t77t4._.js +0 -11
  415. package/.next/server/chunks/[root-of-the-server]__0bed6hu._.js +0 -8
  416. package/.next/server/chunks/[root-of-the-server]__0cro2dz._.js +0 -5
  417. package/.next/server/chunks/[root-of-the-server]__0dupqt7._.js +0 -5
  418. package/.next/server/chunks/[root-of-the-server]__0emr6go._.js +0 -3
  419. package/.next/server/chunks/[root-of-the-server]__0le4d-p._.js +0 -3
  420. package/.next/server/chunks/[root-of-the-server]__0m8ywyo._.js +0 -10
  421. package/.next/server/chunks/[root-of-the-server]__0nfuugi._.js +0 -5
  422. package/.next/server/chunks/[root-of-the-server]__0p32jfz._.js +0 -12
  423. package/.next/server/chunks/[root-of-the-server]__0phdgco._.js +0 -8
  424. package/.next/server/chunks/[root-of-the-server]__0wvgrsl._.js +0 -3
  425. package/.next/server/chunks/[root-of-the-server]__10qshki._.js +0 -3
  426. package/.next/server/chunks/[root-of-the-server]__11j52t0._.js +0 -5
  427. package/.next/server/chunks/[root-of-the-server]__1692zrv._.js +0 -3
  428. package/.next/server/chunks/[root-of-the-server]__16thabu._.js +0 -3
  429. package/.next/server/chunks/[root-of-the-server]__173nhwf._.js +0 -8
  430. package/.next/server/chunks/[root-of-the-server]__1_aoyr9._.js +0 -3
  431. package/.next/server/chunks/[root-of-the-server]__1bn73ih._.js +0 -10
  432. package/.next/server/chunks/[root-of-the-server]__1bqhuh9._.js +0 -8
  433. package/.next/server/chunks/[root-of-the-server]__1cqizfq._.js +0 -5
  434. package/.next/server/chunks/[root-of-the-server]__1e-ci13._.js +0 -3
  435. package/.next/server/chunks/[root-of-the-server]__1fboj2j._.js +0 -3
  436. package/.next/server/chunks/[root-of-the-server]__1lr7j9t._.js +0 -5
  437. package/.next/server/chunks/[root-of-the-server]__1oli73c._.js +0 -3
  438. package/.next/server/chunks/[root-of-the-server]__1txedx6._.js +0 -8
  439. package/.next/server/chunks/[root-of-the-server]__1untqv8._.js +0 -5
  440. package/.next/server/chunks/[root-of-the-server]__1zgpz1w._.js +0 -3
  441. package/.next/server/chunks/_00_584r._.js +0 -293
  442. package/.next/server/chunks/_012yhfm._.js +0 -39
  443. package/.next/server/chunks/_06mxlgn._.js +0 -3
  444. package/.next/server/chunks/_07rtlxv._.js +0 -3
  445. package/.next/server/chunks/_0a351g7._.js +0 -6
  446. package/.next/server/chunks/_0kjjmag._.js +0 -3
  447. package/.next/server/chunks/_0nn99o2._.js +0 -3
  448. package/.next/server/chunks/_0zuzmt_._.js +0 -39
  449. package/.next/server/chunks/_106co2o._.js +0 -3
  450. package/.next/server/chunks/_12bbv5i._.js +0 -100
  451. package/.next/server/chunks/_15q1pig._.js +0 -3
  452. package/.next/server/chunks/_19wg5yn._.js +0 -138
  453. package/.next/server/chunks/_1bt95p9._.js +0 -55
  454. package/.next/server/chunks/_1cbk7ms._.js +0 -26
  455. package/.next/server/chunks/_1drv2et._.js +0 -40
  456. package/.next/server/chunks/_1ic3bew._.js +0 -5
  457. package/.next/server/chunks/_1mxn_lb._.js +0 -3
  458. package/.next/server/chunks/_1rysr-r._.js +0 -10
  459. package/.next/server/chunks/_1u3rwbr._.js +0 -3
  460. package/.next/server/chunks/_1x7p319._.js +0 -6
  461. package/.next/server/chunks/_next-internal_server_app_api_bot_notes_route_actions_0qcsyu7.js +0 -3
  462. package/.next/server/chunks/_next-internal_server_app_api_memory_tidy_route_actions_0_cfuom.js +0 -3
  463. package/.next/server/chunks/features_bot_task_query_ts_11xuzl3._.js +0 -3
  464. package/.next/server/chunks/node_modules_consola_dist_chunks_prompt_mjs_0gzvgfv._.js +0 -3
  465. package/.next/server/chunks/node_modules_consola_dist_chunks_prompt_mjs_0vorg6w._.js +0 -3
  466. package/.next/server/chunks/node_modules_consola_dist_chunks_prompt_mjs_1tpvamf._.js +0 -3
  467. package/.next/server/chunks/node_modules_consola_dist_chunks_prompt_mjs_1vmaq9f._.js +0 -3
  468. package/.next/server/chunks/node_modules_consola_dist_chunks_prompt_mjs_1y9-wcr._.js +0 -3
  469. package/.next/server/chunks/ssr/[externals]_node_crypto_1kn79-7._.js +0 -3
  470. package/.next/server/chunks/ssr/[root-of-the-server]__0iuoa-r._.js +0 -3
  471. package/.next/server/chunks/ssr/[root-of-the-server]__0un-erv._.js +0 -4
  472. package/.next/server/chunks/ssr/[root-of-the-server]__1-mnhyd._.js +0 -92
  473. package/.next/server/chunks/ssr/[root-of-the-server]__1bww500._.js +0 -3
  474. package/.next/server/chunks/ssr/[root-of-the-server]__1vcdast._.js +0 -3
  475. package/.next/server/chunks/ssr/[root-of-the-server]__1xwnu-h._.js +0 -3
  476. package/.next/server/chunks/ssr/_0-mc7ro._.js +0 -3
  477. package/.next/server/chunks/ssr/_01p5r6-._.js +0 -3
  478. package/.next/server/chunks/ssr/_03webdr._.js +0 -3
  479. package/.next/server/chunks/ssr/_09-0k3z._.js +0 -3
  480. package/.next/server/chunks/ssr/_0f_b_9s._.js +0 -3
  481. package/.next/server/chunks/ssr/_0fafgvl._.js +0 -3
  482. package/.next/server/chunks/ssr/_0sf1isi._.js +0 -3
  483. package/.next/server/chunks/ssr/_0su6_08._.js +0 -3
  484. package/.next/server/chunks/ssr/_0ti51xc._.js +0 -3
  485. package/.next/server/chunks/ssr/_0w5bgak._.js +0 -3
  486. package/.next/server/chunks/ssr/_0y84qo6._.js +0 -3
  487. package/.next/server/chunks/ssr/_1ciyoo3._.js +0 -3
  488. package/.next/server/chunks/ssr/_1e-dgz0._.js +0 -4
  489. package/.next/server/chunks/ssr/_1fq7zgb._.js +0 -3
  490. package/.next/server/chunks/ssr/_1g6l4uj._.js +0 -3
  491. package/.next/server/chunks/ssr/_1ruzehw._.js +0 -3
  492. package/.next/server/chunks/ssr/features_bot_components_bot-setting_tsx_01ajft6._.js +0 -4
  493. package/.next/server/chunks/ssr/features_config_components_14jqgy4._.js +0 -3
  494. package/.next/server/chunks/ssr/features_memory_components_memory-setting_tsx_0oa43sw._.js +0 -4
  495. package/.next/server/chunks/ssr/node_modules_zod_v4_classic_external_15fmmz3.js +0 -55
  496. package/.next/static/chunks/05b6nx_o3_f_n.js +0 -1
  497. package/.next/static/chunks/0dmns-gi0raa4.js +0 -1
  498. package/.next/static/chunks/0f9sq-7q_yxyl.js +0 -1
  499. package/.next/static/chunks/0oc4_xoia77dg.css +0 -1
  500. package/.next/static/chunks/0pheo8m3a-rzi.js +0 -1
  501. package/.next/static/chunks/19i8h2t-5evjx.js +0 -1
  502. package/.next/static/chunks/1c5fcj473025q.js +0 -1
  503. package/.next/static/chunks/1pi_hbmjw-f-z.js +0 -1
  504. package/.next/static/chunks/1twt6k09vg4np.js +0 -1
  505. package/.next/static/chunks/1ytnip8wdng93.js +0 -1
  506. package/.next/static/chunks/28gotuisldmom.js +0 -1
  507. package/.next/static/chunks/2998p0y7k1c8z.js +0 -3
  508. package/.next/static/chunks/29gyb3nubj51j.js +0 -53
  509. package/.next/static/chunks/2l233xfycjpd8.js +0 -1
  510. package/.next/static/chunks/2p5n7pa3c40t-.js +0 -2
  511. package/.next/static/chunks/2ru1q132af7h7.js +0 -1
  512. package/.next/static/chunks/2t8wqycwg3y04.js +0 -2
  513. package/.next/static/chunks/2uxum7qbyeyxd.js +0 -1
  514. package/.next/static/chunks/32z_ypgtchw48.js +0 -1
  515. package/.next/static/chunks/33pyjb3xd1iez.js +0 -1
  516. package/.next/static/chunks/3omud5xunrk1f.js +0 -1
  517. package/.next/static/chunks/42dnrkbph9e-1.js +0 -1
  518. package/.next/static/media/favicon.2vob68tjqpejf.ico +0 -0
  519. package/database/migrations/20260909063821_bitter_tenebrous/migration.sql +0 -1
  520. package/features/ai/prompts/memory-tidy.prompt.ts +0 -129
  521. package/features/ai/tools/tidy.tool.ts +0 -22
  522. package/features/bot/bot.notes.ts +0 -120
  523. package/features/memory/components/memory-tidy.tsx +0 -395
  524. package/features/memory/memory.tidy.ts +0 -332
  525. package/features/memory/tidy.query.ts +0 -135
  526. package/features/thursday/use-call-title.ts +0 -31
  527. /package/.next/server/app/api/bot/{notes → memory/files}/route/build-manifest.json +0 -0
  528. /package/.next/server/app/api/bot/{notes → memory/files}/route/server-reference-manifest.json +0 -0
  529. /package/.next/server/app/api/bot/{notes → memory/files}/route.js.map +0 -0
  530. /package/.next/server/app/api/{memory/tidy → bot/memory}/route/build-manifest.json +0 -0
  531. /package/.next/server/app/api/{memory/tidy → bot/memory}/route/server-reference-manifest.json +0 -0
  532. /package/.next/server/app/api/{memory/tidy → bot/memory}/route.js.map +0 -0
  533. /package/.next/static/{YR4NWcDDuzwLbTTU7U7Vr → x1cCqY0kWpR_VcYx_lWO5}/_buildManifest.js +0 -0
  534. /package/.next/static/{YR4NWcDDuzwLbTTU7U7Vr → x1cCqY0kWpR_VcYx_lWO5}/_clientMiddlewareManifest.js +0 -0
  535. /package/.next/static/{YR4NWcDDuzwLbTTU7U7Vr → x1cCqY0kWpR_VcYx_lWO5}/_ssgManifest.js +0 -0
@@ -2,8 +2,10 @@
2
2
 
3
3
  import {
4
4
  Check,
5
+ ChevronRight,
6
+ CircleAlert,
7
+ FolderOpen,
5
8
  History,
6
- PencilLine,
7
9
  Plus,
8
10
  Trash2,
9
11
  Wrench,
@@ -21,34 +23,57 @@ import {
21
23
  CommandList,
22
24
  } from "@/components/ui/command";
23
25
  import { Input } from "@/components/ui/input";
26
+ import {
27
+ InputGroup,
28
+ InputGroupAddon,
29
+ InputGroupInput,
30
+ InputGroupText,
31
+ } from "@/components/ui/input-group";
24
32
  import { notify } from "@/components/ui/notify";
25
- import ShinyText from "@/components/ui/shiny-text";
26
- import { Swatch } from "@/components/ui/swatch";
33
+ import { ShinyText } from "@/components/ui/shiny-text";
34
+ import { Skeleton } from "@/components/ui/skeleton";
27
35
  import { Switch } from "@/components/ui/switch";
28
36
  import { Textarea } from "@/components/ui/textarea";
29
- import { BOT_NOTES, PROMPT_CROWDED } from "@/config";
37
+ import { APP_NAME, BOT_RUN, PROMPT_CROWDED } from "@/config";
30
38
  import { ModelPicker } from "@/features/ai/components/model-picker";
31
- import type { TextModelProviderId } from "@/features/ai/model.schema";
32
39
  import {
33
- clearBotNoteAction,
40
+ compactAtFor,
41
+ contextWindowOf,
42
+ type GatewayModel,
43
+ type TextModelProviderId,
44
+ } from "@/features/ai/model.schema";
45
+ import {
34
46
  createBotAction,
35
47
  createSeedBotsAction,
36
48
  deleteBotAction,
37
- setBotNotesOnAction,
49
+ setBotMemoryOnAction,
38
50
  updateBotAction,
39
51
  } from "@/features/bot/bot.action";
40
52
  import {
41
53
  type Bot,
42
54
  type BotForm,
43
55
  type BotIcon,
44
- isBudgetAsk,
56
+ type BotMemory,
57
+ type BotMemoryFile,
45
58
  MAX_PINNED_TOOLS,
46
59
  randomBotIcon,
47
60
  type Task,
48
61
  } from "@/features/bot/bot.schema";
49
- 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";
50
67
  import { BotMark } from "@/features/bot/components/bot-mark";
68
+ import { MarkPalette } from "@/features/bot/components/mark-palette";
51
69
  import {
70
+ type ConfigStatus,
71
+ isConfigSet,
72
+ MEDIA_MODEL_KEYS,
73
+ mediaModelWords,
74
+ } from "@/features/config/config.const";
75
+ import {
76
+ SettingDialogContent,
52
77
  SettingError,
53
78
  SettingNote,
54
79
  SettingPanes,
@@ -56,13 +81,22 @@ import {
56
81
  SettingRailNote,
57
82
  } from "@/features/settings/components/setting-ui";
58
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";
59
93
  import { useObjectState } from "@/hooks/use-object-state";
60
94
  import { type DateLike, shortAgo, whenOf } from "@/lib/date-like";
61
95
  import { COMMON_VALIDATE } from "@/lib/limits";
62
96
  import { useServerAction } from "@/lib/protocol/use-server-action";
63
97
  import { revalidate, useServerRoute } from "@/lib/protocol/use-server-route";
64
98
  import { cn, formatCount, WAITING_INK } from "@/lib/utils";
65
- import { MARK_PALETTE, MARK_SHAPES, MARK_SYSTEM } from "../mark.const";
99
+ import { MARK_SHAPES, MARK_SYSTEM } from "../mark.const";
66
100
 
67
101
  /**
68
102
  * The bots Thursday hands background work to. Roster on the left, the picked bot's
@@ -77,35 +111,56 @@ export function BotSetting() {
77
111
  /** Picked roster entry: a bot name, NEW, or null for the first bot. */
78
112
  const [picked, setPicked] = useState<string | null>(null);
79
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
+
80
122
  if (isLoading) return <SettingPanesSkeleton />;
81
123
  if (error) return <SettingError message={error.message} />;
82
124
 
83
125
  const drafting = picked === NEW;
126
+ const on = bots.filter((bot) => !bot.disabled).length;
84
127
  const have = new Set(bots.map((bot) => bot.name));
85
128
  const missing = BOT_SEEDS.filter((seed) => !have.has(seed.name));
86
- const current =
87
- drafting || picked === SEEDS
88
- ? null
89
- : (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);
90
132
 
91
133
  return (
92
134
  <SettingPanes
93
- footer={picked === SEEDS ? null : <BotRail bot={current} />}
135
+ footer={bots.length === 0 ? null : <BotRail bot={current} />}
94
136
  left={
95
137
  <div className="flex flex-col py-2">
96
- <button
97
- type="button"
98
- onClick={() => setPicked(NEW)}
99
- className={cn(
100
- "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",
101
- drafting
102
- ? "bg-secondary text-foreground"
103
- : "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
+ />
104
162
  )}
105
- >
106
- <Plus className="size-3.5 shrink-0" />
107
- New bot
108
- </button>
163
+ </div>
109
164
 
110
165
  {bots.map((bot) => (
111
166
  <RosterRow
@@ -117,26 +172,17 @@ export function BotSetting() {
117
172
  />
118
173
  ))}
119
174
 
120
- {missing.length > 0 && (
121
- <SeedEntry
122
- missing={missing}
123
- active={picked === SEEDS}
124
- onPick={() => setPicked(SEEDS)}
125
- />
126
- )}
127
-
128
- {bots.length > PROMPT_CROWDED.bots && (
175
+ {/* Only bots that are on: a switched-off one is in no prompt to crowd. */}
176
+ {on > PROMPT_CROWDED.bots && (
129
177
  <SettingNote className="mx-3 mt-2 leading-relaxed">
130
- {bots.length} bots. Each is a line in every prompt, and one more
131
- for Thursday to choose between.
178
+ {on} bots are on. Each is a line in every prompt, and one more for
179
+ Thursday to choose between.
132
180
  </SettingNote>
133
181
  )}
134
182
  </div>
135
183
  }
136
184
  right={
137
- picked === SEEDS ? (
138
- <SeedPackage have={have} onDone={(name) => setPicked(name)} />
139
- ) : drafting ? (
185
+ drafting ? (
140
186
  <BotPage
141
187
  key="new"
142
188
  jobs={[]}
@@ -153,6 +199,14 @@ export function BotSetting() {
153
199
  .slice(0, RECENT)}
154
200
  onDone={() => setPicked(null)}
155
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
+ />
156
210
  ) : (
157
211
  <div className="space-y-4 p-8">
158
212
  <p className="text-sm leading-relaxed text-muted-foreground">
@@ -173,8 +227,6 @@ export function BotSetting() {
173
227
 
174
228
  /** Roster selections that are not a bot; values no bot name can be. */
175
229
  const NEW = " new";
176
- const SEEDS = " seeds";
177
-
178
230
  /** Jobs shown under Recent on a bot's page. */
179
231
  const RECENT = 3;
180
232
 
@@ -191,7 +243,7 @@ function RosterRow({
191
243
  active: boolean;
192
244
  onPick: () => void;
193
245
  }) {
194
- const line = liveLine(job, bot.lastJobAt);
246
+ const line = liveLine(job, bot.lastJobAt, bot.disabled);
195
247
  return (
196
248
  <button
197
249
  type="button"
@@ -206,9 +258,14 @@ function RosterRow({
206
258
  {...markProps(bot.name, bot.icon)}
207
259
  state={job?.status === "running" ? "thinking" : "idle"}
208
260
  notify={job?.status === "waiting"}
209
- className="shrink-0"
261
+ className={cn("shrink-0", bot.disabled && "opacity-45")}
210
262
  />
211
- <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
+ >
212
269
  <span
213
270
  className={cn(
214
271
  "block truncate text-[13px]",
@@ -218,23 +275,11 @@ function RosterRow({
218
275
  {bot.name}
219
276
  </span>
220
277
  {line.shine ? (
221
- // The colours come from CSS variables so they follow the theme.
222
- <span
223
- className={cn(
224
- "block",
225
- line.amber
226
- ? "[--rest:var(--color-amber-700)] [--shine:var(--color-amber-400)] dark:[--rest:var(--color-amber-400)] dark:[--shine:var(--color-amber-100)]"
227
- : "[--rest:var(--muted-foreground)] [--shine:var(--foreground)]",
228
- )}
229
- >
230
- <ShinyText
231
- text={line.text}
232
- speed={2.4}
233
- color="var(--rest)"
234
- shineColor="var(--shine)"
235
- className="truncate font-mono text-[11px] leading-4"
236
- />
237
- </span>
278
+ <ShinyText
279
+ text={line.text}
280
+ tone={line.amber ? "waiting" : "muted"}
281
+ className="block truncate font-mono text-[11px] leading-4"
282
+ />
238
283
  ) : (
239
284
  <span className="block truncate font-mono text-[11px] leading-4 text-muted-foreground">
240
285
  {line.text}
@@ -245,27 +290,35 @@ function RosterRow({
245
290
  );
246
291
  }
247
292
 
248
- /** 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
+ */
249
298
  function liveLine(
250
299
  job: Task | null,
251
300
  lastJobAt: Bot["lastJobAt"],
301
+ disabled: boolean,
252
302
  ): {
253
303
  text: string;
254
304
  shine?: boolean;
255
305
  amber?: boolean;
256
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" };
257
318
  if (!job) {
258
319
  if (!lastJobAt) return { text: "idle · no jobs yet" };
259
320
  return { text: `idle · last job ${sinceWord(lastJobAt)}` };
260
321
  }
261
- if (job.status === "waiting") {
262
- return isBudgetAsk(job.ask)
263
- ? { text: "out of steps" }
264
- : { text: "waiting on you", shine: true, amber: true };
265
- }
266
- if (job.status === "running") {
267
- return { text: `working · ${job.label}`, shine: true };
268
- }
269
322
  return { text: `idle · last job ${sinceWord(job.updatedAt)}` };
270
323
  }
271
324
 
@@ -274,86 +327,205 @@ function sinceWord(at: DateLike): string {
274
327
  const ago = shortAgo(at);
275
328
  return ago === "now" ? "just now" : `${ago} ago`;
276
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
+ }
277
346
 
278
347
  /**
279
- * The ready-made bots (bot.seed) as one entry rather than three rows.
280
- *
281
- * They used to fill an empty roster, which made a roster with nothing in it look
282
- * full and made the row you clicked create rather than open. Here they sit behind
283
- * a single line carrying the faces of the ones you do not have, and it is shown
284
- * 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.
285
354
  */
286
- function SeedEntry({
287
- missing,
288
- active,
289
- 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,
290
404
  }: {
291
- missing: BotSeed[];
292
- active: boolean;
293
- onPick: () => void;
405
+ have: Set<string>;
406
+ inks: string[];
407
+ picks: ReturnType<typeof useSeedPicks>;
294
408
  }) {
295
409
  return (
296
- <button
297
- type="button"
298
- onClick={onPick}
299
- className={cn(
300
- "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",
301
- active ? "bg-secondary" : "hover:bg-muted/60",
302
- )}
303
- >
304
- <span className="flex shrink-0 items-center">
305
- {missing.map((seed, index) => (
306
- <span key={seed.name} className={cn("block", index > 0 && "-ml-1.5")}>
307
- <BotMark size={22} {...markProps(seed.name, seed.icon)} />
308
- </span>
309
- ))}
310
- </span>
311
- <span className="min-w-0 flex-1 space-y-0.5">
312
- <span
313
- className={cn(
314
- "block truncate text-[13px]",
315
- active ? "font-medium" : "text-foreground/90",
316
- )}
317
- >
318
- Ready-made
319
- </span>
320
- <span className="block truncate font-mono text-[10px] leading-4 text-muted-foreground">
321
- {missing.length === BOT_SEEDS.length
322
- ? `${missing.length} bots`
323
- : `${missing.length} left`}
324
- </span>
325
- </span>
326
- </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
+ </>
327
510
  );
328
511
  }
329
512
 
330
513
  /**
331
- * The package itself: the seeds ticked already, because the package is the
332
- * recommendation (`BotSeed.recommended`) and unticking is the decision. One
333
- * already on the roster is locked on and says so `createBot` would only answer
334
- * "already exists", which is an error about something this screen already knows.
335
- *
336
- * 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.
337
517
  */
338
518
  function SeedPackage({
339
519
  have,
520
+ inks,
340
521
  onDone,
341
522
  }: {
342
523
  /** Names already on the roster. */
343
524
  have: Set<string>;
525
+ inks: string[];
344
526
  onDone: (name: string | null) => void;
345
527
  }) {
346
- const [off, setOff] = useState<Set<string>>(new Set());
347
- const [add, adding] = useServerAction(createSeedBotsAction, {
348
- onOk: (made) => {
349
- revalidate(queryKey.bot);
350
- onDone(made.created[0] ?? null);
351
- },
352
- });
353
-
354
- const wanted = BOT_SEEDS.filter(
355
- (seed) => !have.has(seed.name) && !off.has(seed.name),
356
- );
528
+ const picks = useSeedPicks(have, inks, onDone);
357
529
 
358
530
  return (
359
531
  <div className="flex min-h-full flex-col">
@@ -363,108 +535,115 @@ function SeedPackage({
363
535
 
364
536
  <div className="px-8 pt-7 pb-1">
365
537
  <h3 className="text-[17px] font-medium tracking-tight">
366
- {BOT_SEEDS.length === 1
367
- ? "One bot comes with her"
368
- : `${BOT_SEEDS.length === 2 ? "Two" : BOT_SEEDS.length} bots come with her`}
538
+ Bots you can add
369
539
  </h3>
370
540
  <p className="mt-1.5 max-w-lg text-[13px] leading-relaxed text-muted-foreground break-keep">
371
- They take the work that would leave the call silent, and report back.
372
- Untick anything you would rather not have; each runs on the app
373
- 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.
374
544
  </p>
375
545
  </div>
376
546
 
377
- <div className="flex flex-col gap-2 px-8 pt-4">
378
- {BOT_SEEDS.map((seed) => {
379
- const owned = have.has(seed.name);
380
- const on = owned || !off.has(seed.name);
381
- return (
382
- <div
383
- key={seed.name}
384
- className={cn(
385
- "rounded-2xl ring-1 transition-colors",
386
- owned
387
- ? "opacity-45 ring-border/60"
388
- : on
389
- ? "ring-foreground"
390
- : "ring-border/60",
391
- )}
392
- >
393
- <button
394
- type="button"
395
- disabled={owned || adding}
396
- aria-pressed={on}
397
- onClick={() =>
398
- setOff((was) => {
399
- const next = new Set(was);
400
- if (next.has(seed.name)) next.delete(seed.name);
401
- else next.add(seed.name);
402
- return next;
403
- })
404
- }
405
- 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"
406
- >
407
- <BotMark
408
- size={32}
409
- {...markProps(seed.name, seed.icon)}
410
- className={cn(
411
- "shrink-0 transition-opacity",
412
- !on && "opacity-35",
413
- )}
414
- />
415
- <span className="min-w-0 flex-1 space-y-0.5">
416
- <span className="block text-[14px] font-medium">
417
- {seed.name}
418
- </span>
419
- <span className="block text-[12px] leading-normal text-muted-foreground break-keep">
420
- {seed.description}
421
- </span>
422
- </span>
423
- {owned && (
424
- <span className="shrink-0 font-mono text-[10px] text-muted-foreground">
425
- already added
426
- </span>
427
- )}
428
- <span
429
- className={cn(
430
- "grid size-6 shrink-0 place-items-center rounded-full transition-colors",
431
- on
432
- ? "bg-foreground text-background"
433
- : "ring-1 ring-border/60 ring-inset",
434
- )}
435
- >
436
- {on && <Check className="size-3.5" />}
437
- </span>
438
- </button>
439
- </div>
440
- );
441
- })}
547
+ <div className="px-8 pt-4">
548
+ <SeedRows have={have} inks={inks} picks={picks} />
442
549
  </div>
443
550
 
444
551
  <div className="mt-auto flex items-center gap-3 border-t border-border/60 px-8 py-4">
445
552
  <span className="flex-1 font-mono text-[11px] text-muted-foreground">
446
- {wanted.length === 0
553
+ {picks.wanted.length === 0
447
554
  ? "nothing ticked"
448
- : `${wanted.length} of ${BOT_SEEDS.length - have.size} ticked`}
555
+ : `${picks.wanted.length} of ${picks.addable.length} ticked`}
449
556
  </span>
450
- <Button variant="ghost" onClick={() => onDone(null)}>
451
- Cancel
452
- </Button>
453
- {/* Says what it will do, so the count is checkable before the click. */}
454
- <Button
455
- loading={adding}
456
- disabled={wanted.length === 0}
457
- onClick={() => add(wanted.map((seed) => ({ name: seed.name })))}
458
- >
459
- {wanted.length === 1
460
- ? `Add ${wanted[0].name}`
461
- : `Add ${wanted.length} bots`}
462
- </Button>
557
+ <SeedActions picks={picks} onCancel={() => onDone(null)} />
463
558
  </div>
464
559
  </div>
465
560
  );
466
561
  }
467
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
+
468
647
  /**
469
648
  * The mark seed is the name, not the row id, so a new bot's preview is the face the
470
649
  * roster will show. An unpicked colour stays empty and means the theme ink.
@@ -506,10 +685,34 @@ function BotPage({
506
685
  icon: bot?.icon ?? randomBotIcon(),
507
686
  provider: (bot?.provider ?? null) as TextModelProviderId | null,
508
687
  model: bot?.model ?? "",
688
+ // In thousands of tokens, the unit the field is typed in
689
+ compactAt: bot?.compactAt ? String(bot.compactAt / 1000) : "",
509
690
  toolIds: bot?.tools.map((tool) => tool.id) ?? [],
510
691
  }));
511
- const { name, description, systemPrompt, icon, provider, model, toolIds } =
512
- 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);
513
716
 
514
717
  const [save] = useServerAction(updateBotAction, {
515
718
  onOk: () => revalidate(queryKey.bot),
@@ -534,12 +737,6 @@ function BotPage({
534
737
  onDone(null);
535
738
  },
536
739
  });
537
- // Shared with the rail's switch; SWR dedups the read
538
- const { data: notesOn } = useServerRoute<boolean>(queryKey.botNotes);
539
- const [clearNote, clearingNote] = useServerAction(clearBotNoteAction, {
540
- okMessage: "Notes cleared",
541
- onOk: () => revalidate(queryKey.bot),
542
- });
543
740
 
544
741
  const ready =
545
742
  name.trim() && description.trim() && provider && model.trim() && !creating;
@@ -553,23 +750,13 @@ function BotPage({
553
750
  icon,
554
751
  provider,
555
752
  model: model.trim(),
753
+ compactAt: compactEdited
754
+ ? tokensFromK(compactAt)
755
+ : filledTokens(pickedWindow),
556
756
  toolIds,
557
757
  });
558
758
  };
559
759
 
560
- /** The bot writes its own notes; the only thing to do to them from here is throw them away. */
561
- const confirmClearNote = async () => {
562
- if (!bot) return;
563
- const confirmed = await notify.confirm({
564
- title: `Clear ${bot.name}'s own prompt?`,
565
- description:
566
- "What it worked out about doing jobs here is gone, and it starts the next one without it.",
567
- okText: "Clear",
568
- destructive: true,
569
- });
570
- if (confirmed) clearNote(bot.name);
571
- };
572
-
573
760
  const confirmRemove = async () => {
574
761
  if (!bot) return;
575
762
  const confirmed = await notify.confirm({
@@ -588,97 +775,37 @@ function BotPage({
588
775
  {bot ? bot.name : "New bot"}
589
776
  </span>
590
777
  {bot && (
591
- <Button
592
- size="icon-sm"
593
- variant="ghost"
594
- loading={removing}
595
- aria-label="Delete this bot"
596
- onClick={confirmRemove}
597
- className="text-muted-foreground hover:text-destructive"
598
- >
599
- <Trash2 />
600
- </Button>
601
- )}
602
- </div>
603
-
604
- <div className="flex-1 space-y-5 p-6">
605
- <div className="flex items-start gap-4">
606
- <BotMark size={72} {...markProps(name, icon)} className="shrink-0" />
607
- <div className="min-w-0 flex-1 space-y-2.5">
608
- {bot ? (
609
- <p className="truncate text-lg leading-8 font-medium">
610
- {bot.name}
611
- </p>
612
- ) : (
613
- <Input
614
- value={name}
615
- onChange={(event) => patch({ name: event.target.value })}
616
- placeholder="researcher"
617
- spellCheck={false}
618
- maxLength={COMMON_VALIDATE.name.max}
619
- autoFocus
620
- />
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>
621
784
  )}
622
- <Input
623
- value={description}
624
- onChange={(event) => patch({ description: event.target.value })}
625
- onBlur={() => {
626
- const next = description.trim();
627
- if (bot && next && next !== bot.description) {
628
- commit({ description: next });
629
- }
630
- }}
631
- placeholder="Searches the web and reports back"
632
- maxLength={COMMON_VALIDATE.description.max}
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"
633
791
  />
634
- </div>
635
- </div>
636
-
637
- {bot?.note && (
638
- /* Not a Row like the fields above it: those are settings the owner
639
- typed, this is what the bot has worked out, and a block under its
640
- face reads as part of who it is rather than one more control. */
641
- <div className="overflow-hidden rounded-xl border border-border/60 bg-muted/25">
642
- <div className="flex items-center gap-2.5 border-b border-border/60 px-4 py-3">
643
- <PencilLine className="size-3.5 shrink-0 text-muted-foreground" />
644
- <span className="min-w-0 flex-1 truncate text-[13px] font-medium">
645
- What {bot.name} has worked out
646
- </span>
647
- <span className="shrink-0 font-mono text-[10px] text-muted-foreground">
648
- {bot.note.length}/{BOT_NOTES.chars}
649
- </span>
650
- </div>
651
- <p className="px-4 py-3.5 text-[13px] leading-relaxed whitespace-pre-wrap">
652
- {bot.note}
653
- </p>
654
- <div className="flex items-center gap-2 px-4 pb-3">
655
- {/* No amber: switched off is the user's own choice, not something
656
- waiting on them. The word carries it. */}
657
- <span className="min-w-0 flex-1 truncate font-mono text-[10px] text-muted-foreground">
658
- {notesOn === false
659
- ? "Paused — bots are not keeping their own prompt"
660
- : "It writes this itself when a job ends"}
661
- {bot.noteAt && (
662
- <>
663
- <span className="px-1.5 opacity-50">·</span>
664
- {shortAgo(bot.noteAt)}
665
- </>
666
- )}
667
- </span>
668
- <Button
669
- size="sm"
670
- variant="ghost"
671
- loading={clearingNote}
672
- onClick={confirmClearNote}
673
- className="shrink-0 text-muted-foreground hover:text-destructive"
674
- >
675
- Clear
676
- </Button>
677
- </div>
678
- </div>
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
+ </>
679
803
  )}
804
+ </div>
680
805
 
806
+ <div className="flex-1 space-y-5 p-6">
681
807
  <MarkPicker
808
+ name={name}
682
809
  icon={icon}
683
810
  onChange={(next) => {
684
811
  patch({ icon: next });
@@ -686,18 +813,100 @@ function BotPage({
686
813
  }}
687
814
  />
688
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
+
689
856
  <Row label="Runs on">
690
857
  <ModelPicker
691
858
  provider={provider}
692
859
  model={model}
693
860
  onChange={(next) => {
694
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);
695
864
  // A provider without a model is not a model; save once both are picked.
696
- if (next.model) commit(next);
865
+ if (next.model) {
866
+ commit({
867
+ ...next,
868
+ compactAt: filledTokens(windowOf(next.provider, next.model)),
869
+ });
870
+ }
697
871
  }}
698
872
  />
699
873
  </Row>
700
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
+
701
910
  <Row label="Tools">
702
911
  <ToolPicker
703
912
  selected={toolIds}
@@ -730,6 +939,8 @@ function BotPage({
730
939
  )}
731
940
  </Row>
732
941
 
942
+ {bot && <Memory bot={bot.name} />}
943
+
733
944
  {bot && <Recent jobs={jobs} />}
734
945
 
735
946
  {!bot && (
@@ -751,14 +962,164 @@ function BotPage({
751
962
  );
752
963
  }
753
964
 
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) {
1088
+ return (
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>
1093
+ );
1094
+ }
1095
+ const kind = viewKindOf(path);
1096
+ return (
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
+
754
1113
  /**
755
1114
  * What the roster's pick is, and the one setting that is the whole set's rather
756
- * than any bot's: whether bots keep their own instructions at all.
1115
+ * than any bot's: whether bots keep their own memory at all.
757
1116
  */
758
1117
  function BotRail({ bot }: { bot: Bot | null }) {
759
- const { data: notesOn, mutate } = useServerRoute<boolean>(queryKey.botNotes);
760
- const [setNotesOn] = useServerAction(setBotNotesOnAction, {
761
- onOk: () => revalidate(queryKey.botNotes),
1118
+ const { data: memoryOn, mutate } = useServerRoute<boolean>(
1119
+ queryKey.botMemory,
1120
+ );
1121
+ const [setMemoryOn] = useServerAction(setBotMemoryOnAction, {
1122
+ onOk: () => revalidate(queryKey.botMemory),
762
1123
  });
763
1124
 
764
1125
  const tokens = bot ? bot.tokens.input + bot.tokens.output : 0;
@@ -768,6 +1129,11 @@ function BotRail({ bot }: { bot: Bot | null }) {
768
1129
  {bot ? (
769
1130
  <span className="font-mono text-[11px]">
770
1131
  {bot.name}
1132
+ {bot.disabled && (
1133
+ <>
1134
+ <span className="px-1.5 opacity-50">·</span>off
1135
+ </>
1136
+ )}
771
1137
  <span className="px-1.5 opacity-50">·</span>
772
1138
  <span
773
1139
  title={`in ${formatCount(bot.tokens.input)} · out ${formatCount(bot.tokens.output)}`}
@@ -782,21 +1148,48 @@ function BotRail({ bot }: { bot: Bot | null }) {
782
1148
  )}
783
1149
  </SettingRailNote>
784
1150
  <span className="shrink-0 text-xs text-muted-foreground">
785
- Bots keep their own prompt
1151
+ Bots keep their own memory
786
1152
  </span>
787
1153
  <Switch
788
- checked={notesOn ?? true}
789
- disabled={notesOn === undefined}
1154
+ checked={memoryOn ?? true}
1155
+ disabled={memoryOn === undefined}
790
1156
  onCheckedChange={(on) => {
791
1157
  void mutate(on, false);
792
- setNotesOn(on);
1158
+ setMemoryOn(on);
793
1159
  }}
794
- aria-label="Bots keep their own prompt"
1160
+ aria-label="Bots keep their own memory"
795
1161
  />
796
1162
  </>
797
1163
  );
798
1164
  }
799
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
+
800
1193
  function Row({ label, children }: { label: string; children: ReactNode }) {
801
1194
  return (
802
1195
  <div className="flex gap-3">
@@ -837,11 +1230,18 @@ function Recent({ jobs }: { jobs: Task[] }) {
837
1230
  className="flex h-9 items-center gap-3 border-t border-border/60 text-[13px]"
838
1231
  >
839
1232
  <span className="min-w-0 flex-1 truncate">{job.label}</span>
840
- <span
841
- className={cn("shrink-0 font-mono text-[11px]", state.tone)}
842
- >
843
- {state.text}
844
- </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
+ )}
845
1245
  <span className="w-28 shrink-0 text-right font-mono text-[11px] text-muted-foreground tabular-nums">
846
1246
  {whenOf(job.updatedAt)}
847
1247
  </span>
@@ -856,12 +1256,7 @@ function Recent({ jobs }: { jobs: Task[] }) {
856
1256
  function jobState(job: Task): { text: string; tone: string } {
857
1257
  switch (job.status) {
858
1258
  case "waiting":
859
- return isBudgetAsk(job.ask)
860
- ? { text: "out of steps", tone: "text-muted-foreground" }
861
- : {
862
- text: "waiting on you",
863
- tone: WAITING_INK,
864
- };
1259
+ return { text: "waiting on you", tone: WAITING_INK };
865
1260
  case "running":
866
1261
  return { text: "working", tone: "text-muted-foreground" };
867
1262
  case "failed":
@@ -871,81 +1266,74 @@ function jobState(job: Task): { text: string; tone: string } {
871
1266
  }
872
1267
  }
873
1268
 
874
- /** 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
+ */
875
1274
  function MarkPicker({
1275
+ name,
876
1276
  icon,
877
1277
  onChange,
878
1278
  }: {
1279
+ name: string;
879
1280
  icon: BotIcon;
880
1281
  onChange: (icon: BotIcon) => void;
881
1282
  }) {
882
1283
  return (
883
- <>
884
- <Row label="Color">
885
- <div className="flex flex-wrap items-center gap-1.5">
886
- {/* MARK_SYSTEM is an explicit "follow the theme ink", distinct from no colour. */}
887
- <Swatch
888
- color={null}
889
- picked={icon.color === MARK_SYSTEM}
890
- onPick={() => onChange({ ...icon, color: MARK_SYSTEM })}
891
- />
892
- {MARK_PALETTE.map((color) => (
893
- <Swatch
894
- key={color}
895
- color={color}
896
- picked={icon.color === color}
897
- onPick={() =>
898
- onChange({
899
- ...icon,
900
- color: icon.color === color ? undefined : color,
901
- })
902
- }
903
- />
904
- ))}
905
- </div>
906
- </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
+ />
907
1297
 
908
1298
  {/* Every chip is a toggle: unpicked means "whatever the name seeds", not a value */}
909
- <Row label="Face">
910
- <div className="flex gap-1">
911
- {MARK_SHAPES.map((shape) => (
912
- <button
913
- key={shape}
914
- type="button"
915
- onClick={() =>
916
- onChange({
917
- ...icon,
918
- shape: icon.shape === shape ? undefined : shape,
919
- })
920
- }
921
- className={cn(
922
- "rounded-md px-2 py-1 font-mono text-[11px] capitalize transition-colors",
923
- icon.shape === shape
924
- ? "bg-foreground text-background"
925
- : "bg-muted text-muted-foreground hover:text-foreground",
926
- )}
927
- >
928
- {shape}
929
- </button>
930
- ))}
931
- <span className="mx-1 w-px self-stretch bg-border/60" />
1299
+ <div className="flex gap-1">
1300
+ {MARK_SHAPES.map((shape) => (
932
1301
  <button
1302
+ key={shape}
933
1303
  type="button"
934
1304
  onClick={() =>
935
- onChange({ ...icon, outline: icon.outline ? undefined : true })
1305
+ onChange({
1306
+ ...icon,
1307
+ shape: icon.shape === shape ? undefined : shape,
1308
+ })
936
1309
  }
937
1310
  className={cn(
938
- "rounded-md px-2 py-1 font-mono text-[11px] transition-colors",
939
- icon.outline
1311
+ "rounded-md px-2 py-1 font-mono text-[11px] capitalize transition-colors",
1312
+ icon.shape === shape
940
1313
  ? "bg-foreground text-background"
941
1314
  : "bg-muted text-muted-foreground hover:text-foreground",
942
1315
  )}
943
1316
  >
944
- outline
1317
+ {shape}
945
1318
  </button>
946
- </div>
947
- </Row>
948
- </>
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>
949
1337
  );
950
1338
  }
951
1339