sparkecoder 0.1.21 → 0.1.23
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/dist/agent/index.d.ts +3 -3
- package/dist/agent/index.js +1361 -215
- package/dist/agent/index.js.map +1 -1
- package/dist/cli.js +2179 -349
- package/dist/cli.js.map +1 -1
- package/dist/db/index.d.ts +20 -2
- package/dist/db/index.js +97 -0
- package/dist/db/index.js.map +1 -1
- package/dist/{index-BzedNBK-.d.ts → index-BblbmG_0.d.ts} +42 -6
- package/dist/index.d.ts +6 -6
- package/dist/index.js +2165 -335
- package/dist/index.js.map +1 -1
- package/dist/{schema-CkrIadxa.d.ts → schema-D_8A4k01.d.ts} +270 -3
- package/dist/search-ybREg7F_.d.ts +254 -0
- package/dist/server/index.js +2163 -333
- package/dist/server/index.js.map +1 -1
- package/dist/tools/index.d.ts +7 -56
- package/dist/tools/index.js +894 -27
- package/dist/tools/index.js.map +1 -1
- package/package.json +5 -1
- package/web/.next/BUILD_ID +1 -1
- package/web/.next/standalone/web/.next/BUILD_ID +1 -1
- package/web/.next/standalone/web/.next/app-path-routes-manifest.json +4 -0
- package/web/.next/standalone/web/.next/build-manifest.json +7 -6
- package/web/.next/standalone/web/.next/prerender-manifest.json +99 -3
- package/web/.next/standalone/web/.next/required-server-files.json +28 -4
- package/web/.next/standalone/web/.next/routes-manifest.json +24 -0
- package/web/.next/standalone/web/.next/server/app/(main)/page/build-manifest.json +5 -4
- package/web/.next/standalone/web/.next/server/app/(main)/page.js +2 -2
- package/web/.next/standalone/web/.next/server/app/(main)/page.js.nft.json +1 -1
- package/web/.next/standalone/web/.next/server/app/(main)/page_client-reference-manifest.js +1 -1
- package/web/.next/standalone/web/.next/server/app/(main)/session/[id]/page/build-manifest.json +5 -4
- package/web/.next/standalone/web/.next/server/app/(main)/session/[id]/page.js +2 -2
- package/web/.next/standalone/web/.next/server/app/(main)/session/[id]/page.js.nft.json +1 -1
- package/web/.next/standalone/web/.next/server/app/(main)/session/[id]/page_client-reference-manifest.js +1 -1
- package/web/.next/standalone/web/.next/server/app/_global-error/page/build-manifest.json +5 -4
- package/web/.next/standalone/web/.next/server/app/_global-error/page.js +2 -2
- package/web/.next/standalone/web/.next/server/app/_global-error/page.js.nft.json +1 -1
- package/web/.next/standalone/web/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/web/.next/standalone/web/.next/server/app/_global-error.html +2 -2
- package/web/.next/standalone/web/.next/server/app/_global-error.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/_not-found/page/build-manifest.json +5 -4
- package/web/.next/standalone/web/.next/server/app/_not-found/page.js +2 -2
- package/web/.next/standalone/web/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/web/.next/standalone/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/web/.next/standalone/web/.next/server/app/_not-found.html +1 -1
- package/web/.next/standalone/web/.next/server/app/_not-found.rsc +2 -2
- package/web/.next/standalone/web/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
- package/web/.next/standalone/web/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/_not-found.segments/_index.segment.rsc +2 -2
- package/web/.next/standalone/web/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/web/.next/standalone/web/.next/server/app/api/config/route.js.nft.json +1 -1
- package/web/.next/standalone/web/.next/server/app/api/health/route.js.nft.json +1 -1
- package/web/.next/standalone/web/.next/server/app/docs/installation/page/app-paths-manifest.json +3 -0
- package/web/.next/standalone/web/.next/server/app/docs/installation/page/build-manifest.json +18 -0
- package/web/.next/standalone/web/.next/server/app/docs/installation/page/next-font-manifest.json +11 -0
- package/web/.next/standalone/web/.next/server/app/docs/installation/page/react-loadable-manifest.json +1 -0
- package/web/.next/standalone/web/.next/server/app/docs/installation/page/server-reference-manifest.json +4 -0
- package/web/.next/standalone/web/.next/server/app/docs/installation/page.js +21 -0
- package/web/.next/standalone/web/.next/server/app/docs/installation/page.js.map +5 -0
- package/web/.next/standalone/web/.next/server/app/docs/installation/page.js.nft.json +1 -0
- package/web/.next/standalone/web/.next/server/app/docs/installation/page_client-reference-manifest.js +2 -0
- package/web/.next/standalone/web/.next/server/app/docs/installation.html +86 -0
- package/web/.next/standalone/web/.next/server/app/docs/installation.meta +16 -0
- package/web/.next/standalone/web/.next/server/app/docs/installation.rsc +36 -0
- package/web/.next/standalone/web/.next/server/app/docs/installation.segments/_full.segment.rsc +36 -0
- package/web/.next/standalone/web/.next/server/app/docs/installation.segments/_head.segment.rsc +6 -0
- package/web/.next/standalone/web/.next/server/app/docs/installation.segments/_index.segment.rsc +7 -0
- package/web/.next/standalone/web/.next/server/app/docs/installation.segments/_tree.segment.rsc +3 -0
- package/web/.next/standalone/web/.next/server/app/docs/installation.segments/docs/installation/__PAGE__.segment.rsc +22 -0
- package/web/.next/standalone/web/.next/server/app/docs/installation.segments/docs/installation.segment.rsc +4 -0
- package/web/.next/standalone/web/.next/server/app/docs/installation.segments/docs.segment.rsc +5 -0
- package/web/.next/standalone/web/.next/server/app/docs/page/app-paths-manifest.json +3 -0
- package/web/.next/standalone/web/.next/server/app/docs/page/build-manifest.json +18 -0
- package/web/.next/standalone/web/.next/server/app/docs/page/next-font-manifest.json +11 -0
- package/web/.next/standalone/web/.next/server/app/docs/page/react-loadable-manifest.json +1 -0
- package/web/.next/standalone/web/.next/server/app/docs/page/server-reference-manifest.json +4 -0
- package/web/.next/standalone/web/.next/server/app/docs/page.js +21 -0
- package/web/.next/standalone/web/.next/server/app/docs/page.js.map +5 -0
- package/web/.next/standalone/web/.next/server/app/docs/page.js.nft.json +1 -0
- package/web/.next/standalone/web/.next/server/app/docs/page_client-reference-manifest.js +2 -0
- package/web/.next/standalone/web/.next/server/app/docs/skills/page/app-paths-manifest.json +3 -0
- package/web/.next/standalone/web/.next/server/app/docs/skills/page/build-manifest.json +18 -0
- package/web/.next/standalone/web/.next/server/app/docs/skills/page/next-font-manifest.json +11 -0
- package/web/.next/standalone/web/.next/server/app/docs/skills/page/react-loadable-manifest.json +1 -0
- package/web/.next/standalone/web/.next/server/app/docs/skills/page/server-reference-manifest.json +4 -0
- package/web/.next/standalone/web/.next/server/app/docs/skills/page.js +21 -0
- package/web/.next/standalone/web/.next/server/app/docs/skills/page.js.map +5 -0
- package/web/.next/standalone/web/.next/server/app/docs/skills/page.js.nft.json +1 -0
- package/web/.next/standalone/web/.next/server/app/docs/skills/page_client-reference-manifest.js +2 -0
- package/web/.next/standalone/web/.next/server/app/docs/skills.html +268 -0
- package/web/.next/standalone/web/.next/server/app/docs/skills.meta +16 -0
- package/web/.next/standalone/web/.next/server/app/docs/skills.rsc +82 -0
- package/web/.next/standalone/web/.next/server/app/docs/skills.segments/_full.segment.rsc +82 -0
- package/web/.next/standalone/web/.next/server/app/docs/skills.segments/_head.segment.rsc +6 -0
- package/web/.next/standalone/web/.next/server/app/docs/skills.segments/_index.segment.rsc +7 -0
- package/web/.next/standalone/web/.next/server/app/docs/skills.segments/_tree.segment.rsc +3 -0
- package/web/.next/standalone/web/.next/server/app/docs/skills.segments/docs/skills/__PAGE__.segment.rsc +66 -0
- package/web/.next/standalone/web/.next/server/app/docs/skills.segments/docs/skills.segment.rsc +4 -0
- package/web/.next/standalone/web/.next/server/app/docs/skills.segments/docs.segment.rsc +5 -0
- package/web/.next/standalone/web/.next/server/app/docs/tools/page/app-paths-manifest.json +3 -0
- package/web/.next/standalone/web/.next/server/app/docs/tools/page/build-manifest.json +18 -0
- package/web/.next/standalone/web/.next/server/app/docs/tools/page/next-font-manifest.json +11 -0
- package/web/.next/standalone/web/.next/server/app/docs/tools/page/react-loadable-manifest.json +1 -0
- package/web/.next/standalone/web/.next/server/app/docs/tools/page/server-reference-manifest.json +4 -0
- package/web/.next/standalone/web/.next/server/app/docs/tools/page.js +21 -0
- package/web/.next/standalone/web/.next/server/app/docs/tools/page.js.map +5 -0
- package/web/.next/standalone/web/.next/server/app/docs/tools/page.js.nft.json +1 -0
- package/web/.next/standalone/web/.next/server/app/docs/tools/page_client-reference-manifest.js +2 -0
- package/web/.next/standalone/web/.next/server/app/docs/tools.html +242 -0
- package/web/.next/standalone/web/.next/server/app/docs/tools.meta +16 -0
- package/web/.next/standalone/web/.next/server/app/docs/tools.rsc +87 -0
- package/web/.next/standalone/web/.next/server/app/docs/tools.segments/_full.segment.rsc +87 -0
- package/web/.next/standalone/web/.next/server/app/docs/tools.segments/_head.segment.rsc +6 -0
- package/web/.next/standalone/web/.next/server/app/docs/tools.segments/_index.segment.rsc +7 -0
- package/web/.next/standalone/web/.next/server/app/docs/tools.segments/_tree.segment.rsc +3 -0
- package/web/.next/standalone/web/.next/server/app/docs/tools.segments/docs/tools/__PAGE__.segment.rsc +72 -0
- package/web/.next/standalone/web/.next/server/app/docs/tools.segments/docs/tools.segment.rsc +4 -0
- package/web/.next/standalone/web/.next/server/app/docs/tools.segments/docs.segment.rsc +5 -0
- package/web/.next/standalone/web/.next/server/app/docs.html +74 -0
- package/web/.next/standalone/web/.next/server/app/docs.meta +15 -0
- package/web/.next/standalone/web/.next/server/app/docs.rsc +34 -0
- package/web/.next/standalone/web/.next/server/app/docs.segments/_full.segment.rsc +34 -0
- package/web/.next/standalone/web/.next/server/app/docs.segments/_head.segment.rsc +6 -0
- package/web/.next/standalone/web/.next/server/app/docs.segments/_index.segment.rsc +7 -0
- package/web/.next/standalone/web/.next/server/app/docs.segments/_tree.segment.rsc +3 -0
- package/web/.next/standalone/web/.next/server/app/docs.segments/docs/__PAGE__.segment.rsc +20 -0
- package/web/.next/standalone/web/.next/server/app/docs.segments/docs.segment.rsc +5 -0
- package/web/.next/standalone/web/.next/server/app/index.html +1 -1
- package/web/.next/standalone/web/.next/server/app/index.rsc +4 -4
- package/web/.next/standalone/web/.next/server/app/index.segments/!KG1haW4p/__PAGE__.segment.rsc +2 -2
- package/web/.next/standalone/web/.next/server/app/index.segments/!KG1haW4p.segment.rsc +2 -2
- package/web/.next/standalone/web/.next/server/app/index.segments/_full.segment.rsc +4 -4
- package/web/.next/standalone/web/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/web/.next/standalone/web/.next/server/app/index.segments/_index.segment.rsc +2 -2
- package/web/.next/standalone/web/.next/server/app/index.segments/_tree.segment.rsc +2 -2
- package/web/.next/standalone/web/.next/server/app-paths-manifest.json +4 -0
- package/web/.next/standalone/web/.next/server/chunks/ssr/{2374f_378282b1._.js → 2374f_244589df._.js} +1 -1
- package/web/.next/standalone/web/.next/server/chunks/ssr/{2374f_5de336d2._.js → 2374f_41a27541._.js} +1 -1
- package/web/.next/standalone/web/.next/server/chunks/ssr/{2374f_30f9df13._.js → 2374f_47c9e2d5._.js} +1 -1
- package/web/.next/standalone/web/.next/server/chunks/ssr/{2374f_d94c2b70._.js → 2374f_4bf2df9d._.js} +1 -1
- package/web/.next/standalone/web/.next/server/chunks/ssr/2374f_663d1038._.js +3 -0
- package/web/.next/standalone/web/.next/server/chunks/ssr/2374f_954e49c0._.js +3 -0
- package/web/.next/standalone/web/.next/server/chunks/ssr/{2374f_1d78db71._.js → 2374f_c33b095a._.js} +1 -1
- package/web/.next/standalone/web/.next/server/chunks/ssr/{2374f_bbc99511._.js → 2374f_fa61fbb2._.js} +1 -1
- package/web/.next/standalone/web/.next/server/chunks/ssr/{2374f_8825dcc9._.js → 2374f_fb82ac0d._.js} +1 -1
- package/web/.next/standalone/web/.next/server/chunks/ssr/{web_96bca05b._.js → 2374f_next_dist_bbe64674._.js} +2 -2
- package/web/.next/standalone/web/.next/server/chunks/ssr/{[root-of-the-server]__7f04455b._.js → [root-of-the-server]__1e06ddf7._.js} +2 -2
- package/web/.next/standalone/web/.next/server/chunks/ssr/[root-of-the-server]__2b151e1c._.js +3 -0
- package/web/.next/standalone/web/.next/server/chunks/ssr/[root-of-the-server]__2dbf511a._.js +9 -0
- package/web/.next/standalone/web/.next/server/chunks/ssr/[root-of-the-server]__397fadd4._.js +3 -0
- package/web/.next/standalone/web/.next/server/chunks/ssr/[root-of-the-server]__44bd8bd1._.js +3 -0
- package/web/.next/standalone/web/.next/server/chunks/ssr/[root-of-the-server]__70cecda8._.js +3 -0
- package/web/.next/standalone/web/.next/server/chunks/ssr/[root-of-the-server]__9fdf9974._.js +3 -0
- package/web/.next/standalone/web/.next/server/chunks/ssr/{[root-of-the-server]__f18f92f4._.js → [root-of-the-server]__b050bb8f._.js} +2 -2
- package/web/.next/standalone/web/.next/server/chunks/ssr/[root-of-the-server]__d3034cd2._.js +3 -0
- package/web/.next/standalone/web/.next/server/chunks/ssr/{[root-of-the-server]__c3a1e22c._.js → [root-of-the-server]__ef2713cf._.js} +2 -2
- package/web/.next/standalone/web/.next/server/chunks/ssr/[root-of-the-server]__f764bebe._.js +3 -0
- package/web/.next/standalone/web/.next/server/chunks/ssr/web_046bf7db._.js +3 -0
- package/web/.next/standalone/web/.next/server/chunks/ssr/web_656c1e45._.js +7 -0
- package/web/.next/standalone/web/.next/server/chunks/ssr/web_76ccf09f._.js +8 -0
- package/web/.next/standalone/web/.next/server/chunks/ssr/web__next-internal_server_app_docs_installation_page_actions_52cc0648.js +3 -0
- package/web/.next/standalone/web/.next/server/chunks/ssr/web__next-internal_server_app_docs_page_actions_4fe77da8.js +3 -0
- package/web/.next/standalone/web/.next/server/chunks/ssr/web__next-internal_server_app_docs_skills_page_actions_251df2e1.js +3 -0
- package/web/.next/standalone/web/.next/server/chunks/ssr/web__next-internal_server_app_docs_tools_page_actions_3e6382b0.js +3 -0
- package/web/.next/standalone/web/.next/server/chunks/ssr/web_a565dc94._.js +4 -0
- package/web/.next/standalone/web/.next/server/chunks/ssr/web_b1cce0b7._.js +4 -0
- package/web/.next/standalone/web/.next/server/chunks/ssr/web_b42ed1be._.js +3 -0
- package/web/.next/standalone/web/.next/server/chunks/ssr/web_c0c2bee4._.js +4 -0
- package/web/.next/standalone/web/.next/server/chunks/ssr/web_eea9c122._.js +3 -0
- package/web/.next/standalone/web/.next/server/chunks/ssr/web_ff00a5c3._.js +4 -0
- package/web/.next/standalone/web/.next/server/chunks/ssr/web_src_app_(main)_layout_tsx_453f6492._.js +3 -0
- package/web/.next/standalone/web/.next/server/chunks/ssr/web_src_app_(main)_page_tsx_5ac4794b._.js +3 -0
- package/web/.next/standalone/web/.next/server/middleware-build-manifest.js +5 -4
- package/web/.next/standalone/web/.next/server/next-font-manifest.js +1 -1
- package/web/.next/standalone/web/.next/server/next-font-manifest.json +16 -0
- package/web/.next/standalone/web/.next/server/pages/404.html +1 -1
- package/web/.next/standalone/web/.next/server/pages/500.html +2 -2
- package/web/.next/standalone/web/.next/server/server-reference-manifest.js +1 -1
- package/web/.next/standalone/web/.next/server/server-reference-manifest.json +1 -1
- package/web/.next/standalone/web/.next/static/chunks/0cc382a66266188e.js +7 -0
- package/web/.next/standalone/web/.next/static/chunks/0fda34e553582102.js +1 -0
- package/web/.next/standalone/web/.next/static/{static/chunks/5ec82ce8f3aabaf0.js → chunks/6407c045dfc908fe.js} +3 -3
- package/web/.next/standalone/web/.next/static/chunks/651e187cc15d66de.js +1 -0
- package/web/.next/standalone/web/.next/static/chunks/862ced58ce21a270.js +4 -0
- package/web/.next/standalone/web/.next/static/chunks/89bc21c0443670f4.js +1 -0
- package/web/.next/standalone/web/.next/static/chunks/8f4edf22ededc29b.js +7 -0
- package/web/.next/standalone/web/.next/static/chunks/ad6b9dbb257d62cc.js +1 -0
- package/web/.next/standalone/web/.next/static/chunks/af22745850132107.css +1 -0
- package/web/.next/standalone/web/.next/static/chunks/b9ad1584d4e11d12.js +1 -0
- package/web/.next/standalone/web/.next/static/chunks/db9b22c844a35e20.js +5 -0
- package/web/.next/standalone/web/.next/static/chunks/turbopack-597558bb7b6982f6.js +4 -0
- package/web/.next/standalone/web/.next/static/static/chunks/0cc382a66266188e.js +7 -0
- package/web/.next/standalone/web/.next/static/static/chunks/0fda34e553582102.js +1 -0
- package/web/.next/{static/chunks/5ec82ce8f3aabaf0.js → standalone/web/.next/static/static/chunks/6407c045dfc908fe.js} +3 -3
- package/web/.next/standalone/web/.next/static/static/chunks/651e187cc15d66de.js +1 -0
- package/web/.next/standalone/web/.next/static/static/chunks/862ced58ce21a270.js +4 -0
- package/web/.next/standalone/web/.next/static/static/chunks/89bc21c0443670f4.js +1 -0
- package/web/.next/standalone/web/.next/static/static/chunks/8f4edf22ededc29b.js +7 -0
- package/web/.next/standalone/web/.next/static/static/chunks/ad6b9dbb257d62cc.js +1 -0
- package/web/.next/standalone/web/.next/static/static/chunks/af22745850132107.css +1 -0
- package/web/.next/standalone/web/.next/static/static/chunks/b9ad1584d4e11d12.js +1 -0
- package/web/.next/standalone/web/.next/static/static/chunks/db9b22c844a35e20.js +5 -0
- package/web/.next/standalone/web/.next/static/static/chunks/turbopack-597558bb7b6982f6.js +4 -0
- package/web/.next/standalone/web/mdx-components.tsx +119 -0
- package/web/.next/standalone/web/next.config.ts +15 -1
- package/web/.next/standalone/web/package-lock.json +559 -4
- package/web/.next/standalone/web/package.json +4 -0
- package/web/.next/standalone/web/runtime-config.json +1 -1
- package/web/.next/standalone/web/server.js +1 -1
- package/web/.next/standalone/web/src/app/(main)/page.tsx +127 -5
- package/web/.next/standalone/web/src/app/docs/installation/page.mdx +128 -0
- package/web/.next/standalone/web/src/app/docs/layout.tsx +74 -0
- package/web/.next/standalone/web/src/app/docs/page.mdx +90 -0
- package/web/.next/standalone/web/src/app/docs/skills/page.mdx +334 -0
- package/web/.next/standalone/web/src/app/docs/tools/page.mdx +300 -0
- package/web/.next/standalone/web/src/components/ai-elements/mention-input.tsx +809 -0
- package/web/.next/standalone/web/src/components/ai-elements/search-tool.tsx +400 -0
- package/web/.next/standalone/web/src/components/ai-elements/speech-input.tsx +89 -36
- package/web/.next/standalone/web/src/components/ai-elements/subagent-modal.tsx +275 -0
- package/web/.next/standalone/web/src/components/ai-elements/write-file-tool.tsx +19 -5
- package/web/.next/standalone/web/src/components/chat-interface.tsx +820 -50
- package/web/.next/standalone/web/src/hooks/use-workspace-files.ts +108 -0
- package/web/.next/standalone/web/src/lib/api.ts +223 -6
- package/web/.next/static/chunks/0cc382a66266188e.js +7 -0
- package/web/.next/static/chunks/0fda34e553582102.js +1 -0
- package/web/.next/{standalone/web/.next/static/chunks/5ec82ce8f3aabaf0.js → static/chunks/6407c045dfc908fe.js} +3 -3
- package/web/.next/static/chunks/651e187cc15d66de.js +1 -0
- package/web/.next/static/chunks/862ced58ce21a270.js +4 -0
- package/web/.next/static/chunks/89bc21c0443670f4.js +1 -0
- package/web/.next/static/chunks/8f4edf22ededc29b.js +7 -0
- package/web/.next/static/chunks/ad6b9dbb257d62cc.js +1 -0
- package/web/.next/static/chunks/af22745850132107.css +1 -0
- package/web/.next/static/chunks/b9ad1584d4e11d12.js +1 -0
- package/web/.next/static/chunks/db9b22c844a35e20.js +5 -0
- package/web/.next/static/chunks/turbopack-597558bb7b6982f6.js +4 -0
- package/web/package.json +4 -0
- package/dist/bash-CGAqW7HR.d.ts +0 -80
- package/web/.next/standalone/web/.next/server/chunks/ssr/2374f_9bf3c7f3._.js +0 -3
- package/web/.next/standalone/web/.next/server/chunks/ssr/[root-of-the-server]__0f6b5fa7._.js +0 -3
- package/web/.next/standalone/web/.next/server/chunks/ssr/[root-of-the-server]__513c6b45._.js +0 -3
- package/web/.next/standalone/web/.next/server/chunks/ssr/[root-of-the-server]__a984d933._.js +0 -9
- package/web/.next/standalone/web/.next/server/chunks/ssr/[root-of-the-server]__de58a952._.js +0 -3
- package/web/.next/standalone/web/.next/server/chunks/ssr/web_19b6934c._.js +0 -8
- package/web/.next/standalone/web/.next/server/chunks/ssr/web_d7d3e40d._.js +0 -7
- package/web/.next/standalone/web/.next/server/chunks/ssr/web_e6034803._.js +0 -3
- package/web/.next/standalone/web/.next/static/chunks/03d4169891280e04.js +0 -7
- package/web/.next/standalone/web/.next/static/chunks/2d5da0cfc011b8d9.js +0 -1
- package/web/.next/standalone/web/.next/static/chunks/634fd97fab9ed4e4.js +0 -4
- package/web/.next/standalone/web/.next/static/chunks/77e4bf0421481629.js +0 -1
- package/web/.next/standalone/web/.next/static/chunks/a86053f0894587f2.js +0 -7
- package/web/.next/standalone/web/.next/static/chunks/beb9625c4a470042.js +0 -1
- package/web/.next/standalone/web/.next/static/chunks/c2244168e74b8c78.js +0 -1
- package/web/.next/standalone/web/.next/static/chunks/c81c1aec4369c77f.js +0 -5
- package/web/.next/standalone/web/.next/static/chunks/d0a69c59b1c0d99c.css +0 -1
- package/web/.next/standalone/web/.next/static/chunks/turbopack-54bc7d566cd2d105.js +0 -4
- package/web/.next/standalone/web/.next/static/static/chunks/03d4169891280e04.js +0 -7
- package/web/.next/standalone/web/.next/static/static/chunks/2d5da0cfc011b8d9.js +0 -1
- package/web/.next/standalone/web/.next/static/static/chunks/634fd97fab9ed4e4.js +0 -4
- package/web/.next/standalone/web/.next/static/static/chunks/77e4bf0421481629.js +0 -1
- package/web/.next/standalone/web/.next/static/static/chunks/a86053f0894587f2.js +0 -7
- package/web/.next/standalone/web/.next/static/static/chunks/beb9625c4a470042.js +0 -1
- package/web/.next/standalone/web/.next/static/static/chunks/c2244168e74b8c78.js +0 -1
- package/web/.next/standalone/web/.next/static/static/chunks/c81c1aec4369c77f.js +0 -5
- package/web/.next/standalone/web/.next/static/static/chunks/d0a69c59b1c0d99c.css +0 -1
- package/web/.next/standalone/web/.next/static/static/chunks/turbopack-54bc7d566cd2d105.js +0 -4
- package/web/.next/static/chunks/03d4169891280e04.js +0 -7
- package/web/.next/static/chunks/2d5da0cfc011b8d9.js +0 -1
- package/web/.next/static/chunks/634fd97fab9ed4e4.js +0 -4
- package/web/.next/static/chunks/77e4bf0421481629.js +0 -1
- package/web/.next/static/chunks/a86053f0894587f2.js +0 -7
- package/web/.next/static/chunks/beb9625c4a470042.js +0 -1
- package/web/.next/static/chunks/c2244168e74b8c78.js +0 -1
- package/web/.next/static/chunks/c81c1aec4369c77f.js +0 -5
- package/web/.next/static/chunks/d0a69c59b1c0d99c.css +0 -1
- package/web/.next/static/chunks/turbopack-54bc7d566cd2d105.js +0 -4
- /package/web/.next/standalone/web/.next/static/{kABnAk0Y1tlcrUKDlM8UT → static/uXbuwS0U7fRElucaXbKUe}/_buildManifest.js +0 -0
- /package/web/.next/standalone/web/.next/static/{kABnAk0Y1tlcrUKDlM8UT → static/uXbuwS0U7fRElucaXbKUe}/_clientMiddlewareManifest.json +0 -0
- /package/web/.next/standalone/web/.next/static/{kABnAk0Y1tlcrUKDlM8UT → static/uXbuwS0U7fRElucaXbKUe}/_ssgManifest.js +0 -0
- /package/web/.next/standalone/web/.next/static/{static/kABnAk0Y1tlcrUKDlM8UT → uXbuwS0U7fRElucaXbKUe}/_buildManifest.js +0 -0
- /package/web/.next/standalone/web/.next/static/{static/kABnAk0Y1tlcrUKDlM8UT → uXbuwS0U7fRElucaXbKUe}/_clientMiddlewareManifest.json +0 -0
- /package/web/.next/standalone/web/.next/static/{static/kABnAk0Y1tlcrUKDlM8UT → uXbuwS0U7fRElucaXbKUe}/_ssgManifest.js +0 -0
- /package/web/.next/static/{kABnAk0Y1tlcrUKDlM8UT → uXbuwS0U7fRElucaXbKUe}/_buildManifest.js +0 -0
- /package/web/.next/static/{kABnAk0Y1tlcrUKDlM8UT → uXbuwS0U7fRElucaXbKUe}/_clientMiddlewareManifest.json +0 -0
- /package/web/.next/static/{kABnAk0Y1tlcrUKDlM8UT → uXbuwS0U7fRElucaXbKUe}/_ssgManifest.js +0 -0
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { useState, useRef, useEffect } from 'react';
|
|
4
|
+
import { useRouter } from 'next/navigation';
|
|
4
5
|
import Image from 'next/image';
|
|
6
|
+
import { cn } from '@/lib/utils';
|
|
5
7
|
import { Badge } from '@/components/ui/badge';
|
|
6
8
|
import { Button } from '@/components/ui/button';
|
|
7
9
|
import {
|
|
@@ -70,10 +72,18 @@ import {
|
|
|
70
72
|
import {
|
|
71
73
|
PromptInput,
|
|
72
74
|
PromptInputBody,
|
|
73
|
-
PromptInputTextarea,
|
|
74
75
|
PromptInputFooter,
|
|
75
76
|
PromptInputSubmit,
|
|
77
|
+
PromptInputHeader,
|
|
78
|
+
usePromptInputAttachments,
|
|
76
79
|
} from '@/components/ai-elements/prompt-input';
|
|
80
|
+
import {
|
|
81
|
+
Attachments,
|
|
82
|
+
Attachment,
|
|
83
|
+
AttachmentPreview,
|
|
84
|
+
AttachmentRemove,
|
|
85
|
+
AttachmentInfo,
|
|
86
|
+
} from '@/components/ai-elements/attachments';
|
|
77
87
|
import { Suggestion, Suggestions } from '@/components/ai-elements/suggestion';
|
|
78
88
|
import {
|
|
79
89
|
runAgent,
|
|
@@ -90,6 +100,8 @@ import {
|
|
|
90
100
|
getSessionTodos,
|
|
91
101
|
getSessionCheckpoints,
|
|
92
102
|
revertToCheckpoint,
|
|
103
|
+
checkVersion,
|
|
104
|
+
createSession,
|
|
93
105
|
type Session,
|
|
94
106
|
type SSEEvent,
|
|
95
107
|
type PendingApproval,
|
|
@@ -98,6 +110,8 @@ import {
|
|
|
98
110
|
type TodosResponse,
|
|
99
111
|
type SessionConfig,
|
|
100
112
|
type Checkpoint,
|
|
113
|
+
type RunAgentAttachment,
|
|
114
|
+
type VersionInfo,
|
|
101
115
|
} from '@/lib/api';
|
|
102
116
|
import { TodoPanel } from '@/components/ai-elements/todo-panel';
|
|
103
117
|
import { getConfig, type AppConfig } from '@/lib/config';
|
|
@@ -109,7 +123,7 @@ import {
|
|
|
109
123
|
SelectTrigger,
|
|
110
124
|
SelectValue,
|
|
111
125
|
} from '@/components/ui/select';
|
|
112
|
-
import { MessageSquare, Copy, RefreshCw, AlertTriangle, Terminal as TerminalIcon, FileCode, Radio, Pencil, Check, Settings, RotateCcw, FolderOpen, PanelLeft } from 'lucide-react';
|
|
126
|
+
import { MessageSquare, Copy, RefreshCw, AlertTriangle, Terminal as TerminalIcon, FileCode, Radio, Pencil, Check, Settings, RotateCcw, FolderOpen, PanelLeft, FileIcon, Download, X } from 'lucide-react';
|
|
113
127
|
import { useSidebar } from '@/components/ui/sidebar';
|
|
114
128
|
import {
|
|
115
129
|
Dialog,
|
|
@@ -124,9 +138,15 @@ import { Input } from '@/components/ui/input';
|
|
|
124
138
|
import { WriteFileTool, type WriteFileInput, type WriteFileOutput } from '@/components/ai-elements/write-file-tool';
|
|
125
139
|
import { ReadFileTool, type ReadFileInput, type ReadFileOutput } from '@/components/ai-elements/read-file-tool';
|
|
126
140
|
import { BashTool, type BashInput, type BashOutput } from '@/components/ai-elements/bash-tool';
|
|
141
|
+
import { SearchTool, type SearchInput, type SearchOutput, type SubagentStep } from '@/components/ai-elements/search-tool';
|
|
127
142
|
import { TodoTool, type TodoInput, type TodoOutput } from '@/components/ai-elements/todo-tool';
|
|
128
143
|
import { LoadSkillTool, type LoadSkillInput, type LoadSkillOutput } from '@/components/ai-elements/load-skill-tool';
|
|
129
144
|
import { LinterTool, type LinterInput, type LinterOutput } from '@/components/ai-elements/linter-tool';
|
|
145
|
+
import { SpeechInput } from '@/components/ai-elements/speech-input';
|
|
146
|
+
import {
|
|
147
|
+
MentionInputProvider,
|
|
148
|
+
MentionTextarea,
|
|
149
|
+
} from '@/components/ai-elements/mention-input';
|
|
130
150
|
|
|
131
151
|
interface ToolCallOutput {
|
|
132
152
|
status?: string;
|
|
@@ -152,6 +172,22 @@ interface ToolCallInfo {
|
|
|
152
172
|
terminalId?: string;
|
|
153
173
|
/** Live terminal output being streamed */
|
|
154
174
|
liveOutput?: string;
|
|
175
|
+
/** Live content for write_file streaming */
|
|
176
|
+
liveContent?: string;
|
|
177
|
+
/** Live old_string for write_file str_replace mode */
|
|
178
|
+
liveOldString?: string;
|
|
179
|
+
/** Live new_string for write_file str_replace mode */
|
|
180
|
+
liveNewString?: string;
|
|
181
|
+
/** Search subagent steps - for live search progress */
|
|
182
|
+
searchSteps?: SubagentStep[];
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** Attachment stored with user messages */
|
|
186
|
+
interface UserAttachment {
|
|
187
|
+
type: 'image' | 'file';
|
|
188
|
+
data: string; // base64 data URL
|
|
189
|
+
mediaType?: string;
|
|
190
|
+
filename?: string;
|
|
155
191
|
}
|
|
156
192
|
|
|
157
193
|
interface ChatItem {
|
|
@@ -161,6 +197,8 @@ interface ChatItem {
|
|
|
161
197
|
toolCall?: ToolCallInfo;
|
|
162
198
|
/** For user messages: the message sequence number (used for revert) */
|
|
163
199
|
messageSequence?: number;
|
|
200
|
+
/** For user messages: any attached files/images */
|
|
201
|
+
attachments?: UserAttachment[];
|
|
164
202
|
}
|
|
165
203
|
|
|
166
204
|
interface ChatInterfaceProps {
|
|
@@ -204,10 +242,62 @@ function SidebarToggle() {
|
|
|
204
242
|
);
|
|
205
243
|
}
|
|
206
244
|
|
|
245
|
+
// Component to display attachments in the prompt input
|
|
246
|
+
function PromptInputAttachmentsDisplay() {
|
|
247
|
+
const attachments = usePromptInputAttachments();
|
|
248
|
+
|
|
249
|
+
if (attachments.files.length === 0) {
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return (
|
|
254
|
+
<Attachments variant="inline">
|
|
255
|
+
{attachments.files.map((attachment) => (
|
|
256
|
+
<Attachment
|
|
257
|
+
data={attachment}
|
|
258
|
+
key={attachment.id}
|
|
259
|
+
onRemove={() => attachments.remove(attachment.id)}
|
|
260
|
+
>
|
|
261
|
+
<AttachmentPreview />
|
|
262
|
+
<AttachmentInfo />
|
|
263
|
+
<AttachmentRemove />
|
|
264
|
+
</Attachment>
|
|
265
|
+
))}
|
|
266
|
+
</Attachments>
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Custom submit button that checks both text input and attachments
|
|
271
|
+
function ChatSubmitButton({
|
|
272
|
+
input,
|
|
273
|
+
isRunning,
|
|
274
|
+
onStop
|
|
275
|
+
}: {
|
|
276
|
+
input: string;
|
|
277
|
+
isRunning: boolean;
|
|
278
|
+
onStop: () => void;
|
|
279
|
+
}) {
|
|
280
|
+
const attachments = usePromptInputAttachments();
|
|
281
|
+
const hasContent = input.trim() || attachments.files.length > 0;
|
|
282
|
+
|
|
283
|
+
return (
|
|
284
|
+
<PromptInputSubmit
|
|
285
|
+
disabled={!isRunning && !hasContent}
|
|
286
|
+
status={isRunning ? 'streaming' : 'ready'}
|
|
287
|
+
onStop={onStop}
|
|
288
|
+
className="bg-primary hover:bg-primary/90 transition-colors"
|
|
289
|
+
/>
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
|
|
207
293
|
export function ChatInterface({ session }: ChatInterfaceProps) {
|
|
208
294
|
const [chatItems, setChatItems] = useState<ChatItem[]>([]);
|
|
209
295
|
const [input, setInput] = useState('');
|
|
296
|
+
const [interimTranscript, setInterimTranscript] = useState('');
|
|
210
297
|
const [isRunning, setIsRunning] = useState(false);
|
|
298
|
+
const [sseDebugEnabled, setSseDebugEnabled] = useState(false);
|
|
299
|
+
const [sseDebugEvents, setSseDebugEvents] = useState<string[]>([]);
|
|
300
|
+
const [sseDebugErrors, setSseDebugErrors] = useState<string[]>([]);
|
|
211
301
|
const [isWatching, setIsWatching] = useState(false); // True when watching another client's stream
|
|
212
302
|
const [currentStreamId, setCurrentStreamId] = useState<string | null>(null);
|
|
213
303
|
const [currentText, setCurrentText] = useState('');
|
|
@@ -238,11 +328,34 @@ export function ChatInterface({ session }: ChatInterfaceProps) {
|
|
|
238
328
|
const [sessionConfig, setSessionConfig] = useState<SessionConfig>(session.config || {});
|
|
239
329
|
const [sessionSettingsOpen, setSessionSettingsOpen] = useState(false);
|
|
240
330
|
const nameInputRef = useRef<HTMLInputElement>(null);
|
|
331
|
+
|
|
332
|
+
// Version check state
|
|
333
|
+
const [versionInfo, setVersionInfo] = useState<VersionInfo | null>(null);
|
|
334
|
+
const [updateBannerDismissed, setUpdateBannerDismissed] = useState(false);
|
|
335
|
+
const [isCreatingUpdateSession, setIsCreatingUpdateSession] = useState(false);
|
|
336
|
+
const router = useRouter();
|
|
241
337
|
|
|
242
338
|
// Load config for available models
|
|
243
339
|
useEffect(() => {
|
|
244
340
|
getConfig().then(setConfig);
|
|
245
341
|
}, []);
|
|
342
|
+
|
|
343
|
+
// Check for version updates on mount
|
|
344
|
+
useEffect(() => {
|
|
345
|
+
// Check localStorage for dismissed version
|
|
346
|
+
const dismissedVersion = localStorage.getItem('sparkecoder-dismissed-version');
|
|
347
|
+
|
|
348
|
+
checkVersion()
|
|
349
|
+
.then((info) => {
|
|
350
|
+
// Only show banner if update available and not previously dismissed for this version
|
|
351
|
+
if (info.updateAvailable && info.latestVersion !== dismissedVersion) {
|
|
352
|
+
setVersionInfo(info);
|
|
353
|
+
}
|
|
354
|
+
})
|
|
355
|
+
.catch((err) => {
|
|
356
|
+
console.error('Failed to check version:', err);
|
|
357
|
+
});
|
|
358
|
+
}, []);
|
|
246
359
|
|
|
247
360
|
// Sync local state when session changes (e.g., switching sessions or loading from CLI)
|
|
248
361
|
useEffect(() => {
|
|
@@ -286,6 +399,36 @@ export function ChatInterface({ session }: ChatInterfaceProps) {
|
|
|
286
399
|
}
|
|
287
400
|
};
|
|
288
401
|
|
|
402
|
+
// Handle creating a session to update sparkecoder
|
|
403
|
+
const handleCreateUpdateSession = async () => {
|
|
404
|
+
if (!versionInfo?.updateCommand) return;
|
|
405
|
+
|
|
406
|
+
setIsCreatingUpdateSession(true);
|
|
407
|
+
try {
|
|
408
|
+
const updateSession = await createSession({
|
|
409
|
+
name: `Update to v${versionInfo.latestVersion}`,
|
|
410
|
+
});
|
|
411
|
+
mutateSessions();
|
|
412
|
+
// Navigate to the new session - the user can then ask the agent to run the update
|
|
413
|
+
router.push(`/session/${updateSession.id}`);
|
|
414
|
+
// Dismiss the banner
|
|
415
|
+
setUpdateBannerDismissed(true);
|
|
416
|
+
} catch (err) {
|
|
417
|
+
console.error('Failed to create update session:', err);
|
|
418
|
+
} finally {
|
|
419
|
+
setIsCreatingUpdateSession(false);
|
|
420
|
+
}
|
|
421
|
+
};
|
|
422
|
+
|
|
423
|
+
// Handle dismissing the update banner
|
|
424
|
+
const handleDismissUpdateBanner = () => {
|
|
425
|
+
setUpdateBannerDismissed(true);
|
|
426
|
+
// Remember this version was dismissed
|
|
427
|
+
if (versionInfo?.latestVersion) {
|
|
428
|
+
localStorage.setItem('sparkecoder-dismissed-version', versionInfo.latestVersion);
|
|
429
|
+
}
|
|
430
|
+
};
|
|
431
|
+
|
|
289
432
|
// Helper to unwrap AI Gateway format (output may be wrapped as { type: "json", value: ... })
|
|
290
433
|
const unwrapOutput = (output: unknown): unknown => {
|
|
291
434
|
if (output && typeof output === 'object' && 'type' in output && 'value' in output) {
|
|
@@ -317,11 +460,40 @@ export function ChatInterface({ session }: ChatInterfaceProps) {
|
|
|
317
460
|
let messageSequence = 0;
|
|
318
461
|
for (const msg of apiMessages) {
|
|
319
462
|
if (msg.role === 'user') {
|
|
463
|
+
// Extract text and attachments from user message
|
|
464
|
+
let textContent = '';
|
|
465
|
+
const attachments: UserAttachment[] = [];
|
|
466
|
+
|
|
467
|
+
if (typeof msg.content === 'string') {
|
|
468
|
+
textContent = msg.content;
|
|
469
|
+
} else if (Array.isArray(msg.content)) {
|
|
470
|
+
for (const part of msg.content) {
|
|
471
|
+
if (part.type === 'text' && 'text' in part) {
|
|
472
|
+
textContent += (part as { type: 'text'; text: string }).text;
|
|
473
|
+
} else if (part.type === 'image' && 'image' in part) {
|
|
474
|
+
const imagePart = part as { type: 'image'; image: string; mediaType?: string };
|
|
475
|
+
attachments.push({
|
|
476
|
+
type: 'image',
|
|
477
|
+
data: imagePart.image,
|
|
478
|
+
mediaType: imagePart.mediaType,
|
|
479
|
+
});
|
|
480
|
+
} else if (part.type === 'file' && 'data' in part) {
|
|
481
|
+
const filePart = part as { type: 'file'; data: string; mediaType?: string };
|
|
482
|
+
attachments.push({
|
|
483
|
+
type: 'file',
|
|
484
|
+
data: filePart.data,
|
|
485
|
+
mediaType: filePart.mediaType,
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
320
491
|
items.push({
|
|
321
492
|
id: msg.id,
|
|
322
493
|
type: 'user-message',
|
|
323
|
-
content:
|
|
494
|
+
content: textContent,
|
|
324
495
|
messageSequence, // Track sequence for revert
|
|
496
|
+
attachments: attachments.length > 0 ? attachments : undefined,
|
|
325
497
|
});
|
|
326
498
|
} else if (msg.role === 'assistant') {
|
|
327
499
|
if (typeof msg.content === 'string') {
|
|
@@ -477,7 +649,10 @@ export function ChatInterface({ session }: ChatInterfaceProps) {
|
|
|
477
649
|
if (event.type === 'tool-progress') {
|
|
478
650
|
// Terminal started - subscribe to live output stream
|
|
479
651
|
if (event.toolName === 'bash' && event.data.status === 'started') {
|
|
480
|
-
const terminalId = event.data.terminalId;
|
|
652
|
+
const terminalId = typeof event.data.terminalId === 'string' ? event.data.terminalId : undefined;
|
|
653
|
+
if (!terminalId) {
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
481
656
|
|
|
482
657
|
// Find the most recent bash tool call that doesn't have a terminal ID yet
|
|
483
658
|
// Note: Status might be 'streaming' or 'running' depending on event order
|
|
@@ -511,6 +686,178 @@ export function ChatInterface({ session }: ChatInterfaceProps) {
|
|
|
511
686
|
terminalStreamsRef.current.set(terminalId, cancelStream);
|
|
512
687
|
}
|
|
513
688
|
}
|
|
689
|
+
|
|
690
|
+
// Write file progress - stream content as it's being written
|
|
691
|
+
// NOTE: Progress events may arrive BEFORE tool-input-available because the AI SDK
|
|
692
|
+
// executes tools before emitting the tool-call part. We handle this by creating
|
|
693
|
+
// a placeholder tool call when we receive the first progress event.
|
|
694
|
+
if (event.toolName === 'write_file') {
|
|
695
|
+
const data = event.data as {
|
|
696
|
+
status: 'started' | 'content' | 'completed';
|
|
697
|
+
path?: string;
|
|
698
|
+
relativePath?: string;
|
|
699
|
+
mode?: 'full' | 'str_replace';
|
|
700
|
+
content?: string;
|
|
701
|
+
oldString?: string;
|
|
702
|
+
newString?: string;
|
|
703
|
+
chunkIndex?: number;
|
|
704
|
+
chunkCount?: number;
|
|
705
|
+
chunkStart?: number;
|
|
706
|
+
isChunked?: boolean;
|
|
707
|
+
};
|
|
708
|
+
|
|
709
|
+
console.log(
|
|
710
|
+
'[FRONTEND] tool-progress received:',
|
|
711
|
+
event.toolName,
|
|
712
|
+
data.status,
|
|
713
|
+
data.isChunked ? `chunk=${data.chunkIndex}/${data.chunkCount}` : '',
|
|
714
|
+
`contentLength=${data.content?.length || 0}`,
|
|
715
|
+
'currentToolCalls:',
|
|
716
|
+
toolCallsRef.current.length
|
|
717
|
+
);
|
|
718
|
+
|
|
719
|
+
// Find existing write_file tool call that's running/streaming
|
|
720
|
+
const existingWriteFile = toolCallsRef.current.find(
|
|
721
|
+
(tc) => tc.toolName === 'write_file' && (tc.status === 'running' || tc.status === 'streaming')
|
|
722
|
+
);
|
|
723
|
+
|
|
724
|
+
if (data.status === 'started') {
|
|
725
|
+
if (!existingWriteFile) {
|
|
726
|
+
// Progress arrived before tool-input-available - create placeholder tool call
|
|
727
|
+
// Use the path as a temporary ID (will be updated when tool-input-available arrives)
|
|
728
|
+
console.log('[FRONTEND] Creating placeholder write_file tool call for:', data.path);
|
|
729
|
+
const placeholderToolCall: ToolCallInfo = {
|
|
730
|
+
toolCallId: `write_file_pending_${Date.now()}`,
|
|
731
|
+
toolName: 'write_file',
|
|
732
|
+
input: { path: data.relativePath || data.path, mode: data.mode || 'full' },
|
|
733
|
+
status: 'streaming',
|
|
734
|
+
liveContent: '',
|
|
735
|
+
liveOldString: '',
|
|
736
|
+
liveNewString: '',
|
|
737
|
+
};
|
|
738
|
+
toolCallsRef.current = [...toolCallsRef.current, placeholderToolCall];
|
|
739
|
+
console.log('[FRONTEND] After adding placeholder, toolCallsRef.current:', toolCallsRef.current.length);
|
|
740
|
+
} else {
|
|
741
|
+
// Tool call exists - initialize live content fields
|
|
742
|
+
toolCallsRef.current = toolCallsRef.current.map((tc) => {
|
|
743
|
+
if (tc.toolName === 'write_file' && (tc.status === 'running' || tc.status === 'streaming')) {
|
|
744
|
+
return {
|
|
745
|
+
...tc,
|
|
746
|
+
liveContent: '',
|
|
747
|
+
liveOldString: '',
|
|
748
|
+
liveNewString: '',
|
|
749
|
+
};
|
|
750
|
+
}
|
|
751
|
+
return tc;
|
|
752
|
+
});
|
|
753
|
+
}
|
|
754
|
+
setCurrentToolCalls([...toolCallsRef.current]);
|
|
755
|
+
} else if (data.status === 'content') {
|
|
756
|
+
console.log('[FRONTEND] Received content for write_file, length:', data.content?.length || 0, 'existingWriteFile:', !!existingWriteFile);
|
|
757
|
+
if (!existingWriteFile) {
|
|
758
|
+
// Content arrived before we even got 'started' - create placeholder with content
|
|
759
|
+
console.log('[FRONTEND] Creating placeholder with content');
|
|
760
|
+
const placeholderToolCall: ToolCallInfo = {
|
|
761
|
+
toolCallId: `write_file_pending_${Date.now()}`,
|
|
762
|
+
toolName: 'write_file',
|
|
763
|
+
input: { path: data.relativePath || data.path, mode: data.mode || 'full' },
|
|
764
|
+
status: 'streaming',
|
|
765
|
+
liveContent: data.mode !== 'str_replace' ? (data.content || '') : '',
|
|
766
|
+
liveOldString: data.mode === 'str_replace' ? (data.oldString || '') : '',
|
|
767
|
+
liveNewString: data.mode === 'str_replace' ? (data.newString || '') : '',
|
|
768
|
+
};
|
|
769
|
+
toolCallsRef.current = [...toolCallsRef.current, placeholderToolCall];
|
|
770
|
+
} else {
|
|
771
|
+
console.log('[FRONTEND] Updating existing write_file with content');
|
|
772
|
+
// Update the tool call with the content
|
|
773
|
+
toolCallsRef.current = toolCallsRef.current.map((tc) => {
|
|
774
|
+
if (tc.toolName === 'write_file' && (tc.status === 'running' || tc.status === 'streaming')) {
|
|
775
|
+
if (data.mode === 'str_replace') {
|
|
776
|
+
return {
|
|
777
|
+
...tc,
|
|
778
|
+
liveOldString: data.oldString || '',
|
|
779
|
+
liveNewString: data.newString || '',
|
|
780
|
+
};
|
|
781
|
+
} else {
|
|
782
|
+
const isChunked = data.isChunked || data.chunkIndex !== undefined || data.chunkCount !== undefined;
|
|
783
|
+
const nextContent = data.content || '';
|
|
784
|
+
return {
|
|
785
|
+
...tc,
|
|
786
|
+
liveContent: isChunked ? (tc.liveContent || '') + nextContent : nextContent,
|
|
787
|
+
};
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
return tc;
|
|
791
|
+
});
|
|
792
|
+
}
|
|
793
|
+
setCurrentToolCalls([...toolCallsRef.current]);
|
|
794
|
+
}
|
|
795
|
+
// 'completed' status is handled by the tool-output-available event
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
// Search tool progress - stream subagent steps as they happen
|
|
799
|
+
if (event.toolName === 'search') {
|
|
800
|
+
const data = event.data as {
|
|
801
|
+
status: 'started' | 'step' | 'complete' | 'error';
|
|
802
|
+
subagentId?: string;
|
|
803
|
+
stepType?: 'thought' | 'tool_call' | 'tool_result' | 'text';
|
|
804
|
+
stepContent?: string;
|
|
805
|
+
toolName?: string;
|
|
806
|
+
toolInput?: unknown;
|
|
807
|
+
toolOutput?: unknown;
|
|
808
|
+
error?: string;
|
|
809
|
+
};
|
|
810
|
+
|
|
811
|
+
console.log('[FRONTEND] Search tool-progress:', data.status, data.stepType || '');
|
|
812
|
+
|
|
813
|
+
// Find existing search tool call that's running/streaming
|
|
814
|
+
const existingSearch = toolCallsRef.current.find(
|
|
815
|
+
(tc) => tc.toolName === 'search' && (tc.status === 'running' || tc.status === 'streaming')
|
|
816
|
+
);
|
|
817
|
+
|
|
818
|
+
if (data.status === 'started') {
|
|
819
|
+
if (!existingSearch) {
|
|
820
|
+
// Progress arrived before tool-input-available - create placeholder
|
|
821
|
+
const placeholderToolCall: ToolCallInfo = {
|
|
822
|
+
toolCallId: `search_pending_${Date.now()}`,
|
|
823
|
+
toolName: 'search',
|
|
824
|
+
input: {},
|
|
825
|
+
status: 'streaming',
|
|
826
|
+
searchSteps: [],
|
|
827
|
+
};
|
|
828
|
+
toolCallsRef.current = [...toolCallsRef.current, placeholderToolCall];
|
|
829
|
+
} else {
|
|
830
|
+
// Initialize search steps
|
|
831
|
+
toolCallsRef.current = toolCallsRef.current.map((tc) => {
|
|
832
|
+
if (tc.toolName === 'search' && (tc.status === 'running' || tc.status === 'streaming')) {
|
|
833
|
+
return { ...tc, searchSteps: [] };
|
|
834
|
+
}
|
|
835
|
+
return tc;
|
|
836
|
+
});
|
|
837
|
+
}
|
|
838
|
+
setCurrentToolCalls([...toolCallsRef.current]);
|
|
839
|
+
} else if (data.status === 'step' && data.stepType) {
|
|
840
|
+
// Add step to the search tool call
|
|
841
|
+
const newStep: SubagentStep = {
|
|
842
|
+
id: `step_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`,
|
|
843
|
+
type: data.stepType,
|
|
844
|
+
content: data.stepContent || '',
|
|
845
|
+
toolName: data.toolName,
|
|
846
|
+
toolInput: data.toolInput,
|
|
847
|
+
toolOutput: data.toolOutput,
|
|
848
|
+
timestamp: Date.now(),
|
|
849
|
+
};
|
|
850
|
+
|
|
851
|
+
toolCallsRef.current = toolCallsRef.current.map((tc) => {
|
|
852
|
+
if (tc.toolName === 'search' && (tc.status === 'running' || tc.status === 'streaming')) {
|
|
853
|
+
return { ...tc, searchSteps: [...(tc.searchSteps || []), newStep] };
|
|
854
|
+
}
|
|
855
|
+
return tc;
|
|
856
|
+
});
|
|
857
|
+
setCurrentToolCalls([...toolCallsRef.current]);
|
|
858
|
+
}
|
|
859
|
+
// 'complete' and 'error' are handled by tool-output-available
|
|
860
|
+
}
|
|
514
861
|
return;
|
|
515
862
|
}
|
|
516
863
|
|
|
@@ -529,10 +876,36 @@ export function ChatInterface({ session }: ChatInterfaceProps) {
|
|
|
529
876
|
// Skip if this client initiated the stream (we already added the message in handleSubmit)
|
|
530
877
|
if (event.data?.content && !isStreamInitiatorRef.current) {
|
|
531
878
|
setChatItems((prev) => {
|
|
879
|
+
// Parse content - can be string or array with text/image/file parts
|
|
880
|
+
let textContent = '';
|
|
881
|
+
const attachments: UserAttachment[] = [];
|
|
882
|
+
|
|
883
|
+
const rawContent = event.data.content;
|
|
884
|
+
if (typeof rawContent === 'string') {
|
|
885
|
+
textContent = rawContent;
|
|
886
|
+
} else if (Array.isArray(rawContent)) {
|
|
887
|
+
for (const part of rawContent as Array<{ type: string; text?: string; image?: string; data?: string; mediaType?: string }>) {
|
|
888
|
+
if (part.type === 'text' && part.text) {
|
|
889
|
+
textContent += part.text;
|
|
890
|
+
} else if (part.type === 'image' && part.image) {
|
|
891
|
+
attachments.push({
|
|
892
|
+
type: 'image',
|
|
893
|
+
data: part.image,
|
|
894
|
+
mediaType: part.mediaType,
|
|
895
|
+
});
|
|
896
|
+
} else if (part.type === 'file' && part.data) {
|
|
897
|
+
attachments.push({
|
|
898
|
+
type: 'file',
|
|
899
|
+
data: part.data,
|
|
900
|
+
mediaType: part.mediaType,
|
|
901
|
+
});
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
|
|
532
906
|
// Check if we already have a user message with this content
|
|
533
|
-
// (handles ID mismatches between API-loaded messages and SSE events)
|
|
534
907
|
const contentExists = prev.some(
|
|
535
|
-
(item) => item.type === 'user-message' && item.content ===
|
|
908
|
+
(item) => item.type === 'user-message' && item.content === textContent
|
|
536
909
|
);
|
|
537
910
|
if (contentExists) return prev;
|
|
538
911
|
|
|
@@ -540,7 +913,8 @@ export function ChatInterface({ session }: ChatInterfaceProps) {
|
|
|
540
913
|
return [...prev, {
|
|
541
914
|
id: messageId,
|
|
542
915
|
type: 'user-message',
|
|
543
|
-
content:
|
|
916
|
+
content: textContent,
|
|
917
|
+
attachments: attachments.length > 0 ? attachments : undefined,
|
|
544
918
|
}];
|
|
545
919
|
});
|
|
546
920
|
}
|
|
@@ -629,6 +1003,25 @@ export function ChatInterface({ session }: ChatInterfaceProps) {
|
|
|
629
1003
|
// Check if this tool call already exists (was streaming via tool-input-start)
|
|
630
1004
|
const existingToolCall = toolCallsRef.current.find(tc => tc.toolCallId === event.toolCallId);
|
|
631
1005
|
|
|
1006
|
+
// Check for placeholder write_file tool call created from early progress events
|
|
1007
|
+
// (Progress events arrive before tool-input-available due to AI SDK timing)
|
|
1008
|
+
const placeholderWriteFile = event.toolName === 'write_file'
|
|
1009
|
+
? toolCallsRef.current.find(tc =>
|
|
1010
|
+
tc.toolName === 'write_file' &&
|
|
1011
|
+
tc.toolCallId.startsWith('write_file_pending_') &&
|
|
1012
|
+
tc.status === 'streaming'
|
|
1013
|
+
)
|
|
1014
|
+
: undefined;
|
|
1015
|
+
|
|
1016
|
+
// Check for placeholder search tool call created from early progress events
|
|
1017
|
+
const placeholderSearch = event.toolName === 'search'
|
|
1018
|
+
? toolCallsRef.current.find(tc =>
|
|
1019
|
+
tc.toolName === 'search' &&
|
|
1020
|
+
tc.toolCallId.startsWith('search_pending_') &&
|
|
1021
|
+
tc.status === 'streaming'
|
|
1022
|
+
)
|
|
1023
|
+
: undefined;
|
|
1024
|
+
|
|
632
1025
|
// Flush any remaining text before showing tool
|
|
633
1026
|
if (currentTextRef.current.trim()) {
|
|
634
1027
|
const textItem: ChatItem = {
|
|
@@ -641,17 +1034,35 @@ export function ChatInterface({ session }: ChatInterfaceProps) {
|
|
|
641
1034
|
setCurrentText('');
|
|
642
1035
|
}
|
|
643
1036
|
|
|
644
|
-
// Create/update the tool call info
|
|
1037
|
+
// Create/update the tool call info, preserving live content from placeholder
|
|
645
1038
|
const toolCallInfo: ToolCallInfo = {
|
|
646
1039
|
toolCallId: event.toolCallId,
|
|
647
1040
|
toolName: event.toolName,
|
|
648
1041
|
input: event.input,
|
|
649
1042
|
status: 'running' as const,
|
|
650
1043
|
streamingArgsText: existingToolCall?.streamingArgsText,
|
|
1044
|
+
// Preserve live content from placeholder if it exists
|
|
1045
|
+
liveContent: placeholderWriteFile?.liveContent,
|
|
1046
|
+
liveOldString: placeholderWriteFile?.liveOldString,
|
|
1047
|
+
liveNewString: placeholderWriteFile?.liveNewString,
|
|
1048
|
+
// Preserve search steps from placeholder if it exists
|
|
1049
|
+
searchSteps: placeholderSearch?.searchSteps,
|
|
651
1050
|
};
|
|
652
1051
|
|
|
653
1052
|
// Update toolCallsRef
|
|
654
|
-
if (
|
|
1053
|
+
if (placeholderWriteFile) {
|
|
1054
|
+
// Replace the placeholder with the real tool call (with real toolCallId)
|
|
1055
|
+
toolCallsRef.current = toolCallsRef.current.map((tc) => {
|
|
1056
|
+
if (tc.toolCallId === placeholderWriteFile.toolCallId) return toolCallInfo;
|
|
1057
|
+
return tc;
|
|
1058
|
+
});
|
|
1059
|
+
} else if (placeholderSearch) {
|
|
1060
|
+
// Replace the search placeholder with the real tool call (with real toolCallId)
|
|
1061
|
+
toolCallsRef.current = toolCallsRef.current.map((tc) => {
|
|
1062
|
+
if (tc.toolCallId === placeholderSearch.toolCallId) return toolCallInfo;
|
|
1063
|
+
return tc;
|
|
1064
|
+
});
|
|
1065
|
+
} else if (existingToolCall) {
|
|
655
1066
|
toolCallsRef.current = toolCallsRef.current.map((tc) => {
|
|
656
1067
|
if (tc.toolCallId !== event.toolCallId) return tc;
|
|
657
1068
|
return toolCallInfo;
|
|
@@ -818,6 +1229,9 @@ export function ChatInterface({ session }: ChatInterfaceProps) {
|
|
|
818
1229
|
|
|
819
1230
|
// Load messages and check for active streams when session changes
|
|
820
1231
|
useEffect(() => {
|
|
1232
|
+
// Track if this effect is stale (session changed during async work)
|
|
1233
|
+
let isStale = false;
|
|
1234
|
+
|
|
821
1235
|
const loadMessagesAndCheckStream = async () => {
|
|
822
1236
|
setIsLoadingHistory(true);
|
|
823
1237
|
setChatItems([]);
|
|
@@ -838,6 +1252,10 @@ export function ChatInterface({ session }: ChatInterfaceProps) {
|
|
|
838
1252
|
getSessionMessages(session.id),
|
|
839
1253
|
getSessionCheckpoints(session.id).catch(() => ({ checkpoints: [] })),
|
|
840
1254
|
]);
|
|
1255
|
+
|
|
1256
|
+
// Don't update state if session changed during async work
|
|
1257
|
+
if (isStale) return;
|
|
1258
|
+
|
|
841
1259
|
const sorted = [...apiMessages].sort((a, b) =>
|
|
842
1260
|
new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()
|
|
843
1261
|
);
|
|
@@ -847,6 +1265,10 @@ export function ChatInterface({ session }: ChatInterfaceProps) {
|
|
|
847
1265
|
|
|
848
1266
|
// Check if there's an active stream to watch
|
|
849
1267
|
const streamInfo = await getActiveStream(session.id);
|
|
1268
|
+
|
|
1269
|
+
// Check again after await
|
|
1270
|
+
if (isStale) return;
|
|
1271
|
+
|
|
850
1272
|
if (streamInfo.hasActiveStream && streamInfo.stream) {
|
|
851
1273
|
console.log('Found active stream, connecting...', streamInfo.stream.streamId);
|
|
852
1274
|
isStreamInitiatorRef.current = false; // We're watching, not initiating
|
|
@@ -862,9 +1284,13 @@ export function ChatInterface({ session }: ChatInterfaceProps) {
|
|
|
862
1284
|
cancelRef.current = cancel;
|
|
863
1285
|
}
|
|
864
1286
|
} catch (err) {
|
|
865
|
-
|
|
1287
|
+
if (!isStale) {
|
|
1288
|
+
console.error('Failed to load messages:', err);
|
|
1289
|
+
}
|
|
866
1290
|
} finally {
|
|
867
|
-
|
|
1291
|
+
if (!isStale) {
|
|
1292
|
+
setIsLoadingHistory(false);
|
|
1293
|
+
}
|
|
868
1294
|
}
|
|
869
1295
|
};
|
|
870
1296
|
|
|
@@ -875,6 +1301,7 @@ export function ChatInterface({ session }: ChatInterfaceProps) {
|
|
|
875
1301
|
|
|
876
1302
|
// Cleanup on unmount or session change
|
|
877
1303
|
return () => {
|
|
1304
|
+
isStale = true; // Mark as stale so async work doesn't update state
|
|
878
1305
|
if (cancelRef.current) {
|
|
879
1306
|
cancelRef.current();
|
|
880
1307
|
cancelRef.current = null;
|
|
@@ -887,9 +1314,15 @@ export function ChatInterface({ session }: ChatInterfaceProps) {
|
|
|
887
1314
|
|
|
888
1315
|
// Check for pending approvals - API is source of truth
|
|
889
1316
|
useEffect(() => {
|
|
1317
|
+
// Track if this effect is stale (session changed during async work)
|
|
1318
|
+
let isStale = false;
|
|
1319
|
+
const currentSessionId = session.id;
|
|
1320
|
+
|
|
890
1321
|
const checkApprovals = async () => {
|
|
891
1322
|
try {
|
|
892
|
-
const approvals = await getPendingApprovals(
|
|
1323
|
+
const approvals = await getPendingApprovals(currentSessionId);
|
|
1324
|
+
// Don't update state if session changed during async work
|
|
1325
|
+
if (isStale) return;
|
|
893
1326
|
// Use API response as source of truth - this correctly filters out
|
|
894
1327
|
// already-handled approvals that might have come from SSE replay
|
|
895
1328
|
setPendingApprovals(approvals);
|
|
@@ -900,14 +1333,23 @@ export function ChatInterface({ session }: ChatInterfaceProps) {
|
|
|
900
1333
|
// Initial check
|
|
901
1334
|
checkApprovals();
|
|
902
1335
|
const interval = setInterval(checkApprovals, 2000);
|
|
903
|
-
return () =>
|
|
1336
|
+
return () => {
|
|
1337
|
+
isStale = true;
|
|
1338
|
+
clearInterval(interval);
|
|
1339
|
+
};
|
|
904
1340
|
}, [session.id]);
|
|
905
1341
|
|
|
906
1342
|
// Poll for todos - more frequently when running
|
|
907
1343
|
useEffect(() => {
|
|
1344
|
+
// Track if this effect is stale (session changed during async work)
|
|
1345
|
+
let isStale = false;
|
|
1346
|
+
const currentSessionId = session.id;
|
|
1347
|
+
|
|
908
1348
|
const checkTodos = async () => {
|
|
909
1349
|
try {
|
|
910
|
-
const data = await getSessionTodos(
|
|
1350
|
+
const data = await getSessionTodos(currentSessionId);
|
|
1351
|
+
// Don't update state if session changed during async work
|
|
1352
|
+
if (isStale) return;
|
|
911
1353
|
setTodosData(data);
|
|
912
1354
|
} catch {
|
|
913
1355
|
// Ignore errors
|
|
@@ -918,12 +1360,55 @@ export function ChatInterface({ session }: ChatInterfaceProps) {
|
|
|
918
1360
|
// Poll every 2 seconds when running, 5 seconds otherwise
|
|
919
1361
|
const pollInterval = isRunning ? 1000 : 5000;
|
|
920
1362
|
const interval = setInterval(checkTodos, pollInterval);
|
|
921
|
-
return () =>
|
|
1363
|
+
return () => {
|
|
1364
|
+
isStale = true;
|
|
1365
|
+
clearInterval(interval);
|
|
1366
|
+
};
|
|
922
1367
|
}, [session.id, isRunning]);
|
|
923
1368
|
|
|
1369
|
+
// Optional SSE debug overlay (enable with ?debugSSE=1)
|
|
1370
|
+
useEffect(() => {
|
|
1371
|
+
if (typeof window === 'undefined') return;
|
|
1372
|
+
const params = new URLSearchParams(window.location.search);
|
|
1373
|
+
const enabled = params.get('debugSSE') === '1';
|
|
1374
|
+
setSseDebugEnabled(enabled);
|
|
1375
|
+
if (!enabled) return;
|
|
1376
|
+
|
|
1377
|
+
(window as any).__sparkeSseDebug = true;
|
|
1378
|
+
|
|
1379
|
+
const onEvent = (event: Event) => {
|
|
1380
|
+
const detail = (event as CustomEvent<{ type?: string; size?: number; label?: string; toolName?: string }>).detail;
|
|
1381
|
+
const meta = [detail?.label, detail?.toolName].filter(Boolean).join(' ');
|
|
1382
|
+
const label = detail?.type
|
|
1383
|
+
? `${detail.type}${meta ? ` ${meta}` : ''} (${detail?.size ?? 0}b)`
|
|
1384
|
+
: 'unknown';
|
|
1385
|
+
setSseDebugEvents((prev) => [...prev.slice(-49), label]);
|
|
1386
|
+
};
|
|
1387
|
+
|
|
1388
|
+
const onParseError = (event: Event) => {
|
|
1389
|
+
const detail = (event as CustomEvent<{ error?: string; size?: number }>).detail;
|
|
1390
|
+
const label = `parse-error: ${detail?.error || 'unknown'} (${detail?.size ?? 0}b)`;
|
|
1391
|
+
setSseDebugErrors((prev) => [...prev.slice(-49), label]);
|
|
1392
|
+
};
|
|
1393
|
+
|
|
1394
|
+
window.addEventListener('sparke:sse-event', onEvent as EventListener);
|
|
1395
|
+
window.addEventListener('sparke:sse-parse-error', onParseError as EventListener);
|
|
1396
|
+
|
|
1397
|
+
return () => {
|
|
1398
|
+
window.removeEventListener('sparke:sse-event', onEvent as EventListener);
|
|
1399
|
+
window.removeEventListener('sparke:sse-parse-error', onParseError as EventListener);
|
|
1400
|
+
(window as any).__sparkeSseDebug = false;
|
|
1401
|
+
};
|
|
1402
|
+
}, []);
|
|
1403
|
+
|
|
924
1404
|
// Poll for new active streams when not currently streaming
|
|
925
1405
|
// This allows auto-attaching to streams started from CLI or other tabs
|
|
926
1406
|
useEffect(() => {
|
|
1407
|
+
// Track if this effect is stale (session changed during async work)
|
|
1408
|
+
let isStale = false;
|
|
1409
|
+
// Capture session.id for async closure
|
|
1410
|
+
const currentSessionId = session.id;
|
|
1411
|
+
|
|
927
1412
|
const checkForNewStream = async () => {
|
|
928
1413
|
// Skip if we're already running, watching, or connecting
|
|
929
1414
|
if (isRunning || isConnectingRef.current) {
|
|
@@ -931,7 +1416,10 @@ export function ChatInterface({ session }: ChatInterfaceProps) {
|
|
|
931
1416
|
}
|
|
932
1417
|
|
|
933
1418
|
try {
|
|
934
|
-
const streamInfo = await getActiveStream(
|
|
1419
|
+
const streamInfo = await getActiveStream(currentSessionId);
|
|
1420
|
+
|
|
1421
|
+
// Don't process if session changed
|
|
1422
|
+
if (isStale) return;
|
|
935
1423
|
|
|
936
1424
|
if (streamInfo.hasActiveStream && streamInfo.stream) {
|
|
937
1425
|
const newStreamId = streamInfo.stream.streamId;
|
|
@@ -948,7 +1436,14 @@ export function ChatInterface({ session }: ChatInterfaceProps) {
|
|
|
948
1436
|
// Refresh messages from server to get the user message that triggered this stream
|
|
949
1437
|
// This ensures we see the user message even if we missed the SSE event
|
|
950
1438
|
try {
|
|
951
|
-
const apiMessages = await getSessionMessages(
|
|
1439
|
+
const apiMessages = await getSessionMessages(currentSessionId);
|
|
1440
|
+
|
|
1441
|
+
// Check again after await
|
|
1442
|
+
if (isStale) {
|
|
1443
|
+
isConnectingRef.current = false;
|
|
1444
|
+
return;
|
|
1445
|
+
}
|
|
1446
|
+
|
|
952
1447
|
const sorted = [...apiMessages].sort((a, b) =>
|
|
953
1448
|
new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()
|
|
954
1449
|
);
|
|
@@ -958,13 +1453,19 @@ export function ChatInterface({ session }: ChatInterfaceProps) {
|
|
|
958
1453
|
console.error('Failed to refresh messages:', err);
|
|
959
1454
|
}
|
|
960
1455
|
|
|
1456
|
+
// Final stale check before setting up stream
|
|
1457
|
+
if (isStale) {
|
|
1458
|
+
isConnectingRef.current = false;
|
|
1459
|
+
return;
|
|
1460
|
+
}
|
|
1461
|
+
|
|
961
1462
|
setIsWatching(true);
|
|
962
1463
|
setIsRunning(true);
|
|
963
1464
|
setCurrentStreamId(newStreamId);
|
|
964
1465
|
lastKnownStreamIdRef.current = newStreamId;
|
|
965
1466
|
|
|
966
1467
|
// Start watching the stream
|
|
967
|
-
const cancel = watchStream(
|
|
1468
|
+
const cancel = watchStream(currentSessionId, handleSSEEvent, {
|
|
968
1469
|
streamId: newStreamId,
|
|
969
1470
|
onStreamId: (id) => {
|
|
970
1471
|
setCurrentStreamId(id);
|
|
@@ -985,17 +1486,28 @@ export function ChatInterface({ session }: ChatInterfaceProps) {
|
|
|
985
1486
|
const interval = setInterval(checkForNewStream, 1000);
|
|
986
1487
|
|
|
987
1488
|
return () => {
|
|
1489
|
+
isStale = true; // Mark as stale so async work doesn't update state
|
|
988
1490
|
clearInterval(interval);
|
|
989
1491
|
};
|
|
990
1492
|
}, [session.id, isRunning]);
|
|
991
1493
|
|
|
992
|
-
const handleSubmit = () => {
|
|
993
|
-
if (!
|
|
1494
|
+
const handleSubmit = (promptText: string, attachments?: RunAgentAttachment[]) => {
|
|
1495
|
+
if (!promptText.trim() && (!attachments || attachments.length === 0)) return;
|
|
1496
|
+
if (isRunning) return;
|
|
1497
|
+
|
|
1498
|
+
// Convert RunAgentAttachment to UserAttachment for display
|
|
1499
|
+
const userAttachments: UserAttachment[] | undefined = attachments?.map((a) => ({
|
|
1500
|
+
type: a.type,
|
|
1501
|
+
data: a.data,
|
|
1502
|
+
mediaType: a.mediaType,
|
|
1503
|
+
filename: a.filename,
|
|
1504
|
+
}));
|
|
994
1505
|
|
|
995
1506
|
const userItem: ChatItem = {
|
|
996
1507
|
id: `user-${Date.now()}`,
|
|
997
1508
|
type: 'user-message',
|
|
998
|
-
content:
|
|
1509
|
+
content: promptText || '',
|
|
1510
|
+
attachments: userAttachments,
|
|
999
1511
|
};
|
|
1000
1512
|
|
|
1001
1513
|
setChatItems((prev) => [...prev, userItem]);
|
|
@@ -1011,14 +1523,66 @@ export function ChatInterface({ session }: ChatInterfaceProps) {
|
|
|
1011
1523
|
currentReasoningRef.current = '';
|
|
1012
1524
|
toolCallsRef.current = [];
|
|
1013
1525
|
|
|
1014
|
-
const cancel = runAgent(session.id,
|
|
1526
|
+
const cancel = runAgent(session.id, promptText || 'Please analyze the attached files.', handleSSEEvent, {
|
|
1015
1527
|
onStreamId: (id) => setCurrentStreamId(id),
|
|
1528
|
+
attachments,
|
|
1016
1529
|
});
|
|
1017
1530
|
|
|
1018
1531
|
// Store the cancel function so we can call it from the stop button
|
|
1019
1532
|
cancelRef.current = cancel;
|
|
1020
1533
|
};
|
|
1021
1534
|
|
|
1535
|
+
// Handler for PromptInput that handles file attachments
|
|
1536
|
+
// Note: @ mentions are now inline in the text (e.g., "analyze @src/components/")
|
|
1537
|
+
const handlePromptSubmit = async (message: { text: string; files: Array<{ url?: string; filename?: string; mediaType?: string }> }) => {
|
|
1538
|
+
if (isRunning) return;
|
|
1539
|
+
|
|
1540
|
+
const hasText = Boolean(message.text?.trim());
|
|
1541
|
+
const hasFiles = Boolean(message.files?.length);
|
|
1542
|
+
|
|
1543
|
+
if (!hasText && !hasFiles) return;
|
|
1544
|
+
|
|
1545
|
+
// Convert files to attachments for the API
|
|
1546
|
+
const attachments: RunAgentAttachment[] = [];
|
|
1547
|
+
|
|
1548
|
+
if (hasFiles && message.files.length > 0) {
|
|
1549
|
+
for (const file of message.files) {
|
|
1550
|
+
if (!file.url) continue;
|
|
1551
|
+
|
|
1552
|
+
try {
|
|
1553
|
+
// Fetch the blob and convert to base64
|
|
1554
|
+
const response = await fetch(file.url);
|
|
1555
|
+
const blob = await response.blob();
|
|
1556
|
+
const base64 = await new Promise<string>((resolve) => {
|
|
1557
|
+
const reader = new FileReader();
|
|
1558
|
+
reader.onloadend = () => resolve(reader.result as string);
|
|
1559
|
+
reader.readAsDataURL(blob);
|
|
1560
|
+
});
|
|
1561
|
+
|
|
1562
|
+
// Determine if it's an image or file
|
|
1563
|
+
const mediaType = file.mediaType || blob.type || 'application/octet-stream';
|
|
1564
|
+
const isImage = mediaType.startsWith('image/');
|
|
1565
|
+
|
|
1566
|
+
attachments.push({
|
|
1567
|
+
type: isImage ? 'image' : 'file',
|
|
1568
|
+
data: base64,
|
|
1569
|
+
mediaType,
|
|
1570
|
+
filename: file.filename,
|
|
1571
|
+
});
|
|
1572
|
+
} catch (err) {
|
|
1573
|
+
console.error('Failed to process attachment:', err);
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1576
|
+
}
|
|
1577
|
+
|
|
1578
|
+
// Text already contains @mentions inline (e.g., "explain @src/utils.ts")
|
|
1579
|
+
// Just send it directly - the agent will see the @ references
|
|
1580
|
+
handleSubmit(message.text || '', attachments.length > 0 ? attachments : undefined);
|
|
1581
|
+
};
|
|
1582
|
+
|
|
1583
|
+
// Slash commands now insert prompt text directly via MentionTextarea
|
|
1584
|
+
// No handler needed - the component inserts the prompt text inline
|
|
1585
|
+
|
|
1022
1586
|
const handleStop = async () => {
|
|
1023
1587
|
// Send abort request to server - this stops the agent properly
|
|
1024
1588
|
// The agent will send an abort event back through the stream
|
|
@@ -1070,29 +1634,34 @@ export function ChatInterface({ session }: ChatInterfaceProps) {
|
|
|
1070
1634
|
};
|
|
1071
1635
|
|
|
1072
1636
|
const handleApprove = async (approval: PendingApproval) => {
|
|
1637
|
+
// Always remove from UI immediately - if it's stale, it shouldn't be shown anyway
|
|
1638
|
+
setPendingApprovals((prev) => prev.filter((a) => a.id !== approval.id));
|
|
1073
1639
|
try {
|
|
1074
1640
|
await approveExecution(session.id, approval.toolCallId);
|
|
1075
|
-
setPendingApprovals((prev) => prev.filter((a) => a.id !== approval.id));
|
|
1076
1641
|
} catch (err) {
|
|
1077
1642
|
console.error('Failed to approve:', err);
|
|
1643
|
+
// Don't add back - the API poll will restore it if it's still valid
|
|
1078
1644
|
}
|
|
1079
1645
|
};
|
|
1080
1646
|
|
|
1081
1647
|
const handleReject = async (approval: PendingApproval) => {
|
|
1648
|
+
// Always remove from UI immediately - if it's stale, it shouldn't be shown anyway
|
|
1649
|
+
setPendingApprovals((prev) => prev.filter((a) => a.id !== approval.id));
|
|
1082
1650
|
try {
|
|
1083
1651
|
await rejectExecution(session.id, approval.toolCallId, 'User rejected');
|
|
1084
|
-
setPendingApprovals((prev) => prev.filter((a) => a.id !== approval.id));
|
|
1085
1652
|
} catch (err) {
|
|
1086
1653
|
console.error('Failed to reject:', err);
|
|
1654
|
+
// Don't add back - the API poll will restore it if it's still valid
|
|
1087
1655
|
}
|
|
1088
1656
|
};
|
|
1089
1657
|
|
|
1090
1658
|
// Handle "Don't show again" - approve and disable approval for this tool
|
|
1091
1659
|
const handleApproveAndDisable = async (approval: PendingApproval) => {
|
|
1660
|
+
// Always remove from UI immediately - if it's stale, it shouldn't be shown anyway
|
|
1661
|
+
setPendingApprovals((prev) => prev.filter((a) => a.id !== approval.id));
|
|
1092
1662
|
try {
|
|
1093
1663
|
// First approve this execution
|
|
1094
1664
|
await approveExecution(session.id, approval.toolCallId);
|
|
1095
|
-
setPendingApprovals((prev) => prev.filter((a) => a.id !== approval.id));
|
|
1096
1665
|
|
|
1097
1666
|
// Then disable approval for this tool in session config
|
|
1098
1667
|
const updatedSession = await updateToolApproval(
|
|
@@ -1106,6 +1675,7 @@ export function ChatInterface({ session }: ChatInterfaceProps) {
|
|
|
1106
1675
|
setSessionConfig(updatedSession.config || {});
|
|
1107
1676
|
} catch (err) {
|
|
1108
1677
|
console.error('Failed to approve and disable:', err);
|
|
1678
|
+
// Don't add back - the API poll will restore it if it's still valid
|
|
1109
1679
|
}
|
|
1110
1680
|
};
|
|
1111
1681
|
|
|
@@ -1204,6 +1774,19 @@ export function ChatInterface({ session }: ChatInterfaceProps) {
|
|
|
1204
1774
|
);
|
|
1205
1775
|
}
|
|
1206
1776
|
|
|
1777
|
+
// Search tool - use dedicated SearchTool component with subagent steps
|
|
1778
|
+
if (tc.toolName === 'search') {
|
|
1779
|
+
return (
|
|
1780
|
+
<SearchTool
|
|
1781
|
+
input={tc.input as SearchInput}
|
|
1782
|
+
output={tc.output as SearchOutput}
|
|
1783
|
+
status={tc.status}
|
|
1784
|
+
steps={tc.searchSteps}
|
|
1785
|
+
className="mt-2"
|
|
1786
|
+
/>
|
|
1787
|
+
);
|
|
1788
|
+
}
|
|
1789
|
+
|
|
1207
1790
|
// Todo tool - use dedicated TodoTool component
|
|
1208
1791
|
if (tc.toolName === 'todo') {
|
|
1209
1792
|
return (
|
|
@@ -1588,6 +2171,57 @@ export function ChatInterface({ session }: ChatInterfaceProps) {
|
|
|
1588
2171
|
</div>
|
|
1589
2172
|
</div>
|
|
1590
2173
|
|
|
2174
|
+
{/* Update Available Banner */}
|
|
2175
|
+
{versionInfo?.updateAvailable && !updateBannerDismissed && (
|
|
2176
|
+
<div className="px-4 py-2 border-b border-amber-500/30 bg-amber-500/10 flex items-center justify-between gap-3">
|
|
2177
|
+
<div className="flex items-center gap-2 min-w-0">
|
|
2178
|
+
<Download className="size-4 text-amber-600 dark:text-amber-400 shrink-0" />
|
|
2179
|
+
<span className="text-sm text-amber-700 dark:text-amber-300">
|
|
2180
|
+
Update available: <strong>v{versionInfo.latestVersion}</strong>
|
|
2181
|
+
<span className="text-amber-600/70 dark:text-amber-400/70 ml-1">
|
|
2182
|
+
(current: v{versionInfo.currentVersion})
|
|
2183
|
+
</span>
|
|
2184
|
+
</span>
|
|
2185
|
+
</div>
|
|
2186
|
+
<div className="flex items-center gap-2 shrink-0">
|
|
2187
|
+
<Button
|
|
2188
|
+
size="sm"
|
|
2189
|
+
variant="outline"
|
|
2190
|
+
className="h-7 text-xs border-amber-500/50 bg-amber-500/20 hover:bg-amber-500/30 text-amber-700 dark:text-amber-300"
|
|
2191
|
+
onClick={handleCreateUpdateSession}
|
|
2192
|
+
disabled={isCreatingUpdateSession}
|
|
2193
|
+
>
|
|
2194
|
+
{isCreatingUpdateSession ? (
|
|
2195
|
+
<>
|
|
2196
|
+
<RefreshCw className="size-3 mr-1.5 animate-spin" />
|
|
2197
|
+
Creating...
|
|
2198
|
+
</>
|
|
2199
|
+
) : (
|
|
2200
|
+
<>
|
|
2201
|
+
<Download className="size-3 mr-1.5" />
|
|
2202
|
+
Update Now
|
|
2203
|
+
</>
|
|
2204
|
+
)}
|
|
2205
|
+
</Button>
|
|
2206
|
+
<TooltipProvider>
|
|
2207
|
+
<Tooltip>
|
|
2208
|
+
<TooltipTrigger asChild>
|
|
2209
|
+
<Button
|
|
2210
|
+
size="icon"
|
|
2211
|
+
variant="ghost"
|
|
2212
|
+
className="size-6 text-amber-600 dark:text-amber-400 hover:bg-amber-500/20"
|
|
2213
|
+
onClick={handleDismissUpdateBanner}
|
|
2214
|
+
>
|
|
2215
|
+
<X className="size-3.5" />
|
|
2216
|
+
</Button>
|
|
2217
|
+
</TooltipTrigger>
|
|
2218
|
+
<TooltipContent>Dismiss (won't show again for this version)</TooltipContent>
|
|
2219
|
+
</Tooltip>
|
|
2220
|
+
</TooltipProvider>
|
|
2221
|
+
</div>
|
|
2222
|
+
</div>
|
|
2223
|
+
)}
|
|
2224
|
+
|
|
1591
2225
|
{/* Todo Panel - sticky at top */}
|
|
1592
2226
|
{todosData && todosData.todos && todosData.todos.length > 0 && (
|
|
1593
2227
|
<div className="px-4 py-2 border-b border-border/50 bg-muted/20">
|
|
@@ -1699,12 +2333,22 @@ export function ChatInterface({ session }: ChatInterfaceProps) {
|
|
|
1699
2333
|
{/* Chat items */}
|
|
1700
2334
|
{chatItems
|
|
1701
2335
|
// Filter out tool calls that are pending approval (shown in approval section above)
|
|
2336
|
+
// Also filter out tool calls that are currently streaming (shown in streaming section below)
|
|
1702
2337
|
.filter((item) => {
|
|
1703
2338
|
if (item.type === 'tool-call' && item.toolCall) {
|
|
1704
2339
|
const isPendingApproval = pendingApprovals.some(
|
|
1705
2340
|
(a) => a.toolCallId === item.toolCall?.toolCallId
|
|
1706
2341
|
);
|
|
1707
2342
|
if (isPendingApproval) return false;
|
|
2343
|
+
|
|
2344
|
+
// Don't show if it's currently in the streaming tool calls
|
|
2345
|
+
const isStreaming = currentToolCalls.some(
|
|
2346
|
+
(tc) => tc.toolCallId === item.toolCall?.toolCallId ||
|
|
2347
|
+
// Also check for placeholder matches (same tool name, streaming status)
|
|
2348
|
+
(tc.toolName === item.toolCall?.toolName &&
|
|
2349
|
+
(tc.status === 'streaming' || tc.status === 'running'))
|
|
2350
|
+
);
|
|
2351
|
+
if (isStreaming) return false;
|
|
1708
2352
|
}
|
|
1709
2353
|
return true;
|
|
1710
2354
|
})
|
|
@@ -1717,7 +2361,35 @@ export function ChatInterface({ session }: ChatInterfaceProps) {
|
|
|
1717
2361
|
return (
|
|
1718
2362
|
<Message key={item.id} from="user">
|
|
1719
2363
|
<MessageContent>
|
|
1720
|
-
|
|
2364
|
+
{/* Display attachments if any */}
|
|
2365
|
+
{item.attachments && item.attachments.length > 0 && (
|
|
2366
|
+
<div className="flex flex-wrap gap-2 mb-2">
|
|
2367
|
+
{item.attachments.map((attachment, idx) => (
|
|
2368
|
+
<div key={idx} className="relative">
|
|
2369
|
+
{attachment.type === 'image' ? (
|
|
2370
|
+
<div className="relative rounded-lg overflow-hidden border border-border/50 max-w-[200px]">
|
|
2371
|
+
{/* eslint-disable-next-line @next/next/no-img-element */}
|
|
2372
|
+
<img
|
|
2373
|
+
src={attachment.data.startsWith('data:') ? attachment.data : `data:${attachment.mediaType || 'image/png'};base64,${attachment.data}`}
|
|
2374
|
+
alt={attachment.filename || 'Image attachment'}
|
|
2375
|
+
className="max-w-full h-auto max-h-[150px] object-contain"
|
|
2376
|
+
/>
|
|
2377
|
+
</div>
|
|
2378
|
+
) : (
|
|
2379
|
+
<div className="flex items-center gap-2 px-3 py-2 rounded-lg bg-muted/50 border border-border/50">
|
|
2380
|
+
<FileIcon className="h-4 w-4 text-muted-foreground" />
|
|
2381
|
+
<span className="text-sm text-muted-foreground truncate max-w-[150px]">
|
|
2382
|
+
{attachment.filename || 'File'}
|
|
2383
|
+
</span>
|
|
2384
|
+
</div>
|
|
2385
|
+
)}
|
|
2386
|
+
</div>
|
|
2387
|
+
))}
|
|
2388
|
+
</div>
|
|
2389
|
+
)}
|
|
2390
|
+
{item.content && (
|
|
2391
|
+
<p className="whitespace-pre-wrap">{item.content}</p>
|
|
2392
|
+
)}
|
|
1721
2393
|
</MessageContent>
|
|
1722
2394
|
{hasCheckpoint && !isRunning && (
|
|
1723
2395
|
<MessageActions className="justify-end mt-1">
|
|
@@ -1779,6 +2451,7 @@ export function ChatInterface({ session }: ChatInterfaceProps) {
|
|
|
1779
2451
|
onClick={() => navigator.clipboard.writeText(item.content || '')}
|
|
1780
2452
|
>
|
|
1781
2453
|
<Copy className="size-3" />
|
|
2454
|
+
|
|
1782
2455
|
</MessageAction>
|
|
1783
2456
|
</MessageActions>
|
|
1784
2457
|
</div>
|
|
@@ -1800,6 +2473,9 @@ export function ChatInterface({ session }: ChatInterfaceProps) {
|
|
|
1800
2473
|
output={tc.output as WriteFileOutput}
|
|
1801
2474
|
status={tc.status}
|
|
1802
2475
|
streamingArgsText={tc.streamingArgsText}
|
|
2476
|
+
liveContent={tc.liveContent}
|
|
2477
|
+
liveOldString={tc.liveOldString}
|
|
2478
|
+
liveNewString={tc.liveNewString}
|
|
1803
2479
|
/>
|
|
1804
2480
|
);
|
|
1805
2481
|
}
|
|
@@ -1830,6 +2506,19 @@ export function ChatInterface({ session }: ChatInterfaceProps) {
|
|
|
1830
2506
|
);
|
|
1831
2507
|
}
|
|
1832
2508
|
|
|
2509
|
+
// Use dedicated SearchTool component for search
|
|
2510
|
+
if (tc.toolName === 'search') {
|
|
2511
|
+
return (
|
|
2512
|
+
<SearchTool
|
|
2513
|
+
key={item.id}
|
|
2514
|
+
input={tc.input as SearchInput}
|
|
2515
|
+
output={tc.output as SearchOutput}
|
|
2516
|
+
status={tc.status}
|
|
2517
|
+
steps={tc.searchSteps}
|
|
2518
|
+
/>
|
|
2519
|
+
);
|
|
2520
|
+
}
|
|
2521
|
+
|
|
1833
2522
|
// Use dedicated TodoTool component for todo
|
|
1834
2523
|
if (tc.toolName === 'todo') {
|
|
1835
2524
|
return (
|
|
@@ -1949,6 +2638,9 @@ export function ChatInterface({ session }: ChatInterfaceProps) {
|
|
|
1949
2638
|
output={tc.output as WriteFileOutput}
|
|
1950
2639
|
status={tc.status}
|
|
1951
2640
|
streamingArgsText={tc.streamingArgsText}
|
|
2641
|
+
liveContent={tc.liveContent}
|
|
2642
|
+
liveOldString={tc.liveOldString}
|
|
2643
|
+
liveNewString={tc.liveNewString}
|
|
1952
2644
|
/>
|
|
1953
2645
|
);
|
|
1954
2646
|
}
|
|
@@ -1979,6 +2671,19 @@ export function ChatInterface({ session }: ChatInterfaceProps) {
|
|
|
1979
2671
|
);
|
|
1980
2672
|
}
|
|
1981
2673
|
|
|
2674
|
+
// Use dedicated SearchTool component for search
|
|
2675
|
+
if (tc.toolName === 'search') {
|
|
2676
|
+
return (
|
|
2677
|
+
<SearchTool
|
|
2678
|
+
key={tc.toolCallId}
|
|
2679
|
+
input={tc.input as SearchInput}
|
|
2680
|
+
output={tc.output as SearchOutput}
|
|
2681
|
+
status={tc.status}
|
|
2682
|
+
steps={tc.searchSteps}
|
|
2683
|
+
/>
|
|
2684
|
+
);
|
|
2685
|
+
}
|
|
2686
|
+
|
|
1982
2687
|
// Use dedicated TodoTool component for todo
|
|
1983
2688
|
if (tc.toolName === 'todo') {
|
|
1984
2689
|
return (
|
|
@@ -2046,6 +2751,23 @@ export function ChatInterface({ session }: ChatInterfaceProps) {
|
|
|
2046
2751
|
<ConversationScrollButton />
|
|
2047
2752
|
</Conversation>
|
|
2048
2753
|
|
|
2754
|
+
{sseDebugEnabled && (
|
|
2755
|
+
<div className="fixed bottom-28 right-4 w-80 max-h-80 overflow-auto border bg-background/95 shadow-lg rounded-md text-xs">
|
|
2756
|
+
<div className="px-3 py-2 border-b font-mono text-muted-foreground">SSE Debug</div>
|
|
2757
|
+
<div className="px-3 py-2 space-y-1">
|
|
2758
|
+
{sseDebugEvents.length === 0 && sseDebugErrors.length === 0 && (
|
|
2759
|
+
<div className="text-muted-foreground">No events yet</div>
|
|
2760
|
+
)}
|
|
2761
|
+
{sseDebugEvents.map((evt, index) => (
|
|
2762
|
+
<div key={`evt-${index}`} className="font-mono">{evt}</div>
|
|
2763
|
+
))}
|
|
2764
|
+
{sseDebugErrors.map((err, index) => (
|
|
2765
|
+
<div key={`err-${index}`} className="font-mono text-red-600">{err}</div>
|
|
2766
|
+
))}
|
|
2767
|
+
</div>
|
|
2768
|
+
</div>
|
|
2769
|
+
)}
|
|
2770
|
+
|
|
2049
2771
|
{/* Input */}
|
|
2050
2772
|
<div className="border-t border-border/50 p-4 pb-6 bg-card/30">
|
|
2051
2773
|
<div className="max-w-3xl mx-auto">
|
|
@@ -2064,30 +2786,78 @@ export function ChatInterface({ session }: ChatInterfaceProps) {
|
|
|
2064
2786
|
</Suggestions>
|
|
2065
2787
|
)}
|
|
2066
2788
|
|
|
2067
|
-
<
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
className="
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2789
|
+
<MentionInputProvider>
|
|
2790
|
+
<PromptInput
|
|
2791
|
+
onSubmit={handlePromptSubmit}
|
|
2792
|
+
className="shadow-sm"
|
|
2793
|
+
globalDrop
|
|
2794
|
+
multiple
|
|
2795
|
+
>
|
|
2796
|
+
<PromptInputHeader>
|
|
2797
|
+
<PromptInputAttachmentsDisplay />
|
|
2798
|
+
</PromptInputHeader>
|
|
2799
|
+
<PromptInputBody>
|
|
2800
|
+
<div className="relative w-full">
|
|
2801
|
+
<MentionTextarea
|
|
2802
|
+
sessionId={session.id}
|
|
2803
|
+
value={input + (interimTranscript ? (input && !input.endsWith(' ') && !input.endsWith('\n') ? ' ' : '') + interimTranscript : '')}
|
|
2804
|
+
onChange={(value) => {
|
|
2805
|
+
// Only update if not currently showing interim transcript
|
|
2806
|
+
if (!interimTranscript) {
|
|
2807
|
+
setInput(value);
|
|
2808
|
+
} else {
|
|
2809
|
+
// If user types while interim is showing, clear interim and use their input
|
|
2810
|
+
setInterimTranscript('');
|
|
2811
|
+
setInput(value);
|
|
2812
|
+
}
|
|
2813
|
+
}}
|
|
2814
|
+
placeholder="Ask SparkECoder... (@ to mention files, / for commands)"
|
|
2815
|
+
disabled={isRunning}
|
|
2816
|
+
autoFocus
|
|
2817
|
+
className={cn(
|
|
2818
|
+
"min-h-[80px] focus:ring-2 focus:ring-primary/20 transition-all field-sizing-content max-h-48",
|
|
2819
|
+
interimTranscript && "caret-red-500"
|
|
2820
|
+
)}
|
|
2821
|
+
/>
|
|
2822
|
+
{/* Live transcription indicator */}
|
|
2823
|
+
{interimTranscript && (
|
|
2824
|
+
<div className="absolute bottom-2 right-2 flex items-center gap-1.5 text-xs text-red-500 bg-background/80 backdrop-blur-sm px-2 py-1 rounded-full">
|
|
2825
|
+
<span className="size-2 bg-red-500 rounded-full animate-pulse" />
|
|
2826
|
+
Listening...
|
|
2827
|
+
</div>
|
|
2828
|
+
)}
|
|
2829
|
+
</div>
|
|
2830
|
+
</PromptInputBody>
|
|
2831
|
+
<PromptInputFooter>
|
|
2832
|
+
<div className="flex items-center gap-1 text-xs text-muted-foreground">
|
|
2833
|
+
<span className="hidden sm:inline">Type @ to mention files, / for commands</span>
|
|
2834
|
+
</div>
|
|
2835
|
+
<div className="flex items-center gap-2">
|
|
2836
|
+
<SpeechInput
|
|
2837
|
+
size="icon"
|
|
2838
|
+
className="size-9"
|
|
2839
|
+
onTranscriptionChange={(text) => {
|
|
2840
|
+
// Add finalized transcript to input
|
|
2841
|
+
setInput((prev) => {
|
|
2842
|
+
const needsSpace = prev && !prev.endsWith(' ') && !prev.endsWith('\n');
|
|
2843
|
+
return prev + (needsSpace ? ' ' : '') + text;
|
|
2844
|
+
});
|
|
2845
|
+
}}
|
|
2846
|
+
onInterimTranscription={(text) => {
|
|
2847
|
+
// Show live preview of what's being spoken
|
|
2848
|
+
setInterimTranscript(text);
|
|
2849
|
+
}}
|
|
2850
|
+
disabled={isRunning}
|
|
2851
|
+
/>
|
|
2852
|
+
<ChatSubmitButton
|
|
2853
|
+
input={input + interimTranscript}
|
|
2854
|
+
isRunning={isRunning}
|
|
2855
|
+
onStop={handleStop}
|
|
2856
|
+
/>
|
|
2857
|
+
</div>
|
|
2858
|
+
</PromptInputFooter>
|
|
2859
|
+
</PromptInput>
|
|
2860
|
+
</MentionInputProvider>
|
|
2091
2861
|
</div>
|
|
2092
2862
|
</div>
|
|
2093
2863
|
</div>
|