thursday-agent 0.4.0 → 0.6.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/app-path-routes-manifest.json +3 -2
- package/.next/build-manifest.json +3 -3
- package/.next/prerender-manifest.json +3 -3
- package/.next/required-server-files.json +8 -1
- package/.next/routes-manifest.json +18 -6
- 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 +13 -13
- package/.next/server/app/_not-found.segments/_full.segment.rsc +13 -13
- package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +12 -12
- package/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/api/artifact/route.js +10 -6
- package/.next/server/app/api/artifact/route.js.nft.json +1 -1
- package/.next/server/app/api/bot/memory/files/route.js +6 -5
- package/.next/server/app/api/bot/memory/files/route.js.nft.json +1 -1
- package/.next/server/app/api/bot/memory/route.js +8 -8
- package/.next/server/app/api/bot/memory/route.js.nft.json +1 -1
- package/.next/server/app/api/bot/route.js +8 -8
- package/.next/server/app/api/bot/route.js.nft.json +1 -1
- package/.next/server/app/api/bot/thread/[id]/route/app-paths-manifest.json +3 -0
- package/.next/server/app/api/bot/thread/[id]/route.js +15 -0
- package/.next/server/app/api/bot/thread/[id]/route.js.nft.json +1 -0
- package/.next/server/app/api/bot/thread/[id]/route_client-reference-manifest.js +3 -0
- package/.next/server/app/api/bot/thread/route/app-paths-manifest.json +3 -0
- package/.next/server/app/api/bot/thread/route.js +15 -0
- package/.next/server/app/api/bot/thread/route.js.nft.json +1 -0
- package/.next/server/app/api/bot/thread/route_client-reference-manifest.js +3 -0
- package/.next/server/app/api/config/route.js +7 -7
- package/.next/server/app/api/config/route.js.nft.json +1 -1
- package/.next/server/app/api/events/route.js +3 -3
- package/.next/server/app/api/events/route.js.nft.json +1 -1
- package/.next/server/app/api/file/[...path]/route.js +5 -5
- package/.next/server/app/api/file/[...path]/route.js.nft.json +1 -1
- package/.next/server/app/api/llm-model/catalog/route.js +8 -8
- package/.next/server/app/api/llm-model/catalog/route.js.nft.json +1 -1
- package/.next/server/app/api/llm-model/credits/route.js +8 -8
- package/.next/server/app/api/llm-model/credits/route.js.nft.json +1 -1
- package/.next/server/app/api/llm-model/route.js +8 -8
- package/.next/server/app/api/llm-model/route.js.nft.json +1 -1
- package/.next/server/app/api/llm-model/subscription/route.js +8 -8
- package/.next/server/app/api/llm-model/subscription/route.js.nft.json +1 -1
- package/.next/server/app/api/mcp/[name]/route.js +6 -6
- package/.next/server/app/api/mcp/[name]/route.js.nft.json +1 -1
- package/.next/server/app/api/mcp/oauth/callback/route.js +4 -4
- package/.next/server/app/api/mcp/oauth/callback/route.js.nft.json +1 -1
- package/.next/server/app/api/mcp/route.js +6 -6
- package/.next/server/app/api/mcp/route.js.nft.json +1 -1
- package/.next/server/app/api/mcp/tools/route.js +6 -6
- package/.next/server/app/api/mcp/tools/route.js.nft.json +1 -1
- package/.next/server/app/api/memory/edit/route.js +13 -13
- package/.next/server/app/api/memory/edit/route.js.nft.json +1 -1
- package/.next/server/app/api/memory/route.js +7 -6
- package/.next/server/app/api/memory/route.js.nft.json +1 -1
- package/.next/server/app/api/skills/[source]/[name]/route.js +6 -6
- package/.next/server/app/api/skills/[source]/[name]/route.js.nft.json +1 -1
- package/.next/server/app/api/skills/route.js +6 -6
- package/.next/server/app/api/skills/route.js.nft.json +1 -1
- package/.next/server/app/api/thursday/call/route.js +10 -11
- package/.next/server/app/api/thursday/call/route.js.nft.json +1 -1
- package/.next/server/app/api/thursday/skills/route.js +10 -11
- package/.next/server/app/api/thursday/skills/route.js.nft.json +1 -1
- package/.next/server/app/api/thursday/tool-call/route.js +15 -15
- package/.next/server/app/api/thursday/tool-call/route.js.nft.json +1 -1
- package/.next/server/app/api/workspace/missing/route/app-paths-manifest.json +3 -0
- package/.next/server/app/api/workspace/missing/route/build-manifest.json +12 -0
- package/.next/server/app/api/workspace/missing/route/server-reference-manifest.json +4 -0
- package/.next/server/app/api/workspace/missing/route.js +12 -0
- package/.next/server/app/api/workspace/missing/route.js.map +5 -0
- package/.next/server/app/api/workspace/missing/route.js.nft.json +1 -0
- package/.next/server/app/api/workspace/missing/route_client-reference-manifest.js +3 -0
- package/.next/server/app/api/workspace/route.js +5 -4
- 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 +4 -3
- 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/page/server-reference-manifest.json +67 -45
- package/.next/server/app/page.js +10 -9
- package/.next/server/app/page.js.nft.json +1 -1
- package/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/server/app-paths-manifest.json +3 -2
- package/.next/server/chunks/[root-of-the-server]__02846fb._.js +8 -0
- package/.next/server/chunks/[root-of-the-server]__049f521._.js +3 -0
- package/.next/server/chunks/[root-of-the-server]__05luceg._.js +3 -0
- package/.next/server/chunks/[root-of-the-server]__06--3q3._.js +8 -0
- package/.next/server/chunks/[root-of-the-server]__07bnah_._.js +3 -0
- package/.next/server/chunks/[root-of-the-server]__08eae20._.js +8 -0
- package/.next/server/chunks/[root-of-the-server]__0_ue8oe._.js +3 -0
- package/.next/server/chunks/[root-of-the-server]__0c9fgca._.js +5 -0
- package/.next/server/chunks/[root-of-the-server]__0g-ueei._.js +8 -0
- package/.next/server/chunks/[root-of-the-server]__0h_x6q6._.js +5 -0
- package/.next/server/chunks/{[root-of-the-server]__0pa2n68._.js → [root-of-the-server]__0kxrvxf._.js} +2 -2
- package/.next/server/chunks/{[root-of-the-server]__0q0vvt2._.js → [root-of-the-server]__0ov3iir._.js} +2 -2
- package/.next/server/chunks/[root-of-the-server]__0qwtlmx._.js +8 -0
- package/.next/server/chunks/[root-of-the-server]__0sm5xvm._.js +3 -0
- package/.next/server/chunks/[root-of-the-server]__0ye3da2._.js +3 -0
- package/.next/server/chunks/[root-of-the-server]__0zp35ul._.js +5 -0
- package/.next/server/chunks/[root-of-the-server]__144u9d_._.js +3 -0
- package/.next/server/chunks/[root-of-the-server]__18-2tsq._.js +5 -0
- package/.next/server/chunks/[root-of-the-server]__1e-ci13._.js +3 -0
- package/.next/server/chunks/[root-of-the-server]__1e2365e._.js +8 -0
- package/.next/server/chunks/[root-of-the-server]__1fix932._.js +8 -0
- package/.next/server/chunks/{[root-of-the-server]__0ce8nf8._.js → [root-of-the-server]__1g3t7vn._.js} +3 -3
- package/.next/server/chunks/[root-of-the-server]__1kp-83u._.js +5 -0
- package/.next/server/chunks/[root-of-the-server]__1kv8p3e._.js +5 -0
- package/.next/server/chunks/[root-of-the-server]__1orn-5b._.js +2 -3
- package/.next/server/chunks/_02szwk8._.js +5 -0
- package/.next/server/chunks/_05h2cii._.js +1 -1
- package/.next/server/chunks/_06c9kie._.js +216 -0
- package/.next/server/chunks/{_1x6kd1r._.js → _07y2gj-._.js} +1 -1
- package/.next/server/chunks/_0a43a-0._.js +3 -0
- package/.next/server/chunks/_0bujjey._.js +5 -0
- package/.next/server/chunks/_0c9j6rb._.js +1 -1
- package/.next/server/chunks/_0dkd549._.js +3 -0
- package/.next/server/chunks/_0ej1kvv._.js +3 -0
- package/.next/server/chunks/_0f6ts7z._.js +3 -0
- package/.next/server/chunks/_0gm9hyv._.js +55 -0
- package/.next/server/chunks/_0i55en5._.js +25 -0
- package/.next/server/chunks/_0k50n27._.js +18 -0
- package/.next/server/chunks/_0khihjk._.js +3 -3
- package/.next/server/chunks/_0n74zsf._.js +5 -5
- package/.next/server/chunks/_0nvm9ef._.js +3 -0
- package/.next/server/chunks/_0p56fn0._.js +3 -0
- package/.next/server/chunks/_0recufk._.js +1 -1
- package/.next/server/chunks/_0xbkz10._.js +3 -0
- package/.next/server/chunks/_10tkr26._.js +3 -0
- package/.next/server/chunks/{_0ioz3nd._.js → _11u2kp7._.js} +1 -1
- package/.next/server/chunks/_11ujpya._.js +3 -0
- package/.next/server/chunks/_127nx_p._.js +5 -0
- package/.next/server/chunks/_17l5ygt._.js +1 -1
- package/.next/server/chunks/_19uauj6._.js +3 -0
- package/.next/server/chunks/_1_5mnq4._.js +5 -5
- package/.next/server/chunks/_1abl6t9._.js +55 -0
- package/.next/server/chunks/_1d0j961._.js +3 -3
- package/.next/server/chunks/_1dbixxz._.js +3 -0
- package/.next/server/chunks/_1dc_d1q._.js +98 -0
- package/.next/server/chunks/_1drgfx4._.js +5 -5
- package/.next/server/chunks/_1e48czp._.js +3 -3
- package/.next/server/chunks/_1f31u5x._.js +5 -0
- package/.next/server/chunks/_1kg70wc._.js +3 -3
- package/.next/server/chunks/_1klbcft._.js +5 -0
- package/.next/server/chunks/_1krd9rf._.js +4 -1
- package/.next/server/chunks/_1kwmzab._.js +3 -0
- package/.next/server/chunks/{_1pf4qe-._.js → _1oxj9fa._.js} +1 -1
- package/.next/server/chunks/{_1f46eie._.js → _1pcmdbb._.js} +1 -1
- package/.next/server/chunks/_1qgix-j._.js +3 -0
- package/.next/server/chunks/{lib_logger_ts_1exwjut._.js → _1rpchta._.js} +5 -5
- package/.next/server/chunks/_1vsej-s._.js +5 -0
- package/.next/server/chunks/_1yx0p_s._.js +5 -0
- package/.next/server/chunks/{_0rnf-6n._.js → _20fsr_z._.js} +6 -6
- package/.next/server/chunks/_2128hkj._.js +5 -0
- package/.next/server/chunks/_next-internal_server_app_api_bot_thread_[id]_route_actions_1fstjn2.js +3 -0
- package/.next/server/chunks/_next-internal_server_app_api_bot_thread_route_actions_003ccso.js +3 -0
- package/.next/server/chunks/_next-internal_server_app_api_workspace_missing_route_actions_1fidi6p.js +3 -0
- package/.next/server/chunks/config_ts_1g4sz5o._.js +1 -1
- package/.next/server/chunks/features_0-cnt0l._.js +4 -0
- package/.next/server/chunks/features_0hdaci0._.js +4 -0
- package/.next/server/chunks/features_1e39j7-._.js +8 -0
- package/.next/server/chunks/features_bot_bot_query_ts_12mgkl2._.js +1 -1
- package/.next/server/chunks/features_memory_1uqvksn._.js +1 -1
- package/.next/server/chunks/{node_modules_consola_dist_chunks_prompt_mjs_0r28-sm._.js → node_modules_consola_dist_chunks_prompt_mjs_0a7-pbw._.js} +1 -1
- package/.next/server/chunks/{node_modules_consola_dist_chunks_prompt_mjs_0v_7drq._.js → node_modules_consola_dist_chunks_prompt_mjs_0gzvgfv._.js} +1 -1
- package/.next/server/chunks/{node_modules_consola_dist_chunks_prompt_mjs_0i7cvml._.js → node_modules_consola_dist_chunks_prompt_mjs_107pxfn._.js} +1 -1
- package/.next/server/chunks/{node_modules_consola_dist_chunks_prompt_mjs_0icqaqw._.js → node_modules_consola_dist_chunks_prompt_mjs_15hbel8._.js} +1 -1
- package/.next/server/chunks/node_modules_consola_dist_chunks_prompt_mjs_18i-23e._.js +3 -0
- package/.next/server/chunks/node_modules_consola_dist_chunks_prompt_mjs_1bewgf3._.js +3 -0
- package/.next/server/chunks/node_modules_consola_dist_chunks_prompt_mjs_1kipdeo._.js +3 -0
- package/.next/server/chunks/node_modules_consola_dist_chunks_prompt_mjs_1nlww1m._.js +3 -0
- package/.next/server/chunks/node_modules_consola_dist_chunks_prompt_mjs_1q77nfx._.js +3 -0
- package/.next/server/chunks/node_modules_consola_dist_chunks_prompt_mjs_1xqdzx_._.js +3 -0
- package/.next/server/chunks/{node_modules_zod_v4_1801ax7._.js → node_modules_zod_v4_0lkux5-._.js} +2 -2
- package/.next/server/chunks/{node_modules_zod_v4_1cha_bw._.js → node_modules_zod_v4_1jmr9cq._.js} +2 -2
- package/.next/server/chunks/{node_modules_zod_v4_0ubdflj._.js → node_modules_zod_v4_1yrxmek._.js} +2 -2
- package/.next/server/chunks/node_modules_zod_v4_classic_errors_0tp0y-q.js +3 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__09q9med._.js +1 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__0bxsfyn._.js +101 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__0lyi8i2._.js +1 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__16jc196._.js +1 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__172cq4x._.js +3 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__18dthbp._.js +3 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__18e68w8._.js +3 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__1fll8dc._.js +3 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__1mzi-ju._.js +1 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__1nygfik._.js +5 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__1sdphb0._.js +8 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__1waa3rc._.js +100 -0
- package/.next/server/chunks/ssr/_00rehl_._.js +3 -0
- package/.next/server/chunks/ssr/{_1lz_cq2._.js → _02oc_h5._.js} +76 -89
- package/.next/server/chunks/ssr/_03_928f._.js +3 -0
- package/.next/server/chunks/ssr/_05yghx6._.js +3 -0
- package/.next/server/chunks/ssr/_08c4z-9._.js +3 -0
- package/.next/server/chunks/ssr/_0_qfdlw._.js +4 -0
- package/.next/server/chunks/ssr/_0c9e5w6._.js +9 -0
- package/.next/server/chunks/ssr/{_0s99bgb._.js → _0dh2e1j._.js} +9 -3
- package/.next/server/chunks/ssr/_0dr-ddg._.js +3 -0
- package/.next/server/chunks/ssr/_0eaog6_._.js +3 -0
- package/.next/server/chunks/ssr/_0evkpto._.js +3 -0
- package/.next/server/chunks/ssr/_0g_rlog._.js +3 -0
- package/.next/server/chunks/ssr/_0geg212._.js +3 -0
- package/.next/server/chunks/ssr/{_1gd8q8l._.js → _0hxt2ul._.js} +2 -2
- package/.next/server/chunks/ssr/_0i_6q1w._.js +2 -2
- package/.next/server/chunks/ssr/_0lu1n_x._.js +1 -1
- package/.next/server/chunks/ssr/{_15j2_8f._.js → _0wzaht-._.js} +2 -2
- package/.next/server/chunks/ssr/_0z-g0uy._.js +3 -0
- package/.next/server/chunks/ssr/_123ttsf._.js +3 -0
- package/.next/server/chunks/ssr/_123y-ah._.js +3 -0
- package/.next/server/chunks/ssr/_12w42gi._.js +3 -0
- package/.next/server/chunks/ssr/_161kqav._.js +3 -0
- package/.next/server/chunks/ssr/{_0ap2fzn._.js → _16c6_xm._.js} +2 -2
- package/.next/server/chunks/ssr/_18opah7._.js +3 -0
- package/.next/server/chunks/ssr/_18xqmny._.js +3 -0
- package/.next/server/chunks/ssr/_1_krfph._.js +3 -0
- package/.next/server/chunks/ssr/{_1-_etts._.js → _1anqots._.js} +2 -2
- package/.next/server/chunks/ssr/_1ey7qn-._.js +70 -0
- package/.next/server/chunks/ssr/_1g9vs7q._.js +3 -0
- package/.next/server/chunks/ssr/_1jmz33w._.js +3 -0
- package/.next/server/chunks/ssr/{_0n3toaw._.js → _1k0spz7._.js} +2 -2
- package/.next/server/chunks/ssr/_1rgtxhr._.js +5 -0
- package/.next/server/chunks/ssr/_1slp25y._.js +3 -0
- package/.next/server/chunks/ssr/{_1fojd41._.js → _1x9pskj._.js} +3 -3
- package/.next/server/chunks/ssr/_1y6b5mr._.js +101 -0
- package/.next/server/chunks/ssr/_1ztk7p6._.js +7 -0
- package/.next/server/chunks/ssr/components_ui_tabs_tsx_0ej40b4._.js +3 -0
- package/.next/server/chunks/ssr/features_bot_thread_query_ts_023eklt._.js +4 -0
- package/.next/server/chunks/ssr/features_config_components_config-setting_tsx_0xfwqdg._.js +1 -1
- package/.next/server/chunks/ssr/features_connectors_components_mcp-setting_tsx_0jmbgvp._.js +1 -1
- package/.next/server/chunks/ssr/{_1-teee5._.js → lib_logger_ts_0-olsze._.js} +2 -7
- package/.next/server/chunks/ssr/{node_modules_0cxaxk9._.js → node_modules_00_51_v._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_04jf_m8._.js → node_modules_022sipm._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_0g7fzk3._.js → node_modules_0332gw4._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_1cx_ql6._.js → node_modules_04vxchl._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_1vh5ntv._.js → node_modules_0746rzi._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_0yugy6m._.js → node_modules_0d5uzof._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_09q35v_._.js → node_modules_0exkwcm._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_08dhkiq._.js → node_modules_0fr-ml8._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_14q6g4n._.js → node_modules_0mxvg8h._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_0arus10._.js → node_modules_0qapgjr._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_1eg7m6c._.js → node_modules_0qbfyjh._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_1cz2hle._.js → node_modules_0t-9nup._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_17bwo43._.js → node_modules_0tusx4d._.js} +3 -3
- package/.next/server/chunks/ssr/{node_modules_1efrqxi._.js → node_modules_0vsxbzp._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_0_-_a--._.js → node_modules_0vv0-fg._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_1gdh31p._.js → node_modules_0wsvp00._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_1hpopmo._.js → node_modules_1-urho9._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_1_pc9kq._.js → node_modules_16pi_dd._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_00etblx._.js → node_modules_1d53yiq._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_1aaxuef._.js → node_modules_1g1eihe._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_198nmll._.js → node_modules_1gpednh._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_0pvd7q-._.js → node_modules_1gu1pph._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_0b__sgl._.js → node_modules_1he1w8_._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_1qpjvy5._.js → node_modules_1lnjapm._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_0940ns5._.js → node_modules_1lynj9m._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_0x3-ply._.js → node_modules_1seuurl._.js} +2 -2
- package/.next/server/chunks/ssr/{node_modules_14om4ar._.js → node_modules_1u6w1pb._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_0had7xd._.js → node_modules_1xn6saw._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_@mermaid-js_parser_dist_chunks_mermaid-parser_core_1b748ic._.js → node_modules_@mermaid-js_parser_dist_chunks_mermaid-parser_core_067_exa._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_@mermaid-js_parser_dist_chunks_mermaid-parser_core_1z4hm5e._.js → node_modules_@mermaid-js_parser_dist_chunks_mermaid-parser_core_0nrrdaq._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_@vercel_oidc_dist_06mk8id._.js → node_modules_@vercel_oidc_dist_21a5haa._.js} +1 -1
- package/.next/server/chunks/ssr/node_modules_lucide-react_dist_esm_icons_trash_mjs_0px2gzr._.js +3 -0
- 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 +68 -46
- package/.next/static/chunks/0dr16v67vj4n7.js +2 -0
- package/.next/static/chunks/0g4b348l0pve7.js +1 -0
- package/.next/static/chunks/0joan-69kbspd.js +1 -0
- package/.next/static/chunks/0vlgzqo9ka90v.js +1 -0
- package/.next/static/chunks/0zrupxgci5gqc.js +1 -0
- package/.next/static/chunks/10k2xvap03z20.css +1 -0
- package/.next/static/chunks/18mkhstgiqcbt.js +1 -0
- package/.next/static/chunks/{15m575979un5f.js → 1i-u2wzub_to1.js} +2 -2
- package/.next/static/chunks/1kp5nfp06nj9h.js +5 -0
- package/.next/static/chunks/1ohslk4568tm3.js +1 -0
- package/.next/static/chunks/1sbmwswc9k2-1.js +1 -0
- package/.next/static/chunks/1t9qms5pwwa3n.js +1 -0
- package/.next/static/chunks/1vj_5bnbxx84e.js +1 -0
- package/.next/static/chunks/1vqezbhjm3goq.js +1 -0
- package/.next/static/chunks/{3akmqm77muj4-.js → 1wlo979mx-y3w.js} +1 -1
- package/.next/static/chunks/26ql4y-43xwhp.js +1 -0
- package/.next/static/chunks/{0fl3gwe4-l6tz.js → 2_pnccjp6j1ca.js} +1 -1
- package/.next/static/chunks/2a51-ywz9b2a3.js +1 -0
- package/.next/static/chunks/2fvdgxih7x7hr.js +1 -0
- package/.next/static/chunks/2jlz8zp4jax_m.js +18 -0
- package/.next/static/chunks/2nvepds1fu2ux.js +1 -0
- package/.next/static/chunks/3addj3i4-izma.js +1 -0
- package/.next/static/chunks/3bfwpww3m3x4u.js +1 -0
- package/.next/static/chunks/3gfh7nxwusbt0.js +1 -0
- package/.next/static/chunks/3gwa44cvz4qoi.js +1 -0
- package/.next/static/chunks/{38gweqdo4jpfj.js → 3izg0a3k87q1-.js} +5 -5
- package/.next/static/chunks/3zk6q3-3f9swq.js +99 -0
- package/.next/static/chunks/3zkg5iup3v4a2.js +12 -0
- package/.next/static/chunks/405m0bqtoihev.js +1 -0
- package/.next/static/chunks/42wcg3wfg_m_p.js +1 -0
- package/README.md +27 -216
- package/app/api/events/app-event.ts +6 -4
- package/app/api/query-key.ts +25 -16
- package/bin/database.mjs +37 -0
- package/bin/thursday.mjs +44 -28
- package/components/ui/custom-icon.tsx +0 -60
- package/components/ui/radio-group.tsx +37 -0
- package/components/ui/shiny-text.tsx +33 -6
- package/database/migrations/{20260910173049_numerous_spirit → 20260916145701_clever_multiple_man}/migration.sql +80 -24
- package/database/migrations/{20260910173049_numerous_spirit → 20260916145701_clever_multiple_man}/snapshot.json +689 -110
- package/database/migrations/20260916160713_thread_cancelled/migration.sql +2 -0
- package/database/migrations/{20260911030320_opposite_jack_power → 20260916160713_thread_cancelled}/snapshot.json +706 -84
- package/docs/how-it-works.md +39 -36
- package/docs/images/keeps-going.png +0 -0
- package/docs/images/two-minds.png +0 -0
- package/docs/live-calls.md +231 -0
- package/docs/thread-rooms.md +71 -0
- package/features/ai/chatgpt.ts +1 -2
- package/features/ai/live.schema.ts +128 -0
- package/features/ai/load-tools.ts +148 -77
- package/features/ai/model.schema.ts +2 -120
- package/features/ai/model.ts +52 -88
- package/features/ai/prompts/bot.prompt.ts +91 -214
- package/features/ai/prompts/live.prompt.ts +265 -0
- package/features/ai/prompts/prompt-helper.ts +91 -32
- package/features/ai/prompts/thursday.prompt.ts +125 -172
- package/features/ai/tools/bot.tool.ts +38 -145
- package/features/ai/tools/call.tool.ts +1 -1
- package/features/ai/tools/connected.ts +5 -3
- package/features/ai/tools/mcp.tool.ts +7 -4
- package/features/ai/tools/memory.tool.ts +35 -31
- package/features/ai/tools/skills.tool.ts +36 -14
- package/features/ai/tools/studio.tool.ts +26 -21
- package/features/ai/tools/tool-name.ts +2 -5
- package/features/ai/tools/workspace.tool.ts +33 -10
- package/features/artifact/artifact.action.ts +2 -2
- package/features/artifact/artifact.query.ts +112 -61
- package/features/artifact/artifact.schema.ts +8 -6
- package/features/artifact/components/artifact-setting.tsx +83 -33
- package/features/bot/bot.action.ts +43 -36
- package/features/bot/bot.memory.ts +137 -20
- package/features/bot/bot.query.ts +28 -11
- package/features/bot/bot.run.ts +299 -636
- package/features/bot/bot.runner.ts +468 -844
- package/features/bot/bot.schema.ts +93 -82
- package/features/bot/bot.seed.ts +141 -64
- package/features/bot/components/bot-mark.tsx +324 -232
- package/features/bot/components/bot-room.tsx +1613 -697
- package/features/bot/components/bot-roster.tsx +8 -13
- package/features/bot/components/bot-setting.tsx +58 -42
- package/features/bot/components/bot-tool.tsx +55 -87
- package/features/bot/components/mark-palette.tsx +35 -32
- package/features/bot/components/path-chips.tsx +33 -3
- package/features/bot/components/thread-badge.tsx +34 -0
- package/features/bot/components/thread-reply.tsx +639 -0
- package/features/bot/components/thread-setting.tsx +674 -0
- package/features/bot/mark.const.ts +79 -31
- package/features/bot/room.query.ts +889 -0
- package/features/bot/room.schema.ts +64 -0
- package/features/bot/seed-bots.ts +2 -1
- package/features/bot/thread.query.ts +971 -0
- package/features/bot/thread.store.ts +587 -0
- package/features/config/components/voice-key.tsx +113 -88
- package/features/config/config.const.ts +5 -9
- package/features/connectors/mcp.schema.ts +0 -2
- package/features/intro/components/intro.tsx +169 -139
- package/features/memory/components/memory-setting.tsx +2 -7
- package/features/memory/memory.schema.ts +14 -23
- package/features/settings/components/setting-ui.tsx +0 -44
- package/features/settings/components/settings.tsx +12 -12
- package/features/settings/settings.alert.ts +3 -3
- package/features/settings/settings.store.ts +1 -1
- package/features/skills/skills.schema.ts +0 -2
- package/features/thursday/components/ascii-field.tsx +24 -44
- package/features/thursday/components/ascii-orb.tsx +359 -358
- package/features/thursday/components/call-log.tsx +46 -44
- package/features/thursday/components/face.tsx +35 -10
- package/features/thursday/components/tab-state.tsx +3 -3
- package/features/thursday/components/thursday-mark.tsx +1 -0
- package/features/thursday/components/thursday-setting.tsx +631 -332
- package/features/thursday/components/thursday.tsx +106 -41
- package/features/thursday/components/voice-picker.tsx +255 -0
- package/features/thursday/thursday.action.ts +94 -68
- package/features/thursday/thursday.query.ts +49 -30
- package/features/thursday/thursday.schema.ts +27 -30
- package/features/thursday/thursday.store.ts +11 -7
- package/features/thursday/tool-call.ts +3 -3
- package/features/thursday/tool-line.ts +5 -3
- package/features/thursday/use-thursday.ts +725 -450
- package/features/workspace/workspace.query.ts +17 -0
- package/features/workspace/workspace.ts +103 -50
- package/lib/live/live.schema.ts +47 -0
- package/lib/live/live.server.ts +155 -0
- package/lib/live/live.session.ts +574 -0
- package/lib/{realtime/realtime.tap.ts → live/live.tap.ts} +41 -12
- package/lib/live/live.transport.ts +152 -0
- package/lib/protocol/use-server-pages.ts +9 -5
- package/lib/utils.ts +21 -20
- package/package.json +2 -2
- package/public/voices/alloy.ogg +0 -0
- package/public/voices/ash.ogg +0 -0
- package/public/voices/ballad.ogg +0 -0
- package/public/voices/beacon.ogg +0 -0
- package/public/voices/bossa.ogg +0 -0
- package/public/voices/cedar.ogg +0 -0
- package/public/voices/cinder.ogg +0 -0
- package/public/voices/coral.ogg +0 -0
- package/public/voices/delta.ogg +0 -0
- package/public/voices/echo.ogg +0 -0
- package/public/voices/gleam.ogg +0 -0
- package/public/voices/marin.ogg +0 -0
- package/public/voices/meridian.ogg +0 -0
- package/public/voices/quartz.ogg +0 -0
- package/public/voices/ripple.ogg +0 -0
- package/public/voices/sage.ogg +0 -0
- package/public/voices/shimmer.ogg +0 -0
- package/public/voices/stone.ogg +0 -0
- package/public/voices/tempo.ogg +0 -0
- package/public/voices/verse.ogg +0 -0
- package/public/voices/vesper.ogg +0 -0
- package/public/voices/willow.ogg +0 -0
- package/scripts/artifact-paths.test.mts +25 -0
- package/scripts/bot-context.test.mts +1408 -0
- package/scripts/live-history.test.mts +115 -0
- package/scripts/live-session.test.mts +948 -0
- package/server.js +1 -1
- package/skills/browser/SKILL.md +17 -6
- package/skills/computer/SKILL.md +1 -1
- package/skills/find-skills/SKILL.md +3 -8
- package/skills/interactive-page/SKILL.md +12 -7
- package/skills/interactive-page/references/diagram.md +89 -0
- package/skills/interactive-page/scripts/archify/LICENSE +22 -0
- package/skills/interactive-page/scripts/archify/README.md +25 -0
- package/skills/interactive-page/scripts/archify/assets/JetBrainsMono-OFL.txt +93 -0
- package/skills/interactive-page/scripts/archify/assets/template.html +14934 -0
- package/skills/interactive-page/scripts/archify/bin/archify.mjs +2126 -0
- package/skills/interactive-page/scripts/archify/bin/open-artifact.mjs +86 -0
- package/skills/interactive-page/scripts/archify/bin/preview.mjs +653 -0
- package/skills/interactive-page/scripts/archify/bin/visual-check.mjs +829 -0
- package/skills/interactive-page/scripts/archify/delta/architecture-delta.mjs +1223 -0
- package/skills/interactive-page/scripts/archify/examples/agent-run.lifecycle.json +60 -0
- package/skills/interactive-page/scripts/archify/examples/agent-tool-call.workflow.json +94 -0
- package/skills/interactive-page/scripts/archify/examples/async-job-roundtrip.sequence.json +61 -0
- package/skills/interactive-page/scripts/archify/examples/cache-miss-request.sequence.json +82 -0
- package/skills/interactive-page/scripts/archify/examples/checkout-platform.base.architecture.json +31 -0
- package/skills/interactive-page/scripts/archify/examples/checkout-platform.head.architecture.json +31 -0
- package/skills/interactive-page/scripts/archify/examples/deployment-release.lifecycle.json +49 -0
- package/skills/interactive-page/scripts/archify/examples/event-stream.dataflow.json +57 -0
- package/skills/interactive-page/scripts/archify/examples/incident-response.workflow.json +64 -0
- package/skills/interactive-page/scripts/archify/examples/product-analytics.dataflow.json +76 -0
- package/skills/interactive-page/scripts/archify/examples/production-deployment.architecture.json +71 -0
- package/skills/interactive-page/scripts/archify/examples/release-delivery.workflow.json +62 -0
- package/skills/interactive-page/scripts/archify/examples/web-app.architecture.json +46 -0
- package/skills/interactive-page/scripts/archify/migrations/workflow-v2.mjs +279 -0
- package/skills/interactive-page/scripts/archify/recipes/scenarios.mjs +284 -0
- package/skills/interactive-page/scripts/archify/references/authoring-contract.md +243 -0
- package/skills/interactive-page/scripts/archify/references/delivery-contract.md +120 -0
- package/skills/interactive-page/scripts/archify/references/viewer-runtime.md +45 -0
- package/skills/interactive-page/scripts/archify/renderers/architecture/grid.mjs +62 -0
- package/skills/interactive-page/scripts/archify/renderers/architecture/render-architecture.mjs +1078 -0
- package/skills/interactive-page/scripts/archify/renderers/dataflow/README.md +104 -0
- package/skills/interactive-page/scripts/archify/renderers/dataflow/render-dataflow.mjs +483 -0
- package/skills/interactive-page/scripts/archify/renderers/lifecycle/README.md +115 -0
- package/skills/interactive-page/scripts/archify/renderers/lifecycle/render-lifecycle.mjs +561 -0
- package/skills/interactive-page/scripts/archify/renderers/sequence/README.md +114 -0
- package/skills/interactive-page/scripts/archify/renderers/sequence/render-sequence.mjs +464 -0
- package/skills/interactive-page/scripts/archify/renderers/shared/brand-marks.mjs +563 -0
- package/skills/interactive-page/scripts/archify/renderers/shared/cli.mjs +218 -0
- package/skills/interactive-page/scripts/archify/renderers/shared/desktop-readability.mjs +26 -0
- package/skills/interactive-page/scripts/archify/renderers/shared/diagnostics.mjs +127 -0
- package/skills/interactive-page/scripts/archify/renderers/shared/engineering-profiles.mjs +157 -0
- package/skills/interactive-page/scripts/archify/renderers/shared/generated-brand-marks.mjs +3 -0
- package/skills/interactive-page/scripts/archify/renderers/shared/generated-validators.mjs +13 -0
- package/skills/interactive-page/scripts/archify/renderers/shared/geometry.mjs +1423 -0
- package/skills/interactive-page/scripts/archify/renderers/shared/i18n.mjs +595 -0
- package/skills/interactive-page/scripts/archify/renderers/shared/layout-report.mjs +40 -0
- package/skills/interactive-page/scripts/archify/renderers/shared/legend.mjs +217 -0
- package/skills/interactive-page/scripts/archify/renderers/shared/output-path.mjs +340 -0
- package/skills/interactive-page/scripts/archify/renderers/shared/repository-evidence.mjs +238 -0
- package/skills/interactive-page/scripts/archify/renderers/shared/repository-location.mjs +58 -0
- package/skills/interactive-page/scripts/archify/renderers/shared/text-fit.mjs +49 -0
- package/skills/interactive-page/scripts/archify/renderers/shared/utils.mjs +232 -0
- package/skills/interactive-page/scripts/archify/renderers/shared/validator.mjs +86 -0
- package/skills/interactive-page/scripts/archify/renderers/workflow/README.md +223 -0
- package/skills/interactive-page/scripts/archify/renderers/workflow/render-workflow.mjs +35 -0
- package/skills/interactive-page/scripts/archify/renderers/workflow/workflow-compiler.mjs +4400 -0
- package/skills/interactive-page/scripts/archify/renderers/workflow/workflow-migration-geometry.mjs +144 -0
- package/skills/interactive-page/scripts/archify/schemas/README.md +211 -0
- package/skills/interactive-page/scripts/archify/schemas/architecture.schema.json +178 -0
- package/skills/interactive-page/scripts/archify/schemas/common.schema.json +115 -0
- package/skills/interactive-page/scripts/archify/schemas/dataflow.schema.json +243 -0
- package/skills/interactive-page/scripts/archify/schemas/lifecycle.schema.json +266 -0
- package/skills/interactive-page/scripts/archify/schemas/sequence.schema.json +223 -0
- package/skills/interactive-page/scripts/archify/schemas/workflow.schema.json +428 -0
- package/skills/interactive-page/scripts/archify/scripts/check-render-output.mjs +916 -0
- package/skills/interactive-page/scripts/archify/scripts/render-examples.mjs +26 -0
- package/skills/interactive-page/scripts/bundle-artifact.sh +9 -8
- package/skills/skill-creator/SKILL.md +4 -5
- package/.next/server/app/api/bot/task/[id]/route/app-paths-manifest.json +0 -3
- package/.next/server/app/api/bot/task/[id]/route.js +0 -16
- package/.next/server/app/api/bot/task/[id]/route.js.nft.json +0 -1
- package/.next/server/app/api/bot/task/[id]/route_client-reference-manifest.js +0 -3
- package/.next/server/app/api/bot/task/route/app-paths-manifest.json +0 -3
- package/.next/server/app/api/bot/task/route.js +0 -16
- package/.next/server/app/api/bot/task/route.js.nft.json +0 -1
- package/.next/server/app/api/bot/task/route_client-reference-manifest.js +0 -3
- package/.next/server/chunks/[root-of-the-server]__01dkp4a._.js +0 -3
- package/.next/server/chunks/[root-of-the-server]__02va5wz._.js +0 -3
- package/.next/server/chunks/[root-of-the-server]__02xjoto._.js +0 -3
- package/.next/server/chunks/[root-of-the-server]__04q--0f._.js +0 -8
- package/.next/server/chunks/[root-of-the-server]__0bl-e63._.js +0 -3
- package/.next/server/chunks/[root-of-the-server]__0ffbg85._.js +0 -3
- package/.next/server/chunks/[root-of-the-server]__0jq4ohy._.js +0 -8
- package/.next/server/chunks/[root-of-the-server]__0ls6_8y._.js +0 -8
- package/.next/server/chunks/[root-of-the-server]__0ncs9uu._.js +0 -3
- package/.next/server/chunks/[root-of-the-server]__1-ga0kh._.js +0 -5
- package/.next/server/chunks/[root-of-the-server]__11n256_._.js +0 -5
- package/.next/server/chunks/[root-of-the-server]__12cabg9._.js +0 -3
- package/.next/server/chunks/[root-of-the-server]__1339jnv._.js +0 -8
- package/.next/server/chunks/[root-of-the-server]__18_teo8._.js +0 -8
- package/.next/server/chunks/[root-of-the-server]__1_04mcf._.js +0 -5
- package/.next/server/chunks/[root-of-the-server]__1anc1p9._.js +0 -3
- package/.next/server/chunks/[root-of-the-server]__1ccon1w._.js +0 -10
- package/.next/server/chunks/[root-of-the-server]__1fizn31._.js +0 -3
- package/.next/server/chunks/[root-of-the-server]__1llj3-b._.js +0 -3
- package/.next/server/chunks/[root-of-the-server]__1p8m3f4._.js +0 -3
- package/.next/server/chunks/[root-of-the-server]__1wa1k7r._.js +0 -8
- package/.next/server/chunks/_0-73of8._.js +0 -55
- package/.next/server/chunks/_0-fjql4._.js +0 -26
- package/.next/server/chunks/_00xh_q0._.js +0 -25
- package/.next/server/chunks/_03a0897._.js +0 -3
- package/.next/server/chunks/_051qh8u._.js +0 -3
- package/.next/server/chunks/_05sovvj._.js +0 -3
- package/.next/server/chunks/_0fzqt2y._.js +0 -134
- package/.next/server/chunks/_0mecv0f._.js +0 -26
- package/.next/server/chunks/_0mh8j4m._.js +0 -3
- package/.next/server/chunks/_0obojvv._.js +0 -3
- package/.next/server/chunks/_0sb7tlz._.js +0 -3
- package/.next/server/chunks/_0sbynis._.js +0 -3
- package/.next/server/chunks/_0u10wmw._.js +0 -55
- package/.next/server/chunks/_0u4aiy2._.js +0 -3
- package/.next/server/chunks/_0wp4le3._.js +0 -3
- package/.next/server/chunks/_0y5fg-x._.js +0 -3
- package/.next/server/chunks/_1230wbx._.js +0 -10
- package/.next/server/chunks/_12nheja._.js +0 -3
- package/.next/server/chunks/_13510xh._.js +0 -3
- package/.next/server/chunks/_15hq89o._.js +0 -3
- package/.next/server/chunks/_18-1wcc._.js +0 -55
- package/.next/server/chunks/_1_9mjx-._.js +0 -3
- package/.next/server/chunks/_1c78vw5._.js +0 -229
- package/.next/server/chunks/_1h5_8sk._.js +0 -3
- package/.next/server/chunks/_1hdnxb7._.js +0 -3
- package/.next/server/chunks/_1j8k_j5._.js +0 -3
- package/.next/server/chunks/_1nula3v._.js +0 -3
- package/.next/server/chunks/_1php4eq._.js +0 -3
- package/.next/server/chunks/_1yuv4vp._.js +0 -3
- package/.next/server/chunks/_1zjrdag._.js +0 -3
- package/.next/server/chunks/_next-internal_server_app_api_bot_task_[id]_route_actions_1_k_8u3.js +0 -3
- package/.next/server/chunks/_next-internal_server_app_api_bot_task_route_actions_0xln6ra.js +0 -3
- package/.next/server/chunks/features_0rpfxbk._.js +0 -3
- package/.next/server/chunks/features_1qf3cei._.js +0 -3
- package/.next/server/chunks/features_bot_0zt92oh._.js +0 -3
- package/.next/server/chunks/node_modules_consola_dist_chunks_prompt_mjs_1xoq2rl._.js +0 -3
- package/.next/server/chunks/ssr/[externals]__0dizgek._.js +0 -3
- package/.next/server/chunks/ssr/[root-of-the-server]__092knlj._.js +0 -3
- package/.next/server/chunks/ssr/[root-of-the-server]__0mz33a7._.js +0 -3
- package/.next/server/chunks/ssr/[root-of-the-server]__0s9dq5u._.js +0 -3
- package/.next/server/chunks/ssr/[root-of-the-server]__0s9l2sv._.js +0 -3
- package/.next/server/chunks/ssr/[root-of-the-server]__0vnmv-s._.js +0 -3
- package/.next/server/chunks/ssr/[root-of-the-server]__0xsog8n._.js +0 -3
- package/.next/server/chunks/ssr/[root-of-the-server]__10-dz5o._.js +0 -61
- package/.next/server/chunks/ssr/[root-of-the-server]__1gtlw6v._.js +0 -43
- package/.next/server/chunks/ssr/[root-of-the-server]__1ox7hbk._.js +0 -4
- package/.next/server/chunks/ssr/[root-of-the-server]__202p40z._.js +0 -3
- package/.next/server/chunks/ssr/_001_ms1._.js +0 -28
- package/.next/server/chunks/ssr/_01n3o0m._.js +0 -3
- package/.next/server/chunks/ssr/_0fz4mf_._.js +0 -3
- package/.next/server/chunks/ssr/_0gb4gmf._.js +0 -3
- package/.next/server/chunks/ssr/_0h42fze._.js +0 -3
- package/.next/server/chunks/ssr/_0h7zdni._.js +0 -3
- package/.next/server/chunks/ssr/_0ilna52._.js +0 -42
- package/.next/server/chunks/ssr/_0q0zan6._.js +0 -70
- package/.next/server/chunks/ssr/_0zcjrbg._.js +0 -3
- package/.next/server/chunks/ssr/_0zhoc3r._.js +0 -3
- package/.next/server/chunks/ssr/_13fke0x._.js +0 -3
- package/.next/server/chunks/ssr/_18cymkq._.js +0 -3
- package/.next/server/chunks/ssr/_1b2mjwr._.js +0 -3
- package/.next/server/chunks/ssr/_1f-dg-6._.js +0 -3
- package/.next/server/chunks/ssr/_1h0rw11._.js +0 -3
- package/.next/server/chunks/ssr/_1jhd09t._.js +0 -3
- package/.next/server/chunks/ssr/_1nnsk7z._.js +0 -3
- package/.next/server/chunks/ssr/_1ov37if._.js +0 -3
- package/.next/server/chunks/ssr/_1sji22n._.js +0 -3
- package/.next/server/chunks/ssr/_1vi_uo7._.js +0 -3
- package/.next/server/chunks/ssr/_1vyd1ji._.js +0 -3
- package/.next/server/chunks/ssr/_1xx4zlb._.js +0 -3
- package/.next/server/chunks/ssr/components_ui_1y1l7yj._.js +0 -3
- package/.next/server/chunks/ssr/features_bot_1s_-2vj._.js +0 -3
- package/.next/server/chunks/ssr/lib_realtime_12wg_dd._.js +0 -27
- package/.next/server/chunks/ssr/lib_realtime_15h_v1v._.js +0 -27
- package/.next/server/chunks/ssr/node_modules_lucide-react_dist_esm_icons_0npjf8-._.js +0 -3
- package/.next/static/chunks/00rd4-v8r7axp.js +0 -1
- package/.next/static/chunks/08qe4as2q428z.js +0 -3
- package/.next/static/chunks/0gnqdjn2lvdad.js +0 -2
- package/.next/static/chunks/0hwap1-p0m5sa.js +0 -1
- package/.next/static/chunks/0r5qcctujhxli.js +0 -1
- package/.next/static/chunks/0w52rrrmuj9r9.js +0 -1
- package/.next/static/chunks/0wcprt5ot5she.js +0 -1
- package/.next/static/chunks/10teww7wt-f6h.js +0 -1
- package/.next/static/chunks/17p6tk6k88dq6.js +0 -1
- package/.next/static/chunks/18a96rkhi4_yd.js +0 -1
- package/.next/static/chunks/1dkjg87pxk0n9.js +0 -28
- package/.next/static/chunks/1jd09sxcjddu1.js +0 -1
- package/.next/static/chunks/1w73ece_6bheu.js +0 -18
- package/.next/static/chunks/214ya3n5gxeih.js +0 -1
- package/.next/static/chunks/22_zkas21rpna.js +0 -1
- package/.next/static/chunks/2fh4-3z_q8b_w.js +0 -1
- package/.next/static/chunks/2h0sd2vumhb15.js +0 -1
- package/.next/static/chunks/2t4vhi3m8ahya.js +0 -1
- package/.next/static/chunks/2tigdtz3dpcy8.js +0 -1
- package/.next/static/chunks/2u7xt6mxg44lf.js +0 -1
- package/.next/static/chunks/3786h5vayt80-.js +0 -28
- package/.next/static/chunks/39j0fvpybzabl.js +0 -40
- package/.next/static/chunks/3cq5g494yuke8.css +0 -1
- package/.next/static/chunks/3hibbjnt85-k6.js +0 -1
- package/.next/static/chunks/3woj-xd7ki5_i.js +0 -1
- package/.next/static/chunks/3xk7yqgomx7yq.js +0 -1
- package/.next/static/chunks/3zmcsnuo25zvq.js +0 -1
- package/database/migrations/20260911030320_opposite_jack_power/migration.sql +0 -1
- package/docs/images/task-thread.png +0 -0
- package/features/bot/components/task-badge.tsx +0 -42
- package/features/bot/components/task-reply.tsx +0 -328
- package/features/bot/components/task-setting.tsx +0 -524
- package/features/bot/task.query.ts +0 -976
- package/features/bot/task.store.ts +0 -489
- package/lib/realtime/client-secret.ts +0 -86
- package/lib/realtime/open-session.ts +0 -31
- package/lib/realtime/openai.ts +0 -35
- package/lib/realtime/realtime.audio.ts +0 -196
- package/lib/realtime/realtime.driver.ts +0 -489
- package/lib/realtime/realtime.protocol.ts +0 -393
- package/lib/realtime/realtime.schema.ts +0 -37
- package/lib/realtime/realtime.session.ts +0 -106
- package/lib/realtime/realtime.transport.ts +0 -384
- package/lib/realtime/xai.ts +0 -39
- /package/.next/server/app/api/bot/{task → thread}/[id]/route/build-manifest.json +0 -0
- /package/.next/server/app/api/bot/{task → thread}/[id]/route/server-reference-manifest.json +0 -0
- /package/.next/server/app/api/bot/{task → thread}/[id]/route.js.map +0 -0
- /package/.next/server/app/api/bot/{task → thread}/route/build-manifest.json +0 -0
- /package/.next/server/app/api/bot/{task → thread}/route/server-reference-manifest.json +0 -0
- /package/.next/server/app/api/bot/{task → thread}/route.js.map +0 -0
- /package/.next/static/{kaTO0UHqF1bMnWgCDik2f → 0qzGoKn2wTERVZtblX1tp}/_buildManifest.js +0 -0
- /package/.next/static/{kaTO0UHqF1bMnWgCDik2f → 0qzGoKn2wTERVZtblX1tp}/_clientMiddlewareManifest.js +0 -0
- /package/.next/static/{kaTO0UHqF1bMnWgCDik2f → 0qzGoKn2wTERVZtblX1tp}/_ssgManifest.js +0 -0
|
@@ -4,57 +4,51 @@ import { useCallback, useEffect, useRef, useState } from "react";
|
|
|
4
4
|
import { useAppEvent } from "@/app/api/events/app-event.client";
|
|
5
5
|
import { queryKey } from "@/app/api/query-key";
|
|
6
6
|
import { toast } from "@/components/ui/toast";
|
|
7
|
+
import { CALL_END, CALL_IDLE, CALL_RELAY } from "@/config";
|
|
7
8
|
import { TOOL_NAMES } from "@/features/ai/tools/tool-name";
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
TASK_CONTINUE,
|
|
12
|
-
type Task,
|
|
13
|
-
} from "@/features/bot/bot.schema";
|
|
14
|
-
import { MARK_BANDS } from "@/features/bot/mark.const";
|
|
15
|
-
import { botTasks, screenActs } from "@/features/bot/task.store";
|
|
9
|
+
import { acceptThreadRelaysAction } from "@/features/bot/bot.action";
|
|
10
|
+
import { type Bot, isAppStop, type Thread } from "@/features/bot/bot.schema";
|
|
11
|
+
import { botThreads, screenActs } from "@/features/bot/thread.store";
|
|
16
12
|
import { runRemoteTool } from "@/features/thursday/tool-call";
|
|
17
13
|
import { isCombo, useHotkey } from "@/hooks/use-hotkey";
|
|
18
14
|
import { useWakeWord } from "@/hooks/use-wake-word";
|
|
19
15
|
import { toDate } from "@/lib/date-like";
|
|
20
|
-
import {
|
|
16
|
+
import type { LiveClose } from "@/lib/live/live.schema";
|
|
17
|
+
import {
|
|
18
|
+
type LiveActivity,
|
|
19
|
+
type LiveSession,
|
|
20
|
+
type LiveToolCall,
|
|
21
|
+
openLiveSession,
|
|
22
|
+
} from "@/lib/live/live.session";
|
|
23
|
+
import {
|
|
24
|
+
type AudioTap,
|
|
25
|
+
createAudioTap,
|
|
26
|
+
SPECTRUM_BANDS,
|
|
27
|
+
} from "@/lib/live/live.tap";
|
|
28
|
+
import { type Result, unwrapResult } from "@/lib/protocol/result";
|
|
21
29
|
import {
|
|
22
30
|
revalidate,
|
|
23
31
|
revalidateAll,
|
|
24
32
|
useServerRoute,
|
|
25
33
|
} from "@/lib/protocol/use-server-route";
|
|
26
34
|
import { createOutbox, type Outbox } from "@/lib/queue";
|
|
27
|
-
import { openRealtimeSession } from "@/lib/realtime/open-session";
|
|
28
|
-
import type {
|
|
29
|
-
RealtimeActivity,
|
|
30
|
-
RealtimeSession,
|
|
31
|
-
RealtimeToolCall,
|
|
32
|
-
} from "@/lib/realtime/realtime.session";
|
|
33
|
-
import { type AudioTap, createAudioTap } from "@/lib/realtime/realtime.tap";
|
|
34
35
|
import { errorToString } from "@/lib/utils";
|
|
35
36
|
import {
|
|
36
37
|
endCallAction,
|
|
37
38
|
openCallAction,
|
|
39
|
+
saveThoughtAction,
|
|
38
40
|
saveTurnsAction,
|
|
39
41
|
} from "./thursday.action";
|
|
40
|
-
import type {
|
|
41
|
-
CallMessage,
|
|
42
|
-
CallStatus,
|
|
43
|
-
CallTurn,
|
|
44
|
-
LiveStatus,
|
|
45
|
-
} from "./thursday.schema";
|
|
42
|
+
import type { CallMessage, CallStatus, LiveStatus } from "./thursday.schema";
|
|
46
43
|
import { thursdaySettings, useThursdayStore } from "./thursday.store";
|
|
47
44
|
import { toolBot, toolLine } from "./tool-line";
|
|
48
45
|
|
|
49
46
|
/**
|
|
50
|
-
* One live call, plus the
|
|
51
|
-
* The server
|
|
52
|
-
* the page itself only hangs up.
|
|
47
|
+
* One live call, plus the thread inbox the app watches even with no call open.
|
|
48
|
+
* The server opens the Live session (openCallAction) and runs the tools
|
|
49
|
+
* (tool-call); the page itself only hangs up.
|
|
53
50
|
*/
|
|
54
51
|
|
|
55
|
-
/** Time for a tool result to reach the model before the socket closes. */
|
|
56
|
-
const HANG_UP_DELAY_MS = 400;
|
|
57
|
-
|
|
58
52
|
/** Plays when the line opens. */
|
|
59
53
|
const CONNECTED_SOUND = "/sounds/start_voice.ogg";
|
|
60
54
|
|
|
@@ -69,6 +63,13 @@ const TOOL_LINGER_MS = 2500;
|
|
|
69
63
|
/** A relay from a bot carries more to read than a tool line. */
|
|
70
64
|
const RELAY_LINGER_MS = 5000;
|
|
71
65
|
|
|
66
|
+
/**
|
|
67
|
+
* Once the backend's turn is over, how long the activity line keeps saying it is
|
|
68
|
+
* working while it waits for her voice. Her first word is what normally ends it;
|
|
69
|
+
* this is only for a turn that never reaches one.
|
|
70
|
+
*/
|
|
71
|
+
const THINKING_TAIL_MS = 6000;
|
|
72
|
+
|
|
72
73
|
/**
|
|
73
74
|
* The face lags the activity line: only a tool held longer than this switches
|
|
74
75
|
* to the working face. Shorter tools show on the line only.
|
|
@@ -81,92 +82,88 @@ const FACE_TOOL_MS = 800;
|
|
|
81
82
|
*/
|
|
82
83
|
const FACE_SETTLE_MS = 600;
|
|
83
84
|
|
|
84
|
-
/** Scrollback for the side-by-side layout; the call view shows at most three. */
|
|
85
|
-
const KEEP_MESSAGES = 24;
|
|
86
|
-
|
|
87
|
-
/** Safety net only; the event stream revalidates tasks as they change. */
|
|
88
|
-
const TASK_POLL_FALLBACK_MS = 30_000;
|
|
89
|
-
|
|
90
|
-
/** Notify once about failing saves, then stay quiet for the rest of the call. */
|
|
91
|
-
const SAVE_FAILURE_LIMIT = 3;
|
|
92
|
-
|
|
93
85
|
/**
|
|
94
|
-
*
|
|
95
|
-
* to
|
|
86
|
+
* How long the face says a call failed to open or dropped before it rests: long
|
|
87
|
+
* enough for the orb to spell its ERROR out. The toast carries the reason.
|
|
96
88
|
*/
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
/** If end_call does not follow the goodbye, the page hangs up. */
|
|
100
|
-
const IDLE_GRACE_MS = 15_000;
|
|
89
|
+
const FAILED_FACE_MS = 6000;
|
|
101
90
|
|
|
102
|
-
/**
|
|
103
|
-
const
|
|
91
|
+
/** Scrollback for the side-by-side layout; the call view shows at most three. */
|
|
92
|
+
const KEEP_MESSAGES = 24;
|
|
104
93
|
|
|
105
|
-
/**
|
|
106
|
-
|
|
107
|
-
* long. The line is up and the model is not on it, so there is no goodbye to
|
|
108
|
-
* ask for: the page hangs up. Only armed while an answer is owed, so a quiet
|
|
109
|
-
* line still ends the slow way (IDLE_HANG_UP_MS).
|
|
110
|
-
*/
|
|
111
|
-
const AGENT_SILENT_MS = 30_000;
|
|
94
|
+
/** Safety net only; the event stream revalidates threads as they change. */
|
|
95
|
+
const THREAD_POLL_FALLBACK_MS = 30_000;
|
|
112
96
|
|
|
113
|
-
|
|
114
|
-
|
|
97
|
+
/** Failed saves of one kind before the call stops saving that kind and says so, once. */
|
|
98
|
+
const SAVE_FAILURE_LIMIT = 3;
|
|
115
99
|
|
|
116
100
|
/**
|
|
117
|
-
*
|
|
118
|
-
*
|
|
101
|
+
* Put in as trusted behaviour just ahead of the first relay of a call. The relay
|
|
102
|
+
* itself carries facts only, since the backend reads relays too and routes answers
|
|
103
|
+
* by them.
|
|
119
104
|
*/
|
|
120
|
-
const
|
|
105
|
+
const RELAY_NOTE =
|
|
106
|
+
"An update from background work follows. If you have already told the user what it says, it need not be said again.";
|
|
121
107
|
|
|
122
|
-
|
|
123
|
-
type Said = {
|
|
124
|
-
line: string;
|
|
125
|
-
/** Shown on the activity line when the line is sent. */
|
|
126
|
-
show?: ToolRun;
|
|
127
|
-
};
|
|
108
|
+
const IDLE_LINE = `The user has said nothing for ${Math.round(CALL_IDLE.hangUpMs / 1000)} seconds. Say a one-line goodbye and end the call.`;
|
|
128
109
|
|
|
129
110
|
/**
|
|
130
|
-
*
|
|
131
|
-
* human-readable sentence (tool-line), null when none exists; `name` is the
|
|
111
|
+
* What the activity line draws: a tool the model is using, or a relay. `line` is
|
|
112
|
+
* the human-readable sentence (tool-line), null when none exists; `name` is the
|
|
132
113
|
* tool that actually ran.
|
|
133
114
|
*/
|
|
134
|
-
export type
|
|
115
|
+
export type ActivityLine = {
|
|
135
116
|
name: string;
|
|
136
117
|
line: string | null;
|
|
137
118
|
done: boolean;
|
|
138
|
-
/**
|
|
139
|
-
|
|
119
|
+
/** The tool call this is, so one call finishing never ends another's line. */
|
|
120
|
+
id?: string;
|
|
121
|
+
/** A relay from a bot (answer or question) rather than a tool; `name` is the thread label. */
|
|
122
|
+
kind?: "relay";
|
|
140
123
|
/** The bot this names, when it names one: the row draws its face instead of a glyph. */
|
|
141
124
|
bot?: string | null;
|
|
142
125
|
};
|
|
143
126
|
|
|
144
127
|
export function useThursday() {
|
|
145
|
-
const session = useRef<
|
|
128
|
+
const session = useRef<LiveSession | null>(null);
|
|
146
129
|
// created on the first call: `new Audio()` cannot run during SSR
|
|
147
130
|
const tap = useRef<AudioTap | null>(null);
|
|
148
131
|
/** Created inside the call-starting click; hang-up may come from a tool or a disconnect with no gesture. */
|
|
149
132
|
const farewell = useRef<HTMLAudioElement | null>(null);
|
|
150
133
|
|
|
151
134
|
const [status, setStatus] = useState<CallStatus>("idle");
|
|
135
|
+
/** A call just failed to open or dropped; the face says so for FAILED_FACE_MS. */
|
|
136
|
+
const [failed, setFailed] = useState(false);
|
|
137
|
+
const failedFor = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
138
|
+
const showFailed = useCallback((on: boolean) => {
|
|
139
|
+
if (failedFor.current) clearTimeout(failedFor.current);
|
|
140
|
+
failedFor.current = on
|
|
141
|
+
? setTimeout(() => setFailed(false), FAILED_FACE_MS)
|
|
142
|
+
: null;
|
|
143
|
+
setFailed(on);
|
|
144
|
+
}, []);
|
|
152
145
|
const [messages, setMessages] = useState<CallMessage[]>([]);
|
|
153
|
-
const [tool, setTool] = useState<
|
|
146
|
+
const [tool, setTool] = useState<ActivityLine | null>(null);
|
|
154
147
|
/** When the line opened (ms). */
|
|
155
148
|
const [since, setSince] = useState<number | null>(null);
|
|
156
|
-
/** Seconds until idle hang-up; set only inside
|
|
149
|
+
/** Seconds until idle hang-up; set only inside CALL_IDLE.warnMs. */
|
|
157
150
|
const [idleLeft, setIdleLeft] = useState<number | null>(null);
|
|
151
|
+
/** When the backend picked this turn up (ms); the activity line counts from it. */
|
|
152
|
+
const [thinkingSince, setThinkingSince] = useState<number | null>(null);
|
|
153
|
+
/** The same value where callbacks can read it, and the timer that ends it. */
|
|
154
|
+
const thinking = useRef<number | null>(null);
|
|
155
|
+
const thinkTail = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
158
156
|
/**
|
|
159
|
-
*
|
|
160
|
-
*
|
|
157
|
+
* A relayed update is out and has not been voiced yet: nothing else goes in
|
|
158
|
+
* until her voice has started and stopped.
|
|
161
159
|
*/
|
|
162
|
-
const
|
|
163
|
-
|
|
164
|
-
const mic = useRef<{
|
|
165
|
-
tool: boolean;
|
|
166
|
-
reading: boolean;
|
|
160
|
+
const reading = useRef<{
|
|
161
|
+
on: boolean;
|
|
167
162
|
spoke: boolean;
|
|
168
163
|
giveUp: ReturnType<typeof setTimeout> | null;
|
|
169
|
-
}>({
|
|
164
|
+
}>({ on: false, spoke: false, giveUp: null });
|
|
165
|
+
/** What `session.closed` confirmed for the call being ended; recorded on its row. */
|
|
166
|
+
const finalized = useRef<LiveClose | null>(null);
|
|
170
167
|
/**
|
|
171
168
|
* Last activity time, whether the goodbye was requested, and when an answer
|
|
172
169
|
* started being owed (null once she answers or works).
|
|
@@ -178,30 +175,53 @@ export function useThursday() {
|
|
|
178
175
|
grace: ReturnType<typeof setTimeout> | null;
|
|
179
176
|
}>({ since: 0, asked: false, owed: null, grace: null });
|
|
180
177
|
const linger = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
178
|
+
/** Whether the line holds a relay, so only a relay is finished by her voice. */
|
|
179
|
+
const relayOpen = useRef(false);
|
|
181
180
|
// Every server-run tool in this call starts with this signal, so one abort
|
|
182
|
-
// reaches running browser commands. Delegated
|
|
181
|
+
// reaches running browser commands. Delegated threads are server-owned and outlive the call
|
|
183
182
|
const working = useRef<AbortController | null>(null);
|
|
184
183
|
/** Row the turns are saved to; a ref so long-lived callbacks see it. */
|
|
185
184
|
const callId = useRef<string | null>(null);
|
|
186
185
|
/**
|
|
187
|
-
*
|
|
188
|
-
*
|
|
186
|
+
* Open work put to her while this page has been open, by item key (openWork):
|
|
187
|
+
* each goes in once a call. A key carries its job's last change, so a job that
|
|
188
|
+
* asks or ends again is new work.
|
|
189
189
|
*/
|
|
190
|
-
const
|
|
191
|
-
/** When this call was placed. An ending from before it is already in her prompt. */
|
|
192
|
-
const placedAt = useRef(0);
|
|
190
|
+
const told = useRef(new Set<string>());
|
|
193
191
|
/**
|
|
194
|
-
*
|
|
195
|
-
*
|
|
196
|
-
*
|
|
192
|
+
* Keys put in this call that her voice has not carried yet: rejected, cut short
|
|
193
|
+
* by a hang-up, or never spoken. Hanging up takes them out of `told`, so the
|
|
194
|
+
* next call puts them in again; what she voiced stays out.
|
|
197
195
|
*/
|
|
198
|
-
const
|
|
199
|
-
|
|
196
|
+
const unvoiced = useRef(new Set<string>());
|
|
197
|
+
/** The keys of the update on the line now, carried once her voice starts and stops on it. */
|
|
198
|
+
const onLine = useRef<string[]>([]);
|
|
200
199
|
/**
|
|
201
|
-
*
|
|
202
|
-
*
|
|
200
|
+
* When her voice was last heard: for letting a goodbye finish (CALL_END), and for
|
|
201
|
+
* telling words she already answered from words still owed an answer.
|
|
203
202
|
*/
|
|
204
|
-
const
|
|
203
|
+
const voiced = useRef(0);
|
|
204
|
+
/** The relay note went in on this call (RELAY_NOTE). */
|
|
205
|
+
const relayNoted = useRef(false);
|
|
206
|
+
/** The hang-up waiting on her goodbye once the backend called end_call. */
|
|
207
|
+
const leaving = useRef<ReturnType<typeof setInterval> | null>(null);
|
|
208
|
+
/** When either side's words were last transcribed: the quiet clock for relays. */
|
|
209
|
+
const heard = useRef(0);
|
|
210
|
+
/**
|
|
211
|
+
* The call-back placed this call and its first relay has not gone in yet: that one
|
|
212
|
+
* goes in once she has voiced the opening rather than waiting for a quiet line.
|
|
213
|
+
*/
|
|
214
|
+
const rang = useRef(false);
|
|
215
|
+
/** Backend work or a call tool is running, as the last activity said. */
|
|
216
|
+
const acting = useRef(false);
|
|
217
|
+
/** The inbox as last fetched, read by the relay clock. */
|
|
218
|
+
const latest = useRef<Thread[] | undefined>(undefined);
|
|
219
|
+
/**
|
|
220
|
+
* Context she need not say (screen acts), including what arrives before the
|
|
221
|
+
* line opens. Held until the session exists.
|
|
222
|
+
*/
|
|
223
|
+
const outboxRef = useRef<Outbox<string> | null>(null);
|
|
224
|
+
const outbox = (outboxRef.current ??= createOutbox<string>());
|
|
205
225
|
/** Wants a call, including while connecting. */
|
|
206
226
|
const calling = useRef(false);
|
|
207
227
|
/**
|
|
@@ -210,10 +230,18 @@ export function useThursday() {
|
|
|
210
230
|
* in `call` or the hang-up press is swallowed too.
|
|
211
231
|
*/
|
|
212
232
|
const opening = useRef(false);
|
|
233
|
+
const ending = useRef(false);
|
|
234
|
+
/**
|
|
235
|
+
* Which attempt owns the line. A hang-up during startup moves it on, so an
|
|
236
|
+
* attempt still connecting cannot come up after a newer call has been placed.
|
|
237
|
+
*/
|
|
238
|
+
const attempt = useRef(0);
|
|
213
239
|
|
|
214
|
-
const showTool = useCallback((call:
|
|
240
|
+
const showTool = useCallback((call: LiveToolCall) => {
|
|
215
241
|
if (linger.current) clearTimeout(linger.current);
|
|
242
|
+
relayOpen.current = false;
|
|
216
243
|
setTool({
|
|
244
|
+
id: call.id,
|
|
217
245
|
name: call.name,
|
|
218
246
|
line: toolLine(call.name, call.arguments),
|
|
219
247
|
bot: toolBot(call.name, call.arguments),
|
|
@@ -221,21 +249,22 @@ export function useThursday() {
|
|
|
221
249
|
});
|
|
222
250
|
}, []);
|
|
223
251
|
|
|
224
|
-
/**
|
|
225
|
-
|
|
252
|
+
/**
|
|
253
|
+
* A relay takes the tool line and runs there like a tool does: while it goes in,
|
|
254
|
+
* and while she voices it. Her voice finishing it is what stops the motion
|
|
255
|
+
* (doneReading); after that it stays long enough to read.
|
|
256
|
+
*/
|
|
257
|
+
const showRelay = useCallback((run: ActivityLine) => {
|
|
226
258
|
if (linger.current) clearTimeout(linger.current);
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
() => setTool((open) => (open?.kind === "relay" ? null : open)),
|
|
230
|
-
RELAY_LINGER_MS,
|
|
231
|
-
);
|
|
259
|
+
relayOpen.current = true;
|
|
260
|
+
setTool({ ...run, done: false });
|
|
232
261
|
}, []);
|
|
233
262
|
|
|
234
263
|
/** Tool finished; lingers, then clears. */
|
|
235
|
-
const hideTool = useCallback((
|
|
264
|
+
const hideTool = useCallback((id: string) => {
|
|
236
265
|
setTool((open) =>
|
|
237
266
|
// a second tool already took the line; leave it
|
|
238
|
-
open?.
|
|
267
|
+
open?.id === id ? { ...open, done: true } : open,
|
|
239
268
|
);
|
|
240
269
|
if (linger.current) clearTimeout(linger.current);
|
|
241
270
|
linger.current = setTimeout(
|
|
@@ -244,36 +273,144 @@ export function useThursday() {
|
|
|
244
273
|
);
|
|
245
274
|
}, []);
|
|
246
275
|
|
|
247
|
-
/** The
|
|
248
|
-
const
|
|
249
|
-
|
|
276
|
+
/** The relayed update was voiced, or waited on long enough: the next can go in. */
|
|
277
|
+
const doneReading = useCallback(() => {
|
|
278
|
+
if (reading.current.giveUp) clearTimeout(reading.current.giveUp);
|
|
279
|
+
// Her voice started and stopped on it: carried
|
|
280
|
+
if (reading.current.spoke) {
|
|
281
|
+
for (const key of onLine.current) unvoiced.current.delete(key);
|
|
282
|
+
}
|
|
283
|
+
onLine.current = [];
|
|
284
|
+
reading.current = { on: false, spoke: false, giveUp: null };
|
|
285
|
+
if (!relayOpen.current) return;
|
|
286
|
+
relayOpen.current = false;
|
|
287
|
+
setTool((open) =>
|
|
288
|
+
open?.kind === "relay" ? { ...open, done: true } : open,
|
|
289
|
+
);
|
|
290
|
+
if (linger.current) clearTimeout(linger.current);
|
|
291
|
+
linger.current = setTimeout(
|
|
292
|
+
() => setTool((open) => (open?.kind === "relay" ? null : open)),
|
|
293
|
+
RELAY_LINGER_MS,
|
|
294
|
+
);
|
|
250
295
|
}, []);
|
|
251
296
|
|
|
252
|
-
|
|
253
|
-
if (mic.current.giveUp) clearTimeout(mic.current.giveUp);
|
|
254
|
-
mic.current.giveUp = null;
|
|
255
|
-
mic.current.reading = false;
|
|
256
|
-
mic.current.spoke = false;
|
|
257
|
-
syncMic();
|
|
258
|
-
}, [syncMic]);
|
|
259
|
-
|
|
260
|
-
/** A relayed line is on the wire; the mic stays muted until it is read. */
|
|
297
|
+
/** An update is on the wire: nothing else goes in until she has voiced it. */
|
|
261
298
|
const readAloud = useCallback(() => {
|
|
262
|
-
if (
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
299
|
+
if (reading.current.giveUp) clearTimeout(reading.current.giveUp);
|
|
300
|
+
reading.current = {
|
|
301
|
+
on: true,
|
|
302
|
+
spoke: false,
|
|
303
|
+
giveUp: setTimeout(doneReading, CALL_RELAY.readMs),
|
|
304
|
+
};
|
|
305
|
+
}, [doneReading]);
|
|
306
|
+
|
|
307
|
+
const setThinking = useCallback((at: number | null) => {
|
|
308
|
+
thinking.current = at;
|
|
309
|
+
setThinkingSince(at);
|
|
310
|
+
}, []);
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* What the activity line says about the backend. It starts when the backend takes the
|
|
314
|
+
* turn and ends on her first word, not on the response: a turn that calls two
|
|
315
|
+
* tools settles for a moment between them, and the wait before she speaks is
|
|
316
|
+
* still the same stretch of work. Both would otherwise read as the call stalling.
|
|
317
|
+
*/
|
|
318
|
+
const holdThinking = useCallback(
|
|
319
|
+
(busy: boolean, speaking: boolean) => {
|
|
320
|
+
const dropTail = () => {
|
|
321
|
+
if (thinkTail.current) clearTimeout(thinkTail.current);
|
|
322
|
+
thinkTail.current = null;
|
|
323
|
+
};
|
|
324
|
+
// Her voice always wins: it is the answer the work was for
|
|
325
|
+
if (speaking) {
|
|
326
|
+
dropTail();
|
|
327
|
+
if (thinking.current !== null) setThinking(null);
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
if (busy) {
|
|
331
|
+
dropTail();
|
|
332
|
+
if (thinking.current === null) setThinking(Date.now());
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
if (thinking.current !== null && !thinkTail.current) {
|
|
336
|
+
thinkTail.current = setTimeout(() => {
|
|
337
|
+
thinkTail.current = null;
|
|
338
|
+
setThinking(null);
|
|
339
|
+
}, THINKING_TAIL_MS);
|
|
340
|
+
}
|
|
341
|
+
},
|
|
342
|
+
[setThinking],
|
|
343
|
+
);
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Puts to her what background work still waits on the user, once neither
|
|
347
|
+
* side's words have been transcribed for CALL_RELAY.quietMs and nothing is
|
|
348
|
+
* running: questions not answered, jobs ended or stopped and not yet seen,
|
|
349
|
+
* and progress from jobs still running. Live never speaks unprompted, so
|
|
350
|
+
* nothing reaches the user unless this puts it in. Handling an item — an
|
|
351
|
+
* answer, `thread` `seen`, a click on screen — takes it off the list. Each
|
|
352
|
+
* item goes in once a call, and once her voice has carried it, not on later
|
|
353
|
+
* calls either (`told`, `unvoiced`). One append holds one kind, so an ending
|
|
354
|
+
* is never lost among questions. Nothing goes in once the call is ending.
|
|
355
|
+
*/
|
|
356
|
+
const relayOpenWork = useCallback(() => {
|
|
357
|
+
const live = session.current;
|
|
358
|
+
const threads = latest.current;
|
|
359
|
+
if (!live || !threads || reading.current.on || acting.current) return;
|
|
360
|
+
if (leaving.current || idle.current.asked) return;
|
|
361
|
+
// On a call-back, why she called goes in once the opening is voiced: it is the
|
|
362
|
+
// first thing they ask, and a quiet line would come too late
|
|
363
|
+
const rung = rang.current;
|
|
364
|
+
rang.current = false;
|
|
365
|
+
if (!rung && Date.now() - heard.current < CALL_RELAY.quietMs) return;
|
|
366
|
+
const open = openWork(threads).filter(
|
|
367
|
+
(item) => !told.current.has(item.key),
|
|
368
|
+
);
|
|
369
|
+
const first = open[0];
|
|
370
|
+
if (!first) return;
|
|
371
|
+
const due = open
|
|
372
|
+
.filter((item) => item.kind === first.kind)
|
|
373
|
+
.slice(0, CALL_RELAY.perTurn);
|
|
374
|
+
const lines = due.map((item) => item.line);
|
|
375
|
+
|
|
376
|
+
// Told as it goes out, even if Live refuses it: a refusal is tried again on the next
|
|
377
|
+
// call (unvoiced), never in a loop on this one
|
|
378
|
+
for (const item of due) {
|
|
379
|
+
told.current.add(item.key);
|
|
380
|
+
unvoiced.current.add(item.key);
|
|
381
|
+
}
|
|
382
|
+
readAloud();
|
|
383
|
+
onLine.current = due.map((item) => item.key);
|
|
384
|
+
// On the line before it goes out, so it runs there for the whole wait
|
|
385
|
+
showRelay((due.at(-1) ?? first).show);
|
|
386
|
+
// Appends go out in order, so the note is in her context before the first update
|
|
387
|
+
if (!relayNoted.current) {
|
|
388
|
+
relayNoted.current = true;
|
|
389
|
+
void live.append("instructions", RELAY_NOTE);
|
|
390
|
+
}
|
|
391
|
+
void live
|
|
392
|
+
.append(
|
|
393
|
+
"commentary",
|
|
394
|
+
lines.length === 1
|
|
395
|
+
? lines.join("")
|
|
396
|
+
: `[${lines.length} updates.]\n\n${lines.join("\n\n")}`,
|
|
397
|
+
)
|
|
398
|
+
.then((delivered) => {
|
|
399
|
+
if (!delivered) {
|
|
400
|
+
doneReading();
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
const ids = due.flatMap((item) => item.relayIds);
|
|
404
|
+
if (ids.length) return acceptThreadRelaysAction(ids).then(unwrapResult);
|
|
405
|
+
})
|
|
406
|
+
.catch((cause) =>
|
|
407
|
+
toast.add({
|
|
408
|
+
type: "error",
|
|
409
|
+
title: "Could not record relay delivery",
|
|
410
|
+
description: errorToString(cause),
|
|
411
|
+
}),
|
|
412
|
+
);
|
|
413
|
+
}, [readAloud, doneReading, showRelay]);
|
|
277
414
|
|
|
278
415
|
/**
|
|
279
416
|
* One timer; when it fires it wears the last wanted face, so a tool shorter
|
|
@@ -335,24 +472,22 @@ export function useThursday() {
|
|
|
335
472
|
[restFace, wearFace],
|
|
336
473
|
);
|
|
337
474
|
|
|
338
|
-
const onCall =
|
|
475
|
+
const onCall =
|
|
476
|
+
status !== "idle" && status !== "connecting" && status !== "ending";
|
|
339
477
|
|
|
340
478
|
// Inbox, with or without a call. Revalidated on server events; polling is the safety net
|
|
341
|
-
const { data:
|
|
342
|
-
refreshInterval:
|
|
479
|
+
const { data: threads } = useServerRoute<Thread[]>(queryKey.threads, {
|
|
480
|
+
refreshInterval: THREAD_POLL_FALLBACK_MS,
|
|
343
481
|
});
|
|
344
482
|
|
|
345
483
|
// Server event stream (app/api/events): signals revalidate their key
|
|
346
|
-
const boot = useRef<string | null>(null);
|
|
347
484
|
useAppEvent({
|
|
348
|
-
hello: (
|
|
485
|
+
hello: () => {
|
|
349
486
|
// Sent on every connect, so this is also every reconnect: nothing that
|
|
350
|
-
// changed while the stream was down raised a signal anybody heard
|
|
351
|
-
// changed `boot` means the server restarted on top of that
|
|
352
|
-
boot.current = event.boot;
|
|
487
|
+
// changed while the stream was down raised a signal anybody heard
|
|
353
488
|
void revalidateAll();
|
|
354
489
|
},
|
|
355
|
-
|
|
490
|
+
threads: () => void revalidate(queryKey.threads),
|
|
356
491
|
memory: () => void revalidate(queryKey.memory),
|
|
357
492
|
mcp: () => {
|
|
358
493
|
void revalidate(queryKey.mcp);
|
|
@@ -368,51 +503,34 @@ export function useThursday() {
|
|
|
368
503
|
// faces come from the bot list
|
|
369
504
|
const { data: bots } = useServerRoute<Bot[]>(queryKey.bot);
|
|
370
505
|
useEffect(() => {
|
|
371
|
-
if (
|
|
372
|
-
}, [
|
|
373
|
-
|
|
374
|
-
// A question is relayed on every call, because it still needs an answer. An
|
|
375
|
-
// ending is relayed only on the call it happened during: one from before is
|
|
376
|
-
// already in her prompt, on the `delegate` line that opened it. Telling is not
|
|
377
|
-
// seeing — the badge clears when the user opens the job. Sending while the
|
|
378
|
-
// line is still opening is fine: the outbox holds the line. `status` is in
|
|
379
|
-
// deps because the guard is a ref; without it a list that arrived before the
|
|
380
|
-
// line opened would wait for the next tasks change
|
|
506
|
+
if (threads) botThreads.sync(threads, bots);
|
|
507
|
+
}, [threads, bots]);
|
|
508
|
+
|
|
381
509
|
useEffect(() => {
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
if (task.status === "running") continue;
|
|
385
|
-
// A stop the app picks back up by itself is not news: it runs again in a moment
|
|
386
|
-
if (task.ask?.auto) continue;
|
|
387
|
-
if (
|
|
388
|
-
task.status !== "waiting" &&
|
|
389
|
-
toDate(task.updatedAt).getTime() < placedAt.current
|
|
390
|
-
)
|
|
391
|
-
continue;
|
|
392
|
-
const key = relayKey(task);
|
|
393
|
-
if (relayed.current.has(key)) continue;
|
|
394
|
-
relayed.current.add(key);
|
|
395
|
-
outbox.send({ line: relayLine(task), show: relayShow(task) });
|
|
396
|
-
}
|
|
397
|
-
}, [tasks, outbox, status]);
|
|
510
|
+
latest.current = threads;
|
|
511
|
+
}, [threads]);
|
|
398
512
|
|
|
399
|
-
// The user acted on a
|
|
513
|
+
// The user acted on a thread on screen; she may have just read that question and must not ask again
|
|
400
514
|
useEffect(
|
|
401
515
|
() =>
|
|
402
516
|
screenActs.subscribe((act) => {
|
|
403
517
|
if (!calling.current) return;
|
|
404
|
-
outbox.send(
|
|
405
|
-
|
|
406
|
-
act.
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
});
|
|
518
|
+
outbox.send(
|
|
519
|
+
act.kind === "answered"
|
|
520
|
+
? `The user sent a message on screen to ${act.recipient ?? "the coordinator"} in thread "${act.label}" (${act.id})${act.replyTo ? `, replying to ${act.replyTo}` : ""}: ${act.answer}. It has reached that participant.`
|
|
521
|
+
: `The user stopped thread "${act.label}" on screen. It is no longer running.`,
|
|
522
|
+
);
|
|
410
523
|
}),
|
|
411
524
|
[outbox],
|
|
412
525
|
);
|
|
413
526
|
|
|
414
527
|
/** Closes the session and resets state. Turns were saved during the call. */
|
|
415
528
|
const hangUp = useCallback(async () => {
|
|
529
|
+
if (ending.current) return;
|
|
530
|
+
ending.current = true;
|
|
531
|
+
if (leaving.current) clearInterval(leaving.current);
|
|
532
|
+
leaving.current = null;
|
|
533
|
+
attempt.current += 1;
|
|
416
534
|
const live = session.current;
|
|
417
535
|
const call = callId.current;
|
|
418
536
|
|
|
@@ -420,16 +538,16 @@ export function useThursday() {
|
|
|
420
538
|
callId.current = null;
|
|
421
539
|
calling.current = false;
|
|
422
540
|
opening.current = false;
|
|
423
|
-
|
|
424
|
-
|
|
541
|
+
relayNoted.current = false;
|
|
542
|
+
rang.current = false;
|
|
543
|
+
// What she did not voice goes in again next call; unsent context goes with the session
|
|
544
|
+
for (const key of unvoiced.current) told.current.delete(key);
|
|
545
|
+
unvoiced.current.clear();
|
|
425
546
|
outbox.close();
|
|
426
547
|
outbox.clear();
|
|
427
|
-
// holds the closed session; the next call sets its own
|
|
428
|
-
resumeRelay.current = null;
|
|
429
548
|
working.current?.abort();
|
|
430
549
|
working.current = null;
|
|
431
|
-
|
|
432
|
-
openMic();
|
|
550
|
+
doneReading();
|
|
433
551
|
if (linger.current) clearTimeout(linger.current);
|
|
434
552
|
// a pending face timer must not fire after the call
|
|
435
553
|
restFace();
|
|
@@ -437,28 +555,38 @@ export function useThursday() {
|
|
|
437
555
|
idle.current = { since: 0, asked: false, owed: null, grace: null };
|
|
438
556
|
setIdleLeft(null);
|
|
439
557
|
setSince(null);
|
|
440
|
-
setStatus("idle");
|
|
558
|
+
setStatus(live ? "ending" : "idle");
|
|
441
559
|
setMessages([]);
|
|
442
560
|
setTool(null);
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
561
|
+
setThinking(null);
|
|
562
|
+
if (thinkTail.current) clearTimeout(thinkTail.current);
|
|
563
|
+
thinkTail.current = null;
|
|
564
|
+
relayOpen.current = false;
|
|
565
|
+
// the room is not cleared: threads outlive the call
|
|
566
|
+
|
|
567
|
+
// Waits (bounded) for session.closed, which saves the last turns and says
|
|
568
|
+
// what was billed; the row is ended with that, after the saves it queued
|
|
569
|
+
await live?.close();
|
|
570
|
+
const close = finalized.current;
|
|
571
|
+
finalized.current = null;
|
|
572
|
+
ending.current = false;
|
|
573
|
+
setStatus("idle");
|
|
574
|
+
if (call) void endCallAction(call, close);
|
|
448
575
|
// only when there was a line to close
|
|
449
576
|
if (live && farewell.current) {
|
|
450
577
|
farewell.current.currentTime = 0;
|
|
451
578
|
void farewell.current.play().catch(() => {});
|
|
452
579
|
}
|
|
453
|
-
}, [outbox, restFace,
|
|
580
|
+
}, [outbox, restFace, doneReading, setThinking]);
|
|
454
581
|
|
|
455
582
|
// Unmount during a call must release the mic and stop tools
|
|
456
583
|
useEffect(() => {
|
|
457
584
|
return () => {
|
|
458
|
-
|
|
585
|
+
attempt.current += 1;
|
|
586
|
+
void session.current?.close();
|
|
459
587
|
session.current = null;
|
|
460
588
|
// Closing only the session would leave the row live (thursday.query
|
|
461
|
-
// isAnyCallLive) and finished
|
|
589
|
+
// isAnyCallLive) and finished threads unannounced (bot.runner); end the row too
|
|
462
590
|
const call = callId.current;
|
|
463
591
|
callId.current = null;
|
|
464
592
|
calling.current = false;
|
|
@@ -469,27 +597,48 @@ export function useThursday() {
|
|
|
469
597
|
// clear timers so nothing sets state on an unmounted tree
|
|
470
598
|
if (linger.current) clearTimeout(linger.current);
|
|
471
599
|
if (faceIn.current) clearTimeout(faceIn.current);
|
|
472
|
-
if (
|
|
600
|
+
if (reading.current.giveUp) clearTimeout(reading.current.giveUp);
|
|
601
|
+
if (thinkTail.current) clearTimeout(thinkTail.current);
|
|
473
602
|
if (idle.current.grace) clearTimeout(idle.current.grace);
|
|
603
|
+
if (failedFor.current) clearTimeout(failedFor.current);
|
|
604
|
+
if (leaving.current) clearInterval(leaving.current);
|
|
474
605
|
};
|
|
475
606
|
}, []);
|
|
476
607
|
|
|
477
608
|
/**
|
|
478
|
-
*
|
|
479
|
-
*
|
|
609
|
+
* The backend ended the call: hang up once her goodbye is over. She says it while
|
|
610
|
+
* the backend works, so it may be done already, still going, or yet to start;
|
|
611
|
+
* CALL_END bounds each case. The tool's own result always gets `quietMs` to go out.
|
|
480
612
|
*/
|
|
481
|
-
const
|
|
613
|
+
const leave = useCallback(() => {
|
|
614
|
+
if (leaving.current) return;
|
|
615
|
+
const asked = Date.now();
|
|
616
|
+
leaving.current = setInterval(() => {
|
|
617
|
+
const now = Date.now();
|
|
618
|
+
// A goodbye heard just before the backend ended the call counts as said
|
|
619
|
+
const said = voiced.current > asked - CALL_END.unsaidMs;
|
|
620
|
+
const over = said
|
|
621
|
+
? now - Math.max(voiced.current, asked) >= CALL_END.quietMs
|
|
622
|
+
: now - asked >= CALL_END.unsaidMs;
|
|
623
|
+
if (over || now - asked >= CALL_END.maxMs) void hangUp();
|
|
624
|
+
}, 100);
|
|
625
|
+
}, [hangUp]);
|
|
626
|
+
|
|
627
|
+
/**
|
|
628
|
+
* Rewinds the idle clock. `user` is new words transcribed from them, first seen at
|
|
629
|
+
* `said`: an answer is owed from then, and a requested goodbye is off. Transcripts
|
|
630
|
+
* lag the audio, so her voice heard since `said` already answered them. `agent` is
|
|
631
|
+
* her voice or backend work, which settles what is owed but cannot cancel its own
|
|
632
|
+
* goodbye.
|
|
633
|
+
*/
|
|
634
|
+
const stir = useCallback((who: "user" | "agent", said = 0) => {
|
|
482
635
|
const now = Date.now();
|
|
483
636
|
idle.current.since = now;
|
|
484
637
|
if (who !== "user") {
|
|
485
|
-
// Words or a tool: she is on the line, so nothing is owed.
|
|
486
638
|
idle.current.owed = null;
|
|
487
639
|
return;
|
|
488
640
|
}
|
|
489
|
-
|
|
490
|
-
// turn is never mistaken for a line that stopped answering; it runs from the
|
|
491
|
-
// moment they stopped rather than from the moment they started.
|
|
492
|
-
idle.current.owed = now;
|
|
641
|
+
if (voiced.current < said) idle.current.owed ??= now;
|
|
493
642
|
if (idle.current.grace) clearTimeout(idle.current.grace);
|
|
494
643
|
idle.current.grace = null;
|
|
495
644
|
idle.current.asked = false;
|
|
@@ -503,7 +652,7 @@ export function useThursday() {
|
|
|
503
652
|
const live = session.current;
|
|
504
653
|
if (!live) return;
|
|
505
654
|
const owed = idle.current.owed;
|
|
506
|
-
if (owed !== null && Date.now() - owed >=
|
|
655
|
+
if (owed !== null && Date.now() - owed >= CALL_IDLE.agentSilentMs) {
|
|
507
656
|
// Nothing to say goodbye with; the model is what would have said it.
|
|
508
657
|
toast.add({
|
|
509
658
|
type: "error",
|
|
@@ -513,225 +662,300 @@ export function useThursday() {
|
|
|
513
662
|
void hangUp();
|
|
514
663
|
return;
|
|
515
664
|
}
|
|
516
|
-
const left =
|
|
665
|
+
const left = CALL_IDLE.hangUpMs - (Date.now() - idle.current.since);
|
|
517
666
|
setIdleLeft(
|
|
518
|
-
left <=
|
|
667
|
+
left <= CALL_IDLE.warnMs && !idle.current.asked
|
|
519
668
|
? Math.max(0, Math.ceil(left / 1000))
|
|
520
669
|
: null,
|
|
521
670
|
);
|
|
522
671
|
if (left > 0 || idle.current.asked) return;
|
|
523
672
|
|
|
524
673
|
idle.current.asked = true;
|
|
525
|
-
live.
|
|
526
|
-
idle.current.grace = setTimeout(() => void hangUp(),
|
|
674
|
+
void live.append("instructions", IDLE_LINE);
|
|
675
|
+
idle.current.grace = setTimeout(() => void hangUp(), CALL_IDLE.graceMs);
|
|
527
676
|
}, 1000);
|
|
528
677
|
return () => clearInterval(tick);
|
|
529
678
|
}, [onCall, stir, hangUp]);
|
|
530
679
|
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
if (
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
role: turn.role,
|
|
590
|
-
text: turn.text,
|
|
591
|
-
};
|
|
592
|
-
// holes are turns whose text has not arrived
|
|
593
|
-
setMessages(turns.filter(Boolean).slice(-KEEP_MESSAGES));
|
|
594
|
-
}
|
|
595
|
-
if (turn.done) {
|
|
596
|
-
const { id, role, tool, text, seq } = turn;
|
|
597
|
-
persistTurn(
|
|
598
|
-
handshake.callId,
|
|
599
|
-
{ id, role, tool, text, seq },
|
|
600
|
-
saving,
|
|
601
|
-
);
|
|
680
|
+
// The relay clock: open work goes in only while the line is quiet
|
|
681
|
+
useEffect(() => {
|
|
682
|
+
if (!onCall) return;
|
|
683
|
+
const tick = setInterval(relayOpenWork, 1000);
|
|
684
|
+
return () => clearInterval(tick);
|
|
685
|
+
}, [onCall, relayOpenWork]);
|
|
686
|
+
|
|
687
|
+
const call = useCallback(
|
|
688
|
+
async (placedBy?: "callBack") => {
|
|
689
|
+
// Compared exactly: a face tap hands its click event in here
|
|
690
|
+
const calledBack = placedBy === "callBack";
|
|
691
|
+
// Guard with a ref, not `status`: three entry points (face, wake word,
|
|
692
|
+
// hotkey) can fire in one frame and both see a stale "idle", opening two sessions
|
|
693
|
+
if (opening.current || ending.current) return;
|
|
694
|
+
// with a line open this press hangs up; `calling` covers the gap before re-render
|
|
695
|
+
if (calling.current || status !== "idle") return hangUp();
|
|
696
|
+
|
|
697
|
+
setStatus("connecting");
|
|
698
|
+
showFailed(false);
|
|
699
|
+
// from here on this is a call; the outbox holds updates until the session exists
|
|
700
|
+
opening.current = true;
|
|
701
|
+
calling.current = true;
|
|
702
|
+
finalized.current = null;
|
|
703
|
+
relayNoted.current = false;
|
|
704
|
+
outbox.clear();
|
|
705
|
+
attempt.current += 1;
|
|
706
|
+
const mine = attempt.current;
|
|
707
|
+
const current = () => calling.current && attempt.current === mine;
|
|
708
|
+
try {
|
|
709
|
+
// Inside the gesture, before anything awaits: an AudioContext created later
|
|
710
|
+
// starts suspended. Calls from the wake word or a call-back have no gesture;
|
|
711
|
+
// armAudioUnlock handles those
|
|
712
|
+
tap.current ??= createAudioTap();
|
|
713
|
+
armAudioUnlock(tap.current.open().context);
|
|
714
|
+
const chime = new Audio(CONNECTED_SOUND);
|
|
715
|
+
farewell.current ??= new Audio(HUNG_UP_SOUND);
|
|
716
|
+
|
|
717
|
+
const settings = thursdaySettings();
|
|
718
|
+
const stop = new AbortController();
|
|
719
|
+
working.current = stop;
|
|
720
|
+
const saving = { failures: 0 };
|
|
721
|
+
const thinkingSaves = { failures: 0 };
|
|
722
|
+
/** Filled in by the handshake, read by callbacks that only run after it. */
|
|
723
|
+
const line = { callId: "", opening: null as string | null };
|
|
724
|
+
|
|
725
|
+
// one turn per id; the session reports display groups one at a time
|
|
726
|
+
const turns = new Map<string, CallMessage & { seq: number }>();
|
|
727
|
+
/** When the page first saw each of the user's display groups. */
|
|
728
|
+
const userSaid = new Map<string, number>();
|
|
729
|
+
|
|
730
|
+
const live = await openLiveSession({
|
|
731
|
+
initialize: async (sdp) => {
|
|
732
|
+
const handshake = unwrapResult(
|
|
733
|
+
await openCallAction(settings, sdp, calledBack),
|
|
734
|
+
);
|
|
735
|
+
if (!current()) {
|
|
736
|
+
void endCallAction(handshake.callId);
|
|
737
|
+
throw new Error("The call closed during startup.");
|
|
602
738
|
}
|
|
739
|
+
callId.current = handshake.callId;
|
|
740
|
+
line.callId = handshake.callId;
|
|
741
|
+
line.opening = handshake.opening;
|
|
742
|
+
return handshake.sdp;
|
|
603
743
|
},
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
if (activity.speaking) mic.current.spoke = true;
|
|
614
|
-
else if (mic.current.spoke) {
|
|
615
|
-
mic.current.reading = false;
|
|
616
|
-
// she has finished reading; whatever landed meanwhile goes now
|
|
617
|
-
resumeRelay.current?.();
|
|
744
|
+
audio: tap.current,
|
|
745
|
+
on: {
|
|
746
|
+
// the backend calls tools; the page forwards them to the server
|
|
747
|
+
runTool: async (call) => {
|
|
748
|
+
// end_call is the page's only tool. The line goes down once her
|
|
749
|
+
// goodbye is over (leave), so this reply reaches the model first
|
|
750
|
+
if (call.name === TOOL_NAMES.end_call) {
|
|
751
|
+
leave();
|
|
752
|
+
return "Ending the call.";
|
|
618
753
|
}
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
754
|
+
showTool(call);
|
|
755
|
+
try {
|
|
756
|
+
return await runRemoteTool(line.callId, call, stop.signal);
|
|
757
|
+
} finally {
|
|
758
|
+
hideTool(call.id);
|
|
759
|
+
}
|
|
760
|
+
},
|
|
761
|
+
reasoning: (part) =>
|
|
762
|
+
persist(
|
|
763
|
+
thinkingSaves,
|
|
764
|
+
() =>
|
|
765
|
+
saveThoughtAction(line.callId, {
|
|
766
|
+
...part,
|
|
767
|
+
seq: Math.max(0, Math.round(part.seq)),
|
|
768
|
+
}),
|
|
769
|
+
"The backend's thinking is not being saved",
|
|
770
|
+
),
|
|
771
|
+
turn: (turn) => {
|
|
772
|
+
// New words, not a checkpoint of the same ones: a blank fragment is not words
|
|
773
|
+
const fresh =
|
|
774
|
+
turn.role !== "tool" &&
|
|
775
|
+
Boolean(turn.text.trim()) &&
|
|
776
|
+
turn.text !== turns.get(turn.id)?.text;
|
|
777
|
+
// New words from either side restart the relay's quiet clock
|
|
778
|
+
if (fresh) heard.current = Date.now();
|
|
779
|
+
// Her voice and backend work rewind the idle clock from activity, where an
|
|
780
|
+
// update she reads out is told apart; transcripts lag the audio and cannot
|
|
781
|
+
if (fresh && turn.role === "user") {
|
|
782
|
+
const said = userSaid.get(turn.id) ?? Date.now();
|
|
783
|
+
userSaid.set(turn.id, said);
|
|
784
|
+
stir("user", said);
|
|
785
|
+
}
|
|
786
|
+
// Tool turns are saved but not shown. Hanging up clears the screen
|
|
787
|
+
// before `close()` checkpoints open groups, so only the live call draws.
|
|
788
|
+
if (turn.role !== "tool" && callId.current === line.callId) {
|
|
789
|
+
turns.set(turn.id, {
|
|
790
|
+
seq: turn.seq,
|
|
791
|
+
id: turn.id,
|
|
792
|
+
role: turn.role,
|
|
793
|
+
text: turn.text,
|
|
794
|
+
});
|
|
795
|
+
setMessages(
|
|
796
|
+
[...turns.values()]
|
|
797
|
+
.sort((a, b) => a.seq - b.seq)
|
|
798
|
+
.slice(-KEEP_MESSAGES),
|
|
799
|
+
);
|
|
800
|
+
}
|
|
801
|
+
if (turn.done) {
|
|
802
|
+
persist(saving, () =>
|
|
803
|
+
saveTurnsAction(line.callId, [
|
|
804
|
+
{
|
|
805
|
+
id: turn.id,
|
|
806
|
+
role: turn.role,
|
|
807
|
+
tool: turn.tool,
|
|
808
|
+
text: turn.text,
|
|
809
|
+
// Live orders by audio milliseconds; the row keeps a whole number
|
|
810
|
+
seq: Math.max(0, Math.round(turn.seq)),
|
|
811
|
+
fragments: turn.fragments ?? null,
|
|
812
|
+
},
|
|
813
|
+
]),
|
|
814
|
+
);
|
|
815
|
+
}
|
|
816
|
+
},
|
|
817
|
+
// session facts drive the face through showFace; the tool line is drawn by the tool itself
|
|
818
|
+
activity: (activity) => {
|
|
819
|
+
showFace(statusOf(activity));
|
|
820
|
+
const busy = activity.working || activity.tools.length > 0;
|
|
821
|
+
acting.current = busy;
|
|
822
|
+
// From the moment the backend picks the turn up until her first word
|
|
823
|
+
holdThinking(busy, activity.speaking);
|
|
824
|
+
if (activity.speaking) voiced.current = Date.now();
|
|
825
|
+
// Sound on the mic alone does not rewind the clock: a noisy room would
|
|
826
|
+
// keep a call open forever. Her words and backend work do, except an
|
|
827
|
+
// update she voices on her own: waiting results must not hold a call open.
|
|
828
|
+
if (
|
|
829
|
+
(activity.speaking && !reading.current.on) ||
|
|
830
|
+
activity.working ||
|
|
831
|
+
activity.tools.length
|
|
832
|
+
)
|
|
833
|
+
stir("agent");
|
|
834
|
+
// An update counts as voiced once her voice has started and stopped
|
|
835
|
+
if (reading.current.on) {
|
|
836
|
+
if (activity.speaking) reading.current.spoke = true;
|
|
837
|
+
else if (reading.current.spoke) doneReading();
|
|
838
|
+
}
|
|
839
|
+
},
|
|
840
|
+
finalized: (close) => {
|
|
841
|
+
finalized.current = close;
|
|
842
|
+
},
|
|
843
|
+
warn: (description) =>
|
|
844
|
+
toast.add({
|
|
845
|
+
type: "warning",
|
|
846
|
+
title: "Call warning",
|
|
847
|
+
description,
|
|
848
|
+
}),
|
|
849
|
+
failed: (description) => {
|
|
850
|
+
toast.add({ type: "error", title: "Call failed", description });
|
|
851
|
+
showFailed(true);
|
|
852
|
+
void hangUp();
|
|
853
|
+
},
|
|
628
854
|
},
|
|
629
|
-
}
|
|
630
|
-
});
|
|
631
|
-
|
|
632
|
-
/**
|
|
633
|
-
* One line at a time. Three jobs landing together used to be three
|
|
634
|
-
* system items and three turns in a row, which the user hears
|
|
635
|
-
* as her talking to herself; the box stays closed while she reads one,
|
|
636
|
-
* and whatever arrived meanwhile goes out merged, as one thing to say.
|
|
637
|
-
*/
|
|
638
|
-
const flush = (queued: Said[]) => {
|
|
639
|
-
if (!queued.length) return;
|
|
640
|
-
// not the user's turn while the model reads this
|
|
641
|
-
readAloud();
|
|
642
|
-
live.say(queued.length === 1 ? queued[0].line : mergedRelay(queued));
|
|
643
|
-
const last = queued.at(-1)?.show;
|
|
644
|
-
if (last) showRelay(last);
|
|
645
|
-
outbox.close();
|
|
646
|
-
};
|
|
647
|
-
|
|
648
|
-
resumeRelay.current = () => {
|
|
649
|
-
const held = outbox.clear();
|
|
650
|
-
outbox.open((one) => flush([one]));
|
|
651
|
-
flush(held);
|
|
652
|
-
};
|
|
653
|
-
|
|
654
|
-
// line open: flush held lines first
|
|
655
|
-
outbox.open((one) => flush([one]));
|
|
855
|
+
});
|
|
656
856
|
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
857
|
+
if (!current()) {
|
|
858
|
+
// Hung up while the line was going up; hangUp already ended the row
|
|
859
|
+
await live.close();
|
|
860
|
+
return;
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
/** An opening that never landed leaves nothing to wait for. */
|
|
864
|
+
const unless = (delivered: boolean) => {
|
|
865
|
+
if (!delivered) doneReading();
|
|
866
|
+
};
|
|
867
|
+
|
|
868
|
+
session.current = live;
|
|
869
|
+
opening.current = false;
|
|
870
|
+
rang.current = calledBack;
|
|
871
|
+
// Context is not gated: held lines first, then each as it comes
|
|
872
|
+
outbox.open((text) => void live.append("thinking", text));
|
|
873
|
+
// The quiet clock starts with the line, so nothing is put to her the moment it opens
|
|
874
|
+
heard.current = Date.now();
|
|
875
|
+
|
|
876
|
+
if (line.opening) {
|
|
877
|
+
// The greeting goes first; open work waits until she has said it
|
|
878
|
+
readAloud();
|
|
879
|
+
void live.append("instructions", line.opening).then(unless);
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
wearFace("listening");
|
|
883
|
+
setSince(Date.now());
|
|
884
|
+
// a rejected chime is not worth a message
|
|
885
|
+
void chime.play().catch(() => {});
|
|
886
|
+
} catch (cause) {
|
|
887
|
+
// Hanging up while the line was going up is not a failure to report
|
|
888
|
+
if (current()) {
|
|
889
|
+
toast.add({
|
|
890
|
+
type: "error",
|
|
891
|
+
title: "Could not start the call",
|
|
892
|
+
description: errorToString(cause),
|
|
893
|
+
});
|
|
894
|
+
showFailed(true);
|
|
895
|
+
}
|
|
896
|
+
if (attempt.current !== mine) return;
|
|
897
|
+
// a call that never opened still has a row; close it
|
|
898
|
+
if (callId.current) void endCallAction(callId.current);
|
|
899
|
+
callId.current = null;
|
|
900
|
+
opening.current = false;
|
|
901
|
+
calling.current = false;
|
|
902
|
+
working.current = null;
|
|
903
|
+
outbox.clear();
|
|
904
|
+
setStatus("idle");
|
|
661
905
|
}
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
calling.current = false;
|
|
680
|
-
outbox.clear();
|
|
681
|
-
// nothing was delivered; forget so the next call resends
|
|
682
|
-
relayed.current.clear();
|
|
683
|
-
setStatus("idle");
|
|
684
|
-
}
|
|
685
|
-
}, [
|
|
686
|
-
status,
|
|
687
|
-
hangUp,
|
|
688
|
-
showTool,
|
|
689
|
-
hideTool,
|
|
690
|
-
showRelay,
|
|
691
|
-
showFace,
|
|
692
|
-
wearFace,
|
|
693
|
-
stir,
|
|
694
|
-
outbox,
|
|
695
|
-
readAloud,
|
|
696
|
-
openMic,
|
|
697
|
-
syncMic,
|
|
698
|
-
]);
|
|
906
|
+
},
|
|
907
|
+
[
|
|
908
|
+
status,
|
|
909
|
+
hangUp,
|
|
910
|
+
showTool,
|
|
911
|
+
hideTool,
|
|
912
|
+
showFace,
|
|
913
|
+
wearFace,
|
|
914
|
+
stir,
|
|
915
|
+
outbox,
|
|
916
|
+
readAloud,
|
|
917
|
+
doneReading,
|
|
918
|
+
holdThinking,
|
|
919
|
+
showFailed,
|
|
920
|
+
leave,
|
|
921
|
+
],
|
|
922
|
+
);
|
|
699
923
|
|
|
700
924
|
/**
|
|
701
|
-
* Call-back: opens a call when a
|
|
702
|
-
* open. Tried once per
|
|
703
|
-
*
|
|
925
|
+
* Call-back: opens a call when a thread is waiting on the user and no line is
|
|
926
|
+
* open. Tried once per thread (`woke`), or a failed open would redial forever;
|
|
927
|
+
* threads present at app start count as already tried.
|
|
704
928
|
*/
|
|
705
929
|
const callBack = useThursdayStore((state) => state.callBack);
|
|
706
930
|
const woke = useRef<Set<string> | null>(null);
|
|
707
931
|
useEffect(() => {
|
|
708
|
-
if (!
|
|
709
|
-
const wants =
|
|
710
|
-
(
|
|
711
|
-
!
|
|
712
|
-
!
|
|
713
|
-
(asksSomething(
|
|
714
|
-
(callBack === "any" &&
|
|
932
|
+
if (!threads) return;
|
|
933
|
+
const wants = threads.filter(
|
|
934
|
+
(thread) =>
|
|
935
|
+
!thread.seen &&
|
|
936
|
+
!thread.ask?.auto &&
|
|
937
|
+
(asksSomething(thread) ||
|
|
938
|
+
(callBack === "any" && thread.status !== "running")),
|
|
715
939
|
);
|
|
716
940
|
|
|
717
941
|
if (!woke.current) {
|
|
718
|
-
woke.current = new Set(wants.map((
|
|
942
|
+
woke.current = new Set(wants.map((thread) => thread.id));
|
|
719
943
|
return;
|
|
720
944
|
}
|
|
721
945
|
|
|
722
|
-
// forget
|
|
723
|
-
const open = new Set(wants.map((
|
|
946
|
+
// forget threads that stopped waiting; a later stop counts as fresh
|
|
947
|
+
const open = new Set(wants.map((thread) => thread.id));
|
|
724
948
|
for (const id of woke.current) {
|
|
725
949
|
if (!open.has(id)) woke.current.delete(id);
|
|
726
950
|
}
|
|
727
951
|
|
|
728
952
|
if (callBack === "off" || status !== "idle") return;
|
|
729
|
-
const fresh = wants.filter((
|
|
953
|
+
const fresh = wants.filter((thread) => !woke.current?.has(thread.id));
|
|
730
954
|
if (!fresh.length) return;
|
|
731
955
|
|
|
732
|
-
for (const
|
|
733
|
-
void call();
|
|
734
|
-
}, [
|
|
956
|
+
for (const thread of wants) woke.current.add(thread.id);
|
|
957
|
+
void call("callBack");
|
|
958
|
+
}, [threads, callBack, status, call]);
|
|
735
959
|
|
|
736
960
|
/** Read by the face once per animation frame, outside React state. */
|
|
737
961
|
const getSpectrum = useCallback(() => tap.current?.read() ?? EMPTY_BANDS, []);
|
|
@@ -748,12 +972,15 @@ export function useThursday() {
|
|
|
748
972
|
enabled: status === "idle" && wake.enabled && !wakeBlocked,
|
|
749
973
|
phrases: [wake.phrase],
|
|
750
974
|
onWake: () => void call(),
|
|
751
|
-
onError: () =>
|
|
975
|
+
onError: (reason) => {
|
|
976
|
+
setWakeBlocked(true);
|
|
977
|
+
toast.add({ type: "error", title: "Wake word off", description: reason });
|
|
978
|
+
},
|
|
752
979
|
});
|
|
753
980
|
|
|
754
981
|
// Hotkey presses `call` like the face tap, so it hangs up during a call; disabled while the line goes up or down
|
|
755
982
|
const hotkey = useThursdayStore((state) => state.hotkey);
|
|
756
|
-
const busy = status === "connecting";
|
|
983
|
+
const busy = status === "connecting" || status === "ending";
|
|
757
984
|
useHotkey({
|
|
758
985
|
enabled: hotkey.enabled && !busy,
|
|
759
986
|
combo: hotkey.enabled && isCombo(hotkey.combo) ? hotkey.combo : null,
|
|
@@ -762,12 +989,14 @@ export function useThursday() {
|
|
|
762
989
|
|
|
763
990
|
return {
|
|
764
991
|
status,
|
|
992
|
+
/** A call just failed to open or dropped; true for a few seconds after. */
|
|
993
|
+
failed,
|
|
765
994
|
messages,
|
|
766
995
|
tool,
|
|
996
|
+
/** When the backend picked the turn up (ms); null when it is not working. */
|
|
997
|
+
thinkingSince,
|
|
767
998
|
/** Seconds until idle hang-up; null outside the warning window. */
|
|
768
999
|
idleLeft,
|
|
769
|
-
/** Mic muted: a tool is running or a relay is being read. */
|
|
770
|
-
micOff,
|
|
771
1000
|
/** When the line opened (ms); null without a call. */
|
|
772
1001
|
since,
|
|
773
1002
|
call,
|
|
@@ -780,11 +1009,7 @@ export function useThursday() {
|
|
|
780
1009
|
};
|
|
781
1010
|
}
|
|
782
1011
|
|
|
783
|
-
const EMPTY_BANDS = new Array<number>(
|
|
784
|
-
|
|
785
|
-
/** Relay dedup key: task id plus its last state change. */
|
|
786
|
-
const relayKey = (task: Task) =>
|
|
787
|
-
`${task.id}@${toDate(task.updatedAt).getTime()}`;
|
|
1012
|
+
const EMPTY_BANDS = new Array<number>(SPECTRUM_BANDS).fill(0);
|
|
788
1013
|
|
|
789
1014
|
/**
|
|
790
1015
|
* Calls opened without a gesture (wake word, call-back) get a suspended
|
|
@@ -816,34 +1041,32 @@ function armAudioUnlock(context: AudioContext) {
|
|
|
816
1041
|
});
|
|
817
1042
|
}
|
|
818
1043
|
|
|
819
|
-
/**
|
|
820
|
-
function statusOf(activity:
|
|
821
|
-
if (activity.
|
|
1044
|
+
/** Voice wins over work; delegate gets its own face. */
|
|
1045
|
+
function statusOf(activity: LiveActivity): LiveStatus {
|
|
1046
|
+
if (activity.speaking) return "speaking";
|
|
1047
|
+
if (activity.working || activity.tools.length) {
|
|
822
1048
|
return activity.tools.includes(TOOL_NAMES.delegate)
|
|
823
1049
|
? "delegating"
|
|
824
1050
|
: "working";
|
|
825
1051
|
}
|
|
826
|
-
return
|
|
1052
|
+
return "listening";
|
|
827
1053
|
}
|
|
828
1054
|
|
|
829
|
-
/**
|
|
830
|
-
|
|
831
|
-
* silence.
|
|
832
|
-
*/
|
|
833
|
-
function persistTurn(
|
|
834
|
-
callId: string,
|
|
835
|
-
turn: CallTurn,
|
|
1055
|
+
/** Fire-and-forget save; after SAVE_FAILURE_LIMIT failures of this kind it stops and says so once. */
|
|
1056
|
+
function persist(
|
|
836
1057
|
saving: { failures: number },
|
|
1058
|
+
save: () => Promise<Result<unknown>>,
|
|
1059
|
+
stopped = "This call is not being saved",
|
|
837
1060
|
) {
|
|
838
1061
|
if (saving.failures >= SAVE_FAILURE_LIMIT) return;
|
|
839
|
-
void
|
|
1062
|
+
void save()
|
|
840
1063
|
.then((result) => unwrapResult(result))
|
|
841
1064
|
.catch((cause) => {
|
|
842
1065
|
saving.failures += 1;
|
|
843
1066
|
if (saving.failures !== SAVE_FAILURE_LIMIT) return;
|
|
844
1067
|
toast.add({
|
|
845
1068
|
type: "warning",
|
|
846
|
-
title:
|
|
1069
|
+
title: stopped,
|
|
847
1070
|
description: errorToString(cause),
|
|
848
1071
|
});
|
|
849
1072
|
});
|
|
@@ -853,58 +1076,110 @@ function persistTurn(
|
|
|
853
1076
|
* Waiting on a real question. A job the app stopped waits in the inbox
|
|
854
1077
|
* instead of ringing.
|
|
855
1078
|
*/
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
1079
|
+
const asksSomething = (thread: Thread) => thread.room.questions.length > 0;
|
|
1080
|
+
|
|
1081
|
+
/** One piece of background work waiting on the user, as the relay clock puts it to her. */
|
|
1082
|
+
type OpenWork = {
|
|
1083
|
+
key: string;
|
|
1084
|
+
/** What it is, so one append holds one kind. */
|
|
1085
|
+
kind: "question" | "ending" | "progress";
|
|
1086
|
+
/** The relay text. */
|
|
1087
|
+
line: string;
|
|
1088
|
+
/** Relay rows it covers, accepted once it lands. */
|
|
1089
|
+
relayIds: number[];
|
|
1090
|
+
/** The same item on the activity line, so the user sees where her words came from. */
|
|
1091
|
+
show: ActivityLine;
|
|
1092
|
+
};
|
|
859
1093
|
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
// is a word to it, and the task tool takes it by that name
|
|
867
|
-
const from = `[${task.bot} → you, about "${task.label}" — not the user speaking; they have not heard this. Anything more on this job goes to the task tool as "${task.label}", never a new job.]`;
|
|
868
|
-
switch (task.status) {
|
|
869
|
-
case "waiting": {
|
|
870
|
-
// a stop is told, not read as a question
|
|
871
|
-
const options = task.ask?.options ?? [];
|
|
872
|
-
const said = task.ask?.question ?? task.outcome ?? "";
|
|
873
|
-
if (isAppStop(task.ask)) {
|
|
874
|
-
return `${from} It stopped before finishing. Where it got to: ${said} Ask whether to keep going; if yes, send "${TASK_CONTINUE}" with the task tool.`;
|
|
875
|
-
}
|
|
876
|
-
const list = options.length ? ` Options: ${options.join(" / ")}.` : "";
|
|
877
|
-
return `${from} It asks: ${said}${list} Answer with the task tool if you can; otherwise ask the user and send back what they say.`;
|
|
878
|
-
}
|
|
879
|
-
case "failed":
|
|
880
|
-
return `${from} It could not finish: ${task.outcome ?? ""}`;
|
|
881
|
-
default:
|
|
882
|
-
return `${from} Done. Its answer: ${task.outcome ?? ""}`;
|
|
883
|
-
}
|
|
884
|
-
}
|
|
1094
|
+
const OPEN_RANK = {
|
|
1095
|
+
question: 0,
|
|
1096
|
+
stopped: 1,
|
|
1097
|
+
done: 2,
|
|
1098
|
+
progress: 3,
|
|
1099
|
+
} as const;
|
|
885
1100
|
|
|
886
1101
|
/**
|
|
887
|
-
*
|
|
888
|
-
*
|
|
889
|
-
*
|
|
1102
|
+
* Everything in the inbox still waiting on the user, most pressing first:
|
|
1103
|
+
* questions, then jobs stopped or finished and not yet seen, then
|
|
1104
|
+
* progress from jobs still running. Sent as commentary; the bracket carries
|
|
1105
|
+
* facts only — who, which thread, where an answer goes — because the backend
|
|
1106
|
+
* reads relays too and routes answers by them.
|
|
890
1107
|
*/
|
|
891
|
-
function
|
|
892
|
-
|
|
1108
|
+
function openWork(threads: Thread[]): OpenWork[] {
|
|
1109
|
+
const items: (OpenWork & { rank: number })[] = [];
|
|
1110
|
+
for (const thread of threads) {
|
|
1111
|
+
const { relays, questions } = thread.room;
|
|
1112
|
+
const changed = toDate(thread.updatedAt).getTime();
|
|
1113
|
+
const bracket = (from: string, kind: string, tail = "") =>
|
|
1114
|
+
`[${from} → Thursday, thread "${thread.label}" (${thread.id}), ${kind}.${tail}]`;
|
|
1115
|
+
const show = (bot: string, line: string): ActivityLine => ({
|
|
1116
|
+
kind: "relay",
|
|
1117
|
+
name: thread.label,
|
|
1118
|
+
line,
|
|
1119
|
+
done: true,
|
|
1120
|
+
bot,
|
|
1121
|
+
});
|
|
893
1122
|
|
|
894
|
-
|
|
895
|
-
|
|
1123
|
+
for (const question of questions) {
|
|
1124
|
+
const options = question.options?.length
|
|
1125
|
+
? ` Options: ${question.options.join(" / ")}.`
|
|
1126
|
+
: "";
|
|
1127
|
+
items.push({
|
|
1128
|
+
rank: OPEN_RANK.question,
|
|
1129
|
+
key: `question:${question.id}`,
|
|
1130
|
+
kind: "question",
|
|
1131
|
+
line: `${bracket(question.bot, "question", ` Its answer goes to thread ${thread.id}, recipient ${question.bot}, replyTo ${question.id}.`)}\n${question.text}${options}`,
|
|
1132
|
+
relayIds: relays
|
|
1133
|
+
.filter((relay) => relay.messageId === question.id)
|
|
1134
|
+
.map((relay) => relay.id),
|
|
1135
|
+
show: show(question.bot, `${question.bot}: question`),
|
|
1136
|
+
});
|
|
1137
|
+
}
|
|
896
1138
|
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
1139
|
+
// Relay rows that belong to no open question
|
|
1140
|
+
const loose = relays.filter(
|
|
1141
|
+
(relay) => !questions.some((question) => question.id === relay.messageId),
|
|
1142
|
+
);
|
|
1143
|
+
// A cancel is the user's own and already seen; nothing about it is news
|
|
1144
|
+
const ended = thread.status === "done";
|
|
1145
|
+
// A stop the app picks back up by itself is not news: it runs again in a moment
|
|
1146
|
+
const stopped =
|
|
1147
|
+
thread.status === "waiting" && isAppStop(thread.ask) && !thread.ask?.auto;
|
|
1148
|
+
|
|
1149
|
+
if (ended || stopped) {
|
|
1150
|
+
if (thread.seen) continue;
|
|
1151
|
+
const kind = stopped ? "stopped" : "done";
|
|
1152
|
+
const said =
|
|
1153
|
+
kind === "stopped"
|
|
1154
|
+
? `It stopped before finishing. Where it got to: ${thread.ask?.question ?? thread.outcome ?? ""}`
|
|
1155
|
+
: `Done. Its answer: ${thread.outcome ?? ""}`;
|
|
1156
|
+
items.push({
|
|
1157
|
+
rank: OPEN_RANK[kind],
|
|
1158
|
+
key: `${kind}:${thread.id}@${changed}`,
|
|
1159
|
+
kind: "ending",
|
|
1160
|
+
line: `${bracket(thread.bot, kind)}\n${said}`,
|
|
1161
|
+
// Its ending says what its progress messages said
|
|
1162
|
+
relayIds: loose.map((relay) => relay.id),
|
|
1163
|
+
show: show(
|
|
1164
|
+
thread.bot,
|
|
1165
|
+
kind === "stopped"
|
|
1166
|
+
? `${thread.bot} stopped`
|
|
1167
|
+
: `Answer from ${thread.bot}`,
|
|
1168
|
+
),
|
|
1169
|
+
});
|
|
1170
|
+
continue;
|
|
1171
|
+
}
|
|
909
1172
|
|
|
910
|
-
|
|
1173
|
+
for (const relay of loose) {
|
|
1174
|
+
items.push({
|
|
1175
|
+
rank: OPEN_RANK.progress,
|
|
1176
|
+
key: `progress:${relay.id}`,
|
|
1177
|
+
kind: "progress",
|
|
1178
|
+
line: `${bracket(relay.bot, relay.kind)}\n${relay.text}`,
|
|
1179
|
+
relayIds: [relay.id],
|
|
1180
|
+
show: show(relay.bot, `${relay.bot}: ${relay.kind}`),
|
|
1181
|
+
});
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
return items.sort((a, b) => a.rank - b.rank);
|
|
1185
|
+
}
|