thursday-agent 0.5.0 → 0.7.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 +5 -4
- 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 +4 -4
- 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 +15 -15
- 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 +9 -10
- 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 -5
- 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 +11 -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]__0-06t3f._.js +8 -0
- package/.next/server/chunks/[root-of-the-server]__02846fb._.js +8 -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]__06l05n_._.js +3 -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]__0btqrct._.js +3 -0
- package/.next/server/chunks/[root-of-the-server]__0c9fgca._.js +5 -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]__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/_1gnjfli._.js +3 -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_0v_7drq._.js → node_modules_consola_dist_chunks_prompt_mjs_0a7-pbw._.js} +1 -1
- package/.next/server/chunks/{node_modules_consola_dist_chunks_prompt_mjs_17k8irj._.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_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]__0-e011f._.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]__0lyi8i2._.js +1 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__0pd8yr7._.js +3 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__16jc196._.js +1 -1
- 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]__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]__1vdmfbe._.js +3 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__1waa3rc._.js +85 -0
- package/.next/server/chunks/ssr/_0-2qsxo._.js +3 -0
- package/.next/server/chunks/ssr/_00rehl_._.js +3 -0
- package/.next/server/chunks/ssr/{_1lz_cq2._.js → _02oc_h5._.js} +76 -97
- package/.next/server/chunks/ssr/_05yghx6._.js +3 -0
- package/.next/server/chunks/ssr/_07nbwap._.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/{_0m2lsd1._.js → _0b6_jv-._.js} +2 -2
- package/.next/server/chunks/ssr/_0evkpto._.js +3 -0
- package/.next/server/chunks/ssr/_0geg212._.js +3 -0
- package/.next/server/chunks/ssr/{_1jdgjv9._.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/_0no7i1o._.js +3 -0
- package/.next/server/chunks/ssr/_0tzg--b._.js +3 -0
- 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/_114ylv7._.js +7 -0
- package/.next/server/chunks/ssr/_123y-ah._.js +3 -0
- package/.next/server/chunks/ssr/_12c-iwh._.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/_167hchk._.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/_1_krfph._.js +3 -0
- package/.next/server/chunks/ssr/{_0rzucul._.js → _1_x5spz._.js} +3 -3
- package/.next/server/chunks/ssr/_1a61fc9._.js +3 -0
- 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/_1rgtxhr._.js +5 -0
- package/.next/server/chunks/ssr/_1slp25y._.js +3 -0
- package/.next/server/chunks/ssr/_1w_t7h7._.js +101 -0
- package/.next/server/chunks/ssr/{_0s99bgb._.js → _1wyfaq_._.js} +9 -3
- package/.next/server/chunks/ssr/_200gt-m._.js +102 -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/features_workspace_00jz2hu._.js +8 -0
- package/.next/server/chunks/ssr/{_1-teee5._.js → lib_logger_ts_0-olsze._.js} +2 -7
- package/.next/server/chunks/ssr/{node_modules_05-n6dz._.js → node_modules_02ai7br._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_1chvq5f._.js → node_modules_02deexj._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_0m383r2._.js → node_modules_07za262._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_07dsgkx._.js → node_modules_0_s9get._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_1a0kmjl._.js → node_modules_0n3c7h0._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_0zeg3x_._.js → node_modules_0od6jwr._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_1y79i83._.js → node_modules_0rdt7pk._.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_04sksn9._.js → node_modules_0w1z7kz._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_1uyk91w._.js → node_modules_0wdupfz._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_0ifp389._.js → node_modules_1720w0a._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_0u9da1h._.js → node_modules_18lsg7d._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_1k6n600._.js → node_modules_1b94fxf._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_20c28js._.js → node_modules_1ecdo1s._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_0fkeku3._.js → node_modules_1fa9ag7._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_1j8j4pt._.js → node_modules_1fv-_si._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_0-dr_o7._.js → node_modules_1hf461g._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_1lz0oxc._.js → node_modules_1m71ta-._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_1t77e4n._.js → node_modules_1n93n-g._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_16mkxuo._.js → node_modules_1n9lqu9._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_1k7z2j6._.js → node_modules_1npnsvd._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_056qgf-._.js → node_modules_1rn2r0n._.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_0bvopy2._.js → node_modules_1vq-mfk._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_0iur59s._.js → node_modules_1vzyw1t._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_1k8j8n0._.js → node_modules_1w359w4._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_1x0ztf4._.js → node_modules_1yajbl6._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_1-y7nni._.js → node_modules_20eqhy8._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_@mermaid-js_parser_dist_chunks_mermaid-parser_core_0bkwd0u._.js → node_modules_@mermaid-js_parser_dist_chunks_mermaid-parser_core_019sbis._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_@mermaid-js_parser_dist_chunks_mermaid-parser_core_0p9a9su._.js → node_modules_@mermaid-js_parser_dist_chunks_mermaid-parser_core_0s6piq3._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_@vercel_oidc_dist_1o553h-._.js → node_modules_@vercel_oidc_dist_05itbb1._.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/07vqbk_9t4em4.js +2 -0
- package/.next/static/chunks/08krja1zx7nqg.js +1 -0
- package/.next/static/chunks/0g4b348l0pve7.js +1 -0
- package/.next/static/chunks/0qcuwdk132jjt.js +1 -0
- package/.next/static/chunks/0vlgzqo9ka90v.js +1 -0
- package/.next/static/chunks/{11r2yp_yh9_qn.js → 0z69gvysmave7.js} +1 -1
- package/.next/static/chunks/0zrupxgci5gqc.js +1 -0
- package/.next/static/chunks/12fvz1w5ows75.js +1 -0
- package/.next/static/chunks/14t03ned286iu.js +1 -0
- package/.next/static/chunks/156xxbkrz5lzu.js +18 -0
- package/.next/static/chunks/1fz14reod4lft.js +1 -0
- package/.next/static/chunks/1g23dbabl-vkr.js +1 -0
- package/.next/static/chunks/1ohslk4568tm3.js +1 -0
- package/.next/static/chunks/1qy_rc3oxfxok.js +1 -0
- package/.next/static/chunks/1sbmwswc9k2-1.js +1 -0
- package/.next/static/chunks/1vqezbhjm3goq.js +1 -0
- package/.next/static/chunks/1zea7qdl-7365.js +1 -0
- package/.next/static/chunks/1zvqumkg7kmo0.css +1 -0
- package/.next/static/chunks/26hpqt3y6bogl.js +12 -0
- package/.next/static/chunks/2_j5mqm6d-z18.js +99 -0
- package/.next/static/chunks/2dt6tu7a22svs.js +1 -0
- package/.next/static/chunks/2h5ls3d86kya6.js +1 -0
- package/.next/static/chunks/2wc6y1ht6dfuy.js +1 -0
- package/.next/static/chunks/{435atsnhs7ksu.js → 2xr0zpll7thyi.js} +2 -2
- package/.next/static/chunks/31jztc-yq30m7.js +1 -0
- package/.next/static/chunks/391af0i8eljzk.js +1 -0
- package/.next/static/chunks/3bfwpww3m3x4u.js +1 -0
- package/.next/static/chunks/{38gweqdo4jpfj.js → 3izg0a3k87q1-.js} +5 -5
- package/.next/static/chunks/3p2s2cjokx1aa.js +5 -0
- package/.next/static/chunks/{408ueu4hfvcg7.js → 41s3fml5oivsa.js} +1 -1
- package/README.md +45 -193
- 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 +40 -35
- package/docs/images/keeps-going.png +0 -0
- package/docs/images/two-minds.png +0 -0
- package/docs/live-calls.md +236 -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 +152 -79
- 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 +169 -0
- package/features/ai/prompts/prompt-helper.ts +92 -32
- package/features/ai/prompts/thursday.prompt.ts +164 -190
- package/features/ai/tools/bot.tool.ts +38 -145
- package/features/ai/tools/call.tool.ts +33 -5
- 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 +11 -6
- package/features/ai/tools/studio.tool.ts +26 -21
- package/features/ai/tools/tool-name.ts +3 -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 +296 -744
- package/features/bot/bot.runner.ts +463 -864
- 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/ascii.const.ts +74 -0
- package/features/thursday/components/ascii-field.tsx +24 -44
- package/features/thursday/components/ascii-orb.tsx +525 -381
- package/features/thursday/components/call-log.tsx +46 -44
- package/features/thursday/components/face.tsx +40 -10
- package/features/thursday/components/side-captions.tsx +273 -0
- package/features/thursday/components/tab-state.tsx +14 -6
- package/features/thursday/components/thursday-mark.tsx +1 -0
- package/features/thursday/components/thursday-setting.tsx +633 -333
- package/features/thursday/components/thursday.tsx +315 -274
- package/features/thursday/components/voice-picker.tsx +255 -0
- package/features/thursday/face.store.ts +5 -0
- package/features/thursday/thursday.action.ts +96 -72
- package/features/thursday/thursday.query.ts +49 -30
- package/features/thursday/thursday.schema.ts +37 -35
- package/features/thursday/thursday.store.ts +14 -7
- package/features/thursday/tool-call.ts +3 -3
- package/features/thursday/tool-line.ts +5 -3
- package/features/thursday/use-thursday.ts +801 -457
- package/features/workspace/file-kind.ts +9 -0
- package/features/workspace/workspace.query.ts +17 -0
- package/features/workspace/workspace.ts +82 -36
- package/hooks/use-hotkey.ts +3 -2
- package/lib/live/live.schema.ts +26 -0
- package/lib/live/live.server.ts +153 -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 +38 -0
- package/scripts/bot-context.test.mts +1408 -0
- package/scripts/live-history.test.mts +115 -0
- package/scripts/live-session.test.mts +938 -0
- package/server.js +1 -1
- package/skills/browser/SKILL.md +11 -1
- 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]__026u77e._.js +0 -5
- package/.next/server/chunks/[root-of-the-server]__02va5wz._.js +0 -3
- package/.next/server/chunks/[root-of-the-server]__050zri4._.js +0 -3
- package/.next/server/chunks/[root-of-the-server]__0bg9710._.js +0 -3
- package/.next/server/chunks/[root-of-the-server]__0ffbg85._.js +0 -3
- package/.next/server/chunks/[root-of-the-server]__0in-d5s._.js +0 -3
- package/.next/server/chunks/[root-of-the-server]__0ncs9uu._.js +0 -3
- package/.next/server/chunks/[root-of-the-server]__0pa9k-f._.js +0 -8
- package/.next/server/chunks/[root-of-the-server]__0qi_m0e._.js +0 -8
- package/.next/server/chunks/[root-of-the-server]__0tq0m_k._.js +0 -3
- package/.next/server/chunks/[root-of-the-server]__0xep6j_._.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]__144l7qt._.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]__1fizn31._.js +0 -3
- package/.next/server/chunks/[root-of-the-server]__1jg8t86._.js +0 -8
- package/.next/server/chunks/[root-of-the-server]__1lldzy9._.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]__1peqvsi._.js +0 -3
- package/.next/server/chunks/[root-of-the-server]__1w8gm5h._.js +0 -10
- package/.next/server/chunks/[root-of-the-server]__1xt39nn._.js +0 -8
- package/.next/server/chunks/[root-of-the-server]__1zb8mfo._.js +0 -8
- package/.next/server/chunks/_0-73of8._.js +0 -55
- package/.next/server/chunks/_0-fjql4._.js +0 -31
- package/.next/server/chunks/_00xh_q0._.js +0 -25
- package/.next/server/chunks/_051qh8u._.js +0 -3
- package/.next/server/chunks/_05sovvj._.js +0 -3
- package/.next/server/chunks/_091bi8d._.js +0 -3
- package/.next/server/chunks/_0fzqt2y._.js +0 -142
- package/.next/server/chunks/_0lr92vd._.js +0 -3
- package/.next/server/chunks/_0mecv0f._.js +0 -31
- package/.next/server/chunks/_0mh8j4m._.js +0 -3
- package/.next/server/chunks/_0sb7tlz._.js +0 -3
- package/.next/server/chunks/_0si7xzn._.js +0 -3
- package/.next/server/chunks/_0u10wmw._.js +0 -55
- 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/_18d7-5p._.js +0 -3
- package/.next/server/chunks/_1c78vw5._.js +0 -237
- 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/_1zkfbaa._.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 -8
- package/.next/server/chunks/features_bot_0zt92oh._.js +0 -3
- package/.next/server/chunks/node_modules_consola_dist_chunks_prompt_mjs_1jidylq._.js +0 -3
- package/.next/server/chunks/ssr/[externals]__0dizgek._.js +0 -3
- package/.next/server/chunks/ssr/[root-of-the-server]__0-nob52._.js +0 -4
- 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]__0s4wb56._.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]__0sd3izg._.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]__1-13tcn._.js +0 -43
- package/.next/server/chunks/ssr/[root-of-the-server]__10-dz5o._.js +0 -61
- package/.next/server/chunks/ssr/[root-of-the-server]__1zidpfl._.js +0 -3
- package/.next/server/chunks/ssr/_001_ms1._.js +0 -33
- package/.next/server/chunks/ssr/_01n3o0m._.js +0 -3
- package/.next/server/chunks/ssr/_04z2c7b._.js +0 -3
- package/.next/server/chunks/ssr/_06tssji._.js +0 -3
- package/.next/server/chunks/ssr/_0fk4c7d._.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/_0mggveb._.js +0 -3
- package/.next/server/chunks/ssr/_0nbpc_8._.js +0 -3
- 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/_11j92tu._.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/_1ebxla1._.js +0 -3
- package/.next/server/chunks/ssr/_1nj4niv._.js +0 -42
- package/.next/server/chunks/ssr/_1ov37if._.js +0 -3
- package/.next/server/chunks/ssr/_1ovmc9s._.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/0n3jle9kbqfr2.js +0 -40
- package/.next/static/chunks/0r5qcctujhxli.js +0 -1
- package/.next/static/chunks/10teww7wt-f6h.js +0 -1
- package/.next/static/chunks/126fj4pga_72k.js +0 -1
- package/.next/static/chunks/159udiexf6dom.js +0 -2
- package/.next/static/chunks/18a96rkhi4_yd.js +0 -1
- package/.next/static/chunks/18gi7o2ibrgh8.js +0 -3
- package/.next/static/chunks/1dkjg87pxk0n9.js +0 -28
- package/.next/static/chunks/1q9z1tohil7xn.js +0 -1
- package/.next/static/chunks/1sjgfets224re.js +0 -1
- package/.next/static/chunks/1wszsr83t01gh.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/2szintyibu0s5.js +0 -1
- package/.next/static/chunks/2t4vhi3m8ahya.js +0 -1
- package/.next/static/chunks/2tigdtz3dpcy8.js +0 -1
- package/.next/static/chunks/3-b6b7entqazs.js +0 -1
- package/.next/static/chunks/3786h5vayt80-.js +0 -28
- package/.next/static/chunks/3cq5g494yuke8.css +0 -1
- package/.next/static/chunks/3dmbhsv2u_8kj.js +0 -1
- package/.next/static/chunks/3hibbjnt85-k6.js +0 -1
- package/.next/static/chunks/3i0a5px-oufgc.js +0 -1
- package/.next/static/chunks/3wexk9qo8zuj3.js +0 -18
- package/.next/static/chunks/3xk7yqgomx7yq.js +0 -1
- package/.next/static/chunks/3xre8bu_pk1p_.js +0 -1
- package/.next/static/chunks/3zv6t3i77xhr_.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 -1079
- package/features/bot/task.store.ts +0 -487
- 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/{G6-gNoyGDEcgAsq-L4PE1 → yyRVBc0xxYQsh3GpbQX9b}/_buildManifest.js +0 -0
- /package/.next/static/{G6-gNoyGDEcgAsq-L4PE1 → yyRVBc0xxYQsh3GpbQX9b}/_clientMiddlewareManifest.js +0 -0
- /package/.next/static/{G6-gNoyGDEcgAsq-L4PE1 → yyRVBc0xxYQsh3GpbQX9b}/_ssgManifest.js +0 -0
|
@@ -1,60 +1,61 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import { useCallback, useEffect, useRef, useState } from "react";
|
|
3
|
+
import { useCallback, useEffect, useMemo, 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_BACK, 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";
|
|
36
|
+
import { FACE_WORD_MAX, undrawable } from "./ascii.const";
|
|
35
37
|
import {
|
|
36
38
|
endCallAction,
|
|
37
39
|
openCallAction,
|
|
40
|
+
saveThoughtAction,
|
|
38
41
|
saveTurnsAction,
|
|
39
42
|
} from "./thursday.action";
|
|
40
43
|
import type {
|
|
44
|
+
CallBack,
|
|
41
45
|
CallMessage,
|
|
42
46
|
CallStatus,
|
|
43
|
-
|
|
47
|
+
FaceWord,
|
|
44
48
|
LiveStatus,
|
|
45
49
|
} from "./thursday.schema";
|
|
46
50
|
import { thursdaySettings, useThursdayStore } from "./thursday.store";
|
|
47
51
|
import { toolBot, toolLine } from "./tool-line";
|
|
48
52
|
|
|
49
53
|
/**
|
|
50
|
-
* One live call, plus the
|
|
51
|
-
* The server
|
|
52
|
-
* the page itself only hangs up.
|
|
54
|
+
* One live call, plus the thread inbox the app watches even with no call open.
|
|
55
|
+
* The server opens the Live session (openCallAction) and runs the tools
|
|
56
|
+
* (tool-call); the page itself only hangs up and puts a word on the face.
|
|
53
57
|
*/
|
|
54
58
|
|
|
55
|
-
/** Time for a tool result to reach the model before the socket closes. */
|
|
56
|
-
const HANG_UP_DELAY_MS = 400;
|
|
57
|
-
|
|
58
59
|
/** Plays when the line opens. */
|
|
59
60
|
const CONNECTED_SOUND = "/sounds/start_voice.ogg";
|
|
60
61
|
|
|
@@ -69,6 +70,13 @@ const TOOL_LINGER_MS = 2500;
|
|
|
69
70
|
/** A relay from a bot carries more to read than a tool line. */
|
|
70
71
|
const RELAY_LINGER_MS = 5000;
|
|
71
72
|
|
|
73
|
+
/**
|
|
74
|
+
* Once the backend's turn is over, how long the activity line keeps saying it is
|
|
75
|
+
* working while it waits for her voice. Her first word is what normally ends it;
|
|
76
|
+
* this is only for a turn that never reaches one.
|
|
77
|
+
*/
|
|
78
|
+
const THINKING_TAIL_MS = 6000;
|
|
79
|
+
|
|
72
80
|
/**
|
|
73
81
|
* The face lags the activity line: only a tool held longer than this switches
|
|
74
82
|
* to the working face. Shorter tools show on the line only.
|
|
@@ -81,127 +89,153 @@ const FACE_TOOL_MS = 800;
|
|
|
81
89
|
*/
|
|
82
90
|
const FACE_SETTLE_MS = 600;
|
|
83
91
|
|
|
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
|
-
/**
|
|
94
|
-
* With no user speech, agent speech, or tool for this long, the model is asked
|
|
95
|
-
* to say goodbye and call end_call.
|
|
96
|
-
*/
|
|
97
|
-
const IDLE_HANG_UP_MS = 60_000;
|
|
98
|
-
|
|
99
|
-
/** If end_call does not follow the goodbye, the page hangs up. */
|
|
100
|
-
const IDLE_GRACE_MS = 15_000;
|
|
101
|
-
|
|
102
|
-
/** The countdown shows on screen inside this window. */
|
|
103
|
-
const IDLE_WARN_MS = 15_000;
|
|
104
|
-
|
|
105
92
|
/**
|
|
106
|
-
*
|
|
107
|
-
*
|
|
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).
|
|
93
|
+
* How long the face says a call failed to open or dropped before it rests: long
|
|
94
|
+
* enough for the orb to spell its ERROR out. The toast carries the reason.
|
|
110
95
|
*/
|
|
111
|
-
const
|
|
96
|
+
const FAILED_FACE_MS = 6000;
|
|
112
97
|
|
|
113
|
-
|
|
114
|
-
|
|
98
|
+
/** Scrollback for the side-by-side layout; the call view shows at most three. */
|
|
99
|
+
const KEEP_MESSAGES = 24;
|
|
115
100
|
|
|
116
|
-
/**
|
|
117
|
-
|
|
118
|
-
* voices (the response can be rejected).
|
|
119
|
-
*/
|
|
120
|
-
const RELAY_MIC_MS = 15_000;
|
|
101
|
+
/** Safety net only; the event stream revalidates threads as they change. */
|
|
102
|
+
const THREAD_POLL_FALLBACK_MS = 30_000;
|
|
121
103
|
|
|
122
|
-
/**
|
|
123
|
-
|
|
124
|
-
line: string;
|
|
125
|
-
/** Shown on the activity line when the line is sent. */
|
|
126
|
-
show?: ToolRun;
|
|
127
|
-
};
|
|
104
|
+
/** Failed saves of one kind before the call stops saving that kind and says so, once. */
|
|
105
|
+
const SAVE_FAILURE_LIMIT = 3;
|
|
128
106
|
|
|
129
107
|
/**
|
|
130
|
-
*
|
|
131
|
-
* human-readable sentence (tool-line), null when none exists; `name` is the
|
|
108
|
+
* What the activity line draws: a tool the model is using, or a relay. `line` is
|
|
109
|
+
* the human-readable sentence (tool-line), null when none exists; `name` is the
|
|
132
110
|
* tool that actually ran.
|
|
133
111
|
*/
|
|
134
|
-
export type
|
|
112
|
+
export type ActivityLine = {
|
|
135
113
|
name: string;
|
|
136
114
|
line: string | null;
|
|
137
115
|
done: boolean;
|
|
138
|
-
/**
|
|
139
|
-
|
|
116
|
+
/** The tool call this is, so one call finishing never ends another's line. */
|
|
117
|
+
id?: string;
|
|
118
|
+
/** A relay from a bot (answer or question) rather than a tool; `name` is the thread label. */
|
|
119
|
+
kind?: "relay";
|
|
140
120
|
/** The bot this names, when it names one: the row draws its face instead of a glyph. */
|
|
141
121
|
bot?: string | null;
|
|
142
122
|
};
|
|
143
123
|
|
|
124
|
+
/** A call-back ringing, as the screen names it. */
|
|
125
|
+
export type Ringing = {
|
|
126
|
+
/** The bot whose work rang: the one asking, else the thread's own. */
|
|
127
|
+
bot: string;
|
|
128
|
+
kind: "question" | "done" | "stopped";
|
|
129
|
+
label: string;
|
|
130
|
+
/** Other threads ringing with it. */
|
|
131
|
+
more: number;
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Why a call ended without the user hanging up: the line was quiet (CALL_IDLE),
|
|
136
|
+
* she hung up (`end_call`), Live closed the session, or the connection dropped.
|
|
137
|
+
*/
|
|
138
|
+
export type CallEnd = "quiet" | "hungUp" | "closed" | "dropped";
|
|
139
|
+
|
|
144
140
|
export function useThursday() {
|
|
145
|
-
const session = useRef<
|
|
141
|
+
const session = useRef<LiveSession | null>(null);
|
|
146
142
|
// created on the first call: `new Audio()` cannot run during SSR
|
|
147
143
|
const tap = useRef<AudioTap | null>(null);
|
|
148
144
|
/** Created inside the call-starting click; hang-up may come from a tool or a disconnect with no gesture. */
|
|
149
145
|
const farewell = useRef<HTMLAudioElement | null>(null);
|
|
150
146
|
|
|
151
147
|
const [status, setStatus] = useState<CallStatus>("idle");
|
|
148
|
+
/** A call just failed to open or dropped; the face says so for FAILED_FACE_MS. */
|
|
149
|
+
const [failed, setFailed] = useState(false);
|
|
150
|
+
const failedFor = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
151
|
+
const showFailed = useCallback((on: boolean) => {
|
|
152
|
+
if (failedFor.current) clearTimeout(failedFor.current);
|
|
153
|
+
failedFor.current = on
|
|
154
|
+
? setTimeout(() => setFailed(false), FAILED_FACE_MS)
|
|
155
|
+
: null;
|
|
156
|
+
setFailed(on);
|
|
157
|
+
}, []);
|
|
152
158
|
const [messages, setMessages] = useState<CallMessage[]>([]);
|
|
153
|
-
const [tool, setTool] = useState<
|
|
159
|
+
const [tool, setTool] = useState<ActivityLine | null>(null);
|
|
154
160
|
/** When the line opened (ms). */
|
|
155
161
|
const [since, setSince] = useState<number | null>(null);
|
|
156
|
-
/** Seconds until idle hang-up; set only inside
|
|
162
|
+
/** Seconds until idle hang-up; set only inside CALL_IDLE.warnMs. */
|
|
157
163
|
const [idleLeft, setIdleLeft] = useState<number | null>(null);
|
|
164
|
+
/** Why the last call ended, when the user did not end it; cleared by the next call. */
|
|
165
|
+
const [ended, setEnded] = useState<CallEnd | null>(null);
|
|
166
|
+
/** When the backend picked this turn up (ms); the activity line counts from it. */
|
|
167
|
+
const [thinkingSince, setThinkingSince] = useState<number | null>(null);
|
|
168
|
+
/** The title of the backend's latest reasoning summary in this stretch of work. */
|
|
169
|
+
const [thinkingTitle, setThinkingTitle] = useState<string | null>(null);
|
|
170
|
+
/** The word `emote` last put on the face. */
|
|
171
|
+
const [faceWord, setFaceWord] = useState<FaceWord | null>(null);
|
|
172
|
+
/** The same value where callbacks can read it, and the timer that ends it. */
|
|
173
|
+
const thinking = useRef<number | null>(null);
|
|
174
|
+
const thinkTail = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
158
175
|
/**
|
|
159
|
-
*
|
|
160
|
-
*
|
|
176
|
+
* A relayed update is out and has not been voiced yet: nothing else goes in
|
|
177
|
+
* until her voice has started and stopped.
|
|
161
178
|
*/
|
|
162
|
-
const
|
|
163
|
-
|
|
164
|
-
const mic = useRef<{
|
|
165
|
-
tool: boolean;
|
|
166
|
-
reading: boolean;
|
|
179
|
+
const reading = useRef<{
|
|
180
|
+
on: boolean;
|
|
167
181
|
spoke: boolean;
|
|
168
182
|
giveUp: ReturnType<typeof setTimeout> | null;
|
|
169
|
-
}>({
|
|
170
|
-
/**
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
const idle = useRef<{
|
|
175
|
-
since: number;
|
|
176
|
-
asked: boolean;
|
|
177
|
-
owed: number | null;
|
|
178
|
-
grace: ReturnType<typeof setTimeout> | null;
|
|
179
|
-
}>({ since: 0, asked: false, owed: null, grace: null });
|
|
183
|
+
}>({ on: false, spoke: false, giveUp: null });
|
|
184
|
+
/** What `session.closed` confirmed for the call being ended; recorded on its row. */
|
|
185
|
+
const finalized = useRef<LiveClose | null>(null);
|
|
186
|
+
/** When the line last had new words from the user, her voice or backend work: the idle clock. */
|
|
187
|
+
const stirred = useRef(0);
|
|
180
188
|
const linger = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
189
|
+
/** Whether the line holds a relay, so only a relay is finished by her voice. */
|
|
190
|
+
const relayOpen = useRef(false);
|
|
181
191
|
// Every server-run tool in this call starts with this signal, so one abort
|
|
182
|
-
// reaches running browser commands. Delegated
|
|
192
|
+
// reaches running browser commands. Delegated threads are server-owned and outlive the call
|
|
183
193
|
const working = useRef<AbortController | null>(null);
|
|
184
194
|
/** Row the turns are saved to; a ref so long-lived callbacks see it. */
|
|
185
195
|
const callId = useRef<string | null>(null);
|
|
186
196
|
/**
|
|
187
|
-
*
|
|
188
|
-
*
|
|
197
|
+
* Open work put to her while this page has been open, by item key (openWork):
|
|
198
|
+
* each goes in once a call. A key carries its job's last change, so a job that
|
|
199
|
+
* asks or ends again is new work.
|
|
200
|
+
*/
|
|
201
|
+
const told = useRef(new Set<string>());
|
|
202
|
+
/**
|
|
203
|
+
* The call-back rings only for threads that changed after this: the last call's
|
|
204
|
+
* end, the last ring, or the page opening.
|
|
189
205
|
*/
|
|
190
|
-
const
|
|
191
|
-
/**
|
|
192
|
-
const
|
|
206
|
+
const ringAfter = useRef(Date.now());
|
|
207
|
+
/** The threads a call-back is ringing for, until it is answered, declined or rings out. */
|
|
208
|
+
const [ringingFor, setRingingFor] = useState<string[]>([]);
|
|
209
|
+
const isRinging = ringingFor.length > 0;
|
|
193
210
|
/**
|
|
194
|
-
*
|
|
195
|
-
*
|
|
196
|
-
*
|
|
211
|
+
* Keys put in this call that her voice has not carried yet: rejected, cut short
|
|
212
|
+
* by a hang-up, or never spoken. Hanging up takes them out of `told`, so the
|
|
213
|
+
* next call puts them in again; what she voiced stays out.
|
|
197
214
|
*/
|
|
198
|
-
const
|
|
199
|
-
|
|
215
|
+
const unvoiced = useRef(new Set<string>());
|
|
216
|
+
/** The keys of the update on the line now, carried once her voice starts and stops on it. */
|
|
217
|
+
const onLine = useRef<string[]>([]);
|
|
218
|
+
/** When her voice was last heard, for letting a goodbye finish (CALL_END). */
|
|
219
|
+
const voiced = useRef(0);
|
|
220
|
+
/** The hang-up waiting on her goodbye once the backend called end_call. */
|
|
221
|
+
const leaving = useRef<ReturnType<typeof setInterval> | null>(null);
|
|
222
|
+
/** When either side's words were last transcribed: the quiet clock for relays. */
|
|
223
|
+
const heard = useRef(0);
|
|
200
224
|
/**
|
|
201
|
-
*
|
|
202
|
-
*
|
|
225
|
+
* The call-back placed this call and its first relay has not gone in yet: that one
|
|
226
|
+
* goes in once she has voiced the opening rather than waiting for a quiet line.
|
|
203
227
|
*/
|
|
204
|
-
const
|
|
228
|
+
const rang = useRef(false);
|
|
229
|
+
/** Backend work or a call tool is running, as the last activity said. */
|
|
230
|
+
const acting = useRef(false);
|
|
231
|
+
/** The inbox as last fetched, read by the relay clock. */
|
|
232
|
+
const latest = useRef<Thread[] | undefined>(undefined);
|
|
233
|
+
/**
|
|
234
|
+
* Context she need not say (screen acts), including what arrives before the
|
|
235
|
+
* line opens. Held until the session exists.
|
|
236
|
+
*/
|
|
237
|
+
const outboxRef = useRef<Outbox<string> | null>(null);
|
|
238
|
+
const outbox = (outboxRef.current ??= createOutbox<string>());
|
|
205
239
|
/** Wants a call, including while connecting. */
|
|
206
240
|
const calling = useRef(false);
|
|
207
241
|
/**
|
|
@@ -210,10 +244,18 @@ export function useThursday() {
|
|
|
210
244
|
* in `call` or the hang-up press is swallowed too.
|
|
211
245
|
*/
|
|
212
246
|
const opening = useRef(false);
|
|
247
|
+
const ending = useRef(false);
|
|
248
|
+
/**
|
|
249
|
+
* Which attempt owns the line. A hang-up during startup moves it on, so an
|
|
250
|
+
* attempt still connecting cannot come up after a newer call has been placed.
|
|
251
|
+
*/
|
|
252
|
+
const attempt = useRef(0);
|
|
213
253
|
|
|
214
|
-
const showTool = useCallback((call:
|
|
254
|
+
const showTool = useCallback((call: LiveToolCall) => {
|
|
215
255
|
if (linger.current) clearTimeout(linger.current);
|
|
256
|
+
relayOpen.current = false;
|
|
216
257
|
setTool({
|
|
258
|
+
id: call.id,
|
|
217
259
|
name: call.name,
|
|
218
260
|
line: toolLine(call.name, call.arguments),
|
|
219
261
|
bot: toolBot(call.name, call.arguments),
|
|
@@ -221,21 +263,22 @@ export function useThursday() {
|
|
|
221
263
|
});
|
|
222
264
|
}, []);
|
|
223
265
|
|
|
224
|
-
/**
|
|
225
|
-
|
|
266
|
+
/**
|
|
267
|
+
* A relay takes the tool line and runs there like a tool does: while it goes in,
|
|
268
|
+
* and while she voices it. Her voice finishing it is what stops the motion
|
|
269
|
+
* (doneReading); after that it stays long enough to read.
|
|
270
|
+
*/
|
|
271
|
+
const showRelay = useCallback((run: ActivityLine) => {
|
|
226
272
|
if (linger.current) clearTimeout(linger.current);
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
() => setTool((open) => (open?.kind === "relay" ? null : open)),
|
|
230
|
-
RELAY_LINGER_MS,
|
|
231
|
-
);
|
|
273
|
+
relayOpen.current = true;
|
|
274
|
+
setTool({ ...run, done: false });
|
|
232
275
|
}, []);
|
|
233
276
|
|
|
234
277
|
/** Tool finished; lingers, then clears. */
|
|
235
|
-
const hideTool = useCallback((
|
|
278
|
+
const hideTool = useCallback((id: string) => {
|
|
236
279
|
setTool((open) =>
|
|
237
280
|
// a second tool already took the line; leave it
|
|
238
|
-
open?.
|
|
281
|
+
open?.id === id ? { ...open, done: true } : open,
|
|
239
282
|
);
|
|
240
283
|
if (linger.current) clearTimeout(linger.current);
|
|
241
284
|
linger.current = setTimeout(
|
|
@@ -244,36 +287,141 @@ export function useThursday() {
|
|
|
244
287
|
);
|
|
245
288
|
}, []);
|
|
246
289
|
|
|
247
|
-
/** The
|
|
248
|
-
const
|
|
249
|
-
|
|
290
|
+
/** The relayed update was voiced, or waited on long enough: the next can go in. */
|
|
291
|
+
const doneReading = useCallback(() => {
|
|
292
|
+
if (reading.current.giveUp) clearTimeout(reading.current.giveUp);
|
|
293
|
+
// Her voice started and stopped on it: carried
|
|
294
|
+
if (reading.current.spoke) {
|
|
295
|
+
for (const key of onLine.current) unvoiced.current.delete(key);
|
|
296
|
+
}
|
|
297
|
+
onLine.current = [];
|
|
298
|
+
reading.current = { on: false, spoke: false, giveUp: null };
|
|
299
|
+
if (!relayOpen.current) return;
|
|
300
|
+
relayOpen.current = false;
|
|
301
|
+
setTool((open) =>
|
|
302
|
+
open?.kind === "relay" ? { ...open, done: true } : open,
|
|
303
|
+
);
|
|
304
|
+
if (linger.current) clearTimeout(linger.current);
|
|
305
|
+
linger.current = setTimeout(
|
|
306
|
+
() => setTool((open) => (open?.kind === "relay" ? null : open)),
|
|
307
|
+
RELAY_LINGER_MS,
|
|
308
|
+
);
|
|
309
|
+
}, []);
|
|
310
|
+
|
|
311
|
+
/** An update is on the wire: nothing else goes in until she has voiced it. */
|
|
312
|
+
const readAloud = useCallback(() => {
|
|
313
|
+
if (reading.current.giveUp) clearTimeout(reading.current.giveUp);
|
|
314
|
+
reading.current = {
|
|
315
|
+
on: true,
|
|
316
|
+
spoke: false,
|
|
317
|
+
giveUp: setTimeout(doneReading, CALL_RELAY.readMs),
|
|
318
|
+
};
|
|
319
|
+
}, [doneReading]);
|
|
320
|
+
|
|
321
|
+
const setThinking = useCallback((at: number | null) => {
|
|
322
|
+
thinking.current = at;
|
|
323
|
+
setThinkingSince(at);
|
|
324
|
+
// a stretch of work that starts or ends has not said what it is about yet
|
|
325
|
+
setThinkingTitle(null);
|
|
250
326
|
}, []);
|
|
251
327
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
328
|
+
/**
|
|
329
|
+
* What the activity line says about the backend. It starts when the backend takes the
|
|
330
|
+
* turn and ends on her first word, not on the response: a turn that calls two
|
|
331
|
+
* tools settles for a moment between them, and the wait before she speaks is
|
|
332
|
+
* still the same stretch of work. Both would otherwise read as the call stalling.
|
|
333
|
+
*/
|
|
334
|
+
const holdThinking = useCallback(
|
|
335
|
+
(busy: boolean, speaking: boolean) => {
|
|
336
|
+
const dropTail = () => {
|
|
337
|
+
if (thinkTail.current) clearTimeout(thinkTail.current);
|
|
338
|
+
thinkTail.current = null;
|
|
339
|
+
};
|
|
340
|
+
// Her voice always wins: it is the answer the work was for
|
|
341
|
+
if (speaking) {
|
|
342
|
+
dropTail();
|
|
343
|
+
if (thinking.current !== null) setThinking(null);
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
if (busy) {
|
|
347
|
+
dropTail();
|
|
348
|
+
if (thinking.current === null) setThinking(Date.now());
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
if (thinking.current !== null && !thinkTail.current) {
|
|
352
|
+
thinkTail.current = setTimeout(() => {
|
|
353
|
+
thinkTail.current = null;
|
|
354
|
+
setThinking(null);
|
|
355
|
+
}, THINKING_TAIL_MS);
|
|
356
|
+
}
|
|
357
|
+
},
|
|
358
|
+
[setThinking],
|
|
359
|
+
);
|
|
259
360
|
|
|
260
|
-
/**
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
361
|
+
/**
|
|
362
|
+
* Puts to her what background work still waits on the user, once neither
|
|
363
|
+
* side's words have been transcribed for CALL_RELAY.quietMs and nothing is
|
|
364
|
+
* running: questions not answered, jobs ended or stopped and not yet seen,
|
|
365
|
+
* and progress from jobs still running. Live never speaks unprompted, so
|
|
366
|
+
* nothing reaches the user unless this puts it in. Handling an item — an
|
|
367
|
+
* answer, `thread` `seen`, a click on screen — takes it off the list. Each
|
|
368
|
+
* item goes in once a call, and once her voice has carried it, not on later
|
|
369
|
+
* calls either (`told`, `unvoiced`). One append holds one kind, so an ending
|
|
370
|
+
* is never lost among questions. Nothing goes in once the call is ending.
|
|
371
|
+
*/
|
|
372
|
+
const relayOpenWork = useCallback(() => {
|
|
373
|
+
const live = session.current;
|
|
374
|
+
const threads = latest.current;
|
|
375
|
+
if (!live || !threads || reading.current.on || acting.current) return;
|
|
376
|
+
if (leaving.current) return;
|
|
377
|
+
// On a call-back, why she called goes in once the opening is voiced: it is the
|
|
378
|
+
// first thing they ask, and a quiet line would come too late
|
|
379
|
+
const rung = rang.current;
|
|
380
|
+
rang.current = false;
|
|
381
|
+
if (!rung && Date.now() - heard.current < CALL_RELAY.quietMs) return;
|
|
382
|
+
const open = openWork(threads).filter(
|
|
383
|
+
(item) => !told.current.has(item.key),
|
|
384
|
+
);
|
|
385
|
+
const first = open[0];
|
|
386
|
+
if (!first) return;
|
|
387
|
+
const due = open
|
|
388
|
+
.filter((item) => item.kind === first.kind)
|
|
389
|
+
.slice(0, CALL_RELAY.perTurn);
|
|
390
|
+
const lines = due.map((item) => item.line);
|
|
391
|
+
|
|
392
|
+
// Told as it goes out, even if Live refuses it: a refusal is tried again on the next
|
|
393
|
+
// call (unvoiced), never in a loop on this one
|
|
394
|
+
for (const item of due) {
|
|
395
|
+
told.current.add(item.key);
|
|
396
|
+
unvoiced.current.add(item.key);
|
|
397
|
+
}
|
|
398
|
+
readAloud();
|
|
399
|
+
onLine.current = due.map((item) => item.key);
|
|
400
|
+
// On the line before it goes out, so it runs there for the whole wait
|
|
401
|
+
showRelay((due.at(-1) ?? first).show);
|
|
402
|
+
void live
|
|
403
|
+
.append(
|
|
404
|
+
"commentary",
|
|
405
|
+
lines.length === 1
|
|
406
|
+
? lines.join("")
|
|
407
|
+
: `[${lines.length} updates.]\n\n${lines.join("\n\n")}`,
|
|
408
|
+
)
|
|
409
|
+
.then((delivered) => {
|
|
410
|
+
if (!delivered) {
|
|
411
|
+
doneReading();
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
const ids = due.flatMap((item) => item.relayIds);
|
|
415
|
+
if (ids.length) return acceptThreadRelaysAction(ids).then(unwrapResult);
|
|
416
|
+
})
|
|
417
|
+
.catch((cause) =>
|
|
418
|
+
toast.add({
|
|
419
|
+
type: "error",
|
|
420
|
+
title: "Could not record relay delivery",
|
|
421
|
+
description: errorToString(cause),
|
|
422
|
+
}),
|
|
423
|
+
);
|
|
424
|
+
}, [readAloud, doneReading, showRelay]);
|
|
277
425
|
|
|
278
426
|
/**
|
|
279
427
|
* One timer; when it fires it wears the last wanted face, so a tool shorter
|
|
@@ -335,24 +483,22 @@ export function useThursday() {
|
|
|
335
483
|
[restFace, wearFace],
|
|
336
484
|
);
|
|
337
485
|
|
|
338
|
-
const onCall =
|
|
486
|
+
const onCall =
|
|
487
|
+
status !== "idle" && status !== "connecting" && status !== "ending";
|
|
339
488
|
|
|
340
489
|
// Inbox, with or without a call. Revalidated on server events; polling is the safety net
|
|
341
|
-
const { data:
|
|
342
|
-
refreshInterval:
|
|
490
|
+
const { data: threads } = useServerRoute<Thread[]>(queryKey.threads, {
|
|
491
|
+
refreshInterval: THREAD_POLL_FALLBACK_MS,
|
|
343
492
|
});
|
|
344
493
|
|
|
345
494
|
// Server event stream (app/api/events): signals revalidate their key
|
|
346
|
-
const boot = useRef<string | null>(null);
|
|
347
495
|
useAppEvent({
|
|
348
|
-
hello: (
|
|
496
|
+
hello: () => {
|
|
349
497
|
// 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;
|
|
498
|
+
// changed while the stream was down raised a signal anybody heard
|
|
353
499
|
void revalidateAll();
|
|
354
500
|
},
|
|
355
|
-
|
|
501
|
+
threads: () => void revalidate(queryKey.threads),
|
|
356
502
|
memory: () => void revalidate(queryKey.memory),
|
|
357
503
|
mcp: () => {
|
|
358
504
|
void revalidate(queryKey.mcp);
|
|
@@ -368,97 +514,93 @@ export function useThursday() {
|
|
|
368
514
|
// faces come from the bot list
|
|
369
515
|
const { data: bots } = useServerRoute<Bot[]>(queryKey.bot);
|
|
370
516
|
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
|
|
517
|
+
if (threads) botThreads.sync(threads, bots);
|
|
518
|
+
}, [threads, bots]);
|
|
519
|
+
|
|
381
520
|
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]);
|
|
521
|
+
latest.current = threads;
|
|
522
|
+
}, [threads]);
|
|
398
523
|
|
|
399
|
-
// The user acted on a
|
|
524
|
+
// The user acted on a thread on screen; she may have just read that question and must not ask again
|
|
400
525
|
useEffect(
|
|
401
526
|
() =>
|
|
402
527
|
screenActs.subscribe((act) => {
|
|
403
528
|
if (!calling.current) return;
|
|
404
|
-
outbox.send(
|
|
405
|
-
|
|
406
|
-
act.
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
});
|
|
529
|
+
outbox.send(
|
|
530
|
+
act.kind === "answered"
|
|
531
|
+
? `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.`
|
|
532
|
+
: `The user stopped thread "${act.label}" on screen. It is no longer running.`,
|
|
533
|
+
);
|
|
410
534
|
}),
|
|
411
535
|
[outbox],
|
|
412
536
|
);
|
|
413
537
|
|
|
414
|
-
/** Closes the session and resets state. Turns were saved during the call. */
|
|
415
|
-
const hangUp = useCallback(
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
538
|
+
/** Closes the session and resets state. Turns were saved during the call. `why` is null when the user hung up. */
|
|
539
|
+
const hangUp = useCallback(
|
|
540
|
+
async (why: CallEnd | null = null) => {
|
|
541
|
+
if (ending.current) return;
|
|
542
|
+
ending.current = true;
|
|
543
|
+
if (leaving.current) clearInterval(leaving.current);
|
|
544
|
+
leaving.current = null;
|
|
545
|
+
attempt.current += 1;
|
|
546
|
+
const live = session.current;
|
|
547
|
+
const call = callId.current;
|
|
548
|
+
|
|
549
|
+
session.current = null;
|
|
550
|
+
callId.current = null;
|
|
551
|
+
calling.current = false;
|
|
552
|
+
opening.current = false;
|
|
553
|
+
rang.current = false;
|
|
554
|
+
// What she did not voice goes in again next call; unsent context goes with the session
|
|
555
|
+
for (const key of unvoiced.current) told.current.delete(key);
|
|
556
|
+
unvoiced.current.clear();
|
|
557
|
+
outbox.close();
|
|
558
|
+
outbox.clear();
|
|
559
|
+
working.current?.abort();
|
|
560
|
+
working.current = null;
|
|
561
|
+
doneReading();
|
|
562
|
+
if (linger.current) clearTimeout(linger.current);
|
|
563
|
+
// a pending face timer must not fire after the call
|
|
564
|
+
restFace();
|
|
565
|
+
// What came up on this call was this call's to tell; the call-back rings only for what comes after
|
|
566
|
+
ringAfter.current = Date.now();
|
|
567
|
+
setIdleLeft(null);
|
|
568
|
+
setSince(null);
|
|
569
|
+
setStatus(live ? "ending" : "idle");
|
|
570
|
+
setMessages([]);
|
|
571
|
+
setTool(null);
|
|
572
|
+
setThinking(null);
|
|
573
|
+
if (thinkTail.current) clearTimeout(thinkTail.current);
|
|
574
|
+
thinkTail.current = null;
|
|
575
|
+
relayOpen.current = false;
|
|
576
|
+
// the room is not cleared: threads outlive the call
|
|
577
|
+
|
|
578
|
+
// Waits (bounded) for session.closed, which saves the last turns and says
|
|
579
|
+
// what was billed; the row is ended with that, after the saves it queued
|
|
580
|
+
await live?.close();
|
|
581
|
+
const close = finalized.current;
|
|
582
|
+
finalized.current = null;
|
|
583
|
+
ending.current = false;
|
|
584
|
+
setStatus("idle");
|
|
585
|
+
if (live) setEnded(why);
|
|
586
|
+
if (call) void endCallAction(call, close);
|
|
587
|
+
// only when there was a line to close
|
|
588
|
+
if (live && farewell.current) {
|
|
589
|
+
farewell.current.currentTime = 0;
|
|
590
|
+
void farewell.current.play().catch(() => {});
|
|
591
|
+
}
|
|
592
|
+
},
|
|
593
|
+
[outbox, restFace, doneReading, setThinking],
|
|
594
|
+
);
|
|
454
595
|
|
|
455
596
|
// Unmount during a call must release the mic and stop tools
|
|
456
597
|
useEffect(() => {
|
|
457
598
|
return () => {
|
|
458
|
-
|
|
599
|
+
attempt.current += 1;
|
|
600
|
+
void session.current?.close();
|
|
459
601
|
session.current = null;
|
|
460
602
|
// Closing only the session would leave the row live (thursday.query
|
|
461
|
-
// isAnyCallLive) and finished
|
|
603
|
+
// isAnyCallLive) and finished threads unannounced (bot.runner); end the row too
|
|
462
604
|
const call = callId.current;
|
|
463
605
|
callId.current = null;
|
|
464
606
|
calling.current = false;
|
|
@@ -469,217 +611,278 @@ export function useThursday() {
|
|
|
469
611
|
// clear timers so nothing sets state on an unmounted tree
|
|
470
612
|
if (linger.current) clearTimeout(linger.current);
|
|
471
613
|
if (faceIn.current) clearTimeout(faceIn.current);
|
|
472
|
-
if (
|
|
473
|
-
if (
|
|
614
|
+
if (reading.current.giveUp) clearTimeout(reading.current.giveUp);
|
|
615
|
+
if (thinkTail.current) clearTimeout(thinkTail.current);
|
|
616
|
+
if (failedFor.current) clearTimeout(failedFor.current);
|
|
617
|
+
if (leaving.current) clearInterval(leaving.current);
|
|
474
618
|
};
|
|
475
619
|
}, []);
|
|
476
620
|
|
|
477
621
|
/**
|
|
478
|
-
*
|
|
479
|
-
*
|
|
622
|
+
* The backend ended the call: hang up once her goodbye is over. She says it while
|
|
623
|
+
* the backend works, so it may be done already, still going, or yet to start;
|
|
624
|
+
* CALL_END bounds each case. The tool's own result always gets `quietMs` to go out.
|
|
480
625
|
*/
|
|
481
|
-
const
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
// Idle clock: shows the countdown, then asks for a goodbye and grants a grace period
|
|
626
|
+
const leave = useCallback(() => {
|
|
627
|
+
if (leaving.current) return;
|
|
628
|
+
const asked = Date.now();
|
|
629
|
+
leaving.current = setInterval(() => {
|
|
630
|
+
const now = Date.now();
|
|
631
|
+
// A goodbye heard just before the backend ended the call counts as said
|
|
632
|
+
const said = voiced.current > asked - CALL_END.unsaidMs;
|
|
633
|
+
const over = said
|
|
634
|
+
? now - Math.max(voiced.current, asked) >= CALL_END.quietMs
|
|
635
|
+
: now - asked >= CALL_END.unsaidMs;
|
|
636
|
+
if (over || now - asked >= CALL_END.maxMs) void hangUp("hungUp");
|
|
637
|
+
}, 100);
|
|
638
|
+
}, [hangUp]);
|
|
639
|
+
|
|
640
|
+
// Idle clock: shows the countdown, then hangs up. The page ends a quiet line itself;
|
|
641
|
+
// a goodbye would leave the ending to the model
|
|
499
642
|
useEffect(() => {
|
|
500
643
|
if (!onCall) return;
|
|
501
|
-
|
|
644
|
+
stirred.current = Date.now();
|
|
502
645
|
const tick = setInterval(() => {
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
const owed = idle.current.owed;
|
|
506
|
-
if (owed !== null && Date.now() - owed >= AGENT_SILENT_MS) {
|
|
507
|
-
// Nothing to say goodbye with; the model is what would have said it.
|
|
508
|
-
toast.add({
|
|
509
|
-
type: "error",
|
|
510
|
-
title: "Call ended",
|
|
511
|
-
description: "The model stopped answering.",
|
|
512
|
-
});
|
|
513
|
-
void hangUp();
|
|
514
|
-
return;
|
|
515
|
-
}
|
|
516
|
-
const left = IDLE_HANG_UP_MS - (Date.now() - idle.current.since);
|
|
646
|
+
if (!session.current) return;
|
|
647
|
+
const left = CALL_IDLE.hangUpMs - (Date.now() - stirred.current);
|
|
517
648
|
setIdleLeft(
|
|
518
|
-
left <=
|
|
519
|
-
? Math.max(0, Math.ceil(left / 1000))
|
|
520
|
-
: null,
|
|
649
|
+
left <= CALL_IDLE.warnMs ? Math.max(0, Math.ceil(left / 1000)) : null,
|
|
521
650
|
);
|
|
522
|
-
if (left
|
|
523
|
-
|
|
524
|
-
idle.current.asked = true;
|
|
525
|
-
live.say(IDLE_LINE);
|
|
526
|
-
idle.current.grace = setTimeout(() => void hangUp(), IDLE_GRACE_MS);
|
|
651
|
+
if (left <= 0) void hangUp("quiet");
|
|
527
652
|
}, 1000);
|
|
528
653
|
return () => clearInterval(tick);
|
|
529
|
-
}, [onCall,
|
|
654
|
+
}, [onCall, hangUp]);
|
|
655
|
+
|
|
656
|
+
// The relay clock: open work goes in only while the line is quiet
|
|
657
|
+
useEffect(() => {
|
|
658
|
+
if (!onCall) return;
|
|
659
|
+
const tick = setInterval(relayOpenWork, 1000);
|
|
660
|
+
return () => clearInterval(tick);
|
|
661
|
+
}, [onCall, relayOpenWork]);
|
|
530
662
|
|
|
531
663
|
const call = useCallback(async () => {
|
|
664
|
+
// Every way in answers a ringing call-back: the face, the wake word, the hotkey
|
|
665
|
+
const calledBack = isRinging;
|
|
532
666
|
// Guard with a ref, not `status`: three entry points (face, wake word,
|
|
533
667
|
// hotkey) can fire in one frame and both see a stale "idle", opening two sessions
|
|
534
|
-
if (opening.current) return;
|
|
668
|
+
if (opening.current || ending.current) return;
|
|
535
669
|
// with a line open this press hangs up; `calling` covers the gap before re-render
|
|
536
670
|
if (calling.current || status !== "idle") return hangUp();
|
|
671
|
+
setRingingFor([]);
|
|
537
672
|
|
|
538
673
|
setStatus("connecting");
|
|
539
|
-
|
|
674
|
+
setEnded(null);
|
|
675
|
+
showFailed(false);
|
|
676
|
+
// from here on this is a call; the outbox holds updates until the session exists
|
|
540
677
|
opening.current = true;
|
|
541
678
|
calling.current = true;
|
|
542
|
-
|
|
679
|
+
finalized.current = null;
|
|
543
680
|
outbox.clear();
|
|
681
|
+
attempt.current += 1;
|
|
682
|
+
const mine = attempt.current;
|
|
683
|
+
const current = () => calling.current && attempt.current === mine;
|
|
544
684
|
try {
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
const stop = new AbortController();
|
|
549
|
-
working.current = stop;
|
|
550
|
-
const saving = { failures: 0 };
|
|
551
|
-
|
|
685
|
+
// Inside the gesture, before anything awaits: an AudioContext created later
|
|
686
|
+
// starts suspended. Calls from the wake word or a call-back have no gesture;
|
|
687
|
+
// armAudioUnlock handles those
|
|
552
688
|
tap.current ??= createAudioTap();
|
|
553
|
-
// Inside the gesture: an AudioContext created elsewhere starts suspended.
|
|
554
|
-
// Calls from the wake word or a call-back have no gesture; armAudioUnlock handles those
|
|
555
689
|
armAudioUnlock(tap.current.open().context);
|
|
556
690
|
const chime = new Audio(CONNECTED_SOUND);
|
|
557
691
|
farewell.current ??= new Audio(HUNG_UP_SOUND);
|
|
558
692
|
|
|
559
|
-
|
|
560
|
-
const
|
|
561
|
-
|
|
562
|
-
const
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
693
|
+
const settings = thursdaySettings();
|
|
694
|
+
const stop = new AbortController();
|
|
695
|
+
working.current = stop;
|
|
696
|
+
const saving = { failures: 0 };
|
|
697
|
+
const thinkingSaves = { failures: 0 };
|
|
698
|
+
/** Filled in by the handshake, read by callbacks that only run after it. */
|
|
699
|
+
const line = { callId: "", opening: null as string | null };
|
|
700
|
+
|
|
701
|
+
// one turn per id; the session reports display groups one at a time
|
|
702
|
+
const turns = new Map<string, CallMessage & { seq: number }>();
|
|
703
|
+
|
|
704
|
+
const live = await openLiveSession({
|
|
705
|
+
initialize: async (sdp) => {
|
|
706
|
+
const handshake = unwrapResult(
|
|
707
|
+
await openCallAction(settings, sdp, calledBack),
|
|
708
|
+
);
|
|
709
|
+
if (!current()) {
|
|
710
|
+
void endCallAction(handshake.callId);
|
|
711
|
+
throw new Error("The call closed during startup.");
|
|
712
|
+
}
|
|
713
|
+
callId.current = handshake.callId;
|
|
714
|
+
line.callId = handshake.callId;
|
|
715
|
+
line.opening = handshake.opening;
|
|
716
|
+
return handshake.sdp;
|
|
717
|
+
},
|
|
566
718
|
audio: tap.current,
|
|
567
719
|
on: {
|
|
568
|
-
// the
|
|
720
|
+
// the backend calls tools; the page forwards them to the server
|
|
569
721
|
runTool: async (call) => {
|
|
570
|
-
// end_call
|
|
571
|
-
//
|
|
722
|
+
// end_call and emote are the page's own tools. The line goes down once
|
|
723
|
+
// her goodbye is over (leave), so this reply reaches the model first
|
|
572
724
|
if (call.name === TOOL_NAMES.end_call) {
|
|
573
|
-
|
|
725
|
+
leave();
|
|
574
726
|
return "Ending the call.";
|
|
575
727
|
}
|
|
728
|
+
// the face draws the word; nothing runs anywhere else
|
|
729
|
+
if (call.name === TOOL_NAMES.emote) {
|
|
730
|
+
const { word, reply } = readFaceWord(call.arguments);
|
|
731
|
+
if (word) setFaceWord({ text: word, at: Date.now() });
|
|
732
|
+
return reply;
|
|
733
|
+
}
|
|
576
734
|
showTool(call);
|
|
577
735
|
try {
|
|
578
|
-
return await runRemoteTool(
|
|
736
|
+
return await runRemoteTool(line.callId, call, stop.signal);
|
|
579
737
|
} finally {
|
|
580
|
-
hideTool(call.
|
|
738
|
+
hideTool(call.id);
|
|
581
739
|
}
|
|
582
740
|
},
|
|
741
|
+
reasoning: (part) => {
|
|
742
|
+
// A summary part opens with its title in bold; the activity line says what the work is about
|
|
743
|
+
const title = /^\s*\*\*(.+?)\*\*/.exec(part.text)?.[1]?.trim();
|
|
744
|
+
if (title) setThinkingTitle(title);
|
|
745
|
+
persist(
|
|
746
|
+
thinkingSaves,
|
|
747
|
+
() =>
|
|
748
|
+
saveThoughtAction(line.callId, {
|
|
749
|
+
...part,
|
|
750
|
+
seq: Math.max(0, Math.round(part.seq)),
|
|
751
|
+
}),
|
|
752
|
+
"The backend's thinking is not being saved",
|
|
753
|
+
);
|
|
754
|
+
},
|
|
583
755
|
turn: (turn) => {
|
|
584
|
-
|
|
585
|
-
//
|
|
586
|
-
|
|
587
|
-
|
|
756
|
+
// New words, not a checkpoint of the same ones. A cough or a sigh is not words:
|
|
757
|
+
// it neither holds the line open nor holds an update back
|
|
758
|
+
const words = turn.role === "tool" ? "" : spokenWords(turn.text);
|
|
759
|
+
const fresh =
|
|
760
|
+
Boolean(words) &&
|
|
761
|
+
words !== spokenWords(turns.get(turn.id)?.text ?? "");
|
|
762
|
+
// New words from either side restart the relay's quiet clock
|
|
763
|
+
if (fresh) heard.current = Date.now();
|
|
764
|
+
// Her voice and backend work rewind the idle clock from activity, where an
|
|
765
|
+
// update she reads out is told apart; transcripts lag the audio and cannot
|
|
766
|
+
if (fresh && turn.role === "user") stirred.current = Date.now();
|
|
767
|
+
// Tool turns are saved but not shown. Hanging up clears the screen
|
|
768
|
+
// before `close()` checkpoints open groups, so only the live call draws.
|
|
769
|
+
if (turn.role !== "tool" && callId.current === line.callId) {
|
|
770
|
+
turns.set(turn.id, {
|
|
771
|
+
seq: turn.seq,
|
|
588
772
|
id: turn.id,
|
|
589
773
|
role: turn.role,
|
|
590
774
|
text: turn.text,
|
|
591
|
-
};
|
|
592
|
-
|
|
593
|
-
|
|
775
|
+
});
|
|
776
|
+
setMessages(
|
|
777
|
+
[...turns.values()]
|
|
778
|
+
.sort((a, b) => a.seq - b.seq)
|
|
779
|
+
.slice(-KEEP_MESSAGES),
|
|
780
|
+
);
|
|
594
781
|
}
|
|
595
782
|
if (turn.done) {
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
783
|
+
persist(saving, () =>
|
|
784
|
+
saveTurnsAction(line.callId, [
|
|
785
|
+
{
|
|
786
|
+
id: turn.id,
|
|
787
|
+
role: turn.role,
|
|
788
|
+
tool: turn.tool,
|
|
789
|
+
text: turn.text,
|
|
790
|
+
// Live orders by audio milliseconds; the row keeps a whole number
|
|
791
|
+
seq: Math.max(0, Math.round(turn.seq)),
|
|
792
|
+
fragments: turn.fragments ?? null,
|
|
793
|
+
},
|
|
794
|
+
]),
|
|
601
795
|
);
|
|
602
796
|
}
|
|
603
797
|
},
|
|
604
798
|
// session facts drive the face through showFace; the tool line is drawn by the tool itself
|
|
605
799
|
activity: (activity) => {
|
|
606
800
|
showFace(statusOf(activity));
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
//
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
801
|
+
const busy = activity.working || activity.tools.length > 0;
|
|
802
|
+
acting.current = busy;
|
|
803
|
+
// From the moment the backend picks the turn up until her first word
|
|
804
|
+
holdThinking(busy, activity.speaking);
|
|
805
|
+
if (activity.speaking) voiced.current = Date.now();
|
|
806
|
+
// Sound on the mic alone does not rewind the clock: a noisy room would
|
|
807
|
+
// keep a call open forever. Her words and backend work do, except an
|
|
808
|
+
// update she voices on her own: waiting results must not hold a call open.
|
|
809
|
+
if (
|
|
810
|
+
(activity.speaking && !reading.current.on) ||
|
|
811
|
+
activity.working ||
|
|
812
|
+
activity.tools.length
|
|
813
|
+
)
|
|
814
|
+
stirred.current = Date.now();
|
|
815
|
+
// An update counts as voiced once her voice has started and stopped
|
|
816
|
+
if (reading.current.on) {
|
|
817
|
+
if (activity.speaking) reading.current.spoke = true;
|
|
818
|
+
else if (reading.current.spoke) doneReading();
|
|
619
819
|
}
|
|
620
|
-
|
|
621
|
-
|
|
820
|
+
},
|
|
821
|
+
finalized: (close) => {
|
|
822
|
+
finalized.current = close;
|
|
622
823
|
},
|
|
623
824
|
warn: (description) =>
|
|
624
|
-
toast.add({
|
|
825
|
+
toast.add({
|
|
826
|
+
type: "warning",
|
|
827
|
+
title: "Call warning",
|
|
828
|
+
description,
|
|
829
|
+
}),
|
|
625
830
|
failed: (description) => {
|
|
626
831
|
toast.add({ type: "error", title: "Call failed", description });
|
|
627
|
-
|
|
832
|
+
showFailed(true);
|
|
833
|
+
// Live says session.closed (finalized) when it closes the call itself
|
|
834
|
+
void hangUp(finalized.current ? "closed" : "dropped");
|
|
628
835
|
},
|
|
629
836
|
},
|
|
630
837
|
});
|
|
631
838
|
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
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
|
-
};
|
|
839
|
+
if (!current()) {
|
|
840
|
+
// Hung up while the line was going up; hangUp already ended the row
|
|
841
|
+
await live.close();
|
|
842
|
+
return;
|
|
843
|
+
}
|
|
647
844
|
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
flush(held);
|
|
845
|
+
/** An opening that never landed leaves nothing to wait for. */
|
|
846
|
+
const unless = (delivered: boolean) => {
|
|
847
|
+
if (!delivered) doneReading();
|
|
652
848
|
};
|
|
653
849
|
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
//
|
|
658
|
-
|
|
850
|
+
session.current = live;
|
|
851
|
+
opening.current = false;
|
|
852
|
+
rang.current = calledBack;
|
|
853
|
+
// Context is not gated: held lines first, then each as it comes
|
|
854
|
+
outbox.open((text) => void live.append("thinking", text));
|
|
855
|
+
// The quiet clock starts with the line, so nothing is put to her the moment it opens
|
|
856
|
+
heard.current = Date.now();
|
|
857
|
+
|
|
858
|
+
if (line.opening) {
|
|
859
|
+
// The greeting goes first; open work waits until she has said it
|
|
659
860
|
readAloud();
|
|
660
|
-
live.
|
|
861
|
+
void live.append("instructions", line.opening).then(unless);
|
|
661
862
|
}
|
|
662
863
|
|
|
663
|
-
session.current = live;
|
|
664
|
-
opening.current = false;
|
|
665
864
|
wearFace("listening");
|
|
666
865
|
setSince(Date.now());
|
|
667
866
|
// a rejected chime is not worth a message
|
|
668
867
|
void chime.play().catch(() => {});
|
|
669
868
|
} catch (cause) {
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
869
|
+
// Hanging up while the line was going up is not a failure to report
|
|
870
|
+
if (current()) {
|
|
871
|
+
toast.add({
|
|
872
|
+
type: "error",
|
|
873
|
+
title: "Could not start the call",
|
|
874
|
+
description: errorToString(cause),
|
|
875
|
+
});
|
|
876
|
+
showFailed(true);
|
|
877
|
+
}
|
|
878
|
+
if (attempt.current !== mine) return;
|
|
675
879
|
// a call that never opened still has a row; close it
|
|
676
880
|
if (callId.current) void endCallAction(callId.current);
|
|
677
881
|
callId.current = null;
|
|
678
882
|
opening.current = false;
|
|
679
883
|
calling.current = false;
|
|
884
|
+
working.current = null;
|
|
680
885
|
outbox.clear();
|
|
681
|
-
// nothing was delivered; forget so the next call resends
|
|
682
|
-
relayed.current.clear();
|
|
683
886
|
setStatus("idle");
|
|
684
887
|
}
|
|
685
888
|
}, [
|
|
@@ -687,51 +890,87 @@ export function useThursday() {
|
|
|
687
890
|
hangUp,
|
|
688
891
|
showTool,
|
|
689
892
|
hideTool,
|
|
690
|
-
showRelay,
|
|
691
893
|
showFace,
|
|
692
894
|
wearFace,
|
|
693
|
-
stir,
|
|
694
895
|
outbox,
|
|
695
896
|
readAloud,
|
|
696
|
-
|
|
697
|
-
|
|
897
|
+
doneReading,
|
|
898
|
+
holdThinking,
|
|
899
|
+
showFailed,
|
|
900
|
+
leave,
|
|
901
|
+
isRinging,
|
|
698
902
|
]);
|
|
699
903
|
|
|
700
904
|
/**
|
|
701
|
-
* Call-back:
|
|
702
|
-
*
|
|
703
|
-
*
|
|
905
|
+
* Call-back: rings when a thread is waiting on the user and no line is open,
|
|
906
|
+
* only for threads that changed after `ringAfter`. What came up during a call
|
|
907
|
+
* was that call's to tell, and the next call tells what it left unsaid. Ringing
|
|
908
|
+
* never opens the line; answering does (`call`). A thread handled meanwhile, on
|
|
909
|
+
* screen or by the setting going off, stops ringing for itself.
|
|
704
910
|
*/
|
|
705
911
|
const callBack = useThursdayStore((state) => state.callBack);
|
|
706
|
-
const woke = useRef<Set<string> | null>(null);
|
|
707
912
|
useEffect(() => {
|
|
708
|
-
if (!
|
|
709
|
-
const
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
(asksSomething(task) ||
|
|
714
|
-
(callBack === "any" && task.status !== "running")),
|
|
913
|
+
if (!threads) return;
|
|
914
|
+
const wanted = new Set(
|
|
915
|
+
threads
|
|
916
|
+
.filter((thread) => ringsFor(thread, callBack))
|
|
917
|
+
.map((thread) => thread.id),
|
|
715
918
|
);
|
|
919
|
+
setRingingFor((ids) =>
|
|
920
|
+
ids.every((id) => wanted.has(id))
|
|
921
|
+
? ids
|
|
922
|
+
: ids.filter((id) => wanted.has(id)),
|
|
923
|
+
);
|
|
924
|
+
if (status !== "idle") return;
|
|
925
|
+
const after = ringAfter.current;
|
|
926
|
+
const fresh = threads.filter(
|
|
927
|
+
(thread) =>
|
|
928
|
+
wanted.has(thread.id) && toDate(thread.updatedAt).getTime() > after,
|
|
929
|
+
);
|
|
930
|
+
if (!fresh.length) return;
|
|
716
931
|
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
const open = new Set(wants.map((task) => task.id));
|
|
724
|
-
for (const id of woke.current) {
|
|
725
|
-
if (!open.has(id)) woke.current.delete(id);
|
|
726
|
-
}
|
|
932
|
+
ringAfter.current = Date.now();
|
|
933
|
+
setRingingFor((ids) => [
|
|
934
|
+
...ids,
|
|
935
|
+
...fresh.map((thread) => thread.id).filter((id) => !ids.includes(id)),
|
|
936
|
+
]);
|
|
937
|
+
}, [threads, callBack, status]);
|
|
727
938
|
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
939
|
+
// While ringing: it stops by itself after CALL_BACK.ringMs, and Esc declines.
|
|
940
|
+
// A thread added to a ring already going does not restart the clock
|
|
941
|
+
useEffect(() => {
|
|
942
|
+
if (!isRinging) return;
|
|
943
|
+
const out = setTimeout(() => setRingingFor([]), CALL_BACK.ringMs);
|
|
944
|
+
const onKeyDown = (event: KeyboardEvent) => {
|
|
945
|
+
if (event.key === "Escape" && !event.defaultPrevented) setRingingFor([]);
|
|
946
|
+
};
|
|
947
|
+
window.addEventListener("keydown", onKeyDown);
|
|
948
|
+
return () => {
|
|
949
|
+
clearTimeout(out);
|
|
950
|
+
window.removeEventListener("keydown", onKeyDown);
|
|
951
|
+
};
|
|
952
|
+
}, [isRinging]);
|
|
731
953
|
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
954
|
+
const decline = useCallback(() => setRingingFor([]), []);
|
|
955
|
+
/** What the ringing screen names: the first thread that rang, and how many rang with it. */
|
|
956
|
+
const ringing = useMemo((): Ringing | null => {
|
|
957
|
+
const rung = ringingFor.flatMap(
|
|
958
|
+
(id) => threads?.find((thread) => thread.id === id) ?? [],
|
|
959
|
+
);
|
|
960
|
+
const first = rung[0];
|
|
961
|
+
if (!first) return null;
|
|
962
|
+
const question = first.room.questions[0];
|
|
963
|
+
return {
|
|
964
|
+
bot: question?.bot ?? first.bot,
|
|
965
|
+
kind: question
|
|
966
|
+
? "question"
|
|
967
|
+
: first.status === "done"
|
|
968
|
+
? "done"
|
|
969
|
+
: "stopped",
|
|
970
|
+
label: first.label,
|
|
971
|
+
more: rung.length - 1,
|
|
972
|
+
};
|
|
973
|
+
}, [ringingFor, threads]);
|
|
735
974
|
|
|
736
975
|
/** Read by the face once per animation frame, outside React state. */
|
|
737
976
|
const getSpectrum = useCallback(() => tap.current?.read() ?? EMPTY_BANDS, []);
|
|
@@ -748,12 +987,15 @@ export function useThursday() {
|
|
|
748
987
|
enabled: status === "idle" && wake.enabled && !wakeBlocked,
|
|
749
988
|
phrases: [wake.phrase],
|
|
750
989
|
onWake: () => void call(),
|
|
751
|
-
onError: () =>
|
|
990
|
+
onError: (reason) => {
|
|
991
|
+
setWakeBlocked(true);
|
|
992
|
+
toast.add({ type: "error", title: "Wake word off", description: reason });
|
|
993
|
+
},
|
|
752
994
|
});
|
|
753
995
|
|
|
754
996
|
// Hotkey presses `call` like the face tap, so it hangs up during a call; disabled while the line goes up or down
|
|
755
997
|
const hotkey = useThursdayStore((state) => state.hotkey);
|
|
756
|
-
const busy = status === "connecting";
|
|
998
|
+
const busy = status === "connecting" || status === "ending";
|
|
757
999
|
useHotkey({
|
|
758
1000
|
enabled: hotkey.enabled && !busy,
|
|
759
1001
|
combo: hotkey.enabled && isCombo(hotkey.combo) ? hotkey.combo : null,
|
|
@@ -762,15 +1004,27 @@ export function useThursday() {
|
|
|
762
1004
|
|
|
763
1005
|
return {
|
|
764
1006
|
status,
|
|
1007
|
+
/** A call just failed to open or dropped; true for a few seconds after. */
|
|
1008
|
+
failed,
|
|
765
1009
|
messages,
|
|
766
1010
|
tool,
|
|
1011
|
+
/** Why the last call ended, when the user did not end it; null during a call. */
|
|
1012
|
+
ended,
|
|
1013
|
+
/** When the backend picked the turn up (ms); null when it is not working. */
|
|
1014
|
+
thinkingSince,
|
|
1015
|
+
/** What the backend's latest reasoning summary says it is doing; null before one arrives. */
|
|
1016
|
+
thinkingTitle,
|
|
1017
|
+
/** The word `emote` last put on the face; null before one. */
|
|
1018
|
+
faceWord,
|
|
767
1019
|
/** Seconds until idle hang-up; null outside the warning window. */
|
|
768
1020
|
idleLeft,
|
|
769
|
-
/** Mic muted: a tool is running or a relay is being read. */
|
|
770
|
-
micOff,
|
|
771
1021
|
/** When the line opened (ms); null without a call. */
|
|
772
1022
|
since,
|
|
773
1023
|
call,
|
|
1024
|
+
/** A call-back ringing; null when none is. Any way of placing a call answers it. */
|
|
1025
|
+
ringing,
|
|
1026
|
+
/** Stops the ringing without answering. */
|
|
1027
|
+
decline,
|
|
774
1028
|
getSpectrum,
|
|
775
1029
|
getMicSpectrum,
|
|
776
1030
|
/** null when the tap is the only entry point. */
|
|
@@ -780,11 +1034,37 @@ export function useThursday() {
|
|
|
780
1034
|
};
|
|
781
1035
|
}
|
|
782
1036
|
|
|
783
|
-
const EMPTY_BANDS = new Array<number>(
|
|
1037
|
+
const EMPTY_BANDS = new Array<number>(SPECTRUM_BANDS).fill(0);
|
|
784
1038
|
|
|
785
|
-
/**
|
|
786
|
-
|
|
787
|
-
|
|
1039
|
+
/** What `emote` asked the face to show, and the line the model reads back; `word` is null when nothing is shown. */
|
|
1040
|
+
function readFaceWord(args: string): { word: string | null; reply: string } {
|
|
1041
|
+
let text = "";
|
|
1042
|
+
try {
|
|
1043
|
+
const parsed: unknown = JSON.parse(args);
|
|
1044
|
+
if (parsed && typeof parsed === "object" && "text" in parsed) {
|
|
1045
|
+
text = String(parsed.text ?? "").trim();
|
|
1046
|
+
}
|
|
1047
|
+
} catch {
|
|
1048
|
+
text = "";
|
|
1049
|
+
}
|
|
1050
|
+
const length = Array.from(text).length;
|
|
1051
|
+
if (!length)
|
|
1052
|
+
return { word: null, reply: "Nothing was shown: text is empty." };
|
|
1053
|
+
if (length > FACE_WORD_MAX) {
|
|
1054
|
+
return {
|
|
1055
|
+
word: null,
|
|
1056
|
+
reply: `Too long: ${length} characters, ${FACE_WORD_MAX} at most. Nothing was shown.`,
|
|
1057
|
+
};
|
|
1058
|
+
}
|
|
1059
|
+
const bad = undrawable(text);
|
|
1060
|
+
if (bad) {
|
|
1061
|
+
return {
|
|
1062
|
+
word: null,
|
|
1063
|
+
reply: `Cannot draw "${bad}"; use only the characters \`text\` lists. Nothing was shown.`,
|
|
1064
|
+
};
|
|
1065
|
+
}
|
|
1066
|
+
return { word: text.toUpperCase(), reply: "Shown." };
|
|
1067
|
+
}
|
|
788
1068
|
|
|
789
1069
|
/**
|
|
790
1070
|
* Calls opened without a gesture (wake word, call-back) get a suspended
|
|
@@ -816,95 +1096,159 @@ function armAudioUnlock(context: AudioContext) {
|
|
|
816
1096
|
});
|
|
817
1097
|
}
|
|
818
1098
|
|
|
819
|
-
/**
|
|
820
|
-
function statusOf(activity:
|
|
821
|
-
if (activity.
|
|
1099
|
+
/** Voice wins over work; delegate gets its own face. */
|
|
1100
|
+
function statusOf(activity: LiveActivity): LiveStatus {
|
|
1101
|
+
if (activity.speaking) return "speaking";
|
|
1102
|
+
if (activity.working || activity.tools.length) {
|
|
822
1103
|
return activity.tools.includes(TOOL_NAMES.delegate)
|
|
823
1104
|
? "delegating"
|
|
824
1105
|
: "working";
|
|
825
1106
|
}
|
|
826
|
-
return
|
|
1107
|
+
return "listening";
|
|
827
1108
|
}
|
|
828
1109
|
|
|
829
1110
|
/**
|
|
830
|
-
*
|
|
831
|
-
*
|
|
1111
|
+
* The words in a transcript, without the sounds Live writes in brackets ("[clear throat]",
|
|
1112
|
+
* "[sigh]"); a bracket may close in the next group ("] okay"). Empty when nothing was said.
|
|
832
1113
|
*/
|
|
833
|
-
function
|
|
834
|
-
|
|
835
|
-
|
|
1114
|
+
function spokenWords(text: string) {
|
|
1115
|
+
const words = text.replace(/\[[^\]]*\]?|\]/g, "").trim();
|
|
1116
|
+
return /[\p{L}\p{N}]/u.test(words) ? words : "";
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
/** Fire-and-forget save; after SAVE_FAILURE_LIMIT failures of this kind it stops and says so once. */
|
|
1120
|
+
function persist(
|
|
836
1121
|
saving: { failures: number },
|
|
1122
|
+
save: () => Promise<Result<unknown>>,
|
|
1123
|
+
stopped = "This call is not being saved",
|
|
837
1124
|
) {
|
|
838
1125
|
if (saving.failures >= SAVE_FAILURE_LIMIT) return;
|
|
839
|
-
void
|
|
1126
|
+
void save()
|
|
840
1127
|
.then((result) => unwrapResult(result))
|
|
841
1128
|
.catch((cause) => {
|
|
842
1129
|
saving.failures += 1;
|
|
843
1130
|
if (saving.failures !== SAVE_FAILURE_LIMIT) return;
|
|
844
1131
|
toast.add({
|
|
845
1132
|
type: "warning",
|
|
846
|
-
title:
|
|
1133
|
+
title: stopped,
|
|
847
1134
|
description: errorToString(cause),
|
|
848
1135
|
});
|
|
849
1136
|
});
|
|
850
1137
|
}
|
|
851
1138
|
|
|
852
1139
|
/**
|
|
853
|
-
*
|
|
854
|
-
*
|
|
1140
|
+
* Whether a thread is one the call-back rings for: a real question, or with
|
|
1141
|
+
* "any" every ending. A job the app stops and picks up by itself never rings.
|
|
855
1142
|
*/
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
1143
|
+
const ringsFor = (thread: Thread, callBack: CallBack) =>
|
|
1144
|
+
callBack !== "off" &&
|
|
1145
|
+
!thread.seen &&
|
|
1146
|
+
!thread.ask?.auto &&
|
|
1147
|
+
(thread.room.questions.length > 0 ||
|
|
1148
|
+
(callBack === "any" && thread.status !== "running"));
|
|
1149
|
+
|
|
1150
|
+
/** One piece of background work waiting on the user, as the relay clock puts it to her. */
|
|
1151
|
+
type OpenWork = {
|
|
1152
|
+
key: string;
|
|
1153
|
+
/** What it is, so one append holds one kind. */
|
|
1154
|
+
kind: "question" | "ending" | "progress";
|
|
1155
|
+
/** The relay text. */
|
|
1156
|
+
line: string;
|
|
1157
|
+
/** Relay rows it covers, accepted once it lands. */
|
|
1158
|
+
relayIds: number[];
|
|
1159
|
+
/** The same item on the activity line, so the user sees where her words came from. */
|
|
1160
|
+
show: ActivityLine;
|
|
1161
|
+
};
|
|
859
1162
|
|
|
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
|
-
}
|
|
1163
|
+
const OPEN_RANK = {
|
|
1164
|
+
question: 0,
|
|
1165
|
+
stopped: 1,
|
|
1166
|
+
done: 2,
|
|
1167
|
+
progress: 3,
|
|
1168
|
+
} as const;
|
|
885
1169
|
|
|
886
1170
|
/**
|
|
887
|
-
*
|
|
888
|
-
*
|
|
889
|
-
*
|
|
1171
|
+
* Everything in the inbox still waiting on the user, most pressing first:
|
|
1172
|
+
* questions, then jobs stopped or finished and not yet seen, then
|
|
1173
|
+
* progress from jobs still running. Sent as commentary; the bracket carries
|
|
1174
|
+
* facts only — who, which thread, where an answer goes — because the backend
|
|
1175
|
+
* reads relays too and routes answers by them.
|
|
890
1176
|
*/
|
|
891
|
-
function
|
|
892
|
-
|
|
1177
|
+
function openWork(threads: Thread[]): OpenWork[] {
|
|
1178
|
+
const items: (OpenWork & { rank: number })[] = [];
|
|
1179
|
+
for (const thread of threads) {
|
|
1180
|
+
const { relays, questions } = thread.room;
|
|
1181
|
+
const changed = toDate(thread.updatedAt).getTime();
|
|
1182
|
+
const bracket = (from: string, kind: string, tail = "") =>
|
|
1183
|
+
`[${from} → Thursday, thread "${thread.label}" (${thread.id}), ${kind}.${tail}]`;
|
|
1184
|
+
const show = (bot: string, line: string): ActivityLine => ({
|
|
1185
|
+
kind: "relay",
|
|
1186
|
+
name: thread.label,
|
|
1187
|
+
line,
|
|
1188
|
+
done: true,
|
|
1189
|
+
bot,
|
|
1190
|
+
});
|
|
893
1191
|
|
|
894
|
-
|
|
895
|
-
|
|
1192
|
+
for (const question of questions) {
|
|
1193
|
+
const options = question.options?.length
|
|
1194
|
+
? ` Options: ${question.options.join(" / ")}.`
|
|
1195
|
+
: "";
|
|
1196
|
+
items.push({
|
|
1197
|
+
rank: OPEN_RANK.question,
|
|
1198
|
+
key: `question:${question.id}`,
|
|
1199
|
+
kind: "question",
|
|
1200
|
+
line: `${bracket(question.bot, "question", ` Its answer goes to thread ${thread.id}, recipient ${question.bot}, replyTo ${question.id}.`)}\n${question.text}${options}`,
|
|
1201
|
+
relayIds: relays
|
|
1202
|
+
.filter((relay) => relay.messageId === question.id)
|
|
1203
|
+
.map((relay) => relay.id),
|
|
1204
|
+
show: show(question.bot, `${question.bot}: question`),
|
|
1205
|
+
});
|
|
1206
|
+
}
|
|
896
1207
|
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
1208
|
+
// Relay rows that belong to no open question
|
|
1209
|
+
const loose = relays.filter(
|
|
1210
|
+
(relay) => !questions.some((question) => question.id === relay.messageId),
|
|
1211
|
+
);
|
|
1212
|
+
// A cancel is the user's own and already seen; nothing about it is news
|
|
1213
|
+
const ended = thread.status === "done";
|
|
1214
|
+
// A stop the app picks back up by itself is not news: it runs again in a moment
|
|
1215
|
+
const stopped =
|
|
1216
|
+
thread.status === "waiting" && isAppStop(thread.ask) && !thread.ask?.auto;
|
|
1217
|
+
|
|
1218
|
+
if (ended || stopped) {
|
|
1219
|
+
if (thread.seen) continue;
|
|
1220
|
+
const kind = stopped ? "stopped" : "done";
|
|
1221
|
+
const said =
|
|
1222
|
+
kind === "stopped"
|
|
1223
|
+
? `It stopped before finishing. Where it got to: ${thread.ask?.question ?? thread.outcome ?? ""}`
|
|
1224
|
+
: `Done. Its answer: ${thread.outcome ?? ""}`;
|
|
1225
|
+
items.push({
|
|
1226
|
+
rank: OPEN_RANK[kind],
|
|
1227
|
+
key: `${kind}:${thread.id}@${changed}`,
|
|
1228
|
+
kind: "ending",
|
|
1229
|
+
line: `${bracket(thread.bot, kind)}\n${said}`,
|
|
1230
|
+
// Its ending says what its progress messages said
|
|
1231
|
+
relayIds: loose.map((relay) => relay.id),
|
|
1232
|
+
show: show(
|
|
1233
|
+
thread.bot,
|
|
1234
|
+
kind === "stopped"
|
|
1235
|
+
? `${thread.bot} stopped`
|
|
1236
|
+
: `Answer from ${thread.bot}`,
|
|
1237
|
+
),
|
|
1238
|
+
});
|
|
1239
|
+
continue;
|
|
1240
|
+
}
|
|
909
1241
|
|
|
910
|
-
|
|
1242
|
+
for (const relay of loose) {
|
|
1243
|
+
items.push({
|
|
1244
|
+
rank: OPEN_RANK.progress,
|
|
1245
|
+
key: `progress:${relay.id}`,
|
|
1246
|
+
kind: "progress",
|
|
1247
|
+
line: `${bracket(relay.bot, relay.kind)}\n${relay.text}`,
|
|
1248
|
+
relayIds: [relay.id],
|
|
1249
|
+
show: show(relay.bot, `${relay.bot}: ${relay.kind}`),
|
|
1250
|
+
});
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
return items.sort((a, b) => a.rank - b.rank);
|
|
1254
|
+
}
|