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
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
useMemo,
|
|
19
19
|
useRef,
|
|
20
20
|
useState,
|
|
21
|
+
useSyncExternalStore,
|
|
21
22
|
} from "react";
|
|
22
23
|
import { queryKey } from "@/app/api/query-key";
|
|
23
24
|
import { Button } from "@/components/ui/button";
|
|
@@ -74,8 +75,14 @@ import { useHotkeyLabel } from "@/hooks/use-hotkey";
|
|
|
74
75
|
import { RING_CYCLE_MS } from "@/lib/live/ring";
|
|
75
76
|
import { useServerRoute } from "@/lib/protocol/use-server-route";
|
|
76
77
|
import { cn, plainText } from "@/lib/utils";
|
|
78
|
+
import { ConnectWave } from "./connect-wave";
|
|
77
79
|
import { Face } from "./face";
|
|
78
|
-
import {
|
|
80
|
+
import {
|
|
81
|
+
SideCaptions,
|
|
82
|
+
type Turn,
|
|
83
|
+
turnsOf,
|
|
84
|
+
useTurnFocus,
|
|
85
|
+
} from "./side-captions";
|
|
79
86
|
import { TabState } from "./tab-state";
|
|
80
87
|
import { WriteLine, type WrittenCall } from "./write-line";
|
|
81
88
|
|
|
@@ -163,8 +170,10 @@ function CallScreen({
|
|
|
163
170
|
const hers =
|
|
164
171
|
messages.findLast((turn) => turn.role === "assistant")?.text ?? "";
|
|
165
172
|
// An open thread lies over the right of the call and moves none of it: her face and the
|
|
166
|
-
// captions are where they were when it closes
|
|
167
|
-
|
|
173
|
+
// captions are where they were when it closes. A window too narrow for three columns
|
|
174
|
+
// draws her last line instead, whatever the setting.
|
|
175
|
+
const wide = useWide(SIDES_MIN_WIDTH);
|
|
176
|
+
const sided = captionView === "sides" && status !== "idle" && wide;
|
|
168
177
|
const talk = useMemo(() => turnsOf(messages), [messages]);
|
|
169
178
|
const turns = useTurnFocus(talk, sided);
|
|
170
179
|
const lastRole = talk.at(-1)?.role;
|
|
@@ -178,6 +187,10 @@ function CallScreen({
|
|
|
178
187
|
if (lastRole !== "user") setMaking(false);
|
|
179
188
|
else if (work.on) setMaking(true);
|
|
180
189
|
}, [lastRole, work.on]);
|
|
190
|
+
// The work behind each of her turns stays with it for the call: what ran while she was on
|
|
191
|
+
// it, or before she answered, is kept under her words and shows again when she is gone
|
|
192
|
+
// back to. A later stretch of work on the same turn adds to it
|
|
193
|
+
const kept = useKeptWork(talk, work.lines);
|
|
181
194
|
// the last turn is still being said: her voice is on, or yours came after hers and she has not answered
|
|
182
195
|
const saying =
|
|
183
196
|
(lastRole === "assistant" && status === "speaking") ||
|
|
@@ -196,7 +209,11 @@ function CallScreen({
|
|
|
196
209
|
{/* Top padding in vh, like the face itself, so the face+text column sits below center */}
|
|
197
210
|
<div className="flex min-h-0 flex-1 flex-col items-center justify-center gap-5 pt-[7vh]">
|
|
198
211
|
{/* The face is the control. It reacts to the agent's own voice. */}
|
|
199
|
-
|
|
212
|
+
{/* The layout box is the part of her face she fills while talking; the canvas draws
|
|
213
|
+
`--face-bleed` past it on every side for the room her words, the tail she works
|
|
214
|
+
with and the gathering need, so what sits under her is not pushed away by empty
|
|
215
|
+
field. What stands beside her stands past the canvas (SideCaptions). */}
|
|
216
|
+
<div className="relative w-[min(20rem,52vw,37.5vh)] [--face-bleed:19.5%]">
|
|
200
217
|
<button
|
|
201
218
|
type="button"
|
|
202
219
|
disabled={busy}
|
|
@@ -232,10 +249,11 @@ function CallScreen({
|
|
|
232
249
|
failed={failed}
|
|
233
250
|
word={ringWord ?? faceWord}
|
|
234
251
|
getSpectrum={getSpectrum}
|
|
235
|
-
className="w-
|
|
252
|
+
className="-m-(--face-bleed) w-[calc(100%+2*var(--face-bleed))] max-w-none"
|
|
236
253
|
/>
|
|
237
254
|
</span>
|
|
238
255
|
</button>
|
|
256
|
+
<ConnectWave status={status} charset={face.charset} />
|
|
239
257
|
|
|
240
258
|
{sided && (
|
|
241
259
|
<SideCaptions
|
|
@@ -244,17 +262,18 @@ function CallScreen({
|
|
|
244
262
|
live={saying}
|
|
245
263
|
onPick={turns.pick}
|
|
246
264
|
under={
|
|
247
|
-
work.held ? (
|
|
248
|
-
<WorkStack
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
265
|
+
work.held && work.lines.length > 0 ? (
|
|
266
|
+
<WorkStack lines={work.lines} shown={work.on} />
|
|
267
|
+
) : null
|
|
268
|
+
}
|
|
269
|
+
// with the work gone and her answer not yet begun, her last words come back level;
|
|
270
|
+
// thinking alone takes nothing from them, since it stands under her face
|
|
271
|
+
ahead={making && work.held && work.lines.length > 0}
|
|
272
|
+
workOf={(turn) =>
|
|
273
|
+
kept[turn]?.length ? (
|
|
274
|
+
<WorkStack lines={kept[turn]} shown />
|
|
254
275
|
) : null
|
|
255
276
|
}
|
|
256
|
-
// with the work gone and her answer not yet begun, her last words come back level
|
|
257
|
-
ahead={making && work.held}
|
|
258
277
|
/>
|
|
259
278
|
)}
|
|
260
279
|
</div>
|
|
@@ -267,6 +286,7 @@ function CallScreen({
|
|
|
267
286
|
{ringing && (
|
|
268
287
|
<Incoming
|
|
269
288
|
ringing={ringing}
|
|
289
|
+
wakePhrase={wakePhrase}
|
|
270
290
|
onAnswer={onTap}
|
|
271
291
|
onDecline={() => onDecline?.()}
|
|
272
292
|
/>
|
|
@@ -278,9 +298,11 @@ function CallScreen({
|
|
|
278
298
|
(tool-line). The fast channel; the face does not follow it
|
|
279
299
|
(use-thursday). */}
|
|
280
300
|
<ActivityRow
|
|
281
|
-
// beside her face the
|
|
301
|
+
// beside her face the lines are on her side (WorkStack); what she is thinking
|
|
302
|
+
// about and the meter stay here in either view
|
|
282
303
|
tool={sided ? null : drawn}
|
|
283
|
-
|
|
304
|
+
toolUp={drawn !== null}
|
|
305
|
+
thinkingSince={thinkingSince}
|
|
284
306
|
thinkingTitle={thinkingTitle}
|
|
285
307
|
// nobody is listened to on a call in writing: there is no microphone
|
|
286
308
|
listening={
|
|
@@ -369,6 +391,25 @@ function CornerDot({ alert }: { alert: SectionAlert }) {
|
|
|
369
391
|
);
|
|
370
392
|
}
|
|
371
393
|
|
|
394
|
+
/**
|
|
395
|
+
* Below this window width the captions down the sides would be narrower than about 15rem
|
|
396
|
+
* (24vw) and her lines a few words each, so the call draws her last line under her face.
|
|
397
|
+
*/
|
|
398
|
+
const SIDES_MIN_WIDTH = 1000;
|
|
399
|
+
|
|
400
|
+
/** Whether the window is at least `px` wide, kept current; a server render counts as wide. */
|
|
401
|
+
function useWide(px: number) {
|
|
402
|
+
return useSyncExternalStore(
|
|
403
|
+
(listener) => {
|
|
404
|
+
const query = window.matchMedia(`(min-width: ${px}px)`);
|
|
405
|
+
query.addEventListener("change", listener);
|
|
406
|
+
return () => query.removeEventListener("change", listener);
|
|
407
|
+
},
|
|
408
|
+
() => window.matchMedia(`(min-width: ${px}px)`).matches,
|
|
409
|
+
() => true,
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
|
|
372
413
|
function SettingsCorner() {
|
|
373
414
|
const alerts = useSectionAlerts();
|
|
374
415
|
// The gear opens everything the three buttons do not, so it carries their reports
|
|
@@ -719,7 +760,7 @@ function Activity({ tool }: { tool: ActivityLine }) {
|
|
|
719
760
|
) : tool.done ? (
|
|
720
761
|
<span className={cn(look, "text-muted-foreground")}>{text}</span>
|
|
721
762
|
) : (
|
|
722
|
-
<ShinyText text={text}
|
|
763
|
+
<ShinyText text={text} className={look} />
|
|
723
764
|
)}
|
|
724
765
|
</span>
|
|
725
766
|
);
|
|
@@ -875,13 +916,16 @@ function useHeldThought(
|
|
|
875
916
|
|
|
876
917
|
function ActivityRow({
|
|
877
918
|
tool,
|
|
919
|
+
toolUp,
|
|
878
920
|
thinkingSince,
|
|
879
921
|
thinkingTitle,
|
|
880
922
|
listening,
|
|
881
923
|
getMicSpectrum,
|
|
882
924
|
}: {
|
|
883
|
-
/** The line to draw now (useDwell). */
|
|
925
|
+
/** The line to draw here now (useDwell); none while the lines stand beside her face. */
|
|
884
926
|
tool: ActivityLine | null;
|
|
927
|
+
/** A line is up, here or on her side: it is the same stretch of work, said more exactly. */
|
|
928
|
+
toolUp: boolean;
|
|
885
929
|
thinkingSince: number | null;
|
|
886
930
|
thinkingTitle: string | null;
|
|
887
931
|
listening: boolean;
|
|
@@ -905,7 +949,7 @@ function ActivityRow({
|
|
|
905
949
|
{thought && (
|
|
906
950
|
<Fade
|
|
907
951
|
at="col-start-1 row-start-1"
|
|
908
|
-
shown={thinkingSince !== null && !
|
|
952
|
+
shown={thinkingSince !== null && !toolUp}
|
|
909
953
|
>
|
|
910
954
|
<Thinking title={title} />
|
|
911
955
|
</Fade>
|
|
@@ -967,27 +1011,51 @@ function useWork(drawn: ActivityLine | null, thinking: boolean) {
|
|
|
967
1011
|
return { lines, on, held };
|
|
968
1012
|
}
|
|
969
1013
|
|
|
1014
|
+
/**
|
|
1015
|
+
* The lines of work behind each of her turns, by turn: whatever ran while her latest turn
|
|
1016
|
+
* was hers — before she answered, while she spoke — is that turn's. A turn is theirs until
|
|
1017
|
+
* she speaks, so work before her answer waits for it. Lines are merged by the call they
|
|
1018
|
+
* came from, so a second stretch on one turn adds to the first. A new call starts empty.
|
|
1019
|
+
*/
|
|
1020
|
+
function useKeptWork(talk: Turn[], lines: ActivityLine[]) {
|
|
1021
|
+
const [kept, setKept] = useState<Record<string, ActivityLine[]>>({});
|
|
1022
|
+
const last = talk.at(-1);
|
|
1023
|
+
const owner = last?.role === "assistant" ? last.id : null;
|
|
1024
|
+
useEffect(() => {
|
|
1025
|
+
if (!owner || !lines.length) return;
|
|
1026
|
+
setKept((was) => {
|
|
1027
|
+
const now = new Set(lines.map(lineKey));
|
|
1028
|
+
const merged = [
|
|
1029
|
+
...(was[owner] ?? []).filter((line) => !now.has(lineKey(line))),
|
|
1030
|
+
...lines,
|
|
1031
|
+
];
|
|
1032
|
+
const same =
|
|
1033
|
+
was[owner]?.length === merged.length &&
|
|
1034
|
+
merged.every((line, k) => line === was[owner]?.[k]);
|
|
1035
|
+
return same ? was : { ...was, [owner]: merged };
|
|
1036
|
+
});
|
|
1037
|
+
}, [owner, lines]);
|
|
1038
|
+
useEffect(() => {
|
|
1039
|
+
if (!talk.length) setKept({});
|
|
1040
|
+
}, [talk.length]);
|
|
1041
|
+
return kept;
|
|
1042
|
+
}
|
|
1043
|
+
|
|
970
1044
|
/**
|
|
971
1045
|
* What she is doing, on her side of the captions and under her words: the last few lines
|
|
972
|
-
* of work, older ones fainter
|
|
973
|
-
*
|
|
1046
|
+
* of work, older ones fainter. No rule and no plate — the glyphs at the head of the lines
|
|
1047
|
+
* are what set them apart from her words. What she is thinking about stays under her face.
|
|
974
1048
|
*/
|
|
975
1049
|
function WorkStack({
|
|
976
1050
|
lines,
|
|
977
1051
|
shown,
|
|
978
|
-
thinkingSince,
|
|
979
|
-
thinkingTitle,
|
|
980
1052
|
}: {
|
|
981
1053
|
lines: ActivityLine[];
|
|
982
1054
|
/** The stretch of work is still open; false is the fade on its way out. */
|
|
983
1055
|
shown: boolean;
|
|
984
|
-
thinkingSince: number | null;
|
|
985
|
-
thinkingTitle: string | null;
|
|
986
1056
|
}) {
|
|
987
|
-
const { thought, title } = useHeldThought(thinkingSince, thinkingTitle);
|
|
988
1057
|
const leaving = Math.max(0, lines.length - WORK_LINES);
|
|
989
1058
|
const rows = lines.length - leaving;
|
|
990
|
-
const newest = lines.at(-1);
|
|
991
1059
|
return (
|
|
992
1060
|
<div
|
|
993
1061
|
aria-hidden={!shown}
|
|
@@ -995,7 +1063,7 @@ function WorkStack({
|
|
|
995
1063
|
"pointer-events-auto relative w-[min(100%,16.5rem)] transition-opacity duration-300 ease-out",
|
|
996
1064
|
!shown && "pointer-events-none opacity-0",
|
|
997
1065
|
)}
|
|
998
|
-
style={{ height:
|
|
1066
|
+
style={{ height: rows * WORK_ROW }}
|
|
999
1067
|
>
|
|
1000
1068
|
{lines.map((line, k) => {
|
|
1001
1069
|
const row = k - leaving;
|
|
@@ -1016,23 +1084,6 @@ function WorkStack({
|
|
|
1016
1084
|
</div>
|
|
1017
1085
|
);
|
|
1018
1086
|
})}
|
|
1019
|
-
{thought && (
|
|
1020
|
-
<div
|
|
1021
|
-
style={{
|
|
1022
|
-
transform: `translateY(${rows * WORK_ROW}px)`,
|
|
1023
|
-
height: WORK_ROW,
|
|
1024
|
-
}}
|
|
1025
|
-
className="absolute inset-x-0 top-0 flex items-center transition-transform duration-300 ease-out motion-reduce:transition-none"
|
|
1026
|
-
>
|
|
1027
|
-
{/* A tool wins over thinking: it is the same stretch of work, said more exactly */}
|
|
1028
|
-
<Fade
|
|
1029
|
-
at={cn("min-w-0", rows > 0 && "pl-6")}
|
|
1030
|
-
shown={thinkingSince !== null && (!newest || newest.done)}
|
|
1031
|
-
>
|
|
1032
|
-
<Thinking title={title} />
|
|
1033
|
-
</Fade>
|
|
1034
|
-
</div>
|
|
1035
|
-
)}
|
|
1036
1087
|
</div>
|
|
1037
1088
|
);
|
|
1038
1089
|
}
|
|
@@ -1072,10 +1123,13 @@ const KEY_CAP =
|
|
|
1072
1123
|
*/
|
|
1073
1124
|
function Incoming({
|
|
1074
1125
|
ringing,
|
|
1126
|
+
wakePhrase,
|
|
1075
1127
|
onAnswer,
|
|
1076
1128
|
onDecline,
|
|
1077
1129
|
}: {
|
|
1078
1130
|
ringing: Ringing;
|
|
1131
|
+
/** Saying it answers as the button does (use-thursday `call`); null while it is off. */
|
|
1132
|
+
wakePhrase: string | null;
|
|
1079
1133
|
onAnswer: () => void;
|
|
1080
1134
|
onDecline: () => void;
|
|
1081
1135
|
}) {
|
|
@@ -1144,6 +1198,11 @@ function Incoming({
|
|
|
1144
1198
|
>
|
|
1145
1199
|
Call back
|
|
1146
1200
|
</Button>
|
|
1201
|
+
{wakePhrase && (
|
|
1202
|
+
<span className="mt-3 flex items-center gap-1.5 font-mono text-[11px] text-muted-foreground/70">
|
|
1203
|
+
or say <SaidPhrase phrase={wakePhrase} />
|
|
1204
|
+
</span>
|
|
1205
|
+
)}
|
|
1147
1206
|
</div>
|
|
1148
1207
|
);
|
|
1149
1208
|
}
|
|
@@ -1198,18 +1257,35 @@ function Incoming({
|
|
|
1198
1257
|
)}
|
|
1199
1258
|
</span>
|
|
1200
1259
|
)}
|
|
1201
|
-
<
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1260
|
+
<span className="mt-1 flex items-center gap-3 font-mono text-[11px] text-muted-foreground/70">
|
|
1261
|
+
{wakePhrase && (
|
|
1262
|
+
<span className="flex items-center gap-1.5">
|
|
1263
|
+
say <SaidPhrase phrase={wakePhrase} /> to answer
|
|
1264
|
+
</span>
|
|
1265
|
+
)}
|
|
1266
|
+
<button
|
|
1267
|
+
type="button"
|
|
1268
|
+
onClick={onDecline}
|
|
1269
|
+
className="flex items-center gap-2 rounded-md outline-none hover:text-foreground focus-visible:ring-3 focus-visible:ring-ring/50"
|
|
1270
|
+
>
|
|
1271
|
+
<kbd className={KEY_CAP}>Esc</kbd>
|
|
1272
|
+
not now
|
|
1273
|
+
</button>
|
|
1274
|
+
</span>
|
|
1209
1275
|
</div>
|
|
1210
1276
|
);
|
|
1211
1277
|
}
|
|
1212
1278
|
|
|
1279
|
+
/** The wake phrase as the screen shows it wherever saying it does something. */
|
|
1280
|
+
function SaidPhrase({ phrase }: { phrase: string }) {
|
|
1281
|
+
return (
|
|
1282
|
+
<span className="flex items-center gap-1 rounded-full bg-muted px-2 py-0.5 text-foreground/80">
|
|
1283
|
+
<Mic className="size-3" />
|
|
1284
|
+
{phrase}
|
|
1285
|
+
</span>
|
|
1286
|
+
);
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1213
1289
|
/** What a waiting thread is doing, in one word. */
|
|
1214
1290
|
const RUNG_KIND: Record<Rung["kind"], string> = {
|
|
1215
1291
|
question: "asks",
|
|
@@ -1284,7 +1360,6 @@ function Hint({
|
|
|
1284
1360
|
body = (
|
|
1285
1361
|
<ShinyText
|
|
1286
1362
|
text={status === "connecting" ? "Connecting…" : "Ending…"}
|
|
1287
|
-
motion="pulse"
|
|
1288
1363
|
className="text-muted-foreground/70"
|
|
1289
1364
|
/>
|
|
1290
1365
|
);
|
|
@@ -1324,10 +1399,7 @@ function Hint({
|
|
|
1324
1399
|
body = (
|
|
1325
1400
|
<>
|
|
1326
1401
|
<span>Tap Thursday, or say</span>
|
|
1327
|
-
<
|
|
1328
|
-
<Mic className="size-3" />
|
|
1329
|
-
{wakePhrase}
|
|
1330
|
-
</span>
|
|
1402
|
+
<SaidPhrase phrase={wakePhrase} />
|
|
1331
1403
|
</>
|
|
1332
1404
|
);
|
|
1333
1405
|
} else if (hotkeyLabel) {
|
|
@@ -316,7 +316,7 @@ export function WriteLine({
|
|
|
316
316
|
aria-hidden
|
|
317
317
|
className="pointer-events-none fixed inset-0 z-20 bg-background/60"
|
|
318
318
|
>
|
|
319
|
-
<div className="absolute inset-3 rounded-3xl border-[1.5px] border-dashed border-
|
|
319
|
+
<div className="absolute inset-3 rounded-3xl border-[1.5px] border-dashed border-foreground/30" />
|
|
320
320
|
</div>
|
|
321
321
|
)}
|
|
322
322
|
<div
|
|
@@ -337,7 +337,7 @@ export function WriteLine({
|
|
|
337
337
|
className="px-0.5 pt-0.5"
|
|
338
338
|
>
|
|
339
339
|
{dragging && (
|
|
340
|
-
<span className="flex h-13 items-center gap-2 rounded-[14px] border-[1.5px] border-dashed border-
|
|
340
|
+
<span className="flex h-13 items-center gap-2 rounded-[14px] border-[1.5px] border-dashed border-foreground/60 px-4 text-[13px]">
|
|
341
341
|
<ArrowDownToLine className="size-4" />
|
|
342
342
|
Let go — it waits here
|
|
343
343
|
</span>
|
|
@@ -32,6 +32,14 @@ export type OpenWork = {
|
|
|
32
32
|
show: ActivityLine;
|
|
33
33
|
};
|
|
34
34
|
|
|
35
|
+
/** What a bot's message to the call does, as its line on the call screen says it after the bot's name. */
|
|
36
|
+
const RELAY_SAYS: Record<Thread["room"]["relays"][number]["kind"], string> = {
|
|
37
|
+
message: "says",
|
|
38
|
+
question: "asks",
|
|
39
|
+
report: "reports",
|
|
40
|
+
interrupted: "was cut off",
|
|
41
|
+
};
|
|
42
|
+
|
|
35
43
|
const OPEN_RANK = {
|
|
36
44
|
question: 0,
|
|
37
45
|
stopped: 1,
|
|
@@ -95,7 +103,7 @@ export function openWork(threads: Thread[]): OpenWork[] {
|
|
|
95
103
|
relayIds: relays
|
|
96
104
|
.filter((relay) => relay.messageId === question.id)
|
|
97
105
|
.map((relay) => relay.id),
|
|
98
|
-
show: show(question.bot, `${question.bot}
|
|
106
|
+
show: show(question.bot, `${question.bot} asks`),
|
|
99
107
|
});
|
|
100
108
|
}
|
|
101
109
|
|
|
@@ -127,7 +135,7 @@ export function openWork(threads: Thread[]): OpenWork[] {
|
|
|
127
135
|
thread.bot,
|
|
128
136
|
kind === "stopped"
|
|
129
137
|
? `${thread.bot} stopped`
|
|
130
|
-
:
|
|
138
|
+
: `${thread.bot} finished`,
|
|
131
139
|
),
|
|
132
140
|
});
|
|
133
141
|
continue;
|
|
@@ -140,7 +148,7 @@ export function openWork(threads: Thread[]): OpenWork[] {
|
|
|
140
148
|
kind: "progress",
|
|
141
149
|
line: `${bracket(relay.bot, relay.kind)}\n${spoken(relay.text)}`,
|
|
142
150
|
relayIds: [relay.id],
|
|
143
|
-
show: show(relay.bot, `${relay.bot}
|
|
151
|
+
show: show(relay.bot, `${relay.bot} ${RELAY_SAYS[relay.kind]}`),
|
|
144
152
|
});
|
|
145
153
|
}
|
|
146
154
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { TOOL_NAMES } from "@/features/ai/tools/tool-name";
|
|
2
|
+
import type { Thread } from "@/features/bot/bot.schema";
|
|
2
3
|
|
|
3
4
|
// Human-readable activity lines for tool calls on the call screen. The model
|
|
4
5
|
// never reads these. Unknown names resolve to null and the screen shows the name.
|
|
@@ -6,13 +7,36 @@ const LINES: Record<string, string> = {
|
|
|
6
7
|
[TOOL_NAMES.memory_recall]: "Checking your notes",
|
|
7
8
|
[TOOL_NAMES.memory_remember]: "Noting that down",
|
|
8
9
|
[TOOL_NAMES.memory_forget]: "Forgetting that",
|
|
10
|
+
[TOOL_NAMES.memory_conversation]: "Reading back an earlier call",
|
|
9
11
|
[TOOL_NAMES.bash]: "Doing it on this computer",
|
|
10
12
|
[TOOL_NAMES.web_search]: "Searching the web",
|
|
11
13
|
[TOOL_NAMES.load_skill]: "Reading how to do this",
|
|
12
14
|
[TOOL_NAMES.look_at]: "Looking at the picture",
|
|
13
15
|
[TOOL_NAMES.end_call]: "Ending the call",
|
|
16
|
+
[TOOL_NAMES.routine]: "Checking your routines",
|
|
14
17
|
};
|
|
15
18
|
|
|
19
|
+
/** When a routine being made starts, as the call's arguments say it: "once at 19:10", "every 6 hours". */
|
|
20
|
+
function routineWhen(args: Record<string, unknown>): string | null {
|
|
21
|
+
if (typeof args.at === "string" && args.at.length >= 16) {
|
|
22
|
+
const [day, time] = args.at.split(" ");
|
|
23
|
+
const [year, month, date] = day.split("-").map(Number);
|
|
24
|
+
const on = new Date(year, month - 1, date);
|
|
25
|
+
if (on.toDateString() === new Date().toDateString())
|
|
26
|
+
return `once at ${time}`;
|
|
27
|
+
const named = on.toLocaleDateString("en-US", {
|
|
28
|
+
month: "short",
|
|
29
|
+
day: "numeric",
|
|
30
|
+
});
|
|
31
|
+
return `once, ${named} ${time}`;
|
|
32
|
+
}
|
|
33
|
+
if (typeof args.time === "string" && args.time)
|
|
34
|
+
return `daily at ${args.time}`;
|
|
35
|
+
if (typeof args.everyHours === "number")
|
|
36
|
+
return `every ${args.everyHours} hours`;
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
16
40
|
/**
|
|
17
41
|
* How much of a fact the line carries. What is being written is the one thing
|
|
18
42
|
* on this screen the user cannot check afterwards without opening the note, so
|
|
@@ -48,7 +72,11 @@ function firstFact(
|
|
|
48
72
|
}
|
|
49
73
|
|
|
50
74
|
/** Lines that need the arguments; arguments may still be streaming in. */
|
|
51
|
-
function fromArgs(
|
|
75
|
+
function fromArgs(
|
|
76
|
+
name: string,
|
|
77
|
+
args: Record<string, unknown>,
|
|
78
|
+
bot: string | null,
|
|
79
|
+
): string | null {
|
|
52
80
|
if (name === TOOL_NAMES.memory_remember) {
|
|
53
81
|
const path = typeof args.path === "string" ? args.path.trim() : "";
|
|
54
82
|
const fact = firstFact(args);
|
|
@@ -65,10 +93,26 @@ function fromArgs(name: string, args: Record<string, unknown>): string | null {
|
|
|
65
93
|
const query = typeof args.query === "string" ? args.query.trim() : "";
|
|
66
94
|
return query ? `Searching · ${query}` : null;
|
|
67
95
|
}
|
|
96
|
+
if (name === TOOL_NAMES.routine) {
|
|
97
|
+
if (args.action === "create") {
|
|
98
|
+
const bot = typeof args.bot === "string" ? args.bot.trim() : "";
|
|
99
|
+
const when = routineWhen(args);
|
|
100
|
+
const what = [bot, when].filter(Boolean).join(", ");
|
|
101
|
+
return what ? `Setting a routine · ${what}` : "Setting a routine";
|
|
102
|
+
}
|
|
103
|
+
if (args.action === "change") return "Changing a routine";
|
|
104
|
+
if (args.action === "delete") return "Deleting a routine";
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
68
107
|
const label = typeof args.thread === "string" ? args.thread.trim() : "";
|
|
69
|
-
if (name === TOOL_NAMES.thread_tell
|
|
70
|
-
return "Passing that on";
|
|
71
|
-
if (name === TOOL_NAMES.
|
|
108
|
+
if (name === TOOL_NAMES.thread_tell)
|
|
109
|
+
return bot ? `Telling ${bot}` : "Passing that on";
|
|
110
|
+
if (name === TOOL_NAMES.thread_answer)
|
|
111
|
+
return bot ? `Answering ${bot}` : "Answering that";
|
|
112
|
+
if (name === TOOL_NAMES.thread_cancel)
|
|
113
|
+
return label ? `Stopping ${label}` : "Stopping that";
|
|
114
|
+
if (name === TOOL_NAMES.thread_seen)
|
|
115
|
+
return label ? `Marking ${label} as read` : "Marking that as read";
|
|
72
116
|
if (name === TOOL_NAMES.thread_show)
|
|
73
117
|
return label ? `Opening ${label}` : "Opening that";
|
|
74
118
|
if (name === TOOL_NAMES.thread_status)
|
|
@@ -99,22 +143,60 @@ function parseArgs(args?: string): Record<string, unknown> | null {
|
|
|
99
143
|
return null;
|
|
100
144
|
}
|
|
101
145
|
|
|
102
|
-
|
|
146
|
+
/** The line for a tool call; `bot` is who it reaches (`toolBot`), for the lines that name them. */
|
|
147
|
+
export function toolLine(
|
|
148
|
+
name: string,
|
|
149
|
+
args?: string,
|
|
150
|
+
bot: string | null = null,
|
|
151
|
+
): string | null {
|
|
103
152
|
if (name === TOOL_NAMES.tool_search || name === TOOL_NAMES.tool_call) {
|
|
104
153
|
return "Reaching a connected service";
|
|
105
154
|
}
|
|
106
155
|
const parsed = parseArgs(args);
|
|
107
|
-
return (
|
|
156
|
+
return (
|
|
157
|
+
(parsed && fromArgs(name, parsed, bot)) ?? LINES[name] ?? fromServer(name)
|
|
158
|
+
);
|
|
108
159
|
}
|
|
109
160
|
|
|
161
|
+
/** The call's tools that act on one thread the model names by its label or id. */
|
|
162
|
+
const ON_A_THREAD = new Set<string>([
|
|
163
|
+
TOOL_NAMES.thread_tell,
|
|
164
|
+
TOOL_NAMES.thread_answer,
|
|
165
|
+
TOOL_NAMES.thread_status,
|
|
166
|
+
TOOL_NAMES.thread_show,
|
|
167
|
+
TOOL_NAMES.thread_cancel,
|
|
168
|
+
TOOL_NAMES.thread_seen,
|
|
169
|
+
]);
|
|
170
|
+
|
|
110
171
|
/**
|
|
111
|
-
* The bot a call
|
|
112
|
-
*
|
|
172
|
+
* The bot a call's tool reaches, so its row wears that bot's face rather than a glyph —
|
|
173
|
+
* who work went to is a face everywhere else in the app. The one work is handed to, else
|
|
174
|
+
* the bot of the thread the tool names, found as the server finds a thread (thread.query
|
|
175
|
+
* resolveThread): its id, else its label. An answer goes to the bot that asked; "all", and
|
|
176
|
+
* a thread the page does not hold, reach nobody in particular.
|
|
113
177
|
*/
|
|
114
|
-
export function toolBot(
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
178
|
+
export function toolBot(
|
|
179
|
+
name: string,
|
|
180
|
+
args?: string,
|
|
181
|
+
threads?: Thread[],
|
|
182
|
+
): string | null {
|
|
183
|
+
const parsed = parseArgs(args);
|
|
184
|
+
if (!parsed) return null;
|
|
185
|
+
if (name === TOOL_NAMES.thread_start)
|
|
186
|
+
return typeof parsed.bot === "string" && parsed.bot.trim()
|
|
187
|
+
? parsed.bot.trim()
|
|
188
|
+
: null;
|
|
189
|
+
if (!ON_A_THREAD.has(name) || !threads) return null;
|
|
190
|
+
const ref = typeof parsed.thread === "string" ? parsed.thread.trim() : "";
|
|
191
|
+
if (!ref || ref.toLowerCase() === "all") return null;
|
|
192
|
+
const lower = ref.toLowerCase();
|
|
193
|
+
const thread =
|
|
194
|
+
threads.find((one) => one.id === ref) ??
|
|
195
|
+
threads.find((one) => one.label.toLowerCase() === lower);
|
|
196
|
+
if (!thread) return null;
|
|
197
|
+
return name === TOOL_NAMES.thread_answer
|
|
198
|
+
? (thread.room.questions[0]?.bot ?? thread.bot)
|
|
199
|
+
: thread.bot;
|
|
118
200
|
}
|
|
119
201
|
|
|
120
202
|
/**
|
|
@@ -285,11 +285,12 @@ export function useTextCall(): TextCall {
|
|
|
285
285
|
? searchSourcesOf(JSON.stringify(lastTool.output ?? null))
|
|
286
286
|
: [];
|
|
287
287
|
if (last?.type === "text" && !sources.length) return null;
|
|
288
|
+
const bot = toolBot(name, args, inbox.current);
|
|
288
289
|
return {
|
|
289
290
|
id: lastTool.toolCallId,
|
|
290
291
|
name,
|
|
291
|
-
line: toolLine(name, args),
|
|
292
|
-
bot
|
|
292
|
+
line: toolLine(name, args, bot),
|
|
293
|
+
bot,
|
|
293
294
|
done,
|
|
294
295
|
...(sources.length ? { sources } : {}),
|
|
295
296
|
};
|
|
@@ -16,6 +16,7 @@ import { acceptThreadRelaysAction } from "@/features/bot/bot.action";
|
|
|
16
16
|
import type { Bot, Thread } from "@/features/bot/bot.schema";
|
|
17
17
|
import { botThreads, screenActs } from "@/features/bot/thread.store";
|
|
18
18
|
import { runRemoteTool } from "@/features/thursday/tool-call";
|
|
19
|
+
import { askToNotify } from "@/features/workspace/components/artifact-view";
|
|
19
20
|
import { isCombo, useHotkey } from "@/hooks/use-hotkey";
|
|
20
21
|
import { useWakeWord } from "@/hooks/use-wake-word";
|
|
21
22
|
import type { LiveClose } from "@/lib/live/live.schema";
|
|
@@ -256,11 +257,12 @@ export function useThursday(
|
|
|
256
257
|
const showTool = useCallback((call: LiveToolCall) => {
|
|
257
258
|
if (linger.current) clearTimeout(linger.current);
|
|
258
259
|
relayOpen.current = false;
|
|
260
|
+
const bot = toolBot(call.name, call.arguments, latest.current);
|
|
259
261
|
setTool({
|
|
260
262
|
id: call.id,
|
|
261
263
|
name: call.name,
|
|
262
|
-
line: toolLine(call.name, call.arguments),
|
|
263
|
-
bot
|
|
264
|
+
line: toolLine(call.name, call.arguments, bot),
|
|
265
|
+
bot,
|
|
264
266
|
done: false,
|
|
265
267
|
});
|
|
266
268
|
}, []);
|
|
@@ -708,6 +710,7 @@ export function useThursday(
|
|
|
708
710
|
// with a line open this press hangs up; `calling` covers the gap before re-render
|
|
709
711
|
if (calling.current || status !== "idle") return hangUp();
|
|
710
712
|
answered();
|
|
713
|
+
askToNotify();
|
|
711
714
|
|
|
712
715
|
setStatus("connecting");
|
|
713
716
|
setEnded(null);
|