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.
Files changed (292) hide show
  1. package/dist/agent/index.d.ts +3 -3
  2. package/dist/agent/index.js +1361 -215
  3. package/dist/agent/index.js.map +1 -1
  4. package/dist/cli.js +2179 -349
  5. package/dist/cli.js.map +1 -1
  6. package/dist/db/index.d.ts +20 -2
  7. package/dist/db/index.js +97 -0
  8. package/dist/db/index.js.map +1 -1
  9. package/dist/{index-BzedNBK-.d.ts → index-BblbmG_0.d.ts} +42 -6
  10. package/dist/index.d.ts +6 -6
  11. package/dist/index.js +2165 -335
  12. package/dist/index.js.map +1 -1
  13. package/dist/{schema-CkrIadxa.d.ts → schema-D_8A4k01.d.ts} +270 -3
  14. package/dist/search-ybREg7F_.d.ts +254 -0
  15. package/dist/server/index.js +2163 -333
  16. package/dist/server/index.js.map +1 -1
  17. package/dist/tools/index.d.ts +7 -56
  18. package/dist/tools/index.js +894 -27
  19. package/dist/tools/index.js.map +1 -1
  20. package/package.json +5 -1
  21. package/web/.next/BUILD_ID +1 -1
  22. package/web/.next/standalone/web/.next/BUILD_ID +1 -1
  23. package/web/.next/standalone/web/.next/app-path-routes-manifest.json +4 -0
  24. package/web/.next/standalone/web/.next/build-manifest.json +7 -6
  25. package/web/.next/standalone/web/.next/prerender-manifest.json +99 -3
  26. package/web/.next/standalone/web/.next/required-server-files.json +28 -4
  27. package/web/.next/standalone/web/.next/routes-manifest.json +24 -0
  28. package/web/.next/standalone/web/.next/server/app/(main)/page/build-manifest.json +5 -4
  29. package/web/.next/standalone/web/.next/server/app/(main)/page.js +2 -2
  30. package/web/.next/standalone/web/.next/server/app/(main)/page.js.nft.json +1 -1
  31. package/web/.next/standalone/web/.next/server/app/(main)/page_client-reference-manifest.js +1 -1
  32. package/web/.next/standalone/web/.next/server/app/(main)/session/[id]/page/build-manifest.json +5 -4
  33. package/web/.next/standalone/web/.next/server/app/(main)/session/[id]/page.js +2 -2
  34. package/web/.next/standalone/web/.next/server/app/(main)/session/[id]/page.js.nft.json +1 -1
  35. package/web/.next/standalone/web/.next/server/app/(main)/session/[id]/page_client-reference-manifest.js +1 -1
  36. package/web/.next/standalone/web/.next/server/app/_global-error/page/build-manifest.json +5 -4
  37. package/web/.next/standalone/web/.next/server/app/_global-error/page.js +2 -2
  38. package/web/.next/standalone/web/.next/server/app/_global-error/page.js.nft.json +1 -1
  39. package/web/.next/standalone/web/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  40. package/web/.next/standalone/web/.next/server/app/_global-error.html +2 -2
  41. package/web/.next/standalone/web/.next/server/app/_global-error.rsc +1 -1
  42. package/web/.next/standalone/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  43. package/web/.next/standalone/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  44. package/web/.next/standalone/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  45. package/web/.next/standalone/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  46. package/web/.next/standalone/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  47. package/web/.next/standalone/web/.next/server/app/_not-found/page/build-manifest.json +5 -4
  48. package/web/.next/standalone/web/.next/server/app/_not-found/page.js +2 -2
  49. package/web/.next/standalone/web/.next/server/app/_not-found/page.js.nft.json +1 -1
  50. package/web/.next/standalone/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  51. package/web/.next/standalone/web/.next/server/app/_not-found.html +1 -1
  52. package/web/.next/standalone/web/.next/server/app/_not-found.rsc +2 -2
  53. package/web/.next/standalone/web/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
  54. package/web/.next/standalone/web/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  55. package/web/.next/standalone/web/.next/server/app/_not-found.segments/_index.segment.rsc +2 -2
  56. package/web/.next/standalone/web/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  57. package/web/.next/standalone/web/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  58. package/web/.next/standalone/web/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  59. package/web/.next/standalone/web/.next/server/app/api/config/route.js.nft.json +1 -1
  60. package/web/.next/standalone/web/.next/server/app/api/health/route.js.nft.json +1 -1
  61. package/web/.next/standalone/web/.next/server/app/docs/installation/page/app-paths-manifest.json +3 -0
  62. package/web/.next/standalone/web/.next/server/app/docs/installation/page/build-manifest.json +18 -0
  63. package/web/.next/standalone/web/.next/server/app/docs/installation/page/next-font-manifest.json +11 -0
  64. package/web/.next/standalone/web/.next/server/app/docs/installation/page/react-loadable-manifest.json +1 -0
  65. package/web/.next/standalone/web/.next/server/app/docs/installation/page/server-reference-manifest.json +4 -0
  66. package/web/.next/standalone/web/.next/server/app/docs/installation/page.js +21 -0
  67. package/web/.next/standalone/web/.next/server/app/docs/installation/page.js.map +5 -0
  68. package/web/.next/standalone/web/.next/server/app/docs/installation/page.js.nft.json +1 -0
  69. package/web/.next/standalone/web/.next/server/app/docs/installation/page_client-reference-manifest.js +2 -0
  70. package/web/.next/standalone/web/.next/server/app/docs/installation.html +86 -0
  71. package/web/.next/standalone/web/.next/server/app/docs/installation.meta +16 -0
  72. package/web/.next/standalone/web/.next/server/app/docs/installation.rsc +36 -0
  73. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/_full.segment.rsc +36 -0
  74. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/_head.segment.rsc +6 -0
  75. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/_index.segment.rsc +7 -0
  76. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/_tree.segment.rsc +3 -0
  77. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/docs/installation/__PAGE__.segment.rsc +22 -0
  78. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/docs/installation.segment.rsc +4 -0
  79. package/web/.next/standalone/web/.next/server/app/docs/installation.segments/docs.segment.rsc +5 -0
  80. package/web/.next/standalone/web/.next/server/app/docs/page/app-paths-manifest.json +3 -0
  81. package/web/.next/standalone/web/.next/server/app/docs/page/build-manifest.json +18 -0
  82. package/web/.next/standalone/web/.next/server/app/docs/page/next-font-manifest.json +11 -0
  83. package/web/.next/standalone/web/.next/server/app/docs/page/react-loadable-manifest.json +1 -0
  84. package/web/.next/standalone/web/.next/server/app/docs/page/server-reference-manifest.json +4 -0
  85. package/web/.next/standalone/web/.next/server/app/docs/page.js +21 -0
  86. package/web/.next/standalone/web/.next/server/app/docs/page.js.map +5 -0
  87. package/web/.next/standalone/web/.next/server/app/docs/page.js.nft.json +1 -0
  88. package/web/.next/standalone/web/.next/server/app/docs/page_client-reference-manifest.js +2 -0
  89. package/web/.next/standalone/web/.next/server/app/docs/skills/page/app-paths-manifest.json +3 -0
  90. package/web/.next/standalone/web/.next/server/app/docs/skills/page/build-manifest.json +18 -0
  91. package/web/.next/standalone/web/.next/server/app/docs/skills/page/next-font-manifest.json +11 -0
  92. package/web/.next/standalone/web/.next/server/app/docs/skills/page/react-loadable-manifest.json +1 -0
  93. package/web/.next/standalone/web/.next/server/app/docs/skills/page/server-reference-manifest.json +4 -0
  94. package/web/.next/standalone/web/.next/server/app/docs/skills/page.js +21 -0
  95. package/web/.next/standalone/web/.next/server/app/docs/skills/page.js.map +5 -0
  96. package/web/.next/standalone/web/.next/server/app/docs/skills/page.js.nft.json +1 -0
  97. package/web/.next/standalone/web/.next/server/app/docs/skills/page_client-reference-manifest.js +2 -0
  98. package/web/.next/standalone/web/.next/server/app/docs/skills.html +268 -0
  99. package/web/.next/standalone/web/.next/server/app/docs/skills.meta +16 -0
  100. package/web/.next/standalone/web/.next/server/app/docs/skills.rsc +82 -0
  101. package/web/.next/standalone/web/.next/server/app/docs/skills.segments/_full.segment.rsc +82 -0
  102. package/web/.next/standalone/web/.next/server/app/docs/skills.segments/_head.segment.rsc +6 -0
  103. package/web/.next/standalone/web/.next/server/app/docs/skills.segments/_index.segment.rsc +7 -0
  104. package/web/.next/standalone/web/.next/server/app/docs/skills.segments/_tree.segment.rsc +3 -0
  105. package/web/.next/standalone/web/.next/server/app/docs/skills.segments/docs/skills/__PAGE__.segment.rsc +66 -0
  106. package/web/.next/standalone/web/.next/server/app/docs/skills.segments/docs/skills.segment.rsc +4 -0
  107. package/web/.next/standalone/web/.next/server/app/docs/skills.segments/docs.segment.rsc +5 -0
  108. package/web/.next/standalone/web/.next/server/app/docs/tools/page/app-paths-manifest.json +3 -0
  109. package/web/.next/standalone/web/.next/server/app/docs/tools/page/build-manifest.json +18 -0
  110. package/web/.next/standalone/web/.next/server/app/docs/tools/page/next-font-manifest.json +11 -0
  111. package/web/.next/standalone/web/.next/server/app/docs/tools/page/react-loadable-manifest.json +1 -0
  112. package/web/.next/standalone/web/.next/server/app/docs/tools/page/server-reference-manifest.json +4 -0
  113. package/web/.next/standalone/web/.next/server/app/docs/tools/page.js +21 -0
  114. package/web/.next/standalone/web/.next/server/app/docs/tools/page.js.map +5 -0
  115. package/web/.next/standalone/web/.next/server/app/docs/tools/page.js.nft.json +1 -0
  116. package/web/.next/standalone/web/.next/server/app/docs/tools/page_client-reference-manifest.js +2 -0
  117. package/web/.next/standalone/web/.next/server/app/docs/tools.html +242 -0
  118. package/web/.next/standalone/web/.next/server/app/docs/tools.meta +16 -0
  119. package/web/.next/standalone/web/.next/server/app/docs/tools.rsc +87 -0
  120. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/_full.segment.rsc +87 -0
  121. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/_head.segment.rsc +6 -0
  122. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/_index.segment.rsc +7 -0
  123. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/_tree.segment.rsc +3 -0
  124. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/docs/tools/__PAGE__.segment.rsc +72 -0
  125. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/docs/tools.segment.rsc +4 -0
  126. package/web/.next/standalone/web/.next/server/app/docs/tools.segments/docs.segment.rsc +5 -0
  127. package/web/.next/standalone/web/.next/server/app/docs.html +74 -0
  128. package/web/.next/standalone/web/.next/server/app/docs.meta +15 -0
  129. package/web/.next/standalone/web/.next/server/app/docs.rsc +34 -0
  130. package/web/.next/standalone/web/.next/server/app/docs.segments/_full.segment.rsc +34 -0
  131. package/web/.next/standalone/web/.next/server/app/docs.segments/_head.segment.rsc +6 -0
  132. package/web/.next/standalone/web/.next/server/app/docs.segments/_index.segment.rsc +7 -0
  133. package/web/.next/standalone/web/.next/server/app/docs.segments/_tree.segment.rsc +3 -0
  134. package/web/.next/standalone/web/.next/server/app/docs.segments/docs/__PAGE__.segment.rsc +20 -0
  135. package/web/.next/standalone/web/.next/server/app/docs.segments/docs.segment.rsc +5 -0
  136. package/web/.next/standalone/web/.next/server/app/index.html +1 -1
  137. package/web/.next/standalone/web/.next/server/app/index.rsc +4 -4
  138. package/web/.next/standalone/web/.next/server/app/index.segments/!KG1haW4p/__PAGE__.segment.rsc +2 -2
  139. package/web/.next/standalone/web/.next/server/app/index.segments/!KG1haW4p.segment.rsc +2 -2
  140. package/web/.next/standalone/web/.next/server/app/index.segments/_full.segment.rsc +4 -4
  141. package/web/.next/standalone/web/.next/server/app/index.segments/_head.segment.rsc +1 -1
  142. package/web/.next/standalone/web/.next/server/app/index.segments/_index.segment.rsc +2 -2
  143. package/web/.next/standalone/web/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  144. package/web/.next/standalone/web/.next/server/app-paths-manifest.json +4 -0
  145. package/web/.next/standalone/web/.next/server/chunks/ssr/{2374f_378282b1._.js → 2374f_244589df._.js} +1 -1
  146. package/web/.next/standalone/web/.next/server/chunks/ssr/{2374f_5de336d2._.js → 2374f_41a27541._.js} +1 -1
  147. package/web/.next/standalone/web/.next/server/chunks/ssr/{2374f_30f9df13._.js → 2374f_47c9e2d5._.js} +1 -1
  148. package/web/.next/standalone/web/.next/server/chunks/ssr/{2374f_d94c2b70._.js → 2374f_4bf2df9d._.js} +1 -1
  149. package/web/.next/standalone/web/.next/server/chunks/ssr/2374f_663d1038._.js +3 -0
  150. package/web/.next/standalone/web/.next/server/chunks/ssr/2374f_954e49c0._.js +3 -0
  151. package/web/.next/standalone/web/.next/server/chunks/ssr/{2374f_1d78db71._.js → 2374f_c33b095a._.js} +1 -1
  152. package/web/.next/standalone/web/.next/server/chunks/ssr/{2374f_bbc99511._.js → 2374f_fa61fbb2._.js} +1 -1
  153. package/web/.next/standalone/web/.next/server/chunks/ssr/{2374f_8825dcc9._.js → 2374f_fb82ac0d._.js} +1 -1
  154. package/web/.next/standalone/web/.next/server/chunks/ssr/{web_96bca05b._.js → 2374f_next_dist_bbe64674._.js} +2 -2
  155. package/web/.next/standalone/web/.next/server/chunks/ssr/{[root-of-the-server]__7f04455b._.js → [root-of-the-server]__1e06ddf7._.js} +2 -2
  156. package/web/.next/standalone/web/.next/server/chunks/ssr/[root-of-the-server]__2b151e1c._.js +3 -0
  157. package/web/.next/standalone/web/.next/server/chunks/ssr/[root-of-the-server]__2dbf511a._.js +9 -0
  158. package/web/.next/standalone/web/.next/server/chunks/ssr/[root-of-the-server]__397fadd4._.js +3 -0
  159. package/web/.next/standalone/web/.next/server/chunks/ssr/[root-of-the-server]__44bd8bd1._.js +3 -0
  160. package/web/.next/standalone/web/.next/server/chunks/ssr/[root-of-the-server]__70cecda8._.js +3 -0
  161. package/web/.next/standalone/web/.next/server/chunks/ssr/[root-of-the-server]__9fdf9974._.js +3 -0
  162. package/web/.next/standalone/web/.next/server/chunks/ssr/{[root-of-the-server]__f18f92f4._.js → [root-of-the-server]__b050bb8f._.js} +2 -2
  163. package/web/.next/standalone/web/.next/server/chunks/ssr/[root-of-the-server]__d3034cd2._.js +3 -0
  164. package/web/.next/standalone/web/.next/server/chunks/ssr/{[root-of-the-server]__c3a1e22c._.js → [root-of-the-server]__ef2713cf._.js} +2 -2
  165. package/web/.next/standalone/web/.next/server/chunks/ssr/[root-of-the-server]__f764bebe._.js +3 -0
  166. package/web/.next/standalone/web/.next/server/chunks/ssr/web_046bf7db._.js +3 -0
  167. package/web/.next/standalone/web/.next/server/chunks/ssr/web_656c1e45._.js +7 -0
  168. package/web/.next/standalone/web/.next/server/chunks/ssr/web_76ccf09f._.js +8 -0
  169. package/web/.next/standalone/web/.next/server/chunks/ssr/web__next-internal_server_app_docs_installation_page_actions_52cc0648.js +3 -0
  170. package/web/.next/standalone/web/.next/server/chunks/ssr/web__next-internal_server_app_docs_page_actions_4fe77da8.js +3 -0
  171. package/web/.next/standalone/web/.next/server/chunks/ssr/web__next-internal_server_app_docs_skills_page_actions_251df2e1.js +3 -0
  172. package/web/.next/standalone/web/.next/server/chunks/ssr/web__next-internal_server_app_docs_tools_page_actions_3e6382b0.js +3 -0
  173. package/web/.next/standalone/web/.next/server/chunks/ssr/web_a565dc94._.js +4 -0
  174. package/web/.next/standalone/web/.next/server/chunks/ssr/web_b1cce0b7._.js +4 -0
  175. package/web/.next/standalone/web/.next/server/chunks/ssr/web_b42ed1be._.js +3 -0
  176. package/web/.next/standalone/web/.next/server/chunks/ssr/web_c0c2bee4._.js +4 -0
  177. package/web/.next/standalone/web/.next/server/chunks/ssr/web_eea9c122._.js +3 -0
  178. package/web/.next/standalone/web/.next/server/chunks/ssr/web_ff00a5c3._.js +4 -0
  179. package/web/.next/standalone/web/.next/server/chunks/ssr/web_src_app_(main)_layout_tsx_453f6492._.js +3 -0
  180. package/web/.next/standalone/web/.next/server/chunks/ssr/web_src_app_(main)_page_tsx_5ac4794b._.js +3 -0
  181. package/web/.next/standalone/web/.next/server/middleware-build-manifest.js +5 -4
  182. package/web/.next/standalone/web/.next/server/next-font-manifest.js +1 -1
  183. package/web/.next/standalone/web/.next/server/next-font-manifest.json +16 -0
  184. package/web/.next/standalone/web/.next/server/pages/404.html +1 -1
  185. package/web/.next/standalone/web/.next/server/pages/500.html +2 -2
  186. package/web/.next/standalone/web/.next/server/server-reference-manifest.js +1 -1
  187. package/web/.next/standalone/web/.next/server/server-reference-manifest.json +1 -1
  188. package/web/.next/standalone/web/.next/static/chunks/0cc382a66266188e.js +7 -0
  189. package/web/.next/standalone/web/.next/static/chunks/0fda34e553582102.js +1 -0
  190. package/web/.next/standalone/web/.next/static/{static/chunks/5ec82ce8f3aabaf0.js → chunks/6407c045dfc908fe.js} +3 -3
  191. package/web/.next/standalone/web/.next/static/chunks/651e187cc15d66de.js +1 -0
  192. package/web/.next/standalone/web/.next/static/chunks/862ced58ce21a270.js +4 -0
  193. package/web/.next/standalone/web/.next/static/chunks/89bc21c0443670f4.js +1 -0
  194. package/web/.next/standalone/web/.next/static/chunks/8f4edf22ededc29b.js +7 -0
  195. package/web/.next/standalone/web/.next/static/chunks/ad6b9dbb257d62cc.js +1 -0
  196. package/web/.next/standalone/web/.next/static/chunks/af22745850132107.css +1 -0
  197. package/web/.next/standalone/web/.next/static/chunks/b9ad1584d4e11d12.js +1 -0
  198. package/web/.next/standalone/web/.next/static/chunks/db9b22c844a35e20.js +5 -0
  199. package/web/.next/standalone/web/.next/static/chunks/turbopack-597558bb7b6982f6.js +4 -0
  200. package/web/.next/standalone/web/.next/static/static/chunks/0cc382a66266188e.js +7 -0
  201. package/web/.next/standalone/web/.next/static/static/chunks/0fda34e553582102.js +1 -0
  202. package/web/.next/{static/chunks/5ec82ce8f3aabaf0.js → standalone/web/.next/static/static/chunks/6407c045dfc908fe.js} +3 -3
  203. package/web/.next/standalone/web/.next/static/static/chunks/651e187cc15d66de.js +1 -0
  204. package/web/.next/standalone/web/.next/static/static/chunks/862ced58ce21a270.js +4 -0
  205. package/web/.next/standalone/web/.next/static/static/chunks/89bc21c0443670f4.js +1 -0
  206. package/web/.next/standalone/web/.next/static/static/chunks/8f4edf22ededc29b.js +7 -0
  207. package/web/.next/standalone/web/.next/static/static/chunks/ad6b9dbb257d62cc.js +1 -0
  208. package/web/.next/standalone/web/.next/static/static/chunks/af22745850132107.css +1 -0
  209. package/web/.next/standalone/web/.next/static/static/chunks/b9ad1584d4e11d12.js +1 -0
  210. package/web/.next/standalone/web/.next/static/static/chunks/db9b22c844a35e20.js +5 -0
  211. package/web/.next/standalone/web/.next/static/static/chunks/turbopack-597558bb7b6982f6.js +4 -0
  212. package/web/.next/standalone/web/mdx-components.tsx +119 -0
  213. package/web/.next/standalone/web/next.config.ts +15 -1
  214. package/web/.next/standalone/web/package-lock.json +559 -4
  215. package/web/.next/standalone/web/package.json +4 -0
  216. package/web/.next/standalone/web/runtime-config.json +1 -1
  217. package/web/.next/standalone/web/server.js +1 -1
  218. package/web/.next/standalone/web/src/app/(main)/page.tsx +127 -5
  219. package/web/.next/standalone/web/src/app/docs/installation/page.mdx +128 -0
  220. package/web/.next/standalone/web/src/app/docs/layout.tsx +74 -0
  221. package/web/.next/standalone/web/src/app/docs/page.mdx +90 -0
  222. package/web/.next/standalone/web/src/app/docs/skills/page.mdx +334 -0
  223. package/web/.next/standalone/web/src/app/docs/tools/page.mdx +300 -0
  224. package/web/.next/standalone/web/src/components/ai-elements/mention-input.tsx +809 -0
  225. package/web/.next/standalone/web/src/components/ai-elements/search-tool.tsx +400 -0
  226. package/web/.next/standalone/web/src/components/ai-elements/speech-input.tsx +89 -36
  227. package/web/.next/standalone/web/src/components/ai-elements/subagent-modal.tsx +275 -0
  228. package/web/.next/standalone/web/src/components/ai-elements/write-file-tool.tsx +19 -5
  229. package/web/.next/standalone/web/src/components/chat-interface.tsx +820 -50
  230. package/web/.next/standalone/web/src/hooks/use-workspace-files.ts +108 -0
  231. package/web/.next/standalone/web/src/lib/api.ts +223 -6
  232. package/web/.next/static/chunks/0cc382a66266188e.js +7 -0
  233. package/web/.next/static/chunks/0fda34e553582102.js +1 -0
  234. package/web/.next/{standalone/web/.next/static/chunks/5ec82ce8f3aabaf0.js → static/chunks/6407c045dfc908fe.js} +3 -3
  235. package/web/.next/static/chunks/651e187cc15d66de.js +1 -0
  236. package/web/.next/static/chunks/862ced58ce21a270.js +4 -0
  237. package/web/.next/static/chunks/89bc21c0443670f4.js +1 -0
  238. package/web/.next/static/chunks/8f4edf22ededc29b.js +7 -0
  239. package/web/.next/static/chunks/ad6b9dbb257d62cc.js +1 -0
  240. package/web/.next/static/chunks/af22745850132107.css +1 -0
  241. package/web/.next/static/chunks/b9ad1584d4e11d12.js +1 -0
  242. package/web/.next/static/chunks/db9b22c844a35e20.js +5 -0
  243. package/web/.next/static/chunks/turbopack-597558bb7b6982f6.js +4 -0
  244. package/web/package.json +4 -0
  245. package/dist/bash-CGAqW7HR.d.ts +0 -80
  246. package/web/.next/standalone/web/.next/server/chunks/ssr/2374f_9bf3c7f3._.js +0 -3
  247. package/web/.next/standalone/web/.next/server/chunks/ssr/[root-of-the-server]__0f6b5fa7._.js +0 -3
  248. package/web/.next/standalone/web/.next/server/chunks/ssr/[root-of-the-server]__513c6b45._.js +0 -3
  249. package/web/.next/standalone/web/.next/server/chunks/ssr/[root-of-the-server]__a984d933._.js +0 -9
  250. package/web/.next/standalone/web/.next/server/chunks/ssr/[root-of-the-server]__de58a952._.js +0 -3
  251. package/web/.next/standalone/web/.next/server/chunks/ssr/web_19b6934c._.js +0 -8
  252. package/web/.next/standalone/web/.next/server/chunks/ssr/web_d7d3e40d._.js +0 -7
  253. package/web/.next/standalone/web/.next/server/chunks/ssr/web_e6034803._.js +0 -3
  254. package/web/.next/standalone/web/.next/static/chunks/03d4169891280e04.js +0 -7
  255. package/web/.next/standalone/web/.next/static/chunks/2d5da0cfc011b8d9.js +0 -1
  256. package/web/.next/standalone/web/.next/static/chunks/634fd97fab9ed4e4.js +0 -4
  257. package/web/.next/standalone/web/.next/static/chunks/77e4bf0421481629.js +0 -1
  258. package/web/.next/standalone/web/.next/static/chunks/a86053f0894587f2.js +0 -7
  259. package/web/.next/standalone/web/.next/static/chunks/beb9625c4a470042.js +0 -1
  260. package/web/.next/standalone/web/.next/static/chunks/c2244168e74b8c78.js +0 -1
  261. package/web/.next/standalone/web/.next/static/chunks/c81c1aec4369c77f.js +0 -5
  262. package/web/.next/standalone/web/.next/static/chunks/d0a69c59b1c0d99c.css +0 -1
  263. package/web/.next/standalone/web/.next/static/chunks/turbopack-54bc7d566cd2d105.js +0 -4
  264. package/web/.next/standalone/web/.next/static/static/chunks/03d4169891280e04.js +0 -7
  265. package/web/.next/standalone/web/.next/static/static/chunks/2d5da0cfc011b8d9.js +0 -1
  266. package/web/.next/standalone/web/.next/static/static/chunks/634fd97fab9ed4e4.js +0 -4
  267. package/web/.next/standalone/web/.next/static/static/chunks/77e4bf0421481629.js +0 -1
  268. package/web/.next/standalone/web/.next/static/static/chunks/a86053f0894587f2.js +0 -7
  269. package/web/.next/standalone/web/.next/static/static/chunks/beb9625c4a470042.js +0 -1
  270. package/web/.next/standalone/web/.next/static/static/chunks/c2244168e74b8c78.js +0 -1
  271. package/web/.next/standalone/web/.next/static/static/chunks/c81c1aec4369c77f.js +0 -5
  272. package/web/.next/standalone/web/.next/static/static/chunks/d0a69c59b1c0d99c.css +0 -1
  273. package/web/.next/standalone/web/.next/static/static/chunks/turbopack-54bc7d566cd2d105.js +0 -4
  274. package/web/.next/static/chunks/03d4169891280e04.js +0 -7
  275. package/web/.next/static/chunks/2d5da0cfc011b8d9.js +0 -1
  276. package/web/.next/static/chunks/634fd97fab9ed4e4.js +0 -4
  277. package/web/.next/static/chunks/77e4bf0421481629.js +0 -1
  278. package/web/.next/static/chunks/a86053f0894587f2.js +0 -7
  279. package/web/.next/static/chunks/beb9625c4a470042.js +0 -1
  280. package/web/.next/static/chunks/c2244168e74b8c78.js +0 -1
  281. package/web/.next/static/chunks/c81c1aec4369c77f.js +0 -5
  282. package/web/.next/static/chunks/d0a69c59b1c0d99c.css +0 -1
  283. package/web/.next/static/chunks/turbopack-54bc7d566cd2d105.js +0 -4
  284. /package/web/.next/standalone/web/.next/static/{kABnAk0Y1tlcrUKDlM8UT → static/uXbuwS0U7fRElucaXbKUe}/_buildManifest.js +0 -0
  285. /package/web/.next/standalone/web/.next/static/{kABnAk0Y1tlcrUKDlM8UT → static/uXbuwS0U7fRElucaXbKUe}/_clientMiddlewareManifest.json +0 -0
  286. /package/web/.next/standalone/web/.next/static/{kABnAk0Y1tlcrUKDlM8UT → static/uXbuwS0U7fRElucaXbKUe}/_ssgManifest.js +0 -0
  287. /package/web/.next/standalone/web/.next/static/{static/kABnAk0Y1tlcrUKDlM8UT → uXbuwS0U7fRElucaXbKUe}/_buildManifest.js +0 -0
  288. /package/web/.next/standalone/web/.next/static/{static/kABnAk0Y1tlcrUKDlM8UT → uXbuwS0U7fRElucaXbKUe}/_clientMiddlewareManifest.json +0 -0
  289. /package/web/.next/standalone/web/.next/static/{static/kABnAk0Y1tlcrUKDlM8UT → uXbuwS0U7fRElucaXbKUe}/_ssgManifest.js +0 -0
  290. /package/web/.next/static/{kABnAk0Y1tlcrUKDlM8UT → uXbuwS0U7fRElucaXbKUe}/_buildManifest.js +0 -0
  291. /package/web/.next/static/{kABnAk0Y1tlcrUKDlM8UT → uXbuwS0U7fRElucaXbKUe}/_clientMiddlewareManifest.json +0 -0
  292. /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: typeof msg.content === 'string' ? msg.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 === event.data.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: event.data.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 (existingToolCall) {
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
- console.error('Failed to load messages:', err);
1287
+ if (!isStale) {
1288
+ console.error('Failed to load messages:', err);
1289
+ }
866
1290
  } finally {
867
- setIsLoadingHistory(false);
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(session.id);
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 () => clearInterval(interval);
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(session.id);
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 () => clearInterval(interval);
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(session.id);
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(session.id);
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(session.id, handleSSEEvent, {
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 (!input.trim() || isRunning) return;
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: input,
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, input, handleSSEEvent, {
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
- <p className="whitespace-pre-wrap">{item.content}</p>
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
- <PromptInput
2068
- onSubmit={handleSubmit}
2069
- className="shadow-sm"
2070
- >
2071
- <PromptInputBody>
2072
- <PromptInputTextarea
2073
- value={input}
2074
- onChange={(e) => setInput(e.target.value)}
2075
- placeholder="Ask SparkECoder to help..."
2076
- disabled={isRunning}
2077
- autoFocus
2078
- className="min-h-[80px] focus:ring-2 focus:ring-primary/20 transition-all"
2079
- />
2080
- </PromptInputBody>
2081
- <PromptInputFooter>
2082
- <div className="flex-1" />
2083
- <PromptInputSubmit
2084
- disabled={!isRunning && !input.trim()}
2085
- status={isRunning ? 'streaming' : 'ready'}
2086
- onStop={handleStop}
2087
- className="bg-primary hover:bg-primary/90 transition-colors"
2088
- />
2089
- </PromptInputFooter>
2090
- </PromptInput>
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>