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
|
@@ -15,11 +15,39 @@ const TimeSchema = z
|
|
|
15
15
|
.string()
|
|
16
16
|
.regex(/^([01]\d|2[0-3]):[0-5]\d$/, "A time is HH:MM, 00:00 to 23:59");
|
|
17
17
|
|
|
18
|
+
/** "YYYY-MM-DD HH:MM" in this machine's own time, read as a date. */
|
|
19
|
+
export function momentOf(at: string): Date {
|
|
20
|
+
const [day, time] = at.split(" ");
|
|
21
|
+
const [year, month, date] = day.split("-").map(Number);
|
|
22
|
+
const [hours, minutes] = time.split(":").map(Number);
|
|
23
|
+
return new Date(year, month - 1, date, hours, minutes);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const MOMENT = /^\d{4}-\d\d-\d\d ([01]\d|2[0-3]):[0-5]\d$/;
|
|
27
|
+
|
|
28
|
+
const MomentSchema = z
|
|
29
|
+
.string()
|
|
30
|
+
.regex(MOMENT, 'A moment is "YYYY-MM-DD HH:MM"')
|
|
31
|
+
// a month or a day the calendar does not have rolls over into another when read
|
|
32
|
+
.refine((at) => {
|
|
33
|
+
if (!MOMENT.test(at)) return true;
|
|
34
|
+
const read = momentOf(at);
|
|
35
|
+
return (
|
|
36
|
+
read.getMonth() === Number(at.slice(5, 7)) - 1 &&
|
|
37
|
+
read.getDate() === Number(at.slice(8, 10))
|
|
38
|
+
);
|
|
39
|
+
}, "There is no such day");
|
|
40
|
+
|
|
18
41
|
/**
|
|
19
|
-
* When it starts, in this machine's own time.
|
|
20
|
-
*
|
|
42
|
+
* When it starts, in this machine's own time. Three kinds and no cron string: a model fills
|
|
43
|
+
* any of them without a syntax to get wrong, and the screen draws each without parsing.
|
|
44
|
+
* One that starts `once` switches itself off as it starts (routine.query claimRoutine).
|
|
21
45
|
*/
|
|
22
46
|
export const RoutineScheduleSchema = z.discriminatedUnion("kind", [
|
|
47
|
+
z.object({
|
|
48
|
+
kind: z.literal("once"),
|
|
49
|
+
at: MomentSchema,
|
|
50
|
+
}),
|
|
23
51
|
z.object({
|
|
24
52
|
kind: z.literal("daily"),
|
|
25
53
|
time: TimeSchema,
|
|
@@ -66,9 +94,31 @@ const RoutineSchema = RoutineInputSchema.extend({
|
|
|
66
94
|
export type Routine = z.infer<typeof RoutineSchema>;
|
|
67
95
|
|
|
68
96
|
const DAY_NAMES = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
|
|
97
|
+
const MONTH_NAMES = [
|
|
98
|
+
"Jan",
|
|
99
|
+
"Feb",
|
|
100
|
+
"Mar",
|
|
101
|
+
"Apr",
|
|
102
|
+
"May",
|
|
103
|
+
"Jun",
|
|
104
|
+
"Jul",
|
|
105
|
+
"Aug",
|
|
106
|
+
"Sep",
|
|
107
|
+
"Oct",
|
|
108
|
+
"Nov",
|
|
109
|
+
"Dec",
|
|
110
|
+
];
|
|
69
111
|
|
|
70
|
-
/**
|
|
112
|
+
/**
|
|
113
|
+
* "Once · Sep 21 20:00", "Daily 09:00 · Mon–Fri", "Mon 10:00", "Every 6 hours": the one
|
|
114
|
+
* spelling, on screen and to a model.
|
|
115
|
+
*/
|
|
71
116
|
export function scheduleText(schedule: RoutineSchedule): string {
|
|
117
|
+
if (schedule.kind === "once") {
|
|
118
|
+
const [day, time] = schedule.at.split(" ");
|
|
119
|
+
const [, month, date] = day.split("-").map(Number);
|
|
120
|
+
return `Once · ${MONTH_NAMES[month - 1]} ${date} ${time}`;
|
|
121
|
+
}
|
|
72
122
|
if (schedule.kind === "every")
|
|
73
123
|
return schedule.hours === 1
|
|
74
124
|
? "Every hour"
|
|
@@ -86,9 +136,10 @@ export function scheduleText(schedule: RoutineSchedule): string {
|
|
|
86
136
|
/**
|
|
87
137
|
* The first start after `from`. A daily one is the next listed day at its time; an `every`
|
|
88
138
|
* one counts from `from` itself, so a machine that slept through several starts owes one,
|
|
89
|
-
* not all of them.
|
|
139
|
+
* not all of them. A `once` one has its moment, passed or not.
|
|
90
140
|
*/
|
|
91
141
|
export function nextRun(schedule: RoutineSchedule, from: Date): Date {
|
|
142
|
+
if (schedule.kind === "once") return momentOf(schedule.at);
|
|
92
143
|
if (schedule.kind === "every")
|
|
93
144
|
return new Date(from.getTime() + schedule.hours * 3_600_000);
|
|
94
145
|
const [hours, minutes] = schedule.time.split(":").map(Number);
|
|
@@ -139,13 +139,10 @@ export function InstallButton() {
|
|
|
139
139
|
onClick={() => void install()}
|
|
140
140
|
className="group/install flex w-full items-start gap-2.5 rounded-xl px-3 py-2.5 text-left outline-none transition-colors hover:bg-muted focus-visible:ring-3 focus-visible:ring-ring/50"
|
|
141
141
|
>
|
|
142
|
-
<span className="mt-0.5 grid size-7 shrink-0 place-items-center rounded-lg bg-background ring-1 ring-border">
|
|
143
|
-
<AppWindowMac className="size-3.5" />
|
|
144
|
-
</span>
|
|
145
142
|
<span className="min-w-0 flex-1">
|
|
146
143
|
<span className="flex items-center gap-1 text-sm font-medium">
|
|
147
144
|
Install app
|
|
148
|
-
<ArrowUpRight className="size-3.5 text-muted-foreground transition-transform group-hover/install:translate-x-px group-hover/install:-translate-y-px" />
|
|
145
|
+
<ArrowUpRight className="ml-auto size-3.5 text-muted-foreground transition-transform group-hover/install:translate-x-px group-hover/install:-translate-y-px" />
|
|
149
146
|
</span>
|
|
150
147
|
<span className="block text-xs leading-snug text-muted-foreground">
|
|
151
148
|
Its own window, apart from your tabs
|
|
@@ -5,7 +5,7 @@ import { botFolder, WORKSPACE } from "@/features/workspace/workspace";
|
|
|
5
5
|
import { logger } from "@/lib/logger";
|
|
6
6
|
import type { Sandbox } from "@/lib/sandbox";
|
|
7
7
|
import { errorToString } from "@/lib/utils";
|
|
8
|
-
import { parseFrontmatter } from "./skills.query";
|
|
8
|
+
import { parseFrontmatter, runsHere } from "./skills.query";
|
|
9
9
|
import type { SkillFrontmatter } from "./skills.schema";
|
|
10
10
|
|
|
11
11
|
/** Skills for the prompt, every skill at once, without failing the session over one bad file. */
|
|
@@ -89,6 +89,7 @@ export async function discoverSkills(
|
|
|
89
89
|
continue;
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
+
if (!runsHere(frontmatter)) continue;
|
|
92
93
|
if (seenNames.has(frontmatter.name)) continue;
|
|
93
94
|
seenNames.add(frontmatter.name);
|
|
94
95
|
|
|
@@ -69,6 +69,10 @@ export function splitFrontmatter(content: string): {
|
|
|
69
69
|
return { head: match[1] ?? null, body: rest.trim(), rest, fence: match[0] };
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
+
/** Whether a skill runs on this machine's OS; the list and the prompt both leave out one that does not. */
|
|
73
|
+
export const runsHere = ({ platforms }: SkillFrontmatter) =>
|
|
74
|
+
!platforms || platforms.includes(process.platform);
|
|
75
|
+
|
|
72
76
|
export function parseFrontmatter(content: string): SkillFrontmatter {
|
|
73
77
|
const { head } = splitFrontmatter(content);
|
|
74
78
|
if (!head) {
|
|
@@ -172,7 +176,7 @@ export async function findAllSkills(): Promise<SkillSummary[]> {
|
|
|
172
176
|
}
|
|
173
177
|
for (const dir of names) {
|
|
174
178
|
const summary = await readSummary(source, dir);
|
|
175
|
-
if (summary) out.push(summary);
|
|
179
|
+
if (summary && runsHere(summary)) out.push(summary);
|
|
176
180
|
}
|
|
177
181
|
}
|
|
178
182
|
return out;
|
|
@@ -22,6 +22,8 @@ export const SkillFrontmatterSchema = z.object({
|
|
|
22
22
|
description: z.string().trim().min(1, "Description is required"),
|
|
23
23
|
/** true hides the skill from the list and the prompt; absent means enabled. */
|
|
24
24
|
disabled: z.boolean().optional(),
|
|
25
|
+
/** `process.platform` values it runs on (`darwin`); elsewhere it is not listed at all. Absent means everywhere. */
|
|
26
|
+
platforms: z.array(z.string()).optional(),
|
|
25
27
|
});
|
|
26
28
|
export type SkillFrontmatter = z.infer<typeof SkillFrontmatterSchema>;
|
|
27
29
|
|
|
@@ -226,6 +226,16 @@ const WORD_FADE_IN = 0.3;
|
|
|
226
226
|
const WORD_HOLD = 3.4;
|
|
227
227
|
const WORD_STEP_OUT = 0.08;
|
|
228
228
|
const WORD_FADE_OUT = 0.35;
|
|
229
|
+
/**
|
|
230
|
+
* A word arrives and leaves rough rather than as clean strokes: each cell of a letter keeps
|
|
231
|
+
* its own time within SCATTER (s), a cell still coming in or going out crackles on and off
|
|
232
|
+
* STATIC_RATE times a second, and what goes out drops to RESIDUE and lingers as an
|
|
233
|
+
* afterimage for LINGER (s) before it is gone.
|
|
234
|
+
*/
|
|
235
|
+
const WORD_SCATTER = 0.45;
|
|
236
|
+
const WORD_STATIC_RATE = 14;
|
|
237
|
+
const WORD_RESIDUE = 0.2;
|
|
238
|
+
const WORD_LINGER = 0.9;
|
|
229
239
|
/** A word given this soon after she mounts came with her (ms). */
|
|
230
240
|
const BORN_WITH_MS = 600;
|
|
231
241
|
/**
|
|
@@ -517,22 +527,33 @@ function onTwoLines(text: string): number[][] | null {
|
|
|
517
527
|
];
|
|
518
528
|
}
|
|
519
529
|
|
|
520
|
-
/**
|
|
530
|
+
/**
|
|
531
|
+
* A word's letters `age` seconds into its showing: each lights in turn, holds, and goes out in
|
|
532
|
+
* turn — every cell on its own beat, crackling while it changes, and leaving an afterimage.
|
|
533
|
+
*/
|
|
521
534
|
function wordValue(cell: Cell, t: number, age: number, hold: number) {
|
|
522
535
|
if (cell.word < 0) return 0;
|
|
523
|
-
const jitter = cell.seed *
|
|
536
|
+
const jitter = cell.seed * WORD_SCATTER;
|
|
524
537
|
const appear = smoothstep(
|
|
525
538
|
0,
|
|
526
539
|
1,
|
|
527
540
|
(age - cell.word * WORD_STEP_IN - jitter) / WORD_FADE_IN,
|
|
528
541
|
);
|
|
529
|
-
const
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
542
|
+
const out = age - hold - cell.word * WORD_STEP_OUT - jitter;
|
|
543
|
+
const vanish = smoothstep(0, 1, out / WORD_FADE_OUT);
|
|
544
|
+
// what the letter drops to, then the afterimage fading out of that
|
|
545
|
+
const residue =
|
|
546
|
+
WORD_RESIDUE * (1 - smoothstep(0, 1, (out - WORD_FADE_OUT) / WORD_LINGER));
|
|
547
|
+
const lit = Math.max(appear - vanish, appear > 0.5 ? residue * vanish : 0);
|
|
548
|
+
const changing = (appear > 0 && appear < 1) || (vanish > 0 && residue > 0);
|
|
549
|
+
const crackle =
|
|
550
|
+
changing &&
|
|
551
|
+
hash(Math.floor(t * WORD_STATIC_RATE) + cell.seed * 97, cell.grain * 31) <
|
|
552
|
+
0.35
|
|
553
|
+
? 0.25
|
|
554
|
+
: 1;
|
|
534
555
|
const flick = 0.86 + Math.sin(t * 3.5 + cell.seed * 6) * 0.14;
|
|
535
|
-
return
|
|
556
|
+
return lit * crackle * flick;
|
|
536
557
|
}
|
|
537
558
|
|
|
538
559
|
/**
|
|
@@ -753,11 +774,14 @@ export function AsciiOrb({
|
|
|
753
774
|
// lighting in a moment after an empty face read as a late start
|
|
754
775
|
useEffect(() => {
|
|
755
776
|
if (!word) return;
|
|
777
|
+
// A word is said as it comes. One handed back later — the goodbye still held when a
|
|
778
|
+
// ring's CALL steps aside — would already be over, so it is not said again
|
|
779
|
+
if (Date.now() - word.at > (word.hold ?? WORD_HOLD) * 1000) return;
|
|
756
780
|
const { cw, ch } = pitchRef.current;
|
|
757
781
|
const now = performance.now();
|
|
758
782
|
const lit =
|
|
759
783
|
now - bornAt.current < BORN_WITH_MS
|
|
760
|
-
? word.text.length * WORD_STEP_IN + WORD_FADE_IN +
|
|
784
|
+
? word.text.length * WORD_STEP_IN + WORD_FADE_IN + WORD_SCATTER
|
|
761
785
|
: 0;
|
|
762
786
|
wordRef.current = {
|
|
763
787
|
text: word.text,
|
|
@@ -857,7 +881,12 @@ export function AsciiOrb({
|
|
|
857
881
|
const wording =
|
|
858
882
|
shown !== null &&
|
|
859
883
|
cur.mode !== "error" &&
|
|
860
|
-
wordAge <
|
|
884
|
+
wordAge <
|
|
885
|
+
shown.hold +
|
|
886
|
+
shown.letters * WORD_STEP_OUT +
|
|
887
|
+
WORD_SCATTER +
|
|
888
|
+
WORD_FADE_OUT +
|
|
889
|
+
WORD_LINGER;
|
|
861
890
|
const want: Field = wording
|
|
862
891
|
? {
|
|
863
892
|
scale: 0,
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useRef, useState } from "react";
|
|
4
|
+
import { createPortal } from "react-dom";
|
|
5
|
+
import {
|
|
6
|
+
ALPHA_TOP,
|
|
7
|
+
CHAR_RATE,
|
|
8
|
+
EMOJI_CHAR_RATE,
|
|
9
|
+
EMOJI_MIN_LEVEL,
|
|
10
|
+
EMOJI_POOL,
|
|
11
|
+
EMOJI_RATIO,
|
|
12
|
+
hash,
|
|
13
|
+
RAMP,
|
|
14
|
+
smoothstep,
|
|
15
|
+
} from "../ascii.const";
|
|
16
|
+
import { useCallHeld } from "../call-signal";
|
|
17
|
+
import type { AsciiCharset } from "../face.const";
|
|
18
|
+
import type { CallStatus } from "../thursday.schema";
|
|
19
|
+
|
|
20
|
+
/** Grid step, px: her face's own. */
|
|
21
|
+
const CELL = 11;
|
|
22
|
+
/** Glyph size, px: under the step, so the wave reads as specks with air between them. */
|
|
23
|
+
const GLYPH = 9;
|
|
24
|
+
/** Share of cells that carry a glyph at all: fewer, not larger, keeps the wave light to draw. */
|
|
25
|
+
const KEEP = 0.4;
|
|
26
|
+
/** How far the front travels per second, as a share of the way to the farthest corner. */
|
|
27
|
+
const SPEED = 0.95;
|
|
28
|
+
/** Width of the lit band behind the front, in the same units. */
|
|
29
|
+
const CREST = 0.2;
|
|
30
|
+
/** Share of cells that keep a crumb for a moment after the band has passed. */
|
|
31
|
+
const CRUMBS = 0.06;
|
|
32
|
+
/** Seconds the longest crumb outlives the band. */
|
|
33
|
+
const CRUMB_S = 0.5;
|
|
34
|
+
/** The front starts this far out, so the wave leaves her face rather than covering it. */
|
|
35
|
+
const HOLE = 0.1;
|
|
36
|
+
/** Past the farthest corner, the band and the longest crumb, the wave is over. */
|
|
37
|
+
const DONE_S = (1.2 + CREST) / SPEED + CRUMB_S;
|
|
38
|
+
|
|
39
|
+
type Cell = {
|
|
40
|
+
x: number;
|
|
41
|
+
y: number;
|
|
42
|
+
dd: number;
|
|
43
|
+
seed: number;
|
|
44
|
+
grain: number;
|
|
45
|
+
speck: number;
|
|
46
|
+
roll: number;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const FONT = `700 ${GLYPH}px ui-monospace, SFMono-Regular, Menlo, monospace`;
|
|
50
|
+
|
|
51
|
+
/** An emoji's square on the sheet: emoji reach past their font size. */
|
|
52
|
+
const SLOT = Math.ceil(GLYPH * 1.5);
|
|
53
|
+
|
|
54
|
+
/** The emoji drawn once side by side, so a frame copies squares rather than drawing emoji. */
|
|
55
|
+
function sheet(glyphs: readonly string[]) {
|
|
56
|
+
const canvas = document.createElement("canvas");
|
|
57
|
+
canvas.width = SLOT * glyphs.length;
|
|
58
|
+
canvas.height = SLOT;
|
|
59
|
+
const ctx = canvas.getContext("2d");
|
|
60
|
+
if (!ctx) return null;
|
|
61
|
+
ctx.textAlign = "center";
|
|
62
|
+
ctx.textBaseline = "middle";
|
|
63
|
+
ctx.font = FONT;
|
|
64
|
+
glyphs.forEach((glyph, at) => {
|
|
65
|
+
ctx.fillText(glyph, at * SLOT + SLOT / 2, SLOT / 2);
|
|
66
|
+
});
|
|
67
|
+
return canvas;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Her face fades in over this long as the screen loads (face.tsx `duration-700`); the wave leaves it then. */
|
|
71
|
+
const FACE_IN_MS = 700;
|
|
72
|
+
|
|
73
|
+
const still = () =>
|
|
74
|
+
window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* When the screen loads and when a call connects, one wave of her glyphs leaves her face and rolls out to every corner
|
|
78
|
+
* of the window: a round front made ragged, and crumbs left where it passed. It is drawn
|
|
79
|
+
* over the whole screen, never takes a click, and does not play when the system asks
|
|
80
|
+
* for less motion. Placed inside the box her face is laid out in; the wave starts at its centre.
|
|
81
|
+
*/
|
|
82
|
+
export function ConnectWave({
|
|
83
|
+
status,
|
|
84
|
+
charset,
|
|
85
|
+
}: {
|
|
86
|
+
status: CallStatus;
|
|
87
|
+
charset: AsciiCharset;
|
|
88
|
+
}) {
|
|
89
|
+
const anchor = useRef<HTMLSpanElement>(null);
|
|
90
|
+
const canvas = useRef<HTMLCanvasElement>(null);
|
|
91
|
+
const was = useRef(status);
|
|
92
|
+
const [playing, setPlaying] = useState(false);
|
|
93
|
+
// the first-run intro lies over the call screen, and a wave under it is never seen
|
|
94
|
+
const held = useCallHeld();
|
|
95
|
+
const heldNow = useRef(held);
|
|
96
|
+
heldNow.current = held;
|
|
97
|
+
// Emoji are drawn as the screen loads: the first emoji a page draws is slow, and the
|
|
98
|
+
// moment a call picks up is the wrong moment for it
|
|
99
|
+
const emoji = useRef<HTMLCanvasElement | null>(null);
|
|
100
|
+
useEffect(() => {
|
|
101
|
+
emoji.current = charset === "ascii" ? null : sheet(EMOJI_POOL);
|
|
102
|
+
}, [charset]);
|
|
103
|
+
|
|
104
|
+
useEffect(() => {
|
|
105
|
+
const from = was.current;
|
|
106
|
+
was.current = status;
|
|
107
|
+
// from connecting to anything but connecting, hanging up or failing is a call that picked up
|
|
108
|
+
if (
|
|
109
|
+
from !== "connecting" ||
|
|
110
|
+
status === "connecting" ||
|
|
111
|
+
status === "idle" ||
|
|
112
|
+
status === "ending"
|
|
113
|
+
)
|
|
114
|
+
return;
|
|
115
|
+
if (still()) return;
|
|
116
|
+
setPlaying(true);
|
|
117
|
+
}, [status]);
|
|
118
|
+
|
|
119
|
+
// Once per load, as her face arrives: after the fonts, so the emoji are drawn ready
|
|
120
|
+
useEffect(() => {
|
|
121
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
122
|
+
let gone = false;
|
|
123
|
+
void document.fonts.ready.then(() => {
|
|
124
|
+
if (gone) return;
|
|
125
|
+
timer = setTimeout(() => {
|
|
126
|
+
if (heldNow.current || still()) return;
|
|
127
|
+
setPlaying(true);
|
|
128
|
+
}, FACE_IN_MS);
|
|
129
|
+
});
|
|
130
|
+
return () => {
|
|
131
|
+
gone = true;
|
|
132
|
+
clearTimeout(timer);
|
|
133
|
+
};
|
|
134
|
+
}, []);
|
|
135
|
+
|
|
136
|
+
useEffect(() => {
|
|
137
|
+
const element = canvas.current;
|
|
138
|
+
const box = anchor.current?.getBoundingClientRect();
|
|
139
|
+
if (!playing || !element || !box) return;
|
|
140
|
+
const w = window.innerWidth;
|
|
141
|
+
const h = window.innerHeight;
|
|
142
|
+
const cx = box.left + box.width / 2;
|
|
143
|
+
const cy = box.top + box.height / 2;
|
|
144
|
+
const far = Math.hypot(Math.max(cx, w - cx), Math.max(cy, h - cy));
|
|
145
|
+
const cols = Math.ceil(w / CELL);
|
|
146
|
+
const rows = Math.ceil(h / CELL);
|
|
147
|
+
const cells: Cell[] = [];
|
|
148
|
+
for (let r = 0; r < rows; r++)
|
|
149
|
+
for (let c = 0; c < cols; c++) {
|
|
150
|
+
if (hash(c + 53, r + 5) > KEEP) continue;
|
|
151
|
+
const x = c * CELL + CELL / 2;
|
|
152
|
+
const y = r * CELL + CELL / 2;
|
|
153
|
+
const angle = Math.atan2(y - cy, x - cx);
|
|
154
|
+
const grain = hash(c + 31, r + 17);
|
|
155
|
+
// a few slow lobes and some grain, so the front is round but never a clean circle
|
|
156
|
+
const warp =
|
|
157
|
+
1 +
|
|
158
|
+
0.1 * Math.sin(3 * angle + 1.3) +
|
|
159
|
+
0.05 * Math.sin(7 * angle - 0.7) +
|
|
160
|
+
(grain - 0.5) * 0.1;
|
|
161
|
+
const dd = (Math.hypot(x - cx, y - cy) / far) * warp;
|
|
162
|
+
if (dd < HOLE) continue;
|
|
163
|
+
cells.push({
|
|
164
|
+
x,
|
|
165
|
+
y,
|
|
166
|
+
dd,
|
|
167
|
+
seed: hash(c, r),
|
|
168
|
+
grain,
|
|
169
|
+
speck: hash(c + 7, r + 101),
|
|
170
|
+
roll: hash(c + 211, r + 89),
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
// nearest first, so a frame visits only the cells between the crumbs and the front
|
|
174
|
+
cells.sort((a, b) => a.dd - b.dd);
|
|
175
|
+
const from = (dd: number) => {
|
|
176
|
+
let lo = 0;
|
|
177
|
+
let hi = cells.length;
|
|
178
|
+
while (lo < hi) {
|
|
179
|
+
const mid = (lo + hi) >> 1;
|
|
180
|
+
if (cells[mid].dd < dd) lo = mid + 1;
|
|
181
|
+
else hi = mid;
|
|
182
|
+
}
|
|
183
|
+
return lo;
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
// One pixel per CSS pixel: the wave is gone in two seconds, and a retina-sized
|
|
187
|
+
// canvas over the whole window is four times the pixels to put on screen every frame
|
|
188
|
+
element.width = w;
|
|
189
|
+
element.height = h;
|
|
190
|
+
const ctx = element.getContext("2d");
|
|
191
|
+
const emojis = emoji.current ?? sheet(EMOJI_POOL);
|
|
192
|
+
if (!ctx || !emojis) return;
|
|
193
|
+
ctx.textAlign = "center";
|
|
194
|
+
ctx.textBaseline = "middle";
|
|
195
|
+
ctx.font = FONT;
|
|
196
|
+
// her ink is the page's foreground: black on light, white on dark
|
|
197
|
+
ctx.fillStyle = getComputedStyle(element).color;
|
|
198
|
+
const top = RAMP.length - 1;
|
|
199
|
+
// the same glyphs as her face: all emoji, emoji over the bright ascii, or ascii alone
|
|
200
|
+
const emojiAt = (c: Cell, level: number) =>
|
|
201
|
+
charset === "emojiOnly" ||
|
|
202
|
+
(charset === "emoji" && c.roll < EMOJI_RATIO && level >= EMOJI_MIN_LEVEL);
|
|
203
|
+
const rate = charset === "emojiOnly" ? EMOJI_CHAR_RATE : CHAR_RATE;
|
|
204
|
+
const half = SLOT / 2;
|
|
205
|
+
|
|
206
|
+
const t0 = performance.now();
|
|
207
|
+
let frame = 0;
|
|
208
|
+
let raf = 0;
|
|
209
|
+
const tick = () => {
|
|
210
|
+
const t = (performance.now() - t0) / 1000;
|
|
211
|
+
if (t > DONE_S) return setPlaying(false);
|
|
212
|
+
raf = requestAnimationFrame(tick);
|
|
213
|
+
if (frame++ % 2) return;
|
|
214
|
+
const R = t * SPEED;
|
|
215
|
+
ctx.clearRect(0, 0, w, h);
|
|
216
|
+
for (let i = from(R - CREST - CRUMB_S * SPEED); i < cells.length; i++) {
|
|
217
|
+
const c = cells[i];
|
|
218
|
+
const behind = R - c.dd;
|
|
219
|
+
if (behind < -0.03) break;
|
|
220
|
+
let v =
|
|
221
|
+
smoothstep(-0.03, 0.02, behind) *
|
|
222
|
+
(1 - smoothstep(0.02, CREST, behind)) *
|
|
223
|
+
(0.72 + 0.28 * Math.sin(c.dd * 46 - t * 7 + c.seed * 6.283));
|
|
224
|
+
if (c.speck < CRUMBS) {
|
|
225
|
+
const life = 0.18 + c.grain * (CRUMB_S - 0.18);
|
|
226
|
+
const since = (behind - CREST) / SPEED;
|
|
227
|
+
v = Math.max(
|
|
228
|
+
v,
|
|
229
|
+
(0.3 + 0.4 * c.grain) * (1 - smoothstep(life * 0.4, life, since)),
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
const level = Math.round(v * top);
|
|
233
|
+
if (level < 1) continue;
|
|
234
|
+
const turn = Math.floor(t * rate + c.seed * 10);
|
|
235
|
+
if (emojiAt(c, level)) {
|
|
236
|
+
// emoji keep their own colour, so only alpha varies, as on her face
|
|
237
|
+
ctx.globalAlpha = 0.35 + (level / top) * 0.65;
|
|
238
|
+
const at = Math.floor(hash(c.seed * 97, turn) * EMOJI_POOL.length);
|
|
239
|
+
ctx.drawImage(
|
|
240
|
+
emojis,
|
|
241
|
+
at * SLOT,
|
|
242
|
+
0,
|
|
243
|
+
SLOT,
|
|
244
|
+
SLOT,
|
|
245
|
+
c.x - half,
|
|
246
|
+
c.y - half,
|
|
247
|
+
SLOT,
|
|
248
|
+
SLOT,
|
|
249
|
+
);
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
ctx.globalAlpha = ALPHA_TOP * (level / top);
|
|
253
|
+
const set = RAMP[level];
|
|
254
|
+
ctx.fillText(
|
|
255
|
+
set[Math.floor(hash(c.seed * 131, turn) * set.length)],
|
|
256
|
+
c.x,
|
|
257
|
+
c.y,
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
ctx.globalAlpha = 1;
|
|
261
|
+
};
|
|
262
|
+
raf = requestAnimationFrame(tick);
|
|
263
|
+
return () => cancelAnimationFrame(raf);
|
|
264
|
+
}, [playing, charset]);
|
|
265
|
+
|
|
266
|
+
return (
|
|
267
|
+
<>
|
|
268
|
+
<span
|
|
269
|
+
ref={anchor}
|
|
270
|
+
aria-hidden
|
|
271
|
+
className="pointer-events-none absolute inset-0"
|
|
272
|
+
/>
|
|
273
|
+
{playing &&
|
|
274
|
+
createPortal(
|
|
275
|
+
<canvas
|
|
276
|
+
ref={canvas}
|
|
277
|
+
aria-hidden
|
|
278
|
+
className="pointer-events-none fixed inset-0 z-30 size-full text-foreground"
|
|
279
|
+
/>,
|
|
280
|
+
document.body,
|
|
281
|
+
)}
|
|
282
|
+
</>
|
|
283
|
+
);
|
|
284
|
+
}
|
|
@@ -104,14 +104,16 @@ const HOVER_INK = 0.9;
|
|
|
104
104
|
/** The words are 17px on 1.675; the level turn's first line sits here against the face's middle. */
|
|
105
105
|
const LINE = 17 * 1.675;
|
|
106
106
|
const LIFT = -40;
|
|
107
|
-
/** Between her
|
|
107
|
+
/** Between her level words and what stands under them. */
|
|
108
108
|
const UNDER_GAP = 8;
|
|
109
|
+
/** The height key of what stands under the level turn, beside the turns' own ids. */
|
|
110
|
+
const UNDER = "(under)";
|
|
109
111
|
|
|
110
112
|
/**
|
|
111
113
|
* The conversation beside the face, no plates and no names: a filled dot at
|
|
112
114
|
* the head of each side's level turn says whose it is, and pulses while that
|
|
113
|
-
* turn is still being said. Anchored
|
|
114
|
-
* the face.
|
|
115
|
+
* turn is still being said. Anchored past her canvas (the face box's
|
|
116
|
+
* `--face-bleed` beyond its edge) so nothing covers the face.
|
|
115
117
|
*/
|
|
116
118
|
export function SideCaptions({
|
|
117
119
|
turns,
|
|
@@ -120,13 +122,14 @@ export function SideCaptions({
|
|
|
120
122
|
onPick,
|
|
121
123
|
under = null,
|
|
122
124
|
ahead = false,
|
|
125
|
+
workOf,
|
|
123
126
|
}: {
|
|
124
127
|
turns: Turn[];
|
|
125
128
|
pinned: Pinned;
|
|
126
129
|
/** The last turn is still being said. */
|
|
127
130
|
live: boolean;
|
|
128
131
|
onPick: (role: Role, at: number) => void;
|
|
129
|
-
/**
|
|
132
|
+
/** Her turn in the making: the work under way before she has said anything to it. */
|
|
130
133
|
under?: ReactNode;
|
|
131
134
|
/**
|
|
132
135
|
* That work is her turn in the making — theirs came last and she has not spoken yet — so
|
|
@@ -134,6 +137,8 @@ export function SideCaptions({
|
|
|
134
137
|
* new turn.
|
|
135
138
|
*/
|
|
136
139
|
ahead?: boolean;
|
|
140
|
+
/** The work behind one of her turns, drawn under it while it is level; null for none. */
|
|
141
|
+
workOf?: (turn: string) => ReactNode;
|
|
137
142
|
}) {
|
|
138
143
|
const last = turns.at(-1);
|
|
139
144
|
return (
|
|
@@ -148,6 +153,7 @@ export function SideCaptions({
|
|
|
148
153
|
onPick={(at) => onPick(role, at)}
|
|
149
154
|
under={role === "assistant" ? under : null}
|
|
150
155
|
ahead={role === "assistant" && ahead}
|
|
156
|
+
workOf={role === "assistant" ? workOf : undefined}
|
|
151
157
|
/>
|
|
152
158
|
))}
|
|
153
159
|
</>
|
|
@@ -162,6 +168,7 @@ function SideColumn({
|
|
|
162
168
|
onPick,
|
|
163
169
|
under,
|
|
164
170
|
ahead,
|
|
171
|
+
workOf,
|
|
165
172
|
}: {
|
|
166
173
|
role: Role;
|
|
167
174
|
/** This side's turns only. */
|
|
@@ -172,6 +179,7 @@ function SideColumn({
|
|
|
172
179
|
onPick: (at: number) => void;
|
|
173
180
|
under: ReactNode;
|
|
174
181
|
ahead: boolean;
|
|
182
|
+
workOf: ((turn: string) => ReactNode) | undefined;
|
|
175
183
|
}) {
|
|
176
184
|
const mine = role === "user";
|
|
177
185
|
// Heights as laid out, before any transform: text grows while it is said, and
|
|
@@ -213,18 +221,22 @@ function SideColumn({
|
|
|
213
221
|
// Every turn is placed by its top edge and scales from it. Centring the box
|
|
214
222
|
// instead jumps a turn half a line the moment its text wraps, then slides it
|
|
215
223
|
// back once the taller box is measured a frame later
|
|
224
|
+
// Under the level line: her turn in the making, else the work behind the turn that is
|
|
225
|
+
// level — the latest, or one gone back to
|
|
226
|
+
const below = at === turns.length ? under : (workOf?.(turns[at].id) ?? null);
|
|
216
227
|
const top: number[] = Array(turns.length + 1).fill(0);
|
|
217
228
|
top[at] = -LINE / 2;
|
|
218
229
|
for (let k = at - 1; k >= 0; k--) top[k] = top[k + 1] - GAP - tall(k);
|
|
230
|
+
// what stands under a turn gone back to pushes the turns after it down, not over them
|
|
231
|
+
const room =
|
|
232
|
+
below && at < turns.length ? (heights[UNDER] ?? 0) + UNDER_GAP : 0;
|
|
219
233
|
for (let k = at + 1; k < turns.length; k++)
|
|
220
|
-
top[k] = top[k - 1] + tall(k - 1) + GAP;
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
at === turns.length
|
|
234
|
+
top[k] = top[k - 1] + tall(k - 1) + GAP + (k - 1 === at ? room : 0);
|
|
235
|
+
const underTop = !below
|
|
236
|
+
? null
|
|
237
|
+
: at === turns.length
|
|
224
238
|
? top[at]
|
|
225
|
-
: at
|
|
226
|
-
? top[at] + (heights[turns[at].id] ?? LINE) + UNDER_GAP
|
|
227
|
-
: null;
|
|
239
|
+
: top[at] + (heights[turns[at].id] ?? LINE) + UNDER_GAP;
|
|
228
240
|
|
|
229
241
|
return (
|
|
230
242
|
<div
|
|
@@ -232,8 +244,8 @@ function SideColumn({
|
|
|
232
244
|
"pointer-events-none absolute h-0 w-[min(21.25rem,24vw)]",
|
|
233
245
|
// the vanishing point is the face's edge on the middle line: what recedes goes toward her
|
|
234
246
|
mine
|
|
235
|
-
? "left-full ml-
|
|
236
|
-
: "right-full mr-
|
|
247
|
+
? "left-full ml-[calc(var(--face-bleed,0px)+0.375rem)] [perspective-origin:0_0]"
|
|
248
|
+
: "right-full mr-[calc(var(--face-bleed,0px)+0.375rem)] [perspective-origin:100%_0]",
|
|
237
249
|
)}
|
|
238
250
|
style={{ top: `calc(50% + ${LIFT}px)`, perspective: PERSPECTIVE }}
|
|
239
251
|
>
|
|
@@ -296,17 +308,17 @@ function SideColumn({
|
|
|
296
308
|
</div>
|
|
297
309
|
);
|
|
298
310
|
})}
|
|
299
|
-
{
|
|
311
|
+
{below && (
|
|
300
312
|
<div
|
|
301
|
-
|
|
313
|
+
ref={measure}
|
|
314
|
+
data-turn={UNDER}
|
|
302
315
|
style={{ transform: `translateY(${(underTop ?? 0).toFixed(1)}px)` }}
|
|
303
316
|
className={cn(
|
|
304
317
|
"absolute top-0 flex w-full transition-transform duration-[520ms] ease-[cubic-bezier(0.22,1,0.36,1)] motion-reduce:transition-none",
|
|
305
318
|
mine ? "left-0 justify-start" : "right-0 justify-end",
|
|
306
|
-
underTop === null && "invisible",
|
|
307
319
|
)}
|
|
308
320
|
>
|
|
309
|
-
{
|
|
321
|
+
{below}
|
|
310
322
|
</div>
|
|
311
323
|
)}
|
|
312
324
|
</div>
|
|
@@ -413,7 +413,7 @@ function BackendModelPicker({
|
|
|
413
413
|
<span className="min-w-0 flex-1 truncate text-sm font-medium">
|
|
414
414
|
{model.label}
|
|
415
415
|
</span>
|
|
416
|
-
{picked && <Check className="size-3.5 shrink-0
|
|
416
|
+
{picked && <Check className="size-3.5 shrink-0" />}
|
|
417
417
|
</span>
|
|
418
418
|
<span className="block truncate font-mono text-[11px] text-muted-foreground">
|
|
419
419
|
{model.id} · {model.tier}
|
|
@@ -776,7 +776,7 @@ function Captions({
|
|
|
776
776
|
{CAPTION_LABEL[view].hint}
|
|
777
777
|
</span>
|
|
778
778
|
</span>
|
|
779
|
-
{picked && <Check className="size-4 shrink-0
|
|
779
|
+
{picked && <Check className="size-4 shrink-0" />}
|
|
780
780
|
</span>
|
|
781
781
|
</button>
|
|
782
782
|
);
|