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
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
+
import { format, formatDistanceToNowStrict } from "date-fns";
|
|
3
4
|
import {
|
|
5
|
+
CalendarDays,
|
|
4
6
|
ChevronDown,
|
|
5
7
|
ChevronRight,
|
|
6
8
|
Loader2,
|
|
@@ -9,7 +11,7 @@ import {
|
|
|
9
11
|
Trash2,
|
|
10
12
|
X,
|
|
11
13
|
} from "lucide-react";
|
|
12
|
-
import { useCallback, useEffect, useState } from "react";
|
|
14
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
13
15
|
import { queryKey } from "@/app/api/query-key";
|
|
14
16
|
import { Button } from "@/components/ui/button";
|
|
15
17
|
import {
|
|
@@ -56,6 +58,7 @@ import {
|
|
|
56
58
|
updateRoutineAction,
|
|
57
59
|
} from "../routine.action";
|
|
58
60
|
import {
|
|
61
|
+
momentOf,
|
|
59
62
|
nextRun,
|
|
60
63
|
type Routine,
|
|
61
64
|
type RoutineInput,
|
|
@@ -283,11 +286,23 @@ type Draft = {
|
|
|
283
286
|
bot: string;
|
|
284
287
|
request: string;
|
|
285
288
|
kind: RoutineSchedule["kind"];
|
|
289
|
+
/** The time of day, for one that starts once and one on set days alike. */
|
|
286
290
|
time: string;
|
|
287
291
|
days: number[];
|
|
288
|
-
hours:
|
|
292
|
+
hours: number;
|
|
293
|
+
/** The day one that starts once starts, "YYYY-MM-DD". */
|
|
294
|
+
date: string;
|
|
289
295
|
};
|
|
290
296
|
|
|
297
|
+
const pad = (n: number) => String(n).padStart(2, "0");
|
|
298
|
+
|
|
299
|
+
/** A day in this machine's own time as "YYYY-MM-DD", `ahead` days after `from`. */
|
|
300
|
+
function dayOf(ahead: number, from = new Date()): string {
|
|
301
|
+
const at = new Date(from);
|
|
302
|
+
at.setDate(at.getDate() + ahead);
|
|
303
|
+
return `${at.getFullYear()}-${pad(at.getMonth() + 1)}-${pad(at.getDate())}`;
|
|
304
|
+
}
|
|
305
|
+
|
|
291
306
|
const draftOf = (routine: Routine | null, bots: Bot[]): Draft => {
|
|
292
307
|
const schedule = routine?.schedule;
|
|
293
308
|
return {
|
|
@@ -295,22 +310,44 @@ const draftOf = (routine: Routine | null, bots: Bot[]): Draft => {
|
|
|
295
310
|
bot: routine?.bot ?? bots.find((bot) => !bot.disabled)?.name ?? "",
|
|
296
311
|
request: routine?.request ?? "",
|
|
297
312
|
kind: schedule?.kind ?? "daily",
|
|
298
|
-
time:
|
|
313
|
+
time:
|
|
314
|
+
schedule?.kind === "daily"
|
|
315
|
+
? schedule.time
|
|
316
|
+
: schedule?.kind === "once"
|
|
317
|
+
? schedule.at.slice(11)
|
|
318
|
+
: "09:00",
|
|
299
319
|
days: schedule?.kind === "daily" ? schedule.days : [...WEEKDAYS],
|
|
300
|
-
hours: schedule?.kind === "every" ?
|
|
320
|
+
hours: schedule?.kind === "every" ? schedule.hours : 6,
|
|
321
|
+
date: schedule?.kind === "once" ? schedule.at.slice(0, 10) : dayOf(1),
|
|
301
322
|
};
|
|
302
323
|
};
|
|
303
324
|
|
|
304
|
-
/** The schedule a draft spells; null while it spells none (no day picked,
|
|
325
|
+
/** The schedule a draft spells; null while it spells none (no day picked, no time). */
|
|
305
326
|
function scheduleOf(draft: Draft): RoutineSchedule | null {
|
|
327
|
+
const timed = /^\d\d:\d\d$/.test(draft.time);
|
|
328
|
+
if (draft.kind === "once")
|
|
329
|
+
return timed && draft.date
|
|
330
|
+
? { kind: "once", at: `${draft.date} ${draft.time}` }
|
|
331
|
+
: null;
|
|
306
332
|
if (draft.kind === "daily")
|
|
307
|
-
return draft.days.length &&
|
|
333
|
+
return draft.days.length && timed
|
|
308
334
|
? { kind: "daily", time: draft.time, days: draft.days }
|
|
309
335
|
: null;
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
336
|
+
return { kind: "every", hours: draft.hours };
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/** One that starts once, at a moment already gone: the server refuses it too (routine.query). */
|
|
340
|
+
const isSpent = (schedule: RoutineSchedule | null) =>
|
|
341
|
+
schedule?.kind === "once" && momentOf(schedule.at) <= new Date();
|
|
342
|
+
|
|
343
|
+
/** An hour from now, on the next five minutes: what "In an hour" sets. */
|
|
344
|
+
function inAnHour(): Pick<Draft, "date" | "time"> {
|
|
345
|
+
const at = new Date(Date.now() + 3_600_000);
|
|
346
|
+
at.setMinutes(Math.ceil(at.getMinutes() / 5) * 5, 0, 0);
|
|
347
|
+
return {
|
|
348
|
+
date: dayOf(0, at),
|
|
349
|
+
time: `${pad(at.getHours())}:${pad(at.getMinutes())}`,
|
|
350
|
+
};
|
|
314
351
|
}
|
|
315
352
|
|
|
316
353
|
const DAY_SHORT = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
|
|
@@ -329,6 +366,10 @@ const DAY_WORDS = [
|
|
|
329
366
|
"Saturday",
|
|
330
367
|
"Sunday",
|
|
331
368
|
];
|
|
369
|
+
/** The gaps between starts most routines want; another one, set by a call, shows beside them. */
|
|
370
|
+
const HOUR_STEPS = [1, 2, 3, 6, 12, 24].filter(
|
|
371
|
+
(hours) => hours >= ROUTINE.minHours,
|
|
372
|
+
);
|
|
332
373
|
|
|
333
374
|
/**
|
|
334
375
|
* A routine on a sheet down the settings window's right edge, where a thread opens
|
|
@@ -378,7 +419,11 @@ function RoutineSheet({
|
|
|
378
419
|
|
|
379
420
|
const schedule = scheduleOf(draft);
|
|
380
421
|
const input: RoutineInput | null =
|
|
381
|
-
schedule &&
|
|
422
|
+
schedule &&
|
|
423
|
+
!isSpent(schedule) &&
|
|
424
|
+
draft.label.trim() &&
|
|
425
|
+
draft.bot &&
|
|
426
|
+
draft.request.trim()
|
|
382
427
|
? {
|
|
383
428
|
label: draft.label.trim(),
|
|
384
429
|
bot: draft.bot,
|
|
@@ -388,13 +433,18 @@ function RoutineSheet({
|
|
|
388
433
|
: null;
|
|
389
434
|
|
|
390
435
|
/** One field of a routine that exists, written when it differs from what is kept. */
|
|
391
|
-
const commit = (next: Partial<RoutineInput>) => {
|
|
436
|
+
const commit = (next: Partial<RoutineInput> & { enabled?: boolean }) => {
|
|
392
437
|
if (saved) void update(saved.id, next).catch(() => {});
|
|
393
438
|
};
|
|
394
439
|
const commitSchedule = (next: Partial<Draft>) => {
|
|
395
440
|
patch(next);
|
|
396
441
|
const spelled = scheduleOf({ ...draft, ...next });
|
|
397
|
-
if (spelled
|
|
442
|
+
if (!spelled || isSpent(spelled)) return;
|
|
443
|
+
// Picking the moment of one that starts once sets it, whether or not it already ran
|
|
444
|
+
commit({
|
|
445
|
+
schedule: spelled,
|
|
446
|
+
...(spelled.kind === "once" ? { enabled: true } : {}),
|
|
447
|
+
});
|
|
398
448
|
};
|
|
399
449
|
|
|
400
450
|
const confirmDelete = async () => {
|
|
@@ -593,137 +643,118 @@ function RoutineSheet({
|
|
|
593
643
|
</Field>
|
|
594
644
|
|
|
595
645
|
<Field label="When">
|
|
596
|
-
<
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
{
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
return (
|
|
629
|
-
<button
|
|
630
|
-
key={day}
|
|
631
|
-
type="button"
|
|
632
|
-
aria-pressed={on}
|
|
633
|
-
aria-label={DAY_WORDS[day - 1]}
|
|
634
|
-
onClick={() =>
|
|
635
|
-
commitSchedule({
|
|
636
|
-
days: on
|
|
637
|
-
? draft.days.filter((one) => one !== day)
|
|
638
|
-
: [...draft.days, day].sort(),
|
|
639
|
-
})
|
|
640
|
-
}
|
|
641
|
-
className={cn(
|
|
642
|
-
"flex h-7 items-center justify-center rounded-full px-2.5 text-[12px] outline-none transition-colors focus-visible:ring-3 focus-visible:ring-ring/50",
|
|
643
|
-
on
|
|
644
|
-
? "bg-primary text-primary-foreground"
|
|
645
|
-
: "text-muted-foreground ring-1 ring-border ring-inset hover:text-foreground",
|
|
646
|
-
)}
|
|
646
|
+
<div className="space-y-3.5">
|
|
647
|
+
<Segmented
|
|
648
|
+
aria-label="How it starts"
|
|
649
|
+
value={draft.kind}
|
|
650
|
+
onChange={(kind) => commitSchedule({ kind })}
|
|
651
|
+
options={[
|
|
652
|
+
{ value: "once", label: "Once" },
|
|
653
|
+
{ value: "daily", label: "On set days" },
|
|
654
|
+
{ value: "every", label: "Every few hours" },
|
|
655
|
+
]}
|
|
656
|
+
/>
|
|
657
|
+
<div className="space-y-2.5">
|
|
658
|
+
{draft.kind === "once" ? (
|
|
659
|
+
<>
|
|
660
|
+
<WhenRow word="On">
|
|
661
|
+
<OnceDays
|
|
662
|
+
value={draft.date}
|
|
663
|
+
onPick={(date) => commitSchedule({ date })}
|
|
664
|
+
/>
|
|
665
|
+
</WhenRow>
|
|
666
|
+
<WhenRow word="At">
|
|
667
|
+
<TimeField
|
|
668
|
+
value={draft.time}
|
|
669
|
+
onChange={(time) => patch({ time })}
|
|
670
|
+
onDone={() => commitSchedule({})}
|
|
671
|
+
/>
|
|
672
|
+
<span className="text-[13px] text-muted-foreground">
|
|
673
|
+
or
|
|
674
|
+
</span>
|
|
675
|
+
<Pill
|
|
676
|
+
picked={false}
|
|
677
|
+
onClick={() => commitSchedule(inAnHour())}
|
|
647
678
|
>
|
|
648
|
-
|
|
649
|
-
</
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
679
|
+
In an hour
|
|
680
|
+
</Pill>
|
|
681
|
+
</WhenRow>
|
|
682
|
+
</>
|
|
683
|
+
) : draft.kind === "daily" ? (
|
|
684
|
+
<>
|
|
685
|
+
<WhenRow word="On">
|
|
686
|
+
{WEEKDAYS.map((day) => {
|
|
687
|
+
const on = draft.days.includes(day);
|
|
688
|
+
return (
|
|
689
|
+
<Pill
|
|
690
|
+
key={day}
|
|
691
|
+
picked={on}
|
|
692
|
+
aria-label={DAY_WORDS[day - 1]}
|
|
693
|
+
onClick={() =>
|
|
694
|
+
commitSchedule({
|
|
695
|
+
days: on
|
|
696
|
+
? draft.days.filter(
|
|
697
|
+
(one) => one !== day,
|
|
698
|
+
)
|
|
699
|
+
: [...draft.days, day].sort(
|
|
700
|
+
(a, b) => a - b,
|
|
701
|
+
),
|
|
702
|
+
})
|
|
703
|
+
}
|
|
704
|
+
>
|
|
705
|
+
{DAY_SHORT[day - 1]}
|
|
706
|
+
</Pill>
|
|
707
|
+
);
|
|
708
|
+
})}
|
|
709
|
+
</WhenRow>
|
|
710
|
+
{/* Shortcuts that fill the days: what is on is what the days show */}
|
|
711
|
+
<WhenRow>
|
|
712
|
+
{DAY_SETS.map((set) => (
|
|
713
|
+
<button
|
|
714
|
+
key={set.label}
|
|
715
|
+
type="button"
|
|
716
|
+
onClick={() =>
|
|
717
|
+
commitSchedule({ days: [...set.days] })
|
|
718
|
+
}
|
|
719
|
+
className="h-6.5 rounded-full bg-muted px-2.5 text-xs text-muted-foreground outline-none transition-colors hover:text-foreground focus-visible:ring-3 focus-visible:ring-ring/50"
|
|
720
|
+
>
|
|
721
|
+
{set.label}
|
|
722
|
+
</button>
|
|
723
|
+
))}
|
|
724
|
+
</WhenRow>
|
|
725
|
+
<WhenRow word="At">
|
|
726
|
+
<TimeField
|
|
727
|
+
value={draft.time}
|
|
728
|
+
onChange={(time) => patch({ time })}
|
|
729
|
+
onDone={() => commitSchedule({})}
|
|
730
|
+
/>
|
|
731
|
+
</WhenRow>
|
|
732
|
+
</>
|
|
733
|
+
) : (
|
|
734
|
+
<WhenRow word="Every">
|
|
735
|
+
{(HOUR_STEPS.includes(draft.hours)
|
|
736
|
+
? HOUR_STEPS
|
|
737
|
+
: [...HOUR_STEPS, draft.hours].sort(
|
|
738
|
+
(a, b) => a - b,
|
|
739
|
+
)
|
|
740
|
+
).map((hours) => (
|
|
741
|
+
<Pill
|
|
742
|
+
key={hours}
|
|
743
|
+
picked={hours === draft.hours}
|
|
744
|
+
onClick={() => commitSchedule({ hours })}
|
|
745
|
+
className="min-w-10 px-3"
|
|
671
746
|
>
|
|
672
|
-
{
|
|
673
|
-
</
|
|
674
|
-
)
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
</span>
|
|
684
|
-
<Input
|
|
685
|
-
inputMode="numeric"
|
|
686
|
-
aria-label="Hours between starts"
|
|
687
|
-
value={draft.hours}
|
|
688
|
-
onChange={(event) =>
|
|
689
|
-
patch({ hours: event.target.value })
|
|
690
|
-
}
|
|
691
|
-
onBlur={() => commitSchedule({})}
|
|
692
|
-
className="w-16 font-mono text-[13px]"
|
|
693
|
-
/>
|
|
694
|
-
<span className="text-sm text-muted-foreground">
|
|
695
|
-
hours
|
|
696
|
-
</span>
|
|
697
|
-
</div>
|
|
698
|
-
<Hint>
|
|
699
|
-
No more often than every{" "}
|
|
700
|
-
{ROUTINE.minHours === 1
|
|
701
|
-
? "hour"
|
|
702
|
-
: `${ROUTINE.minHours} hours`}
|
|
703
|
-
, counted from its last start.
|
|
704
|
-
</Hint>
|
|
705
|
-
</>
|
|
706
|
-
)}
|
|
707
|
-
{schedule ? (
|
|
708
|
-
// What was picked, said back in words: the check that the form was read right
|
|
709
|
-
<p className="flex items-center gap-1.5 pt-1 text-[13px]">
|
|
710
|
-
<RoutineMark className="size-3.5 shrink-0 text-muted-foreground" />
|
|
711
|
-
<span className="min-w-0 truncate">
|
|
712
|
-
{scheduleText(schedule)}
|
|
713
|
-
<span className="text-muted-foreground">
|
|
714
|
-
{" "}
|
|
715
|
-
· first start{" "}
|
|
716
|
-
{whenOf(nextRun(schedule, new Date()))}
|
|
717
|
-
</span>
|
|
718
|
-
</span>
|
|
719
|
-
</p>
|
|
720
|
-
) : (
|
|
721
|
-
<p className="text-xs text-destructive">
|
|
722
|
-
{draft.kind === "daily"
|
|
723
|
-
? "Pick a time and at least one day."
|
|
724
|
-
: `A whole number of hours, ${ROUTINE.minHours} or more.`}
|
|
725
|
-
</p>
|
|
726
|
-
)}
|
|
747
|
+
{hours}
|
|
748
|
+
</Pill>
|
|
749
|
+
))}
|
|
750
|
+
<span className="text-[13px] text-muted-foreground">
|
|
751
|
+
hours
|
|
752
|
+
</span>
|
|
753
|
+
</WhenRow>
|
|
754
|
+
)}
|
|
755
|
+
</div>
|
|
756
|
+
<WhenSaid schedule={schedule} kind={draft.kind} />
|
|
757
|
+
</div>
|
|
727
758
|
</Field>
|
|
728
759
|
|
|
729
760
|
<Field label="Job" htmlFor="routine-request">
|
|
@@ -842,7 +873,7 @@ function missingOf(draft: Draft, schedule: RoutineSchedule | null): string {
|
|
|
842
873
|
const lacks = [
|
|
843
874
|
draft.label.trim() ? null : "a name",
|
|
844
875
|
draft.bot ? null : "a bot",
|
|
845
|
-
schedule ?
|
|
876
|
+
!schedule ? "a time" : isSpent(schedule) ? "a time still ahead" : null,
|
|
846
877
|
draft.request.trim() ? null : "a job",
|
|
847
878
|
].filter((one): one is string => one !== null);
|
|
848
879
|
return lacks.length > 1
|
|
@@ -854,6 +885,164 @@ const Hint = ({ children }: { children: React.ReactNode }) => (
|
|
|
854
885
|
<p className="text-xs text-muted-foreground">{children}</p>
|
|
855
886
|
);
|
|
856
887
|
|
|
888
|
+
/**
|
|
889
|
+
* One choice of the When field. What is picked is filled black and what is not is a
|
|
890
|
+
* hairline, as everywhere that something is picked, so no pill is left to read twice.
|
|
891
|
+
*/
|
|
892
|
+
function Pill({
|
|
893
|
+
picked,
|
|
894
|
+
className,
|
|
895
|
+
...button
|
|
896
|
+
}: React.ComponentProps<"button"> & { picked: boolean }) {
|
|
897
|
+
return (
|
|
898
|
+
<button
|
|
899
|
+
type="button"
|
|
900
|
+
aria-pressed={picked}
|
|
901
|
+
className={cn(
|
|
902
|
+
"inline-flex h-8 items-center justify-center gap-1.5 rounded-full px-3.5 text-[13px] whitespace-nowrap outline-none transition-colors focus-visible:ring-3 focus-visible:ring-ring/50",
|
|
903
|
+
picked
|
|
904
|
+
? "bg-primary font-medium text-primary-foreground"
|
|
905
|
+
: "text-foreground ring-1 ring-border ring-inset hover:bg-muted",
|
|
906
|
+
className,
|
|
907
|
+
)}
|
|
908
|
+
{...button}
|
|
909
|
+
/>
|
|
910
|
+
);
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
/** One question of the When field: its word in a column of its own, then the choices. */
|
|
914
|
+
function WhenRow({
|
|
915
|
+
word,
|
|
916
|
+
children,
|
|
917
|
+
}: {
|
|
918
|
+
word?: string;
|
|
919
|
+
children: React.ReactNode;
|
|
920
|
+
}) {
|
|
921
|
+
return (
|
|
922
|
+
<div className="flex items-start gap-2.5">
|
|
923
|
+
<span className="flex h-8 w-9 shrink-0 items-center text-[13px] text-muted-foreground">
|
|
924
|
+
{word}
|
|
925
|
+
</span>
|
|
926
|
+
<div className="flex min-w-0 flex-1 flex-wrap items-center gap-1.5">
|
|
927
|
+
{children}
|
|
928
|
+
</div>
|
|
929
|
+
</div>
|
|
930
|
+
);
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
/** "Sun 21" for a day this week, "Fri, Oct 3" further out. */
|
|
934
|
+
function dayName(day: string): string {
|
|
935
|
+
const at = momentOf(`${day} 00:00`);
|
|
936
|
+
const days = (at.getTime() - momentOf(`${dayOf(0)} 00:00`).getTime()) / 864e5;
|
|
937
|
+
return format(at, days < 7 ? "EEE d" : "EEE, MMM d");
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
/** Today, tomorrow and the two days after as pills; any other day from the calendar. */
|
|
941
|
+
function OnceDays({
|
|
942
|
+
value,
|
|
943
|
+
onPick,
|
|
944
|
+
}: {
|
|
945
|
+
value: string;
|
|
946
|
+
onPick: (day: string) => void;
|
|
947
|
+
}) {
|
|
948
|
+
const picker = useRef<HTMLInputElement>(null);
|
|
949
|
+
const near = [0, 1, 2, 3].map((ahead) => dayOf(ahead));
|
|
950
|
+
const open = () => picker.current?.showPicker();
|
|
951
|
+
return (
|
|
952
|
+
<>
|
|
953
|
+
{near.map((day, ahead) => (
|
|
954
|
+
<Pill key={day} picked={day === value} onClick={() => onPick(day)}>
|
|
955
|
+
{ahead === 0 ? "Today" : ahead === 1 ? "Tomorrow" : dayName(day)}
|
|
956
|
+
</Pill>
|
|
957
|
+
))}
|
|
958
|
+
{!near.includes(value) && (
|
|
959
|
+
<Pill picked onClick={open}>
|
|
960
|
+
{dayName(value)}
|
|
961
|
+
</Pill>
|
|
962
|
+
)}
|
|
963
|
+
{/* The browser's own calendar, opened from the pill it sits under */}
|
|
964
|
+
<span className="relative">
|
|
965
|
+
<Pill
|
|
966
|
+
picked={false}
|
|
967
|
+
aria-label="Another day"
|
|
968
|
+
onClick={open}
|
|
969
|
+
className="w-8 px-0"
|
|
970
|
+
>
|
|
971
|
+
<CalendarDays className="size-3.5" />
|
|
972
|
+
</Pill>
|
|
973
|
+
<input
|
|
974
|
+
ref={picker}
|
|
975
|
+
type="date"
|
|
976
|
+
tabIndex={-1}
|
|
977
|
+
aria-hidden
|
|
978
|
+
min={near[0]}
|
|
979
|
+
value={value}
|
|
980
|
+
onChange={(event) => event.target.value && onPick(event.target.value)}
|
|
981
|
+
className="pointer-events-none absolute inset-0 opacity-0"
|
|
982
|
+
/>
|
|
983
|
+
</span>
|
|
984
|
+
</>
|
|
985
|
+
);
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
/** A time of day, large, without the browser's clock glyph; a click opens its picker all the same. */
|
|
989
|
+
function TimeField({
|
|
990
|
+
value,
|
|
991
|
+
onChange,
|
|
992
|
+
onDone,
|
|
993
|
+
}: {
|
|
994
|
+
value: string;
|
|
995
|
+
onChange: (time: string) => void;
|
|
996
|
+
onDone: () => void;
|
|
997
|
+
}) {
|
|
998
|
+
return (
|
|
999
|
+
<Input
|
|
1000
|
+
type="time"
|
|
1001
|
+
aria-label="Time of day"
|
|
1002
|
+
value={value}
|
|
1003
|
+
onChange={(event) => onChange(event.target.value)}
|
|
1004
|
+
onBlur={onDone}
|
|
1005
|
+
onClick={(event) => event.currentTarget.showPicker?.()}
|
|
1006
|
+
className="h-8.5 w-28 px-3 text-[15px] tabular-nums md:text-[15px] [&::-webkit-calendar-picker-indicator]:hidden"
|
|
1007
|
+
/>
|
|
1008
|
+
);
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
/** What was picked, said back in words: the check that the form was read right. */
|
|
1012
|
+
function WhenSaid({
|
|
1013
|
+
schedule,
|
|
1014
|
+
kind,
|
|
1015
|
+
}: {
|
|
1016
|
+
schedule: RoutineSchedule | null;
|
|
1017
|
+
kind: RoutineSchedule["kind"];
|
|
1018
|
+
}) {
|
|
1019
|
+
if (!schedule || isSpent(schedule))
|
|
1020
|
+
return (
|
|
1021
|
+
<p className="text-xs text-destructive">
|
|
1022
|
+
{schedule
|
|
1023
|
+
? "That time has passed. Pick a later one."
|
|
1024
|
+
: kind === "daily"
|
|
1025
|
+
? "Pick a time and at least one day."
|
|
1026
|
+
: "Pick a time."}
|
|
1027
|
+
</p>
|
|
1028
|
+
);
|
|
1029
|
+
const first = nextRun(schedule, new Date());
|
|
1030
|
+
return (
|
|
1031
|
+
<p className="flex items-start gap-2 text-[13px] leading-5">
|
|
1032
|
+
<RoutineMark className="mt-0.75 size-3.5 shrink-0 text-muted-foreground" />
|
|
1033
|
+
<span className="min-w-0">
|
|
1034
|
+
{scheduleText(schedule)}
|
|
1035
|
+
<span className="text-muted-foreground">
|
|
1036
|
+
{" · "}
|
|
1037
|
+
{schedule.kind === "once"
|
|
1038
|
+
? `${formatDistanceToNowStrict(first, { addSuffix: true })}, then it switches itself off`
|
|
1039
|
+
: `first start ${whenOf(first)}`}
|
|
1040
|
+
</span>
|
|
1041
|
+
</span>
|
|
1042
|
+
</p>
|
|
1043
|
+
);
|
|
1044
|
+
}
|
|
1045
|
+
|
|
857
1046
|
/** A form row, labelled the way a bot's page labels its own. */
|
|
858
1047
|
function Field({
|
|
859
1048
|
label,
|
|
@@ -6,10 +6,12 @@ import { routineTable, threadTable } from "@/database/tables";
|
|
|
6
6
|
import { findJobBot } from "@/features/bot/bot.query";
|
|
7
7
|
import { publicError } from "@/lib/public-error";
|
|
8
8
|
import {
|
|
9
|
+
momentOf,
|
|
9
10
|
nextRun,
|
|
10
11
|
type Routine,
|
|
11
12
|
type RoutineInput,
|
|
12
13
|
type RoutineRun,
|
|
14
|
+
type RoutineSchedule,
|
|
13
15
|
} from "./routine.schema";
|
|
14
16
|
|
|
15
17
|
// Routines and the threads they opened. A run is an ordinary thread carrying `routine_id`;
|
|
@@ -75,6 +77,16 @@ async function botNamed(name: string): Promise<string> {
|
|
|
75
77
|
return bot.name;
|
|
76
78
|
}
|
|
77
79
|
|
|
80
|
+
/** A routine that starts once, switched on, has to have its moment ahead of it. */
|
|
81
|
+
function refuseSpentMoment(schedule: RoutineSchedule, enabled: boolean) {
|
|
82
|
+
if (
|
|
83
|
+
enabled &&
|
|
84
|
+
schedule.kind === "once" &&
|
|
85
|
+
momentOf(schedule.at) <= new Date()
|
|
86
|
+
)
|
|
87
|
+
publicError("That time has passed. Pick a later one.");
|
|
88
|
+
}
|
|
89
|
+
|
|
78
90
|
export async function createRoutine(input: RoutineInput): Promise<Routine> {
|
|
79
91
|
const [{ total }] = await database
|
|
80
92
|
.select({ total: count() })
|
|
@@ -83,6 +95,7 @@ export async function createRoutine(input: RoutineInput): Promise<Routine> {
|
|
|
83
95
|
publicError(
|
|
84
96
|
`There are already ${ROUTINE.max} routines. Delete one to make another.`,
|
|
85
97
|
);
|
|
98
|
+
refuseSpentMoment(input.schedule, true);
|
|
86
99
|
const [row] = await database
|
|
87
100
|
.insert(routineTable)
|
|
88
101
|
.values({
|
|
@@ -111,6 +124,8 @@ export async function updateRoutine(
|
|
|
111
124
|
if (!was) return null;
|
|
112
125
|
const schedule = patch.schedule ?? was.schedule;
|
|
113
126
|
const restarts = Boolean(patch.schedule) || (patch.enabled && !was.enabled);
|
|
127
|
+
// only a moment being given or switched on is asked about: one waiting to start is due, not spent
|
|
128
|
+
if (restarts) refuseSpentMoment(schedule, patch.enabled ?? was.enabled);
|
|
114
129
|
const [row] = await database
|
|
115
130
|
.update(routineTable)
|
|
116
131
|
.set({
|
|
@@ -147,16 +162,22 @@ export async function listDueRoutines(now: Date): Promise<Row[]> {
|
|
|
147
162
|
|
|
148
163
|
/**
|
|
149
164
|
* Moves a due routine on to its next time, and says whether this caller was the one to do
|
|
150
|
-
* it
|
|
151
|
-
* same due row open one
|
|
165
|
+
* it; one that starts once has no next time and is switched off instead. The start it was
|
|
166
|
+
* waiting for is part of the condition, so two ticks that read the same due row open one
|
|
167
|
+
* thread between them.
|
|
152
168
|
*/
|
|
153
169
|
export async function claimRoutine(row: Row, now: Date): Promise<boolean> {
|
|
154
170
|
const claimed = await database
|
|
155
171
|
.update(routineTable)
|
|
156
|
-
.set(
|
|
172
|
+
.set(
|
|
173
|
+
row.schedule.kind === "once"
|
|
174
|
+
? { enabled: false }
|
|
175
|
+
: { nextRunAt: nextRun(row.schedule, now) },
|
|
176
|
+
)
|
|
157
177
|
.where(
|
|
158
178
|
and(
|
|
159
179
|
eq(routineTable.id, row.id),
|
|
180
|
+
eq(routineTable.enabled, true),
|
|
160
181
|
eq(routineTable.nextRunAt, row.nextRunAt),
|
|
161
182
|
),
|
|
162
183
|
)
|