@hienlh/ppm 0.21.0 → 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (606) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/CLAUDE.md +29 -4
  3. package/THIRD-PARTY-NOTICES.md +1 -1
  4. package/assets/skills/ppm/SKILL.md +1 -1
  5. package/assets/skills/ppm/references/http-api.md +1 -1
  6. package/bun.lock +16 -1
  7. package/dist/web/assets/accounts-settings-section-BVr9-FZJ.js +1 -0
  8. package/dist/web/assets/{ai-resource-editor-F09pf8Hd.js → ai-resource-editor-CdDpOwjh.js} +1 -1
  9. package/dist/web/assets/ai-settings-section-B1hQyjAd.js +1 -0
  10. package/dist/web/assets/ai-settings-section-X2UOfSaS.js +1 -0
  11. package/dist/web/assets/{angular-html-DkR4iCth.js → angular-html-G66FNlyn.js} +1 -1
  12. package/dist/web/assets/{angular-ts-DcyL4Chx.js → angular-ts-B-PnUMyt.js} +1 -1
  13. package/dist/web/assets/{api-client-bfB7cXRM.js → api-client-C1ZwRL4Y.js} +1 -1
  14. package/dist/web/assets/api-client-pQL53cDQ.js +1 -0
  15. package/dist/web/assets/{api-settings-BcNjZIvG.js → api-settings-_7Q-ULxp.js} +1 -1
  16. package/dist/web/assets/{api-themes-DBfyVUtX.js → api-themes-DRAERo4Q.js} +1 -1
  17. package/dist/web/assets/{apl-BVlrVtjt.js → apl-BmDqqwE8.js} +1 -1
  18. package/dist/web/assets/{appearance-settings-section-whgnq3ss.js → appearance-settings-section-DLoPK0LD.js} +1 -1
  19. package/dist/web/assets/architecture-PBZL5I3N-Dx6nBEKT.js +1 -0
  20. package/dist/web/assets/{astro-MjJlozLj.js → astro-BfwMNfxr.js} +1 -1
  21. package/dist/web/assets/{audio-preview-Bc54GCAV.js → audio-preview-DsYvgKqj.js} +1 -1
  22. package/dist/web/assets/{blade-DnJWi1fz.js → blade-BsFV4pnN.js} +1 -1
  23. package/dist/web/assets/branch-review-tab-G84Ojzae.js +1 -0
  24. package/dist/web/assets/{bsl-DCmJxSIz.js → bsl-Cw9Outng.js} +1 -1
  25. package/dist/web/assets/{button-VW4QGKCn.js → button-B9CTrsEf.js} +1 -1
  26. package/dist/web/assets/{cairo-C8dDenub.js → cairo-DOK5Wqnd.js} +1 -1
  27. package/dist/web/assets/chat-tab-DQJfjm8E.js +15 -0
  28. package/dist/web/assets/{cobol-DPx_nifl.js → cobol-DRaRy4FK.js} +1 -1
  29. package/dist/web/assets/code-editor-D04iUPsZ.js +7 -0
  30. package/dist/web/assets/{codex-daily-guard-CfybHv_c.js → codex-daily-guard-BSccLRGT.js} +1 -1
  31. package/dist/web/assets/{coffee-DvFzqhgN.js → coffee-DvXuBmEJ.js} +1 -1
  32. package/dist/web/assets/{conflict-editor-DenQUOT2.js → conflict-editor-C4ay676e.js} +2 -2
  33. package/dist/web/assets/context-menu-C6pIsf83.js +1 -0
  34. package/dist/web/assets/{cpp-BLJN0umi.js → cpp-zCmm8rKA.js} +1 -1
  35. package/dist/web/assets/{crystal-BXR-9fGw.js → crystal-CdQx6lB6.js} +1 -1
  36. package/dist/web/assets/{csv-preview-CClrJOuR.js → csv-preview-DbwEmG7o.js} +1 -1
  37. package/dist/web/assets/{data-grid-overlay-editor-D4fhcMfT.js → data-grid-overlay-editor-6xUCeLHh.js} +1 -1
  38. package/dist/web/assets/{database-viewer-DItjm8wB.js → database-viewer-S6LsALjX.js} +1 -1
  39. package/dist/web/assets/{dialog-DQTn55Mt.js → dialog-B84_DJQ8.js} +1 -1
  40. package/dist/web/assets/diff-viewer-3oBA7vgQ.js +4 -0
  41. package/dist/web/assets/{docx-preview-DYBkiUaE.js → docx-preview-CXuk-emm.js} +1 -1
  42. package/dist/web/assets/{edge-rWOlCmXT.js → edge-E8QLt7oi.js} +1 -1
  43. package/dist/web/assets/{edit-diff-preview-COtmo54G.js → edit-diff-preview-BGGGecVw.js} +1 -1
  44. package/dist/web/assets/{editor-language-service-DxkZ51E5.js → editor-language-service-DJmaKVY0.js} +1 -1
  45. package/dist/web/assets/{elixir-E7Y82okW.js → elixir-DPb_-72P.js} +1 -1
  46. package/dist/web/assets/{elm-DTPGbc-w.js → elm-BMzJY5b0.js} +1 -1
  47. package/dist/web/assets/{erb-DHqLHrE6.js → erb-AABQNtMK.js} +1 -1
  48. package/dist/web/assets/{erlang-Byg0nNiV.js → erlang-CEIKOkfP.js} +1 -1
  49. package/dist/web/assets/{esm-D_DmOM7n.js → esm-B9RVMpwV.js} +1 -1
  50. package/dist/web/assets/explorer-body-BQ0jyAGv.js +2 -0
  51. package/dist/web/assets/explorer-window-content-jdkWt34J.js +1 -0
  52. package/dist/web/assets/{extension-manager-section-BhwB6nqr.js → extension-manager-section-DG1guILi.js} +1 -1
  53. package/dist/web/assets/{extension-webview-DI4VL2tJ.js → extension-webview-BGSDUbdK.js} +1 -1
  54. package/dist/web/assets/file-icons-BXdvggK2.js +1 -0
  55. package/dist/web/assets/file-icons.generated-BtBErEwb.css +1 -0
  56. package/dist/web/assets/file-store-ojgMXgAw.js +1 -0
  57. package/dist/web/assets/files-settings-section-Gnyd7han.js +1 -0
  58. package/dist/web/assets/{format-file-meta-DOpzJJhS.js → format-file-meta-C77h04cZ.js} +1 -1
  59. package/dist/web/assets/{fortran-fixed-form-CY1n0Xf6.js → fortran-fixed-form-CrXvLHyi.js} +1 -1
  60. package/dist/web/assets/{fsharp-_vEDSCJz.js → fsharp-DCI4qAz_.js} +1 -1
  61. package/dist/web/assets/{gdresource-B6XT0wAC.js → gdresource-Vb2kO1MG.js} +1 -1
  62. package/dist/web/assets/{general-settings-section-BcxwiZ0X.js → general-settings-section-V6Khh15V.js} +1 -1
  63. package/dist/web/assets/{git-commit-Wk9ovq59.js → git-commit-BMI_OWn-.js} +1 -1
  64. package/dist/web/assets/git-log-panel-Bi513qjT.js +1 -0
  65. package/dist/web/assets/{git-rebase-DSarGPvK.js → git-rebase-D-d_2iqZ.js} +1 -1
  66. package/dist/web/assets/{git-repo-store-Dy69YCHN.js → git-repo-store-88Z8v-pF.js} +1 -1
  67. package/dist/web/assets/gitGraph-HDMCJU4V-C6qRruHI.js +1 -0
  68. package/dist/web/assets/{glide-data-grid-BvgAqSUr.js → glide-data-grid-DnQwYSmQ.js} +7 -7
  69. package/dist/web/assets/{glimmer-js-CkYI35_P.js → glimmer-js-BmNNiR0Z.js} +1 -1
  70. package/dist/web/assets/{glimmer-ts-BqmIZcds.js → glimmer-ts-DQE2Ufha.js} +1 -1
  71. package/dist/web/assets/{glsl-Be-yBCF5.js → glsl-BcIDsxRJ.js} +1 -1
  72. package/dist/web/assets/{graphql-a5DrENHY.js → graphql-DGKSLmGH.js} +1 -1
  73. package/dist/web/assets/group-chat-tab-DpczdwyV.js +1 -0
  74. package/dist/web/assets/{hack-BLAg20aD.js → hack-YiC8bEIb.js} +1 -1
  75. package/dist/web/assets/{haml-Dj6bGOQH.js → haml-DjblxAsz.js} +1 -1
  76. package/dist/web/assets/{handlebars-dmEa36Oh.js → handlebars-BQepOXaU.js} +1 -1
  77. package/dist/web/assets/{html-yacZYzSE.js → html-Cayj_2GP.js} +1 -1
  78. package/dist/web/assets/{html-derivative-lLi3-RNT.js → html-derivative-Weq79bL6.js} +1 -1
  79. package/dist/web/assets/{http-1KaxcE4e.js → http-BeLfdGdz.js} +1 -1
  80. package/dist/web/assets/{hurl-ST-dEEZR.js → hurl-D9rypCLK.js} +1 -1
  81. package/dist/web/assets/{hxml-CpDtuq__.js → hxml-clG8mmSH.js} +1 -1
  82. package/dist/web/assets/{image-preview-CJVxL0uH.js → image-preview-CzCc4_Do.js} +1 -1
  83. package/dist/web/assets/image-preview-geometry-9lgeLYB3.js +1 -0
  84. package/dist/web/assets/index-BH99TIE0.css +1 -0
  85. package/dist/web/assets/index-DU68xk06.js +52 -0
  86. package/dist/web/assets/info-3K5VOQVL-BOSpdwlz.js +1 -0
  87. package/dist/web/assets/{jinja-Cjw-evAn.js → jinja-BShBWfs5.js} +1 -1
  88. package/dist/web/assets/{jira-watcher-section-DIC1yIV3.js → jira-watcher-section-f2HUbGDY.js} +1 -1
  89. package/dist/web/assets/{jison-DfATMYoY.js → jison-dN2Nf_Dr.js} +1 -1
  90. package/dist/web/assets/{julia-DDCaxiqT.js → julia-5U0vR-uJ.js} +1 -1
  91. package/dist/web/assets/{just-CJL529u6.js → just-BMppVRy6.js} +1 -1
  92. package/dist/web/assets/keybindings-store-DCRsnKg5.js +1 -0
  93. package/dist/web/assets/{keyboard-shortcuts-section-BW03P3AF.js → keyboard-shortcuts-section-BpvVEw_Y.js} +1 -1
  94. package/dist/web/assets/{latex-Zc4EpLpW.js → latex-DSSOKjis.js} +1 -1
  95. package/dist/web/assets/{liquid-B_6KMNYS.js → liquid-C24dKoyI.js} +1 -1
  96. package/dist/web/assets/{lua-BtS2idK7.js → lua-DNOsnyhn.js} +1 -1
  97. package/dist/web/assets/markdown-renderer-Bcaf9_bA.js +1 -0
  98. package/dist/web/assets/markdown-renderer-DGJp8HC3.js +3 -0
  99. package/dist/web/assets/{marko-Wk6LBnck.js → marko-26MHO_Q9.js} +1 -1
  100. package/dist/web/assets/{mdc-cRaXBrzL.js → mdc-Dhw9mGrC.js} +1 -1
  101. package/dist/web/assets/{media-url-DL8OCGGg.js → media-url-yfqHpPz0.js} +1 -1
  102. package/dist/web/assets/{mobile-bottom-sheet-CoUauvLF.js → mobile-bottom-sheet-C3TniEP-.js} +1 -1
  103. package/dist/web/assets/mobile-explorer-sheet-Bol3XAO1.js +1 -0
  104. package/dist/web/assets/{nextflow-Bos2wvDV.js → nextflow-xH4oJTex.js} +1 -1
  105. package/dist/web/assets/{nginx-DMlOwzdr.js → nginx-CMz4w0M8.js} +1 -1
  106. package/dist/web/assets/{nim-D2dZQA88.js → nim-D8FnIYsx.js} +1 -1
  107. package/dist/web/assets/notification-store-BXt54VIn.js +1 -0
  108. package/dist/web/assets/{notifications-settings-section-DPUROMw-.js → notifications-settings-section-DqlPDfNX.js} +1 -1
  109. package/dist/web/assets/{number-overlay-editor-DZ2i_Bnb.js → number-overlay-editor-CRGwULhR.js} +1 -1
  110. package/dist/web/assets/{overview-panel-R146OIom.js → overview-panel-Di-3aV9p.js} +1 -1
  111. package/dist/web/assets/packet-RMMSAZCW-BGIE1QdU.js +1 -0
  112. package/dist/web/assets/panel-store-CboGJAwh.js +1 -0
  113. package/dist/web/assets/{pdf-preview-BWUPGLp6.js → pdf-preview-CmBI6E7d.js} +1 -1
  114. package/dist/web/assets/{perl-C5-Z6fPn.js → perl-D9iNWYwb.js} +1 -1
  115. package/dist/web/assets/{php-BXNNhkMH.js → php-Bw3Qo0Zv.js} +1 -1
  116. package/dist/web/assets/pie-UPGHQEXC-Dg-QTbXX.js +1 -0
  117. package/dist/web/assets/{postgres-viewer-CjOvaruz.js → postgres-viewer-8ZAqEALv.js} +3 -3
  118. package/dist/web/assets/{ppmbot-settings-section-DrvWh8l8.js → ppmbot-settings-section-DKJUA3Kq.js} +1 -1
  119. package/dist/web/assets/problems-panel-CFcoUx8h.js +1 -0
  120. package/dist/web/assets/{process-table-BRMS1MrX.js → process-table-EKr2dqG6.js} +1 -1
  121. package/dist/web/assets/project-framework-store-B70Q3dNM.js +1 -0
  122. package/dist/web/assets/project-store-B87hLRSk.js +1 -0
  123. package/dist/web/assets/{proxy-settings-section-Bv0Ny-Yy.js → proxy-settings-section-B7qRVl8w.js} +1 -1
  124. package/dist/web/assets/{pug-C1zf1yWy.js → pug-fT5JC6Vm.js} +1 -1
  125. package/dist/web/assets/{qml-OW58jGpD.js → qml-P_wEDB1w.js} +1 -1
  126. package/dist/web/assets/{query-audit-section-BcHJZcVC.js → query-audit-section-By6qyqdB.js} +1 -1
  127. package/dist/web/assets/radar-KQ55EAFF-DZ6IVu2s.js +1 -0
  128. package/dist/web/assets/{razor-DzjSaobM.js → razor-Drrt2_Ph.js} +1 -1
  129. package/dist/web/assets/remote-desktop-clipboard-notice-0WDYsyCA.js +1 -0
  130. package/dist/web/assets/remote-desktop-mobile-sheet-COYDg16H.js +2 -0
  131. package/dist/web/assets/remote-desktop-mobile-view-2DqSuS2U.js +1 -0
  132. package/dist/web/assets/{remote-desktop-readiness-gate-BTzss2wX.js → remote-desktop-readiness-gate-Dm_qMJMH.js} +1 -1
  133. package/dist/web/assets/remote-desktop-window-content-UodhsF7W.js +1 -0
  134. package/dist/web/assets/{rst-RfuihOUW.js → rst-Dr2mW4-h.js} +1 -1
  135. package/dist/web/assets/{ruby-CNO3pli1.js → ruby-C6EpUsTY.js} +1 -1
  136. package/dist/web/assets/{run-in-terminal-Cb_hVU8x.js → run-in-terminal-CdefEe_R.js} +1 -1
  137. package/dist/web/assets/{sas-DZY5BtXx.js → sas-D8ZEeP6v.js} +1 -1
  138. package/dist/web/assets/{schedules-settings-section-DO8xAOVj.js → schedules-settings-section-bG2lYfGv.js} +1 -1
  139. package/dist/web/assets/{scroll-area-LtNV8g5o.js → scroll-area-BzeiOd-B.js} +1 -1
  140. package/dist/web/assets/{scss--4g-REcN.js → scss-ITL7jQ1f.js} +1 -1
  141. package/dist/web/assets/{send-to-chat-Cnpru1M0.js → send-to-chat-F207yH3u.js} +1 -1
  142. package/dist/web/assets/{separator-Cds1yxy8.js → separator-TRMsJUYe.js} +1 -1
  143. package/dist/web/assets/settings-body-C06bFarv.js +2 -0
  144. package/dist/web/assets/settings-store-krC6bc79.js +2 -0
  145. package/dist/web/assets/settings-tab-34RSaASd.js +1 -0
  146. package/dist/web/assets/settings-window-content-Dr_2lmHH.js +1 -0
  147. package/dist/web/assets/{shaderlab-Cu4sBiVn.js → shaderlab-mW-IIlhk.js} +1 -1
  148. package/dist/web/assets/{shellsession-DWzJfEv7.js → shellsession-DhNl5STt.js} +1 -1
  149. package/dist/web/assets/{soy-BnqE-5Ff.js → soy-SdY0Qjtr.js} +1 -1
  150. package/dist/web/assets/{sparql-Ctg__NhZ.js → sparql-ftqZo2i-.js} +1 -1
  151. package/dist/web/assets/{sql-query-editor-D9k19T_M.js → sql-query-editor-BgRH0cHD.js} +1 -1
  152. package/dist/web/assets/{sqlite-viewer-DoV9hnWJ.js → sqlite-viewer-DUxGslpa.js} +1 -1
  153. package/dist/web/assets/{stata-Qk2h66XW.js → stata-Bj7Yjb4b.js} +1 -1
  154. package/dist/web/assets/{surrealql-8QEX7EfH.js → surrealql-CLhYGAfB.js} +1 -1
  155. package/dist/web/assets/{svelte-D_mubC5-.js → svelte-DTAlRHhK.js} +1 -1
  156. package/dist/web/assets/system-monitor-body-IZuLn7Qh.js +2 -0
  157. package/dist/web/assets/system-monitor-tab-DiSA490Z.js +1 -0
  158. package/dist/web/assets/{system-monitor-window-content-qXLcicFM.js → system-monitor-window-content-CEaFISgj.js} +1 -1
  159. package/dist/web/assets/{tab-host-window-content-7fPZNEni.js → tab-host-window-content-D0TN8Wrq.js} +1 -1
  160. package/dist/web/assets/{tab-store-DGJGnbwv.js → tab-store-OJa1GPCq.js} +1 -1
  161. package/dist/web/assets/team-member-sheet-DDPNcCjd.js +2 -0
  162. package/dist/web/assets/{team-member-window-content-BgvJFypM.js → team-member-window-content-DCiW_1-E.js} +1 -1
  163. package/dist/web/assets/{templ-C1jV0fsO.js → templ-BMfhACxb.js} +1 -1
  164. package/dist/web/assets/{terminal-tab-Cs0sGljx.js → terminal-tab-4ypP0NDY.js} +3 -3
  165. package/dist/web/assets/{tex-D_StFXy_.js → tex-BcssM-QA.js} +1 -1
  166. package/dist/web/assets/tool-cards-Dd6E7Q3k.js +9 -0
  167. package/dist/web/assets/treemap-KZPCXAKY-CE30wCXa.js +1 -0
  168. package/dist/web/assets/{ts-tags-B5m47rEm.js → ts-tags-CrR5IYI8.js} +1 -1
  169. package/dist/web/assets/{twig-C_br6GIl.js → twig-DchfUmyI.js} +1 -1
  170. package/dist/web/assets/{use-blob-url-C-SGi_Hb.js → use-blob-url-CPtCcY3i.js} +1 -1
  171. package/dist/web/assets/{use-inline-blame-B4trxGcR.js → use-inline-blame-vrfFHuaR.js} +1 -1
  172. package/dist/web/assets/{use-lsp-CV0vzILO.js → use-lsp-CsuZu49X.js} +1 -1
  173. package/dist/web/assets/{use-monaco-theme-DBqbWjWX.js → use-monaco-theme-BIVYJBIn.js} +1 -1
  174. package/dist/web/assets/{use-open-team-member-DENsjp0s.js → use-open-team-member-C3mrUzIC.js} +1 -1
  175. package/dist/web/assets/{use-websocket-LgXiX1qN.js → use-websocket-BMbufhX7.js} +1 -1
  176. package/dist/web/assets/{vendor-mermaid-Cbig5zA6.js → vendor-mermaid-BitiXJlL.js} +1 -1
  177. package/dist/web/assets/{vendor-ui-BBdkypKW.js → vendor-ui-DmZ-Mb-H.js} +1 -1
  178. package/dist/web/assets/video-preview-Byj8P_jA.js +1 -0
  179. package/dist/web/assets/{vue-DQwCnLdr.js → vue-1EL6Pw1h.js} +1 -1
  180. package/dist/web/assets/{vue-html-T_EoGpPJ.js → vue-html-BPQNwPC6.js} +1 -1
  181. package/dist/web/assets/{vue-vine-DmxLqX50.js → vue-vine-Bb_dsWLt.js} +1 -1
  182. package/dist/web/assets/{window-pip-registry-3l4hoxuD.js → window-pip-registry-yD6esSju.js} +1 -1
  183. package/dist/web/assets/ws-auth-CEYVQ8NH.js +1 -0
  184. package/dist/web/assets/{xml-Bs6Bw8is.js → xml-dxkLNA0K.js} +1 -1
  185. package/dist/web/assets/{xsl-f5ZNhRCc.js → xsl-Bw2adwrl.js} +1 -1
  186. package/dist/web/index.html +27 -26
  187. package/dist/web/sw.js +1 -1
  188. package/docs/architecture/plugins-and-tracing.md +285 -0
  189. package/docs/lessons-learned.md +31 -0
  190. package/docs/system-architecture.md +1 -0
  191. package/package.json +2 -1
  192. package/packages/ext-git-graph/src/compare-diff-base.test.ts +173 -0
  193. package/packages/ext-git-graph/src/compare-view.ts +22 -3
  194. package/packages/ext-git-graph/src/extension.ts +6 -0
  195. package/packages/ext-git-graph/src/types.ts +19 -0
  196. package/packages/ext-git-graph/src/webview-html.test.ts +134 -1
  197. package/packages/ext-git-graph/src/webview-html.ts +277 -27
  198. package/scripts/gen-file-icons.ts +493 -242
  199. package/src/providers/claude-agent-sdk.ts +42 -1
  200. package/src/providers/codex-app-server/codex-history.ts +77 -23
  201. package/src/server/index.ts +42 -7
  202. package/src/server/routes/chat.ts +15 -11
  203. package/src/server/routes/git.ts +83 -3
  204. package/src/server/routes/remote-desktop.ts +7 -2
  205. package/src/server/ws/global.ts +1 -1
  206. package/src/server/ws/remote-desktop.ts +12 -5
  207. package/src/services/binary-content.ts +25 -0
  208. package/src/services/chat-search.service.ts +1 -1
  209. package/src/services/chat.service.ts +9 -0
  210. package/src/services/event-loop-lag.ts +208 -0
  211. package/src/services/file-lines.ts +114 -0
  212. package/src/services/file-watcher/recreated-dir-poller.ts +108 -6
  213. package/src/services/file-watcher/watch-tree.ts +131 -10
  214. package/src/services/file-watcher.service.ts +73 -23
  215. package/src/services/git-branch-diff/branch-diff-parse.ts +151 -0
  216. package/src/services/git-branch-diff/branch-diff.service.ts +79 -0
  217. package/src/services/git-hunks/git-hunks.service.ts +26 -11
  218. package/src/services/git-hunks/unified-diff.ts +15 -1
  219. package/src/services/git.service.ts +74 -24
  220. package/src/services/jsonl-transcript-parser.ts +138 -6
  221. package/src/services/log-rotate.ts +142 -0
  222. package/src/services/media-transcode/ffmpeg-capabilities.ts +72 -2
  223. package/src/services/remote-desktop/remote-desktop-audio.ts +193 -0
  224. package/src/services/remote-desktop/remote-desktop-capture-input.ts +87 -14
  225. package/src/services/remote-desktop/remote-desktop-capture.ts +40 -7
  226. package/src/services/remote-desktop/remote-desktop-clipboard.ts +166 -0
  227. package/src/services/remote-desktop/remote-desktop-displays.ts +77 -1
  228. package/src/services/remote-desktop/remote-desktop-encoder-args.ts +25 -6
  229. package/src/services/remote-desktop/remote-desktop-evdev-key-map.ts +69 -0
  230. package/src/services/remote-desktop/remote-desktop-input-linux.ts +253 -0
  231. package/src/services/remote-desktop/remote-desktop-input-uinput.ts +242 -0
  232. package/src/services/remote-desktop/remote-desktop-input.ts +40 -8
  233. package/src/services/remote-desktop/remote-desktop-linux-session.ts +146 -0
  234. package/src/services/remote-desktop/remote-desktop-ogg-opus.ts +144 -0
  235. package/src/services/remote-desktop/remote-desktop-privacy.ts +163 -0
  236. package/src/services/remote-desktop/remote-desktop-quality.ts +138 -0
  237. package/src/services/remote-desktop/remote-desktop-requirements.ts +158 -5
  238. package/src/services/remote-desktop/remote-desktop-resolution.ts +302 -0
  239. package/src/services/remote-desktop/remote-desktop-session.ts +523 -21
  240. package/src/services/remote-desktop/remote-desktop-x11.ts +250 -0
  241. package/src/services/supervisor.ts +47 -4
  242. package/src/shared/remote-desktop-custom-quality.ts +117 -0
  243. package/src/shared/remote-desktop-quality.ts +127 -0
  244. package/src/types/chat.ts +4 -0
  245. package/src/types/git.ts +52 -0
  246. package/src/web/components/branch-review/branch-review-tab.tsx +360 -0
  247. package/src/web/components/branch-review/branch-review-tree-row.tsx +138 -0
  248. package/src/web/components/chat/attachment-chips.tsx +30 -6
  249. package/src/web/components/chat/chat-tab.tsx +12 -1
  250. package/src/web/components/chat/message-events.tsx +273 -0
  251. package/src/web/components/chat/message-list.tsx +107 -861
  252. package/src/web/components/chat/message-markdown.tsx +34 -0
  253. package/src/web/components/chat/message-media.tsx +174 -0
  254. package/src/web/components/chat/message-user-bubble.tsx +399 -0
  255. package/src/web/components/chat/provider-selector.tsx +30 -22
  256. package/src/web/components/chat/session-history-list.tsx +2 -2
  257. package/src/web/components/chat/session-list-panel.tsx +3 -2
  258. package/src/web/components/editor/binary-diff-view.tsx +212 -0
  259. package/src/web/components/editor/diff-viewer.tsx +76 -3
  260. package/src/web/components/explorer/file-tree.tsx +235 -134
  261. package/src/web/components/explorer/flatten-visible-tree.ts +35 -40
  262. package/src/web/components/explorer/tree-node.tsx +69 -69
  263. package/src/web/components/explorer/use-tree-row-dnd.ts +3 -4
  264. package/src/web/components/git/git-status-panel.tsx +25 -0
  265. package/src/web/components/layout/draggable-tab.tsx +4 -2
  266. package/src/web/components/layout/nav-section-rail.tsx +12 -3
  267. package/src/web/components/layout/status-bar.tsx +10 -6
  268. package/src/web/components/layout/tab-content.tsx +5 -0
  269. package/src/web/components/layout/tab-pool.tsx +1 -0
  270. package/src/web/components/layout/upgrade-button.tsx +5 -1
  271. package/src/web/components/remote-desktop/remote-desktop-clipboard-client.ts +67 -0
  272. package/src/web/components/remote-desktop/remote-desktop-clipboard-notice.tsx +127 -0
  273. package/src/web/components/remote-desktop/remote-desktop-codec-labels.ts +28 -0
  274. package/src/web/components/remote-desktop/remote-desktop-custom-quality-dialog.tsx +144 -0
  275. package/src/web/components/remote-desktop/remote-desktop-mobile-key-bar.tsx +13 -2
  276. package/src/web/components/remote-desktop/remote-desktop-mobile-settings.tsx +232 -0
  277. package/src/web/components/remote-desktop/remote-desktop-mobile-toolbar.tsx +21 -8
  278. package/src/web/components/remote-desktop/remote-desktop-mobile-view.tsx +82 -3
  279. package/src/web/components/remote-desktop/remote-desktop-recording.ts +76 -0
  280. package/src/web/components/remote-desktop/remote-desktop-resolution-list.ts +100 -0
  281. package/src/web/components/remote-desktop/remote-desktop-toolbar.tsx +497 -0
  282. package/src/web/components/remote-desktop/remote-desktop-view-style.ts +145 -0
  283. package/src/web/components/remote-desktop/remote-desktop-window-content.tsx +126 -35
  284. package/src/web/components/remote-desktop/use-h264-canvas-decoder.ts +11 -0
  285. package/src/web/components/remote-desktop/use-opus-audio-player.ts +148 -0
  286. package/src/web/components/remote-desktop/use-remote-desktop-connection.ts +284 -1
  287. package/src/web/components/remote-desktop/use-remote-desktop-readiness.ts +75 -1
  288. package/src/web/components/remote-desktop/use-remote-desktop-recorder.ts +103 -0
  289. package/src/web/components/remote-desktop/use-remote-desktop-virtual-keyboard.ts +19 -2
  290. package/src/web/components/remote-desktop/use-remote-input-capture.ts +39 -3
  291. package/src/web/components/settings/theme-picker.tsx +2 -1
  292. package/src/web/components/ui/tooltip.tsx +15 -2
  293. package/src/web/hooks/use-delayed-flag.ts +35 -0
  294. package/src/web/lib/branch-review-state.ts +175 -0
  295. package/src/web/lib/file-icon-name.ts +62 -0
  296. package/src/web/lib/file-icons.generated.ts +3287 -25
  297. package/src/web/lib/file-icons.tsx +71 -73
  298. package/src/web/lib/flatten-expansions.ts +18 -5
  299. package/src/web/lib/message-ordinals.ts +23 -0
  300. package/src/web/lib/provider-logos.tsx +143 -0
  301. package/src/web/lib/tab-type-icons.ts +11 -0
  302. package/src/web/stores/file-store.ts +73 -35
  303. package/src/web/stores/panel-utils.ts +5 -0
  304. package/src/web/stores/project-framework-store.ts +82 -0
  305. package/src/web/stores/project-store.ts +7 -1
  306. package/src/web/stores/settings-store.ts +128 -0
  307. package/src/web/stores/tab-store.ts +1 -0
  308. package/src/web/stores/window-panel-persistence.ts +1 -0
  309. package/src/web/styles/file-icons.generated.css +3282 -234
  310. package/src/web/styles/globals.css +26 -0
  311. package/dist/web/assets/accounts-settings-section-BuxD9zH3.js +0 -1
  312. package/dist/web/assets/ai-settings-section-Dd9tOUp0.js +0 -1
  313. package/dist/web/assets/ai-settings-section-FrU2X8Fu.js +0 -1
  314. package/dist/web/assets/api-client-BUCo1lms.js +0 -1
  315. package/dist/web/assets/architecture-PBZL5I3N-5K0wEkAn.js +0 -1
  316. package/dist/web/assets/chat-tab-Cr6_KSGO.js +0 -15
  317. package/dist/web/assets/code-editor-DfmlGamv.js +0 -7
  318. package/dist/web/assets/context-menu-DOODKZdF.js +0 -1
  319. package/dist/web/assets/diff-viewer-BpNj7pN9.js +0 -4
  320. package/dist/web/assets/explorer-body-D-5z464e.js +0 -2
  321. package/dist/web/assets/explorer-window-content-7-1wuRul.js +0 -1
  322. package/dist/web/assets/file-icons-EzPL0q-k.js +0 -1
  323. package/dist/web/assets/file-icons.generated-CfaJuvZu.css +0 -1
  324. package/dist/web/assets/file-store-DMABxOkV.js +0 -1
  325. package/dist/web/assets/files-settings-section-CZzABuKz.js +0 -1
  326. package/dist/web/assets/git-log-panel-CRN4JVOx.js +0 -1
  327. package/dist/web/assets/gitGraph-HDMCJU4V-DkVT2MAM.js +0 -1
  328. package/dist/web/assets/group-chat-tab-DFa5mAx5.js +0 -1
  329. package/dist/web/assets/index-D7DbXD3G.css +0 -1
  330. package/dist/web/assets/index-ZtfxZOyb.js +0 -52
  331. package/dist/web/assets/info-3K5VOQVL-BBsmKGzA.js +0 -1
  332. package/dist/web/assets/keybindings-store-tVfhPANS.js +0 -1
  333. package/dist/web/assets/markdown-renderer-CvL1pWeK.js +0 -3
  334. package/dist/web/assets/markdown-renderer-sJNNVhlS.js +0 -1
  335. package/dist/web/assets/mobile-explorer-sheet-CZ0HZ2E_.js +0 -1
  336. package/dist/web/assets/notification-store-BZbsPnW0.js +0 -1
  337. package/dist/web/assets/packet-RMMSAZCW-DwmYPOlx.js +0 -1
  338. package/dist/web/assets/panel-store-BVLBRn6M.js +0 -1
  339. package/dist/web/assets/pie-UPGHQEXC-D0oogIFi.js +0 -1
  340. package/dist/web/assets/problems-panel-B7BWVhdy.js +0 -1
  341. package/dist/web/assets/project-store-BhMbVln3.js +0 -1
  342. package/dist/web/assets/radar-KQ55EAFF-CQIJOQGa.js +0 -1
  343. package/dist/web/assets/remote-desktop-mobile-sheet-ByjaeZKX.js +0 -2
  344. package/dist/web/assets/remote-desktop-mobile-view-D13mtr9F.js +0 -1
  345. package/dist/web/assets/remote-desktop-window-content-CbqTCaC6.js +0 -1
  346. package/dist/web/assets/settings-body-Rnu-FcX-.js +0 -2
  347. package/dist/web/assets/settings-store-DZPgPtEA.js +0 -2
  348. package/dist/web/assets/settings-tab-C6PHxs0Z.js +0 -1
  349. package/dist/web/assets/settings-window-content-CQ4gT2qj.js +0 -1
  350. package/dist/web/assets/system-monitor-body-2vQ1kMxE.js +0 -2
  351. package/dist/web/assets/system-monitor-tab-krAcvwF7.js +0 -1
  352. package/dist/web/assets/team-member-sheet-2FnRmk1a.js +0 -2
  353. package/dist/web/assets/tool-cards-DU_S2o51.js +0 -9
  354. package/dist/web/assets/treemap-KZPCXAKY-BhEtjljr.js +0 -1
  355. package/dist/web/assets/use-remote-desktop-display-choice-BK_f787P.js +0 -1
  356. package/dist/web/assets/video-preview-BDNwumbW.js +0 -1
  357. package/dist/web/assets/ws-auth-DEAJiGCR.js +0 -1
  358. /package/dist/web/assets/{abap-tkptwpsK.js → abap-IN2yINdN.js} +0 -0
  359. /package/dist/web/assets/{actionscript-3-Di9TMGwg.js → actionscript-3-C_t772eR.js} +0 -0
  360. /package/dist/web/assets/{ada-CB9F079D.js → ada-DPme3emm.js} +0 -0
  361. /package/dist/web/assets/{andromeeda-Dcl8jpb7.js → andromeeda-DSV4i4mZ.js} +0 -0
  362. /package/dist/web/assets/{apache-D0Nk0qsH.js → apache-CWr8s8I8.js} +0 -0
  363. /package/dist/web/assets/{apex-mQIoWncD.js → apex-Z20JnDF5.js} +0 -0
  364. /package/dist/web/assets/{applescript-C6ljbcsI.js → applescript-BKNPu3ga.js} +0 -0
  365. /package/dist/web/assets/{apply-theme-BwIMNXyX.js → apply-theme-CbR31K6I.js} +0 -0
  366. /package/dist/web/assets/{ara-WFqRPGPe.js → ara-hWdY3O96.js} +0 -0
  367. /package/dist/web/assets/{asciidoc-bNd8w4nf.js → asciidoc-eynhq496.js} +0 -0
  368. /package/dist/web/assets/{asm-BQ1_F580.js → asm-Bu02Kq4P.js} +0 -0
  369. /package/dist/web/assets/{aurora-x-Ciqs_Dbu.js → aurora-x-BHBGXbIZ.js} +0 -0
  370. /package/dist/web/assets/{awk-GCR-6KZV.js → awk-B4ojezyv.js} +0 -0
  371. /package/dist/web/assets/{ayu-dark-DxJXuj99.js → ayu-dark-D0GQzFi9.js} +0 -0
  372. /package/dist/web/assets/{ayu-light-jvSyJWDV.js → ayu-light-BVYb5Yn8.js} +0 -0
  373. /package/dist/web/assets/{ayu-mirage-Cg3BPO6H.js → ayu-mirage-BGFW6oNb.js} +0 -0
  374. /package/dist/web/assets/{ballerina-BPz4CrX6.js → ballerina-B6Og3IKT.js} +0 -0
  375. /package/dist/web/assets/{bat-CSTs2hIG.js → bat-D8YFILfx.js} +0 -0
  376. /package/dist/web/assets/{beancount-DMateAZX.js → beancount-BjlbBBuX.js} +0 -0
  377. /package/dist/web/assets/{berry-9lXg0GRM.js → berry-Cz1ygeoj.js} +0 -0
  378. /package/dist/web/assets/{bibtex-DIbqmvFg.js → bibtex-CVkJceJh.js} +0 -0
  379. /package/dist/web/assets/{bicep-02WGsoJ8.js → bicep-DrhYI5GX.js} +0 -0
  380. /package/dist/web/assets/{bird2-BY0tbu3k.js → bird2-BV-KJtmc.js} +0 -0
  381. /package/dist/web/assets/{c-jMBX_9Je.js → c-CuLlvxbG.js} +0 -0
  382. /package/dist/web/assets/{c3-B8JVbNjW.js → c3-CI7-1CJ0.js} +0 -0
  383. /package/dist/web/assets/{cadence-DUIf1tvL.js → cadence-DjxhNP3Q.js} +0 -0
  384. /package/dist/web/assets/{can-open-in-ppm-DGEc0s4P.js → can-open-in-ppm-cv5--yHX.js} +0 -0
  385. /package/dist/web/assets/{catppuccin-frappe-FhUCVxex.js → catppuccin-frappe-CEozpJRh.js} +0 -0
  386. /package/dist/web/assets/{catppuccin-latte-CzfrtmgL.js → catppuccin-latte-C4sXQFMc.js} +0 -0
  387. /package/dist/web/assets/{catppuccin-macchiato-6xL0lOtj.js → catppuccin-macchiato-Ci-fDhXy.js} +0 -0
  388. /package/dist/web/assets/{catppuccin-mocha-D39-xS3P.js → catppuccin-mocha-CH7YXw8d.js} +0 -0
  389. /package/dist/web/assets/{clarity-D8TeGvoT.js → clarity-BWrJ7UmT.js} +0 -0
  390. /package/dist/web/assets/{clojure-Dx_acSX7.js → clojure-C94fh2hb.js} +0 -0
  391. /package/dist/web/assets/{cmake-CyeYPq8o.js → cmake-CtvCkMsG.js} +0 -0
  392. /package/dist/web/assets/{codeowners-DrTI0lmI.js → codeowners-CJnubqzH.js} +0 -0
  393. /package/dist/web/assets/{codeql-CPkFxVW-.js → codeql-BOoP5HY2.js} +0 -0
  394. /package/dist/web/assets/{common-lisp-B0k-3LTj.js → common-lisp-B9nlMFJd.js} +0 -0
  395. /package/dist/web/assets/{coq-uygpo7to.js → coq-CgzIQiSH.js} +0 -0
  396. /package/dist/web/assets/{csharp-DKUYGfSp.js → csharp-4xAIx3jw.js} +0 -0
  397. /package/dist/web/assets/{css-DoOUp7v4.js → css-D4YoLYGn.js} +0 -0
  398. /package/dist/web/assets/{csv-B3YE13gi.js → csv-C2v-ZakW.js} +0 -0
  399. /package/dist/web/assets/{csv-parser-DXePqmXe.js → csv-parser-_oDBUkwQ.js} +0 -0
  400. /package/dist/web/assets/{cue-DViO2wvn.js → cue-Bbznh_an.js} +0 -0
  401. /package/dist/web/assets/{cypher-4BMCIW5N.js → cypher-BKhVEARF.js} +0 -0
  402. /package/dist/web/assets/{d-BT_sTHas.js → d-D6IhWopl.js} +0 -0
  403. /package/dist/web/assets/{dark-plus-DVVXu-1N.js → dark-plus-xAWu790O.js} +0 -0
  404. /package/dist/web/assets/{dart-DHz6f6_6.js → dart-Bj6S9O1j.js} +0 -0
  405. /package/dist/web/assets/{data-grid-types-OgUxMLdQ.js → data-grid-types-C5bthITf.js} +0 -0
  406. /package/dist/web/assets/{dax-ChYCjweK.js → dax-CF2zvaqq.js} +0 -0
  407. /package/dist/web/assets/{desktop-D6Lt4p04.js → desktop-CnmyxMKE.js} +0 -0
  408. /package/dist/web/assets/{diff-D8TDe4hP.js → diff-Qi8WuJsI.js} +0 -0
  409. /package/dist/web/assets/{dist-DOTlFQJA.js → dist-Bjseolz4.js} +0 -0
  410. /package/dist/web/assets/{dist-CrBOCtCW.js → dist-Czq7BUMy.js} +0 -0
  411. /package/dist/web/assets/{docker-Bsp0YbPx.js → docker-vLbbNP5d.js} +0 -0
  412. /package/dist/web/assets/{dotenv-B_7giSY_.js → dotenv-DJMvvNzn.js} +0 -0
  413. /package/dist/web/assets/{dracula-TrQL2iB5.js → dracula-CtAh2rhB.js} +0 -0
  414. /package/dist/web/assets/{dracula-soft-n2XOlgh-.js → dracula-soft-CXtuAAf6.js} +0 -0
  415. /package/dist/web/assets/{dream-maker-CdIEdhpT.js → dream-maker-D5YQyK49.js} +0 -0
  416. /package/dist/web/assets/{editor-font-BDrdiGf1.js → editor-font-ClTC9gaD.js} +0 -0
  417. /package/dist/web/assets/{emacs-lisp-CSbixs6u.js → emacs-lisp-CGTjbiqF.js} +0 -0
  418. /package/dist/web/assets/{everforest-dark-BuZahZYp.js → everforest-dark-D59p7vAQ.js} +0 -0
  419. /package/dist/web/assets/{everforest-light-DPH53z5H.js → everforest-light-B3Qtud3V.js} +0 -0
  420. /package/dist/web/assets/{fennel-B1-K_hlJ.js → fennel-BCyHNUwf.js} +0 -0
  421. /package/dist/web/assets/{fish-Cy12Dbua.js → fish-DDDlLV2G.js} +0 -0
  422. /package/dist/web/assets/{fluent-B_82ys8E.js → fluent-By54R9tl.js} +0 -0
  423. /package/dist/web/assets/{format-bytes-D5sBgIqU.js → format-bytes-D1AQyUCt.js} +0 -0
  424. /package/dist/web/assets/{fortran-free-form-BRQdNzZF.js → fortran-free-form-tP8mRym_.js} +0 -0
  425. /package/dist/web/assets/{gdscript-XBDmLUm9.js → gdscript-nHC2z6Te.js} +0 -0
  426. /package/dist/web/assets/{gdshader-DhVfB3u4.js → gdshader-K-cSmPlW.js} +0 -0
  427. /package/dist/web/assets/{genie-X3K29Y49.js → genie-BAcRhvEe.js} +0 -0
  428. /package/dist/web/assets/{gherkin-DKIx-6cX.js → gherkin-CcifxMLb.js} +0 -0
  429. /package/dist/web/assets/{github-dark-Bf7zdp5s.js → github-dark-67KKlMdB.js} +0 -0
  430. /package/dist/web/assets/{github-dark-default-DqsBBV11.js → github-dark-default-tja8pidR.js} +0 -0
  431. /package/dist/web/assets/{github-dark-dimmed-X2gqRz6w.js → github-dark-dimmed-CtjhYQtx.js} +0 -0
  432. /package/dist/web/assets/{github-dark-high-contrast-Z7tvbGX4.js → github-dark-high-contrast-Bf-LPFgn.js} +0 -0
  433. /package/dist/web/assets/{github-light-BKxt-7zZ.js → github-light-CbJxE7n3.js} +0 -0
  434. /package/dist/web/assets/{github-light-default-B0liF4d4.js → github-light-default-DRQmdHxt.js} +0 -0
  435. /package/dist/web/assets/{github-light-high-contrast-F4encJwr.js → github-light-high-contrast-CJkDgTeM.js} +0 -0
  436. /package/dist/web/assets/{gleam-Pz2bHgBB.js → gleam-D4LCl2ri.js} +0 -0
  437. /package/dist/web/assets/{gn-CgmdNsPU.js → gn-COY8zZmC.js} +0 -0
  438. /package/dist/web/assets/{gnuplot-qLNF5yZ9.js → gnuplot-ROk1VitR.js} +0 -0
  439. /package/dist/web/assets/{go-CSDUNhE7.js → go-H30c3p0C.js} +0 -0
  440. /package/dist/web/assets/{groovy-B3NEnOHF.js → groovy-DBLCaelV.js} +0 -0
  441. /package/dist/web/assets/{gruvbox-dark-hard-DcoftiUX.js → gruvbox-dark-hard-DJy4irps.js} +0 -0
  442. /package/dist/web/assets/{gruvbox-dark-medium-D1wVSuD8.js → gruvbox-dark-medium-Dhfw1QTO.js} +0 -0
  443. /package/dist/web/assets/{gruvbox-dark-soft-DWpunymH.js → gruvbox-dark-soft-BncJU-8w.js} +0 -0
  444. /package/dist/web/assets/{gruvbox-light-hard-rHMvyUdS.js → gruvbox-light-hard-Cq12_YvM.js} +0 -0
  445. /package/dist/web/assets/{gruvbox-light-medium-3O2KiXO7.js → gruvbox-light-medium-DqVja5_v.js} +0 -0
  446. /package/dist/web/assets/{gruvbox-light-soft-lNlXJPwH.js → gruvbox-light-soft-IvwhveGF.js} +0 -0
  447. /package/dist/web/assets/{haskell-aaG3aC0h.js → haskell-DA1JV-MC.js} +0 -0
  448. /package/dist/web/assets/{haxe-CPcHhUDd.js → haxe-V9RTRc5E.js} +0 -0
  449. /package/dist/web/assets/{hcl-CMQNlR_4.js → hcl-hr1lyZ10.js} +0 -0
  450. /package/dist/web/assets/{hjson-DeuklPFj.js → hjson-D3vW3i7X.js} +0 -0
  451. /package/dist/web/assets/{hlsl-BI3hlsaM.js → hlsl-BB85gfJT.js} +0 -0
  452. /package/dist/web/assets/{horizon-C7-gOcjs.js → horizon-DU8OOnj8.js} +0 -0
  453. /package/dist/web/assets/{horizon-bright-Cxt9bRW7.js → horizon-bright-DNMUclW6.js} +0 -0
  454. /package/dist/web/assets/{houston-DbF2G7iD.js → houston-DAiZKf6T.js} +0 -0
  455. /package/dist/web/assets/{hy-DRG6Hs2I.js → hy-C6Zv0LMe.js} +0 -0
  456. /package/dist/web/assets/{image-gallery-CObzTisL.js → image-gallery-CGvlv8zR.js} +0 -0
  457. /package/dist/web/assets/{imba-CxKv_2TW.js → imba-Didnp6Y-.js} +0 -0
  458. /package/dist/web/assets/{ini-BiHsyCdO.js → ini-m0T7q4gQ.js} +0 -0
  459. /package/dist/web/assets/{input-D-dR8RI6.js → input-C7Q8FxeL.js} +0 -0
  460. /package/dist/web/assets/{java-D3RZzgDB.js → java-Buy07jGn.js} +0 -0
  461. /package/dist/web/assets/{javascript-4FmSqG-T.js → javascript-Cjq3CbRL.js} +0 -0
  462. /package/dist/web/assets/{json-CjB236_E.js → json-BSVu8IqD.js} +0 -0
  463. /package/dist/web/assets/{json5-bkfO-Rco.js → json5-NN_Vb7e6.js} +0 -0
  464. /package/dist/web/assets/{jsonc-BwaTeLyv.js → jsonc-BdyZhBUw.js} +0 -0
  465. /package/dist/web/assets/{jsonl-CfxNSwGd.js → jsonl-BsDSNvwi.js} +0 -0
  466. /package/dist/web/assets/{jsonnet-CQETCYrp.js → jsonnet-Gm963F-U.js} +0 -0
  467. /package/dist/web/assets/{jssm-D5o3Zzdz.js → jssm-Ds3bKWgJ.js} +0 -0
  468. /package/dist/web/assets/{jsx-BAMILPLo.js → jsx-BT5mW58X.js} +0 -0
  469. /package/dist/web/assets/{kanagawa-dragon-BhGVxu3R.js → kanagawa-dragon-BvNvf7xd.js} +0 -0
  470. /package/dist/web/assets/{kanagawa-lotus-FH4Y59ro.js → kanagawa-lotus-CuSiliSn.js} +0 -0
  471. /package/dist/web/assets/{kanagawa-wave-eilvP9MB.js → kanagawa-wave-C6g-d6jT.js} +0 -0
  472. /package/dist/web/assets/{kdl-BQ-yvXUK.js → kdl-Bx4kZef-.js} +0 -0
  473. /package/dist/web/assets/{kotlin-BuCx0-VT.js → kotlin-qvaVvCcm.js} +0 -0
  474. /package/dist/web/assets/{kusto-DAN_n_Cy.js → kusto-BPLyNCN6.js} +0 -0
  475. /package/dist/web/assets/{laserwave-Cq2BlxrT.js → laserwave-Sf8ZEhWP.js} +0 -0
  476. /package/dist/web/assets/{lean-Dhq7uXKw.js → lean-DGe6uqgk.js} +0 -0
  477. /package/dist/web/assets/{less-CxaMQb8j.js → less-CqM85x-9.js} +0 -0
  478. /package/dist/web/assets/{lib-BVwmtv2i.js → lib-CD6pOS4u.js} +0 -0
  479. /package/dist/web/assets/{libesm-DdtEjhIi.js → libesm-CPh1Av0r.js} +0 -0
  480. /package/dist/web/assets/{light-plus-B4sMAend.js → light-plus-DBtKwhFA.js} +0 -0
  481. /package/dist/web/assets/{llvm-DQ6-B_6Y.js → llvm-Dpbq3gky.js} +0 -0
  482. /package/dist/web/assets/{log-Bb_vqvu7.js → log-qKMcPm9S.js} +0 -0
  483. /package/dist/web/assets/{logo-BzfYv3A_.js → logo-DFo7i5KH.js} +0 -0
  484. /package/dist/web/assets/{luau-BnAeNR77.js → luau-DGlqWFyL.js} +0 -0
  485. /package/dist/web/assets/{make-B6IFuxpC.js → make-CixdvUQe.js} +0 -0
  486. /package/dist/web/assets/{markdown-Z0xi77Fn.js → markdown-CmRnbngE.js} +0 -0
  487. /package/dist/web/assets/{material-theme-1scy1SYn.js → material-theme-WDUbafB-.js} +0 -0
  488. /package/dist/web/assets/{material-theme-darker-G-JykXQP.js → material-theme-darker-u8y2xpOk.js} +0 -0
  489. /package/dist/web/assets/{material-theme-lighter-BtKkCMVq.js → material-theme-lighter-cyQZK3Tg.js} +0 -0
  490. /package/dist/web/assets/{material-theme-ocean-irrDC89L.js → material-theme-ocean-LBAVvw7k.js} +0 -0
  491. /package/dist/web/assets/{material-theme-palenight-Eq5_K4LM.js → material-theme-palenight-BJqhBxL0.js} +0 -0
  492. /package/dist/web/assets/{matlab-BE3oCXZC.js → matlab-B38sOJee.js} +0 -0
  493. /package/dist/web/assets/{mdx-DYiuFTGw.js → mdx-C8QrE_IW.js} +0 -0
  494. /package/dist/web/assets/{mermaid-DVlaEvtw.js → mermaid-BQ-eeU8A.js} +0 -0
  495. /package/dist/web/assets/{min-dark-DSkeTpQa.js → min-dark-D4gNa5tv.js} +0 -0
  496. /package/dist/web/assets/{min-light-CFv2Q0uF.js → min-light-Dfbhn_PL.js} +0 -0
  497. /package/dist/web/assets/{mipsasm-CiAFuZQ2.js → mipsasm-bLOhJkdm.js} +0 -0
  498. /package/dist/web/assets/{mojo-ARshcaSQ.js → mojo-B0BCOMQW.js} +0 -0
  499. /package/dist/web/assets/{monaco-dotenv-language-BDYnKcnr.js → monaco-dotenv-language-uB-wok6C.js} +0 -0
  500. /package/dist/web/assets/{monokai-CPTV-SOg.js → monokai-Bdw_07OM.js} +0 -0
  501. /package/dist/web/assets/{moonbit-D57B0dZr.js → moonbit-BFdGaeuk.js} +0 -0
  502. /package/dist/web/assets/{move-qgy3G_o9.js → move-CyXQFGPM.js} +0 -0
  503. /package/dist/web/assets/{narrat-CZ3rC-oc.js → narrat-9DvTD3_3.js} +0 -0
  504. /package/dist/web/assets/{nextflow-groovy-BxxIOcI4.js → nextflow-groovy-C4VY3kNY.js} +0 -0
  505. /package/dist/web/assets/{night-owl-B1ryGCda.js → night-owl-feYm66V0.js} +0 -0
  506. /package/dist/web/assets/{night-owl-light-BZvNnvMF.js → night-owl-light-DALqFjvx.js} +0 -0
  507. /package/dist/web/assets/{nix-BVBQi8yv.js → nix-C1kPA9Sw.js} +0 -0
  508. /package/dist/web/assets/{nord-3NXw613h.js → nord-1dvrj1Al.js} +0 -0
  509. /package/dist/web/assets/{nushell-BMOLRalb.js → nushell-DSQ_9Uqj.js} +0 -0
  510. /package/dist/web/assets/{objective-c-AxruEUgh.js → objective-c-Dhcn5ZK8.js} +0 -0
  511. /package/dist/web/assets/{objective-cpp-cFT0qRsD.js → objective-cpp-CGVYCSrp.js} +0 -0
  512. /package/dist/web/assets/{ocaml-_8JH6O8J.js → ocaml-rxXW21An.js} +0 -0
  513. /package/dist/web/assets/{odin-nwYvR0O6.js → odin-ClSJX2DE.js} +0 -0
  514. /package/dist/web/assets/{one-dark-pro-C1LaYJaH.js → one-dark-pro-3gbWDfFW.js} +0 -0
  515. /package/dist/web/assets/{one-light-B7rMc__c.js → one-light-C2q5ZZAw.js} +0 -0
  516. /package/dist/web/assets/{openscad-Cwes8bKO.js → openscad-eHXeafBl.js} +0 -0
  517. /package/dist/web/assets/{pascal-DMt-6tdZ.js → pascal-kKha3Txs.js} +0 -0
  518. /package/dist/web/assets/{pkl-QBTyYQmB.js → pkl-CTiPytg-.js} +0 -0
  519. /package/dist/web/assets/{plastic-BTSUp8Fk.js → plastic-D1Ygezi4.js} +0 -0
  520. /package/dist/web/assets/{plsql-DQv3IFXP.js → plsql-D_gbgZIr.js} +0 -0
  521. /package/dist/web/assets/{po-CNwZx4tW.js → po-BkhbDUnq.js} +0 -0
  522. /package/dist/web/assets/{poimandres-8Os7Qequ.js → poimandres-Do6NywrA.js} +0 -0
  523. /package/dist/web/assets/{polar-b4yK8oD8.js → polar-B3B23wca.js} +0 -0
  524. /package/dist/web/assets/{postcss-sFctwpxx.js → postcss-Dv_YBvWd.js} +0 -0
  525. /package/dist/web/assets/{powerquery-Cg_WrtuN.js → powerquery-fKJWI-Sh.js} +0 -0
  526. /package/dist/web/assets/{powershell-C7Ifz95h.js → powershell-B4Y7KIgx.js} +0 -0
  527. /package/dist/web/assets/{prisma-Z52HUJzQ.js → prisma-J--XdTXX.js} +0 -0
  528. /package/dist/web/assets/{prolog-DLqW-zR1.js → prolog-BBTg4Vb3.js} +0 -0
  529. /package/dist/web/assets/{proto-BOIzxCCJ.js → proto-DWlum3T8.js} +0 -0
  530. /package/dist/web/assets/{puppet-De7TjZIf.js → puppet-ZiUfc9JV.js} +0 -0
  531. /package/dist/web/assets/{purescript-R76ou1WT.js → purescript-CmT8SCHB.js} +0 -0
  532. /package/dist/web/assets/{python-Dprm0tNP.js → python-BURpj-fq.js} +0 -0
  533. /package/dist/web/assets/{qmldir-Clp_jpNw.js → qmldir-CpaiJywQ.js} +0 -0
  534. /package/dist/web/assets/{qss-eQPYYzSo.js → qss-C-zdz0i5.js} +0 -0
  535. /package/dist/web/assets/{r-CCcRHIf5.js → r-K56BeS-s.js} +0 -0
  536. /package/dist/web/assets/{racket-CzE472yp.js → racket-gkn6Z_Jw.js} +0 -0
  537. /package/dist/web/assets/{raku-DQwXrTSi.js → raku-C0ZBuKlx.js} +0 -0
  538. /package/dist/web/assets/{red-KCS6wyop.js → red-BWZ6x-3p.js} +0 -0
  539. /package/dist/web/assets/{reg-CcrAddUh.js → reg-Dt_EsyGR.js} +0 -0
  540. /package/dist/web/assets/{regexp-DDbud-Ri.js → regexp-CT6BLG2C.js} +0 -0
  541. /package/dist/web/assets/{rel-99GDgaHp.js → rel-MJ2LcyzJ.js} +0 -0
  542. /package/dist/web/assets/{resolve-theme-CW2niGV2.js → resolve-theme-B_ukrg_8.js} +0 -0
  543. /package/dist/web/assets/{riscv-Dm2eNv0R.js → riscv-BZYWzMAA.js} +0 -0
  544. /package/dist/web/assets/{ron-CzFt0ygH.js → ron-CLbwHWMn.js} +0 -0
  545. /package/dist/web/assets/{rose-pine-Ck6lCrCP.js → rose-pine-BPiI_daR.js} +0 -0
  546. /package/dist/web/assets/{rose-pine-dawn-vtALRJNj.js → rose-pine-dawn-BjkJc7dW.js} +0 -0
  547. /package/dist/web/assets/{rose-pine-moon-DPto8qXw.js → rose-pine-moon-BQSxkt7g.js} +0 -0
  548. /package/dist/web/assets/{rosmsg-NtS6429H.js → rosmsg-yt4151pJ.js} +0 -0
  549. /package/dist/web/assets/{rust-BjIXvF9h.js → rust-1dAcengL.js} +0 -0
  550. /package/dist/web/assets/{sass-CQ23wf5E.js → sass-DUgtQJsg.js} +0 -0
  551. /package/dist/web/assets/{scala-LfKLM143.js → scala-CxoGYpS0.js} +0 -0
  552. /package/dist/web/assets/{scheme-D3SLBXlF.js → scheme-B2K3u9e3.js} +0 -0
  553. /package/dist/web/assets/{sdbl-ODRd2rGN.js → sdbl-C0Ygk0n6.js} +0 -0
  554. /package/dist/web/assets/{shellscript-3KB1G_oA.js → shellscript-C2CvhqMk.js} +0 -0
  555. /package/dist/web/assets/{slack-dark-DTtyWmIy.js → slack-dark-DYdgzRGj.js} +0 -0
  556. /package/dist/web/assets/{slack-ochin-Bu0PApk3.js → slack-ochin-X-WD0WCg.js} +0 -0
  557. /package/dist/web/assets/{smalltalk-haMT-Sbe.js → smalltalk-Dp7g_zb9.js} +0 -0
  558. /package/dist/web/assets/{snazzy-light-DXJzefE9.js → snazzy-light-DWHWr-a6.js} +0 -0
  559. /package/dist/web/assets/{solarized-dark-B6LyYa18.js → solarized-dark-CxdLsjvb.js} +0 -0
  560. /package/dist/web/assets/{solarized-light-QLADebkv.js → solarized-light-Cz9rAvG5.js} +0 -0
  561. /package/dist/web/assets/{solidity-mzfq1aDc.js → solidity-D33gHHdn.js} +0 -0
  562. /package/dist/web/assets/{splunk-CyH4RWBp.js → splunk-BFbOroYw.js} +0 -0
  563. /package/dist/web/assets/{sql-CWVlT5D3.js → sql-ChpxvmdY.js} +0 -0
  564. /package/dist/web/assets/{ssh-config-DNvQJ1Qx.js → ssh-config-CUuSQ6JG.js} +0 -0
  565. /package/dist/web/assets/{streaming-store-uDu3PoN1.js → streaming-store-BYukdB56.js} +0 -0
  566. /package/dist/web/assets/{stylus-ByQQdtmf.js → stylus-DDihP0_W.js} +0 -0
  567. /package/dist/web/assets/{swift-D1cpY4ow.js → swift-XoalUn4x.js} +0 -0
  568. /package/dist/web/assets/{synthwave-84-BDF_-QiW.js → synthwave-84-DjkG0Bff.js} +0 -0
  569. /package/dist/web/assets/{system-verilog-BYrkMydC.js → system-verilog-f_pLKMYd.js} +0 -0
  570. /package/dist/web/assets/{systemd-CQm3zyK0.js → systemd-DjDcLfxp.js} +0 -0
  571. /package/dist/web/assets/{talonscript-DMW5CzE1.js → talonscript-BpUzYcDl.js} +0 -0
  572. /package/dist/web/assets/{tasl-BhFzMCr2.js → tasl-DP-cUbg8.js} +0 -0
  573. /package/dist/web/assets/{tcl-D7VEAF-9.js → tcl-BHoy-dfE.js} +0 -0
  574. /package/dist/web/assets/{terraform-DdT5A0Vy.js → terraform-vCVxN25B.js} +0 -0
  575. /package/dist/web/assets/{tokyo-night-WGLMM6Q9.js → tokyo-night-D0u4xZba.js} +0 -0
  576. /package/dist/web/assets/{toml-CZ7ipXHZ.js → toml-BS8D0__A.js} +0 -0
  577. /package/dist/web/assets/{tsv-B1zVvoTr.js → tsv-DCb-NQh3.js} +0 -0
  578. /package/dist/web/assets/{tsx-D4HwRUBq.js → tsx-DF8YeRgG.js} +0 -0
  579. /package/dist/web/assets/{turtle-B4r8KEJH.js → turtle-Bhx1GlIy.js} +0 -0
  580. /package/dist/web/assets/{typescript-BnHhxMtp.js → typescript-CAVD2Wb6.js} +0 -0
  581. /package/dist/web/assets/{typespec-DP2q46GT.js → typespec-D7mchdY_.js} +0 -0
  582. /package/dist/web/assets/{typst-DFsD_yaD.js → typst-CeXzq6Yw.js} +0 -0
  583. /package/dist/web/assets/{use-is-mobile-BQ9Bv5Ga.js → use-is-mobile-TkcwP1Gx.js} +0 -0
  584. /package/dist/web/assets/{use-visual-viewport-CbwnktSG.js → use-visual-viewport-Lp878uzs.js} +0 -0
  585. /package/dist/web/assets/{v-Bji8hSVT.js → v-DhzO54je.js} +0 -0
  586. /package/dist/web/assets/{vala-D3TIgGmU.js → vala-Dw_RAk3E.js} +0 -0
  587. /package/dist/web/assets/{vb-DvcxFS2q.js → vb-CiXdNh2Y.js} +0 -0
  588. /package/dist/web/assets/{vendor-xterm-BqPDsE9b.js → vendor-xterm-BHEY6Ctu.js} +0 -0
  589. /package/dist/web/assets/{verilog-Dy7cF35d.js → verilog-fITdxqwV.js} +0 -0
  590. /package/dist/web/assets/{vesper-BzjwUv9J.js → vesper-B1wm_At7.js} +0 -0
  591. /package/dist/web/assets/{vhdl-DuFnFNmW.js → vhdl-ebHjqKp9.js} +0 -0
  592. /package/dist/web/assets/{viml-yAPgKlbz.js → viml-Dgmnoxoo.js} +0 -0
  593. /package/dist/web/assets/{vitesse-black-oSSQdyIZ.js → vitesse-black-BZ5gMb7l.js} +0 -0
  594. /package/dist/web/assets/{vitesse-dark-BZxwPihB.js → vitesse-dark-Lij7kkOt.js} +0 -0
  595. /package/dist/web/assets/{vitesse-light-uIigycvt.js → vitesse-light-DW6Wq7VC.js} +0 -0
  596. /package/dist/web/assets/{vyper-CX3NAxm5.js → vyper-6FxDnVNg.js} +0 -0
  597. /package/dist/web/assets/{wasm-6mEOOvVa.js → wasm-BDxaNPCj.js} +0 -0
  598. /package/dist/web/assets/{wasm-D9td37qO.js → wasm-DKhAtzfe.js} +0 -0
  599. /package/dist/web/assets/{wenyan-lwNIHgk6.js → wenyan-C0k0DYG_.js} +0 -0
  600. /package/dist/web/assets/{wgsl-DQAzCfog.js → wgsl-Y2zagbdn.js} +0 -0
  601. /package/dist/web/assets/{wikitext-DTXmt5NE.js → wikitext-C4QGWzkY.js} +0 -0
  602. /package/dist/web/assets/{wit-Bdw_7WDH.js → wit-CQESzjHy.js} +0 -0
  603. /package/dist/web/assets/{wolfram-DqyHDZ07.js → wolfram-B-ia69Ia.js} +0 -0
  604. /package/dist/web/assets/{yaml-CPXhdByd.js → yaml-CEYYTQPm.js} +0 -0
  605. /package/dist/web/assets/{zenscript-6s_i5zek.js → zenscript-BggWXIeJ.js} +0 -0
  606. /package/dist/web/assets/{zig-B2htezJ_.js → zig-CbaUfBQe.js} +0 -0
@@ -1,7 +1,7 @@
1
- import { useEffect, useRef, useState, useMemo, useCallback, useLayoutEffect, memo, lazy, Suspense } from "react";
1
+ import { useEffect, useRef, useState, useMemo, useCallback, useLayoutEffect, memo } from "react";
2
+ import { userMessageOrdinals } from "@/lib/message-ordinals";
2
3
  import { useStickToBottom } from "use-stick-to-bottom";
3
- import { getAuthToken } from "@/lib/api-client";
4
- import type { ChatMessage, ChatEvent } from "../../../types/chat";
4
+ import type { ChatMessage } from "../../../types/chat";
5
5
  import type { SessionPhase } from "../../../types/api";
6
6
  import type { BashPartialEntry } from "../../hooks/use-chat";
7
7
  import { ToolCard } from "./tool-cards";
@@ -15,50 +15,26 @@ import { TurnChangeRollup } from "./turn-change-rollup";
15
15
  import { TurnCostWarning } from "./turn-cost-warning";
16
16
  import { TaskTracker } from "./task-tracker";
17
17
  import { extractJsonlPath } from "./pre-compact-button";
18
- // Kick off the markdown chunk fetch at module load (not first render): Suspense
19
- // skeletons that resolve *after* the list mounts grow each message and shove the
20
- // bottom of the transcript out of view on fresh load.
21
- const markdownRendererImport = import("@/components/shared/markdown-renderer");
22
- const MarkdownRenderer = lazy(() =>
23
- markdownRendererImport.then((m) => ({ default: m.MarkdownRenderer }))
24
- );
25
- import { cn, basename } from "@/lib/utils";
18
+ import { MarkdownContent } from "./message-markdown";
19
+ import { UserBubble } from "./message-user-bubble";
20
+ import { InterleavedEvents, ThinkingIndicator } from "./message-events";
26
21
  import { RenderErrorBoundary } from "@/components/shared/markdown-error-boundary";
27
22
 
28
23
  import {
29
24
  AlertCircle,
30
25
  ShieldAlert,
31
26
  Bot,
32
- ChevronDown,
33
- ChevronRight,
34
- FileText,
35
- Image as ImageIcon,
36
27
  Copy,
37
28
  Check,
38
- CheckCircle2,
39
29
  Loader2,
40
- RotateCcw,
41
- TerminalSquare,
42
- ChevronUp,
43
- Tag,
44
30
  XCircle,
45
- ExternalLink,
46
- Slash,
47
- Pencil,
48
31
  } from "@/lib/icons";
49
32
  import { ChatWelcome } from "./chat-welcome";
50
33
  import { ChatScrollNav } from "./chat-scroll-nav";
51
- import { MessageActionBar, ActionButton } from "./message-action-bar";
52
- import { VersionSwitcher } from "./version-switcher";
53
34
  import type { VersionGroup } from "../../../types/api";
54
35
  import { QuestionCard } from "./question-card";
55
- import { parseUserMessage, type SystemTag } from "./user-message-parse";
56
36
  import type { Question } from "./question-card";
57
- import { useTabStore } from "@/stores/tab-store";
58
- import { api } from "@/lib/api-client";
59
- import { useProjectStore } from "@/stores/project-store";
60
- import { useImageOverlay } from "@/stores/image-overlay-store";
61
- import { collectGallery, GALLERY_ITEM_ATTR, GALLERY_ROOT_ATTR } from "@/lib/image-gallery";
37
+ import { GALLERY_ROOT_ATTR } from "@/lib/image-gallery";
62
38
 
63
39
  interface MessageListProps {
64
40
  messages: ChatMessage[];
@@ -105,6 +81,40 @@ interface MessageListProps {
105
81
  isCompactExpanded?: (compactMessageId: string) => boolean;
106
82
  }
107
83
 
84
+ /**
85
+ * Placeholder for a compaction segment being fetched.
86
+ *
87
+ * It sits in the flow rather than floating, so scrolling up lands on something
88
+ * the size of the turns that are coming instead of a blank gap that then jerks
89
+ * downward. The layout effect that preserves distance-from-bottom is what keeps
90
+ * inserting it from moving the messages already on screen.
91
+ */
92
+ function PreCompactSkeleton() {
93
+ return (
94
+ <div
95
+ className="px-4 pt-4 space-y-4"
96
+ aria-busy="true"
97
+ aria-label="Loading previous conversation"
98
+ >
99
+ <div className="flex items-center justify-center gap-1.5 text-xs text-text-secondary">
100
+ <Loader2 className="size-3 animate-spin" />
101
+ Loading previous conversation…
102
+ </div>
103
+ {[0, 1, 2].map((i) => (
104
+ <div key={i} className={i % 2 === 0 ? "flex justify-end" : "flex justify-start"}>
105
+ <div
106
+ className={`animate-pulse space-y-2 rounded-lg bg-surface p-3 ${i % 2 === 0 ? "w-1/2" : "w-3/4"}`}
107
+ >
108
+ <div className="h-3 rounded bg-border" />
109
+ <div className="h-3 w-5/6 rounded bg-border" />
110
+ {i % 2 === 1 && <div className="h-3 w-2/3 rounded bg-border" />}
111
+ </div>
112
+ </div>
113
+ ))}
114
+ </div>
115
+ );
116
+ }
117
+
108
118
  export function MessageList({
109
119
  messages,
110
120
  messagesLoading,
@@ -150,6 +160,10 @@ export function MessageList({
150
160
  return hasContent || hasEvents;
151
161
  }), [messages]);
152
162
 
163
+ // Counted once for the whole list — see `userMessageOrdinals` for why the
164
+ // obvious per-row form is the thing that makes a long transcript unusable.
165
+ const userOrdinals = useMemo(() => userMessageOrdinals(filtered), [filtered]);
166
+
153
167
  // The approval card + "thinking…" indicator ride at the end, inside the scrolled
154
168
  // content so stick-to-bottom keeps them in view.
155
169
  const hasTrailing = !!pendingApproval || isStreaming;
@@ -200,14 +214,26 @@ export function MessageList({
200
214
  // distance-from-bottom before the prepend, restore scrollTop after so the content
201
215
  // being read doesn't jump. Only fires for prepends — streaming appends leave the
202
216
  // ref null, so this is a no-op during normal streaming.
217
+ // Declared above the scroll-preserving layout effect below, which reads
218
+ // `autoLoadingCompact` to know whether the skeleton is still occupying space.
219
+ const [autoLoadingCompact, setAutoLoadingCompact] = useState(false);
220
+ // A failed expand used to be invisible: the fetch rejected, nothing caught it,
221
+ // and scrolling to the top of a long chat simply did nothing forever. Holding
222
+ // the reason both shows it and stops the loader retrying on every intersection.
223
+ const [compactLoadError, setCompactLoadError] = useState<string | null>(null);
224
+
203
225
  const preserveFromBottomRef = useRef<number | null>(null);
204
226
  useLayoutEffect(() => {
205
227
  const el = scrollEl;
206
228
  if (el && preserveFromBottomRef.current != null) {
207
229
  el.scrollTop = el.scrollHeight - preserveFromBottomRef.current;
208
- preserveFromBottomRef.current = null;
230
+ // Held, not cleared, while the skeleton is up: it occupies real space at
231
+ // the top, so it moves `scrollHeight` without `filtered.length` changing.
232
+ // Clearing here would leave the view jumped by the skeleton's height for
233
+ // as long as the fetch takes, then jumped back when it resolved.
234
+ if (!autoLoadingCompact) preserveFromBottomRef.current = null;
209
235
  }
210
- }, [filtered.length, scrollEl]);
236
+ }, [filtered.length, scrollEl, autoLoadingCompact]);
211
237
 
212
238
  // Jump to the newest message when the conversation/session swaps (initial mount is
213
239
  // handled by `initial: "instant"`). Keyed on sessionId — NOT on filtered[0].id,
@@ -295,30 +321,53 @@ export function MessageList({
295
321
  const topUnexpandedCompact = findTopUnexpandedCompact();
296
322
  const hasMore = !!topUnexpandedCompact;
297
323
 
324
+ // Held as two strings rather than as the object. `findTopUnexpandedCompact`
325
+ // runs on every render and answers with a fresh literal, so depending on that
326
+ // object made `loadMore` new every render — and the observer effect below
327
+ // tore the IntersectionObserver down and built another one with it, which
328
+ // during streaming is every token batch. Measured before: 1 observer at
329
+ // mount, +1 per re-render against an unchanged message list.
330
+ const topCompactId = topUnexpandedCompact?.id ?? null;
331
+ const topCompactPath = topUnexpandedCompact?.jsonlPath ?? null;
332
+
298
333
  // Fetch pre-compact history from the server (prepends older messages).
299
- const [autoLoadingCompact, setAutoLoadingCompact] = useState(false);
300
334
  const loadMore = useCallback(async () => {
301
- if (!topUnexpandedCompact || !onExpandCompact || autoLoadingCompact) return;
335
+ if (!topCompactId || !topCompactPath || !onExpandCompact || autoLoadingCompact) return;
302
336
  // Capture distance-from-bottom so the post-prepend layout effect can hold the
303
337
  // reading position steady while older messages are inserted above.
304
338
  const el = scrollEl;
305
339
  preserveFromBottomRef.current = el ? el.scrollHeight - el.scrollTop : null;
306
340
  setAutoLoadingCompact(true);
341
+ setCompactLoadError(null);
307
342
  try {
308
- await onExpandCompact(topUnexpandedCompact.id, topUnexpandedCompact.jsonlPath);
343
+ await onExpandCompact(topCompactId, topCompactPath);
344
+ } catch (e) {
345
+ setCompactLoadError(e instanceof Error ? e.message : "Could not load previous conversation");
309
346
  } finally {
310
347
  setAutoLoadingCompact(false);
311
348
  }
312
- }, [topUnexpandedCompact, onExpandCompact, autoLoadingCompact, scrollEl]);
313
-
314
- // Auto-load older history when the user scrolls near the top.
349
+ }, [topCompactId, topCompactPath, onExpandCompact, autoLoadingCompact, scrollEl]);
350
+
351
+ // Lazy-load older history when a sentinel at the top of the transcript comes
352
+ // into range. This replaces a scroll listener that re-ran its check on every
353
+ // scroll event and — the part that actually failed — could not fire at all
354
+ // when the loaded history was shorter than the viewport, because there was
355
+ // nothing to scroll. `loadMore` guards on `autoLoadingCompact`, so repeat
356
+ // intersections while a fetch is in flight are free.
357
+ const topSentinelRef = useRef<HTMLDivElement | null>(null);
315
358
  useEffect(() => {
316
359
  const el = scrollEl;
317
- if (!el || !hasMore) return;
318
- const onScroll = () => { if (el.scrollTop < 200 && !autoLoadingCompact) loadMore(); };
319
- el.addEventListener("scroll", onScroll, { passive: true });
320
- return () => el.removeEventListener("scroll", onScroll);
321
- }, [scrollEl, hasMore, autoLoadingCompact, loadMore]);
360
+ const sentinel = topSentinelRef.current;
361
+ if (!el || !sentinel || !hasMore || compactLoadError) return;
362
+ const io = new IntersectionObserver(
363
+ (entries) => { if (entries.some((e) => e.isIntersecting)) loadMore(); },
364
+ // Start the fetch while the top is still a screenful away, so the next
365
+ // segment is usually there before the user reaches the end of this one.
366
+ { root: el, rootMargin: "400px 0px 0px 0px" },
367
+ );
368
+ io.observe(sentinel);
369
+ return () => io.disconnect();
370
+ }, [scrollEl, hasMore, compactLoadError, loadMore]);
322
371
 
323
372
  if (messagesLoading && (!keepStaleWhileLoading || messages.length === 0)) {
324
373
  return (
@@ -353,10 +402,17 @@ export function MessageList({
353
402
  </button>
354
403
  </div>
355
404
  )}
356
- {autoLoadingCompact && (
357
- <div className="absolute top-2 left-0 right-0 z-10 flex items-center justify-center gap-1.5 text-xs text-text-secondary pointer-events-none">
358
- <Loader2 className="size-3 animate-spin" />
359
- Loading previous conversation...
405
+ {!autoLoadingCompact && compactLoadError && (
406
+ <div className="absolute top-2 left-0 right-0 z-10 flex justify-center">
407
+ <button
408
+ type="button"
409
+ onClick={() => { setCompactLoadError(null); loadMore(); }}
410
+ className="inline-flex items-center gap-1.5 rounded-md border border-border bg-surface px-3 py-2 text-xs text-text-primary hover:bg-surface-hover min-h-[44px] md:min-h-0"
411
+ >
412
+ <AlertCircle className="size-3.5 text-destructive" />
413
+ <span>Could not load previous conversation: {compactLoadError}</span>
414
+ <span className="underline">Retry</span>
415
+ </button>
360
416
  </div>
361
417
  )}
362
418
  <div
@@ -366,12 +422,12 @@ export function MessageList({
366
422
  style={{ WebkitOverflowScrolling: "touch", overscrollBehavior: "contain" }}
367
423
  >
368
424
  <div ref={contentRef as unknown as React.Ref<HTMLDivElement>} className="w-full">
425
+ {hasMore && <div ref={topSentinelRef} aria-hidden className="h-px" />}
426
+ {autoLoadingCompact && <PreCompactSkeleton />}
369
427
  {filtered.map((msg, globalIdx) => {
370
428
  const prevMsg = globalIdx > 0 ? filtered[globalIdx - 1] : undefined;
371
429
  // User-message ordinal (1-based) — stable version-group anchor across forks.
372
- const versionOrdinal = msg.role === "user"
373
- ? filtered.slice(0, globalIdx + 1).reduce((n, m) => n + (m.role === "user" ? 1 : 0), 0)
374
- : 0;
430
+ const versionOrdinal = userOrdinals[globalIdx] ?? 0;
375
431
  // Resolved here rather than deeper down so the ordinal→group lookup
376
432
  // happens once per message instead of being threaded through bubbles.
377
433
  const versionGroup = versionOrdinal ? versionMap?.[versionOrdinal] : undefined;
@@ -533,820 +589,10 @@ const MessageBubble = memo(function MessageBubble({ message, isStreaming, isLast
533
589
  );
534
590
  });
535
591
 
536
- /** Image extensions that can be previewed inline */
537
- const IMAGE_EXTS = new Set([".png", ".jpg", ".jpeg", ".gif", ".webp"]);
538
-
539
- /** Build a preview URL for an uploaded file (served from /chat/uploads/:filename) */
540
- function uploadPreviewUrl(filePath: string, projectName?: string): string {
541
- const filename = basename(filePath);
542
- // Use a generic project name — the upload route is project-scoped but files are global
543
- return `/api/project/${encodeURIComponent(projectName ?? "_")}/chat/uploads/${encodeURIComponent(filename)}`;
544
- }
545
-
546
- /** Check if a file path is an image based on extension */
547
- function isImagePath(path: string): boolean {
548
- const dot = path.lastIndexOf(".");
549
- if (dot === -1) return false;
550
- return IMAGE_EXTS.has(path.slice(dot).toLowerCase());
551
- }
552
-
553
- function isPdfPath(path: string): boolean {
554
- return path.toLowerCase().endsWith(".pdf");
555
- }
556
-
557
- /** Detect if tags contain system-injected content (not real user input) */
558
- const SYSTEM_TAG_NAMES = new Set(["task-notification", "environment_details", "local-command-caveat"]);
559
-
560
- /** User message bubble — full width, collapsible, with system tag badges */
561
- function UserBubble({ content, messageId, timestamp, projectName, onFork, onEdit, isEditing, sessionId, providerId, versionGroup, onNavigateVersion, versionNavDisabled }: {
562
- content: string;
563
- messageId?: string;
564
- timestamp: string;
565
- projectName?: string;
566
- onFork?: () => void;
567
- onEdit?: () => void;
568
- isEditing?: boolean;
569
- sessionId?: string;
570
- providerId?: string;
571
- versionGroup?: VersionGroup;
572
- onNavigateVersion?: (sessionId: string) => void;
573
- versionNavDisabled?: boolean;
574
- }) {
575
- const { files, text, tags, command, terminalBlocks, idePath, agent } = useMemo(
576
- () => parseUserMessage(content),
577
- [content],
578
- );
579
-
580
- const isSystemContext = tags.some((t) => SYSTEM_TAG_NAMES.has(t.name));
581
-
582
- const [expanded, setExpanded] = useState(false);
583
- const [isOverflowing, setIsOverflowing] = useState(false);
584
- const contentRef = useRef<HTMLDivElement>(null);
585
-
586
- useEffect(() => {
587
- const el = contentRef.current;
588
- if (!el) return;
589
- const check = () => setIsOverflowing(el.scrollHeight > el.clientHeight + 2);
590
- check();
591
- const ro = new ResizeObserver(check);
592
- ro.observe(el);
593
- return () => ro.disconnect();
594
- }, [text]);
595
-
596
- return (
597
- <div className={cn("flex flex-col gap-1", !isSystemContext && "items-end")}>
598
- {/* Own-content bubble: accent-wash fill + custom radius (sharp bottom-right
599
- corner anchors it to the sender on the right). */}
600
- <div
601
- data-user-message={!isSystemContext ? "true" : undefined}
602
- style={!isSystemContext ? { borderRadius: "var(--rad) var(--rad) 4px var(--rad)" } : undefined}
603
- className={cn(
604
- "group/user relative px-3 py-2 text-sm border shadow-sm transition-all",
605
- isSystemContext
606
- ? "rounded-lg bg-surface/40 border-border/40 text-text-secondary"
607
- : "max-w-[80%] bg-accent-wash border-accent-wash-border text-text",
608
- isEditing && "ring-2 ring-primary/60 border-primary/40",
609
- )}>
610
- {/* System tags as badges */}
611
- {tags.length > 0 && <SystemTagBadges tags={tags} />}
612
-
613
- {/* Agent delegation chip — parsed back from the "Use the X agent to" prefix */}
614
- {agent && (
615
- <div className="flex items-center gap-1.5 mb-0.5">
616
- <span className="inline-flex items-center gap-1 rounded-md bg-sky-500/15 border border-sky-500/25 px-2 py-0.5 text-xs font-medium text-sky-600 dark:text-sky-400">
617
- <Bot className="size-3 shrink-0" />
618
- {agent}
619
- </span>
620
- </div>
621
- )}
622
-
623
- {/* Slash command chip — args rendered in body for expand/collapse support */}
624
- {command && (
625
- <div className="flex items-center gap-1.5 mb-0.5">
626
- <span className="inline-flex items-center gap-1 rounded-md bg-primary/15 border border-primary/20 px-2 py-0.5 text-xs font-medium text-primary">
627
- <Slash className="size-3 shrink-0" />
628
- {command.name}
629
- </span>
630
- </div>
631
- )}
632
-
633
- {/* IDE context — the file the user had open, as a clickable chip */}
634
- {idePath && (
635
- <div className="flex items-center gap-1.5 mb-1 text-[11px] text-text-subtle">
636
- <span className="shrink-0">Opened in IDE:</span>
637
- <FilePathChip path={idePath} projectName={projectName} />
638
- </div>
639
- )}
640
-
641
- {/* Attached files — image thumbnails + file chips */}
642
- {files.length > 0 && (
643
- <div className="flex flex-wrap gap-1.5">
644
- {files.map((filePath, i) =>
645
- isImagePath(filePath) ? (
646
- <AuthImageThumbnail key={i} filePath={filePath} projectName={projectName} />
647
- ) : (
648
- <div
649
- key={i}
650
- className="flex items-center gap-1 rounded-md border border-border/60 bg-background/40 px-1.5 py-0.5 text-[11px] text-text-secondary"
651
- >
652
- <FileText className="size-3 shrink-0" />
653
- <span className="truncate max-w-32">{basename(filePath)}</span>
654
- </div>
655
- ),
656
- )}
657
- </div>
658
- )}
659
-
660
- {/* Terminal output previews */}
661
- {terminalBlocks.length > 0 && (
662
- <div className="space-y-1.5">
663
- {terminalBlocks.map((block, i) => (
664
- <TerminalBlockPreview key={i} content={block} />
665
- ))}
666
- </div>
667
- )}
668
-
669
- {/* Text content — 2-line clamp by default, expandable */}
670
- {text && (
671
- <div
672
- ref={contentRef}
673
- className={cn(
674
- "whitespace-pre-wrap break-words transition-all duration-200 select-text",
675
- !expanded && "line-clamp-2",
676
- expanded && "max-h-[50vh] overflow-y-auto",
677
- )}
678
- >
679
- {isSystemContext ? <TextWithFilePaths text={text} projectName={projectName} /> : <TextWithLinks text={text} />}
680
- </div>
681
- )}
682
- {(isOverflowing || expanded) && (
683
- <button
684
- onClick={() => setExpanded(!expanded)}
685
- className={cn(
686
- "flex items-center gap-1 text-xs mt-1 transition-colors",
687
- isSystemContext ? "text-text-subtle hover:text-text-secondary" : "text-primary/70 hover:text-primary",
688
- )}
689
- >
690
- {expanded ? <><ChevronUp className="size-3" />Show less</> : <><ChevronDown className="size-3" />Show more</>}
691
- </button>
692
- )}
693
- {/* Version switcher — only when this message has edited siblings */}
694
- {!isSystemContext && onNavigateVersion && (
695
- <VersionSwitcher
696
- group={versionGroup}
697
- onNavigate={onNavigateVersion}
698
- disabled={versionNavDisabled}
699
- />
700
- )}
701
- </div>
702
- {/* Action bar below the bubble — timestamp, copy, edit/fork (real user messages only) */}
703
- {!isSystemContext && (
704
- <MessageActionBar timestamp={timestamp} content={content}>
705
- {onEdit && (
706
- <ActionButton
707
- icon={<Pencil className="size-3.5" />}
708
- label="Edit"
709
- title="Edit this message (continue in the same tab)"
710
- onClick={onEdit}
711
- />
712
- )}
713
- {onFork && (
714
- <ActionButton
715
- icon={<RotateCcw className="size-3.5" />}
716
- label="Fork"
717
- title="Retry from this message (fork into a new tab)"
718
- onClick={onFork}
719
- />
720
- )}
721
- </MessageActionBar>
722
- )}
723
- </div>
724
- );
725
- }
726
-
727
- /** Collapsible terminal output preview in user messages */
728
- function TerminalBlockPreview({ content }: { content: string }) {
729
- const [expanded, setExpanded] = useState(false);
730
- return (
731
- <div>
732
- <button
733
- type="button"
734
- onClick={() => setExpanded(!expanded)}
735
- className={cn(
736
- "flex items-center gap-1.5 rounded-md border px-2 py-1 text-xs transition-colors",
737
- expanded
738
- ? "border-primary/50 bg-surface-elevated text-text-primary"
739
- : "border-border/60 bg-background/40 text-text-secondary hover:bg-surface",
740
- )}
741
- >
742
- <TerminalSquare className="size-3.5 shrink-0" />
743
- <span>Terminal output</span>
744
- <ChevronDown className={cn("size-3 shrink-0 transition-transform", expanded && "rotate-180")} />
745
- </button>
746
- {expanded && (
747
- <pre className="mt-1 max-h-40 overflow-auto rounded-md border border-border bg-background p-2 text-xs text-text-primary font-mono whitespace-pre-wrap break-words">
748
- {content}
749
- </pre>
750
- )}
751
- </div>
752
- );
753
- }
754
-
755
- /** Render system tags as collapsible badges */
756
- function SystemTagBadges({ tags }: { tags: SystemTag[] }) {
757
- return (
758
- <div className="flex flex-wrap gap-1.5">
759
- {tags.map((tag, i) => (
760
- <SystemTagBadge key={i} tag={tag} />
761
- ))}
762
- </div>
763
- );
764
- }
765
-
766
- function SystemTagBadge({ tag }: { tag: SystemTag }) {
767
- const [open, setOpen] = useState(false);
768
-
769
- // Task notification: render formatted instead of raw XML
770
- if (tag.name === "task-notification") {
771
- return <TaskNotificationBadge content={tag.content} />;
772
- }
773
-
774
- return (
775
- <div className="text-xs">
776
- <button
777
- onClick={() => setOpen(!open)}
778
- className="flex items-center gap-1 rounded-full border border-border/60 bg-surface/50 px-2 py-0.5 text-text-subtle hover:text-text-secondary hover:bg-surface transition-colors"
779
- >
780
- <Tag className="size-2.5" />
781
- <span>{tag.label}</span>
782
- <ChevronRight className={cn("size-2.5 transition-transform", open && "rotate-90")} />
783
- </button>
784
- {open && (
785
- <div className="mt-1 rounded border border-border/40 bg-surface/30 px-2 py-1.5 text-[11px] text-text-subtle/80 whitespace-pre-wrap max-h-40 overflow-y-auto leading-relaxed">
786
- {tag.content}
787
- </div>
788
- )}
789
- </div>
790
- );
791
- }
792
-
793
- /** Extract a sub-tag value from XML-like content */
794
- function xmlTag(content: string, tag: string): string | undefined {
795
- const m = content.match(new RegExp(`<${tag}>([\\s\\S]*?)</${tag}>`));
796
- return m?.[1]?.trim() || undefined;
797
- }
798
-
799
- /** Formatted badge for <task-notification> — shows status, summary, output file, result */
800
- function TaskNotificationBadge({ content }: { content: string }) {
801
- const [open, setOpen] = useState(false);
802
- const status = xmlTag(content, "status");
803
- const summary = xmlTag(content, "summary");
804
- const outputFile = xmlTag(content, "output-file");
805
- const result = xmlTag(content, "result");
806
- const isOk = status === "completed";
807
-
808
- return (
809
- <div className="text-xs">
810
- <button
811
- onClick={() => setOpen(!open)}
812
- className="flex items-center gap-1.5 rounded-full border border-border/60 bg-surface/50 px-2 py-0.5 text-text-subtle hover:text-text-secondary hover:bg-surface transition-colors"
813
- >
814
- {isOk ? <CheckCircle2 className="size-2.5 text-success" /> : <XCircle className="size-2.5 text-warning" />}
815
- <span className="truncate max-w-80">{summary ?? "Task notification"}</span>
816
- <ChevronRight className={cn("size-2.5 transition-transform shrink-0", open && "rotate-90")} />
817
- </button>
818
- {open && (
819
- <div className="mt-1 rounded border border-border/40 bg-surface/30 px-2 py-1.5 space-y-1.5">
820
- {/* Full summary (button truncates it) */}
821
- {summary && <p className="text-[11px] text-text-secondary">{summary}</p>}
822
- {outputFile && <FilePathChip path={outputFile} />}
823
- {result && (
824
- <div className="text-[11px] text-text-subtle/80 max-h-60 overflow-y-auto leading-relaxed">
825
- <MarkdownContent content={result} />
826
- </div>
827
- )}
828
- </div>
829
- )}
830
- </div>
831
- );
832
- }
833
-
834
- /** Clickable file path chip — opens file in editor tab */
835
- function FilePathChip({ path, projectName }: { path: string; projectName?: string }) {
836
- const handleClick = useCallback(() => {
837
- const openTab = useTabStore.getState().openTab;
838
- const pName = projectName ?? useProjectStore.getState().activeProject?.name;
839
- const fileName = basename(path);
840
- const meta: Record<string, unknown> = { filePath: path };
841
- if (pName) meta.projectName = pName;
842
- // Try to verify file exists, then open; fallback: open directly
843
- api.get(`/api/fs/read?path=${encodeURIComponent(path)}`).then(() => {
844
- openTab({ type: "editor", title: fileName, metadata: meta, projectId: null, closable: true });
845
- }).catch(() => {
846
- openTab({ type: "editor", title: fileName, metadata: meta, projectId: null, closable: true });
847
- });
848
- }, [path, projectName]);
849
-
850
- return (
851
- <button
852
- type="button"
853
- onClick={handleClick}
854
- className="inline-flex items-center gap-1 rounded border border-border/50 bg-surface/50 px-1.5 py-0.5 font-mono text-[10px] text-text-secondary hover:text-text-primary hover:bg-surface transition-colors cursor-pointer"
855
- >
856
- <FileText className="size-2.5 shrink-0" />
857
- <span className="truncate max-w-60">{basename(path)}</span>
858
- <ExternalLink className="size-2 shrink-0 opacity-50" />
859
- </button>
860
- );
861
- }
862
-
863
- /** Render plain text with http(s) URLs turned into clickable links. Preserves
864
- * whitespace/newlines via the surrounding `whitespace-pre-wrap` container. */
865
- function TextWithLinks({ text }: { text: string }) {
866
- const parts = useMemo(() => {
867
- const re = /(https?:\/\/[^\s<>()]+[^\s<>().,;:!?'"])/g;
868
- const result: { kind: "text" | "url"; value: string }[] = [];
869
- let last = 0;
870
- let m;
871
- while ((m = re.exec(text)) !== null) {
872
- if (m.index > last) result.push({ kind: "text", value: text.slice(last, m.index) });
873
- result.push({ kind: "url", value: m[1]! });
874
- last = m.index + m[0].length;
875
- }
876
- if (last < text.length) result.push({ kind: "text", value: text.slice(last) });
877
- return result;
878
- }, [text]);
879
-
880
- // No URLs — return the raw string so nothing about the layout changes.
881
- if (parts.every((p) => p.kind === "text")) return <>{text}</>;
882
-
883
- return (
884
- <>
885
- {parts.map((p, i) =>
886
- p.kind === "url" ? (
887
- <a
888
- key={i}
889
- href={p.value}
890
- target="_blank"
891
- rel="noopener noreferrer"
892
- className="text-primary underline underline-offset-2 hover:text-primary/80 break-all"
893
- onClick={(e) => e.stopPropagation()}
894
- >
895
- {p.value}
896
- </a>
897
- ) : (
898
- <span key={i}>{p.value}</span>
899
- ),
900
- )}
901
- </>
902
- );
903
- }
904
-
905
- /** Render text with absolute file paths detected and turned into clickable chips */
906
- function TextWithFilePaths({ text, projectName }: { text: string; projectName?: string }) {
907
- const parts = useMemo(() => {
908
- // Match absolute file paths (at least 2 segments)
909
- const re = /(\/(?:[\w.\-]+\/)+[\w.\-]+)/g;
910
- const result: { kind: "text" | "path"; value: string }[] = [];
911
- let last = 0;
912
- let m;
913
- while ((m = re.exec(text)) !== null) {
914
- if (m.index > last) result.push({ kind: "text", value: text.slice(last, m.index) });
915
- result.push({ kind: "path", value: m[1]! });
916
- last = m.index + m[0].length;
917
- }
918
- if (last < text.length) result.push({ kind: "text", value: text.slice(last) });
919
- return result;
920
- }, [text]);
921
-
922
- return (
923
- <>
924
- {parts.map((p, i) =>
925
- p.kind === "path"
926
- ? <FilePathChip key={i} path={p.value} projectName={projectName} />
927
- : <span key={i}>{p.value}</span>,
928
- )}
929
- </>
930
- );
931
- }
932
-
933
- /**
934
- * Session-scoped image cache: blob URL + rendered box per src. Virtualized rows
935
- * unmount/remount constantly while scrolling; refetching the image and re-growing
936
- * from the placeholder on EVERY remount changed the row height after paint — the
937
- * repeatable downward jerk when scrolling up through image-bearing messages.
938
- * URLs are intentionally never revoked (bounded by unique images per session).
939
- */
940
- const imageBlobCache = new Map<string, { url: string; w?: number; h?: number }>();
941
-
942
- /** Hook: fetch an image via auth header, return blob URL (cached across mounts) */
943
- function useAuthBlob(src: string): { blobUrl: string | null; error: boolean } {
944
- const [blobUrl, setBlobUrl] = useState<string | null>(() => imageBlobCache.get(src)?.url ?? null);
945
- const [error, setError] = useState(false);
946
-
947
- useEffect(() => {
948
- const cached = imageBlobCache.get(src);
949
- if (cached) { setBlobUrl(cached.url); return; }
950
- let stale = false;
951
- const token = getAuthToken();
952
- fetch(src, { headers: token ? { Authorization: `Bearer ${token}` } : {} })
953
- .then((r) => { if (!r.ok) throw new Error("Failed"); return r.blob(); })
954
- .then((blob) => {
955
- if (stale) return;
956
- const url = URL.createObjectURL(blob);
957
- imageBlobCache.set(src, { url });
958
- setBlobUrl(url);
959
- })
960
- .catch(() => { if (!stale) setError(true); });
961
- return () => { stale = true; }; // cache owns the URL — no revoke
962
- }, [src]);
963
-
964
- return { blobUrl, error };
965
- }
966
-
967
- /** Fetches image with auth header, renders as blob URL — click opens lightbox */
968
- function AuthImage({ src, alt }: { src: string; alt: string }) {
969
- const { blobUrl, error } = useAuthBlob(src);
970
- const openOverlay = useImageOverlay((s) => s.open);
971
- // Rendered box captured after the first load — pins the layout on remounts so
972
- // the row never grows after paint (see imageBlobCache).
973
- const cached = imageBlobCache.get(src);
974
- const box = cached?.w && cached?.h ? { width: cached.w, height: cached.h } : undefined;
975
-
976
- if (error) {
977
- return (
978
- <div className="flex items-center gap-1.5 rounded-md border border-border bg-background/50 px-2 py-1 text-xs text-text-secondary">
979
- <ImageIcon className="size-3.5 shrink-0" />
980
- <span className="truncate max-w-40">{alt}</span>
981
- </div>
982
- );
983
- }
984
-
985
- if (!blobUrl) {
986
- return <div className="rounded-md bg-surface border border-border h-24 w-32 animate-pulse" style={box} />;
987
- }
988
-
989
- return (
990
- <button
991
- type="button"
992
- onClick={(e) => openOverlay(blobUrl, alt, collectGallery(e.currentTarget))}
993
- className="block text-left"
994
- >
995
- <img
996
- src={blobUrl}
997
- alt={alt}
998
- style={box}
999
- {...{ [GALLERY_ITEM_ATTR]: "" }}
1000
- onLoad={(e) => {
1001
- const el = e.currentTarget;
1002
- const c = imageBlobCache.get(src);
1003
- if (c && (!c.w || !c.h)) { c.w = el.offsetWidth; c.h = el.offsetHeight; }
1004
- }}
1005
- className="rounded-md max-h-48 max-w-full object-contain border border-border cursor-pointer hover:opacity-90 transition-opacity"
1006
- />
1007
- </button>
1008
- );
1009
- }
1010
-
1011
- /** Chip for attached images in user bubble — tiny preview replaces icon, click opens lightbox */
1012
- function AuthImageThumbnail({ filePath, projectName }: { filePath: string; projectName?: string }) {
1013
- const src = uploadPreviewUrl(filePath, projectName);
1014
- const { blobUrl, error } = useAuthBlob(src);
1015
- const openOverlay = useImageOverlay((s) => s.open);
1016
- const name = basename(filePath);
1017
-
1018
- return (
1019
- <button
1020
- type="button"
1021
- onClick={(e) => blobUrl && openOverlay(blobUrl, name, collectGallery(e.currentTarget))}
1022
- className="flex items-center gap-1 rounded-md border border-border/60 bg-background/40 px-1.5 py-0.5 text-[11px] text-text-secondary hover:bg-surface transition-colors cursor-pointer"
1023
- >
1024
- {blobUrl ? (
1025
- <img src={blobUrl} alt={name} {...{ [GALLERY_ITEM_ATTR]: "" }} className="size-4 rounded-sm object-cover shrink-0" />
1026
- ) : error ? (
1027
- <ImageIcon className="size-3 shrink-0" />
1028
- ) : (
1029
- <div className="size-4 rounded-sm bg-surface animate-pulse shrink-0" />
1030
- )}
1031
- <span className="truncate max-w-32">{name}</span>
1032
- </button>
1033
- );
1034
- }
1035
-
1036
- /** Fetches file with auth, opens in new browser tab (for PDFs, etc.) */
1037
- function AuthFileLink({ src, filename, mimeType }: { src: string; filename: string; mimeType: string }) {
1038
- const [loading, setLoading] = useState(false);
1039
-
1040
- const handleClick = useCallback(async () => {
1041
- setLoading(true);
1042
- try {
1043
- const token = getAuthToken();
1044
- const res = await fetch(src, { headers: token ? { Authorization: `Bearer ${token}` } : {} });
1045
- if (!res.ok) throw new Error("Failed to load");
1046
- const blob = await res.blob();
1047
- const url = URL.createObjectURL(new Blob([blob], { type: mimeType }));
1048
- window.open(url, "_blank");
1049
- // Revoke after a delay to let the new tab load
1050
- setTimeout(() => URL.revokeObjectURL(url), 60_000);
1051
- } catch {
1052
- // Fallback: try direct link
1053
- window.open(src, "_blank");
1054
- } finally {
1055
- setLoading(false);
1056
- }
1057
- }, [src, mimeType]);
1058
592
 
1059
- return (
1060
- <button
1061
- type="button"
1062
- onClick={handleClick}
1063
- disabled={loading}
1064
- className="flex items-center gap-1.5 rounded-md border border-border bg-background/50 px-2 py-1 text-xs text-text-secondary hover:bg-surface hover:text-text-primary transition-colors cursor-pointer disabled:opacity-50"
1065
- >
1066
- <FileText className="size-3.5 shrink-0 text-error" />
1067
- <span className="truncate max-w-40">{filename}</span>
1068
- {loading && <span className="animate-spin text-[10px]">...</span>}
1069
- </button>
1070
- );
1071
- }
1072
593
 
1073
- /**
1074
- * Renders events in order — consecutive text events merged into one bubble,
1075
- * tool_use/tool_result render as cards between text sections.
1076
- * Last text group shows streaming cursor when actively streaming.
1077
- */
1078
- type EventGroup =
1079
- | { kind: "text"; content: string }
1080
- | { kind: "thinking"; content: string }
1081
- | { kind: "tool"; tool: ChatEvent; result?: ChatEvent; completed?: boolean };
1082
594
 
1083
- function InterleavedEvents({ events, isStreaming, projectName, bashPartialOutput }: {
1084
- events: ChatEvent[];
1085
- isStreaming: boolean;
1086
- projectName?: string;
1087
- bashPartialOutput?: React.RefObject<Map<string, BashPartialEntry>>;
1088
- }) {
1089
- // Group: consecutive text → merged text block; tool_use + tool_result paired by toolUseId
1090
- const groups: EventGroup[] = [];
1091
- let textBuffer = "";
1092
595
 
1093
- // First pass: create groups for text, thinking, and tool_use events
1094
- let thinkingBuffer = "";
1095
- for (let i = 0; i < events.length; i++) {
1096
- const event = events[i]!;
1097
- if (event.type === "thinking") {
1098
- // Flush text buffer first if any
1099
- if (textBuffer) { groups.push({ kind: "text", content: textBuffer }); textBuffer = ""; }
1100
- thinkingBuffer += event.content;
1101
- continue;
1102
- }
1103
- // Flush thinking buffer when non-thinking event arrives
1104
- if (thinkingBuffer) {
1105
- groups.push({ kind: "thinking", content: thinkingBuffer });
1106
- thinkingBuffer = "";
1107
- }
1108
- if (event.type === "account_retry") {
1109
- if (textBuffer) { groups.push({ kind: "text", content: textBuffer }); textBuffer = ""; }
1110
- const label = (event as any).accountLabel ?? "another account";
1111
- const reason = (event as any).reason ?? "Auth failed";
1112
- groups.push({ kind: "text", content: `\n\n> ↻ ${reason} — retrying with **${label}**...\n\n` });
1113
- continue;
1114
- }
1115
- if (event.type === "text") {
1116
- textBuffer += event.content;
1117
- } else if (event.type === "tool_use") {
1118
- // A call may be announced before it has anything to show and re-announced
1119
- // once it does — image generation starts with no file and no prompt, and
1120
- // only learns both when it finishes. The later event describes the same
1121
- // call, so it replaces the card rather than adding a second one.
1122
- const useId = (event as any).toolUseId;
1123
- const existing = useId
1124
- ? groups.find(
1125
- (g) => g.kind === "tool" && g.tool.type === "tool_use" && (g.tool as any).toolUseId === useId,
1126
- ) as (EventGroup & { kind: "tool" }) | undefined
1127
- : undefined;
1128
- if (existing) {
1129
- existing.tool = event;
1130
- continue;
1131
- }
1132
- if (textBuffer) {
1133
- groups.push({ kind: "text", content: textBuffer });
1134
- textBuffer = "";
1135
- }
1136
- groups.push({ kind: "tool", tool: event });
1137
- } else if (event.type === "tool_result") {
1138
- // Skip tool_results in first pass — matched below
1139
- } else {
1140
- if (textBuffer) {
1141
- groups.push({ kind: "text", content: textBuffer });
1142
- textBuffer = "";
1143
- }
1144
- groups.push({ kind: "tool", tool: event });
1145
- }
1146
- }
1147
- if (thinkingBuffer) {
1148
- groups.push({ kind: "thinking", content: thinkingBuffer });
1149
- }
1150
- if (textBuffer) {
1151
- groups.push({ kind: "text", content: textBuffer });
1152
- }
1153
-
1154
- // Second pass: match tool_result events to their tool_use by toolUseId
1155
- const toolResults = events.filter((e) => e.type === "tool_result");
1156
- for (const tr of toolResults) {
1157
- const trId = (tr as any).toolUseId;
1158
- // Match by ID if available
1159
- if (trId) {
1160
- const match = groups.find(
1161
- (g) => g.kind === "tool" && g.tool.type === "tool_use" && (g.tool as any).toolUseId === trId,
1162
- ) as (EventGroup & { kind: "tool" }) | undefined;
1163
- if (match) {
1164
- match.result = tr;
1165
- continue;
1166
- }
1167
- }
1168
- // Fallback: attach to first tool group without a result
1169
- const unmatched = groups.find(
1170
- (g) => g.kind === "tool" && !g.result,
1171
- ) as (EventGroup & { kind: "tool" }) | undefined;
1172
- if (unmatched) {
1173
- unmatched.result = tr;
1174
- }
1175
- }
1176
-
1177
- // Third pass: fallback to embedded result from buffer enrichment (reconnect).
1178
- // When BE buffers tool_result, it also attaches result onto the matching tool_use event.
1179
- for (const g of groups) {
1180
- if (g.kind === "tool" && !g.result && g.tool.type === "tool_use") {
1181
- const embedded = (g.tool as any).result;
1182
- if (embedded) {
1183
- g.result = { type: "tool_result", output: embedded.output, isError: embedded.isError, exitCode: embedded.exitCode } as ChatEvent;
1184
- }
1185
- }
1186
- }
1187
-
1188
- // Mark tool groups without explicit tool_result as completed when:
1189
- // 1. It's a Read and a later Edit on the same file has a result (Edit implies Read finished)
1190
- // 2. Streaming is fully finished
1191
- for (let gi = 0; gi < groups.length; gi++) {
1192
- const g = groups[gi]!;
1193
- if (g.kind === "tool" && !g.result) {
1194
- let impliedDone = false;
1195
- if (g.tool.type === "tool_use" && g.tool.tool === "Read") {
1196
- const readPath = (g.tool.input as any)?.file_path;
1197
- if (readPath) {
1198
- impliedDone = groups.slice(gi + 1).some(
1199
- (later) => later.kind === "tool" && later.result
1200
- && later.tool.type === "tool_use" && later.tool.tool === "Edit"
1201
- && (later.tool.input as any)?.file_path === readPath,
1202
- );
1203
- }
1204
- }
1205
- g.completed = impliedDone || !isStreaming;
1206
- }
1207
- }
1208
-
1209
- return (
1210
- <>
1211
- {groups.map((group, i) => {
1212
- if (group.kind === "thinking") {
1213
- return <ThinkingBlock key={`think-${i}`} content={group.content} isStreaming={isStreaming && i === groups.length - 1} />;
1214
- }
1215
- if (group.kind === "text") {
1216
- const isLast = isStreaming && i === groups.length - 1;
1217
- return (
1218
- <div key={`text-${i}`} className="text-sm text-text-primary select-text">
1219
- <StreamingText content={group.content} animate={isLast} projectName={projectName} />
1220
- </div>
1221
- );
1222
- }
1223
- return <ToolCard key={`tool-${i}`} tool={group.tool} result={group.result} completed={group.completed} projectName={projectName} bashPartialOutput={bashPartialOutput} />;
1224
- })}
1225
- </>
1226
- );
1227
- }
1228
-
1229
- /** Collapsible thinking block — shows Claude's reasoning, collapsed by default when done */
1230
- function ThinkingBlock({ content, isStreaming }: { content: string; isStreaming: boolean }) {
1231
- const [expanded, setExpanded] = useState(isStreaming);
1232
- const scrollRef = useRef<HTMLDivElement>(null);
1233
-
1234
- // Auto-collapse when streaming finishes
1235
- useEffect(() => {
1236
- if (!isStreaming && content.length > 0) setExpanded(false);
1237
- }, [isStreaming, content.length]);
1238
-
1239
- // Auto-scroll to bottom during streaming
1240
- useEffect(() => {
1241
- if (isStreaming && expanded && scrollRef.current) {
1242
- scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
1243
- }
1244
- }, [content, isStreaming, expanded]);
1245
-
1246
- return (
1247
- <div className="rounded border border-border/50 bg-surface/30 text-xs">
1248
- <button
1249
- onClick={() => setExpanded(!expanded)}
1250
- className="flex items-center gap-2 px-2 py-1.5 w-full text-left hover:bg-surface transition-colors text-text-subtle"
1251
- >
1252
- {isStreaming ? <Loader2 className="size-3 animate-spin" /> : <ChevronRight className={`size-3 transition-transform ${expanded ? "rotate-90" : ""}`} />}
1253
- <span>Thinking{isStreaming ? "..." : ""}</span>
1254
- {!isStreaming && <span className="text-text-subtle/50 ml-auto">{content.length > 100 ? `${Math.round(content.length / 4)} tokens` : ""}</span>}
1255
- </button>
1256
- {expanded && (
1257
- <div ref={scrollRef} className="max-h-60 overflow-y-auto">
1258
- <div className="px-2 pb-2 text-text-subtle/80 whitespace-pre-wrap text-[11px] leading-relaxed">
1259
- {content}
1260
- </div>
1261
- </div>
1262
- )}
1263
- </div>
1264
- );
1265
- }
1266
-
1267
- /**
1268
- * Text component that renders streamed content directly.
1269
- * WebSocket already delivers tokens incrementally — no fake animation needed.
1270
- * When `isStreaming=true`, shows a blinking cursor at the end.
1271
- */
1272
- function StreamingText({ content, animate: isStreaming, projectName }: { content: string; animate: boolean; projectName?: string }) {
1273
- return (
1274
- <>
1275
- <MarkdownContent content={content} projectName={projectName} isStreaming={isStreaming} />
1276
- {isStreaming && (
1277
- <span className="text-text-subtle text-sm animate-pulse">Thinking...</span>
1278
- )}
1279
- </>
1280
- );
1281
- }
1282
-
1283
- /**
1284
- * Shows streaming status with elapsed time and warnings:
1285
- * - No assistant message: "Connecting to Claude..." with elapsed timer
1286
- * - After tool: "Processing..."
1287
- * - Text streaming: hidden
1288
- */
1289
- function ThinkingIndicator({ lastMessage, phase, elapsed, statusMessage }: { lastMessage?: ChatMessage; phase?: SessionPhase; elapsed?: number; statusMessage?: string | null }) {
1290
- // Show indicator when:
1291
- // 1. No assistant message yet (waiting for first response)
1292
- // 2. Last event is tool_result (Claude thinking after tool execution)
1293
- // 3. statusMessage is active (account routing/refreshing)
1294
- // Hide when text is actively streaming (text itself is the indicator)
1295
-
1296
- const isWaiting = !lastMessage || lastMessage.role !== "assistant";
1297
- const isAfterTool = (() => {
1298
- if (!lastMessage?.events?.length) return false;
1299
- const last = lastMessage.events[lastMessage.events.length - 1]!;
1300
- return last.type === "tool_result";
1301
- })();
1302
-
1303
- if (!statusMessage && !isWaiting && !isAfterTool) return null;
1304
-
1305
- const label = statusMessage
1306
- ? statusMessage
1307
- : phase === "initializing" ? "Initializing"
1308
- : phase === "connecting" ? "Connecting"
1309
- : phase === "thinking" ? "Thinking"
1310
- : "Processing";
1311
-
1312
- const isLong = phase === "connecting" && (elapsed ?? 0) >= 30;
1313
-
1314
- return (
1315
- <div className="flex flex-col gap-1 text-sm">
1316
- <div className="flex items-center gap-2 text-text-subtle">
1317
- <Loader2 className="size-3 animate-spin" />
1318
- <span>
1319
- {label}
1320
- {isWaiting && (elapsed ?? 0) > 0 && <span className="text-text-subtle/60">... ({elapsed}s)</span>}
1321
- </span>
1322
- </div>
1323
- {isLong && (
1324
- <p className="text-xs text-warning/80 ml-5">
1325
- Taking longer than usual — may be rate-limited or API slow. Try sending a new message to retry.
1326
- </p>
1327
- )}
1328
- </div>
1329
- );
1330
- }
1331
-
1332
- /** Strip SDK teammate-message XML tags from text — team popover shows these */
1333
- const TEAMMATE_MSG_RE = /<teammate-message[^>]*>[\s\S]*?<\/teammate-message>/g;
1334
- function stripTeammateMessages(text: string): string {
1335
- return text.replace(TEAMMATE_MSG_RE, "").replace(/\n{3,}/g, "\n\n").trim();
1336
- }
1337
-
1338
- /** Wrapper: delegates to shared MarkdownRenderer with code actions enabled */
1339
- function MarkdownContent({ content, projectName, isStreaming }: { content: string; projectName?: string; isStreaming?: boolean }) {
1340
- const cleaned = stripTeammateMessages(content);
1341
- if (!cleaned) return null;
1342
- return (
1343
- <RenderErrorBoundary fallbackContent={cleaned}>
1344
- <Suspense fallback={<div className="animate-pulse h-4 bg-muted rounded" />}>
1345
- <MarkdownRenderer content={cleaned} projectName={projectName} codeActions isStreaming={isStreaming} />
1346
- </Suspense>
1347
- </RenderErrorBoundary>
1348
- );
1349
- }
1350
596
 
1351
597
  /* ToolCard, ToolSummary, ToolDetails extracted to ./tool-cards.tsx */
1352
598