thursday-agent 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.next/BUILD_ID +1 -1
- package/.next/app-path-routes-manifest.json +3 -2
- package/.next/build-manifest.json +3 -3
- package/.next/prerender-manifest.json +3 -3
- package/.next/required-server-files.json +8 -1
- package/.next/routes-manifest.json +18 -6
- package/.next/server/app/_global-error/page.js.nft.json +1 -1
- package/.next/server/app/_global-error.html +1 -1
- package/.next/server/app/_global-error.rsc +1 -1
- package/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/server/app/_not-found.html +1 -1
- package/.next/server/app/_not-found.rsc +13 -13
- package/.next/server/app/_not-found.segments/_full.segment.rsc +13 -13
- package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +12 -12
- package/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/api/artifact/route.js +10 -6
- package/.next/server/app/api/artifact/route.js.nft.json +1 -1
- package/.next/server/app/api/bot/memory/files/route.js +6 -5
- package/.next/server/app/api/bot/memory/files/route.js.nft.json +1 -1
- package/.next/server/app/api/bot/memory/route.js +8 -8
- package/.next/server/app/api/bot/memory/route.js.nft.json +1 -1
- package/.next/server/app/api/bot/route.js +8 -8
- package/.next/server/app/api/bot/route.js.nft.json +1 -1
- package/.next/server/app/api/bot/thread/[id]/route/app-paths-manifest.json +3 -0
- package/.next/server/app/api/bot/thread/[id]/route.js +15 -0
- package/.next/server/app/api/bot/thread/[id]/route.js.nft.json +1 -0
- package/.next/server/app/api/bot/thread/[id]/route_client-reference-manifest.js +3 -0
- package/.next/server/app/api/bot/thread/route/app-paths-manifest.json +3 -0
- package/.next/server/app/api/bot/thread/route.js +15 -0
- package/.next/server/app/api/bot/thread/route.js.nft.json +1 -0
- package/.next/server/app/api/bot/thread/route_client-reference-manifest.js +3 -0
- package/.next/server/app/api/config/route.js +7 -7
- package/.next/server/app/api/config/route.js.nft.json +1 -1
- package/.next/server/app/api/events/route.js +3 -3
- package/.next/server/app/api/events/route.js.nft.json +1 -1
- package/.next/server/app/api/file/[...path]/route.js +5 -5
- package/.next/server/app/api/file/[...path]/route.js.nft.json +1 -1
- package/.next/server/app/api/llm-model/catalog/route.js +8 -8
- package/.next/server/app/api/llm-model/catalog/route.js.nft.json +1 -1
- package/.next/server/app/api/llm-model/credits/route.js +8 -8
- package/.next/server/app/api/llm-model/credits/route.js.nft.json +1 -1
- package/.next/server/app/api/llm-model/route.js +8 -8
- package/.next/server/app/api/llm-model/route.js.nft.json +1 -1
- package/.next/server/app/api/llm-model/subscription/route.js +8 -8
- package/.next/server/app/api/llm-model/subscription/route.js.nft.json +1 -1
- package/.next/server/app/api/mcp/[name]/route.js +6 -6
- package/.next/server/app/api/mcp/[name]/route.js.nft.json +1 -1
- package/.next/server/app/api/mcp/oauth/callback/route.js +4 -4
- package/.next/server/app/api/mcp/oauth/callback/route.js.nft.json +1 -1
- package/.next/server/app/api/mcp/route.js +6 -6
- package/.next/server/app/api/mcp/route.js.nft.json +1 -1
- package/.next/server/app/api/mcp/tools/route.js +6 -6
- package/.next/server/app/api/mcp/tools/route.js.nft.json +1 -1
- package/.next/server/app/api/memory/edit/route.js +13 -13
- package/.next/server/app/api/memory/edit/route.js.nft.json +1 -1
- package/.next/server/app/api/memory/route.js +7 -6
- package/.next/server/app/api/memory/route.js.nft.json +1 -1
- package/.next/server/app/api/skills/[source]/[name]/route.js +6 -6
- package/.next/server/app/api/skills/[source]/[name]/route.js.nft.json +1 -1
- package/.next/server/app/api/skills/route.js +6 -6
- package/.next/server/app/api/skills/route.js.nft.json +1 -1
- package/.next/server/app/api/thursday/call/route.js +10 -11
- package/.next/server/app/api/thursday/call/route.js.nft.json +1 -1
- package/.next/server/app/api/thursday/skills/route.js +10 -11
- package/.next/server/app/api/thursday/skills/route.js.nft.json +1 -1
- package/.next/server/app/api/thursday/tool-call/route.js +15 -15
- package/.next/server/app/api/thursday/tool-call/route.js.nft.json +1 -1
- package/.next/server/app/api/workspace/missing/route/app-paths-manifest.json +3 -0
- package/.next/server/app/api/workspace/missing/route/build-manifest.json +12 -0
- package/.next/server/app/api/workspace/missing/route/server-reference-manifest.json +4 -0
- package/.next/server/app/api/workspace/missing/route.js +12 -0
- package/.next/server/app/api/workspace/missing/route.js.map +5 -0
- package/.next/server/app/api/workspace/missing/route.js.nft.json +1 -0
- package/.next/server/app/api/workspace/missing/route_client-reference-manifest.js +3 -0
- package/.next/server/app/api/workspace/route.js +5 -4
- package/.next/server/app/api/workspace/route.js.nft.json +1 -1
- package/.next/server/app/artifact/[...path]/page/server-reference-manifest.json +1 -1
- package/.next/server/app/artifact/[...path]/page.js +4 -3
- package/.next/server/app/artifact/[...path]/page.js.nft.json +1 -1
- package/.next/server/app/artifact/[...path]/page_client-reference-manifest.js +1 -1
- package/.next/server/app/favicon.ico/route.js.nft.json +1 -1
- package/.next/server/app/page/server-reference-manifest.json +67 -45
- package/.next/server/app/page.js +10 -9
- package/.next/server/app/page.js.nft.json +1 -1
- package/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/server/app-paths-manifest.json +3 -2
- package/.next/server/chunks/[root-of-the-server]__02846fb._.js +8 -0
- package/.next/server/chunks/[root-of-the-server]__049f521._.js +3 -0
- package/.next/server/chunks/[root-of-the-server]__05luceg._.js +3 -0
- package/.next/server/chunks/[root-of-the-server]__06--3q3._.js +8 -0
- package/.next/server/chunks/[root-of-the-server]__07bnah_._.js +3 -0
- package/.next/server/chunks/[root-of-the-server]__08eae20._.js +8 -0
- package/.next/server/chunks/[root-of-the-server]__0_ue8oe._.js +3 -0
- package/.next/server/chunks/[root-of-the-server]__0c9fgca._.js +5 -0
- package/.next/server/chunks/[root-of-the-server]__0g-ueei._.js +8 -0
- package/.next/server/chunks/[root-of-the-server]__0h_x6q6._.js +5 -0
- package/.next/server/chunks/{[root-of-the-server]__0pa2n68._.js → [root-of-the-server]__0kxrvxf._.js} +2 -2
- package/.next/server/chunks/{[root-of-the-server]__0q0vvt2._.js → [root-of-the-server]__0ov3iir._.js} +2 -2
- package/.next/server/chunks/[root-of-the-server]__0qwtlmx._.js +8 -0
- package/.next/server/chunks/[root-of-the-server]__0sm5xvm._.js +3 -0
- package/.next/server/chunks/[root-of-the-server]__0ye3da2._.js +3 -0
- package/.next/server/chunks/[root-of-the-server]__0zp35ul._.js +5 -0
- package/.next/server/chunks/[root-of-the-server]__144u9d_._.js +3 -0
- package/.next/server/chunks/[root-of-the-server]__18-2tsq._.js +5 -0
- package/.next/server/chunks/[root-of-the-server]__1e-ci13._.js +3 -0
- package/.next/server/chunks/[root-of-the-server]__1e2365e._.js +8 -0
- package/.next/server/chunks/[root-of-the-server]__1fix932._.js +8 -0
- package/.next/server/chunks/{[root-of-the-server]__0ce8nf8._.js → [root-of-the-server]__1g3t7vn._.js} +3 -3
- package/.next/server/chunks/[root-of-the-server]__1kp-83u._.js +5 -0
- package/.next/server/chunks/[root-of-the-server]__1kv8p3e._.js +5 -0
- package/.next/server/chunks/[root-of-the-server]__1orn-5b._.js +2 -3
- package/.next/server/chunks/_02szwk8._.js +5 -0
- package/.next/server/chunks/_05h2cii._.js +1 -1
- package/.next/server/chunks/_06c9kie._.js +216 -0
- package/.next/server/chunks/{_1x6kd1r._.js → _07y2gj-._.js} +1 -1
- package/.next/server/chunks/_0a43a-0._.js +3 -0
- package/.next/server/chunks/_0bujjey._.js +5 -0
- package/.next/server/chunks/_0c9j6rb._.js +1 -1
- package/.next/server/chunks/_0dkd549._.js +3 -0
- package/.next/server/chunks/_0ej1kvv._.js +3 -0
- package/.next/server/chunks/_0f6ts7z._.js +3 -0
- package/.next/server/chunks/_0gm9hyv._.js +55 -0
- package/.next/server/chunks/_0i55en5._.js +25 -0
- package/.next/server/chunks/_0k50n27._.js +18 -0
- package/.next/server/chunks/_0khihjk._.js +3 -3
- package/.next/server/chunks/_0n74zsf._.js +5 -5
- package/.next/server/chunks/_0nvm9ef._.js +3 -0
- package/.next/server/chunks/_0p56fn0._.js +3 -0
- package/.next/server/chunks/_0recufk._.js +1 -1
- package/.next/server/chunks/_0xbkz10._.js +3 -0
- package/.next/server/chunks/_10tkr26._.js +3 -0
- package/.next/server/chunks/{_0ioz3nd._.js → _11u2kp7._.js} +1 -1
- package/.next/server/chunks/_11ujpya._.js +3 -0
- package/.next/server/chunks/_127nx_p._.js +5 -0
- package/.next/server/chunks/_17l5ygt._.js +1 -1
- package/.next/server/chunks/_19uauj6._.js +3 -0
- package/.next/server/chunks/_1_5mnq4._.js +5 -5
- package/.next/server/chunks/_1abl6t9._.js +55 -0
- package/.next/server/chunks/_1d0j961._.js +3 -3
- package/.next/server/chunks/_1dbixxz._.js +3 -0
- package/.next/server/chunks/_1dc_d1q._.js +98 -0
- package/.next/server/chunks/_1drgfx4._.js +5 -5
- package/.next/server/chunks/_1e48czp._.js +3 -3
- package/.next/server/chunks/_1f31u5x._.js +5 -0
- package/.next/server/chunks/_1kg70wc._.js +3 -3
- package/.next/server/chunks/_1klbcft._.js +5 -0
- package/.next/server/chunks/_1krd9rf._.js +4 -1
- package/.next/server/chunks/_1kwmzab._.js +3 -0
- package/.next/server/chunks/{_1pf4qe-._.js → _1oxj9fa._.js} +1 -1
- package/.next/server/chunks/{_1f46eie._.js → _1pcmdbb._.js} +1 -1
- package/.next/server/chunks/_1qgix-j._.js +3 -0
- package/.next/server/chunks/{lib_logger_ts_1exwjut._.js → _1rpchta._.js} +5 -5
- package/.next/server/chunks/_1vsej-s._.js +5 -0
- package/.next/server/chunks/_1yx0p_s._.js +5 -0
- package/.next/server/chunks/{_0rnf-6n._.js → _20fsr_z._.js} +6 -6
- package/.next/server/chunks/_2128hkj._.js +5 -0
- package/.next/server/chunks/_next-internal_server_app_api_bot_thread_[id]_route_actions_1fstjn2.js +3 -0
- package/.next/server/chunks/_next-internal_server_app_api_bot_thread_route_actions_003ccso.js +3 -0
- package/.next/server/chunks/_next-internal_server_app_api_workspace_missing_route_actions_1fidi6p.js +3 -0
- package/.next/server/chunks/config_ts_1g4sz5o._.js +1 -1
- package/.next/server/chunks/features_0-cnt0l._.js +4 -0
- package/.next/server/chunks/features_0hdaci0._.js +4 -0
- package/.next/server/chunks/features_1e39j7-._.js +8 -0
- package/.next/server/chunks/features_bot_bot_query_ts_12mgkl2._.js +1 -1
- package/.next/server/chunks/features_memory_1uqvksn._.js +1 -1
- package/.next/server/chunks/{node_modules_consola_dist_chunks_prompt_mjs_0r28-sm._.js → node_modules_consola_dist_chunks_prompt_mjs_0a7-pbw._.js} +1 -1
- package/.next/server/chunks/{node_modules_consola_dist_chunks_prompt_mjs_0v_7drq._.js → node_modules_consola_dist_chunks_prompt_mjs_0gzvgfv._.js} +1 -1
- package/.next/server/chunks/{node_modules_consola_dist_chunks_prompt_mjs_0i7cvml._.js → node_modules_consola_dist_chunks_prompt_mjs_107pxfn._.js} +1 -1
- package/.next/server/chunks/{node_modules_consola_dist_chunks_prompt_mjs_0icqaqw._.js → node_modules_consola_dist_chunks_prompt_mjs_15hbel8._.js} +1 -1
- package/.next/server/chunks/node_modules_consola_dist_chunks_prompt_mjs_18i-23e._.js +3 -0
- package/.next/server/chunks/node_modules_consola_dist_chunks_prompt_mjs_1bewgf3._.js +3 -0
- package/.next/server/chunks/node_modules_consola_dist_chunks_prompt_mjs_1kipdeo._.js +3 -0
- package/.next/server/chunks/node_modules_consola_dist_chunks_prompt_mjs_1nlww1m._.js +3 -0
- package/.next/server/chunks/node_modules_consola_dist_chunks_prompt_mjs_1q77nfx._.js +3 -0
- package/.next/server/chunks/node_modules_consola_dist_chunks_prompt_mjs_1xqdzx_._.js +3 -0
- package/.next/server/chunks/{node_modules_zod_v4_1801ax7._.js → node_modules_zod_v4_0lkux5-._.js} +2 -2
- package/.next/server/chunks/{node_modules_zod_v4_1cha_bw._.js → node_modules_zod_v4_1jmr9cq._.js} +2 -2
- package/.next/server/chunks/{node_modules_zod_v4_0ubdflj._.js → node_modules_zod_v4_1yrxmek._.js} +2 -2
- package/.next/server/chunks/node_modules_zod_v4_classic_errors_0tp0y-q.js +3 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__09q9med._.js +1 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__0bxsfyn._.js +101 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__0lyi8i2._.js +1 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__16jc196._.js +1 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__172cq4x._.js +3 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__18dthbp._.js +3 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__18e68w8._.js +3 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__1fll8dc._.js +3 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__1mzi-ju._.js +1 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__1nygfik._.js +5 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__1sdphb0._.js +8 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__1waa3rc._.js +100 -0
- package/.next/server/chunks/ssr/_00rehl_._.js +3 -0
- package/.next/server/chunks/ssr/{_1lz_cq2._.js → _02oc_h5._.js} +76 -89
- package/.next/server/chunks/ssr/_03_928f._.js +3 -0
- package/.next/server/chunks/ssr/_05yghx6._.js +3 -0
- package/.next/server/chunks/ssr/_08c4z-9._.js +3 -0
- package/.next/server/chunks/ssr/_0_qfdlw._.js +4 -0
- package/.next/server/chunks/ssr/_0c9e5w6._.js +9 -0
- package/.next/server/chunks/ssr/{_0s99bgb._.js → _0dh2e1j._.js} +9 -3
- package/.next/server/chunks/ssr/_0dr-ddg._.js +3 -0
- package/.next/server/chunks/ssr/_0eaog6_._.js +3 -0
- package/.next/server/chunks/ssr/_0evkpto._.js +3 -0
- package/.next/server/chunks/ssr/_0g_rlog._.js +3 -0
- package/.next/server/chunks/ssr/_0geg212._.js +3 -0
- package/.next/server/chunks/ssr/{_1gd8q8l._.js → _0hxt2ul._.js} +2 -2
- package/.next/server/chunks/ssr/_0i_6q1w._.js +2 -2
- package/.next/server/chunks/ssr/_0lu1n_x._.js +1 -1
- package/.next/server/chunks/ssr/{_15j2_8f._.js → _0wzaht-._.js} +2 -2
- package/.next/server/chunks/ssr/_0z-g0uy._.js +3 -0
- package/.next/server/chunks/ssr/_123ttsf._.js +3 -0
- package/.next/server/chunks/ssr/_123y-ah._.js +3 -0
- package/.next/server/chunks/ssr/_12w42gi._.js +3 -0
- package/.next/server/chunks/ssr/_161kqav._.js +3 -0
- package/.next/server/chunks/ssr/{_0ap2fzn._.js → _16c6_xm._.js} +2 -2
- package/.next/server/chunks/ssr/_18opah7._.js +3 -0
- package/.next/server/chunks/ssr/_18xqmny._.js +3 -0
- package/.next/server/chunks/ssr/_1_krfph._.js +3 -0
- package/.next/server/chunks/ssr/{_1-_etts._.js → _1anqots._.js} +2 -2
- package/.next/server/chunks/ssr/_1ey7qn-._.js +70 -0
- package/.next/server/chunks/ssr/_1g9vs7q._.js +3 -0
- package/.next/server/chunks/ssr/_1jmz33w._.js +3 -0
- package/.next/server/chunks/ssr/{_0n3toaw._.js → _1k0spz7._.js} +2 -2
- package/.next/server/chunks/ssr/_1rgtxhr._.js +5 -0
- package/.next/server/chunks/ssr/_1slp25y._.js +3 -0
- package/.next/server/chunks/ssr/{_1fojd41._.js → _1x9pskj._.js} +3 -3
- package/.next/server/chunks/ssr/_1y6b5mr._.js +101 -0
- package/.next/server/chunks/ssr/_1ztk7p6._.js +7 -0
- package/.next/server/chunks/ssr/components_ui_tabs_tsx_0ej40b4._.js +3 -0
- package/.next/server/chunks/ssr/features_bot_thread_query_ts_023eklt._.js +4 -0
- package/.next/server/chunks/ssr/features_config_components_config-setting_tsx_0xfwqdg._.js +1 -1
- package/.next/server/chunks/ssr/features_connectors_components_mcp-setting_tsx_0jmbgvp._.js +1 -1
- package/.next/server/chunks/ssr/{_1-teee5._.js → lib_logger_ts_0-olsze._.js} +2 -7
- package/.next/server/chunks/ssr/{node_modules_0cxaxk9._.js → node_modules_00_51_v._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_04jf_m8._.js → node_modules_022sipm._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_0g7fzk3._.js → node_modules_0332gw4._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_1cx_ql6._.js → node_modules_04vxchl._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_1vh5ntv._.js → node_modules_0746rzi._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_0yugy6m._.js → node_modules_0d5uzof._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_09q35v_._.js → node_modules_0exkwcm._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_08dhkiq._.js → node_modules_0fr-ml8._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_14q6g4n._.js → node_modules_0mxvg8h._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_0arus10._.js → node_modules_0qapgjr._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_1eg7m6c._.js → node_modules_0qbfyjh._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_1cz2hle._.js → node_modules_0t-9nup._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_17bwo43._.js → node_modules_0tusx4d._.js} +3 -3
- package/.next/server/chunks/ssr/{node_modules_1efrqxi._.js → node_modules_0vsxbzp._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_0_-_a--._.js → node_modules_0vv0-fg._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_1gdh31p._.js → node_modules_0wsvp00._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_1hpopmo._.js → node_modules_1-urho9._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_1_pc9kq._.js → node_modules_16pi_dd._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_00etblx._.js → node_modules_1d53yiq._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_1aaxuef._.js → node_modules_1g1eihe._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_198nmll._.js → node_modules_1gpednh._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_0pvd7q-._.js → node_modules_1gu1pph._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_0b__sgl._.js → node_modules_1he1w8_._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_1qpjvy5._.js → node_modules_1lnjapm._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_0940ns5._.js → node_modules_1lynj9m._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_0x3-ply._.js → node_modules_1seuurl._.js} +2 -2
- package/.next/server/chunks/ssr/{node_modules_14om4ar._.js → node_modules_1u6w1pb._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_0had7xd._.js → node_modules_1xn6saw._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_@mermaid-js_parser_dist_chunks_mermaid-parser_core_1b748ic._.js → node_modules_@mermaid-js_parser_dist_chunks_mermaid-parser_core_067_exa._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_@mermaid-js_parser_dist_chunks_mermaid-parser_core_1z4hm5e._.js → node_modules_@mermaid-js_parser_dist_chunks_mermaid-parser_core_0nrrdaq._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_@vercel_oidc_dist_06mk8id._.js → node_modules_@vercel_oidc_dist_21a5haa._.js} +1 -1
- package/.next/server/chunks/ssr/node_modules_lucide-react_dist_esm_icons_trash_mjs_0px2gzr._.js +3 -0
- package/.next/server/middleware-build-manifest.js +3 -3
- package/.next/server/pages/404.html +1 -1
- package/.next/server/pages/500.html +1 -1
- package/.next/server/server-reference-manifest.js +1 -1
- package/.next/server/server-reference-manifest.json +68 -46
- package/.next/static/chunks/0dr16v67vj4n7.js +2 -0
- package/.next/static/chunks/0g4b348l0pve7.js +1 -0
- package/.next/static/chunks/0joan-69kbspd.js +1 -0
- package/.next/static/chunks/0vlgzqo9ka90v.js +1 -0
- package/.next/static/chunks/0zrupxgci5gqc.js +1 -0
- package/.next/static/chunks/10k2xvap03z20.css +1 -0
- package/.next/static/chunks/18mkhstgiqcbt.js +1 -0
- package/.next/static/chunks/{15m575979un5f.js → 1i-u2wzub_to1.js} +2 -2
- package/.next/static/chunks/1kp5nfp06nj9h.js +5 -0
- package/.next/static/chunks/1ohslk4568tm3.js +1 -0
- package/.next/static/chunks/1sbmwswc9k2-1.js +1 -0
- package/.next/static/chunks/1t9qms5pwwa3n.js +1 -0
- package/.next/static/chunks/1vj_5bnbxx84e.js +1 -0
- package/.next/static/chunks/1vqezbhjm3goq.js +1 -0
- package/.next/static/chunks/{3akmqm77muj4-.js → 1wlo979mx-y3w.js} +1 -1
- package/.next/static/chunks/26ql4y-43xwhp.js +1 -0
- package/.next/static/chunks/{0fl3gwe4-l6tz.js → 2_pnccjp6j1ca.js} +1 -1
- package/.next/static/chunks/2a51-ywz9b2a3.js +1 -0
- package/.next/static/chunks/2fvdgxih7x7hr.js +1 -0
- package/.next/static/chunks/2jlz8zp4jax_m.js +18 -0
- package/.next/static/chunks/2nvepds1fu2ux.js +1 -0
- package/.next/static/chunks/3addj3i4-izma.js +1 -0
- package/.next/static/chunks/3bfwpww3m3x4u.js +1 -0
- package/.next/static/chunks/3gfh7nxwusbt0.js +1 -0
- package/.next/static/chunks/3gwa44cvz4qoi.js +1 -0
- package/.next/static/chunks/{38gweqdo4jpfj.js → 3izg0a3k87q1-.js} +5 -5
- package/.next/static/chunks/3zk6q3-3f9swq.js +99 -0
- package/.next/static/chunks/3zkg5iup3v4a2.js +12 -0
- package/.next/static/chunks/405m0bqtoihev.js +1 -0
- package/.next/static/chunks/42wcg3wfg_m_p.js +1 -0
- package/README.md +27 -216
- package/app/api/events/app-event.ts +6 -4
- package/app/api/query-key.ts +25 -16
- package/bin/database.mjs +37 -0
- package/bin/thursday.mjs +44 -28
- package/components/ui/custom-icon.tsx +0 -60
- package/components/ui/radio-group.tsx +37 -0
- package/components/ui/shiny-text.tsx +33 -6
- package/database/migrations/{20260910173049_numerous_spirit → 20260916145701_clever_multiple_man}/migration.sql +80 -24
- package/database/migrations/{20260910173049_numerous_spirit → 20260916145701_clever_multiple_man}/snapshot.json +689 -110
- package/database/migrations/20260916160713_thread_cancelled/migration.sql +2 -0
- package/database/migrations/{20260911030320_opposite_jack_power → 20260916160713_thread_cancelled}/snapshot.json +706 -84
- package/docs/how-it-works.md +39 -36
- package/docs/images/keeps-going.png +0 -0
- package/docs/images/two-minds.png +0 -0
- package/docs/live-calls.md +231 -0
- package/docs/thread-rooms.md +71 -0
- package/features/ai/chatgpt.ts +1 -2
- package/features/ai/live.schema.ts +128 -0
- package/features/ai/load-tools.ts +148 -77
- package/features/ai/model.schema.ts +2 -120
- package/features/ai/model.ts +52 -88
- package/features/ai/prompts/bot.prompt.ts +91 -214
- package/features/ai/prompts/live.prompt.ts +265 -0
- package/features/ai/prompts/prompt-helper.ts +91 -32
- package/features/ai/prompts/thursday.prompt.ts +125 -172
- package/features/ai/tools/bot.tool.ts +38 -145
- package/features/ai/tools/call.tool.ts +1 -1
- package/features/ai/tools/connected.ts +5 -3
- package/features/ai/tools/mcp.tool.ts +7 -4
- package/features/ai/tools/memory.tool.ts +35 -31
- package/features/ai/tools/skills.tool.ts +36 -14
- package/features/ai/tools/studio.tool.ts +26 -21
- package/features/ai/tools/tool-name.ts +2 -5
- package/features/ai/tools/workspace.tool.ts +33 -10
- package/features/artifact/artifact.action.ts +2 -2
- package/features/artifact/artifact.query.ts +112 -61
- package/features/artifact/artifact.schema.ts +8 -6
- package/features/artifact/components/artifact-setting.tsx +83 -33
- package/features/bot/bot.action.ts +43 -36
- package/features/bot/bot.memory.ts +137 -20
- package/features/bot/bot.query.ts +28 -11
- package/features/bot/bot.run.ts +299 -636
- package/features/bot/bot.runner.ts +468 -844
- package/features/bot/bot.schema.ts +93 -82
- package/features/bot/bot.seed.ts +141 -64
- package/features/bot/components/bot-mark.tsx +324 -232
- package/features/bot/components/bot-room.tsx +1613 -697
- package/features/bot/components/bot-roster.tsx +8 -13
- package/features/bot/components/bot-setting.tsx +58 -42
- package/features/bot/components/bot-tool.tsx +55 -87
- package/features/bot/components/mark-palette.tsx +35 -32
- package/features/bot/components/path-chips.tsx +33 -3
- package/features/bot/components/thread-badge.tsx +34 -0
- package/features/bot/components/thread-reply.tsx +639 -0
- package/features/bot/components/thread-setting.tsx +674 -0
- package/features/bot/mark.const.ts +79 -31
- package/features/bot/room.query.ts +889 -0
- package/features/bot/room.schema.ts +64 -0
- package/features/bot/seed-bots.ts +2 -1
- package/features/bot/thread.query.ts +971 -0
- package/features/bot/thread.store.ts +587 -0
- package/features/config/components/voice-key.tsx +113 -88
- package/features/config/config.const.ts +5 -9
- package/features/connectors/mcp.schema.ts +0 -2
- package/features/intro/components/intro.tsx +169 -139
- package/features/memory/components/memory-setting.tsx +2 -7
- package/features/memory/memory.schema.ts +14 -23
- package/features/settings/components/setting-ui.tsx +0 -44
- package/features/settings/components/settings.tsx +12 -12
- package/features/settings/settings.alert.ts +3 -3
- package/features/settings/settings.store.ts +1 -1
- package/features/skills/skills.schema.ts +0 -2
- package/features/thursday/components/ascii-field.tsx +24 -44
- package/features/thursday/components/ascii-orb.tsx +359 -358
- package/features/thursday/components/call-log.tsx +46 -44
- package/features/thursday/components/face.tsx +35 -10
- package/features/thursday/components/tab-state.tsx +3 -3
- package/features/thursday/components/thursday-mark.tsx +1 -0
- package/features/thursday/components/thursday-setting.tsx +631 -332
- package/features/thursday/components/thursday.tsx +106 -41
- package/features/thursday/components/voice-picker.tsx +255 -0
- package/features/thursday/thursday.action.ts +94 -68
- package/features/thursday/thursday.query.ts +49 -30
- package/features/thursday/thursday.schema.ts +27 -30
- package/features/thursday/thursday.store.ts +11 -7
- package/features/thursday/tool-call.ts +3 -3
- package/features/thursday/tool-line.ts +5 -3
- package/features/thursday/use-thursday.ts +725 -450
- package/features/workspace/workspace.query.ts +17 -0
- package/features/workspace/workspace.ts +103 -50
- package/lib/live/live.schema.ts +47 -0
- package/lib/live/live.server.ts +155 -0
- package/lib/live/live.session.ts +574 -0
- package/lib/{realtime/realtime.tap.ts → live/live.tap.ts} +41 -12
- package/lib/live/live.transport.ts +152 -0
- package/lib/protocol/use-server-pages.ts +9 -5
- package/lib/utils.ts +21 -20
- package/package.json +2 -2
- package/public/voices/alloy.ogg +0 -0
- package/public/voices/ash.ogg +0 -0
- package/public/voices/ballad.ogg +0 -0
- package/public/voices/beacon.ogg +0 -0
- package/public/voices/bossa.ogg +0 -0
- package/public/voices/cedar.ogg +0 -0
- package/public/voices/cinder.ogg +0 -0
- package/public/voices/coral.ogg +0 -0
- package/public/voices/delta.ogg +0 -0
- package/public/voices/echo.ogg +0 -0
- package/public/voices/gleam.ogg +0 -0
- package/public/voices/marin.ogg +0 -0
- package/public/voices/meridian.ogg +0 -0
- package/public/voices/quartz.ogg +0 -0
- package/public/voices/ripple.ogg +0 -0
- package/public/voices/sage.ogg +0 -0
- package/public/voices/shimmer.ogg +0 -0
- package/public/voices/stone.ogg +0 -0
- package/public/voices/tempo.ogg +0 -0
- package/public/voices/verse.ogg +0 -0
- package/public/voices/vesper.ogg +0 -0
- package/public/voices/willow.ogg +0 -0
- package/scripts/artifact-paths.test.mts +25 -0
- package/scripts/bot-context.test.mts +1408 -0
- package/scripts/live-history.test.mts +115 -0
- package/scripts/live-session.test.mts +948 -0
- package/server.js +1 -1
- package/skills/browser/SKILL.md +17 -6
- package/skills/computer/SKILL.md +1 -1
- package/skills/find-skills/SKILL.md +3 -8
- package/skills/interactive-page/SKILL.md +12 -7
- package/skills/interactive-page/references/diagram.md +89 -0
- package/skills/interactive-page/scripts/archify/LICENSE +22 -0
- package/skills/interactive-page/scripts/archify/README.md +25 -0
- package/skills/interactive-page/scripts/archify/assets/JetBrainsMono-OFL.txt +93 -0
- package/skills/interactive-page/scripts/archify/assets/template.html +14934 -0
- package/skills/interactive-page/scripts/archify/bin/archify.mjs +2126 -0
- package/skills/interactive-page/scripts/archify/bin/open-artifact.mjs +86 -0
- package/skills/interactive-page/scripts/archify/bin/preview.mjs +653 -0
- package/skills/interactive-page/scripts/archify/bin/visual-check.mjs +829 -0
- package/skills/interactive-page/scripts/archify/delta/architecture-delta.mjs +1223 -0
- package/skills/interactive-page/scripts/archify/examples/agent-run.lifecycle.json +60 -0
- package/skills/interactive-page/scripts/archify/examples/agent-tool-call.workflow.json +94 -0
- package/skills/interactive-page/scripts/archify/examples/async-job-roundtrip.sequence.json +61 -0
- package/skills/interactive-page/scripts/archify/examples/cache-miss-request.sequence.json +82 -0
- package/skills/interactive-page/scripts/archify/examples/checkout-platform.base.architecture.json +31 -0
- package/skills/interactive-page/scripts/archify/examples/checkout-platform.head.architecture.json +31 -0
- package/skills/interactive-page/scripts/archify/examples/deployment-release.lifecycle.json +49 -0
- package/skills/interactive-page/scripts/archify/examples/event-stream.dataflow.json +57 -0
- package/skills/interactive-page/scripts/archify/examples/incident-response.workflow.json +64 -0
- package/skills/interactive-page/scripts/archify/examples/product-analytics.dataflow.json +76 -0
- package/skills/interactive-page/scripts/archify/examples/production-deployment.architecture.json +71 -0
- package/skills/interactive-page/scripts/archify/examples/release-delivery.workflow.json +62 -0
- package/skills/interactive-page/scripts/archify/examples/web-app.architecture.json +46 -0
- package/skills/interactive-page/scripts/archify/migrations/workflow-v2.mjs +279 -0
- package/skills/interactive-page/scripts/archify/recipes/scenarios.mjs +284 -0
- package/skills/interactive-page/scripts/archify/references/authoring-contract.md +243 -0
- package/skills/interactive-page/scripts/archify/references/delivery-contract.md +120 -0
- package/skills/interactive-page/scripts/archify/references/viewer-runtime.md +45 -0
- package/skills/interactive-page/scripts/archify/renderers/architecture/grid.mjs +62 -0
- package/skills/interactive-page/scripts/archify/renderers/architecture/render-architecture.mjs +1078 -0
- package/skills/interactive-page/scripts/archify/renderers/dataflow/README.md +104 -0
- package/skills/interactive-page/scripts/archify/renderers/dataflow/render-dataflow.mjs +483 -0
- package/skills/interactive-page/scripts/archify/renderers/lifecycle/README.md +115 -0
- package/skills/interactive-page/scripts/archify/renderers/lifecycle/render-lifecycle.mjs +561 -0
- package/skills/interactive-page/scripts/archify/renderers/sequence/README.md +114 -0
- package/skills/interactive-page/scripts/archify/renderers/sequence/render-sequence.mjs +464 -0
- package/skills/interactive-page/scripts/archify/renderers/shared/brand-marks.mjs +563 -0
- package/skills/interactive-page/scripts/archify/renderers/shared/cli.mjs +218 -0
- package/skills/interactive-page/scripts/archify/renderers/shared/desktop-readability.mjs +26 -0
- package/skills/interactive-page/scripts/archify/renderers/shared/diagnostics.mjs +127 -0
- package/skills/interactive-page/scripts/archify/renderers/shared/engineering-profiles.mjs +157 -0
- package/skills/interactive-page/scripts/archify/renderers/shared/generated-brand-marks.mjs +3 -0
- package/skills/interactive-page/scripts/archify/renderers/shared/generated-validators.mjs +13 -0
- package/skills/interactive-page/scripts/archify/renderers/shared/geometry.mjs +1423 -0
- package/skills/interactive-page/scripts/archify/renderers/shared/i18n.mjs +595 -0
- package/skills/interactive-page/scripts/archify/renderers/shared/layout-report.mjs +40 -0
- package/skills/interactive-page/scripts/archify/renderers/shared/legend.mjs +217 -0
- package/skills/interactive-page/scripts/archify/renderers/shared/output-path.mjs +340 -0
- package/skills/interactive-page/scripts/archify/renderers/shared/repository-evidence.mjs +238 -0
- package/skills/interactive-page/scripts/archify/renderers/shared/repository-location.mjs +58 -0
- package/skills/interactive-page/scripts/archify/renderers/shared/text-fit.mjs +49 -0
- package/skills/interactive-page/scripts/archify/renderers/shared/utils.mjs +232 -0
- package/skills/interactive-page/scripts/archify/renderers/shared/validator.mjs +86 -0
- package/skills/interactive-page/scripts/archify/renderers/workflow/README.md +223 -0
- package/skills/interactive-page/scripts/archify/renderers/workflow/render-workflow.mjs +35 -0
- package/skills/interactive-page/scripts/archify/renderers/workflow/workflow-compiler.mjs +4400 -0
- package/skills/interactive-page/scripts/archify/renderers/workflow/workflow-migration-geometry.mjs +144 -0
- package/skills/interactive-page/scripts/archify/schemas/README.md +211 -0
- package/skills/interactive-page/scripts/archify/schemas/architecture.schema.json +178 -0
- package/skills/interactive-page/scripts/archify/schemas/common.schema.json +115 -0
- package/skills/interactive-page/scripts/archify/schemas/dataflow.schema.json +243 -0
- package/skills/interactive-page/scripts/archify/schemas/lifecycle.schema.json +266 -0
- package/skills/interactive-page/scripts/archify/schemas/sequence.schema.json +223 -0
- package/skills/interactive-page/scripts/archify/schemas/workflow.schema.json +428 -0
- package/skills/interactive-page/scripts/archify/scripts/check-render-output.mjs +916 -0
- package/skills/interactive-page/scripts/archify/scripts/render-examples.mjs +26 -0
- package/skills/interactive-page/scripts/bundle-artifact.sh +9 -8
- package/skills/skill-creator/SKILL.md +4 -5
- package/.next/server/app/api/bot/task/[id]/route/app-paths-manifest.json +0 -3
- package/.next/server/app/api/bot/task/[id]/route.js +0 -16
- package/.next/server/app/api/bot/task/[id]/route.js.nft.json +0 -1
- package/.next/server/app/api/bot/task/[id]/route_client-reference-manifest.js +0 -3
- package/.next/server/app/api/bot/task/route/app-paths-manifest.json +0 -3
- package/.next/server/app/api/bot/task/route.js +0 -16
- package/.next/server/app/api/bot/task/route.js.nft.json +0 -1
- package/.next/server/app/api/bot/task/route_client-reference-manifest.js +0 -3
- package/.next/server/chunks/[root-of-the-server]__01dkp4a._.js +0 -3
- package/.next/server/chunks/[root-of-the-server]__02va5wz._.js +0 -3
- package/.next/server/chunks/[root-of-the-server]__02xjoto._.js +0 -3
- package/.next/server/chunks/[root-of-the-server]__04q--0f._.js +0 -8
- package/.next/server/chunks/[root-of-the-server]__0bl-e63._.js +0 -3
- package/.next/server/chunks/[root-of-the-server]__0ffbg85._.js +0 -3
- package/.next/server/chunks/[root-of-the-server]__0jq4ohy._.js +0 -8
- package/.next/server/chunks/[root-of-the-server]__0ls6_8y._.js +0 -8
- package/.next/server/chunks/[root-of-the-server]__0ncs9uu._.js +0 -3
- package/.next/server/chunks/[root-of-the-server]__1-ga0kh._.js +0 -5
- package/.next/server/chunks/[root-of-the-server]__11n256_._.js +0 -5
- package/.next/server/chunks/[root-of-the-server]__12cabg9._.js +0 -3
- package/.next/server/chunks/[root-of-the-server]__1339jnv._.js +0 -8
- package/.next/server/chunks/[root-of-the-server]__18_teo8._.js +0 -8
- package/.next/server/chunks/[root-of-the-server]__1_04mcf._.js +0 -5
- package/.next/server/chunks/[root-of-the-server]__1anc1p9._.js +0 -3
- package/.next/server/chunks/[root-of-the-server]__1ccon1w._.js +0 -10
- package/.next/server/chunks/[root-of-the-server]__1fizn31._.js +0 -3
- package/.next/server/chunks/[root-of-the-server]__1llj3-b._.js +0 -3
- package/.next/server/chunks/[root-of-the-server]__1p8m3f4._.js +0 -3
- package/.next/server/chunks/[root-of-the-server]__1wa1k7r._.js +0 -8
- package/.next/server/chunks/_0-73of8._.js +0 -55
- package/.next/server/chunks/_0-fjql4._.js +0 -26
- package/.next/server/chunks/_00xh_q0._.js +0 -25
- package/.next/server/chunks/_03a0897._.js +0 -3
- package/.next/server/chunks/_051qh8u._.js +0 -3
- package/.next/server/chunks/_05sovvj._.js +0 -3
- package/.next/server/chunks/_0fzqt2y._.js +0 -134
- package/.next/server/chunks/_0mecv0f._.js +0 -26
- package/.next/server/chunks/_0mh8j4m._.js +0 -3
- package/.next/server/chunks/_0obojvv._.js +0 -3
- package/.next/server/chunks/_0sb7tlz._.js +0 -3
- package/.next/server/chunks/_0sbynis._.js +0 -3
- package/.next/server/chunks/_0u10wmw._.js +0 -55
- package/.next/server/chunks/_0u4aiy2._.js +0 -3
- package/.next/server/chunks/_0wp4le3._.js +0 -3
- package/.next/server/chunks/_0y5fg-x._.js +0 -3
- package/.next/server/chunks/_1230wbx._.js +0 -10
- package/.next/server/chunks/_12nheja._.js +0 -3
- package/.next/server/chunks/_13510xh._.js +0 -3
- package/.next/server/chunks/_15hq89o._.js +0 -3
- package/.next/server/chunks/_18-1wcc._.js +0 -55
- package/.next/server/chunks/_1_9mjx-._.js +0 -3
- package/.next/server/chunks/_1c78vw5._.js +0 -229
- package/.next/server/chunks/_1h5_8sk._.js +0 -3
- package/.next/server/chunks/_1hdnxb7._.js +0 -3
- package/.next/server/chunks/_1j8k_j5._.js +0 -3
- package/.next/server/chunks/_1nula3v._.js +0 -3
- package/.next/server/chunks/_1php4eq._.js +0 -3
- package/.next/server/chunks/_1yuv4vp._.js +0 -3
- package/.next/server/chunks/_1zjrdag._.js +0 -3
- package/.next/server/chunks/_next-internal_server_app_api_bot_task_[id]_route_actions_1_k_8u3.js +0 -3
- package/.next/server/chunks/_next-internal_server_app_api_bot_task_route_actions_0xln6ra.js +0 -3
- package/.next/server/chunks/features_0rpfxbk._.js +0 -3
- package/.next/server/chunks/features_1qf3cei._.js +0 -3
- package/.next/server/chunks/features_bot_0zt92oh._.js +0 -3
- package/.next/server/chunks/node_modules_consola_dist_chunks_prompt_mjs_1xoq2rl._.js +0 -3
- package/.next/server/chunks/ssr/[externals]__0dizgek._.js +0 -3
- package/.next/server/chunks/ssr/[root-of-the-server]__092knlj._.js +0 -3
- package/.next/server/chunks/ssr/[root-of-the-server]__0mz33a7._.js +0 -3
- package/.next/server/chunks/ssr/[root-of-the-server]__0s9dq5u._.js +0 -3
- package/.next/server/chunks/ssr/[root-of-the-server]__0s9l2sv._.js +0 -3
- package/.next/server/chunks/ssr/[root-of-the-server]__0vnmv-s._.js +0 -3
- package/.next/server/chunks/ssr/[root-of-the-server]__0xsog8n._.js +0 -3
- package/.next/server/chunks/ssr/[root-of-the-server]__10-dz5o._.js +0 -61
- package/.next/server/chunks/ssr/[root-of-the-server]__1gtlw6v._.js +0 -43
- package/.next/server/chunks/ssr/[root-of-the-server]__1ox7hbk._.js +0 -4
- package/.next/server/chunks/ssr/[root-of-the-server]__202p40z._.js +0 -3
- package/.next/server/chunks/ssr/_001_ms1._.js +0 -28
- package/.next/server/chunks/ssr/_01n3o0m._.js +0 -3
- package/.next/server/chunks/ssr/_0fz4mf_._.js +0 -3
- package/.next/server/chunks/ssr/_0gb4gmf._.js +0 -3
- package/.next/server/chunks/ssr/_0h42fze._.js +0 -3
- package/.next/server/chunks/ssr/_0h7zdni._.js +0 -3
- package/.next/server/chunks/ssr/_0ilna52._.js +0 -42
- package/.next/server/chunks/ssr/_0q0zan6._.js +0 -70
- package/.next/server/chunks/ssr/_0zcjrbg._.js +0 -3
- package/.next/server/chunks/ssr/_0zhoc3r._.js +0 -3
- package/.next/server/chunks/ssr/_13fke0x._.js +0 -3
- package/.next/server/chunks/ssr/_18cymkq._.js +0 -3
- package/.next/server/chunks/ssr/_1b2mjwr._.js +0 -3
- package/.next/server/chunks/ssr/_1f-dg-6._.js +0 -3
- package/.next/server/chunks/ssr/_1h0rw11._.js +0 -3
- package/.next/server/chunks/ssr/_1jhd09t._.js +0 -3
- package/.next/server/chunks/ssr/_1nnsk7z._.js +0 -3
- package/.next/server/chunks/ssr/_1ov37if._.js +0 -3
- package/.next/server/chunks/ssr/_1sji22n._.js +0 -3
- package/.next/server/chunks/ssr/_1vi_uo7._.js +0 -3
- package/.next/server/chunks/ssr/_1vyd1ji._.js +0 -3
- package/.next/server/chunks/ssr/_1xx4zlb._.js +0 -3
- package/.next/server/chunks/ssr/components_ui_1y1l7yj._.js +0 -3
- package/.next/server/chunks/ssr/features_bot_1s_-2vj._.js +0 -3
- package/.next/server/chunks/ssr/lib_realtime_12wg_dd._.js +0 -27
- package/.next/server/chunks/ssr/lib_realtime_15h_v1v._.js +0 -27
- package/.next/server/chunks/ssr/node_modules_lucide-react_dist_esm_icons_0npjf8-._.js +0 -3
- package/.next/static/chunks/00rd4-v8r7axp.js +0 -1
- package/.next/static/chunks/08qe4as2q428z.js +0 -3
- package/.next/static/chunks/0gnqdjn2lvdad.js +0 -2
- package/.next/static/chunks/0hwap1-p0m5sa.js +0 -1
- package/.next/static/chunks/0r5qcctujhxli.js +0 -1
- package/.next/static/chunks/0w52rrrmuj9r9.js +0 -1
- package/.next/static/chunks/0wcprt5ot5she.js +0 -1
- package/.next/static/chunks/10teww7wt-f6h.js +0 -1
- package/.next/static/chunks/17p6tk6k88dq6.js +0 -1
- package/.next/static/chunks/18a96rkhi4_yd.js +0 -1
- package/.next/static/chunks/1dkjg87pxk0n9.js +0 -28
- package/.next/static/chunks/1jd09sxcjddu1.js +0 -1
- package/.next/static/chunks/1w73ece_6bheu.js +0 -18
- package/.next/static/chunks/214ya3n5gxeih.js +0 -1
- package/.next/static/chunks/22_zkas21rpna.js +0 -1
- package/.next/static/chunks/2fh4-3z_q8b_w.js +0 -1
- package/.next/static/chunks/2h0sd2vumhb15.js +0 -1
- package/.next/static/chunks/2t4vhi3m8ahya.js +0 -1
- package/.next/static/chunks/2tigdtz3dpcy8.js +0 -1
- package/.next/static/chunks/2u7xt6mxg44lf.js +0 -1
- package/.next/static/chunks/3786h5vayt80-.js +0 -28
- package/.next/static/chunks/39j0fvpybzabl.js +0 -40
- package/.next/static/chunks/3cq5g494yuke8.css +0 -1
- package/.next/static/chunks/3hibbjnt85-k6.js +0 -1
- package/.next/static/chunks/3woj-xd7ki5_i.js +0 -1
- package/.next/static/chunks/3xk7yqgomx7yq.js +0 -1
- package/.next/static/chunks/3zmcsnuo25zvq.js +0 -1
- package/database/migrations/20260911030320_opposite_jack_power/migration.sql +0 -1
- package/docs/images/task-thread.png +0 -0
- package/features/bot/components/task-badge.tsx +0 -42
- package/features/bot/components/task-reply.tsx +0 -328
- package/features/bot/components/task-setting.tsx +0 -524
- package/features/bot/task.query.ts +0 -976
- package/features/bot/task.store.ts +0 -489
- package/lib/realtime/client-secret.ts +0 -86
- package/lib/realtime/open-session.ts +0 -31
- package/lib/realtime/openai.ts +0 -35
- package/lib/realtime/realtime.audio.ts +0 -196
- package/lib/realtime/realtime.driver.ts +0 -489
- package/lib/realtime/realtime.protocol.ts +0 -393
- package/lib/realtime/realtime.schema.ts +0 -37
- package/lib/realtime/realtime.session.ts +0 -106
- package/lib/realtime/realtime.transport.ts +0 -384
- package/lib/realtime/xai.ts +0 -39
- /package/.next/server/app/api/bot/{task → thread}/[id]/route/build-manifest.json +0 -0
- /package/.next/server/app/api/bot/{task → thread}/[id]/route/server-reference-manifest.json +0 -0
- /package/.next/server/app/api/bot/{task → thread}/[id]/route.js.map +0 -0
- /package/.next/server/app/api/bot/{task → thread}/route/build-manifest.json +0 -0
- /package/.next/server/app/api/bot/{task → thread}/route/server-reference-manifest.json +0 -0
- /package/.next/server/app/api/bot/{task → thread}/route.js.map +0 -0
- /package/.next/static/{kaTO0UHqF1bMnWgCDik2f → 0qzGoKn2wTERVZtblX1tp}/_buildManifest.js +0 -0
- /package/.next/static/{kaTO0UHqF1bMnWgCDik2f → 0qzGoKn2wTERVZtblX1tp}/_clientMiddlewareManifest.js +0 -0
- /package/.next/static/{kaTO0UHqF1bMnWgCDik2f → 0qzGoKn2wTERVZtblX1tp}/_ssgManifest.js +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import { format } from "date-fns";
|
|
3
|
+
import { format, isThisYear, isToday, isYesterday } from "date-fns";
|
|
4
4
|
import {
|
|
5
5
|
ArrowUp,
|
|
6
6
|
Check,
|
|
@@ -8,8 +8,9 @@ import {
|
|
|
8
8
|
ChevronLeft,
|
|
9
9
|
ChevronRight,
|
|
10
10
|
ChevronsRight,
|
|
11
|
+
CirclePause,
|
|
12
|
+
CircleQuestionMark,
|
|
11
13
|
Copy,
|
|
12
|
-
History,
|
|
13
14
|
Loader2,
|
|
14
15
|
Plus,
|
|
15
16
|
RotateCw,
|
|
@@ -19,37 +20,50 @@ import {
|
|
|
19
20
|
Fragment,
|
|
20
21
|
memo,
|
|
21
22
|
type ReactNode,
|
|
23
|
+
type RefObject,
|
|
22
24
|
useCallback,
|
|
23
25
|
useEffect,
|
|
24
26
|
useMemo,
|
|
25
27
|
useRef,
|
|
26
28
|
useState,
|
|
27
29
|
} from "react";
|
|
30
|
+
import { useAppEvent } from "@/app/api/events/app-event.client";
|
|
28
31
|
import { queryKey } from "@/app/api/query-key";
|
|
29
32
|
import { Bubble, BubbleContent } from "@/components/ui/bubble";
|
|
30
33
|
import { Button } from "@/components/ui/button";
|
|
31
34
|
import { FoldedText } from "@/components/ui/folded-text";
|
|
32
35
|
import { Markdown } from "@/components/ui/markdown";
|
|
33
|
-
import { ShinyText
|
|
36
|
+
import { ShinyText } from "@/components/ui/shiny-text";
|
|
37
|
+
import { Skeleton } from "@/components/ui/skeleton";
|
|
38
|
+
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|
34
39
|
import { Textarea } from "@/components/ui/textarea";
|
|
35
40
|
import { toast } from "@/components/ui/toast";
|
|
36
|
-
import {
|
|
41
|
+
import { PAGE_SIZE, ROOM_KEEP_READ_MS } from "@/config";
|
|
42
|
+
import { startThreadAction } from "@/features/bot/bot.action";
|
|
37
43
|
import {
|
|
38
44
|
type Bot,
|
|
39
45
|
type BotIcon,
|
|
40
46
|
DEFAULT_BOT,
|
|
41
47
|
isAppStop,
|
|
42
|
-
|
|
48
|
+
needsThreadReply,
|
|
49
|
+
THREAD_CONTINUE,
|
|
50
|
+
type Thread,
|
|
43
51
|
} from "@/features/bot/bot.schema";
|
|
44
52
|
import { BotMark } from "@/features/bot/components/bot-mark";
|
|
45
53
|
import { BotRoster } from "@/features/bot/components/bot-roster";
|
|
46
54
|
import { PathChips } from "@/features/bot/components/path-chips";
|
|
47
|
-
import {
|
|
48
|
-
|
|
55
|
+
import {
|
|
56
|
+
ThreadReply,
|
|
57
|
+
useAnswerThread,
|
|
58
|
+
} from "@/features/bot/components/thread-reply";
|
|
49
59
|
import { ThursdayMark } from "@/features/thursday/components/thursday-mark";
|
|
50
60
|
import { FileViewer } from "@/features/workspace/components/file-view";
|
|
51
|
-
import { shortAgo, toDate } from "@/lib/date-like";
|
|
61
|
+
import { type DateLike, shortAgo, toDate } from "@/lib/date-like";
|
|
52
62
|
import { useServerAction } from "@/lib/protocol/use-server-action";
|
|
63
|
+
import {
|
|
64
|
+
type ServerPages,
|
|
65
|
+
useServerPages,
|
|
66
|
+
} from "@/lib/protocol/use-server-pages";
|
|
53
67
|
import { revalidate, useServerRoute } from "@/lib/protocol/use-server-route";
|
|
54
68
|
import {
|
|
55
69
|
cn,
|
|
@@ -59,138 +73,185 @@ import {
|
|
|
59
73
|
WAITING_INK,
|
|
60
74
|
} from "@/lib/utils";
|
|
61
75
|
import { MARK_PALETTE, MARK_SHAPES } from "../mark.const";
|
|
76
|
+
import { ROOM_THURSDAY } from "../room.schema";
|
|
62
77
|
import {
|
|
63
78
|
type BotRef,
|
|
64
|
-
|
|
79
|
+
botThreads,
|
|
65
80
|
type Chatter,
|
|
66
|
-
|
|
67
|
-
isOutcome,
|
|
81
|
+
heardBy,
|
|
68
82
|
lastSaid,
|
|
69
83
|
latestPerBot,
|
|
70
84
|
rosterOf,
|
|
71
|
-
type
|
|
72
|
-
type
|
|
85
|
+
type ThreadItem,
|
|
86
|
+
type ThreadView,
|
|
87
|
+
type ThreadViewStatus,
|
|
88
|
+
threadFromRow,
|
|
73
89
|
threadItems,
|
|
74
|
-
|
|
90
|
+
useBotThreads,
|
|
75
91
|
useSeenOnDetail,
|
|
76
|
-
} from "../
|
|
92
|
+
} from "../thread.store";
|
|
77
93
|
import { BotTool } from "./bot-tool";
|
|
78
94
|
|
|
79
95
|
/**
|
|
80
|
-
* The
|
|
81
|
-
* asking, and what just finished (
|
|
96
|
+
* The thread room and inbox in the corner of the call screen: what is running, what is
|
|
97
|
+
* asking, and what just finished (thread.query listInboxThreads). It only projects server
|
|
82
98
|
* state.
|
|
83
99
|
*
|
|
84
100
|
* Folded, it is not a badge you have to open. Anything waiting on an answer is drawn
|
|
85
101
|
* on the chip itself, with its buttons; what a bot is doing right now sits beside its
|
|
86
102
|
* face, and what passed between two parties rides above one for a moment
|
|
87
|
-
* (
|
|
88
|
-
* chevron that reveals the rows would put a click in front of the one thing this
|
|
89
|
-
* corner exists to remove.
|
|
103
|
+
* (useHandoff). Open, a thread can be read and answered in place.
|
|
90
104
|
*
|
|
91
|
-
* memo: the parent re-renders per transcript chunk and
|
|
105
|
+
* memo: the parent re-renders per transcript chunk, and nothing here reads its props.
|
|
92
106
|
*/
|
|
93
107
|
export const BotRoom = memo(function BotRoom() {
|
|
94
|
-
const
|
|
108
|
+
const threads = useBotThreads();
|
|
95
109
|
const { data: bots } = useServerRoute<Bot[]>(queryKey.bot);
|
|
96
110
|
const [open, setOpen] = useState(false);
|
|
97
111
|
/** Open thread; null shows the list. */
|
|
98
112
|
const [picked, setPicked] = useState<string | null>(null);
|
|
99
113
|
/** Writing a new job, folded or in the room. */
|
|
100
114
|
const [composing, setComposing] = useState(false);
|
|
115
|
+
/** The bot each thread shows, by thread id; a thread not in here is on All. */
|
|
116
|
+
const [sides, setSides] = useState<Record<string, string | null>>({});
|
|
117
|
+
/** The list on screen: what is current, or everything that has ended. */
|
|
118
|
+
const [tab, setTab] = useState<RoomTab>("now");
|
|
119
|
+
/** Where History was scrolled, kept while one of its threads is open. */
|
|
120
|
+
const scroll = useRef(0);
|
|
121
|
+
|
|
122
|
+
const newest = [...threads].reverse();
|
|
123
|
+
// An ending the user has opened moves to History once it has been over for
|
|
124
|
+
// ROOM_KEEP_READ_MS; until then it stays in reach on Now. A cancel is the
|
|
125
|
+
// user's own stop, with nothing to read, so it goes at once.
|
|
126
|
+
const moment = Date.now();
|
|
127
|
+
const leavesAt = (entry: ThreadView) => {
|
|
128
|
+
if (entry.status === "cancelled") return 0;
|
|
129
|
+
if (entry.status !== "done" || !entry.seen) return Number.POSITIVE_INFINITY;
|
|
130
|
+
return toDate(entry.updatedAt).getTime() + ROOM_KEEP_READ_MS;
|
|
131
|
+
};
|
|
132
|
+
const now = newest.filter((entry) => leavesAt(entry) > moment);
|
|
133
|
+
// Nothing else re-renders the room when that time passes.
|
|
134
|
+
const [, settle] = useState(0);
|
|
135
|
+
const nextLeave = Math.min(...now.map(leavesAt));
|
|
136
|
+
useEffect(() => {
|
|
137
|
+
if (!Number.isFinite(nextLeave)) return;
|
|
138
|
+
const timer = setTimeout(
|
|
139
|
+
() => settle((count) => count + 1),
|
|
140
|
+
nextLeave - Date.now(),
|
|
141
|
+
);
|
|
142
|
+
return () => clearTimeout(timer);
|
|
143
|
+
}, [nextLeave]);
|
|
144
|
+
|
|
145
|
+
// Read only while History is on screen, or one of its threads is.
|
|
146
|
+
const browsing = open && tab === "history";
|
|
147
|
+
const history = useServerPages<Thread>({
|
|
148
|
+
key: (index, previous) => {
|
|
149
|
+
if (!browsing) return null;
|
|
150
|
+
if (index === 0) return queryKey.threadHistory(null);
|
|
151
|
+
const tail = previous?.at(-1);
|
|
152
|
+
return tail
|
|
153
|
+
? queryKey.threadHistory(toDate(tail.updatedAt).toISOString())
|
|
154
|
+
: null;
|
|
155
|
+
},
|
|
156
|
+
size: PAGE_SIZE,
|
|
157
|
+
});
|
|
158
|
+
// Loaded pages stay cached while the tab is away and nothing refreshes them
|
|
159
|
+
// then, so opening History reads them again — unless this is the first read.
|
|
160
|
+
const { refresh } = history;
|
|
161
|
+
const cached = useRef(false);
|
|
162
|
+
cached.current = history.items.length > 0;
|
|
163
|
+
useEffect(() => {
|
|
164
|
+
if (browsing && cached.current) void refresh();
|
|
165
|
+
}, [browsing, refresh]);
|
|
166
|
+
const past = useMemo(
|
|
167
|
+
() =>
|
|
168
|
+
history.items
|
|
169
|
+
.filter((row) => row.status === "done" || row.status === "cancelled")
|
|
170
|
+
.map((row) => threadFromRow(row, bots)),
|
|
171
|
+
[history.items, bots],
|
|
172
|
+
);
|
|
101
173
|
|
|
102
|
-
|
|
174
|
+
// The inbox copy first: it is the one the threads signal keeps live.
|
|
103
175
|
const current = picked
|
|
104
|
-
? (newest.find((entry) => entry.id === picked) ??
|
|
176
|
+
? (newest.find((entry) => entry.id === picked) ??
|
|
177
|
+
past.find((entry) => entry.id === picked) ??
|
|
178
|
+
null)
|
|
105
179
|
: null;
|
|
106
180
|
|
|
107
|
-
const [
|
|
108
|
-
const { crew, more } = useMemo(() => crewOf(bots,
|
|
181
|
+
const [bubble, handoff] = useHandoff();
|
|
182
|
+
const { crew, more } = useMemo(() => crewOf(bots, threads), [bots, threads]);
|
|
109
183
|
|
|
110
|
-
// What each
|
|
111
|
-
// sync. Only what changed since then just happened.
|
|
112
|
-
const known = useRef<Map<string,
|
|
184
|
+
// What each thread and participant was, and which lines had landed, at the
|
|
185
|
+
// previous sync. Only what changed since then just happened.
|
|
186
|
+
const known = useRef<Map<string, ThreadViewStatus> | null>(null);
|
|
187
|
+
const standing = useRef(new Map<string, string>());
|
|
113
188
|
const passed = useRef(new Set<string>());
|
|
114
189
|
|
|
115
190
|
useEffect(() => {
|
|
116
|
-
if (!
|
|
117
|
-
const now = new Map(newest.map((entry) => [entry.id, entry.status]));
|
|
191
|
+
if (!botThreads.primed()) return;
|
|
118
192
|
const was = known.current;
|
|
119
|
-
known.current =
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
for (const task of tasks) {
|
|
123
|
-
for (const line of task.lines) {
|
|
124
|
-
if (line.kind === "ask" && line.to) seen.add(line.id);
|
|
125
|
-
}
|
|
126
|
-
}
|
|
193
|
+
known.current = new Map(threads.map((entry) => [entry.id, entry.status]));
|
|
194
|
+
const stood = standing.current;
|
|
195
|
+
standing.current = participantStates(threads);
|
|
127
196
|
const had = passed.current;
|
|
128
|
-
passed.current =
|
|
197
|
+
passed.current = new Set(
|
|
198
|
+
threads.flatMap((thread) => thread.lines.map((line) => line.id)),
|
|
199
|
+
);
|
|
129
200
|
|
|
130
|
-
// The first list only seeds
|
|
201
|
+
// The first list only seeds these: nothing on it just happened.
|
|
131
202
|
if (!was) return;
|
|
132
203
|
|
|
133
|
-
//
|
|
134
|
-
//
|
|
135
|
-
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
const round = rounds.get(line.bot.name) ?? [];
|
|
144
|
-
round.push({ from: line.bot, to: line.to, text: line.text });
|
|
145
|
-
rounds.set(line.bot.name, round);
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
for (const [giver, round] of rounds) {
|
|
149
|
-
if (round.length === 1) {
|
|
150
|
-
handoff({
|
|
151
|
-
at: round[0].to.name,
|
|
152
|
-
from: round[0].from,
|
|
153
|
-
text: clipWord(round[0].text),
|
|
154
|
-
});
|
|
155
|
-
continue;
|
|
156
|
-
}
|
|
157
|
-
handoff({
|
|
158
|
-
at: giver,
|
|
159
|
-
from: round[0].from,
|
|
160
|
-
text: `sent ${round.length} parts out`,
|
|
161
|
-
});
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
for (const task of newest) {
|
|
165
|
-
if (was.get(task.id) === task.status || task.status !== "working")
|
|
166
|
-
continue;
|
|
167
|
-
// A job that was never seen is one arriving; one that was is you having
|
|
168
|
-
// answered it. The mark says who gave it away — and a job typed into the
|
|
169
|
-
// compose box is Thursday's here too, because a task row does not record
|
|
170
|
-
// which of the two started it.
|
|
171
|
-
handoff(
|
|
172
|
-
was.has(task.id)
|
|
173
|
-
? { at: task.bot.name, from: null, text: "picked it back up" }
|
|
174
|
-
: {
|
|
175
|
-
at: task.bot.name,
|
|
176
|
-
from: THURSDAY,
|
|
177
|
-
text: `took on \u201c${clipWord(task.label)}\u201d`,
|
|
178
|
-
},
|
|
204
|
+
// One bubble at a time: of what this sync brought, the one that matters
|
|
205
|
+
// most, and of equals the later.
|
|
206
|
+
let top: Happening | null = null;
|
|
207
|
+
for (const thread of threads) {
|
|
208
|
+
const happened = happenedIn(
|
|
209
|
+
thread,
|
|
210
|
+
was.get(thread.id),
|
|
211
|
+
had,
|
|
212
|
+
stood,
|
|
213
|
+
standing.current,
|
|
179
214
|
);
|
|
215
|
+
for (const one of happened) {
|
|
216
|
+
if (!top || one.rank >= top.rank) top = one;
|
|
217
|
+
}
|
|
180
218
|
}
|
|
181
|
-
|
|
219
|
+
if (top) handoff(top);
|
|
220
|
+
}, [threads, handoff]);
|
|
182
221
|
|
|
183
222
|
// Reading a thread is reading its ending; that is what clears its dot.
|
|
184
223
|
useSeenOnDetail(open ? current : null);
|
|
185
224
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
225
|
+
// Thursday put a job in front of the user (`thread` `open`): the room opens on it
|
|
226
|
+
useAppEvent({
|
|
227
|
+
showThread: (event) => {
|
|
228
|
+
setPicked(event.threadId);
|
|
229
|
+
setOpen(true);
|
|
230
|
+
},
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
const busy = threads.filter((entry) => entry.status === "working").length;
|
|
234
|
+
const pending = newest.filter(needsYou).length;
|
|
189
235
|
const unread = newest.filter(isUnread);
|
|
190
|
-
|
|
236
|
+
// In the open room the list says what each bot is on; its foot keeps the faces,
|
|
237
|
+
// so a step's words never come and go under the list.
|
|
238
|
+
const faces = useMemo(
|
|
239
|
+
() => crew.map((face) => ({ ...face, word: null })),
|
|
240
|
+
[crew],
|
|
241
|
+
);
|
|
191
242
|
|
|
192
243
|
const closeCompose = useCallback(() => setComposing(false), []);
|
|
193
244
|
|
|
245
|
+
// The room always opens on Now. A thread left open would greet the next click
|
|
246
|
+
// on the pill, and a History one would not be found once its pages stop being read.
|
|
247
|
+
const fold = () => {
|
|
248
|
+
setOpen(false);
|
|
249
|
+
setComposing(false);
|
|
250
|
+
setPicked(null);
|
|
251
|
+
setTab("now");
|
|
252
|
+
scroll.current = 0;
|
|
253
|
+
};
|
|
254
|
+
|
|
194
255
|
return (
|
|
195
256
|
// As wide as the resting pill may grow: 80% of the window. The open room
|
|
196
257
|
// keeps its own 33rem inside it.
|
|
@@ -200,44 +261,80 @@ export const BotRoom = memo(function BotRoom() {
|
|
|
200
261
|
{current ? (
|
|
201
262
|
<>
|
|
202
263
|
<ThreadHeader
|
|
203
|
-
|
|
264
|
+
thread={current}
|
|
204
265
|
onBack={() => setPicked(null)}
|
|
205
|
-
onClose={
|
|
266
|
+
onClose={fold}
|
|
206
267
|
/>
|
|
207
|
-
<Conversation
|
|
208
|
-
|
|
209
|
-
|
|
268
|
+
<Conversation
|
|
269
|
+
thread={current}
|
|
270
|
+
tab={sides[current.id] ?? null}
|
|
271
|
+
onTab={(bot) =>
|
|
272
|
+
setSides((was) => ({ ...was, [current.id]: bot }))
|
|
273
|
+
}
|
|
274
|
+
className="min-h-0 flex-1"
|
|
275
|
+
/>
|
|
276
|
+
<ThreadReply
|
|
277
|
+
thread={{
|
|
210
278
|
id: current.id,
|
|
211
279
|
label: current.label,
|
|
212
280
|
bot: current.bot.name,
|
|
213
|
-
ask:
|
|
281
|
+
ask: current.ask,
|
|
282
|
+
room: current.room,
|
|
214
283
|
}}
|
|
215
284
|
status={
|
|
216
285
|
current.status === "working" ? "running" : current.status
|
|
217
286
|
}
|
|
218
|
-
|
|
287
|
+
faces={rosterOf(current)}
|
|
288
|
+
to={sides[current.id] ?? current.bot.name}
|
|
219
289
|
className="mx-3 mb-2 shrink-0"
|
|
220
290
|
/>
|
|
221
291
|
</>
|
|
222
292
|
) : (
|
|
223
293
|
<>
|
|
224
294
|
<ListHeader
|
|
225
|
-
|
|
226
|
-
|
|
295
|
+
tab={tab}
|
|
296
|
+
current={now.length}
|
|
227
297
|
composing={composing}
|
|
298
|
+
onTab={(next) => {
|
|
299
|
+
setComposing(false);
|
|
300
|
+
setTab(next);
|
|
301
|
+
}}
|
|
228
302
|
onCompose={() => setComposing(true)}
|
|
229
|
-
onClose={() =>
|
|
230
|
-
composing ? setComposing(false) : setOpen(false)
|
|
231
|
-
}
|
|
303
|
+
onClose={() => (composing ? setComposing(false) : fold())}
|
|
232
304
|
/>
|
|
233
305
|
{composing ? (
|
|
234
306
|
<Compose bots={bots} onDone={closeCompose} />
|
|
307
|
+
) : tab === "history" ? (
|
|
308
|
+
<HistoryList
|
|
309
|
+
pages={history}
|
|
310
|
+
threads={past}
|
|
311
|
+
scroll={scroll}
|
|
312
|
+
onPick={setPicked}
|
|
313
|
+
/>
|
|
314
|
+
) : now.length ? (
|
|
315
|
+
<ThreadList threads={now} onPick={setPicked} />
|
|
235
316
|
) : newest.length ? (
|
|
236
|
-
<
|
|
317
|
+
<Quiet />
|
|
237
318
|
) : (
|
|
238
319
|
<Empty bots={bots} />
|
|
239
320
|
)}
|
|
240
|
-
{
|
|
321
|
+
{/* The pill's own row at the room's foot: open, it is the same object
|
|
322
|
+
grown. A hand-off speaks here rather than over the list's rows. */}
|
|
323
|
+
<CrewRow
|
|
324
|
+
crew={faces}
|
|
325
|
+
more={more}
|
|
326
|
+
bubble={null}
|
|
327
|
+
label="Fold the room away"
|
|
328
|
+
onClick={fold}
|
|
329
|
+
side={
|
|
330
|
+
bubble ? (
|
|
331
|
+
<Moment handoff={bubble} />
|
|
332
|
+
) : (
|
|
333
|
+
// Open, the room is the hand the pill offers.
|
|
334
|
+
<RoomState busy={busy} pending={pending} grown />
|
|
335
|
+
)
|
|
336
|
+
}
|
|
337
|
+
/>
|
|
241
338
|
</>
|
|
242
339
|
)}
|
|
243
340
|
</div>
|
|
@@ -245,14 +342,13 @@ export const BotRoom = memo(function BotRoom() {
|
|
|
245
342
|
<Chip
|
|
246
343
|
crew={crew}
|
|
247
344
|
more={more}
|
|
248
|
-
|
|
345
|
+
bubble={bubble}
|
|
249
346
|
bots={bots}
|
|
250
|
-
rows={
|
|
251
|
-
count={
|
|
347
|
+
rows={newest.filter((thread) => needsYou(thread) || isUnread(thread))}
|
|
348
|
+
count={threads.length}
|
|
252
349
|
busy={busy}
|
|
253
350
|
pending={pending}
|
|
254
351
|
unread={unread.length}
|
|
255
|
-
failed={failed}
|
|
256
352
|
composing={composing}
|
|
257
353
|
onCompose={() => setComposing(true)}
|
|
258
354
|
onCloseCompose={closeCompose}
|
|
@@ -260,13 +356,8 @@ export const BotRoom = memo(function BotRoom() {
|
|
|
260
356
|
setPicked(id);
|
|
261
357
|
setOpen(true);
|
|
262
358
|
}}
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
// question, or with none waiting, a single answer nobody has read.
|
|
266
|
-
const only = attention.length ? attention : unread;
|
|
267
|
-
if (!picked && only.length === 1) setPicked(only[0].id);
|
|
268
|
-
setOpen(true);
|
|
269
|
-
}}
|
|
359
|
+
// Always the list: a thread opens from its row, or when Thursday opens it.
|
|
360
|
+
onOpen={() => setOpen(true)}
|
|
270
361
|
/>
|
|
271
362
|
)}
|
|
272
363
|
</div>
|
|
@@ -275,8 +366,6 @@ export const BotRoom = memo(function BotRoom() {
|
|
|
275
366
|
|
|
276
367
|
/** Faces the row draws before the count takes over. */
|
|
277
368
|
const CREW_MAX = 10;
|
|
278
|
-
/** Stable identity for a row that can never have one up. */
|
|
279
|
-
const EMPTY_BUBBLES: Map<string, Handoff> = new Map();
|
|
280
369
|
/** How long a hand-off stays above a face, ms. */
|
|
281
370
|
const HANDOFF_MS = 3400;
|
|
282
371
|
/** A step is a glance, not a sentence. */
|
|
@@ -296,68 +385,250 @@ type CrewFace = {
|
|
|
296
385
|
standIn?: boolean;
|
|
297
386
|
};
|
|
298
387
|
|
|
388
|
+
/** The glyph a hand-off's words carry when they report how something stands. */
|
|
389
|
+
const SIGNS = {
|
|
390
|
+
done: Check,
|
|
391
|
+
cancelled: X,
|
|
392
|
+
question: CircleQuestionMark,
|
|
393
|
+
stopped: CirclePause,
|
|
394
|
+
resumed: RotateCw,
|
|
395
|
+
} as const;
|
|
396
|
+
|
|
397
|
+
/** The one status ink a sign can take: amber, for what waits on the user. */
|
|
398
|
+
const SIGN_INK: Partial<Record<keyof typeof SIGNS, string>> = {
|
|
399
|
+
question: WAITING_INK,
|
|
400
|
+
stopped: WAITING_INK,
|
|
401
|
+
};
|
|
402
|
+
|
|
299
403
|
/**
|
|
300
|
-
* Something
|
|
301
|
-
*
|
|
404
|
+
* Something that just happened: who spoke, to whom, and how it stands. A step
|
|
405
|
+
* is the bot working alone and belongs beside its face, not in a bubble.
|
|
302
406
|
*/
|
|
303
407
|
type Handoff = {
|
|
304
|
-
/** Whose face it points at. */
|
|
408
|
+
/** Whose face it points at: whoever spoke, or the bot the user reached. */
|
|
305
409
|
at: string;
|
|
306
|
-
/** Who
|
|
307
|
-
from: BotRef
|
|
410
|
+
/** Who spoke; Thursday stands for the user's side. */
|
|
411
|
+
from: BotRef;
|
|
412
|
+
/** The bots it reached, tucked behind the speaker's face. */
|
|
413
|
+
to: BotRef[];
|
|
308
414
|
text: string;
|
|
415
|
+
sign?: keyof typeof SIGNS;
|
|
309
416
|
};
|
|
310
417
|
|
|
418
|
+
/** A hand-off, and how much it matters against others from the same sync. */
|
|
419
|
+
type Happening = Handoff & { rank: number };
|
|
420
|
+
|
|
311
421
|
const clipWord = (text: string) => {
|
|
312
422
|
const one = plainText(text).replace(/\s+/g, " ").trim();
|
|
313
423
|
return one.length > WORD_MAX ? `${one.slice(0, WORD_MAX - 1)}…` : one;
|
|
314
424
|
};
|
|
315
425
|
|
|
316
426
|
/**
|
|
317
|
-
*
|
|
318
|
-
*
|
|
319
|
-
*
|
|
320
|
-
* two of them collide is folded into one before it gets here (BotRoom).
|
|
427
|
+
* The one hand-off up. A newer one replaces it rather than queueing: bubbles
|
|
428
|
+
* over neighbouring faces would cover each other, and the newest thing is the
|
|
429
|
+
* true one.
|
|
321
430
|
*/
|
|
322
|
-
function
|
|
323
|
-
const [up, setUp] = useState<
|
|
324
|
-
const
|
|
431
|
+
function useHandoff(): [Handoff | null, (one: Handoff) => void] {
|
|
432
|
+
const [up, setUp] = useState<Handoff | null>(null);
|
|
433
|
+
const timer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
325
434
|
|
|
326
435
|
useEffect(
|
|
327
436
|
() => () => {
|
|
328
|
-
|
|
437
|
+
if (timer.current) clearTimeout(timer.current);
|
|
329
438
|
},
|
|
330
439
|
[],
|
|
331
440
|
);
|
|
332
441
|
|
|
333
442
|
const show = useCallback((one: Handoff) => {
|
|
334
|
-
setUp(
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
timers.current.delete(one.at);
|
|
341
|
-
setUp((was) => {
|
|
342
|
-
const next = new Map(was);
|
|
343
|
-
next.delete(one.at);
|
|
344
|
-
return next;
|
|
345
|
-
});
|
|
346
|
-
}, HANDOFF_MS),
|
|
347
|
-
);
|
|
443
|
+
setUp(one);
|
|
444
|
+
if (timer.current) clearTimeout(timer.current);
|
|
445
|
+
timer.current = setTimeout(() => {
|
|
446
|
+
timer.current = null;
|
|
447
|
+
setUp(null);
|
|
448
|
+
}, HANDOFF_MS);
|
|
348
449
|
}, []);
|
|
349
450
|
|
|
350
451
|
return [up, show];
|
|
351
452
|
}
|
|
352
453
|
|
|
454
|
+
/** Each room participant's state, keyed by thread and bot (one per bot: thread.query withLines). */
|
|
455
|
+
function participantStates(threads: ThreadView[]): Map<string, string> {
|
|
456
|
+
const out = new Map<string, string>();
|
|
457
|
+
for (const thread of threads) {
|
|
458
|
+
for (const one of thread.room.participants) {
|
|
459
|
+
out.set(`${thread.id}\n${one.bot}`, one.state);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
return out;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* What happened in one thread since the last sync, ranked so one bubble can
|
|
467
|
+
* stand for a sync: a failure, then a question, then an ending or a stop, then
|
|
468
|
+
* messages, then a job arriving, an answer, a bot finishing its part or going
|
|
469
|
+
* back to work.
|
|
470
|
+
*/
|
|
471
|
+
function happenedIn(
|
|
472
|
+
thread: ThreadView,
|
|
473
|
+
was: ThreadViewStatus | undefined,
|
|
474
|
+
had: Set<string>,
|
|
475
|
+
stood: Map<string, string>,
|
|
476
|
+
stands: Map<string, string>,
|
|
477
|
+
): Happening[] {
|
|
478
|
+
const out: Happening[] = [];
|
|
479
|
+
const fresh = thread.lines.filter((line) => !had.has(line.id));
|
|
480
|
+
const own = thread.bot;
|
|
481
|
+
|
|
482
|
+
// Bot to bot. A giver handing work to several at once is one bubble naming them all.
|
|
483
|
+
const rounds = new Map<string, Chatter[]>();
|
|
484
|
+
for (const line of fresh) {
|
|
485
|
+
if (line.kind === "user") {
|
|
486
|
+
out.push({
|
|
487
|
+
rank: 1,
|
|
488
|
+
at: line.bot.name,
|
|
489
|
+
from: THURSDAY,
|
|
490
|
+
to: [line.bot],
|
|
491
|
+
text: clipWord(line.text),
|
|
492
|
+
});
|
|
493
|
+
} else if (line.kind === "stop") {
|
|
494
|
+
out.push({
|
|
495
|
+
rank: 3,
|
|
496
|
+
at: line.bot.name,
|
|
497
|
+
from: line.bot,
|
|
498
|
+
to: [],
|
|
499
|
+
sign: "stopped",
|
|
500
|
+
text: leadOf(line.text),
|
|
501
|
+
});
|
|
502
|
+
} else if (line.kind === "ask" && line.to?.name === THURSDAY.name) {
|
|
503
|
+
out.push(
|
|
504
|
+
line.question
|
|
505
|
+
? {
|
|
506
|
+
rank: 4,
|
|
507
|
+
at: line.bot.name,
|
|
508
|
+
from: line.bot,
|
|
509
|
+
to: [],
|
|
510
|
+
sign: "question",
|
|
511
|
+
text: `asks you · ${clipWord(line.text)}`,
|
|
512
|
+
}
|
|
513
|
+
: {
|
|
514
|
+
rank: 2,
|
|
515
|
+
at: line.bot.name,
|
|
516
|
+
from: line.bot,
|
|
517
|
+
to: [THURSDAY],
|
|
518
|
+
text: clipWord(line.text),
|
|
519
|
+
},
|
|
520
|
+
);
|
|
521
|
+
} else if (line.kind === "ask" && line.to) {
|
|
522
|
+
rounds.set(line.bot.name, [...(rounds.get(line.bot.name) ?? []), line]);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
for (const [giver, round] of rounds) {
|
|
526
|
+
const reached = new Map<string, BotRef>();
|
|
527
|
+
for (const line of round) {
|
|
528
|
+
if (line.to) reached.set(line.to.name, line.to);
|
|
529
|
+
}
|
|
530
|
+
out.push({
|
|
531
|
+
rank: 2,
|
|
532
|
+
at: giver,
|
|
533
|
+
from: round[0].bot,
|
|
534
|
+
to: [...reached.values()],
|
|
535
|
+
text:
|
|
536
|
+
round.length === 1
|
|
537
|
+
? clipWord(round[0].text)
|
|
538
|
+
: `sent ${round.length} parts out`,
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// Another bot done with its part and no last word; the thread's own ending is the job's.
|
|
543
|
+
for (const bot of rosterOf(thread)) {
|
|
544
|
+
const key = `${thread.id}\n${bot.name}`;
|
|
545
|
+
if (
|
|
546
|
+
bot.name !== own.name &&
|
|
547
|
+
stood.get(key) === "running" &&
|
|
548
|
+
stands.get(key) === "done" &&
|
|
549
|
+
!fresh.some(
|
|
550
|
+
(line) =>
|
|
551
|
+
line.bot.name === bot.name &&
|
|
552
|
+
(line.kind === "ask" || line.kind === "say"),
|
|
553
|
+
)
|
|
554
|
+
) {
|
|
555
|
+
out.push({
|
|
556
|
+
rank: 1,
|
|
557
|
+
at: bot.name,
|
|
558
|
+
from: bot,
|
|
559
|
+
to: [],
|
|
560
|
+
sign: "done",
|
|
561
|
+
text: "finished its part",
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
if (!was) {
|
|
567
|
+
if (thread.status === "working") {
|
|
568
|
+
out.push({
|
|
569
|
+
rank: 1,
|
|
570
|
+
at: own.name,
|
|
571
|
+
from: THURSDAY,
|
|
572
|
+
to: [own],
|
|
573
|
+
text: `took on “${clipWord(thread.label)}”`,
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
return out;
|
|
577
|
+
}
|
|
578
|
+
if (was === thread.status) return out;
|
|
579
|
+
const label = clipWord(thread.label);
|
|
580
|
+
if (thread.status === "cancelled") {
|
|
581
|
+
out.push({
|
|
582
|
+
rank: 3,
|
|
583
|
+
at: own.name,
|
|
584
|
+
from: own,
|
|
585
|
+
to: [],
|
|
586
|
+
sign: "cancelled",
|
|
587
|
+
text: `stopped · ${label}`,
|
|
588
|
+
});
|
|
589
|
+
} else if (thread.status === "done") {
|
|
590
|
+
out.push({
|
|
591
|
+
rank: 3,
|
|
592
|
+
at: own.name,
|
|
593
|
+
from: own,
|
|
594
|
+
to: [],
|
|
595
|
+
sign: "done",
|
|
596
|
+
text: `done · ${label}`,
|
|
597
|
+
});
|
|
598
|
+
} else if (thread.status === "working") {
|
|
599
|
+
// Words from the user already have their bubble.
|
|
600
|
+
if (!fresh.some((line) => line.kind === "user")) {
|
|
601
|
+
out.push({
|
|
602
|
+
rank: 1,
|
|
603
|
+
at: own.name,
|
|
604
|
+
from: own,
|
|
605
|
+
to: [],
|
|
606
|
+
sign: "resumed",
|
|
607
|
+
text: "picked it back up",
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
} else if (
|
|
611
|
+
isAppStop(thread.ask) &&
|
|
612
|
+
!out.some((one) => one.sign === "question" || one.sign === "stopped")
|
|
613
|
+
) {
|
|
614
|
+
// A stop with no stop line of its own: the turn limit, or a room gone idle
|
|
615
|
+
out.push({
|
|
616
|
+
rank: 3,
|
|
617
|
+
at: own.name,
|
|
618
|
+
from: own,
|
|
619
|
+
to: [],
|
|
620
|
+
sign: "stopped",
|
|
621
|
+
text: "paused",
|
|
622
|
+
});
|
|
623
|
+
}
|
|
624
|
+
return out;
|
|
625
|
+
}
|
|
626
|
+
|
|
353
627
|
/** The step a bot is on, as the model labelled it. */
|
|
354
628
|
function wordOf(line: Chatter | null): string | null {
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
return clipWord(line.tool.note ?? `${line.tool.name} · ${line.tool.input}`);
|
|
359
|
-
}
|
|
360
|
-
return line.kind === "say" ? clipWord(line.text) : null;
|
|
629
|
+
return line?.kind === "tool" || line?.kind === "say"
|
|
630
|
+
? clipWord(stepOf(line))
|
|
631
|
+
: null;
|
|
361
632
|
}
|
|
362
633
|
|
|
363
634
|
/**
|
|
@@ -366,21 +637,21 @@ function wordOf(line: Chatter | null): string | null {
|
|
|
366
637
|
* Anyone moving sorts to the front, so the count at the tail only ever hides
|
|
367
638
|
* idle bots — a bot with something to say always has a face to say it from,
|
|
368
639
|
* which is what lets a hand-off point at one. A bot that spoke inside somebody
|
|
369
|
-
* else's job is in the room too, whether or not it owns a
|
|
640
|
+
* else's job is in the room too, whether or not it owns a thread (latestPerBot).
|
|
370
641
|
*/
|
|
371
642
|
function crewOf(
|
|
372
643
|
bots: Bot[] | undefined,
|
|
373
|
-
|
|
644
|
+
threads: ThreadView[],
|
|
374
645
|
): { crew: CrewFace[]; more: number } {
|
|
375
646
|
const live = new Map<string, { waiting: boolean; word: string | null }>();
|
|
376
|
-
for (const entry of latestPerBot(
|
|
377
|
-
const {
|
|
647
|
+
for (const entry of latestPerBot(threads)) {
|
|
648
|
+
const { thread, bot, line } = entry;
|
|
378
649
|
// Waiting outranks working: a bot that stopped to ask is not on a step.
|
|
379
|
-
if (
|
|
650
|
+
if (thread.status === "waiting" && thread.bot.name === bot.name) {
|
|
380
651
|
live.set(bot.name, { waiting: true, word: null });
|
|
381
652
|
continue;
|
|
382
653
|
}
|
|
383
|
-
if (
|
|
654
|
+
if (thread.status !== "working" || live.get(bot.name)?.waiting) continue;
|
|
384
655
|
live.set(bot.name, { waiting: false, word: wordOf(line) });
|
|
385
656
|
}
|
|
386
657
|
|
|
@@ -429,79 +700,38 @@ function crewOf(
|
|
|
429
700
|
};
|
|
430
701
|
}
|
|
431
702
|
|
|
432
|
-
|
|
433
|
-
function askFor(task: TaskView): TaskView["ask"] {
|
|
434
|
-
if (!task.ask?.question) return task.ask;
|
|
435
|
-
const last = task.lines.at(-1);
|
|
436
|
-
return last && isOutcome(last) && last.text === task.ask.question
|
|
437
|
-
? { ...task.ask, question: "" }
|
|
438
|
-
: task.ask;
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
/**
|
|
442
|
-
* A task that cannot move until the user answers: a question, or a stop the app made.
|
|
443
|
-
*
|
|
444
|
-
* A finished job is not on this list. It has nothing for the user to do, and
|
|
445
|
-
* counting it here made the chip hold itself open over an answer nobody had to
|
|
446
|
-
* act on. An answer says itself once, in the sentence, and then it is a row in
|
|
447
|
-
* the room like every other one.
|
|
448
|
-
*/
|
|
449
|
-
const needsYou = (task: TaskView) => task.status === "waiting";
|
|
703
|
+
const needsYou = needsThreadReply;
|
|
450
704
|
|
|
451
705
|
/** An ending nobody has opened. It needs the user too, to read rather than to answer. */
|
|
452
|
-
const isUnread = (
|
|
453
|
-
|
|
706
|
+
const isUnread = (thread: ThreadView) =>
|
|
707
|
+
thread.status === "done" && !thread.seen;
|
|
454
708
|
|
|
455
709
|
/**
|
|
456
710
|
* What the room itself is doing, and nothing else — the right side of the pill.
|
|
457
711
|
*
|
|
458
|
-
*
|
|
459
|
-
* running
|
|
460
|
-
*
|
|
712
|
+
* It says two things only, in muted ink: someone waits on the user, or work is
|
|
713
|
+
* running. Endings say nothing here — a result or a failure grows a row above
|
|
714
|
+
* the pill, and that row is the notice. With nothing going on and nothing
|
|
715
|
+
* grown, the pill offers a hand; with a row grown and nothing going on, it is
|
|
716
|
+
* quiet.
|
|
461
717
|
*/
|
|
462
718
|
function restingState({
|
|
463
|
-
count,
|
|
464
719
|
busy,
|
|
465
720
|
pending,
|
|
466
|
-
|
|
467
|
-
failed,
|
|
721
|
+
grown,
|
|
468
722
|
}: {
|
|
469
|
-
count: number;
|
|
470
723
|
busy: number;
|
|
471
724
|
pending: number;
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
failed: number;
|
|
475
|
-
}): { text: string; tone: string; shine: ShinyTone | null } {
|
|
725
|
+
grown: boolean;
|
|
726
|
+
}): { text: string; shine: boolean; spin?: boolean } | null {
|
|
476
727
|
if (pending > 0)
|
|
477
728
|
return {
|
|
478
729
|
text: pending === 1 ? "waiting on you" : `${pending} waiting on you`,
|
|
479
|
-
|
|
480
|
-
shine: "waiting",
|
|
730
|
+
shine: true,
|
|
481
731
|
};
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
return {
|
|
486
|
-
text: failed === 1 ? "1 failed" : `${failed} failed`,
|
|
487
|
-
tone: "text-destructive",
|
|
488
|
-
shine: null,
|
|
489
|
-
};
|
|
490
|
-
if (unread > 0)
|
|
491
|
-
return {
|
|
492
|
-
text: unread === 1 ? "1 new answer" : `${unread} new answers`,
|
|
493
|
-
tone: WAITING_INK,
|
|
494
|
-
shine: null,
|
|
495
|
-
};
|
|
496
|
-
if (busy > 0)
|
|
497
|
-
return {
|
|
498
|
-
text: busy === 1 ? "working" : `${busy} running`,
|
|
499
|
-
tone: "text-muted-foreground",
|
|
500
|
-
shine: "muted",
|
|
501
|
-
};
|
|
502
|
-
if (count > 0)
|
|
503
|
-
return { text: "all done", tone: "text-muted-foreground", shine: null };
|
|
504
|
-
return { text: "no jobs yet", tone: "text-muted-foreground", shine: null };
|
|
732
|
+
if (busy > 0) return { text: "working", shine: true, spin: true };
|
|
733
|
+
if (!grown) return { text: "Need a hand?", shine: false };
|
|
734
|
+
return null;
|
|
505
735
|
}
|
|
506
736
|
|
|
507
737
|
/**
|
|
@@ -514,10 +744,8 @@ function restingState({
|
|
|
514
744
|
* takes the glyph away. Anything that passes between two parties is neither, so
|
|
515
745
|
* it rides above a face in a bubble.
|
|
516
746
|
*
|
|
517
|
-
*
|
|
518
|
-
*
|
|
519
|
-
* something to answer and leave when it is answered. The pill's own click opens
|
|
520
|
-
* the room, to read.
|
|
747
|
+
* Questions and unread results grow above the row until answered or opened.
|
|
748
|
+
* The pill's own click opens the room to read the full history.
|
|
521
749
|
*
|
|
522
750
|
* The corner radius does not animate with the height: interpolating a pill radius
|
|
523
751
|
* down to a card radius while the box is also resizing warps the corners in flight.
|
|
@@ -525,14 +753,13 @@ function restingState({
|
|
|
525
753
|
function Chip({
|
|
526
754
|
crew,
|
|
527
755
|
more,
|
|
528
|
-
|
|
756
|
+
bubble,
|
|
529
757
|
bots,
|
|
530
758
|
rows,
|
|
531
759
|
count,
|
|
532
760
|
busy,
|
|
533
761
|
pending,
|
|
534
762
|
unread,
|
|
535
|
-
failed,
|
|
536
763
|
composing,
|
|
537
764
|
onCompose,
|
|
538
765
|
onCloseCompose,
|
|
@@ -541,23 +768,22 @@ function Chip({
|
|
|
541
768
|
}: {
|
|
542
769
|
crew: CrewFace[];
|
|
543
770
|
more: number;
|
|
544
|
-
|
|
771
|
+
/** The hand-off up, if any (useHandoff). */
|
|
772
|
+
bubble: Handoff | null;
|
|
545
773
|
bots?: Bot[];
|
|
546
|
-
/**
|
|
547
|
-
rows:
|
|
774
|
+
/** Open questions and unread endings, newest first. */
|
|
775
|
+
rows: ThreadView[];
|
|
548
776
|
count: number;
|
|
549
777
|
busy: number;
|
|
550
778
|
pending: number;
|
|
551
779
|
unread: number;
|
|
552
|
-
failed: number;
|
|
553
780
|
composing: boolean;
|
|
554
781
|
onCompose: () => void;
|
|
555
782
|
onCloseCompose: () => void;
|
|
556
783
|
onPick: (id: string) => void;
|
|
557
784
|
onOpen: () => void;
|
|
558
785
|
}) {
|
|
559
|
-
const
|
|
560
|
-
const grown = composing || pending > 0;
|
|
786
|
+
const grown = composing || rows.length > 0;
|
|
561
787
|
|
|
562
788
|
return (
|
|
563
789
|
<div
|
|
@@ -568,7 +794,7 @@ function Chip({
|
|
|
568
794
|
// A card with questions in it keeps the room's width: only the resting
|
|
569
795
|
// row grows with the corner, and a reply's long lines would stretch it.
|
|
570
796
|
grown
|
|
571
|
-
? "
|
|
797
|
+
? "w-132 rounded-3xl shadow-lg shadow-black/8"
|
|
572
798
|
: "rounded-full shadow-sm shadow-black/3",
|
|
573
799
|
)}
|
|
574
800
|
>
|
|
@@ -587,17 +813,28 @@ function Chip({
|
|
|
587
813
|
) : (
|
|
588
814
|
<>
|
|
589
815
|
<p className="flex items-center gap-2 px-3 py-2 font-mono text-[10px] tracking-wide text-muted-foreground">
|
|
590
|
-
<span className="flex-1">
|
|
816
|
+
<span className="flex-1">
|
|
817
|
+
{[
|
|
818
|
+
pending > 0
|
|
819
|
+
? `${pending} ${pending === 1 ? "needs" : "need"} a reply`
|
|
820
|
+
: "",
|
|
821
|
+
unread > 0
|
|
822
|
+
? `${unread} new ${unread === 1 ? "result" : "results"}`
|
|
823
|
+
: "",
|
|
824
|
+
]
|
|
825
|
+
.filter(Boolean)
|
|
826
|
+
.join(" · ")}
|
|
827
|
+
</span>
|
|
591
828
|
<ComposeButton onClick={onCompose} />
|
|
592
829
|
</p>
|
|
593
830
|
{/* px-1: a row keeps its own 8px, so its mark lands on the rail while
|
|
594
831
|
the shape it lights up on hover stays inside the card's corners */}
|
|
595
|
-
<div className="px-1.5 pb-2">
|
|
596
|
-
{rows.map((
|
|
597
|
-
<
|
|
598
|
-
key={
|
|
599
|
-
|
|
600
|
-
onPick={() => onPick(
|
|
832
|
+
<div className="max-h-[45vh] overflow-y-auto px-1.5 pb-2">
|
|
833
|
+
{rows.map((thread) => (
|
|
834
|
+
<ThreadRow
|
|
835
|
+
key={thread.id}
|
|
836
|
+
thread={thread}
|
|
837
|
+
onPick={() => onPick(thread.id)}
|
|
601
838
|
/>
|
|
602
839
|
))}
|
|
603
840
|
</div>
|
|
@@ -608,64 +845,100 @@ function Chip({
|
|
|
608
845
|
</div>
|
|
609
846
|
|
|
610
847
|
{/* The pill row. Same geometry either way, so the card shrinks into it. */}
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
>
|
|
621
|
-
<Crew crew={crew} more={more} bubbles={bubbles} />
|
|
622
|
-
|
|
623
|
-
{/* The room's own state and its glyph: one group, and the only thing on
|
|
624
|
-
the right. `ml-auto` keeps it there when the crew says nothing. */}
|
|
625
|
-
<span className="ml-auto flex h-7 shrink-0 items-center gap-1.5">
|
|
626
|
-
{state.shine ? (
|
|
627
|
-
<ShinyText
|
|
628
|
-
text={state.text}
|
|
629
|
-
tone={state.shine}
|
|
630
|
-
speed={2.6}
|
|
631
|
-
className="block truncate text-[14px] leading-5 tracking-[-0.15px]"
|
|
632
|
-
/>
|
|
633
|
-
) : (
|
|
634
|
-
<span
|
|
635
|
-
key={state.text}
|
|
636
|
-
className={cn(
|
|
637
|
-
"block animate-in truncate text-[14px] leading-5 tracking-[-0.15px] fade-in duration-300",
|
|
638
|
-
state.tone,
|
|
639
|
-
)}
|
|
640
|
-
>
|
|
641
|
-
{state.text}
|
|
642
|
-
</span>
|
|
643
|
-
)}
|
|
644
|
-
{/* Always 16px, empty or not: a glyph that comes and goes moves the
|
|
645
|
-
sentence's right end even when the sentence has not changed. */}
|
|
646
|
-
<span className="grid size-4 shrink-0 place-items-center">
|
|
647
|
-
{pending === 0 && unread === 0 && busy > 0 && (
|
|
648
|
-
<Loader2 className="size-4 animate-spin text-muted-foreground/70" />
|
|
649
|
-
)}
|
|
650
|
-
{pending === 0 &&
|
|
651
|
-
failed === 0 &&
|
|
652
|
-
(unread > 0 || (busy === 0 && count > 0)) && (
|
|
653
|
-
<Check className="size-4 text-muted-foreground/60" />
|
|
654
|
-
)}
|
|
655
|
-
</span>
|
|
656
|
-
</span>
|
|
657
|
-
</button>
|
|
658
|
-
|
|
848
|
+
<CrewRow
|
|
849
|
+
crew={crew}
|
|
850
|
+
more={more}
|
|
851
|
+
// Grown, the rows already say what needs the user, and a bubble would cover them.
|
|
852
|
+
bubble={grown ? null : bubble}
|
|
853
|
+
label={count ? `Threads (${count})` : "Bots"}
|
|
854
|
+
onClick={onOpen}
|
|
855
|
+
side={<RoomState busy={busy} pending={pending} grown={grown} />}
|
|
856
|
+
>
|
|
659
857
|
{composing && (
|
|
660
858
|
<RoundButton onClick={onCloseCompose} label="Cancel the message">
|
|
661
859
|
<X className="size-3.5" />
|
|
662
860
|
</RoundButton>
|
|
663
861
|
)}
|
|
664
|
-
</
|
|
862
|
+
</CrewRow>
|
|
863
|
+
</div>
|
|
864
|
+
);
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
/**
|
|
868
|
+
* The pill's row: who is here on the left, what the room is doing on the right.
|
|
869
|
+
* The folded chip draws it and so does the open room's foot, so opening the room
|
|
870
|
+
* reads as the pill growing rather than as a card swapped in for it.
|
|
871
|
+
*
|
|
872
|
+
* px-3 is the chip's one rail: the faces here, the section line and every row's
|
|
873
|
+
* mark all start at 12px, and the trailing glyph ends at 12px — which is why the
|
|
874
|
+
* well carries no box of its own.
|
|
875
|
+
*/
|
|
876
|
+
function CrewRow({
|
|
877
|
+
crew,
|
|
878
|
+
more,
|
|
879
|
+
bubble,
|
|
880
|
+
side,
|
|
881
|
+
label,
|
|
882
|
+
onClick,
|
|
883
|
+
children,
|
|
884
|
+
}: {
|
|
885
|
+
crew: CrewFace[];
|
|
886
|
+
more: number;
|
|
887
|
+
bubble: Handoff | null;
|
|
888
|
+
/** The right side: the room's state, or what just happened. */
|
|
889
|
+
side: ReactNode;
|
|
890
|
+
label: string;
|
|
891
|
+
onClick: () => void;
|
|
892
|
+
/** Beside the row's button rather than inside it. */
|
|
893
|
+
children?: ReactNode;
|
|
894
|
+
}) {
|
|
895
|
+
return (
|
|
896
|
+
<div className="flex shrink-0 items-center gap-2 px-3 py-1.5">
|
|
897
|
+
<button
|
|
898
|
+
type="button"
|
|
899
|
+
onClick={onClick}
|
|
900
|
+
aria-label={label}
|
|
901
|
+
className="flex min-w-0 flex-1 items-center gap-2 rounded-full text-left outline-none focus-visible:ring-3 focus-visible:ring-ring/50"
|
|
902
|
+
>
|
|
903
|
+
<Crew crew={crew} more={more} bubble={bubble} />
|
|
904
|
+
{side}
|
|
905
|
+
</button>
|
|
906
|
+
{children}
|
|
665
907
|
</div>
|
|
666
908
|
);
|
|
667
909
|
}
|
|
668
910
|
|
|
911
|
+
/**
|
|
912
|
+
* The room's own state and its glyph: one group, and the only thing on the right.
|
|
913
|
+
* `ml-auto` keeps it there when the crew says nothing.
|
|
914
|
+
*/
|
|
915
|
+
function RoomState(props: { busy: number; pending: number; grown: boolean }) {
|
|
916
|
+
const state = restingState(props);
|
|
917
|
+
if (!state) return null;
|
|
918
|
+
return (
|
|
919
|
+
<span className="ml-auto flex h-7 shrink-0 items-center gap-1.5">
|
|
920
|
+
{state.shine ? (
|
|
921
|
+
<ShinyText
|
|
922
|
+
text={state.text}
|
|
923
|
+
speed={2.6}
|
|
924
|
+
className="block truncate text-[14px] leading-5 tracking-[-0.15px]"
|
|
925
|
+
/>
|
|
926
|
+
) : (
|
|
927
|
+
<span
|
|
928
|
+
key={state.text}
|
|
929
|
+
className="block animate-in truncate text-[14px] leading-5 tracking-[-0.15px] text-muted-foreground fade-in duration-300"
|
|
930
|
+
>
|
|
931
|
+
{state.text}
|
|
932
|
+
</span>
|
|
933
|
+
)}
|
|
934
|
+
{/* No empty slot: words without a spinner end at the pill's edge. */}
|
|
935
|
+
{state.spin && (
|
|
936
|
+
<Loader2 className="size-4 shrink-0 animate-spin text-muted-foreground/70" />
|
|
937
|
+
)}
|
|
938
|
+
</span>
|
|
939
|
+
);
|
|
940
|
+
}
|
|
941
|
+
|
|
669
942
|
/** Starts a job. Lives in the header of whichever list is on screen. */
|
|
670
943
|
function ComposeButton({ onClick }: { onClick: () => void }) {
|
|
671
944
|
return (
|
|
@@ -698,7 +971,7 @@ function RoundButton({
|
|
|
698
971
|
|
|
699
972
|
/**
|
|
700
973
|
* Handing a bot a job without the call in the room: pick one, write, send
|
|
701
|
-
* (bot.action
|
|
974
|
+
* (bot.action startThreadAction). The message is the whole request — there is no
|
|
702
975
|
* conversation to draw the rest from, which is why the box asks for a sentence.
|
|
703
976
|
*
|
|
704
977
|
* Nothing announces itself from here. The job shows up as a row on the next poll
|
|
@@ -707,9 +980,9 @@ function RoundButton({
|
|
|
707
980
|
function Compose({ bots, onDone }: { bots?: Bot[]; onDone: () => void }) {
|
|
708
981
|
const [picked, setPicked] = useState<BotRef | null>(null);
|
|
709
982
|
const [draft, setDraft] = useState("");
|
|
710
|
-
const [start, starting] = useServerAction(
|
|
983
|
+
const [start, starting] = useServerAction(startThreadAction, {
|
|
711
984
|
onOk: () => {
|
|
712
|
-
revalidate(queryKey.
|
|
985
|
+
revalidate(queryKey.threads);
|
|
713
986
|
onDone();
|
|
714
987
|
},
|
|
715
988
|
});
|
|
@@ -749,10 +1022,10 @@ function Compose({ bots, onDone }: { bots?: Bot[]; onDone: () => void }) {
|
|
|
749
1022
|
<BotMark
|
|
750
1023
|
size={22}
|
|
751
1024
|
seed={bot.name}
|
|
752
|
-
vary={bot.name}
|
|
753
1025
|
color={bot.icon?.color}
|
|
754
1026
|
shape={bot.icon?.shape}
|
|
755
1027
|
outline={bot.icon?.outline}
|
|
1028
|
+
paint={bot.icon?.paint}
|
|
756
1029
|
className="shrink-0"
|
|
757
1030
|
/>
|
|
758
1031
|
<span className="min-w-0 flex-1 truncate text-[13px]">
|
|
@@ -782,10 +1055,10 @@ function Compose({ bots, onDone }: { bots?: Bot[]; onDone: () => void }) {
|
|
|
782
1055
|
<BotMark
|
|
783
1056
|
size={22}
|
|
784
1057
|
seed={picked.name}
|
|
785
|
-
vary={picked.name}
|
|
786
1058
|
color={picked.icon?.color}
|
|
787
1059
|
shape={picked.icon?.shape}
|
|
788
1060
|
outline={picked.icon?.outline}
|
|
1061
|
+
paint={picked.icon?.paint}
|
|
789
1062
|
className="shrink-0"
|
|
790
1063
|
/>
|
|
791
1064
|
<span className="min-w-0 flex-1 truncate text-[12.5px] font-medium">
|
|
@@ -851,9 +1124,6 @@ const GHOSTS: CrewFace[] = ["alto", "brio", "cinder", "delta"].map(
|
|
|
851
1124
|
/** A single face reads as one worker, not as a crew; below this the row is padded out. */
|
|
852
1125
|
const FLOOR = 3;
|
|
853
1126
|
|
|
854
|
-
/** The awake halo, in the mark's own view-box units: about 2px of blur at 28px. */
|
|
855
|
-
const AWAKE_GLOW = 20;
|
|
856
|
-
|
|
857
1127
|
/**
|
|
858
1128
|
* The crew, and what each of them is doing.
|
|
859
1129
|
*
|
|
@@ -862,24 +1132,23 @@ const AWAKE_GLOW = 20;
|
|
|
862
1132
|
* is on that step. Both are derived — there is no queue and no cap, because
|
|
863
1133
|
* every bot that is working is saying something true at the same time.
|
|
864
1134
|
*
|
|
865
|
-
* A face that is moving is awake: lifted
|
|
866
|
-
*
|
|
867
|
-
*
|
|
1135
|
+
* A face that is moving is awake: lifted and swollen a little. That layer alone
|
|
1136
|
+
* survives every collision — three bots at once are three awake faces, with no
|
|
1137
|
+
* order to decide.
|
|
868
1138
|
*/
|
|
869
1139
|
function Crew({
|
|
870
1140
|
crew,
|
|
871
1141
|
more,
|
|
872
|
-
|
|
1142
|
+
bubble,
|
|
873
1143
|
}: {
|
|
874
1144
|
crew: CrewFace[];
|
|
875
1145
|
more: number;
|
|
876
|
-
/**
|
|
877
|
-
|
|
1146
|
+
/** The hand-off up, drawn over the face it points at. */
|
|
1147
|
+
bubble: Handoff | null;
|
|
878
1148
|
}) {
|
|
879
1149
|
return (
|
|
880
1150
|
<span className="flex min-w-0 shrink items-center">
|
|
881
1151
|
{crew.map((face, index) => {
|
|
882
|
-
const bubble = bubbles.get(face.name) ?? null;
|
|
883
1152
|
return (
|
|
884
1153
|
<Fragment key={face.name}>
|
|
885
1154
|
<span
|
|
@@ -887,19 +1156,23 @@ function Crew({
|
|
|
887
1156
|
// these are not circles. Earlier faces sit on top, so the dot on a
|
|
888
1157
|
// waiting face is never covered by its neighbour.
|
|
889
1158
|
className={cn(
|
|
890
|
-
"relative shrink-0 transition-[margin
|
|
1159
|
+
"relative shrink-0 transition-[margin] duration-500 ease-out",
|
|
891
1160
|
// A word to the left has already broken the shingle.
|
|
892
1161
|
index > 0 && !crew[index - 1].word && "-ml-2",
|
|
893
1162
|
face.standIn && "opacity-35",
|
|
894
|
-
face.awake && "-translate-y-0.5 scale-110",
|
|
895
1163
|
)}
|
|
896
1164
|
style={{ zIndex: crew.length - index }}
|
|
897
1165
|
>
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
1166
|
+
<span
|
|
1167
|
+
// The lift is the face's alone, so an awake face does not swell its bubble.
|
|
1168
|
+
className={cn(
|
|
1169
|
+
"flex transition-transform duration-500 ease-out",
|
|
1170
|
+
face.awake && "-translate-y-0.5 scale-110",
|
|
1171
|
+
)}
|
|
1172
|
+
>
|
|
1173
|
+
<CrewMark face={face} />
|
|
1174
|
+
</span>
|
|
1175
|
+
{bubble?.at === face.name && <HandoffBubble handoff={bubble} />}
|
|
903
1176
|
</span>
|
|
904
1177
|
{face.word && (
|
|
905
1178
|
// No box: the shine is what says this is happening right now, so a
|
|
@@ -932,65 +1205,117 @@ function Crew({
|
|
|
932
1205
|
);
|
|
933
1206
|
}
|
|
934
1207
|
|
|
935
|
-
/**
|
|
936
|
-
* One crew face. Awake, it wears a halo in its own colour, so who is moving
|
|
937
|
-
* reads before any word does; the options object is memoized because a new one
|
|
938
|
-
* every render re-derives the silhouette behind it.
|
|
939
|
-
*/
|
|
1208
|
+
/** One crew face. Who is moving reads from the lift its row gives it. */
|
|
940
1209
|
function CrewMark({ face }: { face: CrewFace }) {
|
|
941
|
-
const glow = useMemo(
|
|
942
|
-
() => (face.awake ? { glow: AWAKE_GLOW } : undefined),
|
|
943
|
-
[face.awake],
|
|
944
|
-
);
|
|
945
1210
|
return (
|
|
946
1211
|
<BotMark
|
|
947
1212
|
size={28}
|
|
948
1213
|
seed={face.name}
|
|
949
|
-
vary={face.name}
|
|
950
1214
|
color={face.icon?.color}
|
|
951
1215
|
shape={face.icon?.shape}
|
|
952
1216
|
outline={face.icon?.outline}
|
|
1217
|
+
paint={face.icon?.paint}
|
|
953
1218
|
notify={face.waiting}
|
|
954
|
-
options={glow}
|
|
955
1219
|
/>
|
|
956
1220
|
);
|
|
957
1221
|
}
|
|
958
1222
|
|
|
959
1223
|
/**
|
|
960
|
-
*
|
|
961
|
-
* it away — Thursday, another bot, or nobody at all when it was you.
|
|
1224
|
+
* What just happened, over the face of whoever spoke: the faces, then the words.
|
|
962
1225
|
*
|
|
963
1226
|
* It sits outside the pill's box on purpose, in space the corner is not using;
|
|
964
1227
|
* the chip cannot clip its own children while one is up.
|
|
965
1228
|
*/
|
|
966
1229
|
function HandoffBubble({ handoff }: { handoff: Handoff }) {
|
|
967
1230
|
return (
|
|
968
|
-
<span className="pointer-events-none absolute bottom-[calc(100%+
|
|
969
|
-
<span className="flex
|
|
970
|
-
{handoff
|
|
971
|
-
<ThursdayMark size={14} className="shrink-0 opacity-75" />
|
|
972
|
-
) : (
|
|
973
|
-
handoff.from && (
|
|
974
|
-
<BotMark
|
|
975
|
-
size={14}
|
|
976
|
-
seed={handoff.from.name}
|
|
977
|
-
vary={handoff.from.name}
|
|
978
|
-
color={handoff.from.icon?.color}
|
|
979
|
-
shape={handoff.from.icon?.shape}
|
|
980
|
-
outline={handoff.from.icon?.outline}
|
|
981
|
-
className="shrink-0 opacity-75"
|
|
982
|
-
/>
|
|
983
|
-
)
|
|
984
|
-
)}
|
|
985
|
-
{handoff.text}
|
|
1231
|
+
<span className="pointer-events-none absolute bottom-[calc(100%+10px)] left-1/2 flex w-max -translate-x-1/2 animate-in flex-col items-center fade-in zoom-in-95 duration-200">
|
|
1232
|
+
<span className="flex rounded-full bg-background py-2 pr-3.5 pl-2.5 shadow-lg shadow-black/10 ring-1 ring-border">
|
|
1233
|
+
<HandoffWords handoff={handoff} />
|
|
986
1234
|
</span>
|
|
987
1235
|
{/* Two triangles: the ring's, then the fill's a pixel over it. */}
|
|
988
|
-
<span className="-mt-px size-0 border-x-[
|
|
989
|
-
<span className="-mt-[
|
|
1236
|
+
<span className="-mt-px size-0 border-x-[6px] border-t-[7px] border-x-transparent border-t-border" />
|
|
1237
|
+
<span className="-mt-[7.5px] size-0 border-x-[6px] border-t-[7px] border-x-transparent border-t-background" />
|
|
1238
|
+
</span>
|
|
1239
|
+
);
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
/** A hand-off in the open room's foot, where the room's width holds it and no bubble covers the list. */
|
|
1243
|
+
function Moment({ handoff }: { handoff: Handoff }) {
|
|
1244
|
+
return (
|
|
1245
|
+
<span className="ml-auto flex h-7 min-w-0 shrink animate-in items-center pl-4 fade-in duration-200">
|
|
1246
|
+
<HandoffWords handoff={handoff} />
|
|
1247
|
+
</span>
|
|
1248
|
+
);
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
/**
|
|
1252
|
+
* A hand-off's faces, sign and words. A sign that waits on the user takes the
|
|
1253
|
+
* waiting ink, like every other status.
|
|
1254
|
+
*/
|
|
1255
|
+
function HandoffWords({ handoff }: { handoff: Handoff }) {
|
|
1256
|
+
const Sign = handoff.sign ? SIGNS[handoff.sign] : null;
|
|
1257
|
+
const ink = handoff.sign ? SIGN_INK[handoff.sign] : undefined;
|
|
1258
|
+
return (
|
|
1259
|
+
<span
|
|
1260
|
+
className={cn(
|
|
1261
|
+
"flex min-w-0 items-center gap-2 text-[13px] leading-[18px] tracking-[-0.1px] whitespace-nowrap",
|
|
1262
|
+
ink,
|
|
1263
|
+
)}
|
|
1264
|
+
>
|
|
1265
|
+
<Party from={handoff.from} to={handoff.to} />
|
|
1266
|
+
{Sign && (
|
|
1267
|
+
<Sign
|
|
1268
|
+
className={cn(
|
|
1269
|
+
"size-3.5 shrink-0",
|
|
1270
|
+
!ink && "text-muted-foreground/70",
|
|
1271
|
+
)}
|
|
1272
|
+
/>
|
|
1273
|
+
)}
|
|
1274
|
+
<span className="min-w-0 truncate">{handoff.text}</span>
|
|
1275
|
+
</span>
|
|
1276
|
+
);
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
/**
|
|
1280
|
+
* Whoever spoke in front and the bots it reached tucked behind, overlapped the
|
|
1281
|
+
* way the crew row overlaps faces. Nothing is drawn between them: the order says
|
|
1282
|
+
* who spoke, and a bubble's tail points at the same face.
|
|
1283
|
+
*/
|
|
1284
|
+
function Party({ from, to }: { from: BotRef; to: BotRef[] }) {
|
|
1285
|
+
const faces = [from, ...to];
|
|
1286
|
+
return (
|
|
1287
|
+
<span className="flex shrink-0 items-center">
|
|
1288
|
+
{faces.map((bot, index) => (
|
|
1289
|
+
<span
|
|
1290
|
+
key={bot.name}
|
|
1291
|
+
className={cn("relative flex", index > 0 && "-ml-1.25")}
|
|
1292
|
+
style={{ zIndex: faces.length - index }}
|
|
1293
|
+
>
|
|
1294
|
+
<Speaker bot={bot} />
|
|
1295
|
+
</span>
|
|
1296
|
+
))}
|
|
990
1297
|
</span>
|
|
991
1298
|
);
|
|
992
1299
|
}
|
|
993
1300
|
|
|
1301
|
+
/** A face in a bubble: Thursday's own mark for the user's side, else the bot's. */
|
|
1302
|
+
function Speaker({ bot }: { bot: BotRef }) {
|
|
1303
|
+
return bot.name === THURSDAY.name ? (
|
|
1304
|
+
<ThursdayMark size={18} className="shrink-0" />
|
|
1305
|
+
) : (
|
|
1306
|
+
<BotMark
|
|
1307
|
+
size={18}
|
|
1308
|
+
seed={bot.name}
|
|
1309
|
+
color={bot.icon?.color}
|
|
1310
|
+
shape={bot.icon?.shape}
|
|
1311
|
+
outline={bot.icon?.outline}
|
|
1312
|
+
paint={bot.icon?.paint}
|
|
1313
|
+
notify={false}
|
|
1314
|
+
className="shrink-0"
|
|
1315
|
+
/>
|
|
1316
|
+
);
|
|
1317
|
+
}
|
|
1318
|
+
|
|
994
1319
|
/**
|
|
995
1320
|
* Folds the room; shared by both headers. An X on its own filled circle, not a
|
|
996
1321
|
* chevron: in the thread header it sits a few pixels from the back arrow, and two
|
|
@@ -1010,27 +1335,47 @@ function FoldButton({ onClick }: { onClick: () => void }) {
|
|
|
1010
1335
|
);
|
|
1011
1336
|
}
|
|
1012
1337
|
|
|
1338
|
+
/** The room's two lists. */
|
|
1339
|
+
type RoomTab = "now" | "history";
|
|
1340
|
+
|
|
1341
|
+
/** The two lists as pills, drawn like a thread's bot tabs; the compose "+" and the fold beside them. */
|
|
1013
1342
|
function ListHeader({
|
|
1014
|
-
|
|
1015
|
-
|
|
1343
|
+
tab,
|
|
1344
|
+
current,
|
|
1016
1345
|
composing,
|
|
1346
|
+
onTab,
|
|
1017
1347
|
onCompose,
|
|
1018
1348
|
onClose,
|
|
1019
1349
|
}: {
|
|
1020
|
-
|
|
1021
|
-
|
|
1350
|
+
tab: RoomTab;
|
|
1351
|
+
/** Rows on Now. */
|
|
1352
|
+
current: number;
|
|
1022
1353
|
composing: boolean;
|
|
1354
|
+
onTab: (tab: RoomTab) => void;
|
|
1023
1355
|
onCompose: () => void;
|
|
1024
1356
|
onClose: () => void;
|
|
1025
1357
|
}) {
|
|
1026
1358
|
return (
|
|
1027
|
-
<div className="flex items-center gap-2
|
|
1028
|
-
<
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1359
|
+
<div className="flex items-center gap-2 pt-3 pr-3.5 pb-1.5 pl-2.5">
|
|
1360
|
+
<Tabs
|
|
1361
|
+
value={composing ? null : tab}
|
|
1362
|
+
onValueChange={(value) => onTab(value as RoomTab)}
|
|
1363
|
+
className="gap-0"
|
|
1364
|
+
>
|
|
1365
|
+
<TabsList className="gap-1 rounded-none bg-transparent p-0 group-data-horizontal/tabs:h-auto">
|
|
1366
|
+
<TabsTrigger value="now" className={TAB}>
|
|
1367
|
+
Now
|
|
1368
|
+
{current > 0 && (
|
|
1369
|
+
<span className="font-mono text-[10px] font-normal text-muted-foreground tabular-nums">
|
|
1370
|
+
{current}
|
|
1371
|
+
</span>
|
|
1372
|
+
)}
|
|
1373
|
+
</TabsTrigger>
|
|
1374
|
+
<TabsTrigger value="history" className={TAB}>
|
|
1375
|
+
History
|
|
1376
|
+
</TabsTrigger>
|
|
1377
|
+
</TabsList>
|
|
1378
|
+
</Tabs>
|
|
1034
1379
|
<span className="flex-1" />
|
|
1035
1380
|
{/* Same rule as the chip: whichever list is on screen carries the "+". */}
|
|
1036
1381
|
{!composing && <ComposeButton onClick={onCompose} />}
|
|
@@ -1040,11 +1385,11 @@ function ListHeader({
|
|
|
1040
1385
|
}
|
|
1041
1386
|
|
|
1042
1387
|
function ThreadHeader({
|
|
1043
|
-
|
|
1388
|
+
thread,
|
|
1044
1389
|
onBack,
|
|
1045
1390
|
onClose,
|
|
1046
1391
|
}: {
|
|
1047
|
-
|
|
1392
|
+
thread: ThreadView;
|
|
1048
1393
|
onBack: () => void;
|
|
1049
1394
|
onClose: () => void;
|
|
1050
1395
|
}) {
|
|
@@ -1060,32 +1405,41 @@ function ThreadHeader({
|
|
|
1060
1405
|
</button>
|
|
1061
1406
|
<BotMark
|
|
1062
1407
|
size={22}
|
|
1063
|
-
seed={
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1408
|
+
seed={thread.bot.name}
|
|
1409
|
+
color={thread.bot.icon?.color}
|
|
1410
|
+
shape={thread.bot.icon?.shape}
|
|
1411
|
+
outline={thread.bot.icon?.outline}
|
|
1412
|
+
paint={thread.bot.icon?.paint}
|
|
1068
1413
|
notify={false}
|
|
1069
1414
|
/>
|
|
1070
1415
|
<span className="min-w-0 flex-1 truncate text-[13px] font-medium">
|
|
1071
|
-
{
|
|
1416
|
+
{thread.label}
|
|
1072
1417
|
<span
|
|
1073
|
-
title={
|
|
1418
|
+
title={thread.id}
|
|
1074
1419
|
className="ml-1.5 font-mono text-[10px] font-normal text-muted-foreground/60"
|
|
1075
1420
|
>
|
|
1076
|
-
{
|
|
1421
|
+
{thread.id.slice(0, 8)}
|
|
1077
1422
|
</span>
|
|
1078
1423
|
</span>
|
|
1079
|
-
<
|
|
1080
|
-
<Context task={task} />
|
|
1081
|
-
<State task={task} />
|
|
1424
|
+
<ThreadFacts thread={thread} />
|
|
1082
1425
|
<FoldButton onClick={onClose} />
|
|
1083
1426
|
</div>
|
|
1084
1427
|
);
|
|
1085
1428
|
}
|
|
1086
1429
|
|
|
1087
|
-
/**
|
|
1088
|
-
function
|
|
1430
|
+
/** Where a thread stands: tokens, the context meter, the status word. The room's header and the Threads reader draw it. */
|
|
1431
|
+
export function ThreadFacts({ thread }: { thread: ThreadView }) {
|
|
1432
|
+
return (
|
|
1433
|
+
<>
|
|
1434
|
+
<Tokens usage={thread.tokens} />
|
|
1435
|
+
<Context thread={thread} />
|
|
1436
|
+
<State thread={thread} />
|
|
1437
|
+
</>
|
|
1438
|
+
);
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
/** Tokens this thread has used: the total shown, the split in the title. */
|
|
1442
|
+
function Tokens({ usage }: { usage: ThreadView["tokens"] }) {
|
|
1089
1443
|
const total = usage.input + usage.output;
|
|
1090
1444
|
if (!total) return null;
|
|
1091
1445
|
return (
|
|
@@ -1099,8 +1453,8 @@ function Tokens({ usage }: { usage: TaskView["tokens"] }) {
|
|
|
1099
1453
|
}
|
|
1100
1454
|
|
|
1101
1455
|
/** Context fill of the last step, not the running total. At the budget the bot compacts (bot.run compact) and the bar drops. */
|
|
1102
|
-
function Context({
|
|
1103
|
-
const { contextTokens: used, contextBudget: budget } =
|
|
1456
|
+
function Context({ thread }: { thread: ThreadView }) {
|
|
1457
|
+
const { contextTokens: used, contextBudget: budget } = thread;
|
|
1104
1458
|
if (!used || !budget) return null;
|
|
1105
1459
|
const full = Math.min(1, used / budget);
|
|
1106
1460
|
|
|
@@ -1120,21 +1474,21 @@ function Context({ task }: { task: TaskView }) {
|
|
|
1120
1474
|
);
|
|
1121
1475
|
}
|
|
1122
1476
|
|
|
1123
|
-
/** Only waiting (amber)
|
|
1124
|
-
const STATE_LOOK: Record<
|
|
1477
|
+
/** Only waiting (amber) carries colour. */
|
|
1478
|
+
const STATE_LOOK: Record<ThreadViewStatus, string> = {
|
|
1125
1479
|
working: "text-muted-foreground",
|
|
1126
1480
|
waiting: WAITING_INK,
|
|
1127
1481
|
done: "text-foreground",
|
|
1128
|
-
|
|
1482
|
+
cancelled: "text-muted-foreground",
|
|
1129
1483
|
};
|
|
1130
1484
|
|
|
1131
|
-
function State({
|
|
1485
|
+
function State({ thread }: { thread: ThreadView }) {
|
|
1132
1486
|
const look =
|
|
1133
|
-
|
|
1487
|
+
thread.status === "done" && thread.seen
|
|
1134
1488
|
? "text-muted-foreground"
|
|
1135
|
-
: STATE_LOOK[
|
|
1489
|
+
: STATE_LOOK[thread.status];
|
|
1136
1490
|
|
|
1137
|
-
if (
|
|
1491
|
+
if (thread.status === "working") {
|
|
1138
1492
|
return (
|
|
1139
1493
|
<ShinyText
|
|
1140
1494
|
text="working"
|
|
@@ -1151,96 +1505,192 @@ function State({ task }: { task: TaskView }) {
|
|
|
1151
1505
|
look,
|
|
1152
1506
|
)}
|
|
1153
1507
|
>
|
|
1154
|
-
{
|
|
1508
|
+
{thread.status}
|
|
1155
1509
|
</span>
|
|
1156
1510
|
);
|
|
1157
1511
|
}
|
|
1158
1512
|
|
|
1513
|
+
/** A room that has never had a thread. The faces are already in the room's foot. */
|
|
1159
1514
|
function Empty({ bots }: { bots?: Bot[] }) {
|
|
1160
|
-
const { crew, more } = crewOf(bots, []);
|
|
1161
1515
|
return (
|
|
1162
|
-
<
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
</p>
|
|
1168
|
-
) : (
|
|
1169
|
-
<p className="text-[12px] text-muted-foreground">
|
|
1170
|
-
Thursday hands work to {DEFAULT_BOT.name} until you make bots of your
|
|
1171
|
-
own in Settings › Bots.
|
|
1172
|
-
</p>
|
|
1173
|
-
)}
|
|
1174
|
-
</div>
|
|
1516
|
+
<p className="px-6 pt-4 pb-5 text-center text-[12px] text-muted-foreground">
|
|
1517
|
+
{bots?.length
|
|
1518
|
+
? "Nothing handed over yet — ask for something that takes a while."
|
|
1519
|
+
: `Thursday hands work to ${DEFAULT_BOT.name} until you make bots of your own in Settings › Bots.`}
|
|
1520
|
+
</p>
|
|
1175
1521
|
);
|
|
1176
1522
|
}
|
|
1177
1523
|
|
|
1178
|
-
/**
|
|
1179
|
-
function
|
|
1524
|
+
/** Now with nothing on it, when the room has had threads: what ended is one tab over. */
|
|
1525
|
+
function Quiet() {
|
|
1180
1526
|
return (
|
|
1181
|
-
<
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
onClick={() => openSettings("tasks")}
|
|
1185
|
-
className="flex items-center gap-1 rounded-md px-1.5 py-1 font-mono text-[10px] text-muted-foreground outline-none transition-colors hover:text-foreground focus-visible:ring-3 focus-visible:ring-ring/50"
|
|
1186
|
-
>
|
|
1187
|
-
<History className="size-3" />
|
|
1188
|
-
History
|
|
1189
|
-
</button>
|
|
1190
|
-
</div>
|
|
1527
|
+
<p className="px-6 pt-4 pb-5 text-center text-[12px] text-muted-foreground">
|
|
1528
|
+
Nothing running or waiting.
|
|
1529
|
+
</p>
|
|
1191
1530
|
);
|
|
1192
1531
|
}
|
|
1193
1532
|
|
|
1194
|
-
/**
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
id: "working",
|
|
1203
|
-
label: "working",
|
|
1204
|
-
holds: (task) => task.status === "working",
|
|
1205
|
-
},
|
|
1206
|
-
{ id: "done", label: "done", holds: () => true },
|
|
1207
|
-
];
|
|
1208
|
-
|
|
1209
|
-
function TaskList({
|
|
1210
|
-
tasks,
|
|
1533
|
+
/**
|
|
1534
|
+
* Every thread that has ended, newest first, grouped by day. Pages arrive as the
|
|
1535
|
+
* end of the list scrolls into view (listThreadHistory).
|
|
1536
|
+
*/
|
|
1537
|
+
function HistoryList({
|
|
1538
|
+
pages,
|
|
1539
|
+
threads,
|
|
1540
|
+
scroll,
|
|
1211
1541
|
onPick,
|
|
1212
1542
|
}: {
|
|
1213
|
-
|
|
1543
|
+
pages: ServerPages<Thread>;
|
|
1544
|
+
/** The ended threads among the loaded pages. */
|
|
1545
|
+
threads: ThreadView[];
|
|
1546
|
+
/** Where the list was left, restored when a thread opened from it closes. */
|
|
1547
|
+
scroll: RefObject<number>;
|
|
1214
1548
|
onPick: (id: string) => void;
|
|
1215
1549
|
}) {
|
|
1216
|
-
const
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1550
|
+
const restore = useCallback(
|
|
1551
|
+
(node: HTMLDivElement | null) => {
|
|
1552
|
+
if (node) node.scrollTop = scroll.current;
|
|
1553
|
+
},
|
|
1554
|
+
[scroll],
|
|
1555
|
+
);
|
|
1556
|
+
|
|
1557
|
+
const days: { day: string; rows: ThreadView[] }[] = [];
|
|
1558
|
+
for (const thread of threads) {
|
|
1559
|
+
const day = dayOf(thread.updatedAt);
|
|
1560
|
+
const last = days.at(-1);
|
|
1561
|
+
if (last?.day === day) last.rows.push(thread);
|
|
1562
|
+
else days.push({ day, rows: [thread] });
|
|
1222
1563
|
}
|
|
1223
1564
|
|
|
1224
1565
|
return (
|
|
1225
|
-
<div
|
|
1226
|
-
{
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1566
|
+
<div
|
|
1567
|
+
ref={restore}
|
|
1568
|
+
onScroll={(event) => {
|
|
1569
|
+
scroll.current = event.currentTarget.scrollTop;
|
|
1570
|
+
}}
|
|
1571
|
+
className="flex min-h-0 flex-1 flex-col gap-0.5 overflow-y-auto px-2 pt-1 pb-2 scrollbar-none"
|
|
1572
|
+
>
|
|
1573
|
+
{pages.error ? (
|
|
1574
|
+
<p className="px-2.5 py-3 text-[12px] text-destructive">
|
|
1575
|
+
{pages.error.message}
|
|
1576
|
+
</p>
|
|
1577
|
+
) : pages.isLoading ? (
|
|
1578
|
+
<>
|
|
1579
|
+
<GhostRow />
|
|
1580
|
+
<GhostRow />
|
|
1581
|
+
<GhostRow />
|
|
1582
|
+
</>
|
|
1583
|
+
) : (
|
|
1584
|
+
<>
|
|
1585
|
+
{days.map((group, at) => (
|
|
1586
|
+
<Fragment key={group.day}>
|
|
1587
|
+
<p
|
|
1588
|
+
className={cn(
|
|
1589
|
+
"px-2.5 pb-1 font-mono text-[10px] text-muted-foreground",
|
|
1590
|
+
at === 0 ? "pt-1" : "pt-3",
|
|
1591
|
+
)}
|
|
1592
|
+
>
|
|
1593
|
+
{group.day} · {group.rows.length}
|
|
1594
|
+
</p>
|
|
1595
|
+
{group.rows.map((thread) => (
|
|
1596
|
+
<ThreadRow
|
|
1597
|
+
key={thread.id}
|
|
1598
|
+
thread={thread}
|
|
1599
|
+
onPick={() => onPick(thread.id)}
|
|
1600
|
+
/>
|
|
1601
|
+
))}
|
|
1602
|
+
</Fragment>
|
|
1603
|
+
))}
|
|
1604
|
+
{pages.hasMore ? (
|
|
1605
|
+
<div ref={pages.sentinelRef}>
|
|
1606
|
+
<GhostRow />
|
|
1607
|
+
</div>
|
|
1608
|
+
) : (
|
|
1609
|
+
days.length === 0 && (
|
|
1610
|
+
<p className="px-6 pt-3 pb-4 text-center text-[12px] text-muted-foreground">
|
|
1611
|
+
Nothing has ended yet.
|
|
1612
|
+
</p>
|
|
1613
|
+
)
|
|
1614
|
+
)}
|
|
1615
|
+
</>
|
|
1616
|
+
)}
|
|
1617
|
+
</div>
|
|
1618
|
+
);
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
/** "today", "yesterday", "sep 14" — lower case, like the room's other group labels. */
|
|
1622
|
+
function dayOf(value: DateLike): string {
|
|
1623
|
+
const date = toDate(value);
|
|
1624
|
+
if (isToday(date)) return "today";
|
|
1625
|
+
if (isYesterday(date)) return "yesterday";
|
|
1626
|
+
return format(date, isThisYear(date) ? "MMM d" : "MMM d, yyyy").toLowerCase();
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
/** A row's shape while its page is on the way: the face, the label, the line. */
|
|
1630
|
+
function GhostRow() {
|
|
1631
|
+
return (
|
|
1632
|
+
<div className="flex items-center gap-2.5 px-2 py-1.5">
|
|
1633
|
+
<Skeleton className="size-8 shrink-0 rounded-[10px]" />
|
|
1634
|
+
<span className="flex h-9 min-w-0 flex-1 flex-col justify-center gap-1.5">
|
|
1635
|
+
<Skeleton className="h-3.5 w-2/5" />
|
|
1636
|
+
<Skeleton className="h-2.5 w-3/5" />
|
|
1637
|
+
</span>
|
|
1638
|
+
</div>
|
|
1639
|
+
);
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1642
|
+
/** List sections in order, split by what a thread asks of the user rather than by status. First match wins. */
|
|
1643
|
+
const GROUPS: {
|
|
1644
|
+
id: string;
|
|
1645
|
+
label: string;
|
|
1646
|
+
holds: (thread: ThreadView) => boolean;
|
|
1647
|
+
}[] = [
|
|
1648
|
+
{ id: "you", label: "needs you", holds: needsYou },
|
|
1649
|
+
{ id: "unread", label: "new results", holds: isUnread },
|
|
1650
|
+
{
|
|
1651
|
+
id: "working",
|
|
1652
|
+
label: "working",
|
|
1653
|
+
holds: (thread) => thread.status === "working",
|
|
1654
|
+
},
|
|
1655
|
+
{ id: "done", label: "done", holds: () => true },
|
|
1656
|
+
];
|
|
1657
|
+
|
|
1658
|
+
function ThreadList({
|
|
1659
|
+
threads,
|
|
1660
|
+
onPick,
|
|
1661
|
+
}: {
|
|
1662
|
+
threads: ThreadView[];
|
|
1663
|
+
onPick: (id: string) => void;
|
|
1664
|
+
}) {
|
|
1665
|
+
const bucket = new Map<string, ThreadView[]>();
|
|
1666
|
+
for (const thread of threads) {
|
|
1667
|
+
const group =
|
|
1668
|
+
GROUPS.find((one) => one.holds(thread)) ?? GROUPS[GROUPS.length - 1];
|
|
1669
|
+
const rows = bucket.get(group.id);
|
|
1670
|
+
if (rows) rows.push(thread);
|
|
1671
|
+
else bucket.set(group.id, [thread]);
|
|
1672
|
+
}
|
|
1673
|
+
|
|
1674
|
+
return (
|
|
1675
|
+
<div className="flex min-h-0 flex-1 flex-col gap-0.5 overflow-y-auto px-2 pt-1 pb-1 scrollbar-none">
|
|
1676
|
+
{GROUPS.map((group, at) => {
|
|
1677
|
+
const rows = bucket.get(group.id) ?? [];
|
|
1678
|
+
if (!rows.length) return null;
|
|
1679
|
+
return (
|
|
1680
|
+
<Fragment key={group.id}>
|
|
1681
|
+
<p
|
|
1682
|
+
className={cn(
|
|
1683
|
+
"px-2.5 pb-1 font-mono text-[10px] text-muted-foreground",
|
|
1684
|
+
at === 0 ? "pt-1" : "pt-3",
|
|
1235
1685
|
)}
|
|
1236
1686
|
>
|
|
1237
1687
|
{group.label} · {rows.length}
|
|
1238
1688
|
</p>
|
|
1239
|
-
{rows.map((
|
|
1240
|
-
<
|
|
1241
|
-
key={
|
|
1242
|
-
|
|
1243
|
-
onPick={() => onPick(
|
|
1689
|
+
{rows.map((thread) => (
|
|
1690
|
+
<ThreadRow
|
|
1691
|
+
key={thread.id}
|
|
1692
|
+
thread={thread}
|
|
1693
|
+
onPick={() => onPick(thread.id)}
|
|
1244
1694
|
/>
|
|
1245
1695
|
))}
|
|
1246
1696
|
</Fragment>
|
|
@@ -1250,19 +1700,35 @@ function TaskList({
|
|
|
1250
1700
|
);
|
|
1251
1701
|
}
|
|
1252
1702
|
|
|
1253
|
-
function
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1703
|
+
function ThreadRow({
|
|
1704
|
+
thread,
|
|
1705
|
+
onPick,
|
|
1706
|
+
}: {
|
|
1707
|
+
thread: ThreadView;
|
|
1708
|
+
onPick: () => void;
|
|
1709
|
+
}) {
|
|
1710
|
+
const attention = needsYou(thread);
|
|
1711
|
+
// secondLine runs plainText over the whole answer. Every sync rebuilds each ThreadView
|
|
1712
|
+
// (thread.store), so depend on the fields that change the line, not on `thread`.
|
|
1713
|
+
const last = thread.lines.at(-1);
|
|
1258
1714
|
const line = useMemo(
|
|
1259
|
-
() => secondLine(
|
|
1260
|
-
[
|
|
1715
|
+
() => secondLine(thread),
|
|
1716
|
+
[
|
|
1717
|
+
thread.status,
|
|
1718
|
+
thread.outcome,
|
|
1719
|
+
thread.seen,
|
|
1720
|
+
thread.ask,
|
|
1721
|
+
thread.room.questions,
|
|
1722
|
+
last?.id,
|
|
1723
|
+
],
|
|
1261
1724
|
);
|
|
1262
|
-
const [answer, answering] =
|
|
1725
|
+
const [answer, answering] = useAnswerThread();
|
|
1263
1726
|
const [sending, setSending] = useState<string | null>(null);
|
|
1264
1727
|
// Options are answered inline, without opening the thread.
|
|
1265
|
-
const options =
|
|
1728
|
+
const options =
|
|
1729
|
+
thread.status === "waiting" && isAppStop(thread.ask)
|
|
1730
|
+
? (thread.ask?.options ?? [])
|
|
1731
|
+
: [];
|
|
1266
1732
|
|
|
1267
1733
|
return (
|
|
1268
1734
|
// The row is not itself a button: the option buttons cannot nest inside one.
|
|
@@ -1274,21 +1740,33 @@ function TaskRow({ task, onPick }: { task: TaskView; onPick: () => void }) {
|
|
|
1274
1740
|
>
|
|
1275
1741
|
<span
|
|
1276
1742
|
className="relative flex size-8 shrink-0 items-center justify-center overflow-visible"
|
|
1277
|
-
title={
|
|
1743
|
+
title={thread.bot.name}
|
|
1278
1744
|
>
|
|
1279
1745
|
<BotMark
|
|
1280
1746
|
size={32}
|
|
1281
|
-
seed={
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
state={
|
|
1747
|
+
seed={thread.bot.name}
|
|
1748
|
+
color={thread.bot.icon?.color}
|
|
1749
|
+
shape={thread.bot.icon?.shape}
|
|
1750
|
+
outline={thread.bot.icon?.outline}
|
|
1751
|
+
paint={thread.bot.icon?.paint}
|
|
1752
|
+
state={thread.status === "working" ? "thinking" : "idle"}
|
|
1287
1753
|
notify={attention}
|
|
1754
|
+
crossed={thread.status === "cancelled"}
|
|
1288
1755
|
/>
|
|
1289
1756
|
</span>
|
|
1290
1757
|
|
|
1291
1758
|
<span className="min-w-0 flex-1">
|
|
1759
|
+
{(attention || isUnread(thread)) && (
|
|
1760
|
+
<span className="block text-[10px] font-medium text-foreground">
|
|
1761
|
+
{thread.room.questions.length
|
|
1762
|
+
? `${thread.room.questions.length === 1 ? "Question" : `${thread.room.questions.length} questions`} · ${[...new Set(thread.room.questions.map((question) => question.bot))].join(", ")}`
|
|
1763
|
+
: thread.status === "waiting"
|
|
1764
|
+
? isAppStop(thread.ask)
|
|
1765
|
+
? "Paused"
|
|
1766
|
+
: "Question"
|
|
1767
|
+
: "New result"}
|
|
1768
|
+
</span>
|
|
1769
|
+
)}
|
|
1292
1770
|
<span className="flex items-center justify-between gap-2">
|
|
1293
1771
|
<span
|
|
1294
1772
|
className={cn(
|
|
@@ -1296,27 +1774,28 @@ function TaskRow({ task, onPick }: { task: TaskView; onPick: () => void }) {
|
|
|
1296
1774
|
attention ? "text-foreground" : "text-foreground/80",
|
|
1297
1775
|
)}
|
|
1298
1776
|
>
|
|
1299
|
-
{
|
|
1777
|
+
{thread.label}
|
|
1300
1778
|
</span>
|
|
1301
|
-
<BotRoster bots={rosterOf(
|
|
1779
|
+
<BotRoster bots={rosterOf(thread)} />
|
|
1302
1780
|
<span className="flex-1" />
|
|
1303
1781
|
<span className="shrink-0 font-mono text-[11px] leading-4 text-muted-foreground/70 tabular-nums">
|
|
1304
|
-
{shortAgo(
|
|
1782
|
+
{shortAgo(thread.updatedAt)}
|
|
1305
1783
|
</span>
|
|
1306
1784
|
</span>
|
|
1307
1785
|
<span className="mt-px flex h-4 items-center gap-1.5">
|
|
1308
|
-
{
|
|
1786
|
+
{thread.status === "working" && (
|
|
1309
1787
|
<Loader2 className="size-3 shrink-0 animate-spin text-muted-foreground/70" />
|
|
1310
1788
|
)}
|
|
1311
1789
|
{/* Anything still moving says so by shining, here as in the thread
|
|
1312
|
-
(bot-tool) and the pill (
|
|
1313
|
-
{
|
|
1790
|
+
(bot-tool) and the pill (Chip). */}
|
|
1791
|
+
{thread.status === "working" && !attention ? (
|
|
1314
1792
|
<ShinyText
|
|
1315
1793
|
text={line.text}
|
|
1316
1794
|
speed={2.2}
|
|
1795
|
+
// The shine brings its own ink; only the placeholder's italic carries over.
|
|
1317
1796
|
className={cn(
|
|
1318
1797
|
"min-w-0 flex-1 truncate text-[12px] leading-4",
|
|
1319
|
-
line.tone,
|
|
1798
|
+
line.tone.includes("italic") && "italic",
|
|
1320
1799
|
)}
|
|
1321
1800
|
/>
|
|
1322
1801
|
) : (
|
|
@@ -1344,12 +1823,12 @@ function TaskRow({ task, onPick }: { task: TaskView; onPick: () => void }) {
|
|
|
1344
1823
|
disabled={answering}
|
|
1345
1824
|
onClick={async () => {
|
|
1346
1825
|
setSending(option);
|
|
1347
|
-
await answer(
|
|
1826
|
+
await answer(thread, option);
|
|
1348
1827
|
setSending(null);
|
|
1349
1828
|
}}
|
|
1350
|
-
className="h-7 gap-1.5 rounded-full border-
|
|
1829
|
+
className="h-7 gap-1.5 rounded-full border-border bg-background px-3 text-[12px]"
|
|
1351
1830
|
>
|
|
1352
|
-
{option ===
|
|
1831
|
+
{option === THREAD_CONTINUE && (
|
|
1353
1832
|
<ChevronsRight className="size-3.5 text-muted-foreground" />
|
|
1354
1833
|
)}
|
|
1355
1834
|
{option}
|
|
@@ -1361,37 +1840,37 @@ function TaskRow({ task, onPick }: { task: TaskView; onPick: () => void }) {
|
|
|
1361
1840
|
);
|
|
1362
1841
|
}
|
|
1363
1842
|
|
|
1364
|
-
/** Second row of a
|
|
1365
|
-
function secondLine(
|
|
1366
|
-
|
|
1843
|
+
/** Second row of a thread line: the question, the outcome, or the bot's last step. */
|
|
1844
|
+
function secondLine(thread: ThreadView): { text: string; tone: string } {
|
|
1845
|
+
const question = thread.room.questions[0];
|
|
1846
|
+
if (question) {
|
|
1847
|
+
return { text: plainText(question.text), tone: "text-foreground" };
|
|
1848
|
+
}
|
|
1849
|
+
if (thread.status === "waiting" && thread.ask) {
|
|
1367
1850
|
// A budget stop is not a question, but it waits on the user exactly as one
|
|
1368
1851
|
// does, so it carries the waiting colour too; only the words differ.
|
|
1369
1852
|
return {
|
|
1370
|
-
text: isAppStop(
|
|
1371
|
-
? plainText(
|
|
1372
|
-
:
|
|
1853
|
+
text: isAppStop(thread.ask)
|
|
1854
|
+
? plainText(thread.outcome ?? thread.ask.question)
|
|
1855
|
+
: thread.ask.question,
|
|
1373
1856
|
tone: WAITING_INK,
|
|
1374
1857
|
};
|
|
1375
1858
|
}
|
|
1376
|
-
// Reports are markdown; keep only the text.
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
return {
|
|
1381
|
-
text: plainText(task.outcome ?? "Failed"),
|
|
1382
|
-
tone: had ? "text-destructive/70" : "text-destructive",
|
|
1383
|
-
};
|
|
1859
|
+
// Reports are markdown; keep only the text. An ending the user has opened steps back.
|
|
1860
|
+
const had = thread.seen;
|
|
1861
|
+
if (thread.status === "cancelled") {
|
|
1862
|
+
return { text: "Stopped", tone: "text-muted-foreground" };
|
|
1384
1863
|
}
|
|
1385
|
-
if (
|
|
1864
|
+
if (thread.status === "done") {
|
|
1386
1865
|
return {
|
|
1387
|
-
text: plainText(
|
|
1866
|
+
text: plainText(thread.outcome ?? "Done"),
|
|
1388
1867
|
tone: had ? "text-muted-foreground" : "text-foreground",
|
|
1389
1868
|
};
|
|
1390
1869
|
}
|
|
1391
|
-
const last = lastSaid(
|
|
1870
|
+
const last = lastSaid(thread);
|
|
1392
1871
|
if (!last) {
|
|
1393
1872
|
return {
|
|
1394
|
-
text: `${
|
|
1873
|
+
text: `${thread.bot.name} is taking it on…`,
|
|
1395
1874
|
tone: "text-muted-foreground italic",
|
|
1396
1875
|
};
|
|
1397
1876
|
}
|
|
@@ -1405,30 +1884,89 @@ function secondLine(task: TaskView): { text: string; tone: string } {
|
|
|
1405
1884
|
};
|
|
1406
1885
|
}
|
|
1407
1886
|
|
|
1408
|
-
/**
|
|
1887
|
+
/**
|
|
1888
|
+
* The thread as it is drawn: without the questions still waiting on the user.
|
|
1889
|
+
* The reply sheet holds those (ThreadReply), and each joins the thread as a record
|
|
1890
|
+
* once answered. A room question is matched by who asked and what, since its ID
|
|
1891
|
+
* names the exchange rather than the thread line.
|
|
1892
|
+
*/
|
|
1893
|
+
function withoutOpenQuestions(thread: ThreadView): ThreadView {
|
|
1894
|
+
const open = new Set(
|
|
1895
|
+
thread.room.questions.map(
|
|
1896
|
+
(question) => `${question.bot}\n${question.text.trim()}`,
|
|
1897
|
+
),
|
|
1898
|
+
);
|
|
1899
|
+
if (!open.size) return thread;
|
|
1900
|
+
return {
|
|
1901
|
+
...thread,
|
|
1902
|
+
lines: thread.lines.filter(
|
|
1903
|
+
(line) =>
|
|
1904
|
+
!(
|
|
1905
|
+
line.kind === "ask" &&
|
|
1906
|
+
line.question &&
|
|
1907
|
+
line.to?.name === THURSDAY.name &&
|
|
1908
|
+
open.has(`${line.bot.name}\n${line.text.trim()}`)
|
|
1909
|
+
),
|
|
1910
|
+
),
|
|
1911
|
+
};
|
|
1912
|
+
}
|
|
1913
|
+
|
|
1914
|
+
/**
|
|
1915
|
+
* A whole thread as the conversation between its participants, from one bot's
|
|
1916
|
+
* tab (thread.store threadItems). Follows the newest line while the reader is at
|
|
1917
|
+
* the bottom. Also used by the Threads settings screen.
|
|
1918
|
+
*/
|
|
1409
1919
|
export function Conversation({
|
|
1410
|
-
|
|
1920
|
+
thread,
|
|
1921
|
+
tab = null,
|
|
1922
|
+
onTab,
|
|
1411
1923
|
className,
|
|
1924
|
+
tabsClassName,
|
|
1412
1925
|
}: {
|
|
1413
|
-
|
|
1926
|
+
thread: ThreadView;
|
|
1927
|
+
/** Another bot whose tab is open; null is the thread's own. */
|
|
1928
|
+
tab?: string | null;
|
|
1929
|
+
/** Keeps the picked tab. Without it the thread draws on its own bot's tab, with no tabs. */
|
|
1930
|
+
onTab?: (bot: string | null) => void;
|
|
1414
1931
|
className?: string;
|
|
1932
|
+
tabsClassName?: string;
|
|
1415
1933
|
}) {
|
|
1416
1934
|
const scroller = useRef<HTMLDivElement>(null);
|
|
1417
1935
|
const following = useRef(true);
|
|
1936
|
+
const roster = rosterOf(thread);
|
|
1937
|
+
const side =
|
|
1938
|
+
onTab && tab !== thread.bot.name && roster.some((bot) => bot.name === tab)
|
|
1939
|
+
? tab
|
|
1940
|
+
: null;
|
|
1941
|
+
const self = side ?? thread.bot.name;
|
|
1942
|
+
// Who is still at it says so at the end of the tab that holds them.
|
|
1943
|
+
const live = roster.filter(
|
|
1944
|
+
(bot) => (!side || bot.name === side) && standingOf(thread, bot.name),
|
|
1945
|
+
);
|
|
1946
|
+
const items = threadItems(withoutOpenQuestions(thread), self, live);
|
|
1418
1947
|
|
|
1419
|
-
// A different
|
|
1948
|
+
// A different thread, or another side of it, starts at its own end.
|
|
1420
1949
|
useEffect(() => {
|
|
1421
1950
|
following.current = true;
|
|
1422
|
-
}, [
|
|
1951
|
+
}, [thread.id, side]);
|
|
1423
1952
|
|
|
1424
1953
|
useEffect(() => {
|
|
1425
1954
|
const box = scroller.current;
|
|
1426
1955
|
if (box && following.current) box.scrollTop = box.scrollHeight;
|
|
1427
|
-
}, [
|
|
1956
|
+
}, [thread.lines, thread.status, side]);
|
|
1428
1957
|
|
|
1429
1958
|
return (
|
|
1430
1959
|
// One FileViewer per conversation, not per bubble.
|
|
1431
1960
|
<FileViewer>
|
|
1961
|
+
{onTab && (
|
|
1962
|
+
<ThreadTabs
|
|
1963
|
+
thread={thread}
|
|
1964
|
+
roster={roster}
|
|
1965
|
+
side={side}
|
|
1966
|
+
onPick={onTab}
|
|
1967
|
+
className={tabsClassName}
|
|
1968
|
+
/>
|
|
1969
|
+
)}
|
|
1432
1970
|
<div
|
|
1433
1971
|
ref={scroller}
|
|
1434
1972
|
onScroll={(event) => {
|
|
@@ -1437,71 +1975,397 @@ export function Conversation({
|
|
|
1437
1975
|
box.scrollHeight - box.scrollTop - box.clientHeight < 24;
|
|
1438
1976
|
}}
|
|
1439
1977
|
className={cn(
|
|
1440
|
-
"max-h-[64vh] space-y-3 overflow-y-auto px-4 pt-6 pb-4 mask-[linear-gradient(to_bottom,transparent,black_2rem)] scrollbar-none",
|
|
1978
|
+
"min-w-0 max-h-[64vh] space-y-3 overflow-y-auto px-4 pt-6 pb-4 mask-[linear-gradient(to_bottom,transparent,black_2rem)] scrollbar-none",
|
|
1441
1979
|
className,
|
|
1442
1980
|
)}
|
|
1443
1981
|
>
|
|
1444
|
-
<Request
|
|
1445
|
-
{
|
|
1982
|
+
{!side && <Request thread={thread} />}
|
|
1983
|
+
{items.map((item) =>
|
|
1446
1984
|
item.kind === "invite" ? (
|
|
1447
1985
|
<Invite key={item.key} from={item.from} to={item.to} />
|
|
1448
1986
|
) : (
|
|
1449
|
-
<
|
|
1987
|
+
<SpeakerTurn
|
|
1988
|
+
key={item.key}
|
|
1989
|
+
turn={item}
|
|
1990
|
+
thread={thread}
|
|
1991
|
+
self={self}
|
|
1992
|
+
/>
|
|
1450
1993
|
),
|
|
1451
1994
|
)}
|
|
1452
|
-
{task.status === "working" && <NextMove task={task} />}
|
|
1453
1995
|
</div>
|
|
1454
1996
|
</FileViewer>
|
|
1455
1997
|
);
|
|
1456
1998
|
}
|
|
1457
1999
|
|
|
1458
2000
|
/**
|
|
1459
|
-
* The
|
|
1460
|
-
*
|
|
1461
|
-
*
|
|
2001
|
+
* The thread's own bot first, where the thread opens and always there, then each
|
|
2002
|
+
* bot it brought in. A running bot's tab spins and one waiting on the user
|
|
2003
|
+
* carries its dot, so who is busy reads before any tab is opened.
|
|
1462
2004
|
*/
|
|
1463
|
-
function
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
2005
|
+
function ThreadTabs({
|
|
2006
|
+
thread,
|
|
2007
|
+
roster,
|
|
2008
|
+
side,
|
|
2009
|
+
onPick,
|
|
2010
|
+
className,
|
|
2011
|
+
}: {
|
|
2012
|
+
thread: ThreadView;
|
|
2013
|
+
roster: BotRef[];
|
|
2014
|
+
side: string | null;
|
|
2015
|
+
onPick: (bot: string | null) => void;
|
|
2016
|
+
className?: string;
|
|
2017
|
+
}) {
|
|
1470
2018
|
return (
|
|
1471
|
-
<
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
: `${task.bot.name} is taking it on…`
|
|
2019
|
+
<Tabs
|
|
2020
|
+
value={side ?? thread.bot.name}
|
|
2021
|
+
onValueChange={(value) =>
|
|
2022
|
+
onPick(value === thread.bot.name ? null : String(value))
|
|
1476
2023
|
}
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
2024
|
+
className={cn("shrink-0 gap-0 px-3 pt-1 pb-0.5", className)}
|
|
2025
|
+
>
|
|
2026
|
+
<TabsList className="w-full justify-start gap-1 overflow-x-auto rounded-none bg-transparent p-0 scrollbar-none group-data-horizontal/tabs:h-auto">
|
|
2027
|
+
{roster.map((bot) => {
|
|
2028
|
+
const standing = standingOf(thread, bot.name);
|
|
2029
|
+
return (
|
|
2030
|
+
<TabsTrigger
|
|
2031
|
+
key={bot.name}
|
|
2032
|
+
value={bot.name}
|
|
2033
|
+
className={cn(TAB, "pl-1.5")}
|
|
2034
|
+
>
|
|
2035
|
+
<BotMark
|
|
2036
|
+
size={16}
|
|
2037
|
+
seed={bot.name}
|
|
2038
|
+
color={bot.icon?.color}
|
|
2039
|
+
shape={bot.icon?.shape}
|
|
2040
|
+
outline={bot.icon?.outline}
|
|
2041
|
+
paint={bot.icon?.paint}
|
|
2042
|
+
notify={standing === "asking"}
|
|
2043
|
+
className="shrink-0"
|
|
2044
|
+
/>
|
|
2045
|
+
<span className="max-w-28 truncate">{bot.name}</span>
|
|
2046
|
+
{standing === "running" && (
|
|
2047
|
+
<Loader2 className="size-3 animate-spin text-muted-foreground/70" />
|
|
2048
|
+
)}
|
|
2049
|
+
</TabsTrigger>
|
|
2050
|
+
);
|
|
2051
|
+
})}
|
|
2052
|
+
</TabsList>
|
|
2053
|
+
</Tabs>
|
|
2054
|
+
);
|
|
2055
|
+
}
|
|
2056
|
+
|
|
2057
|
+
/** A tab as a pill that fills while its side is on screen, in place of the boxed look TabsTrigger brings. */
|
|
2058
|
+
const TAB =
|
|
2059
|
+
"h-7 flex-none rounded-full px-3 py-0 text-[12px] text-muted-foreground hover:bg-muted/60 data-active:bg-muted data-active:text-foreground group-data-[variant=default]/tabs-list:data-active:shadow-none dark:data-active:border-transparent dark:data-active:bg-muted";
|
|
2060
|
+
|
|
2061
|
+
/** Whether a bot is at work in this thread or waiting on the user's answer. */
|
|
2062
|
+
function standingOf(
|
|
2063
|
+
thread: ThreadView,
|
|
2064
|
+
bot: string,
|
|
2065
|
+
): "running" | "asking" | null {
|
|
2066
|
+
const { room } = thread;
|
|
2067
|
+
if (room.questions.some((question) => question.bot === bot)) return "asking";
|
|
2068
|
+
return thread.status === "working" &&
|
|
2069
|
+
room.participants.some((one) => one.bot === bot && one.state === "running")
|
|
2070
|
+
? "running"
|
|
2071
|
+
: null;
|
|
2072
|
+
}
|
|
2073
|
+
|
|
2074
|
+
/**
|
|
2075
|
+
* One speaker's entries in a row. The open tab's bot holds the left, its work in
|
|
2076
|
+
* full and its words on no surface; everyone else answers from the right — the
|
|
2077
|
+
* user's side in the dark bubble, other bots on `secondary` with their work
|
|
2078
|
+
* folded into rows. A message to a bot other than the tab's names it at its head,
|
|
2079
|
+
* once while the addressee stays the same.
|
|
2080
|
+
*/
|
|
2081
|
+
function SpeakerTurn({
|
|
2082
|
+
turn,
|
|
2083
|
+
thread,
|
|
2084
|
+
self,
|
|
2085
|
+
}: {
|
|
2086
|
+
turn: Extract<ThreadItem, { kind: "turn" }>;
|
|
2087
|
+
thread: ThreadView;
|
|
2088
|
+
/** The bot whose tab is open. */
|
|
2089
|
+
self: string;
|
|
2090
|
+
}) {
|
|
2091
|
+
const mine = turn.speaker.name === self;
|
|
2092
|
+
const surface: Surface = mine
|
|
2093
|
+
? "none"
|
|
2094
|
+
: turn.speaker.name === THURSDAY.name
|
|
2095
|
+
? "dark"
|
|
2096
|
+
: "secondary";
|
|
2097
|
+
let named: string | null = null;
|
|
2098
|
+
return (
|
|
2099
|
+
<div className="animate-in fade-in slide-in-from-bottom-2 duration-300">
|
|
2100
|
+
<Turn
|
|
2101
|
+
side={mine ? "start" : "end"}
|
|
2102
|
+
name={turn.speaker.name}
|
|
2103
|
+
mark={<TurnMark bot={turn.speaker} />}
|
|
2104
|
+
>
|
|
2105
|
+
{turn.entries.map((entry) => {
|
|
2106
|
+
if (entry.kind === "work") {
|
|
2107
|
+
return mine ? (
|
|
2108
|
+
<OwnWork
|
|
2109
|
+
key={entry.key}
|
|
2110
|
+
lines={entry.lines}
|
|
2111
|
+
trailing={entry.open}
|
|
2112
|
+
bot={turn.speaker}
|
|
2113
|
+
thread={thread}
|
|
2114
|
+
/>
|
|
2115
|
+
) : (
|
|
2116
|
+
<WorkRow
|
|
2117
|
+
key={entry.key}
|
|
2118
|
+
lines={entry.lines}
|
|
2119
|
+
trailing={entry.open}
|
|
2120
|
+
bot={turn.speaker}
|
|
2121
|
+
thread={thread}
|
|
2122
|
+
/>
|
|
2123
|
+
);
|
|
2124
|
+
}
|
|
2125
|
+
const to = heardBy(entry.line);
|
|
2126
|
+
const mention =
|
|
2127
|
+
to &&
|
|
2128
|
+
to.name !== turn.speaker.name &&
|
|
2129
|
+
to.name !== self &&
|
|
2130
|
+
to.name !== THURSDAY.name &&
|
|
2131
|
+
to.name !== named
|
|
2132
|
+
? to
|
|
2133
|
+
: null;
|
|
2134
|
+
if (to) named = to.name;
|
|
2135
|
+
return (
|
|
2136
|
+
<Message
|
|
2137
|
+
key={entry.key}
|
|
2138
|
+
line={entry.line}
|
|
2139
|
+
surface={surface}
|
|
2140
|
+
mention={mention}
|
|
2141
|
+
/>
|
|
2142
|
+
);
|
|
2143
|
+
})}
|
|
2144
|
+
</Turn>
|
|
2145
|
+
</div>
|
|
1480
2146
|
);
|
|
1481
2147
|
}
|
|
1482
2148
|
|
|
2149
|
+
/**
|
|
2150
|
+
* A bot's work between its messages as one row: what it did, counted, once it
|
|
2151
|
+
* has moved on, and the step it is on while it is still at it. It opens in
|
|
2152
|
+
* place to the steps, stops and words beside them.
|
|
2153
|
+
*/
|
|
2154
|
+
function WorkRow({
|
|
2155
|
+
lines,
|
|
2156
|
+
trailing,
|
|
2157
|
+
bot,
|
|
2158
|
+
thread,
|
|
2159
|
+
}: {
|
|
2160
|
+
lines: Chatter[];
|
|
2161
|
+
/** No message has followed it yet, so it may be what the bot is on now. */
|
|
2162
|
+
trailing: boolean;
|
|
2163
|
+
bot: BotRef;
|
|
2164
|
+
thread: ThreadView;
|
|
2165
|
+
}) {
|
|
2166
|
+
const [open, setOpen] = useState(false);
|
|
2167
|
+
const standing = trailing ? standingOf(thread, bot.name) : null;
|
|
2168
|
+
const counts = countsOf(lines);
|
|
2169
|
+
if (!standing && !counts) return null;
|
|
2170
|
+
const last = lines.findLast(
|
|
2171
|
+
(line) => line.kind === "tool" || line.kind === "say",
|
|
2172
|
+
);
|
|
2173
|
+
const shown = open && lines.length > 0;
|
|
2174
|
+
|
|
2175
|
+
return (
|
|
2176
|
+
<div
|
|
2177
|
+
className={cn(
|
|
2178
|
+
"flex min-w-0 flex-col rounded-2xl bg-muted/40 p-1",
|
|
2179
|
+
shown ? "w-full" : "w-fit max-w-full",
|
|
2180
|
+
)}
|
|
2181
|
+
>
|
|
2182
|
+
<button
|
|
2183
|
+
type="button"
|
|
2184
|
+
disabled={!lines.length}
|
|
2185
|
+
aria-expanded={shown}
|
|
2186
|
+
onClick={() => setOpen((was) => !was)}
|
|
2187
|
+
className="flex min-w-0 items-center gap-2 rounded-xl px-2.5 py-1.5 text-left outline-none transition-colors enabled:hover:bg-muted/60 focus-visible:ring-3 focus-visible:ring-ring/50"
|
|
2188
|
+
>
|
|
2189
|
+
{standing === "running" && (
|
|
2190
|
+
<Loader2 className="size-3 shrink-0 animate-spin text-muted-foreground/60" />
|
|
2191
|
+
)}
|
|
2192
|
+
{standing ? (
|
|
2193
|
+
<ShinyText
|
|
2194
|
+
text={
|
|
2195
|
+
standing === "asking"
|
|
2196
|
+
? "waiting on you"
|
|
2197
|
+
: last
|
|
2198
|
+
? stepOf(last)
|
|
2199
|
+
: "working"
|
|
2200
|
+
}
|
|
2201
|
+
tone={standing === "asking" ? "waiting" : "muted"}
|
|
2202
|
+
speed={2.4}
|
|
2203
|
+
className="min-w-0 truncate text-[12px] leading-4"
|
|
2204
|
+
/>
|
|
2205
|
+
) : (
|
|
2206
|
+
<span className="min-w-0 truncate font-mono text-[10px] leading-4 text-muted-foreground">
|
|
2207
|
+
{counts}
|
|
2208
|
+
</span>
|
|
2209
|
+
)}
|
|
2210
|
+
{lines.length > 0 && (
|
|
2211
|
+
<ChevronDown
|
|
2212
|
+
className={cn(
|
|
2213
|
+
"size-3 shrink-0 text-muted-foreground/50 transition-transform",
|
|
2214
|
+
shown && "rotate-180",
|
|
2215
|
+
)}
|
|
2216
|
+
/>
|
|
2217
|
+
)}
|
|
2218
|
+
</button>
|
|
2219
|
+
{shown && (
|
|
2220
|
+
<div className="flex min-w-0 flex-col gap-0.5 pt-0.5">
|
|
2221
|
+
{runs(lines).map((run) =>
|
|
2222
|
+
run.kind === "tools" ? (
|
|
2223
|
+
run.lines.map(
|
|
2224
|
+
(line) =>
|
|
2225
|
+
line.tool && (
|
|
2226
|
+
// All start collapsed; a running call expands itself (bot-tool Frame).
|
|
2227
|
+
<BotTool
|
|
2228
|
+
key={line.id}
|
|
2229
|
+
tool={line.tool}
|
|
2230
|
+
threadId={thread.id}
|
|
2231
|
+
collapsed
|
|
2232
|
+
/>
|
|
2233
|
+
),
|
|
2234
|
+
)
|
|
2235
|
+
) : run.kind === "stops" ? (
|
|
2236
|
+
<div key={run.key} className="px-1.5 py-1">
|
|
2237
|
+
<Stops lines={run.lines} />
|
|
2238
|
+
</div>
|
|
2239
|
+
) : (
|
|
2240
|
+
run.lines.map((line) => <Line key={line.id} line={line} inset />)
|
|
2241
|
+
),
|
|
2242
|
+
)}
|
|
2243
|
+
</div>
|
|
2244
|
+
)}
|
|
2245
|
+
</div>
|
|
2246
|
+
);
|
|
2247
|
+
}
|
|
2248
|
+
|
|
2249
|
+
/**
|
|
2250
|
+
* The open tab's bot's own work, in full: nobody opens a row to read what the
|
|
2251
|
+
* bot on screen did. While it is at it with nothing mid-call, a shining line says
|
|
2252
|
+
* so; a call mid-way already shines on its own row.
|
|
2253
|
+
*/
|
|
2254
|
+
function OwnWork({
|
|
2255
|
+
lines,
|
|
2256
|
+
trailing,
|
|
2257
|
+
bot,
|
|
2258
|
+
thread,
|
|
2259
|
+
}: {
|
|
2260
|
+
lines: Chatter[];
|
|
2261
|
+
/** No message has followed it yet, so it may be what the bot is on now. */
|
|
2262
|
+
trailing: boolean;
|
|
2263
|
+
bot: BotRef;
|
|
2264
|
+
thread: ThreadView;
|
|
2265
|
+
}) {
|
|
2266
|
+
const standing = trailing ? standingOf(thread, bot.name) : null;
|
|
2267
|
+
const calling = lines.some(
|
|
2268
|
+
(line) => line.tool && line.tool.results === undefined,
|
|
2269
|
+
);
|
|
2270
|
+
return (
|
|
2271
|
+
<>
|
|
2272
|
+
{runs(lines).map((run) =>
|
|
2273
|
+
run.kind === "tools" ? (
|
|
2274
|
+
<Steps key={run.key} lines={run.lines} threadId={thread.id} />
|
|
2275
|
+
) : run.kind === "stops" ? (
|
|
2276
|
+
<Stops key={run.key} lines={run.lines} />
|
|
2277
|
+
) : (
|
|
2278
|
+
run.lines.map((line) => <Line key={line.id} line={line} />)
|
|
2279
|
+
),
|
|
2280
|
+
)}
|
|
2281
|
+
{standing && !calling && (
|
|
2282
|
+
<ShinyText
|
|
2283
|
+
text={standing === "asking" ? "waiting on you" : "on the next step…"}
|
|
2284
|
+
tone={standing === "asking" ? "waiting" : "muted"}
|
|
2285
|
+
speed={2.2}
|
|
2286
|
+
className="block px-1 font-mono text-[10px]"
|
|
2287
|
+
/>
|
|
2288
|
+
)}
|
|
2289
|
+
</>
|
|
2290
|
+
);
|
|
2291
|
+
}
|
|
2292
|
+
|
|
2293
|
+
/**
|
|
2294
|
+
* A run of tool calls as one box of collapsed rows, so the length costs scroll,
|
|
2295
|
+
* not noise. A running call opens itself (bot-tool Frame).
|
|
2296
|
+
*/
|
|
2297
|
+
function Steps({ lines, threadId }: { lines: Chatter[]; threadId: string }) {
|
|
2298
|
+
return (
|
|
2299
|
+
<div className="flex w-full flex-col gap-0.5 rounded-2xl bg-muted/40 p-1">
|
|
2300
|
+
{lines.length > 1 && (
|
|
2301
|
+
<p className="px-2 py-1 font-mono text-[10px] text-muted-foreground">
|
|
2302
|
+
{lines.length} steps
|
|
2303
|
+
</p>
|
|
2304
|
+
)}
|
|
2305
|
+
{lines.map(
|
|
2306
|
+
(line) =>
|
|
2307
|
+
line.tool && (
|
|
2308
|
+
<BotTool
|
|
2309
|
+
key={line.id}
|
|
2310
|
+
tool={line.tool}
|
|
2311
|
+
threadId={threadId}
|
|
2312
|
+
collapsed
|
|
2313
|
+
/>
|
|
2314
|
+
),
|
|
2315
|
+
)}
|
|
2316
|
+
</div>
|
|
2317
|
+
);
|
|
2318
|
+
}
|
|
2319
|
+
|
|
2320
|
+
/** "3 steps · 1 note", leaving out what did not happen; notes are the words beside a call. */
|
|
2321
|
+
function countsOf(lines: Chatter[]): string {
|
|
2322
|
+
const words = [
|
|
2323
|
+
["tool", "step"],
|
|
2324
|
+
["say", "note"],
|
|
2325
|
+
["stop", "stop"],
|
|
2326
|
+
["note", "compaction"],
|
|
2327
|
+
] as const;
|
|
2328
|
+
return words
|
|
2329
|
+
.map(
|
|
2330
|
+
([kind, word]) =>
|
|
2331
|
+
[lines.filter((line) => line.kind === kind).length, word] as const,
|
|
2332
|
+
)
|
|
2333
|
+
.filter(([count]) => count > 0)
|
|
2334
|
+
.map(([count, word]) => `${count} ${word}${count === 1 ? "" : "s"}`)
|
|
2335
|
+
.join(" · ");
|
|
2336
|
+
}
|
|
2337
|
+
|
|
2338
|
+
/** What a line is doing in the model's own words: a step's label, or what was said. */
|
|
2339
|
+
function stepOf(line: Chatter): string {
|
|
2340
|
+
if (line.kind === "tool" && line.tool) {
|
|
2341
|
+
return line.tool.note ?? `${line.tool.name} · ${line.tool.input}`;
|
|
2342
|
+
}
|
|
2343
|
+
return plainText(line.text).replace(/\s+/g, " ").trim();
|
|
2344
|
+
}
|
|
2345
|
+
|
|
1483
2346
|
/** The delegated request, folded to three lines (FoldedText). */
|
|
1484
|
-
function Request({
|
|
2347
|
+
function Request({ thread }: { thread: ThreadView }) {
|
|
1485
2348
|
return (
|
|
1486
2349
|
<>
|
|
1487
|
-
<Invite from={THURSDAY} to={
|
|
1488
|
-
<
|
|
1489
|
-
<
|
|
1490
|
-
<
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
2350
|
+
<Invite from={THURSDAY} to={thread.bot} />
|
|
2351
|
+
<Turn side="end" name={THURSDAY.name} mark={<TurnMark bot={THURSDAY} />}>
|
|
2352
|
+
<Said dark className="py-2 pr-2 pl-3.5">
|
|
2353
|
+
<FoldedText
|
|
2354
|
+
text={thread.request}
|
|
2355
|
+
subject="request"
|
|
2356
|
+
className="wrap-anywhere"
|
|
2357
|
+
/>
|
|
2358
|
+
</Said>
|
|
2359
|
+
</Turn>
|
|
1495
2360
|
</>
|
|
1496
2361
|
);
|
|
1497
2362
|
}
|
|
1498
2363
|
|
|
1499
2364
|
/**
|
|
1500
|
-
*
|
|
1501
|
-
*
|
|
1502
|
-
* her own domain (features/thursday), so this only has to name her.
|
|
2365
|
+
* The user's side of a thread, which Thursday stands for; she is not a bot and has
|
|
2366
|
+
* no row to read a face from. Her face is always ThursdayMark (features/thursday).
|
|
1503
2367
|
*/
|
|
1504
|
-
const THURSDAY: BotRef = { name:
|
|
2368
|
+
const THURSDAY: BotRef = { name: ROOM_THURSDAY };
|
|
1505
2369
|
|
|
1506
2370
|
/**
|
|
1507
2371
|
* A bot joining the room. It is not a message — nobody said it — so it takes no
|
|
@@ -1523,44 +2387,75 @@ function Invite({ from, to }: { from: BotRef; to: BotRef }) {
|
|
|
1523
2387
|
function Face({ bot }: { bot: BotRef }) {
|
|
1524
2388
|
return (
|
|
1525
2389
|
<span className="flex min-w-0 items-center gap-1.5">
|
|
1526
|
-
|
|
1527
|
-
size={15}
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
2390
|
+
{bot.name === THURSDAY.name ? (
|
|
2391
|
+
<ThursdayMark size={15} className="shrink-0" />
|
|
2392
|
+
) : (
|
|
2393
|
+
<BotMark
|
|
2394
|
+
size={15}
|
|
2395
|
+
seed={bot.name}
|
|
2396
|
+
color={bot.icon?.color}
|
|
2397
|
+
shape={bot.icon?.shape}
|
|
2398
|
+
outline={bot.icon?.outline}
|
|
2399
|
+
paint={bot.icon?.paint}
|
|
2400
|
+
notify={false}
|
|
2401
|
+
className="shrink-0"
|
|
2402
|
+
/>
|
|
2403
|
+
)}
|
|
1535
2404
|
<span className="truncate">{bot.name}</span>
|
|
1536
2405
|
</span>
|
|
1537
2406
|
);
|
|
1538
2407
|
}
|
|
1539
2408
|
|
|
1540
|
-
/**
|
|
1541
|
-
function
|
|
2409
|
+
/** A turn's face: the user's side draws Thursday's, a bot its own. */
|
|
2410
|
+
function TurnMark({ bot }: { bot: BotRef }) {
|
|
2411
|
+
return bot.name === THURSDAY.name ? (
|
|
2412
|
+
<ThursdayMark size={26} className="mt-1 shrink-0" />
|
|
2413
|
+
) : (
|
|
2414
|
+
<BotMark
|
|
2415
|
+
size={26}
|
|
2416
|
+
seed={bot.name}
|
|
2417
|
+
color={bot.icon?.color}
|
|
2418
|
+
shape={bot.icon?.shape}
|
|
2419
|
+
outline={bot.icon?.outline}
|
|
2420
|
+
paint={bot.icon?.paint}
|
|
2421
|
+
notify={false}
|
|
2422
|
+
className="mt-1 shrink-0"
|
|
2423
|
+
/>
|
|
2424
|
+
);
|
|
2425
|
+
}
|
|
2426
|
+
|
|
2427
|
+
/**
|
|
2428
|
+
* Words from the other side of the tab. The user's side speaks in the one dark
|
|
2429
|
+
* bubble, whose contents take the other theme (`.inverse`, globals.css) so
|
|
2430
|
+
* Markdown's own surfaces — inline code, a code block, a link — stay readable on
|
|
2431
|
+
* it; other bots answer on `secondary`. `.inverse` is not on `BubbleContent`
|
|
2432
|
+
* itself, whose `bg-primary` would swap too.
|
|
2433
|
+
*/
|
|
2434
|
+
function Said({
|
|
2435
|
+
dark = false,
|
|
2436
|
+
children,
|
|
2437
|
+
className,
|
|
2438
|
+
}: {
|
|
2439
|
+
dark?: boolean;
|
|
2440
|
+
children: ReactNode;
|
|
2441
|
+
className?: string;
|
|
2442
|
+
}) {
|
|
1542
2443
|
return (
|
|
1543
|
-
<
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
<BotMark
|
|
1548
|
-
size={26}
|
|
1549
|
-
seed="thursday"
|
|
1550
|
-
notify={false}
|
|
1551
|
-
className="mt-1 shrink-0"
|
|
1552
|
-
/>
|
|
1553
|
-
}
|
|
2444
|
+
<Bubble
|
|
2445
|
+
align="end"
|
|
2446
|
+
variant={dark ? "default" : "secondary"}
|
|
2447
|
+
className="max-w-full"
|
|
1554
2448
|
>
|
|
1555
|
-
{
|
|
1556
|
-
|
|
2449
|
+
<BubbleContent className={cn("rounded-tr-md px-3.5", className)}>
|
|
2450
|
+
<div className={cn("min-w-0", dark && "inverse")}>{children}</div>
|
|
2451
|
+
</BubbleContent>
|
|
2452
|
+
</Bubble>
|
|
1557
2453
|
);
|
|
1558
2454
|
}
|
|
1559
2455
|
|
|
1560
2456
|
/**
|
|
1561
|
-
* One speaker's turn
|
|
1562
|
-
*
|
|
1563
|
-
* answers from the right.
|
|
2457
|
+
* One speaker's turn: a face, the name, and what it said and did. The start side
|
|
2458
|
+
* is the open tab's bot; the end side is everyone who talks with it.
|
|
1564
2459
|
*/
|
|
1565
2460
|
function Turn({
|
|
1566
2461
|
side,
|
|
@@ -1577,7 +2472,7 @@ function Turn({
|
|
|
1577
2472
|
<div className={cn("flex gap-2.5", side === "end" && "flex-row-reverse")}>
|
|
1578
2473
|
{mark}
|
|
1579
2474
|
{/* One cap on the turn's column, not one per block inside it: an answer and
|
|
1580
|
-
a one-line remark from the same
|
|
2475
|
+
a one-line remark from the same speaker then end on the same edge. The
|
|
1581
2476
|
answering side hugs that edge, so the two sides face each other. */}
|
|
1582
2477
|
<div
|
|
1583
2478
|
className={cn(
|
|
@@ -1594,146 +2489,191 @@ function Turn({
|
|
|
1594
2489
|
);
|
|
1595
2490
|
}
|
|
1596
2491
|
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
2492
|
+
/** Where a message sits: the tab's bot on none, the user's side dark, other bots `secondary`. */
|
|
2493
|
+
type Surface = "none" | "dark" | "secondary";
|
|
2494
|
+
|
|
2495
|
+
/**
|
|
2496
|
+
* A message: what passed between participants, a bot's reply, or the ending. A
|
|
2497
|
+
* question keeps its word as a record; the ending carries the files it names and
|
|
2498
|
+
* a copy.
|
|
2499
|
+
*/
|
|
2500
|
+
function Message({
|
|
2501
|
+
line,
|
|
2502
|
+
surface,
|
|
2503
|
+
mention,
|
|
2504
|
+
}: {
|
|
2505
|
+
line: Chatter;
|
|
2506
|
+
surface: Surface;
|
|
2507
|
+
/** The bot it is for, named at its head (SpeakerTurn). */
|
|
2508
|
+
mention: BotRef | null;
|
|
2509
|
+
}) {
|
|
2510
|
+
const question = Boolean(line.question);
|
|
2511
|
+
const ending = line.kind === "result";
|
|
2512
|
+
const bubble = surface !== "none";
|
|
2513
|
+
const words = (
|
|
2514
|
+
<>
|
|
2515
|
+
{question && <QuestionWord />}
|
|
2516
|
+
{mention ? (
|
|
2517
|
+
<Mentioned bot={mention} bubble={bubble}>
|
|
2518
|
+
{line.text}
|
|
2519
|
+
</Mentioned>
|
|
2520
|
+
) : (
|
|
2521
|
+
<MessageText bubble={bubble} className={cn(!ending && "leading-snug")}>
|
|
2522
|
+
{line.text}
|
|
2523
|
+
</MessageText>
|
|
2524
|
+
)}
|
|
2525
|
+
</>
|
|
2526
|
+
);
|
|
1614
2527
|
|
|
1615
2528
|
return (
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
2529
|
+
<>
|
|
2530
|
+
{bubble ? (
|
|
2531
|
+
<Said dark={surface === "dark"}>{words}</Said>
|
|
2532
|
+
) : (
|
|
2533
|
+
<div className="min-w-0 max-w-full px-1">{words}</div>
|
|
2534
|
+
)}
|
|
2535
|
+
{ending && (
|
|
2536
|
+
// The files it names, and the copy: the end of the answer is where a
|
|
2537
|
+
// reader is when they want either.
|
|
2538
|
+
<div className="flex items-center gap-2 px-1">
|
|
2539
|
+
<PathChips text={line.text} className="min-w-0" />
|
|
2540
|
+
<CopyReport text={line.text} />
|
|
2541
|
+
</div>
|
|
2542
|
+
)}
|
|
2543
|
+
</>
|
|
2544
|
+
);
|
|
2545
|
+
}
|
|
2546
|
+
|
|
2547
|
+
/**
|
|
2548
|
+
* Streamdown's code block as one surface inside a message bubble: its own frame
|
|
2549
|
+
* there would be a box in a box.
|
|
2550
|
+
*/
|
|
2551
|
+
const IN_BUBBLE =
|
|
2552
|
+
"[&_[data-streamdown=inline-code]]:text-[12px] [&_[data-streamdown=code-block]]:my-2 [&_[data-streamdown=code-block]]:gap-0 [&_[data-streamdown=code-block]]:rounded-[10px] [&_[data-streamdown=code-block]]:border-0 [&_[data-streamdown=code-block]]:bg-background [&_[data-streamdown=code-block]]:p-0 [&_[data-streamdown=code-block-header]]:h-7 [&_[data-streamdown=code-block-header]]:px-2.5 [&_[data-streamdown=code-block-header]]:text-[11px] [&_[data-streamdown=code-block-body]]:border-0 [&_[data-streamdown=code-block-body]]:bg-transparent! [&_[data-streamdown=code-block-body]]:px-2.5 [&_[data-streamdown=code-block-body]]:pt-0 [&_[data-streamdown=code-block-body]]:pb-2.5 [&_[data-streamdown=code-block-body]]:text-[12.5px] [&_[data-streamdown=code-block]_div:has(>[data-streamdown=code-block-actions])]:-mt-7 [&_[data-streamdown=code-block-actions]]:mr-1 [&_[data-streamdown=code-block-actions]]:border-0 [&_[data-streamdown=code-block-actions]]:bg-transparent!";
|
|
2553
|
+
|
|
2554
|
+
function MessageText({
|
|
2555
|
+
children,
|
|
2556
|
+
bubble = false,
|
|
2557
|
+
className,
|
|
2558
|
+
}: {
|
|
2559
|
+
children: string;
|
|
2560
|
+
/** Inside a message bubble (Said). */
|
|
2561
|
+
bubble?: boolean;
|
|
2562
|
+
className?: string;
|
|
2563
|
+
}) {
|
|
2564
|
+
return (
|
|
2565
|
+
<Markdown
|
|
2566
|
+
className={cn(
|
|
2567
|
+
"min-w-0 max-w-full text-[13px] leading-relaxed wrap-anywhere break-keep [&_h1]:text-[15px] [&_h2]:text-[14px] [&_h3]:text-[13px] [&_h3]:font-semibold [&_li]:my-0.5 [&_table]:text-[12px] [&_table]:wrap-normal [&>*:first-child]:mt-0 [&>*:last-child]:mb-0",
|
|
2568
|
+
bubble && IN_BUBBLE,
|
|
2569
|
+
className,
|
|
2570
|
+
)}
|
|
2571
|
+
>
|
|
2572
|
+
{children}
|
|
2573
|
+
</Markdown>
|
|
2574
|
+
);
|
|
2575
|
+
}
|
|
2576
|
+
|
|
2577
|
+
/**
|
|
2578
|
+
* Who a message is for, at its head: the addressee's face and name. In a bubble
|
|
2579
|
+
* the chip takes the page's background, which stands off both bubbles in either
|
|
2580
|
+
* theme; on no surface it is muted.
|
|
2581
|
+
*/
|
|
2582
|
+
function Mention({ bot, bubble }: { bot: BotRef; bubble: boolean }) {
|
|
2583
|
+
return (
|
|
2584
|
+
<span
|
|
2585
|
+
className={cn(
|
|
2586
|
+
"mr-1.5 inline-flex h-5 max-w-full items-center gap-1 rounded-full pr-2 pl-[3px] align-[-4px] text-[12px] leading-none font-medium whitespace-nowrap text-foreground",
|
|
2587
|
+
bubble ? "bg-background" : "bg-muted",
|
|
2588
|
+
)}
|
|
2589
|
+
>
|
|
2590
|
+
<BotMark
|
|
2591
|
+
size={14}
|
|
2592
|
+
seed={bot.name}
|
|
2593
|
+
color={bot.icon?.color}
|
|
2594
|
+
shape={bot.icon?.shape}
|
|
2595
|
+
outline={bot.icon?.outline}
|
|
2596
|
+
paint={bot.icon?.paint}
|
|
2597
|
+
notify={false}
|
|
2598
|
+
className="shrink-0"
|
|
2599
|
+
/>
|
|
2600
|
+
<span className="truncate">{bot.name}</span>
|
|
2601
|
+
</span>
|
|
2602
|
+
);
|
|
2603
|
+
}
|
|
2604
|
+
|
|
2605
|
+
/**
|
|
2606
|
+
* Words that follow a mention. The Markdown box dissolves (`contents`) so its
|
|
2607
|
+
* first paragraph runs on from the mention; later blocks still break lines.
|
|
2608
|
+
*/
|
|
2609
|
+
function Mentioned({
|
|
2610
|
+
bot,
|
|
2611
|
+
bubble,
|
|
2612
|
+
children,
|
|
2613
|
+
}: {
|
|
2614
|
+
bot: BotRef;
|
|
2615
|
+
bubble: boolean;
|
|
2616
|
+
children: string;
|
|
2617
|
+
}) {
|
|
2618
|
+
return (
|
|
2619
|
+
<div className="min-w-0 max-w-full text-[13px] leading-snug">
|
|
2620
|
+
<Mention bot={bot} bubble={bubble} />
|
|
2621
|
+
<MessageText
|
|
2622
|
+
bubble={bubble}
|
|
2623
|
+
className="contents leading-snug [&>p:first-child]:inline"
|
|
1632
2624
|
>
|
|
1633
|
-
{
|
|
1634
|
-
|
|
1635
|
-
<Steps key={run.key} lines={run.lines} taskId={task.id} />
|
|
1636
|
-
) : run.kind === "stops" ? (
|
|
1637
|
-
<Stops key={run.key} lines={run.lines} />
|
|
1638
|
-
) : (
|
|
1639
|
-
run.lines.map((line) => (
|
|
1640
|
-
<Line key={line.id} line={line} taskId={task.id} />
|
|
1641
|
-
))
|
|
1642
|
-
),
|
|
1643
|
-
)}
|
|
1644
|
-
</Turn>
|
|
2625
|
+
{children}
|
|
2626
|
+
</MessageText>
|
|
1645
2627
|
</div>
|
|
1646
2628
|
);
|
|
1647
2629
|
}
|
|
1648
2630
|
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
2631
|
+
/** A question kept as a record. While it waits, the reply sheet holds it in amber. */
|
|
2632
|
+
function QuestionWord() {
|
|
2633
|
+
return (
|
|
2634
|
+
<p className="mb-1 flex items-center gap-1.5 text-[12px] leading-4 font-medium text-muted-foreground">
|
|
2635
|
+
<CircleQuestionMark className="size-3.5 shrink-0" />
|
|
2636
|
+
Question
|
|
2637
|
+
</p>
|
|
2638
|
+
);
|
|
2639
|
+
}
|
|
1654
2640
|
|
|
2641
|
+
/** Work that is not a step: a compaction, or the words beside a call. */
|
|
2642
|
+
function Line({
|
|
2643
|
+
line,
|
|
2644
|
+
inset = false,
|
|
2645
|
+
}: {
|
|
2646
|
+
line: Chatter;
|
|
2647
|
+
/** Inside a work row, in line with its steps' labels. */
|
|
2648
|
+
inset?: boolean;
|
|
2649
|
+
}) {
|
|
1655
2650
|
// A compact summary (bot.run compact): a divider, with the summary behind it.
|
|
1656
2651
|
if (line.kind === "note") {
|
|
1657
2652
|
return (
|
|
1658
|
-
<details className="w-full py-1 text-muted-foreground">
|
|
2653
|
+
<details className="w-full px-2 py-1 text-muted-foreground">
|
|
1659
2654
|
<summary className="flex cursor-pointer list-none items-center gap-2.5 outline-none [&::-webkit-details-marker]:hidden">
|
|
1660
2655
|
<span className="h-px flex-1 bg-border" />
|
|
1661
|
-
<span className="
|
|
2656
|
+
<span className="min-w-0 text-center font-mono text-[10px]">
|
|
1662
2657
|
Compacted — it goes on from its summary
|
|
1663
2658
|
</span>
|
|
1664
2659
|
<span className="h-px flex-1 bg-border" />
|
|
1665
2660
|
</summary>
|
|
1666
|
-
<
|
|
2661
|
+
<MessageText className="mt-2 rounded-xl bg-muted/40 px-3 py-2 text-[11px]">
|
|
1667
2662
|
{line.text}
|
|
1668
|
-
</
|
|
2663
|
+
</MessageText>
|
|
1669
2664
|
</details>
|
|
1670
2665
|
);
|
|
1671
2666
|
}
|
|
1672
2667
|
|
|
1673
|
-
// Passing remarks are muted; the answer is the one thing here at full weight.
|
|
1674
|
-
if (!isOutcome(line)) {
|
|
1675
|
-
return (
|
|
1676
|
-
<p className="px-1 text-[12.5px] leading-relaxed break-keep text-muted-foreground">
|
|
1677
|
-
{line.text}
|
|
1678
|
-
</p>
|
|
1679
|
-
);
|
|
1680
|
-
}
|
|
1681
|
-
|
|
1682
|
-
// A question the bot stopped on; the options are what was offered at the time.
|
|
1683
|
-
if (line.options) {
|
|
1684
|
-
return (
|
|
1685
|
-
<div className="w-fit max-w-full space-y-1.5 rounded-2xl bg-amber-500/8 px-3 py-2 ring-1 ring-amber-500/25">
|
|
1686
|
-
<p className="text-[13px] leading-snug break-keep">{line.text}</p>
|
|
1687
|
-
{line.options.length > 0 && (
|
|
1688
|
-
<p className="flex flex-wrap gap-1">
|
|
1689
|
-
{line.options.map((option) => (
|
|
1690
|
-
<span
|
|
1691
|
-
key={option}
|
|
1692
|
-
className="rounded-full bg-background/70 px-2 py-0.5 font-mono text-[10px] text-muted-foreground"
|
|
1693
|
-
>
|
|
1694
|
-
{option}
|
|
1695
|
-
</span>
|
|
1696
|
-
))}
|
|
1697
|
-
</p>
|
|
1698
|
-
)}
|
|
1699
|
-
</div>
|
|
1700
|
-
);
|
|
1701
|
-
}
|
|
1702
|
-
|
|
1703
|
-
const failed = line.kind === "error";
|
|
1704
|
-
|
|
1705
2668
|
return (
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
// rides the name line above (Turn's tail), where nothing else was.
|
|
1711
|
-
<div className="min-w-0 px-1">
|
|
1712
|
-
{failed && (
|
|
1713
|
-
<p className="mb-1.5 flex items-center gap-1.5 font-mono text-[10px] text-destructive">
|
|
1714
|
-
<X className="size-3 shrink-0" />
|
|
1715
|
-
Could not finish
|
|
1716
|
-
</p>
|
|
2669
|
+
<MessageText
|
|
2670
|
+
className={cn(
|
|
2671
|
+
"text-[12.5px] text-muted-foreground",
|
|
2672
|
+
inset ? "px-2.5 py-1" : "px-1",
|
|
1717
2673
|
)}
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
failed && "text-destructive",
|
|
1722
|
-
)}
|
|
1723
|
-
>
|
|
1724
|
-
{line.text}
|
|
1725
|
-
</Markdown>
|
|
1726
|
-
{/* The files it names, and the copy — one row, because the end of the
|
|
1727
|
-
answer is where a reader is when they want either. */}
|
|
1728
|
-
<div className="mt-2 flex items-center gap-2">
|
|
1729
|
-
<PathChips text={line.text} className="min-w-0" />
|
|
1730
|
-
{!failed && (
|
|
1731
|
-
<span className="ml-auto shrink-0">
|
|
1732
|
-
<CopyReport text={line.text} />
|
|
1733
|
-
</span>
|
|
1734
|
-
)}
|
|
1735
|
-
</div>
|
|
1736
|
-
</div>
|
|
2674
|
+
>
|
|
2675
|
+
{line.text}
|
|
2676
|
+
</MessageText>
|
|
1737
2677
|
);
|
|
1738
2678
|
}
|
|
1739
2679
|
|
|
@@ -1775,32 +2715,8 @@ function CopyReport({ text }: { text: string }) {
|
|
|
1775
2715
|
}
|
|
1776
2716
|
|
|
1777
2717
|
/**
|
|
1778
|
-
*
|
|
1779
|
-
*
|
|
1780
|
-
* click before they can tell whether the bot went the right way. Each row is
|
|
1781
|
-
* one collapsed line, so the length costs scroll, not noise.
|
|
1782
|
-
*/
|
|
1783
|
-
function Steps({ lines, taskId }: { lines: Chatter[]; taskId: string }) {
|
|
1784
|
-
return (
|
|
1785
|
-
<div className="flex w-full flex-col gap-0.5 rounded-2xl bg-muted/40 p-1">
|
|
1786
|
-
{lines.length > 1 && (
|
|
1787
|
-
<p className="px-2 py-1 font-mono text-[10px] text-muted-foreground">
|
|
1788
|
-
{lines.length} steps
|
|
1789
|
-
</p>
|
|
1790
|
-
)}
|
|
1791
|
-
{lines.map((line) =>
|
|
1792
|
-
line.tool ? (
|
|
1793
|
-
// All start collapsed; a running call expands itself (bot-tool Frame).
|
|
1794
|
-
<BotTool key={line.id} tool={line.tool} taskId={taskId} collapsed />
|
|
1795
|
-
) : null,
|
|
1796
|
-
)}
|
|
1797
|
-
</div>
|
|
1798
|
-
);
|
|
1799
|
-
}
|
|
1800
|
-
|
|
1801
|
-
/**
|
|
1802
|
-
* Where the app stopped the run (bot.runner parkTask): a failed model call, a
|
|
1803
|
-
* restart, a closed browser. Muted and in the bot's turn, since the bot goes on
|
|
2718
|
+
* Where the app stopped the run (room.query pauseRoom, a break the runner retries): a failed model call, a
|
|
2719
|
+
* restart, a closed browser. Muted and in the bot's work, since the bot goes on
|
|
1804
2720
|
* from here. The same reason in a row is one line with a count; opening it lists
|
|
1805
2721
|
* each stop by the time it happened.
|
|
1806
2722
|
*/
|
|
@@ -1833,10 +2749,10 @@ function Stops({ lines }: { lines: Chatter[] }) {
|
|
|
1833
2749
|
);
|
|
1834
2750
|
}
|
|
1835
2751
|
|
|
1836
|
-
/** What stopped it, without the words behind it: a
|
|
2752
|
+
/** What stopped it, without the words behind it: a reason may name those in parentheses. */
|
|
1837
2753
|
const leadOf = (text: string) => text.split(" (")[0].replace(/\.$/, "");
|
|
1838
2754
|
|
|
1839
|
-
/** Splits
|
|
2755
|
+
/** Splits a bot's work into runs of tool calls, of stops, and of everything else. */
|
|
1840
2756
|
type Run =
|
|
1841
2757
|
| { kind: "tools"; key: string; lines: Chatter[] }
|
|
1842
2758
|
| { kind: "stops"; key: string; lines: Chatter[] }
|