thursday-agent 0.8.0 → 0.9.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 (264) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/build-manifest.json +3 -3
  3. package/.next/prerender-manifest.json +3 -3
  4. package/.next/server/app/_global-error/page.js.nft.json +1 -1
  5. package/.next/server/app/_global-error.html +1 -1
  6. package/.next/server/app/_global-error.rsc +1 -1
  7. package/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  8. package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  9. package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  10. package/.next/server/app/_not-found/page.js.nft.json +1 -1
  11. package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  12. package/.next/server/app/_not-found.html +1 -1
  13. package/.next/server/app/_not-found.rsc +2 -2
  14. package/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
  15. package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
  16. package/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  17. package/.next/server/app/api/artifact/route.js.nft.json +1 -1
  18. package/.next/server/app/api/bot/keep-working/route.js.nft.json +1 -1
  19. package/.next/server/app/api/bot/memory/files/route.js.nft.json +1 -1
  20. package/.next/server/app/api/bot/memory/route.js.nft.json +1 -1
  21. package/.next/server/app/api/bot/route.js.nft.json +1 -1
  22. package/.next/server/app/api/bot/thread/[id]/route.js.nft.json +1 -1
  23. package/.next/server/app/api/bot/thread/route.js.nft.json +1 -1
  24. package/.next/server/app/api/config/route.js.nft.json +1 -1
  25. package/.next/server/app/api/events/route.js.nft.json +1 -1
  26. package/.next/server/app/api/favicon/[host]/route.js.nft.json +1 -1
  27. package/.next/server/app/api/file/[...path]/route.js.nft.json +1 -1
  28. package/.next/server/app/api/llm-model/catalog/route.js.nft.json +1 -1
  29. package/.next/server/app/api/llm-model/credits/route.js.nft.json +1 -1
  30. package/.next/server/app/api/llm-model/route.js.nft.json +1 -1
  31. package/.next/server/app/api/llm-model/subscription/route.js.nft.json +1 -1
  32. package/.next/server/app/api/mcp/[name]/route.js.nft.json +1 -1
  33. package/.next/server/app/api/mcp/oauth/callback/route.js.nft.json +1 -1
  34. package/.next/server/app/api/mcp/route.js.nft.json +1 -1
  35. package/.next/server/app/api/mcp/tools/route.js.nft.json +1 -1
  36. package/.next/server/app/api/memory/edit/route.js +1 -1
  37. package/.next/server/app/api/memory/edit/route.js.nft.json +1 -1
  38. package/.next/server/app/api/memory/route.js.nft.json +1 -1
  39. package/.next/server/app/api/probe/route.js.nft.json +1 -1
  40. package/.next/server/app/api/reach/route.js +11 -11
  41. package/.next/server/app/api/reach/route.js.nft.json +1 -1
  42. package/.next/server/app/api/routine/route.js +6 -6
  43. package/.next/server/app/api/routine/route.js.nft.json +1 -1
  44. package/.next/server/app/api/signins/route.js.nft.json +1 -1
  45. package/.next/server/app/api/skills/[source]/[name]/route.js.nft.json +1 -1
  46. package/.next/server/app/api/skills/route.js.nft.json +1 -1
  47. package/.next/server/app/api/thursday/call/route.js.nft.json +1 -1
  48. package/.next/server/app/api/thursday/skills/route.js.nft.json +1 -1
  49. package/.next/server/app/api/thursday/text/route.js +11 -11
  50. package/.next/server/app/api/thursday/text/route.js.nft.json +1 -1
  51. package/.next/server/app/api/thursday/tool-call/route.js +6 -6
  52. package/.next/server/app/api/thursday/tool-call/route.js.nft.json +1 -1
  53. package/.next/server/app/api/workspace/files/route.js.nft.json +1 -1
  54. package/.next/server/app/api/workspace/route.js.nft.json +1 -1
  55. package/.next/server/app/artifact/[...path]/page/server-reference-manifest.json +1 -1
  56. package/.next/server/app/artifact/[...path]/page.js.nft.json +1 -1
  57. package/.next/server/app/artifact/[...path]/page_client-reference-manifest.js +1 -1
  58. package/.next/server/app/favicon.ico/route.js.nft.json +1 -1
  59. package/.next/server/app/manifest.webmanifest/route.js.nft.json +1 -1
  60. package/.next/server/app/page/server-reference-manifest.json +56 -56
  61. package/.next/server/app/page.js +3 -3
  62. package/.next/server/app/page.js.nft.json +1 -1
  63. package/.next/server/app/page_client-reference-manifest.js +1 -1
  64. package/.next/server/chunks/[root-of-the-server]__012s2o8._.js +1 -1
  65. package/.next/server/chunks/[root-of-the-server]__04r4h-2._.js +1 -1
  66. package/.next/server/chunks/[root-of-the-server]__0mrk2it._.js +1 -1
  67. package/.next/server/chunks/[root-of-the-server]__0nmhgeo._.js +3 -0
  68. package/.next/server/chunks/[root-of-the-server]__1fy2g82._.js +3 -0
  69. package/.next/server/chunks/[root-of-the-server]__1ndi14y._.js +1 -1
  70. package/.next/server/chunks/[root-of-the-server]__1nmflqk._.js +3 -0
  71. package/.next/server/chunks/[root-of-the-server]__1v7-zke._.js +1 -1
  72. package/.next/server/chunks/[root-of-the-server]__1wnzmx9._.js +3 -0
  73. package/.next/server/chunks/[root-of-the-server]__1xv-5cq._.js +1 -1
  74. package/.next/server/chunks/[root-of-the-server]__1ygplx1._.js +1 -1
  75. package/.next/server/chunks/_05v8k21._.js +1 -1
  76. package/.next/server/chunks/_0awtznl._.js +4 -4
  77. package/.next/server/chunks/_0k6qjcf._.js +1 -1
  78. package/.next/server/chunks/_0na2fj1._.js +5 -5
  79. package/.next/server/chunks/_0x-ej3f._.js +21 -21
  80. package/.next/server/chunks/_1dc_d1q._.js +22 -20
  81. package/.next/server/chunks/_1jc5g4p._.js +2 -2
  82. package/.next/server/chunks/_1jn-aep._.js +50 -48
  83. package/.next/server/chunks/{_1u8ygbz._.js → _1m88lkh._.js} +7 -7
  84. package/.next/server/chunks/_1uuucy-._.js +4 -4
  85. package/.next/server/chunks/_1v9r2ke._.js +1 -1
  86. package/.next/server/chunks/ssr/{[root-of-the-server]__1h4bnrf._.js → [root-of-the-server]__0oo-wlv._.js} +2 -2
  87. package/.next/server/chunks/ssr/{[root-of-the-server]__1kyi568._.js → [root-of-the-server]__0tvavy9._.js} +1 -1
  88. package/.next/server/chunks/ssr/[root-of-the-server]__0tzekbl._.js +1 -1
  89. package/.next/server/chunks/ssr/[root-of-the-server]__0zuns7e._.js +3 -0
  90. package/.next/server/chunks/ssr/[root-of-the-server]__1se25x0._.js +1 -1
  91. package/.next/server/chunks/ssr/{_1i3ddn7._.js → _05pl_u8._.js} +1 -1
  92. package/.next/server/chunks/ssr/_070nv1_._.js +3 -0
  93. package/.next/server/chunks/ssr/_097-ljd._.js +1 -1
  94. package/.next/server/chunks/ssr/_0dshsh4._.js +3 -0
  95. package/.next/server/chunks/ssr/_0fqhhwi._.js +1 -1
  96. package/.next/server/chunks/ssr/{_0z8jpch._.js → _0ital-2._.js} +2 -2
  97. package/.next/server/chunks/ssr/{_19g570d._.js → _0kjj5ts._.js} +3 -3
  98. package/.next/server/chunks/ssr/{_1nod5hh._.js → _0l_mcfz._.js} +2 -2
  99. package/.next/server/chunks/ssr/_0ld9_qq._.js +1 -1
  100. package/.next/server/chunks/ssr/_0ltz2q5._.js +3 -0
  101. package/.next/server/chunks/ssr/_0scf7xy._.js +3 -0
  102. package/.next/server/chunks/ssr/_0u5xv6g._.js +3 -0
  103. package/.next/server/chunks/ssr/{_0ewory0._.js → _0urj3mc._.js} +2 -2
  104. package/.next/server/chunks/ssr/{_0pm-2p-._.js → _1-zyvdb._.js} +2 -2
  105. package/.next/server/chunks/ssr/{_1_rqu8x._.js → _12mijol._.js} +7 -7
  106. package/.next/server/chunks/ssr/_15zqt-t._.js +1 -1
  107. package/.next/server/chunks/ssr/_184327j._.js +1 -1
  108. package/.next/server/chunks/ssr/{_1y-ea28._.js → _1a-nuxx._.js} +2 -2
  109. package/.next/server/chunks/ssr/_1alxym6._.js +3 -0
  110. package/.next/server/chunks/ssr/{_0hzz82u._.js → _1d423aj._.js} +25 -23
  111. package/.next/server/chunks/ssr/{_1u1mxu9._.js → _1flbs77._.js} +2 -2
  112. package/.next/server/chunks/ssr/_1mqn9mc._.js +1 -1
  113. package/.next/server/chunks/ssr/_1neoj22._.js +1 -1
  114. package/.next/server/chunks/ssr/{_1oijqjw._.js → _1tmu8z-._.js} +2 -2
  115. package/.next/server/chunks/ssr/{_10y6r1n._.js → _1uy0xom._.js} +3 -3
  116. package/.next/server/chunks/ssr/_1x-kojb._.js +20 -0
  117. package/.next/server/chunks/ssr/_1xoch6f._.js +1 -1
  118. package/.next/server/chunks/ssr/_1z74dmq._.js +164 -0
  119. package/.next/server/chunks/ssr/features_bot_1nr94ks._.js +3 -0
  120. package/.next/server/chunks/ssr/features_bot_1s_-2vj._.js +1 -1
  121. package/.next/server/chunks/ssr/features_connectors_components_mcp-setting_tsx_0jmbgvp._.js +1 -1
  122. package/.next/server/chunks/ssr/features_memory_components_memory-setting_tsx_0oa43sw._.js +1 -1
  123. package/.next/server/chunks/ssr/features_routine_components_routine-setting_tsx_055fknz._.js +1 -1
  124. package/.next/server/chunks/ssr/{node_modules_111sh9o._.js → node_modules_03d0rfz._.js} +1 -1
  125. package/.next/server/chunks/ssr/{node_modules_0lg75qx._.js → node_modules_04_sbbc._.js} +1 -1
  126. package/.next/server/chunks/ssr/{node_modules_10gbm2y._.js → node_modules_05ybmta._.js} +1 -1
  127. package/.next/server/chunks/ssr/{node_modules_1v9tc1c._.js → node_modules_068ws5v._.js} +1 -1
  128. package/.next/server/chunks/ssr/{node_modules_1_8mv4v._.js → node_modules_06bb4q8._.js} +1 -1
  129. package/.next/server/chunks/ssr/{node_modules_01f8_se._.js → node_modules_07rs_p2._.js} +1 -1
  130. package/.next/server/chunks/ssr/{node_modules_0ii1l89._.js → node_modules_0dzm4um._.js} +1 -1
  131. package/.next/server/chunks/ssr/{node_modules_20xmwtu._.js → node_modules_0jljel9._.js} +1 -1
  132. package/.next/server/chunks/ssr/{node_modules_1ix-g49._.js → node_modules_0sgy-dc._.js} +1 -1
  133. package/.next/server/chunks/ssr/{node_modules_0woscox._.js → node_modules_0t_ez6-._.js} +1 -1
  134. package/.next/server/chunks/ssr/{node_modules_059vtf2._.js → node_modules_0th357h._.js} +1 -1
  135. package/.next/server/chunks/ssr/{node_modules_06w645b._.js → node_modules_0ubdtk-._.js} +1 -1
  136. package/.next/server/chunks/ssr/{node_modules_0b8df3s._.js → node_modules_0uyh_vb._.js} +1 -1
  137. package/.next/server/chunks/ssr/{node_modules_07ih6wb._.js → node_modules_0wnlddw._.js} +1 -1
  138. package/.next/server/chunks/ssr/{node_modules_0gt-uw9._.js → node_modules_0x630an._.js} +1 -1
  139. package/.next/server/chunks/ssr/{node_modules_1x825hz._.js → node_modules_0yvz9e0._.js} +1 -1
  140. package/.next/server/chunks/ssr/{node_modules_0t-44lj._.js → node_modules_12et8d5._.js} +1 -1
  141. package/.next/server/chunks/ssr/{node_modules_1m3gb16._.js → node_modules_13kwejm._.js} +1 -1
  142. package/.next/server/chunks/ssr/{node_modules_1p9rdsy._.js → node_modules_1af3w91._.js} +1 -1
  143. package/.next/server/chunks/ssr/{node_modules_0sz5yg1._.js → node_modules_1bucjlk._.js} +1 -1
  144. package/.next/server/chunks/ssr/{node_modules_1teyzfa._.js → node_modules_1lkix61._.js} +1 -1
  145. package/.next/server/chunks/ssr/{node_modules_1lsyy7k._.js → node_modules_1ogczzz._.js} +1 -1
  146. package/.next/server/chunks/ssr/{node_modules_1-7qawg._.js → node_modules_1s-gb-1._.js} +1 -1
  147. package/.next/server/chunks/ssr/{node_modules_0yv-re6._.js → node_modules_1uwg-gz._.js} +1 -1
  148. package/.next/server/chunks/ssr/{node_modules_03ptrl9._.js → node_modules_1wbkzl9._.js} +1 -1
  149. package/.next/server/chunks/ssr/{node_modules_00jv9_3._.js → node_modules_1z1lrbk._.js} +1 -1
  150. package/.next/server/chunks/ssr/{node_modules_@base-ui_react_0jne69c._.js → node_modules_@base-ui_react_0buz1-j._.js} +1 -1
  151. package/.next/server/chunks/ssr/{node_modules_@mermaid-js_parser_dist_chunks_mermaid-parser_core_00q1oa3._.js → node_modules_@mermaid-js_parser_dist_chunks_mermaid-parser_core_0r443fh._.js} +1 -1
  152. package/.next/server/chunks/ssr/{node_modules_@mermaid-js_parser_dist_chunks_mermaid-parser_core_1-2_5z7._.js → node_modules_@mermaid-js_parser_dist_chunks_mermaid-parser_core_10wnlbk._.js} +1 -1
  153. package/.next/server/middleware-build-manifest.js +3 -3
  154. package/.next/server/pages/404.html +1 -1
  155. package/.next/server/pages/500.html +1 -1
  156. package/.next/server/server-reference-manifest.js +1 -1
  157. package/.next/server/server-reference-manifest.json +57 -57
  158. package/.next/static/chunks/06nt9tatr-dug.js +1 -0
  159. package/.next/static/chunks/0_awn8tucznxc.js +1 -0
  160. package/.next/static/chunks/{34s52v8f841zo.js → 0ge-hx0ed83a1.js} +1 -1
  161. package/.next/static/chunks/0pda_vf-abwvx.js +1 -0
  162. package/.next/static/chunks/{2u9yjkpkoij75.js → 0qmd8n52hp-6v.js} +1 -1
  163. package/.next/static/chunks/0rowp9rx3qbxu.js +18 -0
  164. package/.next/static/chunks/{1ocy4w0v20a3v.js → 13ixzheqw7o-w.js} +2 -2
  165. package/.next/static/chunks/1grkt5gr8pbg1.js +17 -0
  166. package/.next/static/chunks/{21gf2t_whhurc.js → 1yugpvvi8qrbu.js} +2 -2
  167. package/.next/static/chunks/{1e4yrzs5evq_r.js → 245hb85nsy6j4.js} +1 -1
  168. package/.next/static/chunks/27lkanxvtdn09.js +1 -0
  169. package/.next/static/chunks/{0oz7a68qanm99.js → 27tvv1i-smrck.js} +1 -1
  170. package/.next/static/chunks/{2zhfdbq50ee5i.js → 2c64drdevdla3.js} +1 -1
  171. package/.next/static/chunks/{0k5tkt_qi52kb.js → 2ndw4w4u2x51r.js} +1 -1
  172. package/.next/static/chunks/37j27b3e1gt7r.js +1 -0
  173. package/.next/static/chunks/{2om4lkg86feb4.js → 38301ji-85i30.js} +1 -1
  174. package/.next/static/chunks/{09c13vnqy7al3.js → 3bbzsitgiltnt.js} +2 -2
  175. package/.next/static/chunks/3fec-aeteddr-.js +1 -0
  176. package/.next/static/chunks/3go3hwp1z2_h2.css +1 -0
  177. package/.next/static/chunks/3h2bun3hcggg8.js +1 -0
  178. package/.next/static/chunks/{0g1p98s2pfx2w.js → 3h2e-xv1zxh_f.js} +2 -2
  179. package/.next/static/chunks/3j1y335o95mj7.js +1 -0
  180. package/.next/static/chunks/3obf9n3cxj99k.js +1 -0
  181. package/.next/static/chunks/{2upc9mkcc3nlq.js → 427qsc6kahxyo.js} +1 -1
  182. package/README.md +31 -14
  183. package/bin/port.mjs +11 -7
  184. package/bin/thursday.mjs +1 -1
  185. package/components/ui/radio-group.tsx +2 -2
  186. package/components/ui/shiny-text.tsx +3 -22
  187. package/docs/how-it-works.md +11 -3
  188. package/docs/images/first-run.png +0 -0
  189. package/docs/images/keeps-going.png +0 -0
  190. package/docs/images/two-minds.png +0 -0
  191. package/features/ai/components/model-browser.tsx +1 -1
  192. package/features/ai/components/model-picker.tsx +1 -1
  193. package/features/ai/guide.ts +9 -4
  194. package/features/ai/prompts/bot.prompt.ts +4 -1
  195. package/features/ai/tools/routine.tool.ts +28 -16
  196. package/features/bot/bot.runner.ts +3 -1
  197. package/features/bot/components/bot-mark.tsx +6 -1
  198. package/features/bot/components/bot-setting.tsx +2 -4
  199. package/features/bot/components/bot-tool.tsx +2 -0
  200. package/features/bot/components/room-list.tsx +12 -3
  201. package/features/bot/components/room-pill.tsx +26 -19
  202. package/features/config/components/config-setting.tsx +17 -5
  203. package/features/config/components/voice-key.tsx +38 -18
  204. package/features/connectors/mcp.const.ts +90 -59
  205. package/features/intro/components/intro.tsx +50 -27
  206. package/features/routine/components/routine-mark.tsx +4 -6
  207. package/features/routine/components/routine-setting.tsx +332 -143
  208. package/features/routine/routine.query.ts +24 -3
  209. package/features/routine/routine.schema.ts +55 -4
  210. package/features/settings/components/install-app.tsx +1 -4
  211. package/features/settings/components/setting-ui.tsx +1 -1
  212. package/features/skills/skills.discover.ts +2 -1
  213. package/features/skills/skills.query.ts +5 -1
  214. package/features/skills/skills.schema.ts +2 -0
  215. package/features/thursday/components/ascii-orb.tsx +39 -10
  216. package/features/thursday/components/connect-wave.tsx +284 -0
  217. package/features/thursday/components/side-captions.tsx +29 -17
  218. package/features/thursday/components/thursday-setting.tsx +2 -2
  219. package/features/thursday/components/thursday.tsx +130 -58
  220. package/features/thursday/components/voice-picker.tsx +1 -1
  221. package/features/thursday/components/write-line.tsx +2 -2
  222. package/features/thursday/open-work.ts +11 -3
  223. package/features/thursday/tool-line.ts +94 -12
  224. package/features/thursday/use-text-call.ts +3 -2
  225. package/features/thursday/use-thursday.ts +5 -2
  226. package/features/workspace/components/artifact-view.tsx +129 -7
  227. package/guide/bots.md +17 -5
  228. package/guide/calls.md +15 -7
  229. package/guide/setup.md +10 -2
  230. package/lib/desktop-notify.ts +3 -2
  231. package/package.json +1 -1
  232. package/scripts/bot-context.test.mts +1 -1
  233. package/skills/computer/SKILL.md +1 -0
  234. package/skills/interactive-page/SKILL.md +4 -0
  235. package/.next/server/chunks/[root-of-the-server]__0n-l_n_._.js +0 -3
  236. package/.next/server/chunks/[root-of-the-server]__0wvo2of._.js +0 -3
  237. package/.next/server/chunks/[root-of-the-server]__1e6aut3._.js +0 -3
  238. package/.next/server/chunks/[root-of-the-server]__1ziojdq._.js +0 -3
  239. package/.next/server/chunks/ssr/[root-of-the-server]__1srp8l5._.js +0 -3
  240. package/.next/server/chunks/ssr/_0mes4s0._.js +0 -3
  241. package/.next/server/chunks/ssr/_0ya0cb0._.js +0 -3
  242. package/.next/server/chunks/ssr/_0yto5s7._.js +0 -20
  243. package/.next/server/chunks/ssr/_10dw2nv._.js +0 -3
  244. package/.next/server/chunks/ssr/_1p0gxeo._.js +0 -164
  245. package/.next/server/chunks/ssr/_1r41yvh._.js +0 -3
  246. package/.next/server/chunks/ssr/_1r79ksv._.js +0 -3
  247. package/.next/server/chunks/ssr/features_bot_1ogjt52._.js +0 -3
  248. package/.next/server/chunks/ssr/node_modules_@base-ui_react_0b3d4m9._.js +0 -3
  249. package/.next/static/chunks/03omwz_vi2ndg.js +0 -1
  250. package/.next/static/chunks/0_1bqgd6wt_og.js +0 -1
  251. package/.next/static/chunks/0abgkzikaznxc.js +0 -1
  252. package/.next/static/chunks/0bddkpash59-o.js +0 -17
  253. package/.next/static/chunks/0c2-bzhy3a0ib.js +0 -1
  254. package/.next/static/chunks/0iilfb1ocevnx.js +0 -1
  255. package/.next/static/chunks/0imbhx-j830a_.css +0 -1
  256. package/.next/static/chunks/24h1y9dhy3m_2.js +0 -1
  257. package/.next/static/chunks/2be08eqxsfc1y.js +0 -1
  258. package/.next/static/chunks/2d3yhjtafgmtw.js +0 -1
  259. package/.next/static/chunks/2f_5saocsko_b.js +0 -1
  260. package/.next/static/chunks/2lwbr5vx0ixaj.js +0 -18
  261. package/.next/static/chunks/44kjmi5h_p8px.js +0 -1
  262. /package/.next/static/{UiOMPYpQ4fqGyu5PzS-7j → u8kJqShUAa49vY7BxAWsw}/_buildManifest.js +0 -0
  263. /package/.next/static/{UiOMPYpQ4fqGyu5PzS-7j → u8kJqShUAa49vY7BxAWsw}/_clientMiddlewareManifest.js +0 -0
  264. /package/.next/static/{UiOMPYpQ4fqGyu5PzS-7j → u8kJqShUAa49vY7BxAWsw}/_ssgManifest.js +0 -0
@@ -18,6 +18,7 @@ import {
18
18
  useMemo,
19
19
  useRef,
20
20
  useState,
21
+ useSyncExternalStore,
21
22
  } from "react";
22
23
  import { queryKey } from "@/app/api/query-key";
23
24
  import { Button } from "@/components/ui/button";
@@ -74,8 +75,14 @@ import { useHotkeyLabel } from "@/hooks/use-hotkey";
74
75
  import { RING_CYCLE_MS } from "@/lib/live/ring";
75
76
  import { useServerRoute } from "@/lib/protocol/use-server-route";
76
77
  import { cn, plainText } from "@/lib/utils";
78
+ import { ConnectWave } from "./connect-wave";
77
79
  import { Face } from "./face";
78
- import { SideCaptions, turnsOf, useTurnFocus } from "./side-captions";
80
+ import {
81
+ SideCaptions,
82
+ type Turn,
83
+ turnsOf,
84
+ useTurnFocus,
85
+ } from "./side-captions";
79
86
  import { TabState } from "./tab-state";
80
87
  import { WriteLine, type WrittenCall } from "./write-line";
81
88
 
@@ -163,8 +170,10 @@ function CallScreen({
163
170
  const hers =
164
171
  messages.findLast((turn) => turn.role === "assistant")?.text ?? "";
165
172
  // An open thread lies over the right of the call and moves none of it: her face and the
166
- // captions are where they were when it closes
167
- const sided = captionView === "sides" && status !== "idle";
173
+ // captions are where they were when it closes. A window too narrow for three columns
174
+ // draws her last line instead, whatever the setting.
175
+ const wide = useWide(SIDES_MIN_WIDTH);
176
+ const sided = captionView === "sides" && status !== "idle" && wide;
168
177
  const talk = useMemo(() => turnsOf(messages), [messages]);
169
178
  const turns = useTurnFocus(talk, sided);
170
179
  const lastRole = talk.at(-1)?.role;
@@ -178,6 +187,10 @@ function CallScreen({
178
187
  if (lastRole !== "user") setMaking(false);
179
188
  else if (work.on) setMaking(true);
180
189
  }, [lastRole, work.on]);
190
+ // The work behind each of her turns stays with it for the call: what ran while she was on
191
+ // it, or before she answered, is kept under her words and shows again when she is gone
192
+ // back to. A later stretch of work on the same turn adds to it
193
+ const kept = useKeptWork(talk, work.lines);
181
194
  // the last turn is still being said: her voice is on, or yours came after hers and she has not answered
182
195
  const saying =
183
196
  (lastRole === "assistant" && status === "speaking") ||
@@ -196,7 +209,11 @@ function CallScreen({
196
209
  {/* Top padding in vh, like the face itself, so the face+text column sits below center */}
197
210
  <div className="flex min-h-0 flex-1 flex-col items-center justify-center gap-5 pt-[7vh]">
198
211
  {/* The face is the control. It reacts to the agent's own voice. */}
199
- <div className="relative w-[min(28rem,72vw,52vh)]">
212
+ {/* The layout box is the part of her face she fills while talking; the canvas draws
213
+ `--face-bleed` past it on every side for the room her words, the tail she works
214
+ with and the gathering need, so what sits under her is not pushed away by empty
215
+ field. What stands beside her stands past the canvas (SideCaptions). */}
216
+ <div className="relative w-[min(20rem,52vw,37.5vh)] [--face-bleed:19.5%]">
200
217
  <button
201
218
  type="button"
202
219
  disabled={busy}
@@ -232,10 +249,11 @@ function CallScreen({
232
249
  failed={failed}
233
250
  word={ringWord ?? faceWord}
234
251
  getSpectrum={getSpectrum}
235
- className="w-full"
252
+ className="-m-(--face-bleed) w-[calc(100%+2*var(--face-bleed))] max-w-none"
236
253
  />
237
254
  </span>
238
255
  </button>
256
+ <ConnectWave status={status} charset={face.charset} />
239
257
 
240
258
  {sided && (
241
259
  <SideCaptions
@@ -244,17 +262,18 @@ function CallScreen({
244
262
  live={saying}
245
263
  onPick={turns.pick}
246
264
  under={
247
- work.held ? (
248
- <WorkStack
249
- lines={work.lines}
250
- shown={work.on}
251
- thinkingSince={thinkingSince}
252
- thinkingTitle={thinkingTitle}
253
- />
265
+ work.held && work.lines.length > 0 ? (
266
+ <WorkStack lines={work.lines} shown={work.on} />
267
+ ) : null
268
+ }
269
+ // with the work gone and her answer not yet begun, her last words come back level;
270
+ // thinking alone takes nothing from them, since it stands under her face
271
+ ahead={making && work.held && work.lines.length > 0}
272
+ workOf={(turn) =>
273
+ kept[turn]?.length ? (
274
+ <WorkStack lines={kept[turn]} shown />
254
275
  ) : null
255
276
  }
256
- // with the work gone and her answer not yet begun, her last words come back level
257
- ahead={making && work.held}
258
277
  />
259
278
  )}
260
279
  </div>
@@ -267,6 +286,7 @@ function CallScreen({
267
286
  {ringing && (
268
287
  <Incoming
269
288
  ringing={ringing}
289
+ wakePhrase={wakePhrase}
270
290
  onAnswer={onTap}
271
291
  onDecline={() => onDecline?.()}
272
292
  />
@@ -278,9 +298,11 @@ function CallScreen({
278
298
  (tool-line). The fast channel; the face does not follow it
279
299
  (use-thursday). */}
280
300
  <ActivityRow
281
- // beside her face the work is on her side (WorkStack); this slot keeps the meter
301
+ // beside her face the lines are on her side (WorkStack); what she is thinking
302
+ // about and the meter stay here in either view
282
303
  tool={sided ? null : drawn}
283
- thinkingSince={sided ? null : thinkingSince}
304
+ toolUp={drawn !== null}
305
+ thinkingSince={thinkingSince}
284
306
  thinkingTitle={thinkingTitle}
285
307
  // nobody is listened to on a call in writing: there is no microphone
286
308
  listening={
@@ -369,6 +391,25 @@ function CornerDot({ alert }: { alert: SectionAlert }) {
369
391
  );
370
392
  }
371
393
 
394
+ /**
395
+ * Below this window width the captions down the sides would be narrower than about 15rem
396
+ * (24vw) and her lines a few words each, so the call draws her last line under her face.
397
+ */
398
+ const SIDES_MIN_WIDTH = 1000;
399
+
400
+ /** Whether the window is at least `px` wide, kept current; a server render counts as wide. */
401
+ function useWide(px: number) {
402
+ return useSyncExternalStore(
403
+ (listener) => {
404
+ const query = window.matchMedia(`(min-width: ${px}px)`);
405
+ query.addEventListener("change", listener);
406
+ return () => query.removeEventListener("change", listener);
407
+ },
408
+ () => window.matchMedia(`(min-width: ${px}px)`).matches,
409
+ () => true,
410
+ );
411
+ }
412
+
372
413
  function SettingsCorner() {
373
414
  const alerts = useSectionAlerts();
374
415
  // The gear opens everything the three buttons do not, so it carries their reports
@@ -719,7 +760,7 @@ function Activity({ tool }: { tool: ActivityLine }) {
719
760
  ) : tool.done ? (
720
761
  <span className={cn(look, "text-muted-foreground")}>{text}</span>
721
762
  ) : (
722
- <ShinyText text={text} motion="pulse" className={look} />
763
+ <ShinyText text={text} className={look} />
723
764
  )}
724
765
  </span>
725
766
  );
@@ -875,13 +916,16 @@ function useHeldThought(
875
916
 
876
917
  function ActivityRow({
877
918
  tool,
919
+ toolUp,
878
920
  thinkingSince,
879
921
  thinkingTitle,
880
922
  listening,
881
923
  getMicSpectrum,
882
924
  }: {
883
- /** The line to draw now (useDwell). */
925
+ /** The line to draw here now (useDwell); none while the lines stand beside her face. */
884
926
  tool: ActivityLine | null;
927
+ /** A line is up, here or on her side: it is the same stretch of work, said more exactly. */
928
+ toolUp: boolean;
885
929
  thinkingSince: number | null;
886
930
  thinkingTitle: string | null;
887
931
  listening: boolean;
@@ -905,7 +949,7 @@ function ActivityRow({
905
949
  {thought && (
906
950
  <Fade
907
951
  at="col-start-1 row-start-1"
908
- shown={thinkingSince !== null && !tool}
952
+ shown={thinkingSince !== null && !toolUp}
909
953
  >
910
954
  <Thinking title={title} />
911
955
  </Fade>
@@ -967,27 +1011,51 @@ function useWork(drawn: ActivityLine | null, thinking: boolean) {
967
1011
  return { lines, on, held };
968
1012
  }
969
1013
 
1014
+ /**
1015
+ * The lines of work behind each of her turns, by turn: whatever ran while her latest turn
1016
+ * was hers — before she answered, while she spoke — is that turn's. A turn is theirs until
1017
+ * she speaks, so work before her answer waits for it. Lines are merged by the call they
1018
+ * came from, so a second stretch on one turn adds to the first. A new call starts empty.
1019
+ */
1020
+ function useKeptWork(talk: Turn[], lines: ActivityLine[]) {
1021
+ const [kept, setKept] = useState<Record<string, ActivityLine[]>>({});
1022
+ const last = talk.at(-1);
1023
+ const owner = last?.role === "assistant" ? last.id : null;
1024
+ useEffect(() => {
1025
+ if (!owner || !lines.length) return;
1026
+ setKept((was) => {
1027
+ const now = new Set(lines.map(lineKey));
1028
+ const merged = [
1029
+ ...(was[owner] ?? []).filter((line) => !now.has(lineKey(line))),
1030
+ ...lines,
1031
+ ];
1032
+ const same =
1033
+ was[owner]?.length === merged.length &&
1034
+ merged.every((line, k) => line === was[owner]?.[k]);
1035
+ return same ? was : { ...was, [owner]: merged };
1036
+ });
1037
+ }, [owner, lines]);
1038
+ useEffect(() => {
1039
+ if (!talk.length) setKept({});
1040
+ }, [talk.length]);
1041
+ return kept;
1042
+ }
1043
+
970
1044
  /**
971
1045
  * What she is doing, on her side of the captions and under her words: the last few lines
972
- * of work, older ones fainter, and what the backend is thinking about under them. No rule
973
- * and no plate — the glyphs at the head of the lines are what set them apart from her words.
1046
+ * of work, older ones fainter. No rule and no plate — the glyphs at the head of the lines
1047
+ * are what set them apart from her words. What she is thinking about stays under her face.
974
1048
  */
975
1049
  function WorkStack({
976
1050
  lines,
977
1051
  shown,
978
- thinkingSince,
979
- thinkingTitle,
980
1052
  }: {
981
1053
  lines: ActivityLine[];
982
1054
  /** The stretch of work is still open; false is the fade on its way out. */
983
1055
  shown: boolean;
984
- thinkingSince: number | null;
985
- thinkingTitle: string | null;
986
1056
  }) {
987
- const { thought, title } = useHeldThought(thinkingSince, thinkingTitle);
988
1057
  const leaving = Math.max(0, lines.length - WORK_LINES);
989
1058
  const rows = lines.length - leaving;
990
- const newest = lines.at(-1);
991
1059
  return (
992
1060
  <div
993
1061
  aria-hidden={!shown}
@@ -995,7 +1063,7 @@ function WorkStack({
995
1063
  "pointer-events-auto relative w-[min(100%,16.5rem)] transition-opacity duration-300 ease-out",
996
1064
  !shown && "pointer-events-none opacity-0",
997
1065
  )}
998
- style={{ height: (rows + 1) * WORK_ROW }}
1066
+ style={{ height: rows * WORK_ROW }}
999
1067
  >
1000
1068
  {lines.map((line, k) => {
1001
1069
  const row = k - leaving;
@@ -1016,23 +1084,6 @@ function WorkStack({
1016
1084
  </div>
1017
1085
  );
1018
1086
  })}
1019
- {thought && (
1020
- <div
1021
- style={{
1022
- transform: `translateY(${rows * WORK_ROW}px)`,
1023
- height: WORK_ROW,
1024
- }}
1025
- className="absolute inset-x-0 top-0 flex items-center transition-transform duration-300 ease-out motion-reduce:transition-none"
1026
- >
1027
- {/* A tool wins over thinking: it is the same stretch of work, said more exactly */}
1028
- <Fade
1029
- at={cn("min-w-0", rows > 0 && "pl-6")}
1030
- shown={thinkingSince !== null && (!newest || newest.done)}
1031
- >
1032
- <Thinking title={title} />
1033
- </Fade>
1034
- </div>
1035
- )}
1036
1087
  </div>
1037
1088
  );
1038
1089
  }
@@ -1072,10 +1123,13 @@ const KEY_CAP =
1072
1123
  */
1073
1124
  function Incoming({
1074
1125
  ringing,
1126
+ wakePhrase,
1075
1127
  onAnswer,
1076
1128
  onDecline,
1077
1129
  }: {
1078
1130
  ringing: Ringing;
1131
+ /** Saying it answers as the button does (use-thursday `call`); null while it is off. */
1132
+ wakePhrase: string | null;
1079
1133
  onAnswer: () => void;
1080
1134
  onDecline: () => void;
1081
1135
  }) {
@@ -1144,6 +1198,11 @@ function Incoming({
1144
1198
  >
1145
1199
  Call back
1146
1200
  </Button>
1201
+ {wakePhrase && (
1202
+ <span className="mt-3 flex items-center gap-1.5 font-mono text-[11px] text-muted-foreground/70">
1203
+ or say <SaidPhrase phrase={wakePhrase} />
1204
+ </span>
1205
+ )}
1147
1206
  </div>
1148
1207
  );
1149
1208
  }
@@ -1198,18 +1257,35 @@ function Incoming({
1198
1257
  )}
1199
1258
  </span>
1200
1259
  )}
1201
- <button
1202
- type="button"
1203
- onClick={onDecline}
1204
- className="mt-1 flex items-center gap-2 rounded-md font-mono text-[11px] text-muted-foreground/70 outline-none hover:text-foreground focus-visible:ring-3 focus-visible:ring-ring/50"
1205
- >
1206
- <kbd className={KEY_CAP}>Esc</kbd>
1207
- not now
1208
- </button>
1260
+ <span className="mt-1 flex items-center gap-3 font-mono text-[11px] text-muted-foreground/70">
1261
+ {wakePhrase && (
1262
+ <span className="flex items-center gap-1.5">
1263
+ say <SaidPhrase phrase={wakePhrase} /> to answer
1264
+ </span>
1265
+ )}
1266
+ <button
1267
+ type="button"
1268
+ onClick={onDecline}
1269
+ className="flex items-center gap-2 rounded-md outline-none hover:text-foreground focus-visible:ring-3 focus-visible:ring-ring/50"
1270
+ >
1271
+ <kbd className={KEY_CAP}>Esc</kbd>
1272
+ not now
1273
+ </button>
1274
+ </span>
1209
1275
  </div>
1210
1276
  );
1211
1277
  }
1212
1278
 
1279
+ /** The wake phrase as the screen shows it wherever saying it does something. */
1280
+ function SaidPhrase({ phrase }: { phrase: string }) {
1281
+ return (
1282
+ <span className="flex items-center gap-1 rounded-full bg-muted px-2 py-0.5 text-foreground/80">
1283
+ <Mic className="size-3" />
1284
+ {phrase}
1285
+ </span>
1286
+ );
1287
+ }
1288
+
1213
1289
  /** What a waiting thread is doing, in one word. */
1214
1290
  const RUNG_KIND: Record<Rung["kind"], string> = {
1215
1291
  question: "asks",
@@ -1284,7 +1360,6 @@ function Hint({
1284
1360
  body = (
1285
1361
  <ShinyText
1286
1362
  text={status === "connecting" ? "Connecting…" : "Ending…"}
1287
- motion="pulse"
1288
1363
  className="text-muted-foreground/70"
1289
1364
  />
1290
1365
  );
@@ -1324,10 +1399,7 @@ function Hint({
1324
1399
  body = (
1325
1400
  <>
1326
1401
  <span>Tap Thursday, or say</span>
1327
- <span className="flex items-center gap-1 rounded-full bg-muted px-2 py-0.5 text-foreground/80">
1328
- <Mic className="size-3" />
1329
- {wakePhrase}
1330
- </span>
1402
+ <SaidPhrase phrase={wakePhrase} />
1331
1403
  </>
1332
1404
  );
1333
1405
  } else if (hotkeyLabel) {
@@ -226,7 +226,7 @@ export function VoicePicker({
226
226
  </span>
227
227
  <Check
228
228
  className={cn(
229
- "size-3.5 shrink-0 text-brand",
229
+ "size-3.5 shrink-0",
230
230
  name === voice ? "opacity-100" : "opacity-0",
231
231
  )}
232
232
  />
@@ -316,7 +316,7 @@ export function WriteLine({
316
316
  aria-hidden
317
317
  className="pointer-events-none fixed inset-0 z-20 bg-background/60"
318
318
  >
319
- <div className="absolute inset-3 rounded-3xl border-[1.5px] border-dashed border-brand/55" />
319
+ <div className="absolute inset-3 rounded-3xl border-[1.5px] border-dashed border-foreground/30" />
320
320
  </div>
321
321
  )}
322
322
  <div
@@ -337,7 +337,7 @@ export function WriteLine({
337
337
  className="px-0.5 pt-0.5"
338
338
  >
339
339
  {dragging && (
340
- <span className="flex h-13 items-center gap-2 rounded-[14px] border-[1.5px] border-dashed border-brand px-4 text-[13px] text-brand">
340
+ <span className="flex h-13 items-center gap-2 rounded-[14px] border-[1.5px] border-dashed border-foreground/60 px-4 text-[13px]">
341
341
  <ArrowDownToLine className="size-4" />
342
342
  Let go — it waits here
343
343
  </span>
@@ -32,6 +32,14 @@ export type OpenWork = {
32
32
  show: ActivityLine;
33
33
  };
34
34
 
35
+ /** What a bot's message to the call does, as its line on the call screen says it after the bot's name. */
36
+ const RELAY_SAYS: Record<Thread["room"]["relays"][number]["kind"], string> = {
37
+ message: "says",
38
+ question: "asks",
39
+ report: "reports",
40
+ interrupted: "was cut off",
41
+ };
42
+
35
43
  const OPEN_RANK = {
36
44
  question: 0,
37
45
  stopped: 1,
@@ -95,7 +103,7 @@ export function openWork(threads: Thread[]): OpenWork[] {
95
103
  relayIds: relays
96
104
  .filter((relay) => relay.messageId === question.id)
97
105
  .map((relay) => relay.id),
98
- show: show(question.bot, `${question.bot}: question`),
106
+ show: show(question.bot, `${question.bot} asks`),
99
107
  });
100
108
  }
101
109
 
@@ -127,7 +135,7 @@ export function openWork(threads: Thread[]): OpenWork[] {
127
135
  thread.bot,
128
136
  kind === "stopped"
129
137
  ? `${thread.bot} stopped`
130
- : `Answer from ${thread.bot}`,
138
+ : `${thread.bot} finished`,
131
139
  ),
132
140
  });
133
141
  continue;
@@ -140,7 +148,7 @@ export function openWork(threads: Thread[]): OpenWork[] {
140
148
  kind: "progress",
141
149
  line: `${bracket(relay.bot, relay.kind)}\n${spoken(relay.text)}`,
142
150
  relayIds: [relay.id],
143
- show: show(relay.bot, `${relay.bot}: ${relay.kind}`),
151
+ show: show(relay.bot, `${relay.bot} ${RELAY_SAYS[relay.kind]}`),
144
152
  });
145
153
  }
146
154
  }
@@ -1,4 +1,5 @@
1
1
  import { TOOL_NAMES } from "@/features/ai/tools/tool-name";
2
+ import type { Thread } from "@/features/bot/bot.schema";
2
3
 
3
4
  // Human-readable activity lines for tool calls on the call screen. The model
4
5
  // never reads these. Unknown names resolve to null and the screen shows the name.
@@ -6,13 +7,36 @@ const LINES: Record<string, string> = {
6
7
  [TOOL_NAMES.memory_recall]: "Checking your notes",
7
8
  [TOOL_NAMES.memory_remember]: "Noting that down",
8
9
  [TOOL_NAMES.memory_forget]: "Forgetting that",
10
+ [TOOL_NAMES.memory_conversation]: "Reading back an earlier call",
9
11
  [TOOL_NAMES.bash]: "Doing it on this computer",
10
12
  [TOOL_NAMES.web_search]: "Searching the web",
11
13
  [TOOL_NAMES.load_skill]: "Reading how to do this",
12
14
  [TOOL_NAMES.look_at]: "Looking at the picture",
13
15
  [TOOL_NAMES.end_call]: "Ending the call",
16
+ [TOOL_NAMES.routine]: "Checking your routines",
14
17
  };
15
18
 
19
+ /** When a routine being made starts, as the call's arguments say it: "once at 19:10", "every 6 hours". */
20
+ function routineWhen(args: Record<string, unknown>): string | null {
21
+ if (typeof args.at === "string" && args.at.length >= 16) {
22
+ const [day, time] = args.at.split(" ");
23
+ const [year, month, date] = day.split("-").map(Number);
24
+ const on = new Date(year, month - 1, date);
25
+ if (on.toDateString() === new Date().toDateString())
26
+ return `once at ${time}`;
27
+ const named = on.toLocaleDateString("en-US", {
28
+ month: "short",
29
+ day: "numeric",
30
+ });
31
+ return `once, ${named} ${time}`;
32
+ }
33
+ if (typeof args.time === "string" && args.time)
34
+ return `daily at ${args.time}`;
35
+ if (typeof args.everyHours === "number")
36
+ return `every ${args.everyHours} hours`;
37
+ return null;
38
+ }
39
+
16
40
  /**
17
41
  * How much of a fact the line carries. What is being written is the one thing
18
42
  * on this screen the user cannot check afterwards without opening the note, so
@@ -48,7 +72,11 @@ function firstFact(
48
72
  }
49
73
 
50
74
  /** Lines that need the arguments; arguments may still be streaming in. */
51
- function fromArgs(name: string, args: Record<string, unknown>): string | null {
75
+ function fromArgs(
76
+ name: string,
77
+ args: Record<string, unknown>,
78
+ bot: string | null,
79
+ ): string | null {
52
80
  if (name === TOOL_NAMES.memory_remember) {
53
81
  const path = typeof args.path === "string" ? args.path.trim() : "";
54
82
  const fact = firstFact(args);
@@ -65,10 +93,26 @@ function fromArgs(name: string, args: Record<string, unknown>): string | null {
65
93
  const query = typeof args.query === "string" ? args.query.trim() : "";
66
94
  return query ? `Searching · ${query}` : null;
67
95
  }
96
+ if (name === TOOL_NAMES.routine) {
97
+ if (args.action === "create") {
98
+ const bot = typeof args.bot === "string" ? args.bot.trim() : "";
99
+ const when = routineWhen(args);
100
+ const what = [bot, when].filter(Boolean).join(", ");
101
+ return what ? `Setting a routine · ${what}` : "Setting a routine";
102
+ }
103
+ if (args.action === "change") return "Changing a routine";
104
+ if (args.action === "delete") return "Deleting a routine";
105
+ return null;
106
+ }
68
107
  const label = typeof args.thread === "string" ? args.thread.trim() : "";
69
- if (name === TOOL_NAMES.thread_tell || name === TOOL_NAMES.thread_answer)
70
- return "Passing that on";
71
- if (name === TOOL_NAMES.thread_cancel) return "Stopping that";
108
+ if (name === TOOL_NAMES.thread_tell)
109
+ return bot ? `Telling ${bot}` : "Passing that on";
110
+ if (name === TOOL_NAMES.thread_answer)
111
+ return bot ? `Answering ${bot}` : "Answering that";
112
+ if (name === TOOL_NAMES.thread_cancel)
113
+ return label ? `Stopping ${label}` : "Stopping that";
114
+ if (name === TOOL_NAMES.thread_seen)
115
+ return label ? `Marking ${label} as read` : "Marking that as read";
72
116
  if (name === TOOL_NAMES.thread_show)
73
117
  return label ? `Opening ${label}` : "Opening that";
74
118
  if (name === TOOL_NAMES.thread_status)
@@ -99,22 +143,60 @@ function parseArgs(args?: string): Record<string, unknown> | null {
99
143
  return null;
100
144
  }
101
145
 
102
- export function toolLine(name: string, args?: string): string | null {
146
+ /** The line for a tool call; `bot` is who it reaches (`toolBot`), for the lines that name them. */
147
+ export function toolLine(
148
+ name: string,
149
+ args?: string,
150
+ bot: string | null = null,
151
+ ): string | null {
103
152
  if (name === TOOL_NAMES.tool_search || name === TOOL_NAMES.tool_call) {
104
153
  return "Reaching a connected service";
105
154
  }
106
155
  const parsed = parseArgs(args);
107
- return (parsed && fromArgs(name, parsed)) ?? LINES[name] ?? fromServer(name);
156
+ return (
157
+ (parsed && fromArgs(name, parsed, bot)) ?? LINES[name] ?? fromServer(name)
158
+ );
108
159
  }
109
160
 
161
+ /** The call's tools that act on one thread the model names by its label or id. */
162
+ const ON_A_THREAD = new Set<string>([
163
+ TOOL_NAMES.thread_tell,
164
+ TOOL_NAMES.thread_answer,
165
+ TOOL_NAMES.thread_status,
166
+ TOOL_NAMES.thread_show,
167
+ TOOL_NAMES.thread_cancel,
168
+ TOOL_NAMES.thread_seen,
169
+ ]);
170
+
110
171
  /**
111
- * The bot a call hands work to, when it names one. The row draws that bot's own
112
- * face instead of a glyph: who it went to is a face everywhere else in the app.
172
+ * The bot a call's tool reaches, so its row wears that bot's face rather than a glyph —
173
+ * who work went to is a face everywhere else in the app. The one work is handed to, else
174
+ * the bot of the thread the tool names, found as the server finds a thread (thread.query
175
+ * resolveThread): its id, else its label. An answer goes to the bot that asked; "all", and
176
+ * a thread the page does not hold, reach nobody in particular.
113
177
  */
114
- export function toolBot(name: string, args?: string): string | null {
115
- if (name !== TOOL_NAMES.thread_start) return null;
116
- const bot = parseArgs(args)?.bot;
117
- return typeof bot === "string" && bot.trim() ? bot.trim() : null;
178
+ export function toolBot(
179
+ name: string,
180
+ args?: string,
181
+ threads?: Thread[],
182
+ ): string | null {
183
+ const parsed = parseArgs(args);
184
+ if (!parsed) return null;
185
+ if (name === TOOL_NAMES.thread_start)
186
+ return typeof parsed.bot === "string" && parsed.bot.trim()
187
+ ? parsed.bot.trim()
188
+ : null;
189
+ if (!ON_A_THREAD.has(name) || !threads) return null;
190
+ const ref = typeof parsed.thread === "string" ? parsed.thread.trim() : "";
191
+ if (!ref || ref.toLowerCase() === "all") return null;
192
+ const lower = ref.toLowerCase();
193
+ const thread =
194
+ threads.find((one) => one.id === ref) ??
195
+ threads.find((one) => one.label.toLowerCase() === lower);
196
+ if (!thread) return null;
197
+ return name === TOOL_NAMES.thread_answer
198
+ ? (thread.room.questions[0]?.bot ?? thread.bot)
199
+ : thread.bot;
118
200
  }
119
201
 
120
202
  /**
@@ -285,11 +285,12 @@ export function useTextCall(): TextCall {
285
285
  ? searchSourcesOf(JSON.stringify(lastTool.output ?? null))
286
286
  : [];
287
287
  if (last?.type === "text" && !sources.length) return null;
288
+ const bot = toolBot(name, args, inbox.current);
288
289
  return {
289
290
  id: lastTool.toolCallId,
290
291
  name,
291
- line: toolLine(name, args),
292
- bot: toolBot(name, args),
292
+ line: toolLine(name, args, bot),
293
+ bot,
293
294
  done,
294
295
  ...(sources.length ? { sources } : {}),
295
296
  };
@@ -16,6 +16,7 @@ import { acceptThreadRelaysAction } from "@/features/bot/bot.action";
16
16
  import type { Bot, Thread } from "@/features/bot/bot.schema";
17
17
  import { botThreads, screenActs } from "@/features/bot/thread.store";
18
18
  import { runRemoteTool } from "@/features/thursday/tool-call";
19
+ import { askToNotify } from "@/features/workspace/components/artifact-view";
19
20
  import { isCombo, useHotkey } from "@/hooks/use-hotkey";
20
21
  import { useWakeWord } from "@/hooks/use-wake-word";
21
22
  import type { LiveClose } from "@/lib/live/live.schema";
@@ -256,11 +257,12 @@ export function useThursday(
256
257
  const showTool = useCallback((call: LiveToolCall) => {
257
258
  if (linger.current) clearTimeout(linger.current);
258
259
  relayOpen.current = false;
260
+ const bot = toolBot(call.name, call.arguments, latest.current);
259
261
  setTool({
260
262
  id: call.id,
261
263
  name: call.name,
262
- line: toolLine(call.name, call.arguments),
263
- bot: toolBot(call.name, call.arguments),
264
+ line: toolLine(call.name, call.arguments, bot),
265
+ bot,
264
266
  done: false,
265
267
  });
266
268
  }, []);
@@ -708,6 +710,7 @@ export function useThursday(
708
710
  // with a line open this press hangs up; `calling` covers the gap before re-render
709
711
  if (calling.current || status !== "idle") return hangUp();
710
712
  answered();
713
+ askToNotify();
711
714
 
712
715
  setStatus("connecting");
713
716
  setEnded(null);