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
package/README.md CHANGED
@@ -4,13 +4,13 @@
4
4
 
5
5
  ### Everyone wanted Friday. This is Thursday.
6
6
 
7
- **An open-source voice assistant that runs on your computer, with a team of AI bots behind it.**<br>
8
- You talk. Bots take the slow work to a real browser, a shell and your files. The conversation never stops.
7
+ **An open-source voice assistant on GPT-Live 1 that runs on your computer, with a team of AI bots behind it.**<br>
8
+ You talk. Bots take the slow work to a real browser, a shell and your files, and she keeps talking while they work.
9
9
 
10
10
  [![npm](https://img.shields.io/npm/v/thursday-agent?style=flat-square&color=111&label=npm)](https://www.npmjs.com/package/thursday-agent)
11
11
  [![CI](https://img.shields.io/github/actions/workflow/status/cgoinglove/thursday/ci.yml?style=flat-square&label=ci)](https://github.com/cgoinglove/thursday/actions/workflows/ci.yml)
12
12
  [![MIT](https://img.shields.io/badge/license-MIT-111?style=flat-square)](LICENSE)
13
- [![stars](https://img.shields.io/github/stars/cgoinglove/thursday?style=flat-square&color=111)](https://github.com/cgoinglove/thursday/stargazers)
13
+ [![node](https://img.shields.io/node/v/thursday-agent?style=flat-square&color=111)](https://nodejs.org)
14
14
 
15
15
  [English](README.md) · [한국어](README.ko.md)
16
16
 
@@ -22,41 +22,58 @@ You talk. Bots take the slow work to a real browser, a shell and your files. The
22
22
  npx thursday-agent
23
23
  ```
24
24
 
25
- Paste an OpenAI API key, pick your starter bots, and say **“hey thursday.”** No account, no `.env`.
25
+ Needs Node.js 22.18+ and one OpenAI API key, pasted into the first screen. Pick your starter bots and **tap her face**. No account, no `.env`. The voice costs about $0.05 a minute on that key.
26
+
27
+ ![The first run: a key, the starter bots, and one button that places the first call](docs/images/first-run.png)
28
+
29
+ ## Try saying
30
+
31
+ - “Find me a coat under two hundred dollars and put the options on one page.”
32
+ - “Every weekday at nine, go through my mail and draft the replies.”
33
+ - “Remember my sister's birthday is March third.”
34
+ - “Where is that coat job?”, while it is still running.
26
35
 
27
36
  ## Talk while it works
28
37
 
29
- Most agents make you type and wait. Thursday is a real-time voice call you can interrupt. Anything that takes more than a few seconds goes to a bot in the background, so the call never goes quiet, and she tells you when it's back.
38
+ A speech model that opens a browser goes silent for a minute, and a silent call is a dead call. So Thursday splits the call in two: GPT-Live 1 holds the conversation, and anything slower than a few seconds goes to a bot in the background. Interrupt her, change the subject, or ask how the job is going.
30
39
 
31
- ![A voice call with Thursday while a bot looks for hotels in the background](docs/images/call.png)
40
+ ![Asked how a job is going, she answers with the step the bot is on, while the call goes on](docs/images/call.png)
32
41
 
33
42
  ## One sentence, a whole team
34
43
 
35
44
  Bots hand parts of a job to each other, check what comes back, and ask you only when a decision is yours. Every handoff is saved: open the thread to see who did what, or step in.
36
45
 
37
- ![A planner bot splits a trip request between two bots and one asks the user a question](docs/images/room.png)
46
+ ![Bots split a request, trade findings, ask the user one question, and each message becomes part of the finished page](docs/images/team.png)
38
47
 
39
48
  ## Errands, in a real browser
40
49
 
41
- Orders, bookings, forms, the inbox. Bots use their own browser or the Chrome you're already signed into, and ask before using a login you gave them. A purchase stops at the Pay button, left open on your screen.
50
+ Orders, bookings, forms, the inbox. Bots use their own browser or the Chrome you're already signed into. When a site needs you, the bot opens a window, you sign in yourself, and the app keeps that sign-in for the bots you allow. A purchase stops at the Pay button, left open on your screen.
42
51
 
43
- ![A checkout a bot filled in and left open, with the Pay button for the user to press](docs/images/errands.png)
52
+ ![A checkout a bot filled in and left open, with the Pay button left for the user to press](docs/images/errands.png)
44
53
 
45
54
  ## Results you can keep
46
55
 
47
56
  Pages, charts, videos, slides, docs and scripts, saved as files on your machine. A finished one shows up in the corner of your screen, and “show me” opens it.
48
57
 
49
- ![Files made by six different bots: a rent chart, a short video, a carousel, a trip page, launch copy, and a script](docs/images/artifacts.png)
58
+ ![A finished page waiting in the corner of the screen while she says the job is back](docs/images/results.png)
50
59
 
51
60
  ## Your crew
52
61
 
53
62
  Pick starter bots on the first run, or make your own with a name and one sentence about what it's for. Give each its own model and tools.
54
63
 
55
- ![A lineup of bots, each with its own face and job, plus a slot to make your own](docs/images/bots.png)
64
+ ![Picking the starter bots on the first run, each with its own face and job](docs/images/bots.png)
65
+
66
+ ## From your phone
67
+
68
+ Write to her from Telegram, Discord or Slack while the app runs at home. A bot's question arrives as buttons, and the files she names come with her answer. Nothing on your computer is opened to the internet.
69
+
70
+ ![A chat with Thursday on a phone: a page sent back, and a bot's question as two buttons](docs/images/phone.png)
56
71
 
57
72
  ## And also
58
73
 
59
- - **Hang up, it keeps going.** Jobs run on your machine, and Thursday tells you when they finish, or calls you if you turn that on.
74
+ - **Hang up, it keeps going.** Jobs run on your machine, and Thursday tells you when they finish, or rings your screen if you turn that on.
75
+ - **Type instead.** Press `/` and write to her: the same memory and bots, no microphone, nothing billed by the minute.
76
+ - **Routines.** “Every weekday at nine, go through my mail”: a bot, a job and a time, set up by saying it.
60
77
  - **Memory you can read.** What she knows about you is plain notes. Open, edit, or delete any line.
61
78
  - **Skills and MCP.** Teach bots new methods with Agent Skills, and connect any MCP server.
62
79
  - **Any model.** OpenAI, Anthropic, Google, xAI, Vercel AI Gateway, or your ChatGPT sign-in, per bot.
@@ -78,7 +95,7 @@ Thursday is free and MIT-licensed. You bring the keys: OpenAI bills the voice at
78
95
  <details>
79
96
  <summary><b>Where does my data go?</b></summary>
80
97
 
81
- What you say on a call and what bots work on go to the model providers you set up and the services you connect. “Hey thursday” uses your browser's speech recognition, which in Chrome sends microphone audio to Google while the app is open and no call is; switch it off in Settings › Thursday and start calls with a tap or `alt+shift+T`. The app listens only on `127.0.0.1`, and your calls, memory and files live in `~/.thursday` (or the checkout, when you run from source).
98
+ What you say on a call and what bots work on go to the model providers you set up and the services you connect. The “hey thursday” wake phrase is off until you switch it on: it uses your browser's speech recognition, which in Chrome sends microphone audio to Google while the tab is open. A tap on her face or `alt+shift+T` starts a call without it. The app listens only on `127.0.0.1`, and your calls, memory and files live in `~/.thursday` (or the checkout, when you run from source).
82
99
 
83
100
  </details>
84
101
 
@@ -109,6 +126,6 @@ Requires pnpm 10+.
109
126
 
110
127
  **[How it works](docs/how-it-works.md)** · [Contributing](CONTRIBUTING.md) · [Security](SECURITY.md) · [MIT](LICENSE)
111
128
 
112
- If you'd rather say it than type it, [star Thursday](https://github.com/cgoinglove/thursday).
129
+ If you'd rather say it than type it, [star Thursday](https://github.com/cgoinglove/thursday). Ran it? [Tell me where it stopped](https://github.com/cgoinglove/thursday/issues/new): that list is the roadmap.
113
130
 
114
131
  </div>
package/bin/port.mjs CHANGED
@@ -6,8 +6,13 @@ import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
6
6
  import { createServer } from "node:net";
7
7
  import { join } from "node:path";
8
8
 
9
- /** The port when none is asked for. */
10
- const DEFAULT_PORT = 3000;
9
+ /**
10
+ * The port when none is asked for, and so the address a first run makes home. Not 3000,
11
+ * the most occupied port on a developer's machine: the browser keeps the installed app,
12
+ * her settings and the microphone permission per address, so home is best a port nothing
13
+ * else comes to want later.
14
+ */
15
+ const DEFAULT_PORT = 4747;
11
16
  /** How far past it to look before giving up. */
12
17
  const SEARCH = 20;
13
18
 
@@ -15,7 +20,7 @@ const SEARCH = 20;
15
20
  * Every address a holder of the port may have bound. No single probe sees them
16
21
  * all: on macOS a socket on 127.0.0.1 does not block `::`, and one on `::` does
17
22
  * not block 127.0.0.1 — yet `localhost` reaches whichever answers first, so two
18
- * servers "on 3000" each serve some of the page. Probed one at a time, since
23
+ * servers "on one port" each serve some of the page. Probed one at a time, since
19
24
  * `0.0.0.0` and `::` block each other.
20
25
  */
21
26
  const ADDRESSES = ["127.0.0.1", "::1", "0.0.0.0", "::"];
@@ -66,10 +71,9 @@ const keepPort = (home, port) => {
66
71
  };
67
72
 
68
73
  /**
69
- * 3000 is the most occupied port on a developer's machine. A port nobody asked
70
- * for is ours to move; a port that was asked for (`--port`, `PORT`) is not, and
71
- * saying so is more use than moving it quietly. Exits with the reason when no
72
- * port will do.
74
+ * A port nobody asked for is ours to move; a port that was asked for (`--port`,
75
+ * `PORT`) is not, and saying so is more use than moving it quietly. Exits with
76
+ * the reason when no port will do.
73
77
  */
74
78
  export async function freePort(asked, home) {
75
79
  const port = await pickPort(asked, home);
package/bin/thursday.mjs CHANGED
@@ -35,7 +35,7 @@ if (has("-h", "--help")) {
35
35
  $ thursday [options]
36
36
 
37
37
  Options
38
- --port <n> Port to serve on (default 3000, or the next free one)
38
+ --port <n> Port to serve on (default 4747, or the next free one)
39
39
  --home <dir> Where your data lives (default ~/.thursday)
40
40
  --no-open Do not open a browser
41
41
  -v, --version Print the version
@@ -19,7 +19,7 @@ function RadioGroupItem({ className, ...props }: RadioPrimitive.Root.Props) {
19
19
  <RadioPrimitive.Root
20
20
  data-slot="radio-group-item"
21
21
  className={cn(
22
- "group/radio-group-item peer relative flex aspect-square size-4 shrink-0 rounded-full border border-input outline-none group-has-[:focus-visible]/field-label:ring-0 group-has-[:focus-visible]/field-label:not-data-checked:border-input after:absolute after:-inset-x-3 after:-inset-y-2 focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 aria-invalid:aria-checked:border-brand dark:bg-input/30 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 data-checked:border-brand data-checked:bg-brand data-checked:text-brand-foreground group-has-[:focus-visible]/field-label:data-checked:border-brand dark:data-checked:bg-brand",
22
+ "group/radio-group-item peer relative flex aspect-square size-4 shrink-0 rounded-full border border-input outline-none group-has-[:focus-visible]/field-label:ring-0 group-has-[:focus-visible]/field-label:not-data-checked:border-input after:absolute after:-inset-x-3 after:-inset-y-2 focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 aria-invalid:aria-checked:border-primary dark:bg-input/30 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 data-checked:border-primary data-checked:bg-primary data-checked:text-primary-foreground group-has-[:focus-visible]/field-label:data-checked:border-primary dark:data-checked:bg-primary",
23
23
  className,
24
24
  )}
25
25
  {...props}
@@ -28,7 +28,7 @@ function RadioGroupItem({ className, ...props }: RadioPrimitive.Root.Props) {
28
28
  data-slot="radio-group-indicator"
29
29
  className="flex size-4 items-center justify-center"
30
30
  >
31
- <span className="absolute top-1/2 left-1/2 size-2 -translate-x-1/2 -translate-y-1/2 rounded-full bg-brand-foreground" />
31
+ <span className="absolute top-1/2 left-1/2 size-2 -translate-x-1/2 -translate-y-1/2 rounded-full bg-primary-foreground" />
32
32
  </RadioPrimitive.Indicator>
33
33
  </RadioPrimitive.Root>
34
34
  );
@@ -1,20 +1,18 @@
1
1
  import type { CSSProperties } from "react";
2
- import { cn, WAITING_INK } from "@/lib/utils";
2
+ import { cn } from "@/lib/utils";
3
3
 
4
4
  /**
5
- * The resting ink, the ink under a passing band, and the band. On a light page
5
+ * The ink under a passing band, and the band. On a light page
6
6
  * the band moves toward the ink: a lighter band on light ground reads as the
7
7
  * words fading out. A dark band over the full muted ink barely shows there, so
8
8
  * the words under it rest lighter on a light page than they do still.
9
9
  */
10
10
  const TONES = {
11
11
  muted: {
12
- rest: "text-muted-foreground",
13
12
  under: "text-muted-foreground/60 dark:text-muted-foreground",
14
13
  band: "via-foreground",
15
14
  },
16
15
  waiting: {
17
- rest: WAITING_INK,
18
16
  under: "text-amber-700/60 dark:text-amber-400",
19
17
  band: "via-amber-950 dark:via-amber-100",
20
18
  },
@@ -34,33 +32,16 @@ export type ShinyTone = keyof typeof TONES;
34
32
  export function ShinyText({
35
33
  text,
36
34
  tone = "muted",
37
- motion = "shine",
38
35
  speed = 2.4,
39
36
  className,
40
37
  }: {
41
38
  text: string;
42
39
  tone?: ShinyTone;
43
- /** How it says it is still moving: a band crossing the words, or the words easing out and back. */
44
- motion?: "shine" | "pulse";
45
40
  /** Seconds for the band to cross once. */
46
41
  speed?: number;
47
42
  className?: string;
48
43
  }) {
49
- const { rest, under, band } = TONES[tone];
50
- if (motion === "pulse") {
51
- return (
52
- <span
53
- data-slot="shiny-text"
54
- className={cn(
55
- "inline-block max-w-full animate-pulse motion-reduce:animate-none",
56
- rest,
57
- className,
58
- )}
59
- >
60
- {text}
61
- </span>
62
- );
63
- }
44
+ const { under, band } = TONES[tone];
64
45
  return (
65
46
  <span
66
47
  data-slot="shiny-text"
@@ -12,10 +12,15 @@ A call runs on two models: **GPT-Live 1** holds the conversation, and a Response
12
12
  - **Everything else goes to a bot.** She hands it over and keeps talking. A speech model that ran a browser itself would go silent for minutes.
13
13
  - **When a job comes back,** she tells you in a sentence. A file it made shows up in the corner of your screen, and a question it asks shows up as buttons.
14
14
 
15
- Settings › Thursday picks the voice, the backend model and its reasoning, web search, the wake word and hotkey, and whether she calls you when a job ends.
15
+ Settings › Thursday picks the voice, the backend model and its reasoning, web search, the wake word (off until you switch it on) and hotkey, and whether she rings your screen when a job ends.
16
16
 
17
17
  The voice is billed per active minute, silence included; the backend is billed per token. Both use your OpenAI API key, not a ChatGPT subscription.
18
18
 
19
+ ## Without your voice
20
+
21
+ - **Writing.** Press `/`, or the + on the pill, and type: a call in writing, with the same memory, tools and bots, no microphone, and nothing billed by the minute. It runs on a ChatGPT sign-in when you have one, else on your OpenAI key, and you can pick another model for it.
22
+ - **From your phone.** Connect Telegram, Discord or Slack in Settings › Phone and write to her there while the app runs on your computer. The app connects out to the chat service; nothing on your computer is opened to the internet. One person is let in per service, and you allow them on the computer's screen.
23
+
19
24
  ## Bots
20
25
 
21
26
  ![A bot gets a shell, a real browser, skills, MCP servers, a studio and its own memory](images/machine.png)
@@ -30,9 +35,11 @@ A bot is a text model from any provider you added, with:
30
35
  - a **studio** for images, video, speech, and transcription
31
36
  - **each other**, to hand off part of a job
32
37
 
38
+ ![A thread open over the call: a planner hands work to another bot, its steps fold into tiles, and a question waits for you](images/room.png)
39
+
33
40
  The first run lets you pick a few starter bots; more are ready in Settings › Bots, or make your own with a name and one sentence about what it is for. That sentence is how Thursday decides who gets a job. Switch a bot off without deleting it.
34
41
 
35
- A job ends in the thing you asked for and a short report. Anything longer than a few lines is a file in `artifacts/`. When only you can do something — sign in, choose between two real options — the bot sets it up, asks, and waits. It never presses Pay: a purchase stops on the last screen, left open for you.
42
+ A job ends in the thing you asked for and a short report. Anything longer than a few lines is a file in `artifacts/`. When only you can do something — sign in, choose between two real options — the bot sets it up, asks, and waits. It is told never to press Pay: a purchase stops on the last screen, left open for you. That is an instruction the model follows, not a lock ([SECURITY.md](../SECURITY.md)).
36
43
 
37
44
  Each bot keeps its own memory as files you can open from its page.
38
45
 
@@ -56,10 +63,11 @@ Open any note in Settings › Memory to see exactly what she knows. To change it
56
63
  ```text
57
64
  ~/.thursday
58
65
  ├── local.db calls, memory, bots, jobs, keys
66
+ ├── .sign-ins/ the sites you signed in to, one file a site
59
67
  └── .ai-workspace
60
68
  ├── artifacts/ finished work, a folder per bot
61
69
  ├── projects/ code and longer-lived projects
62
- ├── bots/ each bot's memory and saved sessions
70
+ ├── bots/ each bot's memory and its own kit
63
71
  └── .agents/ skills you installed
64
72
  ```
65
73
 
Binary file
Binary file
Binary file
@@ -293,7 +293,7 @@ function ModelRow({
293
293
  <span className={cn("truncate text-[13px]", picked && "font-medium")}>
294
294
  {model.label}
295
295
  </span>
296
- {picked && <Check className="size-3.5 shrink-0 text-brand" />}
296
+ {picked && <Check className="size-3.5 shrink-0" />}
297
297
  {model.retiring && (
298
298
  <span className="shrink-0 font-mono text-[10px] text-muted-foreground">
299
299
  retiring
@@ -237,7 +237,7 @@ export function ModelPicker({
237
237
  {entry.id}
238
238
  </span>
239
239
  </span>
240
- {on && <Check className="size-3.5 shrink-0 text-brand" />}
240
+ {on && <Check className="size-3.5 shrink-0" />}
241
241
  </button>
242
242
  );
243
243
  })}
@@ -6,11 +6,12 @@ import { APP_DIR, APP_NAME, DATA_DIR, PATHS } from "@/config";
6
6
  /**
7
7
  * The app's guide for the person using it (`guide/`), which Thursday reads when an
8
8
  * answer depends on how the app works. Everything the code knows about it is in this
9
- * file. Four places reach it and nothing else may: boot installs it
9
+ * file. Five places reach it and nothing else may: boot installs it
10
10
  * (instrumentation-node), the call's backend prompt carries `guideLine`
11
- * (prompts/thursday.prompt), scripts/pack ships the folder, and one assertion in
12
- * scripts/live-session.test checks the line. Removing the feature is this file, those
13
- * four, the folder, and its lines in AGENTS.md.
11
+ * (prompts/thursday.prompt), a bot's carries `botGuideLine` (prompts/bot.prompt),
12
+ * scripts/pack ships the folder, and one assertion in scripts/live-session.test checks
13
+ * the line. Removing the feature is this file, those five, the folder, and its lines in
14
+ * AGENTS.md.
14
15
  */
15
16
 
16
17
  /** Ships with the app. */
@@ -37,3 +38,7 @@ export async function installGuide(): Promise<void> {
37
38
  /** What the call's backend is told about it, under its This computer chapter. */
38
39
  export const guideLine = (): string =>
39
40
  `How ${APP_NAME} works for the person using it — its screens, its settings, what it connects to, what to do when something stops — is written under \`${FOLDER}/\` here, \`index.md\` first. Read it whenever an answer depends on how the app works: what can be asked for, where something is changed, a wish that needs a setting switched on, something that stopped or was refused. Answer from it rather than from what you assume.`;
41
+
42
+ /** What a bot is told about it: that it is there, for the rare job that is about the app. */
43
+ export const botGuideLine = (): string =>
44
+ `How ${APP_NAME} itself works for the user — its screens and settings — is written under \`${FOLDER}/\` here, \`index.md\` first; read it only when the job is about the app.`;
@@ -7,6 +7,7 @@ import {
7
7
  PROMPT_LINE,
8
8
  WORKSPACE_KEEP,
9
9
  } from "@/config";
10
+ import { botGuideLine } from "@/features/ai/guide";
10
11
  import { TOOL_NAMES } from "@/features/ai/tools/tool-name";
11
12
  import { botMemoryFolder, listBotMemory } from "@/features/bot/bot.memory";
12
13
  import { listJobBots, readBotMemoryOn } from "@/features/bot/bot.query";
@@ -297,7 +298,9 @@ Your workspace, where \`${TOOL_NAMES.bash}\` runs. Everything you write goes in
297
298
  - \`${folders?.scratch ?? PATHS.scratch}/\` — this job's working material, shared by every bot on it; cleared ${Math.round(WORKSPACE_KEEP.forMs / 86_400_000)} days after the job ends.
298
299
  - \`${folders?.own ?? PATHS.bots}/\` — yours across every job you run here: what you keep for next time.
299
300
 
300
- Inside the workspace, set up whatever the job needs yourself; installing anything machine-wide waits for the user's yes. Never the directory above the workspace — it is the app's, not the user's; outside the workspace, only where the user pointed you.`;
301
+ Inside the workspace, set up whatever the job needs yourself; installing anything machine-wide waits for the user's yes. Never the directory above the workspace — it is the app's, not the user's; outside the workspace, only where the user pointed you.
302
+
303
+ ${botGuideLine()}`;
301
304
 
302
305
  /** The other bots, used the other way from the call's roster: which part of a held job is someone else's. */
303
306
  function roster(peers: JobBot[]): string {
@@ -22,7 +22,7 @@ import { isPublicError } from "@/lib/public-error";
22
22
  import { clip } from "@/lib/utils";
23
23
 
24
24
  const routineSpec = {
25
- description: `Routines: work that starts by itself, each one a bot, the work and a time. List them, make one, change one, or delete one. Every start opens a thread like one started by hand; its result reaches the conversation the same way. At most ${ROUTINE.max} exist.`,
25
+ description: `Routines: work that starts by itself, each one a bot, the work and a time — once at a moment, a time of day on chosen days, or every few hours. List them, make one, change one, or delete one. Every start opens a thread like one started by hand; its result reaches the conversation the same way. At most ${ROUTINE.max} exist.`,
26
26
  parameters: z.object({
27
27
  action: z.enum(["list", "create", "change", "delete"]),
28
28
  routine: z
@@ -47,11 +47,17 @@ const routineSpec = {
47
47
  .describe(
48
48
  "With create: the whole of the work, handed to the bot as written every time it starts. Nobody is there then to fill in what is missing, so everything it needs goes here, in the user's own language and words. Their request, never your reading of it.",
49
49
  ),
50
+ at: z
51
+ .string()
52
+ .nullish()
53
+ .describe(
54
+ 'Starts once, at this moment, "YYYY-MM-DD HH:MM" in the user\'s own time, then switches itself off. Count "in an hour" or "tomorrow at eight" from **Now**. Give one of `at`, `time` or `everyHours`.',
55
+ ),
50
56
  time: z
51
57
  .string()
52
58
  .nullish()
53
59
  .describe(
54
- 'Starts at this time of day, 24-hour "HH:MM" in the user\'s own time, on `days`. Give this or `everyHours`, never both.',
60
+ 'Starts at this time of day, 24-hour "HH:MM" in the user\'s own time, on `days`. Give one of `at`, `time` or `everyHours`.',
55
61
  ),
56
62
  days: z
57
63
  .array(z.number().int().min(1).max(7))
@@ -64,7 +70,7 @@ const routineSpec = {
64
70
  .int()
65
71
  .nullish()
66
72
  .describe(
67
- `Starts again this many hours after it last started, ${ROUTINE.minHours} at least. Give this or \`time\`, never both.`,
73
+ `Starts again this many hours after it last started, ${ROUTINE.minHours} at least. Give one of \`at\`, \`time\` or \`everyHours\`.`,
68
74
  ),
69
75
  enabled: z
70
76
  .boolean()
@@ -79,17 +85,19 @@ type Args = z.infer<typeof routineSpec.parameters>;
79
85
 
80
86
  /** The schedule the arguments spell, none when they say nothing about time, or the line that says what is wrong. */
81
87
  function scheduleOf(args: Args): RoutineSchedule | string | null {
82
- if (args.time && args.everyHours)
83
- return "Give `time` or `everyHours`, not both.";
84
- const spelled = args.time
85
- ? {
86
- kind: "daily",
87
- time: args.time,
88
- days: args.days?.length ? args.days : [1, 2, 3, 4, 5, 6, 7],
89
- }
90
- : args.everyHours
91
- ? { kind: "every", hours: args.everyHours }
92
- : null;
88
+ if ([args.at, args.time, args.everyHours].filter(Boolean).length > 1)
89
+ return "Give one of `at`, `time` or `everyHours`, not two.";
90
+ const spelled = args.at
91
+ ? { kind: "once", at: args.at }
92
+ : args.time
93
+ ? {
94
+ kind: "daily",
95
+ time: args.time,
96
+ days: args.days?.length ? args.days : [1, 2, 3, 4, 5, 6, 7],
97
+ }
98
+ : args.everyHours
99
+ ? { kind: "every", hours: args.everyHours }
100
+ : null;
93
101
  if (!spelled) return null;
94
102
  const parsed = RoutineScheduleSchema.safeParse(spelled);
95
103
  return parsed.success ? parsed.data : parsed.error.issues[0].message;
@@ -148,7 +156,7 @@ export function createRoutineTools() {
148
156
 
149
157
  if (args.action === "create") {
150
158
  if (!schedule)
151
- return "Say when it starts: `time` (with `days`), or `everyHours`.";
159
+ return "Say when it starts: `at`, `time` (with `days`), or `everyHours`.";
152
160
  const input = RoutineInputSchema.safeParse({
153
161
  bot: args.bot ?? "",
154
162
  label: args.label ?? "",
@@ -170,9 +178,13 @@ export function createRoutineTools() {
170
178
  const held = (await readKeepWorkingOn())
171
179
  ? ""
172
180
  : " As things are set, it starts only while the app is open in a tab: a time that passes with it closed starts once when it is opened again. Settings › Routines has the switch that lets work go on with the app closed.";
181
+ const first = whenOf(toDate(made.nextRunAt));
173
182
  return {
174
183
  ...told(made),
175
- note: `${made.bot} starts "${made.label}" by itself, ${scheduleText(made.schedule)}, first ${whenOf(toDate(made.nextRunAt))}. Each start is a thread of its own, and its result reaches the conversation like any thread's.${held}`,
184
+ note:
185
+ made.schedule.kind === "once"
186
+ ? `${made.bot} starts "${made.label}" once, ${first}, and the routine then switches itself off. The start is a thread of its own, and its result reaches the conversation like any thread's.${held}`
187
+ : `${made.bot} starts "${made.label}" by itself, ${scheduleText(made.schedule)}, first ${first}. Each start is a thread of its own, and its result reaches the conversation like any thread's.${held}`,
176
188
  };
177
189
  }
178
190
 
@@ -252,7 +252,9 @@ async function drive(work: RoomWork, signal: AbortSignal) {
252
252
  await finishRoomWork(work, final.text);
253
253
  const thread = await findThread(work.threadId);
254
254
  if (thread?.status === "done") {
255
- if (!(await isAnyCallLive()))
255
+ // With a page open the page tells it (artifact-view): a notification it shows brings
256
+ // Thursday forward when pressed, where the OS one opens the script runner behind it
257
+ if (!presence.watching && !(await isAnyCallLive()))
256
258
  desktopNotify(thread.label, thread.outcome ?? "");
257
259
  const files = await filesOnDisk(pathsIn(thread.outcome ?? ""), null);
258
260
  // A page to read leads the notice; the rest follow in the order they were written.
@@ -2,6 +2,7 @@
2
2
 
3
3
  import { useEffect, useId, useMemo, useRef } from "react";
4
4
  import { createVoiceFollower, SPECTRUM_BANDS } from "@/lib/live/live.tap";
5
+ import { cn } from "@/lib/utils";
5
6
  import {
6
7
  MARK_PAINTS,
7
8
  type MarkPaint,
@@ -17,6 +18,9 @@ const BOX = 240;
17
18
  const CENTER = BOX / 2;
18
19
  const R = 112;
19
20
  const PAD = 18; // headroom so squash/stretch and the notify ring can bleed out
21
+ /** The waiting dot pops in rather than blinking on, the ring it cuts growing with it. */
22
+ const NOTIFY_POP =
23
+ "origin-center animate-in duration-300 zoom-in-0 [transform-box:fill-box]";
20
24
  const VIEW_BOX = `${-PAD} ${-PAD} ${BOX + PAD * 2} ${BOX + PAD * 2}`;
21
25
  /** Every silhouette is resampled to this many polar points, so blending two is a plain lerp. */
22
26
  const MARK_RESOLUTION = 128;
@@ -1464,6 +1468,7 @@ export function BotMark({
1464
1468
  cy={f(CENTER + Math.sin(nAngle) * cfg.notifyDist)}
1465
1469
  r={cfg.notifyR + cfg.notifyRing}
1466
1470
  fill="#000"
1471
+ className={NOTIFY_POP}
1467
1472
  />
1468
1473
  )}
1469
1474
  </mask>
@@ -1568,7 +1573,7 @@ export function BotMark({
1568
1573
  cx={f(CENTER + Math.cos(nAngle) * cfg.notifyDist)}
1569
1574
  cy={f(CENTER + Math.sin(nAngle) * cfg.notifyDist)}
1570
1575
  r={cfg.notifyR}
1571
- className="fill-amber-600 dark:fill-amber-400"
1576
+ className={cn("fill-amber-600 dark:fill-amber-400", NOTIFY_POP)}
1572
1577
  />
1573
1578
  )}
1574
1579
  </g>
@@ -472,7 +472,7 @@ function SeedRows({
472
472
  className={cn(
473
473
  "grid size-5 shrink-0 place-items-center rounded-full transition-colors",
474
474
  on
475
- ? "bg-brand text-brand-foreground"
475
+ ? "bg-foreground text-background"
476
476
  : "ring-1 ring-border/60 ring-inset",
477
477
  )}
478
478
  >
@@ -1487,9 +1487,7 @@ function ToolPicker({
1487
1487
  <span className="min-w-0 flex-1 truncate font-mono text-xs">
1488
1488
  {tool.name}
1489
1489
  </span>
1490
- {isPicked && (
1491
- <Check className="size-3.5 shrink-0 text-brand" />
1492
- )}
1490
+ {isPicked && <Check className="size-3.5 shrink-0" />}
1493
1491
  </CommandItem>
1494
1492
  );
1495
1493
  })}
@@ -30,6 +30,7 @@ import type { ResultPart } from "@/features/bot/bot.schema";
30
30
  import { imagePathsIn } from "@/features/bot/components/attachments";
31
31
  import { McpMark } from "@/features/connectors/components/mcp-mark";
32
32
  import { MemoryMark } from "@/features/memory/components/memory-mark";
33
+ import { RoutineMark } from "@/features/routine/components/routine-mark";
33
34
  import { SkillsMark } from "@/features/skills/components/skills-mark";
34
35
  import { FileLink } from "@/features/workspace/components/file-view";
35
36
  import { useServerRoute } from "@/lib/protocol/use-server-route";
@@ -83,6 +84,7 @@ const TOOL_ICONS: Partial<Record<string, LucideIcon>> = {
83
84
  [TOOL_NAMES.sign_in_use]: KeyRound,
84
85
  [TOOL_NAMES.sign_in_keep]: KeyRound,
85
86
  [TOOL_NAMES.end_call]: PhoneOff,
87
+ [TOOL_NAMES.routine]: RoutineMark,
86
88
  };
87
89
 
88
90
  export const toolIcon = (name: string): LucideIcon =>
@@ -366,7 +366,13 @@ export function ThreadRow({
366
366
 
367
367
  <span className="min-w-0 flex-1">
368
368
  {(attention || isUnread(thread)) && (
369
- <span className="block text-[10px] font-medium text-foreground">
369
+ <span
370
+ className={cn(
371
+ "block font-mono text-[10px] tracking-wide",
372
+ // a result is what arrived; a question keeps the muted label, its words carry the amber
373
+ attention ? "text-muted-foreground" : "text-brand",
374
+ )}
375
+ >
370
376
  {thread.room.questions.length
371
377
  ? `${thread.room.questions.length === 1 ? "Question" : `${thread.room.questions.length} questions`} · ${[...new Set(thread.room.questions.map((question) => question.bot))].join(", ")}`
372
378
  : thread.status === "waiting"
@@ -381,6 +387,8 @@ export function ThreadRow({
381
387
  className={cn(
382
388
  "min-w-0 truncate text-[14px] leading-5 tracking-[-0.15px]",
383
389
  attention ? "text-foreground" : "text-foreground/80",
390
+ // the rows that need you or are new stand out by weight, not by a heavier label
391
+ (attention || isUnread(thread)) && "font-medium",
384
392
  )}
385
393
  >
386
394
  {thread.label}
@@ -440,10 +448,11 @@ export function ThreadRow({
440
448
  await answer(thread, option);
441
449
  setSending(null);
442
450
  }}
443
- className="h-7 gap-1.5 rounded-full border-border bg-background px-3 text-[12px]"
451
+ // what moves the job on, so blue; outlined, so a list of them does not outweigh the call
452
+ className="h-7 gap-1.5 rounded-full border-brand bg-background px-3 text-[12px] text-brand hover:bg-brand/8 hover:text-brand dark:border-brand dark:bg-transparent dark:hover:bg-brand/12"
444
453
  >
445
454
  {option === THREAD_CONTINUE && (
446
- <ChevronsRight className="size-3.5 text-muted-foreground" />
455
+ <ChevronsRight className="size-3.5" />
447
456
  )}
448
457
  {option}
449
458
  </Button>