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.
- package/.next/BUILD_ID +1 -1
- package/.next/build-manifest.json +3 -3
- package/.next/prerender-manifest.json +3 -3
- package/.next/server/app/_global-error/page.js.nft.json +1 -1
- package/.next/server/app/_global-error.html +1 -1
- package/.next/server/app/_global-error.rsc +1 -1
- package/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/server/app/_not-found.html +1 -1
- package/.next/server/app/_not-found.rsc +2 -2
- package/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
- package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
- package/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/api/artifact/route.js.nft.json +1 -1
- package/.next/server/app/api/bot/keep-working/route.js.nft.json +1 -1
- package/.next/server/app/api/bot/memory/files/route.js.nft.json +1 -1
- package/.next/server/app/api/bot/memory/route.js.nft.json +1 -1
- package/.next/server/app/api/bot/route.js.nft.json +1 -1
- package/.next/server/app/api/bot/thread/[id]/route.js.nft.json +1 -1
- package/.next/server/app/api/bot/thread/route.js.nft.json +1 -1
- package/.next/server/app/api/config/route.js.nft.json +1 -1
- package/.next/server/app/api/events/route.js.nft.json +1 -1
- package/.next/server/app/api/favicon/[host]/route.js.nft.json +1 -1
- package/.next/server/app/api/file/[...path]/route.js.nft.json +1 -1
- package/.next/server/app/api/llm-model/catalog/route.js.nft.json +1 -1
- package/.next/server/app/api/llm-model/credits/route.js.nft.json +1 -1
- package/.next/server/app/api/llm-model/route.js.nft.json +1 -1
- package/.next/server/app/api/llm-model/subscription/route.js.nft.json +1 -1
- package/.next/server/app/api/mcp/[name]/route.js.nft.json +1 -1
- package/.next/server/app/api/mcp/oauth/callback/route.js.nft.json +1 -1
- package/.next/server/app/api/mcp/route.js.nft.json +1 -1
- package/.next/server/app/api/mcp/tools/route.js.nft.json +1 -1
- package/.next/server/app/api/memory/edit/route.js +1 -1
- package/.next/server/app/api/memory/edit/route.js.nft.json +1 -1
- package/.next/server/app/api/memory/route.js.nft.json +1 -1
- package/.next/server/app/api/probe/route.js.nft.json +1 -1
- package/.next/server/app/api/reach/route.js +11 -11
- package/.next/server/app/api/reach/route.js.nft.json +1 -1
- package/.next/server/app/api/routine/route.js +6 -6
- package/.next/server/app/api/routine/route.js.nft.json +1 -1
- package/.next/server/app/api/signins/route.js.nft.json +1 -1
- package/.next/server/app/api/skills/[source]/[name]/route.js.nft.json +1 -1
- package/.next/server/app/api/skills/route.js.nft.json +1 -1
- package/.next/server/app/api/thursday/call/route.js.nft.json +1 -1
- package/.next/server/app/api/thursday/skills/route.js.nft.json +1 -1
- package/.next/server/app/api/thursday/text/route.js +11 -11
- package/.next/server/app/api/thursday/text/route.js.nft.json +1 -1
- package/.next/server/app/api/thursday/tool-call/route.js +6 -6
- package/.next/server/app/api/thursday/tool-call/route.js.nft.json +1 -1
- package/.next/server/app/api/workspace/files/route.js.nft.json +1 -1
- package/.next/server/app/api/workspace/route.js.nft.json +1 -1
- package/.next/server/app/artifact/[...path]/page/server-reference-manifest.json +1 -1
- package/.next/server/app/artifact/[...path]/page.js.nft.json +1 -1
- package/.next/server/app/artifact/[...path]/page_client-reference-manifest.js +1 -1
- package/.next/server/app/favicon.ico/route.js.nft.json +1 -1
- package/.next/server/app/manifest.webmanifest/route.js.nft.json +1 -1
- package/.next/server/app/page/server-reference-manifest.json +56 -56
- package/.next/server/app/page.js +3 -3
- package/.next/server/app/page.js.nft.json +1 -1
- package/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/server/chunks/[root-of-the-server]__012s2o8._.js +1 -1
- package/.next/server/chunks/[root-of-the-server]__04r4h-2._.js +1 -1
- package/.next/server/chunks/[root-of-the-server]__0mrk2it._.js +1 -1
- package/.next/server/chunks/[root-of-the-server]__0nmhgeo._.js +3 -0
- package/.next/server/chunks/[root-of-the-server]__1fy2g82._.js +3 -0
- package/.next/server/chunks/[root-of-the-server]__1ndi14y._.js +1 -1
- package/.next/server/chunks/[root-of-the-server]__1nmflqk._.js +3 -0
- package/.next/server/chunks/[root-of-the-server]__1v7-zke._.js +1 -1
- package/.next/server/chunks/[root-of-the-server]__1wnzmx9._.js +3 -0
- package/.next/server/chunks/[root-of-the-server]__1xv-5cq._.js +1 -1
- package/.next/server/chunks/[root-of-the-server]__1ygplx1._.js +1 -1
- package/.next/server/chunks/_05v8k21._.js +1 -1
- package/.next/server/chunks/_0awtznl._.js +4 -4
- package/.next/server/chunks/_0k6qjcf._.js +1 -1
- package/.next/server/chunks/_0na2fj1._.js +5 -5
- package/.next/server/chunks/_0x-ej3f._.js +21 -21
- package/.next/server/chunks/_1dc_d1q._.js +22 -20
- package/.next/server/chunks/_1jc5g4p._.js +2 -2
- package/.next/server/chunks/_1jn-aep._.js +50 -48
- package/.next/server/chunks/{_1u8ygbz._.js → _1m88lkh._.js} +7 -7
- package/.next/server/chunks/_1uuucy-._.js +4 -4
- package/.next/server/chunks/_1v9r2ke._.js +1 -1
- package/.next/server/chunks/ssr/{[root-of-the-server]__1h4bnrf._.js → [root-of-the-server]__0oo-wlv._.js} +2 -2
- package/.next/server/chunks/ssr/{[root-of-the-server]__1kyi568._.js → [root-of-the-server]__0tvavy9._.js} +1 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__0tzekbl._.js +1 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__0zuns7e._.js +3 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__1se25x0._.js +1 -1
- package/.next/server/chunks/ssr/{_1i3ddn7._.js → _05pl_u8._.js} +1 -1
- package/.next/server/chunks/ssr/_070nv1_._.js +3 -0
- package/.next/server/chunks/ssr/_097-ljd._.js +1 -1
- package/.next/server/chunks/ssr/_0dshsh4._.js +3 -0
- package/.next/server/chunks/ssr/_0fqhhwi._.js +1 -1
- package/.next/server/chunks/ssr/{_0z8jpch._.js → _0ital-2._.js} +2 -2
- package/.next/server/chunks/ssr/{_19g570d._.js → _0kjj5ts._.js} +3 -3
- package/.next/server/chunks/ssr/{_1nod5hh._.js → _0l_mcfz._.js} +2 -2
- package/.next/server/chunks/ssr/_0ld9_qq._.js +1 -1
- package/.next/server/chunks/ssr/_0ltz2q5._.js +3 -0
- package/.next/server/chunks/ssr/_0scf7xy._.js +3 -0
- package/.next/server/chunks/ssr/_0u5xv6g._.js +3 -0
- package/.next/server/chunks/ssr/{_0ewory0._.js → _0urj3mc._.js} +2 -2
- package/.next/server/chunks/ssr/{_0pm-2p-._.js → _1-zyvdb._.js} +2 -2
- package/.next/server/chunks/ssr/{_1_rqu8x._.js → _12mijol._.js} +7 -7
- package/.next/server/chunks/ssr/_15zqt-t._.js +1 -1
- package/.next/server/chunks/ssr/_184327j._.js +1 -1
- package/.next/server/chunks/ssr/{_1y-ea28._.js → _1a-nuxx._.js} +2 -2
- package/.next/server/chunks/ssr/_1alxym6._.js +3 -0
- package/.next/server/chunks/ssr/{_0hzz82u._.js → _1d423aj._.js} +25 -23
- package/.next/server/chunks/ssr/{_1u1mxu9._.js → _1flbs77._.js} +2 -2
- package/.next/server/chunks/ssr/_1mqn9mc._.js +1 -1
- package/.next/server/chunks/ssr/_1neoj22._.js +1 -1
- package/.next/server/chunks/ssr/{_1oijqjw._.js → _1tmu8z-._.js} +2 -2
- package/.next/server/chunks/ssr/{_10y6r1n._.js → _1uy0xom._.js} +3 -3
- package/.next/server/chunks/ssr/_1x-kojb._.js +20 -0
- package/.next/server/chunks/ssr/_1xoch6f._.js +1 -1
- package/.next/server/chunks/ssr/_1z74dmq._.js +164 -0
- package/.next/server/chunks/ssr/features_bot_1nr94ks._.js +3 -0
- package/.next/server/chunks/ssr/features_bot_1s_-2vj._.js +1 -1
- package/.next/server/chunks/ssr/features_connectors_components_mcp-setting_tsx_0jmbgvp._.js +1 -1
- package/.next/server/chunks/ssr/features_memory_components_memory-setting_tsx_0oa43sw._.js +1 -1
- package/.next/server/chunks/ssr/features_routine_components_routine-setting_tsx_055fknz._.js +1 -1
- package/.next/server/chunks/ssr/{node_modules_111sh9o._.js → node_modules_03d0rfz._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_0lg75qx._.js → node_modules_04_sbbc._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_10gbm2y._.js → node_modules_05ybmta._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_1v9tc1c._.js → node_modules_068ws5v._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_1_8mv4v._.js → node_modules_06bb4q8._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_01f8_se._.js → node_modules_07rs_p2._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_0ii1l89._.js → node_modules_0dzm4um._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_20xmwtu._.js → node_modules_0jljel9._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_1ix-g49._.js → node_modules_0sgy-dc._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_0woscox._.js → node_modules_0t_ez6-._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_059vtf2._.js → node_modules_0th357h._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_06w645b._.js → node_modules_0ubdtk-._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_0b8df3s._.js → node_modules_0uyh_vb._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_07ih6wb._.js → node_modules_0wnlddw._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_0gt-uw9._.js → node_modules_0x630an._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_1x825hz._.js → node_modules_0yvz9e0._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_0t-44lj._.js → node_modules_12et8d5._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_1m3gb16._.js → node_modules_13kwejm._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_1p9rdsy._.js → node_modules_1af3w91._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_0sz5yg1._.js → node_modules_1bucjlk._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_1teyzfa._.js → node_modules_1lkix61._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_1lsyy7k._.js → node_modules_1ogczzz._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_1-7qawg._.js → node_modules_1s-gb-1._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_0yv-re6._.js → node_modules_1uwg-gz._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_03ptrl9._.js → node_modules_1wbkzl9._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_00jv9_3._.js → node_modules_1z1lrbk._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_@base-ui_react_0jne69c._.js → node_modules_@base-ui_react_0buz1-j._.js} +1 -1
- 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
- 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
- package/.next/server/middleware-build-manifest.js +3 -3
- package/.next/server/pages/404.html +1 -1
- package/.next/server/pages/500.html +1 -1
- package/.next/server/server-reference-manifest.js +1 -1
- package/.next/server/server-reference-manifest.json +57 -57
- package/.next/static/chunks/06nt9tatr-dug.js +1 -0
- package/.next/static/chunks/0_awn8tucznxc.js +1 -0
- package/.next/static/chunks/{34s52v8f841zo.js → 0ge-hx0ed83a1.js} +1 -1
- package/.next/static/chunks/0pda_vf-abwvx.js +1 -0
- package/.next/static/chunks/{2u9yjkpkoij75.js → 0qmd8n52hp-6v.js} +1 -1
- package/.next/static/chunks/0rowp9rx3qbxu.js +18 -0
- package/.next/static/chunks/{1ocy4w0v20a3v.js → 13ixzheqw7o-w.js} +2 -2
- package/.next/static/chunks/1grkt5gr8pbg1.js +17 -0
- package/.next/static/chunks/{21gf2t_whhurc.js → 1yugpvvi8qrbu.js} +2 -2
- package/.next/static/chunks/{1e4yrzs5evq_r.js → 245hb85nsy6j4.js} +1 -1
- package/.next/static/chunks/27lkanxvtdn09.js +1 -0
- package/.next/static/chunks/{0oz7a68qanm99.js → 27tvv1i-smrck.js} +1 -1
- package/.next/static/chunks/{2zhfdbq50ee5i.js → 2c64drdevdla3.js} +1 -1
- package/.next/static/chunks/{0k5tkt_qi52kb.js → 2ndw4w4u2x51r.js} +1 -1
- package/.next/static/chunks/37j27b3e1gt7r.js +1 -0
- package/.next/static/chunks/{2om4lkg86feb4.js → 38301ji-85i30.js} +1 -1
- package/.next/static/chunks/{09c13vnqy7al3.js → 3bbzsitgiltnt.js} +2 -2
- package/.next/static/chunks/3fec-aeteddr-.js +1 -0
- package/.next/static/chunks/3go3hwp1z2_h2.css +1 -0
- package/.next/static/chunks/3h2bun3hcggg8.js +1 -0
- package/.next/static/chunks/{0g1p98s2pfx2w.js → 3h2e-xv1zxh_f.js} +2 -2
- package/.next/static/chunks/3j1y335o95mj7.js +1 -0
- package/.next/static/chunks/3obf9n3cxj99k.js +1 -0
- package/.next/static/chunks/{2upc9mkcc3nlq.js → 427qsc6kahxyo.js} +1 -1
- package/README.md +31 -14
- package/bin/port.mjs +11 -7
- package/bin/thursday.mjs +1 -1
- package/components/ui/radio-group.tsx +2 -2
- package/components/ui/shiny-text.tsx +3 -22
- package/docs/how-it-works.md +11 -3
- package/docs/images/first-run.png +0 -0
- package/docs/images/keeps-going.png +0 -0
- package/docs/images/two-minds.png +0 -0
- package/features/ai/components/model-browser.tsx +1 -1
- package/features/ai/components/model-picker.tsx +1 -1
- package/features/ai/guide.ts +9 -4
- package/features/ai/prompts/bot.prompt.ts +4 -1
- package/features/ai/tools/routine.tool.ts +28 -16
- package/features/bot/bot.runner.ts +3 -1
- package/features/bot/components/bot-mark.tsx +6 -1
- package/features/bot/components/bot-setting.tsx +2 -4
- package/features/bot/components/bot-tool.tsx +2 -0
- package/features/bot/components/room-list.tsx +12 -3
- package/features/bot/components/room-pill.tsx +26 -19
- package/features/config/components/config-setting.tsx +17 -5
- package/features/config/components/voice-key.tsx +38 -18
- package/features/connectors/mcp.const.ts +90 -59
- package/features/intro/components/intro.tsx +50 -27
- package/features/routine/components/routine-mark.tsx +4 -6
- package/features/routine/components/routine-setting.tsx +332 -143
- package/features/routine/routine.query.ts +24 -3
- package/features/routine/routine.schema.ts +55 -4
- package/features/settings/components/install-app.tsx +1 -4
- package/features/settings/components/setting-ui.tsx +1 -1
- package/features/skills/skills.discover.ts +2 -1
- package/features/skills/skills.query.ts +5 -1
- package/features/skills/skills.schema.ts +2 -0
- package/features/thursday/components/ascii-orb.tsx +39 -10
- package/features/thursday/components/connect-wave.tsx +284 -0
- package/features/thursday/components/side-captions.tsx +29 -17
- package/features/thursday/components/thursday-setting.tsx +2 -2
- package/features/thursday/components/thursday.tsx +130 -58
- package/features/thursday/components/voice-picker.tsx +1 -1
- package/features/thursday/components/write-line.tsx +2 -2
- package/features/thursday/open-work.ts +11 -3
- package/features/thursday/tool-line.ts +94 -12
- package/features/thursday/use-text-call.ts +3 -2
- package/features/thursday/use-thursday.ts +5 -2
- package/features/workspace/components/artifact-view.tsx +129 -7
- package/guide/bots.md +17 -5
- package/guide/calls.md +15 -7
- package/guide/setup.md +10 -2
- package/lib/desktop-notify.ts +3 -2
- package/package.json +1 -1
- package/scripts/bot-context.test.mts +1 -1
- package/skills/computer/SKILL.md +1 -0
- package/skills/interactive-page/SKILL.md +4 -0
- package/.next/server/chunks/[root-of-the-server]__0n-l_n_._.js +0 -3
- package/.next/server/chunks/[root-of-the-server]__0wvo2of._.js +0 -3
- package/.next/server/chunks/[root-of-the-server]__1e6aut3._.js +0 -3
- package/.next/server/chunks/[root-of-the-server]__1ziojdq._.js +0 -3
- package/.next/server/chunks/ssr/[root-of-the-server]__1srp8l5._.js +0 -3
- package/.next/server/chunks/ssr/_0mes4s0._.js +0 -3
- package/.next/server/chunks/ssr/_0ya0cb0._.js +0 -3
- package/.next/server/chunks/ssr/_0yto5s7._.js +0 -20
- package/.next/server/chunks/ssr/_10dw2nv._.js +0 -3
- package/.next/server/chunks/ssr/_1p0gxeo._.js +0 -164
- package/.next/server/chunks/ssr/_1r41yvh._.js +0 -3
- package/.next/server/chunks/ssr/_1r79ksv._.js +0 -3
- package/.next/server/chunks/ssr/features_bot_1ogjt52._.js +0 -3
- package/.next/server/chunks/ssr/node_modules_@base-ui_react_0b3d4m9._.js +0 -3
- package/.next/static/chunks/03omwz_vi2ndg.js +0 -1
- package/.next/static/chunks/0_1bqgd6wt_og.js +0 -1
- package/.next/static/chunks/0abgkzikaznxc.js +0 -1
- package/.next/static/chunks/0bddkpash59-o.js +0 -17
- package/.next/static/chunks/0c2-bzhy3a0ib.js +0 -1
- package/.next/static/chunks/0iilfb1ocevnx.js +0 -1
- package/.next/static/chunks/0imbhx-j830a_.css +0 -1
- package/.next/static/chunks/24h1y9dhy3m_2.js +0 -1
- package/.next/static/chunks/2be08eqxsfc1y.js +0 -1
- package/.next/static/chunks/2d3yhjtafgmtw.js +0 -1
- package/.next/static/chunks/2f_5saocsko_b.js +0 -1
- package/.next/static/chunks/2lwbr5vx0ixaj.js +0 -18
- package/.next/static/chunks/44kjmi5h_p8px.js +0 -1
- /package/.next/static/{UiOMPYpQ4fqGyu5PzS-7j → u8kJqShUAa49vY7BxAWsw}/_buildManifest.js +0 -0
- /package/.next/static/{UiOMPYpQ4fqGyu5PzS-7j → u8kJqShUAa49vY7BxAWsw}/_clientMiddlewareManifest.js +0 -0
- /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
|
|
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
|
[](https://www.npmjs.com/package/thursday-agent)
|
|
11
11
|
[](https://github.com/cgoinglove/thursday/actions/workflows/ci.yml)
|
|
12
12
|
[](LICENSE)
|
|
13
|
-
[](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
|
-
|
|
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
|
+

|
|
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
|
-
|
|
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
|
-

|
|
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
|
-

|
|
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
|
|
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
|
-

|
|
52
|
+

|
|
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
|
-

|
|
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
|
-

|
|
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
|
+

|
|
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
|
|
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. “
|
|
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
|
-
/**
|
|
10
|
-
|
|
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
|
|
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
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
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
|
|
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-
|
|
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-
|
|
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
|
|
2
|
+
import { cn } from "@/lib/utils";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* The
|
|
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 {
|
|
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"
|
package/docs/how-it-works.md
CHANGED
|
@@ -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
|
|
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
|

|
|
@@ -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
|
+

|
|
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
|
|
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
|
|
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
|
|
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
|
package/features/ai/guide.ts
CHANGED
|
@@ -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.
|
|
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),
|
|
12
|
-
* scripts/
|
|
13
|
-
*
|
|
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
|
|
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
|
|
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
|
|
83
|
-
return "Give `time` or `everyHours`, not
|
|
84
|
-
const spelled = args.
|
|
85
|
-
? {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
:
|
|
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:
|
|
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
|
-
|
|
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-
|
|
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
|
|
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
|
-
|
|
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
|
|
455
|
+
<ChevronsRight className="size-3.5" />
|
|
447
456
|
)}
|
|
448
457
|
{option}
|
|
449
458
|
</Button>
|