thursday-agent 0.1.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (358) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/app-path-routes-manifest.json +1 -0
  3. package/.next/build-manifest.json +3 -3
  4. package/.next/prerender-manifest.json +3 -3
  5. package/.next/routes-manifest.json +6 -0
  6. package/.next/server/app/_global-error/page.js.nft.json +1 -1
  7. package/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  8. package/.next/server/app/_global-error.html +1 -1
  9. package/.next/server/app/_global-error.rsc +1 -1
  10. package/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  11. package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  12. package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  13. package/.next/server/app/_not-found/page.js.nft.json +1 -1
  14. package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  15. package/.next/server/app/_not-found.html +1 -1
  16. package/.next/server/app/_not-found.rsc +13 -13
  17. package/.next/server/app/_not-found.segments/_full.segment.rsc +13 -13
  18. package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +12 -12
  19. package/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  20. package/.next/server/app/api/artifact/route.js +1 -1
  21. package/.next/server/app/api/artifact/route.js.nft.json +1 -1
  22. package/.next/server/app/api/bot/notes/route/app-paths-manifest.json +3 -0
  23. package/.next/server/app/api/bot/notes/route/build-manifest.json +12 -0
  24. package/.next/server/app/api/bot/notes/route/server-reference-manifest.json +4 -0
  25. package/.next/server/app/api/bot/notes/route.js +14 -0
  26. package/.next/server/app/api/bot/notes/route.js.map +5 -0
  27. package/.next/server/app/api/bot/notes/route.js.nft.json +1 -0
  28. package/.next/server/app/api/bot/notes/route_client-reference-manifest.js +3 -0
  29. package/.next/server/app/api/bot/route.js +4 -4
  30. package/.next/server/app/api/bot/route.js.nft.json +1 -1
  31. package/.next/server/app/api/bot/task/[id]/route.js +6 -6
  32. package/.next/server/app/api/bot/task/[id]/route.js.nft.json +1 -1
  33. package/.next/server/app/api/bot/task/route.js +6 -6
  34. package/.next/server/app/api/bot/task/route.js.nft.json +1 -1
  35. package/.next/server/app/api/config/route.js +3 -3
  36. package/.next/server/app/api/config/route.js.nft.json +1 -1
  37. package/.next/server/app/api/events/route.js +1 -1
  38. package/.next/server/app/api/events/route.js.nft.json +1 -1
  39. package/.next/server/app/api/file/[...path]/route.js +1 -1
  40. package/.next/server/app/api/file/[...path]/route.js.nft.json +1 -1
  41. package/.next/server/app/api/llm-model/catalog/route.js +6 -6
  42. package/.next/server/app/api/llm-model/catalog/route.js.nft.json +1 -1
  43. package/.next/server/app/api/llm-model/route.js +3 -3
  44. package/.next/server/app/api/llm-model/route.js.nft.json +1 -1
  45. package/.next/server/app/api/mcp/[name]/route.js +3 -3
  46. package/.next/server/app/api/mcp/[name]/route.js.nft.json +1 -1
  47. package/.next/server/app/api/mcp/oauth/callback/route.js +5 -5
  48. package/.next/server/app/api/mcp/oauth/callback/route.js.nft.json +1 -1
  49. package/.next/server/app/api/mcp/route.js +3 -3
  50. package/.next/server/app/api/mcp/route.js.nft.json +1 -1
  51. package/.next/server/app/api/mcp/tools/route.js +3 -3
  52. package/.next/server/app/api/mcp/tools/route.js.nft.json +1 -1
  53. package/.next/server/app/api/memory/route.js +7 -7
  54. package/.next/server/app/api/memory/route.js.nft.json +1 -1
  55. package/.next/server/app/api/memory/tidy/route.js +10 -11
  56. package/.next/server/app/api/memory/tidy/route.js.nft.json +1 -1
  57. package/.next/server/app/api/skills/[source]/[name]/route.js +1 -1
  58. package/.next/server/app/api/skills/[source]/[name]/route.js.nft.json +1 -1
  59. package/.next/server/app/api/skills/route.js +1 -1
  60. package/.next/server/app/api/skills/route.js.nft.json +1 -1
  61. package/.next/server/app/api/thursday/call/route.js +8 -8
  62. package/.next/server/app/api/thursday/call/route.js.nft.json +1 -1
  63. package/.next/server/app/api/thursday/skills/route.js +8 -8
  64. package/.next/server/app/api/thursday/skills/route.js.nft.json +1 -1
  65. package/.next/server/app/api/thursday/tool-call/route.js +11 -12
  66. package/.next/server/app/api/thursday/tool-call/route.js.nft.json +1 -1
  67. package/.next/server/app/api/workspace/route.js +1 -1
  68. package/.next/server/app/api/workspace/route.js.nft.json +1 -1
  69. package/.next/server/app/artifact/[...path]/page/server-reference-manifest.json +1 -1
  70. package/.next/server/app/artifact/[...path]/page.js +3 -3
  71. package/.next/server/app/artifact/[...path]/page.js.nft.json +1 -1
  72. package/.next/server/app/artifact/[...path]/page_client-reference-manifest.js +1 -1
  73. package/.next/server/app/favicon.ico/route.js.nft.json +1 -1
  74. package/.next/server/app/page/server-reference-manifest.json +63 -41
  75. package/.next/server/app/page.js +7 -7
  76. package/.next/server/app/page.js.nft.json +1 -1
  77. package/.next/server/app/page_client-reference-manifest.js +1 -1
  78. package/.next/server/app-paths-manifest.json +1 -0
  79. package/.next/server/chunks/0d-b_next_dist_server_app-render_after-task-async-storage_external_0-exn7p.js +3 -0
  80. package/.next/server/chunks/{[root-of-the-server]__0t_dqaj._.js → [root-of-the-server]__0-npkh_._.js} +1 -1
  81. package/.next/server/chunks/[root-of-the-server]__005var5._.js +3 -0
  82. package/.next/server/chunks/[root-of-the-server]__019dx9f._.js +2 -2
  83. package/.next/server/chunks/[root-of-the-server]__02va5wz._.js +3 -0
  84. package/.next/server/chunks/{[root-of-the-server]__1mzp_el._.js → [root-of-the-server]__04_8spk._.js} +2 -2
  85. package/.next/server/chunks/[root-of-the-server]__06t77t4._.js +1 -1
  86. package/.next/server/chunks/[root-of-the-server]__0bed6hu._.js +4 -4
  87. package/.next/server/chunks/[root-of-the-server]__0cro2dz._.js +5 -0
  88. package/.next/server/chunks/[root-of-the-server]__0emr6go._.js +3 -0
  89. package/.next/server/chunks/[root-of-the-server]__0le4d-p._.js +1 -1
  90. package/.next/server/chunks/[root-of-the-server]__0m8ywyo._.js +5 -5
  91. package/.next/server/chunks/{[root-of-the-server]__113fqg5._.js → [root-of-the-server]__0p32jfz._.js} +3 -3
  92. package/.next/server/chunks/[root-of-the-server]__0phdgco._.js +4 -4
  93. package/.next/server/chunks/[root-of-the-server]__0wvgrsl._.js +1 -1
  94. package/.next/server/chunks/[root-of-the-server]__11j52t0._.js +5 -0
  95. package/.next/server/chunks/{[root-of-the-server]__0j_n5l3._.js → [root-of-the-server]__1692zrv._.js} +2 -2
  96. package/.next/server/chunks/[root-of-the-server]__16thabu._.js +1 -1
  97. package/.next/server/chunks/[root-of-the-server]__173nhwf._.js +3 -3
  98. package/.next/server/chunks/[root-of-the-server]__1bn73ih._.js +10 -0
  99. package/.next/server/chunks/[root-of-the-server]__1bqhuh9._.js +4 -4
  100. package/.next/server/chunks/[root-of-the-server]__1ccon1w._.js +10 -0
  101. package/.next/server/chunks/[root-of-the-server]__1cqizfq._.js +1 -1
  102. package/.next/server/chunks/{[root-of-the-server]__1u_qvc7._.js → [root-of-the-server]__1fboj2j._.js} +2 -2
  103. package/.next/server/chunks/{[root-of-the-server]__1dow0bz._.js → [root-of-the-server]__1oli73c._.js} +2 -2
  104. package/.next/server/chunks/[root-of-the-server]__1txedx6._.js +4 -4
  105. package/.next/server/chunks/[root-of-the-server]__1untqv8._.js +5 -0
  106. package/.next/server/chunks/_0-73of8._.js +55 -0
  107. package/.next/server/chunks/{_2050gwn._.js → _00_584r._.js} +66 -28
  108. package/.next/server/chunks/{_0y8is-s._.js → _03a0897._.js} +1 -1
  109. package/.next/server/chunks/_05h2cii._.js +1 -1
  110. package/.next/server/chunks/_06mxlgn._.js +1 -1
  111. package/.next/server/chunks/_0a351g7._.js +1 -1
  112. package/.next/server/chunks/{_0hin38_._.js → _0khihjk._.js} +5 -5
  113. package/.next/server/chunks/_0kjjmag._.js +1 -1
  114. package/.next/server/chunks/{_1t8fv7b._.js → _0n74zsf._.js} +5 -5
  115. package/.next/server/chunks/_0nn99o2._.js +3 -0
  116. package/.next/server/chunks/_0pppbb5._.js +10 -6
  117. package/.next/server/chunks/{_1icgbi8._.js → _0rnf-6n._.js} +5 -5
  118. package/.next/server/chunks/_0u10wmw._.js +55 -0
  119. package/.next/server/chunks/{_0kl37zl._.js → _0vnutv9._.js} +1 -1
  120. package/.next/server/chunks/_0wp4le3._.js +1 -1
  121. package/.next/server/chunks/_0y5fg-x._.js +1 -1
  122. package/.next/server/chunks/_106co2o._.js +3 -0
  123. package/.next/server/chunks/_12bbv5i._.js +10 -6
  124. package/.next/server/chunks/_15hq89o._.js +1 -1
  125. package/.next/server/chunks/_15q1pig._.js +1 -1
  126. package/.next/server/chunks/_18-1wcc._.js +55 -0
  127. package/.next/server/chunks/_19wg5yn._.js +138 -0
  128. package/.next/server/chunks/_1_5mnq4._.js +18 -0
  129. package/.next/server/chunks/_1bt95p9._.js +55 -0
  130. package/.next/server/chunks/_1cbk7ms._.js +9 -9
  131. package/.next/server/chunks/_1drgfx4._.js +18 -0
  132. package/.next/server/chunks/_1drv2et._.js +10 -6
  133. package/.next/server/chunks/{_10qam6o._.js → _1e48czp._.js} +5 -5
  134. package/.next/server/chunks/{_1skgy7b._.js → _1kg70wc._.js} +5 -5
  135. package/.next/server/chunks/_1php4eq._.js +1 -1
  136. package/.next/server/chunks/_1rysr-r._.js +10 -0
  137. package/.next/server/chunks/_1u3rwbr._.js +3 -0
  138. package/.next/server/chunks/_1x7p319._.js +1 -1
  139. package/.next/server/chunks/_1yuv4vp._.js +1 -1
  140. package/.next/server/chunks/_next-internal_server_app_api_bot_notes_route_actions_0qcsyu7.js +3 -0
  141. package/.next/server/chunks/config_ts_1g4sz5o._.js +1 -1
  142. package/.next/server/chunks/features_bot_task_query_ts_11xuzl3._.js +1 -1
  143. package/.next/server/chunks/{node_modules_consola_dist_chunks_prompt_mjs_0228f43._.js → node_modules_consola_dist_chunks_prompt_mjs_02s3hjt._.js} +1 -1
  144. package/.next/server/chunks/{node_modules_consola_dist_chunks_prompt_mjs_0ir-vo2._.js → node_modules_consola_dist_chunks_prompt_mjs_06067w6._.js} +1 -1
  145. package/.next/server/chunks/{node_modules_consola_dist_chunks_prompt_mjs_0otw8ef._.js → node_modules_consola_dist_chunks_prompt_mjs_096ais_._.js} +1 -1
  146. package/.next/server/chunks/{node_modules_consola_dist_chunks_prompt_mjs_0tf290y._.js → node_modules_consola_dist_chunks_prompt_mjs_0a1rd09._.js} +1 -1
  147. package/.next/server/chunks/node_modules_consola_dist_chunks_prompt_mjs_0gzvgfv._.js +3 -0
  148. package/.next/server/chunks/node_modules_consola_dist_chunks_prompt_mjs_0j61-ma._.js +3 -0
  149. package/.next/server/chunks/node_modules_consola_dist_chunks_prompt_mjs_0tvkfgh._.js +3 -0
  150. package/.next/server/chunks/node_modules_consola_dist_chunks_prompt_mjs_11zjo-1._.js +3 -0
  151. package/.next/server/chunks/node_modules_consola_dist_chunks_prompt_mjs_1pycqsq._.js +3 -0
  152. package/.next/server/chunks/node_modules_consola_dist_chunks_prompt_mjs_1tpvamf._.js +3 -0
  153. package/.next/server/chunks/node_modules_consola_dist_chunks_prompt_mjs_1y9-wcr._.js +3 -0
  154. package/.next/server/chunks/ssr/[externals]__0dizgek._.js +3 -0
  155. package/.next/server/chunks/ssr/[root-of-the-server]__0f6s48x._.js +1 -1
  156. package/.next/server/chunks/ssr/[root-of-the-server]__0m1b5ky._.js +3 -0
  157. package/.next/server/chunks/ssr/{[root-of-the-server]__0dfwvr4._.js → [root-of-the-server]__0un-erv._.js} +2 -2
  158. package/.next/server/chunks/ssr/[root-of-the-server]__1-mnhyd._.js +5 -3
  159. package/.next/server/chunks/ssr/[root-of-the-server]__1bww500._.js +3 -0
  160. package/.next/server/chunks/ssr/[root-of-the-server]__1mzi-ju._.js +1 -1
  161. package/.next/server/chunks/ssr/[root-of-the-server]__1qfqvmg._.js +3 -0
  162. package/.next/server/chunks/ssr/[root-of-the-server]__1vcdast._.js +3 -0
  163. package/.next/server/chunks/ssr/[root-of-the-server]__1xwnu-h._.js +3 -0
  164. package/.next/server/chunks/ssr/_03webdr._.js +3 -0
  165. package/.next/server/chunks/ssr/{_0gd4vaa._.js → _040sk32._.js} +71 -29
  166. package/.next/server/chunks/ssr/_077-g6s._.js +1 -1
  167. package/.next/server/chunks/ssr/{_0lc_7c8._.js → _09ja_w1._.js} +2 -2
  168. package/.next/server/chunks/ssr/{_1dcxzqe._.js → _0f_b_9s._.js} +2 -2
  169. package/.next/server/chunks/ssr/_0h7zdni._.js +1 -1
  170. package/.next/server/chunks/ssr/_0lu1n_x._.js +3 -0
  171. package/.next/server/chunks/ssr/_0sf1isi._.js +1 -1
  172. package/.next/server/chunks/ssr/_0su6_08._.js +3 -0
  173. package/.next/server/chunks/ssr/{_0nso8tp._.js → _0w5bgak._.js} +2 -2
  174. package/.next/server/chunks/ssr/{_19_o9d-._.js → _0x_nu5e._.js} +2 -2
  175. package/.next/server/chunks/ssr/_1-teee5._.js +3 -3
  176. package/.next/server/chunks/ssr/_13punpm._.js +1 -1
  177. package/.next/server/chunks/ssr/{_0ay6b01._.js → _1ciyoo3._.js} +2 -2
  178. package/.next/server/chunks/ssr/_1e-dgz0._.js +1 -1
  179. package/.next/server/chunks/ssr/_1fq7zgb._.js +1 -1
  180. package/.next/server/chunks/ssr/{_0cvjbuz._.js → _1g6l4uj._.js} +2 -2
  181. package/.next/server/chunks/ssr/{_0q_2f2x._.js → _1lip4th._.js} +2 -2
  182. package/.next/server/chunks/ssr/_1m3bsfr._.js +3 -0
  183. package/.next/server/chunks/ssr/_1mznrxk._.js +1 -1
  184. package/.next/server/chunks/ssr/_1ruzehw._.js +3 -0
  185. package/.next/server/chunks/ssr/_1vyd1ji._.js +3 -0
  186. package/.next/server/chunks/ssr/features_bot_components_bot-setting_tsx_01ajft6._.js +2 -2
  187. package/.next/server/chunks/ssr/features_config_components_14jqgy4._.js +1 -1
  188. package/.next/server/chunks/ssr/features_config_components_config-setting_tsx_0xfwqdg._.js +1 -1
  189. package/.next/server/chunks/ssr/features_connectors_components_mcp-setting_tsx_0jmbgvp._.js +1 -1
  190. package/.next/server/chunks/ssr/features_memory_components_memory-setting_tsx_0oa43sw._.js +1 -1
  191. package/.next/server/chunks/ssr/lib_realtime_12wg_dd._.js +27 -0
  192. package/.next/server/chunks/ssr/lib_realtime_15h_v1v._.js +27 -0
  193. package/.next/server/chunks/ssr/{node_modules_108wd2o._.js → node_modules_004qlr4._.js} +1 -1
  194. package/.next/server/chunks/ssr/{node_modules_1ub40gk._.js → node_modules_01qnv-a._.js} +1 -1
  195. package/.next/server/chunks/ssr/{node_modules_1y8_3vg._.js → node_modules_04n7mjs._.js} +1 -1
  196. package/.next/server/chunks/ssr/{node_modules_0_j7-g4._.js → node_modules_08d8bke._.js} +1 -1
  197. package/.next/server/chunks/ssr/{node_modules_0y2-av4._.js → node_modules_09xhur7._.js} +1 -1
  198. package/.next/server/chunks/ssr/{node_modules_043-236._.js → node_modules_0_n241r._.js} +1 -1
  199. package/.next/server/chunks/ssr/{node_modules_0ls96je._.js → node_modules_0iz1rzd._.js} +1 -1
  200. package/.next/server/chunks/ssr/{node_modules_0xwx4u-._.js → node_modules_0kl7-tn._.js} +1 -1
  201. package/.next/server/chunks/ssr/{node_modules_1jatz7w._.js → node_modules_0kxz4gt._.js} +1 -1
  202. package/.next/server/chunks/ssr/{node_modules_1t0y5jt._.js → node_modules_0lrjk5t._.js} +1 -1
  203. package/.next/server/chunks/ssr/{node_modules_13f2md6._.js → node_modules_0m93zeg._.js} +1 -1
  204. package/.next/server/chunks/ssr/{node_modules_1nhw9qg._.js → node_modules_0n1kj4p._.js} +1 -1
  205. package/.next/server/chunks/ssr/{node_modules_1k29j1j._.js → node_modules_0ndvxub._.js} +1 -1
  206. package/.next/server/chunks/ssr/{node_modules_16i4o_3._.js → node_modules_13r44jq._.js} +1 -1
  207. package/.next/server/chunks/ssr/{node_modules_0fbw5-u._.js → node_modules_17lrt3y._.js} +1 -1
  208. package/.next/server/chunks/ssr/{node_modules_1d-is1o._.js → node_modules_18_dk7_._.js} +1 -1
  209. package/.next/server/chunks/ssr/{node_modules_1hn_sd5._.js → node_modules_19dp7ko._.js} +1 -1
  210. package/.next/server/chunks/ssr/{node_modules_20ynonm._.js → node_modules_1dwguw_._.js} +1 -1
  211. package/.next/server/chunks/ssr/{node_modules_00d6675._.js → node_modules_1hd-5tv._.js} +1 -1
  212. package/.next/server/chunks/ssr/{node_modules_1ee8gav._.js → node_modules_1hn8z1f._.js} +1 -1
  213. package/.next/server/chunks/ssr/{node_modules_0bue6gy._.js → node_modules_1n5ygqe._.js} +1 -1
  214. package/.next/server/chunks/ssr/{node_modules_1_588_4._.js → node_modules_1q0p85z._.js} +1 -1
  215. package/.next/server/chunks/ssr/{node_modules_0ipn1ho._.js → node_modules_1toafc-._.js} +1 -1
  216. package/.next/server/chunks/ssr/{node_modules_0_fvxnt._.js → node_modules_1xyd7ov._.js} +1 -1
  217. package/.next/server/chunks/ssr/{node_modules_1v0077l._.js → node_modules_1zu988w._.js} +1 -1
  218. package/.next/server/chunks/ssr/{node_modules_06kvvuo._.js → node_modules_20yebtj._.js} +1 -1
  219. package/.next/server/chunks/ssr/{node_modules_@mermaid-js_parser_dist_chunks_mermaid-parser_core_092s8nx._.js → node_modules_@mermaid-js_parser_dist_chunks_mermaid-parser_core_0m9a4y0._.js} +1 -1
  220. package/.next/server/chunks/ssr/{node_modules_@mermaid-js_parser_dist_chunks_mermaid-parser_core_1nvexgu._.js → node_modules_@mermaid-js_parser_dist_chunks_mermaid-parser_core_13w_wy9._.js} +1 -1
  221. package/.next/server/chunks/ssr/node_modules_consola_dist_chunks_prompt_mjs_05cwqv2._.js +47 -0
  222. package/.next/server/middleware-build-manifest.js +3 -3
  223. package/.next/server/pages/404.html +1 -1
  224. package/.next/server/pages/500.html +1 -1
  225. package/.next/server/server-reference-manifest.js +1 -1
  226. package/.next/server/server-reference-manifest.json +64 -42
  227. package/.next/static/chunks/05b6nx_o3_f_n.js +1 -0
  228. package/.next/static/chunks/{3aquirz5h71da.js → 07kl352c-lvcz.js} +1 -1
  229. package/.next/static/chunks/0dmns-gi0raa4.js +1 -0
  230. package/.next/static/chunks/0oc4_xoia77dg.css +1 -0
  231. package/.next/static/chunks/0pheo8m3a-rzi.js +1 -0
  232. package/.next/static/chunks/{2ihkf6l2jowo9.js → 18vhba44l1fc9.js} +3 -3
  233. package/.next/static/chunks/19i8h2t-5evjx.js +1 -0
  234. package/.next/static/chunks/{1gy3bbv7usnjo.js → 1c5fcj473025q.js} +1 -1
  235. package/.next/static/chunks/1pi_hbmjw-f-z.js +1 -0
  236. package/.next/static/chunks/{0tydnn2kybibk.js → 1twt6k09vg4np.js} +1 -1
  237. package/.next/static/chunks/{21rk8w7bda-su.js → 28gotuisldmom.js} +1 -1
  238. package/.next/static/chunks/{3xxa2j56-f3nm.js → 2998p0y7k1c8z.js} +1 -1
  239. package/.next/static/chunks/2l233xfycjpd8.js +1 -0
  240. package/.next/static/chunks/2p5n7pa3c40t-.js +2 -0
  241. package/.next/static/chunks/2ru1q132af7h7.js +1 -0
  242. package/.next/static/chunks/{2fq_86jvq9ymq.js → 2t8wqycwg3y04.js} +2 -2
  243. package/.next/static/chunks/32z_ypgtchw48.js +1 -0
  244. package/.next/static/chunks/{247d-r95j8xs0.js → 33pyjb3xd1iez.js} +1 -1
  245. package/.next/static/chunks/3786h5vayt80-.js +28 -0
  246. package/.next/static/chunks/3_h9txt3tq8t8.js +28 -0
  247. package/.next/static/chunks/{1khakdu_iuedc.js → 3ard_noqapnw1.js} +1 -1
  248. package/.next/static/chunks/{1fr5m75pwtyic.js → 3omud5xunrk1f.js} +1 -1
  249. package/.next/static/chunks/42dnrkbph9e-1.js +1 -0
  250. package/app/api/events/app-event.client.ts +24 -0
  251. package/app/api/events/app-event.server.ts +45 -0
  252. package/app/api/events/app-event.ts +44 -0
  253. package/app/api/llm-model/catalog/route.ts +12 -0
  254. package/app/api/llm-model/route.ts +21 -0
  255. package/app/api/query-key.ts +144 -0
  256. package/app/api/thursday/tool-call/route.ts +67 -0
  257. package/components/ui/button-group.tsx +45 -0
  258. package/components/ui/custom-icon.tsx +259 -0
  259. package/components/ui/dropdown-menu.tsx +271 -0
  260. package/components/ui/folded-text.tsx +68 -0
  261. package/components/ui/input-group.tsx +157 -0
  262. package/components/ui/json-view.tsx +240 -0
  263. package/components/ui/shiny-text.tsx +141 -0
  264. package/components/ui/text-type.tsx +213 -0
  265. package/components/ui/theme-sync.tsx +9 -0
  266. package/database/migrations/{20260908030742_rainy_marvel_zombies → 20260909060813_optimal_sabretooth}/migration.sql +7 -0
  267. package/database/migrations/{20260908030742_rainy_marvel_zombies → 20260909060813_optimal_sabretooth}/snapshot.json +54 -1
  268. package/database/migrations/20260909063821_bitter_tenebrous/migration.sql +1 -0
  269. package/database/migrations/20260909063821_bitter_tenebrous/snapshot.json +1285 -0
  270. package/docs/how-it-works.md +59 -0
  271. package/docs/images/keeps-going.png +0 -0
  272. package/docs/images/task-thread.png +0 -0
  273. package/docs/images/two-minds.png +0 -0
  274. package/features/ai/load-tools.ts +55 -5
  275. package/features/ai/prompts/bot.prompt.ts +121 -26
  276. package/features/ai/prompts/memory-tidy.prompt.ts +7 -1
  277. package/features/ai/prompts/prompt-helper.ts +31 -5
  278. package/features/ai/prompts/thursday.prompt.ts +2 -0
  279. package/features/ai/tools/bot.tool.ts +37 -18
  280. package/features/ai/tools/mcp.tool.ts +5 -5
  281. package/features/ai/tools/memory.tool.ts +17 -10
  282. package/features/ai/tools/studio.tool.ts +5 -5
  283. package/features/ai/tools/tool-name.ts +2 -0
  284. package/features/ai/tools/workspace.tool.ts +33 -5
  285. package/features/bot/bot.action.ts +31 -4
  286. package/features/bot/bot.notes.ts +120 -0
  287. package/features/bot/bot.query.ts +56 -1
  288. package/features/bot/bot.run.ts +43 -3
  289. package/features/bot/bot.runner.ts +94 -20
  290. package/features/bot/bot.schema.ts +17 -0
  291. package/features/bot/components/bot-room.tsx +405 -306
  292. package/features/bot/components/bot-setting.tsx +123 -20
  293. package/features/bot/task.query.ts +18 -5
  294. package/features/memory/components/memory-setting.tsx +11 -2
  295. package/features/memory/memory.query.ts +9 -2
  296. package/features/memory/memory.schema.ts +30 -0
  297. package/features/skills/components/skills-setting.tsx +9 -1
  298. package/features/workspace/workspace.ts +136 -3
  299. package/hooks/use-draft.ts +37 -0
  300. package/hooks/use-hotkey.ts +105 -0
  301. package/hooks/use-object-state.ts +51 -0
  302. package/hooks/use-on-visible.ts +72 -0
  303. package/hooks/use-theme.ts +106 -0
  304. package/hooks/use-wake-word.ts +262 -0
  305. package/lib/realtime/realtime.driver.ts +34 -1
  306. package/lib/realtime/realtime.protocol.ts +6 -0
  307. package/lib/sandbox.ts +9 -1
  308. package/lib/tokens.ts +19 -0
  309. package/package.json +1 -1
  310. package/.next/server/chunks/[root-of-the-server]__02kt8yn._.js +0 -10
  311. package/.next/server/chunks/[root-of-the-server]__0h59di-._.js +0 -5
  312. package/.next/server/chunks/[root-of-the-server]__0qjqtwo._.js +0 -5
  313. package/.next/server/chunks/[root-of-the-server]__0yk6s3w._.js +0 -5
  314. package/.next/server/chunks/[root-of-the-server]__0z3gelv._.js +0 -3
  315. package/.next/server/chunks/[root-of-the-server]__11w_7_3._.js +0 -3
  316. package/.next/server/chunks/[root-of-the-server]__1_0q_9w._.js +0 -3
  317. package/.next/server/chunks/[root-of-the-server]__1gmn1cs._.js +0 -10
  318. package/.next/server/chunks/_0fzqt2y._.js +0 -100
  319. package/.next/server/chunks/_0jgm3b8._.js +0 -55
  320. package/.next/server/chunks/_0ks6_sg._.js +0 -55
  321. package/.next/server/chunks/_0tzfesa._.js +0 -3
  322. package/.next/server/chunks/_0zmestz._.js +0 -55
  323. package/.next/server/chunks/_12nr4xc._.js +0 -3
  324. package/.next/server/chunks/_1hzrsvg._.js +0 -55
  325. package/.next/server/chunks/_1uj4_io._.js +0 -10
  326. package/.next/server/chunks/_1yldgo4._.js +0 -55
  327. package/.next/server/chunks/features_0scmr4i._.js +0 -3
  328. package/.next/server/chunks/features_bot_bot_query_ts_12mgkl2._.js +0 -3
  329. package/.next/server/chunks/node_modules_consola_dist_chunks_prompt_mjs_0ykklj0._.js +0 -3
  330. package/.next/server/chunks/node_modules_consola_dist_chunks_prompt_mjs_1ryg3wh._.js +0 -3
  331. package/.next/server/chunks/node_modules_consola_dist_chunks_prompt_mjs_1vwbnq_._.js +0 -3
  332. package/.next/server/chunks/ssr/[root-of-the-server]__01id-v3._.js +0 -3
  333. package/.next/server/chunks/ssr/[root-of-the-server]__0b-0i32._.js +0 -3
  334. package/.next/server/chunks/ssr/[root-of-the-server]__185grso._.js +0 -3
  335. package/.next/server/chunks/ssr/[root-of-the-server]__1minz8_._.js +0 -3
  336. package/.next/server/chunks/ssr/_0j4v8of._.js +0 -3
  337. package/.next/server/chunks/ssr/_0l81bzy._.js +0 -3
  338. package/.next/server/chunks/ssr/_10uvw3l._.js +0 -3
  339. package/.next/server/chunks/ssr/_1jbv7m1._.js +0 -3
  340. package/.next/server/chunks/ssr/lib_realtime_0aetuyp._.js +0 -24
  341. package/.next/server/chunks/ssr/lib_realtime_18k_aj-._.js +0 -24
  342. package/.next/server/chunks/ssr/node_modules_zod_v4_classic_errors_05xv7vp.js +0 -3
  343. package/.next/static/chunks/0yysv1gzin17t.js +0 -1
  344. package/.next/static/chunks/1keib_gt066gc.js +0 -1
  345. package/.next/static/chunks/28g42tw8tsvhd.js +0 -1
  346. package/.next/static/chunks/2cwjgkt_hoyhj.js +0 -22
  347. package/.next/static/chunks/2rbf1fpd8n80-.js +0 -22
  348. package/.next/static/chunks/2ukj8k69y89um.js +0 -1
  349. package/.next/static/chunks/2wngeetfnzxkp.js +0 -2
  350. package/.next/static/chunks/388-t2v72jh39.css +0 -1
  351. package/.next/static/chunks/3fqvc0cg-atq_.js +0 -1
  352. package/.next/static/chunks/3p4egvfjxpatb.js +0 -1
  353. package/.next/static/chunks/3qe1w1ei-huhl.js +0 -1
  354. package/.next/static/chunks/3y40riycdip-i.js +0 -1
  355. package/.next/static/chunks/45a6zbtkelph9.js +0 -1
  356. /package/.next/static/{X34BUd9bfdH27ripuwwBm → YR4NWcDDuzwLbTTU7U7Vr}/_buildManifest.js +0 -0
  357. /package/.next/static/{X34BUd9bfdH27ripuwwBm → YR4NWcDDuzwLbTTU7U7Vr}/_clientMiddlewareManifest.js +0 -0
  358. /package/.next/static/{X34BUd9bfdH27ripuwwBm → YR4NWcDDuzwLbTTU7U7Vr}/_ssgManifest.js +0 -0
@@ -0,0 +1,105 @@
1
+ "use client";
2
+
3
+ import { useEffect, useRef, useState } from "react";
4
+
5
+ const HOLD = ["ctrl", "alt", "meta"] as const;
6
+
7
+ const IS_MODIFIER = /^(Control|Alt|Shift|Meta|OS)/;
8
+
9
+ const EDITABLE = /^(input|textarea|select)$/i;
10
+
11
+ export const HOTKEY_CAPTURE = { "data-hotkey-capture": "" } as const;
12
+
13
+ const busy = (target: EventTarget | null) => {
14
+ const node = target as HTMLElement | null;
15
+ if (!node) return false;
16
+ return Boolean(
17
+ EDITABLE.test(node.tagName) ||
18
+ node.isContentEditable ||
19
+ node.closest?.("[data-hotkey-capture]"),
20
+ );
21
+ };
22
+
23
+ export function comboOf(event: {
24
+ code: string;
25
+ ctrlKey: boolean;
26
+ altKey: boolean;
27
+ shiftKey: boolean;
28
+ metaKey: boolean;
29
+ }): string | null {
30
+ const { code, ctrlKey, altKey, shiftKey, metaKey } = event;
31
+ if (!code || IS_MODIFIER.test(code)) return null;
32
+ if (!ctrlKey && !altKey && !metaKey) return null;
33
+
34
+ const held: string[] = [];
35
+ if (ctrlKey) held.push("ctrl");
36
+ if (altKey) held.push("alt");
37
+ if (shiftKey) held.push("shift");
38
+ if (metaKey) held.push("meta");
39
+ return [...held, code].join("+");
40
+ }
41
+
42
+ export const isCombo = (combo: string) => {
43
+ const parts = combo.split("+");
44
+ const code = parts.at(-1);
45
+ return Boolean(
46
+ code &&
47
+ !IS_MODIFIER.test(code) &&
48
+ parts.slice(0, -1).some((part) => HOLD.includes(part as never)),
49
+ );
50
+ };
51
+
52
+ const SIGN: Record<string, [plain: string, mac: string]> = {
53
+ ctrl: ["Ctrl", "⌃"],
54
+ alt: ["Alt", "⌥"],
55
+ shift: ["Shift", "⇧"],
56
+ meta: ["Meta", "⌘"],
57
+ };
58
+
59
+ const keyLabel = (code: string) =>
60
+ code.replace(/^(Key|Digit|Numpad)/, "").replace(/([a-z])([A-Z])/g, "$1 $2") ||
61
+ code;
62
+
63
+ export function hotkeyLabel(combo: string, mac = false): string {
64
+ const parts = combo.split("+");
65
+ const code = parts.at(-1) ?? "";
66
+ const held = parts
67
+ .slice(0, -1)
68
+ .map((part) => SIGN[part]?.[mac ? 1 : 0] ?? part);
69
+ const key = keyLabel(code);
70
+ return mac ? [...held, key].join("") : [...held, key].join(" + ");
71
+ }
72
+
73
+ export function useHotkeyLabel(combo: string | null): string | null {
74
+ const [mac, setMac] = useState(false);
75
+ useEffect(() => setMac(/mac/i.test(navigator.userAgent)), []);
76
+ return combo ? hotkeyLabel(combo, mac) : null;
77
+ }
78
+
79
+ export function useHotkey({
80
+ enabled,
81
+ combo,
82
+ onPress,
83
+ }: {
84
+ enabled: boolean;
85
+ combo: string | null;
86
+ onPress: () => void;
87
+ }) {
88
+ const latest = useRef(onPress);
89
+ latest.current = onPress;
90
+
91
+ useEffect(() => {
92
+ if (!enabled || !combo) return;
93
+
94
+ const onKeyDown = (event: KeyboardEvent) => {
95
+ if (event.repeat || busy(event.target)) return;
96
+ if (comboOf(event) !== combo) return;
97
+
98
+ event.preventDefault();
99
+ latest.current();
100
+ };
101
+
102
+ window.addEventListener("keydown", onKeyDown);
103
+ return () => window.removeEventListener("keydown", onKeyDown);
104
+ }, [enabled, combo]);
105
+ }
@@ -0,0 +1,51 @@
1
+ import { useMemo, useState } from "react";
2
+ import { isFunction } from "../lib/utils";
3
+
4
+ type SetPartialStateAction<T> = Partial<T> | ((prev: T) => Partial<T>);
5
+
6
+ type Patch<T> = ((action: SetPartialStateAction<T>) => void) & {
7
+ [K in keyof T]-?: (value: T[K] | ((prev: T[K]) => T[K])) => void;
8
+ };
9
+
10
+ const hasChanged = <T extends object>(prev: T, next: Partial<T>) =>
11
+ (Object.keys(next) as (keyof T)[]).some(
12
+ (key) => !Object.is(prev[key], next[key]),
13
+ );
14
+
15
+ export const useObjectState = <T extends object>(initial: T | (() => T)) => {
16
+ const [state, setState] = useState<T>(initial);
17
+
18
+ const patch = useMemo(() => {
19
+ const apply = (action: SetPartialStateAction<T>) => {
20
+ setState((prev) => {
21
+ const delta = isFunction(action) ? action(prev) : action;
22
+ return hasChanged(prev, delta) ? { ...prev, ...delta } : prev;
23
+ });
24
+ };
25
+
26
+ // Cache per-key setters so their identity is stable in deps arrays.
27
+ const cache = new Map<string, (value: unknown) => void>();
28
+
29
+ return new Proxy(apply, {
30
+ get(target, p, receiver) {
31
+ if (typeof p === "symbol") return Reflect.get(target, p, receiver);
32
+
33
+ let setter = cache.get(p);
34
+ if (!setter) {
35
+ setter = (value) => {
36
+ apply(
37
+ (prev) =>
38
+ ({
39
+ [p]: isFunction(value) ? value(prev[p as keyof T]) : value,
40
+ }) as Partial<T>,
41
+ );
42
+ };
43
+ cache.set(p, setter);
44
+ }
45
+ return setter;
46
+ },
47
+ }) as Patch<T>;
48
+ }, []);
49
+
50
+ return [state, patch] as const;
51
+ };
@@ -0,0 +1,72 @@
1
+ "use client";
2
+
3
+ import { useCallback, useEffect, useRef } from "react";
4
+
5
+ /**
6
+ * Calls `onVisible` when the attached element scrolls into view (infinite
7
+ * lists). The callback lives in a ref: rebuilding the observer on an element
8
+ * already in view would fire it again.
9
+ */
10
+ export function useOnVisible(
11
+ onVisible: () => void,
12
+ options: {
13
+ /** False stops observing (nothing left, or a request in flight). */
14
+ enabled?: boolean;
15
+ /**
16
+ * Bump to rebuild the observer, which re-fires if the element is still in
17
+ * view. Observers report crossings, not states: a list shorter than the
18
+ * viewport would otherwise stop after one page.
19
+ */
20
+ revision?: unknown;
21
+ /** IntersectionObserver rootMargin; fires before the element appears. */
22
+ margin?: string;
23
+ } = {},
24
+ ) {
25
+ const { enabled = true, margin = "600px", revision } = options;
26
+
27
+ const callback = useRef(onVisible);
28
+ callback.current = onVisible;
29
+
30
+ const node = useRef<HTMLElement | null>(null);
31
+ const observer = useRef<IntersectionObserver | null>(null);
32
+
33
+ const attach = useCallback((next: HTMLElement | null) => {
34
+ node.current = next;
35
+ }, []);
36
+
37
+ useEffect(() => {
38
+ observer.current?.disconnect();
39
+ const target = node.current;
40
+ if (!target || !enabled) return;
41
+
42
+ observer.current = new IntersectionObserver(
43
+ ([entry]) => {
44
+ if (entry.isIntersecting) callback.current();
45
+ },
46
+ { root: scrollParent(target), rootMargin: margin },
47
+ );
48
+ observer.current.observe(target);
49
+ return () => observer.current?.disconnect();
50
+ }, [enabled, margin, revision]);
51
+
52
+ return attach;
53
+ }
54
+
55
+ /**
56
+ * Nearest scrolling ancestor, or null for the window. With the default root an
57
+ * ancestor's overflow clips the sentinel and it never intersects.
58
+ */
59
+ function scrollParent(node: HTMLElement): Element | null {
60
+ let parent = node.parentElement;
61
+ while (parent) {
62
+ const { overflowY } = getComputedStyle(parent);
63
+ if (
64
+ (overflowY === "auto" || overflowY === "scroll") &&
65
+ parent.scrollHeight > parent.clientHeight
66
+ ) {
67
+ return parent;
68
+ }
69
+ parent = parent.parentElement;
70
+ }
71
+ return null;
72
+ }
@@ -0,0 +1,106 @@
1
+ "use client";
2
+
3
+ import { useEffect, useState, useSyncExternalStore } from "react";
4
+ import { THEME_STORAGE_KEY, THEMES, type Theme } from "@/lib/theme";
5
+
6
+ /**
7
+ * Theme preference stored in localStorage and applied as the `dark` class on
8
+ * `<html>`. The boot script in app/layout reads the same key before hydration;
9
+ * this store only follows changes after that.
10
+ */
11
+
12
+ const isTheme = (value: unknown): value is Theme =>
13
+ (THEMES as readonly unknown[]).includes(value);
14
+
15
+ function read(): Theme {
16
+ if (typeof window === "undefined") return "system";
17
+ try {
18
+ const raw = window.localStorage.getItem(THEME_STORAGE_KEY);
19
+ return isTheme(raw) ? raw : "system";
20
+ } catch {
21
+ return "system";
22
+ }
23
+ }
24
+
25
+ let theme = read();
26
+ const listeners = new Set<() => void>();
27
+
28
+ function announce() {
29
+ for (const listener of listeners) listener();
30
+ }
31
+
32
+ /** Actual brightness the setting resolves to. */
33
+ export function resolveTheme(value: Theme): "light" | "dark" {
34
+ if (value !== "system") return value;
35
+ return window.matchMedia("(prefers-color-scheme: dark)").matches
36
+ ? "dark"
37
+ : "light";
38
+ }
39
+
40
+ /** Applies to `<html>`; touches exactly what the boot script touches. */
41
+ export function applyTheme(value: Theme) {
42
+ const dark = resolveTheme(value) === "dark";
43
+ document.documentElement.classList.toggle("dark", dark);
44
+ document.documentElement.style.colorScheme = dark ? "dark" : "light";
45
+ }
46
+
47
+ export function setTheme(next: Theme) {
48
+ theme = next;
49
+ try {
50
+ window.localStorage.setItem(THEME_STORAGE_KEY, next);
51
+ } catch {
52
+ // Private mode or blocked storage; the in-memory value still changes.
53
+ }
54
+ applyTheme(next);
55
+ announce();
56
+ }
57
+
58
+ export function useTheme(): Theme {
59
+ return useSyncExternalStore(
60
+ (listener) => {
61
+ listeners.add(listener);
62
+ // Another tab changed it.
63
+ const sync = (event: StorageEvent) => {
64
+ if (event.key !== THEME_STORAGE_KEY) return;
65
+ theme = read();
66
+ applyTheme(theme);
67
+ announce();
68
+ };
69
+ window.addEventListener("storage", sync);
70
+ return () => {
71
+ listeners.delete(listener);
72
+ window.removeEventListener("storage", sync);
73
+ };
74
+ },
75
+ () => theme,
76
+ // Server snapshot; hydration swaps in the stored value.
77
+ () => "system",
78
+ );
79
+ }
80
+
81
+ /** Resolved theme; follows the OS while the setting is "system". */
82
+ export function useResolvedTheme(): "light" | "dark" {
83
+ const current = useTheme();
84
+ const [systemDark, setSystemDark] = useState(false);
85
+ useEffect(() => {
86
+ const media = window.matchMedia("(prefers-color-scheme: dark)");
87
+ const follow = () => setSystemDark(media.matches);
88
+ follow();
89
+ media.addEventListener("change", follow);
90
+ return () => media.removeEventListener("change", follow);
91
+ }, []);
92
+ if (current !== "system") return current;
93
+ return systemDark ? "dark" : "light";
94
+ }
95
+
96
+ /** Re-applies "system" when the OS theme changes. Mount once, in the layout. */
97
+ export function useFollowSystemTheme() {
98
+ const current = useTheme();
99
+ useEffect(() => {
100
+ if (current !== "system") return;
101
+ const media = window.matchMedia("(prefers-color-scheme: dark)");
102
+ const follow = () => applyTheme("system");
103
+ media.addEventListener("change", follow);
104
+ return () => media.removeEventListener("change", follow);
105
+ }, [current]);
106
+ }
@@ -0,0 +1,262 @@
1
+ "use client";
2
+
3
+ import { useEffect, useRef } from "react";
4
+
5
+ /**
6
+ * Wake-word listener built on the browser's Web Speech API (no realtime socket
7
+ * while idle). It is a transcriber, not a KWS engine, so matching is fuzzy.
8
+ * Chrome streams idle audio to Google; Firefox has no recognizer (onError only).
9
+ */
10
+
11
+ /** lib.dom types the result objects but not the recognizer itself. */
12
+ type SpeechRecognitionLike = {
13
+ lang: string;
14
+ continuous: boolean;
15
+ interimResults: boolean;
16
+ maxAlternatives: number;
17
+ start(): void;
18
+ stop(): void;
19
+ abort(): void;
20
+ onresult:
21
+ | ((event: {
22
+ resultIndex: number;
23
+ results: SpeechRecognitionResultList;
24
+ }) => void)
25
+ | null;
26
+ onerror: ((event: { error: string }) => void) | null;
27
+ onend: (() => void) | null;
28
+ };
29
+
30
+ type SpeechRecognitionCtor = new () => SpeechRecognitionLike;
31
+
32
+ /**
33
+ * Default recognizer language. English keeps one spelling to compare against
34
+ * regardless of the speaker's accent; callers with a non-English name pass
35
+ * `lang` and `phrases` together.
36
+ */
37
+ export const WAKE_LANG = "en-US";
38
+
39
+ /**
40
+ * Allowed edit distance per word as a fraction of its length ("hey" = 1 edit,
41
+ * "thursday" = 3). Lower misses accents; higher lets ordinary speech wake.
42
+ */
43
+ const WAKE_TOLERANCE = 0.34;
44
+
45
+ /** Alternatives per result; the second-ranked guess is often the right one. */
46
+ const ALTERNATIVES = 3;
47
+
48
+ /** Delay before restarting after `onend`, so error loops do not spin. */
49
+ const RESTART_MS = 300;
50
+ /** Backoff cap while the recognition service is unreachable. */
51
+ const RESTART_MAX_MS = 10_000;
52
+
53
+ /** Errors a restart cannot fix. */
54
+ const FATAL: Record<string, string> = {
55
+ "not-allowed": "The microphone is not allowed",
56
+ "service-not-allowed": "The microphone is not allowed",
57
+ "audio-capture": "There is no microphone",
58
+ "language-not-supported": "This browser cannot listen in that language",
59
+ };
60
+
61
+ /** Case and punctuation vary per recognizer; word boundaries do not. */
62
+ const normalize = (text: string) =>
63
+ text
64
+ .toLowerCase()
65
+ .replace(/[^a-z0-9 ]/g, "")
66
+ .split(/\s+/)
67
+ .filter(Boolean);
68
+
69
+ /** Plain Levenshtein distance between two words. */
70
+ function distance(heard: string, want: string): number {
71
+ let previous = Array.from({ length: heard.length + 1 }, (_, index) => index);
72
+ let current = new Array<number>(heard.length + 1).fill(0);
73
+
74
+ for (let row = 1; row <= want.length; row++) {
75
+ current[0] = row;
76
+ for (let column = 1; column <= heard.length; column++) {
77
+ const substitute =
78
+ previous[column - 1] + (want[row - 1] === heard[column - 1] ? 0 : 1);
79
+ current[column] = Math.min(
80
+ substitute,
81
+ previous[column] + 1,
82
+ current[column - 1] + 1,
83
+ );
84
+ }
85
+ [previous, current] = [current, previous];
86
+ }
87
+
88
+ return previous[heard.length];
89
+ }
90
+
91
+ /** Within `budget` edits; the length check short-circuits most words. */
92
+ const near = (heard: string, want: string, budget: number) =>
93
+ Math.abs(heard.length - want.length) <= budget &&
94
+ distance(heard, want) <= budget;
95
+
96
+ /**
97
+ * Whether `want[from..]` is spoken contiguously starting at `heard[at]`.
98
+ * One wanted word may span two heard words ("there's day" for "thursday").
99
+ */
100
+ function saidAt(
101
+ heard: readonly string[],
102
+ at: number,
103
+ want: readonly string[],
104
+ from: number,
105
+ tolerance: number,
106
+ ): boolean {
107
+ if (from === want.length) return true;
108
+ if (at >= heard.length) return false;
109
+
110
+ const word = want[from];
111
+ // Never 0, or "hay" for "hey" fails.
112
+ const budget = Math.max(1, Math.round(word.length * tolerance));
113
+
114
+ if (
115
+ near(heard[at], word, budget) &&
116
+ saidAt(heard, at + 1, want, from + 1, tolerance)
117
+ ) {
118
+ return true;
119
+ }
120
+ return (
121
+ at + 1 < heard.length &&
122
+ near(heard[at] + heard[at + 1], word, budget) &&
123
+ saidAt(heard, at + 2, want, from + 1, tolerance)
124
+ );
125
+ }
126
+
127
+ /**
128
+ * Whether `heard` contains `want` as one contiguous phrase. Scoring is per
129
+ * word, not over the joined string, so every word must be present and adjacent.
130
+ */
131
+ function contains(
132
+ heard: readonly string[],
133
+ want: readonly string[],
134
+ tolerance: number,
135
+ ): boolean {
136
+ for (let at = 0; at < heard.length; at++) {
137
+ if (saidAt(heard, at, want, 0, tolerance)) return true;
138
+ }
139
+ return false;
140
+ }
141
+
142
+ type WakeOptions = {
143
+ /** Releases the microphone when false; must be false while a call is open. */
144
+ enabled: boolean;
145
+ /** Receives the raw transcript that matched. */
146
+ onWake: (heard: string) => void;
147
+ /** Phrases to listen for, matched fuzzily. */
148
+ phrases: readonly string[];
149
+ lang?: string;
150
+ /** See WAKE_TOLERANCE. */
151
+ tolerance?: number;
152
+ /** Unrecoverable failures only (denied or missing microphone). */
153
+ onError?: (reason: string) => void;
154
+ };
155
+
156
+ export function useWakeWord({
157
+ enabled,
158
+ onWake,
159
+ phrases,
160
+ lang = WAKE_LANG,
161
+ tolerance = WAKE_TOLERANCE,
162
+ onError,
163
+ }: WakeOptions) {
164
+ // Read at result time so re-renders and phrase changes do not restart the
165
+ // recognizer; only `lang` must be set before start.
166
+ const latest = useRef({ onWake, onError, phrases, tolerance });
167
+ latest.current = { onWake, onError, phrases, tolerance };
168
+
169
+ useEffect(() => {
170
+ if (!enabled) return;
171
+
172
+ const global = window as unknown as Record<string, unknown>;
173
+ const Ctor = (global.SpeechRecognition ?? global.webkitSpeechRecognition) as
174
+ | SpeechRecognitionCtor
175
+ | undefined;
176
+
177
+ if (!Ctor) {
178
+ latest.current.onError?.("This browser cannot listen for a wake word");
179
+ return;
180
+ }
181
+
182
+ const recognition = new Ctor();
183
+ // Latched off on cleanup so late events cannot revive the recognizer.
184
+ let alive = true;
185
+ // Off from a match until the restart that clears the transcript.
186
+ let armed = true;
187
+ let restart: ReturnType<typeof setTimeout> | null = null;
188
+ let delay = RESTART_MS;
189
+
190
+ recognition.lang = lang;
191
+ // Interim results are enough; waiting for isFinal loses a beat.
192
+ recognition.continuous = true;
193
+ recognition.interimResults = true;
194
+ recognition.maxAlternatives = ALTERNATIVES;
195
+
196
+ const listen = () => {
197
+ if (!alive) return;
198
+ armed = true;
199
+ try {
200
+ recognition.start();
201
+ } catch {
202
+ // Already running (InvalidStateError).
203
+ }
204
+ };
205
+
206
+ recognition.onresult = ({ resultIndex, results }) => {
207
+ if (!alive || !armed) return;
208
+ delay = RESTART_MS;
209
+
210
+ const { phrases, tolerance, onWake } = latest.current;
211
+ const targets = phrases.map(normalize).filter((words) => words.length);
212
+
213
+ // Results before resultIndex are final and already seen.
214
+ for (let index = resultIndex; index < results.length; index++) {
215
+ const result = results[index];
216
+ for (let rank = 0; rank < result.length; rank++) {
217
+ const heard = result[rank].transcript;
218
+ const words = normalize(heard);
219
+ if (!targets.some((target) => contains(words, target, tolerance))) {
220
+ continue;
221
+ }
222
+
223
+ // Abort before notifying: the transcript keeps growing and would
224
+ // match again on the next event. `onend` restarts unless the caller
225
+ // has dropped `enabled` by then.
226
+ armed = false;
227
+ recognition.abort();
228
+ onWake(heard.trim());
229
+ return;
230
+ }
231
+ }
232
+ };
233
+
234
+ recognition.onerror = ({ error }) => {
235
+ const reason = FATAL[error];
236
+ if (reason) {
237
+ alive = false;
238
+ latest.current.onError?.(reason);
239
+ } else if (error === "network") {
240
+ delay = Math.min(delay * 2, RESTART_MAX_MS);
241
+ }
242
+ // no-speech and aborted are normal for this API.
243
+ };
244
+
245
+ // Chrome ends a silent session after about a minute; keep restarting.
246
+ recognition.onend = () => {
247
+ if (!alive) return;
248
+ restart = setTimeout(listen, delay);
249
+ };
250
+
251
+ listen();
252
+
253
+ return () => {
254
+ alive = false;
255
+ if (restart) clearTimeout(restart);
256
+ recognition.onresult = null;
257
+ recognition.onerror = null;
258
+ recognition.onend = null;
259
+ recognition.abort();
260
+ };
261
+ }, [enabled, lang]);
262
+ }
@@ -1,3 +1,4 @@
1
+ import { logger } from "@/lib/logger";
1
2
  import { errorToString } from "@/lib/utils";
2
3
  import type {
3
4
  RealtimeFunctionTool,
@@ -85,6 +86,9 @@ export function realtimeSession(
85
86
  } | null = null;
86
87
  let ackTimer: ReturnType<typeof setTimeout> | null = null;
87
88
 
89
+ /** Event types this session has already noted as unhandled (see `default`). */
90
+ const seenUnhandled = new Set<string>();
91
+
88
92
  const turns: Turn[] = [];
89
93
  const slot = new Map<string, number>();
90
94
  const finalized = new Set<string>();
@@ -339,7 +343,25 @@ export function realtimeSession(
339
343
 
340
344
  case "conversation.item.input_audio_transcription.completed":
341
345
  write(event.item_id, "user", event.transcript ?? "", "replace");
342
- finish(event.item_id);
346
+ // Not always the end of the turn. xAI sends this while the user is
347
+ // still speaking, each time with a longer transcript and `in_progress`
348
+ // on it; finishing on the first froze the turn at its opening words
349
+ // and every later transcript was written but never reported (`write`
350
+ // reports only what is not finalized). A provider that sends it once
351
+ // omits the field, so anything but `in_progress` ends the turn.
352
+ if (event.status !== "in_progress") finish(event.item_id);
353
+ break;
354
+
355
+ // A turn nobody could read. Without this the item simply never gets
356
+ // words, and an item with no words is not reported at all (`report`) —
357
+ // the turn disappears from the transcript, and every reader downstream
358
+ // sees a conversation the user did not have.
359
+ case "conversation.item.input_audio_transcription.failed":
360
+ on.warn(
361
+ event.error?.message
362
+ ? `That turn could not be transcribed: ${event.error.message}`
363
+ : "That turn could not be transcribed.",
364
+ );
343
365
  break;
344
366
 
345
367
  case "response.output_audio_transcript.delta":
@@ -388,6 +410,17 @@ export function realtimeSession(
388
410
  finish(event.item_id);
389
411
  onToolCall(event);
390
412
  break;
413
+
414
+ default:
415
+ // Providers speak this protocol with their own additions, and a case
416
+ // that is missing here is invisible: the event is dropped and the
417
+ // conversation quietly loses whatever it carried. Once per type per
418
+ // session, so a busy stream cannot drown the log.
419
+ if (!seenUnhandled.has(event.type)) {
420
+ seenUnhandled.add(event.type);
421
+ logger.debug(`realtime: no case for ${event.type}`);
422
+ }
423
+ break;
391
424
  }
392
425
  settle();
393
426
  };
@@ -308,6 +308,12 @@ export type RealtimeServerEvent =
308
308
  type: "conversation.item.input_audio_transcription.completed";
309
309
  item_id: string;
310
310
  transcript: string;
311
+ /**
312
+ * xAI sends this event repeatedly while the turn is still being
313
+ * transcribed, each time with more of it and `in_progress` here; only the
314
+ * last carries `completed`. OpenAI sends it once and omits the field.
315
+ */
316
+ status?: "in_progress" | "completed" | string;
311
317
  }
312
318
  /** OpenAI only. */
313
319
  | {
package/lib/sandbox.ts CHANGED
@@ -4,6 +4,14 @@ import { join, resolve as pathResolve, relative, sep } from "node:path";
4
4
 
5
5
  export type ExecResult = { stdout: string; stderr: string; exitCode: number };
6
6
 
7
+ /**
8
+ * How long one command may run before its process group is killed. Nobody is
9
+ * watching the shell, so a command waiting on a prompt would otherwise hang
10
+ * the job; anything that legitimately runs longer belongs in the background.
11
+ * Named because the shell guide states it to the model (ai/tools/workspace.tool).
12
+ */
13
+ export const EXEC_TIMEOUT_MS = 180_000;
14
+
7
15
  export interface Sandbox {
8
16
  /** Absolute. Relative paths resolve from here */
9
17
  readonly cwd: string;
@@ -155,7 +163,7 @@ export const createSandBox = ({
155
163
  return out;
156
164
  },
157
165
 
158
- exec(command, { cwd: c, timeoutMs = 180_000, signal, env } = {}) {
166
+ exec(command, { cwd: c, timeoutMs = EXEC_TIMEOUT_MS, signal, env } = {}) {
159
167
  return new Promise((resolve) => {
160
168
  const base = shellEnv();
161
169
  if (extraPath) base.PATH = `${base.PATH ?? ""}:${extraPath}`;