@thaut/claude-ide 0.0.0-stage → 1.0.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 (559) hide show
  1. package/bin/claude-ide.js +23 -0
  2. package/package.json +75 -4
  3. package/server/add-dirs.ts +126 -0
  4. package/server/auth.test.ts +148 -0
  5. package/server/auth.ts +235 -0
  6. package/server/bang.ts +54 -0
  7. package/server/btw-instructions.ts +19 -0
  8. package/server/btw-policy.ts +17 -0
  9. package/server/btw.ts +143 -0
  10. package/server/child-env.ts +29 -0
  11. package/server/cli-control.ts +255 -0
  12. package/server/cli-files.ts +55 -0
  13. package/server/cli-info.ts +73 -0
  14. package/server/config/account.ts +65 -0
  15. package/server/config/agents.ts +186 -0
  16. package/server/config/mcp.ts +86 -0
  17. package/server/config/memory.ts +193 -0
  18. package/server/config/paths.ts +156 -0
  19. package/server/config/plugins.ts +204 -0
  20. package/server/config/routes.ts +95 -0
  21. package/server/config/settings.ts +246 -0
  22. package/server/config.ts +46 -0
  23. package/server/db.ts +121 -0
  24. package/server/directories.test.ts +180 -0
  25. package/server/directories.ts +87 -0
  26. package/server/dist-version.ts +69 -0
  27. package/server/durable-tasks.ts +61 -0
  28. package/server/file-transfers.ts +79 -0
  29. package/server/files.test.ts +127 -0
  30. package/server/files.ts +386 -0
  31. package/server/fs-queue.ts +14 -0
  32. package/server/fsutil.ts +144 -0
  33. package/server/git.ts +86 -0
  34. package/server/goal-state.test.ts +64 -0
  35. package/server/goal-state.ts +78 -0
  36. package/server/history-pages.test.ts +619 -0
  37. package/server/history-pages.ts +513 -0
  38. package/server/http-channel-manager.ts +468 -0
  39. package/server/http-channel-pull.test.ts +314 -0
  40. package/server/http-channel-push.test.ts +364 -0
  41. package/server/http-channel-registration.test.ts +291 -0
  42. package/server/http-channel-routes.ts +73 -0
  43. package/server/http-channel-store.ts +142 -0
  44. package/server/http-channel-test-fixture.ts +103 -0
  45. package/server/ide-messages-mcp.test.ts +82 -0
  46. package/server/ide-messages-mcp.ts +47 -0
  47. package/server/idecron-mcp.ts +65 -0
  48. package/server/idecron.test.ts +79 -0
  49. package/server/idecron.ts +271 -0
  50. package/server/index.ts +336 -0
  51. package/server/live-session.test.ts +1126 -0
  52. package/server/live-session.ts +1165 -0
  53. package/server/project-routes.ts +46 -0
  54. package/server/projects.test.ts +28 -0
  55. package/server/push.ts +315 -0
  56. package/server/report-cache.ts +28 -0
  57. package/server/session-aliases.test.ts +57 -0
  58. package/server/session-aliases.ts +50 -0
  59. package/server/session-branch.ts +71 -0
  60. package/server/session-dirs.ts +189 -0
  61. package/server/session-file.test.ts +134 -0
  62. package/server/session-file.ts +113 -0
  63. package/server/session-messaging.test.ts +297 -0
  64. package/server/session-messaging.ts +165 -0
  65. package/server/sessions.ts +638 -0
  66. package/server/subagents.test.ts +43 -0
  67. package/server/subagents.ts +58 -0
  68. package/server/tool-images.test.ts +76 -0
  69. package/server/tool-images.ts +45 -0
  70. package/server/turn-changes.ts +308 -0
  71. package/server/worktree-git.ts +87 -0
  72. package/server/worktrees.ts +138 -0
  73. package/server/ws.test.ts +195 -0
  74. package/server/ws.ts +197 -0
  75. package/shared/agent-model.ts +15 -0
  76. package/shared/agent-tasks.ts +12 -0
  77. package/shared/config.ts +175 -0
  78. package/shared/directories.ts +12 -0
  79. package/shared/files.ts +51 -0
  80. package/shared/http-channels.ts +63 -0
  81. package/shared/message-window.ts +189 -0
  82. package/shared/project.ts +77 -0
  83. package/shared/protocol.ts +203 -0
  84. package/shared/task-types.ts +4 -0
  85. package/shared/todos.ts +40 -0
  86. package/shared/write-policy.ts +9 -0
  87. package/web/dist/assets/AgentView-wrTKgcJ3.js +1 -0
  88. package/web/dist/assets/AnnotationSelection-BZmNmwVW.js +7 -0
  89. package/web/dist/assets/ChangesView-G1WTzh4q.js +2 -0
  90. package/web/dist/assets/FilesView-DPgAbYt_.js +6 -0
  91. package/web/dist/assets/Form-BKTwiYyw.js +1 -0
  92. package/web/dist/assets/HomePage-Dq2apjBe.js +1 -0
  93. package/web/dist/assets/Markdown-IwP-xF29.js +67 -0
  94. package/web/dist/assets/MessageList-DD9GypSy.js +15 -0
  95. package/web/dist/assets/Modal-g2rv8I_n.js +1 -0
  96. package/web/dist/assets/NotificationSettings-DpuLO_Bv.js +1 -0
  97. package/web/dist/assets/OverviewPage-Dx6miabE.js +1 -0
  98. package/web/dist/assets/ProjectHome-CousyTLL.js +1 -0
  99. package/web/dist/assets/SessionView-CPVL7_HO.js +2 -0
  100. package/web/dist/assets/SettingsPage-D_oYSnW2.js +7 -0
  101. package/web/dist/assets/WorktreesView-C7e0HV__.js +1 -0
  102. package/web/dist/assets/abap-CLvhMVsD.js +1 -0
  103. package/web/dist/assets/actionscript-3-Cs6JJXT5.js +1 -0
  104. package/web/dist/assets/ada-X3Y2mlq2.js +1 -0
  105. package/web/dist/assets/ahk-DwADkxyd.js +1 -0
  106. package/web/dist/assets/ahk2-_6rzW9BS.js +1 -0
  107. package/web/dist/assets/andromeeda-vGVdxbeo.js +1 -0
  108. package/web/dist/assets/angular-html-Cw29lun8.js +1 -0
  109. package/web/dist/assets/angular-ts-BXz-O3SJ.js +1 -0
  110. package/web/dist/assets/apache-U0d_L8uA.js +1 -0
  111. package/web/dist/assets/apex-KFGFyKwG.js +1 -0
  112. package/web/dist/assets/apl-xBchdRhy.js +1 -0
  113. package/web/dist/assets/applescript-CCn79oCD.js +1 -0
  114. package/web/dist/assets/ara-4CJ0cIlV.js +1 -0
  115. package/web/dist/assets/asciidoc-SCjQUq34.js +1 -0
  116. package/web/dist/assets/asm-Cmm7eHzH.js +1 -0
  117. package/web/dist/assets/astro-1q6DGuN3.js +1 -0
  118. package/web/dist/assets/aurora-x-CDeNXAV0.js +1 -0
  119. package/web/dist/assets/awk-BWXHIvNe.js +1 -0
  120. package/web/dist/assets/ayu-dark-DluEY0Gj.js +1 -0
  121. package/web/dist/assets/ayu-light-C3h-C4tm.js +1 -0
  122. package/web/dist/assets/ayu-mirage-Bqwy1Gya.js +1 -0
  123. package/web/dist/assets/ballerina-B7ZEbQpA.js +1 -0
  124. package/web/dist/assets/bat-qdxAdBWQ.js +1 -0
  125. package/web/dist/assets/beancount-C2IN4Vk8.js +1 -0
  126. package/web/dist/assets/berry-DKpUyyne.js +1 -0
  127. package/web/dist/assets/bibtex-Ci_nEsc7.js +1 -0
  128. package/web/dist/assets/bicep-CUHmPFLl.js +1 -0
  129. package/web/dist/assets/bird2-CYIUgYqB.js +1 -0
  130. package/web/dist/assets/blade-Cqdv4oZN.js +1 -0
  131. package/web/dist/assets/bsl-CVED6lJ1.js +1 -0
  132. package/web/dist/assets/c-D9gIxSfx.js +1 -0
  133. package/web/dist/assets/c3-D0apG41V.js +1 -0
  134. package/web/dist/assets/cadence-CQ2zXKGN.js +1 -0
  135. package/web/dist/assets/cairo-DRAZz75c.js +1 -0
  136. package/web/dist/assets/catppuccin-frappe-C9Q5zVZY.js +1 -0
  137. package/web/dist/assets/catppuccin-latte-CtsyrtPq.js +1 -0
  138. package/web/dist/assets/catppuccin-macchiato-R8nAf1vR.js +1 -0
  139. package/web/dist/assets/catppuccin-mocha-DjuaAJKi.js +1 -0
  140. package/web/dist/assets/chapel-CuseGurG.js +1 -0
  141. package/web/dist/assets/clarity-CtfBBspg.js +1 -0
  142. package/web/dist/assets/clojure-DqKBuwfJ.js +1 -0
  143. package/web/dist/assets/cmake-Bj61d0ZC.js +1 -0
  144. package/web/dist/assets/cobol-Bdpj3Qoh.js +1 -0
  145. package/web/dist/assets/codeowners-C8r90Shi.js +1 -0
  146. package/web/dist/assets/codeql-oeQT6MSM.js +1 -0
  147. package/web/dist/assets/coffee-Sk8pHFjs.js +1 -0
  148. package/web/dist/assets/common-lisp-Cv5bFMCO.js +1 -0
  149. package/web/dist/assets/coq-CGaSfhwR.js +1 -0
  150. package/web/dist/assets/core-BxI4V4-T.js +12 -0
  151. package/web/dist/assets/cpp-BQfBYKx3.js +1 -0
  152. package/web/dist/assets/crystal-B3j7DoOZ.js +1 -0
  153. package/web/dist/assets/csharp-dmMxLVLW.js +1 -0
  154. package/web/dist/assets/css-DpFbYmRh.js +1 -0
  155. package/web/dist/assets/csv-Dx-8-gkx.js +1 -0
  156. package/web/dist/assets/cue-CE9AQfxI.js +1 -0
  157. package/web/dist/assets/cypher-ClKdZ_lG.js +1 -0
  158. package/web/dist/assets/d-qD-0Kul2.js +1 -0
  159. package/web/dist/assets/dark-plus-Cs2F2srj.js +1 -0
  160. package/web/dist/assets/dart-CHZY9Xhz.js +1 -0
  161. package/web/dist/assets/dax-BkyTk9wS.js +1 -0
  162. package/web/dist/assets/desktop-Dlh5hvp9.js +1 -0
  163. package/web/dist/assets/diff-woXpYk--.js +1 -0
  164. package/web/dist/assets/docker-IyjqRm3v.js +1 -0
  165. package/web/dist/assets/dotenv-CuH5my32.js +1 -0
  166. package/web/dist/assets/dracula-BHWKrbxM.js +1 -0
  167. package/web/dist/assets/dracula-soft-5eyTD99u.js +1 -0
  168. package/web/dist/assets/dream-maker-DW3nJb8Q.js +1 -0
  169. package/web/dist/assets/edge-DXQWmXTB.js +1 -0
  170. package/web/dist/assets/elixir-CrDIY2Vo.js +1 -0
  171. package/web/dist/assets/elm-BslFEg8K.js +1 -0
  172. package/web/dist/assets/emacs-lisp-DR2vtWaj.js +1 -0
  173. package/web/dist/assets/engine-javascript-DJg3-Vu2.js +141 -0
  174. package/web/dist/assets/erb-CUYhg0Ws.js +1 -0
  175. package/web/dist/assets/erlang-BXdgHVfi.js +1 -0
  176. package/web/dist/assets/everforest-dark-sB-x3p7T.js +1 -0
  177. package/web/dist/assets/everforest-light-Df2xbC6M.js +1 -0
  178. package/web/dist/assets/fennel-DQxkIbk2.js +1 -0
  179. package/web/dist/assets/fish-BJitypiv.js +1 -0
  180. package/web/dist/assets/fluent-C03EYrpw.js +1 -0
  181. package/web/dist/assets/format-DrBC7w0A.js +1 -0
  182. package/web/dist/assets/fortran-fixed-form-D6mN0kjy.js +1 -0
  183. package/web/dist/assets/fortran-free-form-CYNrtFtB.js +1 -0
  184. package/web/dist/assets/fsharp-CwoEF39o.js +1 -0
  185. package/web/dist/assets/gdresource-CZkFGo3B.js +1 -0
  186. package/web/dist/assets/gdscript-DYvRD3hg.js +1 -0
  187. package/web/dist/assets/gdshader-CBce3t8t.js +1 -0
  188. package/web/dist/assets/genie-CV2tkWYe.js +1 -0
  189. package/web/dist/assets/gherkin-DExj1W_8.js +1 -0
  190. package/web/dist/assets/git-commit-dUGrvm6Q.js +1 -0
  191. package/web/dist/assets/git-rebase-4zCRqF28.js +1 -0
  192. package/web/dist/assets/github-dark-C-LZuMrd.js +1 -0
  193. package/web/dist/assets/github-dark-default-DXG-b-1a.js +1 -0
  194. package/web/dist/assets/github-dark-dimmed-Bx1FflLF.js +1 -0
  195. package/web/dist/assets/github-dark-high-contrast-B_tTalzw.js +1 -0
  196. package/web/dist/assets/github-light-EUqPIrTm.js +1 -0
  197. package/web/dist/assets/github-light-default-BXViO-2h.js +1 -0
  198. package/web/dist/assets/github-light-high-contrast-B68TUdTA.js +1 -0
  199. package/web/dist/assets/gleam-CSRkHgEL.js +1 -0
  200. package/web/dist/assets/glimmer-js-7adTRvH4.js +1 -0
  201. package/web/dist/assets/glimmer-ts-IdSskDYw.js +1 -0
  202. package/web/dist/assets/glsl-Qzy53KDt.js +1 -0
  203. package/web/dist/assets/gn-CAXYAgV-.js +1 -0
  204. package/web/dist/assets/gnuplot-7GGW24-e.js +1 -0
  205. package/web/dist/assets/go-rLFTqkRN.js +1 -0
  206. package/web/dist/assets/graphql-CV_puxbB.js +1 -0
  207. package/web/dist/assets/groovy-CacY0gHj.js +1 -0
  208. package/web/dist/assets/gruvbox-dark-hard-C820rvS2.js +1 -0
  209. package/web/dist/assets/gruvbox-dark-medium-BPjhmG05.js +1 -0
  210. package/web/dist/assets/gruvbox-dark-soft-MrdJrrXF.js +1 -0
  211. package/web/dist/assets/gruvbox-light-hard-BC_s9l72.js +1 -0
  212. package/web/dist/assets/gruvbox-light-medium-BAWPOn9u.js +1 -0
  213. package/web/dist/assets/gruvbox-light-soft-BSMLrYjP.js +1 -0
  214. package/web/dist/assets/hack-C5GPjiLV.js +1 -0
  215. package/web/dist/assets/haml-BQ5YR1DQ.js +1 -0
  216. package/web/dist/assets/handlebars-DnhRK9A1.js +1 -0
  217. package/web/dist/assets/haskell-D8IpX4py.js +1 -0
  218. package/web/dist/assets/haxe-C8oCnwJJ.js +1 -0
  219. package/web/dist/assets/hcl-Dh228itO.js +1 -0
  220. package/web/dist/assets/highlight.worker-CY-ZeDAl.js +1 -0
  221. package/web/dist/assets/hjson-CxZEssPk.js +1 -0
  222. package/web/dist/assets/hlsl-Cvrh5tZx.js +1 -0
  223. package/web/dist/assets/horizon-CE9ld1lL.js +1 -0
  224. package/web/dist/assets/horizon-bright-DSNQnXHK.js +1 -0
  225. package/web/dist/assets/houston-CsvMBhTu.js +1 -0
  226. package/web/dist/assets/html-CmHiEKV9.js +1 -0
  227. package/web/dist/assets/html-derivative-BXE-rYIK.js +1 -0
  228. package/web/dist/assets/http-DPjjqtcL.js +1 -0
  229. package/web/dist/assets/hurl-AMBpSloO.js +1 -0
  230. package/web/dist/assets/hxml-D7Y24wKF.js +1 -0
  231. package/web/dist/assets/hy-CZbG8q4J.js +1 -0
  232. package/web/dist/assets/imba-DsUTQ-LC.js +1 -0
  233. package/web/dist/assets/index-DNPnl4sh.css +2 -0
  234. package/web/dist/assets/index-DQh72lEx.js +14 -0
  235. package/web/dist/assets/ini-B5eOa1yu.js +1 -0
  236. package/web/dist/assets/java-CEM7Tdw_.js +1 -0
  237. package/web/dist/assets/javascript-CU3PLowd.js +1 -0
  238. package/web/dist/assets/jinja-fR0x_9Xy.js +1 -0
  239. package/web/dist/assets/jison-BoKsxs5M.js +1 -0
  240. package/web/dist/assets/json-BM2FWsRB.js +1 -0
  241. package/web/dist/assets/json5-BR5RXkoi.js +1 -0
  242. package/web/dist/assets/jsonc-CYpm1nAK.js +1 -0
  243. package/web/dist/assets/jsonl-CmCQp5Yx.js +1 -0
  244. package/web/dist/assets/jsonnet-CJTPZ8u_.js +1 -0
  245. package/web/dist/assets/jssm-DXw9l8Rf.js +1 -0
  246. package/web/dist/assets/jsx-B5fTecQP.js +1 -0
  247. package/web/dist/assets/julia-DGyDG39_.js +1 -0
  248. package/web/dist/assets/just-B-RyLcSR.js +1 -0
  249. package/web/dist/assets/kanagawa-dragon-Bozd7klA.js +1 -0
  250. package/web/dist/assets/kanagawa-lotus-BmuOG9ZW.js +1 -0
  251. package/web/dist/assets/kanagawa-wave-BML5mF2J.js +1 -0
  252. package/web/dist/assets/kdl-CsD5j6eV.js +1 -0
  253. package/web/dist/assets/kotlin-DhhofPvG.js +1 -0
  254. package/web/dist/assets/kusto-C7mF5XQf.js +1 -0
  255. package/web/dist/assets/langs-CciPKqaF.js +1 -0
  256. package/web/dist/assets/laserwave-C_8bwKvT.js +1 -0
  257. package/web/dist/assets/latex-CV1pQelm.js +1 -0
  258. package/web/dist/assets/lean-CewbzKMR.js +1 -0
  259. package/web/dist/assets/less-DVTAwKKz.js +1 -0
  260. package/web/dist/assets/lib-DlcVMUqT.js +1 -0
  261. package/web/dist/assets/light-plus-DVQuIRkW.js +1 -0
  262. package/web/dist/assets/liquid-9p05RSzQ.js +1 -0
  263. package/web/dist/assets/llvm-DqHOMS-s.js +1 -0
  264. package/web/dist/assets/log-BNLmms1o.js +1 -0
  265. package/web/dist/assets/logo-Cluzi2Zq.js +1 -0
  266. package/web/dist/assets/lua-CIPVh0S-.js +1 -0
  267. package/web/dist/assets/luau-VGWVLoqX.js +1 -0
  268. package/web/dist/assets/make-Dixweg8N.js +1 -0
  269. package/web/dist/assets/markdown-BYOwaDjH.js +1 -0
  270. package/web/dist/assets/marko-h2h6YUMS.js +1 -0
  271. package/web/dist/assets/material-theme-Bm3Qr25_.js +1 -0
  272. package/web/dist/assets/material-theme-darker-2IIEA8gg.js +1 -0
  273. package/web/dist/assets/material-theme-lighter-uhdI0v04.js +1 -0
  274. package/web/dist/assets/material-theme-ocean-CHQ94UKr.js +1 -0
  275. package/web/dist/assets/material-theme-palenight-B5W6OYN7.js +1 -0
  276. package/web/dist/assets/matlab-D7qyCx1q.js +1 -0
  277. package/web/dist/assets/mdc-yAyZMdoC.js +1 -0
  278. package/web/dist/assets/mdx-DQZ5AkYe.js +1 -0
  279. package/web/dist/assets/mermaid-BxpfTIzt.js +1 -0
  280. package/web/dist/assets/message-font-size-DoaZfwkT.js +1 -0
  281. package/web/dist/assets/min-dark-BSWPekZh.js +1 -0
  282. package/web/dist/assets/min-light-DDpmG2fV.js +1 -0
  283. package/web/dist/assets/mipsasm-BMqwQI7S.js +1 -0
  284. package/web/dist/assets/mojo-fg40_ntQ.js +1 -0
  285. package/web/dist/assets/monokai-BscAjiQZ.js +1 -0
  286. package/web/dist/assets/moonbit-D9W784E_.js +1 -0
  287. package/web/dist/assets/move-BYnGhZoM.js +1 -0
  288. package/web/dist/assets/narrat-_X_XdTYD.js +1 -0
  289. package/web/dist/assets/nextflow-BoTnSBQ_.js +1 -0
  290. package/web/dist/assets/nextflow-groovy-D7IRt6Pp.js +1 -0
  291. package/web/dist/assets/nginx-jWINucNT.js +1 -0
  292. package/web/dist/assets/night-owl-DhmEMT88.js +1 -0
  293. package/web/dist/assets/night-owl-light-eJ-hLW7d.js +1 -0
  294. package/web/dist/assets/nim-Dtt9DZ0t.js +1 -0
  295. package/web/dist/assets/nix-Cl_7024t.js +1 -0
  296. package/web/dist/assets/nord-Cb4Vim4T.js +1 -0
  297. package/web/dist/assets/nsis-CjNLYCLg.js +1 -0
  298. package/web/dist/assets/nushell--0fT3PI1.js +1 -0
  299. package/web/dist/assets/objective-c-D1A_Heim.js +1 -0
  300. package/web/dist/assets/objective-cpp-BsSzOQcm.js +1 -0
  301. package/web/dist/assets/ocaml-O90oeIOV.js +1 -0
  302. package/web/dist/assets/odin-B1RWQWA5.js +1 -0
  303. package/web/dist/assets/one-dark-pro-C6YyR8OD.js +1 -0
  304. package/web/dist/assets/one-light-D7Lr4KcI.js +1 -0
  305. package/web/dist/assets/openscad-BUDT5pXO.js +1 -0
  306. package/web/dist/assets/org-ClD5huOa.js +1 -0
  307. package/web/dist/assets/pascal-4ZHwLPI5.js +1 -0
  308. package/web/dist/assets/perl-BFegxyeJ.js +1 -0
  309. package/web/dist/assets/php-B-qOA3Y4.js +1 -0
  310. package/web/dist/assets/pkl-ot-7Btpt.js +1 -0
  311. package/web/dist/assets/plastic-DQwYfKfQ.js +1 -0
  312. package/web/dist/assets/plsql-DGHpHOYJ.js +1 -0
  313. package/web/dist/assets/po-BiJDBrnU.js +1 -0
  314. package/web/dist/assets/poimandres-DRFjx7u4.js +1 -0
  315. package/web/dist/assets/polar-C7UOKdEL.js +1 -0
  316. package/web/dist/assets/postcss-BXeXVLqQ.js +1 -0
  317. package/web/dist/assets/powerquery-DNMTfnFr.js +1 -0
  318. package/web/dist/assets/powershell-QtSnmXcu.js +1 -0
  319. package/web/dist/assets/prisma-DMgNhnYS.js +1 -0
  320. package/web/dist/assets/prolog-iXnhIJG7.js +1 -0
  321. package/web/dist/assets/proto-DB4EqR-F.js +1 -0
  322. package/web/dist/assets/pug-Cq0NBPu-.js +1 -0
  323. package/web/dist/assets/puppet-CDv2pdJW.js +1 -0
  324. package/web/dist/assets/purescript-9MfHhQsQ.js +1 -0
  325. package/web/dist/assets/python-gzcpVVnB.js +1 -0
  326. package/web/dist/assets/qml-CYUsa87k.js +1 -0
  327. package/web/dist/assets/qmldir-DCQb3MpD.js +1 -0
  328. package/web/dist/assets/qss-Fe1Jh2GI.js +1 -0
  329. package/web/dist/assets/r-4xJhgbOf.js +1 -0
  330. package/web/dist/assets/racket-DcIDlBhZ.js +1 -0
  331. package/web/dist/assets/raku-B3gFvitq.js +1 -0
  332. package/web/dist/assets/razor-BlHaOHx8.js +1 -0
  333. package/web/dist/assets/rbs-C0MpOrj4.js +1 -0
  334. package/web/dist/assets/react-B4UGpcWC.js +1 -0
  335. package/web/dist/assets/red-CYlAq2b0.js +1 -0
  336. package/web/dist/assets/reg-CRGYupPL.js +1 -0
  337. package/web/dist/assets/regexp-BUSxjv2o.js +1 -0
  338. package/web/dist/assets/rel-BtDbiS_P.js +1 -0
  339. package/web/dist/assets/riscv-Ckw8ddFX.js +1 -0
  340. package/web/dist/assets/ron-VUp2lXgN.js +1 -0
  341. package/web/dist/assets/rose-pine-aMsaMD1D.js +1 -0
  342. package/web/dist/assets/rose-pine-dawn-8PwUyIlP.js +1 -0
  343. package/web/dist/assets/rose-pine-moon-DO-qXg-I.js +1 -0
  344. package/web/dist/assets/rosmsg-CAekHB0j.js +1 -0
  345. package/web/dist/assets/rst-zjfguEh7.js +1 -0
  346. package/web/dist/assets/ruby-4oR9waBK.js +1 -0
  347. package/web/dist/assets/rust-DpiIf5JO.js +1 -0
  348. package/web/dist/assets/sas-Do4V_2i9.js +1 -0
  349. package/web/dist/assets/sass-DXrisJhu.js +1 -0
  350. package/web/dist/assets/scala-BiFt_7mu.js +1 -0
  351. package/web/dist/assets/scheme-DQCgrYNe.js +1 -0
  352. package/web/dist/assets/scss-CW6JPtag.js +1 -0
  353. package/web/dist/assets/sdbl-bTVj8UrX.js +1 -0
  354. package/web/dist/assets/shaderlab-C4rnI7x0.js +1 -0
  355. package/web/dist/assets/shellscript-zBmOMFJh.js +1 -0
  356. package/web/dist/assets/shellsession-DxGUTWaf.js +1 -0
  357. package/web/dist/assets/slack-dark-DnToyrRv.js +1 -0
  358. package/web/dist/assets/slack-ochin-B2OO5cIa.js +1 -0
  359. package/web/dist/assets/smalltalk-BHpQb5nM.js +1 -0
  360. package/web/dist/assets/smithy-Dgaz7aur.js +1 -0
  361. package/web/dist/assets/snazzy-light-4G7pJPwS.js +1 -0
  362. package/web/dist/assets/solarized-dark-b97czJOk.js +1 -0
  363. package/web/dist/assets/solarized-light-DFPxpIYQ.js +1 -0
  364. package/web/dist/assets/solidity-Bj7E1mRn.js +1 -0
  365. package/web/dist/assets/soy-DVtw20Yn.js +1 -0
  366. package/web/dist/assets/sparql-C0ia5K60.js +1 -0
  367. package/web/dist/assets/splunk-BC2Px7Mm.js +1 -0
  368. package/web/dist/assets/sql-DGCM-jag.js +1 -0
  369. package/web/dist/assets/ssh-config-BgfXC-Er.js +1 -0
  370. package/web/dist/assets/stata-DfuMOXna.js +1 -0
  371. package/web/dist/assets/stylus-B6D30XZt.js +1 -0
  372. package/web/dist/assets/surrealql-CBoRWxfR.js +1 -0
  373. package/web/dist/assets/svelte-g_uCUem5.js +1 -0
  374. package/web/dist/assets/swift-CyEgAFGc.js +1 -0
  375. package/web/dist/assets/synthwave-84-nFMaYfgc.js +1 -0
  376. package/web/dist/assets/system-verilog-Dh5ihWbf.js +1 -0
  377. package/web/dist/assets/systemd-BxMlprV5.js +1 -0
  378. package/web/dist/assets/talonscript-CohzipZa.js +1 -0
  379. package/web/dist/assets/tasl-DMoTqEGO.js +1 -0
  380. package/web/dist/assets/tcl-CZd0xW_V.js +1 -0
  381. package/web/dist/assets/templ-N5C1hJ4Y.js +1 -0
  382. package/web/dist/assets/terraform-DswuEJGm.js +1 -0
  383. package/web/dist/assets/tex-mx6xDwuK.js +1 -0
  384. package/web/dist/assets/themes-CNElGaXw.js +1 -0
  385. package/web/dist/assets/tokyo-night-oM2G3aXe.js +1 -0
  386. package/web/dist/assets/toml-CcmNWLt0.js +1 -0
  387. package/web/dist/assets/ts-tags-C-j71FRb.js +1 -0
  388. package/web/dist/assets/tsv-sltzmVWM.js +1 -0
  389. package/web/dist/assets/tsx-z04y7qR9.js +1 -0
  390. package/web/dist/assets/turtle-ByJddavk.js +1 -0
  391. package/web/dist/assets/twig-Bz57G_nl.js +1 -0
  392. package/web/dist/assets/typescript-RMqVTmzR.js +1 -0
  393. package/web/dist/assets/typespec-BGhR2SqG.js +1 -0
  394. package/web/dist/assets/typst-BgkqA2Pg.js +1 -0
  395. package/web/dist/assets/v-DBj4K9d_.js +1 -0
  396. package/web/dist/assets/vala-zf12oZj6.js +1 -0
  397. package/web/dist/assets/vb-DOwzaHZ0.js +1 -0
  398. package/web/dist/assets/verilog-B2gwz-sJ.js +1 -0
  399. package/web/dist/assets/vesper-DdrHHSXu.js +1 -0
  400. package/web/dist/assets/vhdl-BroJfC0k.js +1 -0
  401. package/web/dist/assets/viml-DvXPmvsu.js +1 -0
  402. package/web/dist/assets/vitesse-black-fwtXNY1n.js +1 -0
  403. package/web/dist/assets/vitesse-dark-BZCL-v6S.js +1 -0
  404. package/web/dist/assets/vitesse-light-VbXTXTou.js +1 -0
  405. package/web/dist/assets/vue-CQgvQytb.js +1 -0
  406. package/web/dist/assets/vue-html-Dhl6fh9l.js +1 -0
  407. package/web/dist/assets/vue-vine-Dw_uU59K.js +1 -0
  408. package/web/dist/assets/vyper-CgoNMtux.js +1 -0
  409. package/web/dist/assets/wasm-ByWQv1Qj.js +1 -0
  410. package/web/dist/assets/wenyan-C8pVoKbM.js +1 -0
  411. package/web/dist/assets/wgsl-BsKzXJz4.js +1 -0
  412. package/web/dist/assets/wikitext-ClFFjSW2.js +1 -0
  413. package/web/dist/assets/wit-DdvCle-K.js +1 -0
  414. package/web/dist/assets/wolfram-DLL8P-h_.js +1 -0
  415. package/web/dist/assets/xml-B49klYQ3.js +1 -0
  416. package/web/dist/assets/xsl-D7tnD3qq.js +1 -0
  417. package/web/dist/assets/yaml-DNQ5_xkL.js +1 -0
  418. package/web/dist/assets/zenscript-BnlCZFoB.js +1 -0
  419. package/web/dist/assets/zig-CMLA9XwU.js +1 -0
  420. package/web/dist/icons/apple-touch-icon.png +0 -0
  421. package/web/dist/icons/badge-96.png +0 -0
  422. package/web/dist/icons/icon-192.png +0 -0
  423. package/web/dist/icons/icon-512.png +0 -0
  424. package/web/dist/icons/icon.svg +4 -0
  425. package/web/dist/icons/maskable-192.png +0 -0
  426. package/web/dist/icons/maskable-512.png +0 -0
  427. package/web/dist/index.html +30 -0
  428. package/web/dist/manifest.webmanifest +20 -0
  429. package/web/dist/sw.js +176 -0
  430. package/web/dist/version.json +1 -0
  431. package/web/src/App.tsx +97 -0
  432. package/web/src/agent-history-flow.test.ts +191 -0
  433. package/web/src/agent-history-flow.ts +58 -0
  434. package/web/src/agent-owners.ts +30 -0
  435. package/web/src/annotation-selection.test.ts +36 -0
  436. package/web/src/annotation-selection.ts +123 -0
  437. package/web/src/annotations.test.ts +279 -0
  438. package/web/src/annotations.ts +191 -0
  439. package/web/src/api.ts +27 -0
  440. package/web/src/auth-events.ts +11 -0
  441. package/web/src/auth-lifecycle.test.ts +105 -0
  442. package/web/src/clipboard.ts +32 -0
  443. package/web/src/components/ActivityNotifier.tsx +22 -0
  444. package/web/src/components/AnnotationEditor.tsx +135 -0
  445. package/web/src/components/AnnotationSelection.tsx +122 -0
  446. package/web/src/components/Automation.tsx +390 -0
  447. package/web/src/components/CodeView.tsx +121 -0
  448. package/web/src/components/Composer.tsx +552 -0
  449. package/web/src/components/ComposerAnnotations.tsx +45 -0
  450. package/web/src/components/Confirmation.tsx +91 -0
  451. package/web/src/components/CopyButton.tsx +23 -0
  452. package/web/src/components/DiffView.tsx +94 -0
  453. package/web/src/components/DirectoryPicker.tsx +230 -0
  454. package/web/src/components/FileCopyMenu.tsx +58 -0
  455. package/web/src/components/FileEntryActions.tsx +95 -0
  456. package/web/src/components/ImagePreview.tsx +68 -0
  457. package/web/src/components/Items.tsx +608 -0
  458. package/web/src/components/Markdown.tsx +73 -0
  459. package/web/src/components/MessageList.tsx +458 -0
  460. package/web/src/components/Modal.tsx +72 -0
  461. package/web/src/components/NotificationEntry.tsx +31 -0
  462. package/web/src/components/NotificationSettings.tsx +226 -0
  463. package/web/src/components/NotificationToasts.tsx +59 -0
  464. package/web/src/components/OpenTabs.tsx +112 -0
  465. package/web/src/components/Pending.tsx +175 -0
  466. package/web/src/components/ProjectExtras.tsx +299 -0
  467. package/web/src/components/ProjectNav.tsx +47 -0
  468. package/web/src/components/SessionActions.tsx +271 -0
  469. package/web/src/components/SessionChannels.tsx +288 -0
  470. package/web/src/components/SessionExtras.tsx +263 -0
  471. package/web/src/components/SettingsLink.tsx +21 -0
  472. package/web/src/components/Shell.tsx +320 -0
  473. package/web/src/components/SvgPreview.tsx +18 -0
  474. package/web/src/components/TodoPanel.tsx +37 -0
  475. package/web/src/components/VersionNotice.tsx +40 -0
  476. package/web/src/components/icons.tsx +43 -0
  477. package/web/src/diff.ts +62 -0
  478. package/web/src/drafts.test.ts +80 -0
  479. package/web/src/drafts.ts +64 -0
  480. package/web/src/format.ts +5 -0
  481. package/web/src/highlight.worker.ts +49 -0
  482. package/web/src/hooks.ts +59 -0
  483. package/web/src/item-view-state.test.ts +51 -0
  484. package/web/src/item-view-state.ts +43 -0
  485. package/web/src/live-history-flow.test.ts +77 -0
  486. package/web/src/live-merge.test.ts +64 -0
  487. package/web/src/live-merge.ts +50 -0
  488. package/web/src/live-message-hydration.test.ts +253 -0
  489. package/web/src/live.ts +404 -0
  490. package/web/src/main.tsx +27 -0
  491. package/web/src/markdown-assets.test.ts +70 -0
  492. package/web/src/markdown-assets.ts +78 -0
  493. package/web/src/markdown-html.test.ts +84 -0
  494. package/web/src/markdown-html.ts +60 -0
  495. package/web/src/markdown-svg.test.ts +37 -0
  496. package/web/src/markdown-svg.ts +42 -0
  497. package/web/src/markdown-tables.test.ts +34 -0
  498. package/web/src/markdown-tables.ts +17 -0
  499. package/web/src/message-bodies.ts +117 -0
  500. package/web/src/message-cache.test.ts +208 -0
  501. package/web/src/message-cache.ts +158 -0
  502. package/web/src/message-font-size.test.ts +41 -0
  503. package/web/src/message-font-size.ts +57 -0
  504. package/web/src/message-time.test.ts +32 -0
  505. package/web/src/message-time.ts +26 -0
  506. package/web/src/notify.ts +132 -0
  507. package/web/src/open-tabs.test.ts +69 -0
  508. package/web/src/open-tabs.ts +110 -0
  509. package/web/src/pages/AgentView.tsx +138 -0
  510. package/web/src/pages/ChangesView.tsx +193 -0
  511. package/web/src/pages/FilesView.tsx +406 -0
  512. package/web/src/pages/HomePage.tsx +96 -0
  513. package/web/src/pages/LoginPage.tsx +56 -0
  514. package/web/src/pages/OverviewPage.tsx +124 -0
  515. package/web/src/pages/ProjectHome.tsx +136 -0
  516. package/web/src/pages/SessionView.tsx +238 -0
  517. package/web/src/pages/WorktreesView.tsx +197 -0
  518. package/web/src/pages/settings/AccountPage.tsx +149 -0
  519. package/web/src/pages/settings/AgentsPage.tsx +182 -0
  520. package/web/src/pages/settings/ConfigPage.tsx +181 -0
  521. package/web/src/pages/settings/GeneralPage.tsx +27 -0
  522. package/web/src/pages/settings/HooksPage.tsx +244 -0
  523. package/web/src/pages/settings/McpPage.tsx +310 -0
  524. package/web/src/pages/settings/MemoryPage.tsx +309 -0
  525. package/web/src/pages/settings/PermissionsPage.tsx +160 -0
  526. package/web/src/pages/settings/PluginsPage.tsx +251 -0
  527. package/web/src/pages/settings/SettingsPage.tsx +141 -0
  528. package/web/src/pages/settings/SkillsPage.tsx +142 -0
  529. package/web/src/pages/settings/ui.tsx +165 -0
  530. package/web/src/preference-document.ts +133 -0
  531. package/web/src/project-actions.ts +18 -0
  532. package/web/src/project-files-state.test.ts +49 -0
  533. package/web/src/project-files-state.ts +64 -0
  534. package/web/src/project-images.ts +13 -0
  535. package/web/src/pwa.ts +33 -0
  536. package/web/src/queryClient.ts +5 -0
  537. package/web/src/reasoning-preference.test.ts +39 -0
  538. package/web/src/reasoning-preference.ts +42 -0
  539. package/web/src/session-confirmations.ts +36 -0
  540. package/web/src/stream-replay.test.ts +163 -0
  541. package/web/src/styles.css +231 -0
  542. package/web/src/tool-result-body.test.ts +115 -0
  543. package/web/src/transcript.test.ts +186 -0
  544. package/web/src/transcript.ts +583 -0
  545. package/web/src/turns.test.ts +338 -0
  546. package/web/src/turns.ts +178 -0
  547. package/web/src/ui/Button.tsx +60 -0
  548. package/web/src/ui/Controls.tsx +227 -0
  549. package/web/src/ui/Form.tsx +203 -0
  550. package/web/src/ui/MenuPopover.tsx +144 -0
  551. package/web/src/ui/Popover.tsx +147 -0
  552. package/web/src/ui/ResizablePane.tsx +110 -0
  553. package/web/src/ui/Select.tsx +150 -0
  554. package/web/src/ui/icons.tsx +270 -0
  555. package/web/src/unsaved.tsx +99 -0
  556. package/web/src/user-text.test.ts +60 -0
  557. package/web/src/user-text.ts +35 -0
  558. package/web/src/uuid.ts +12 -0
  559. package/README.md +0 -3
@@ -0,0 +1,39 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { createReasoningPreference, REASONING_EXPANDED_STORAGE_KEY } from "./reasoning-preference.ts";
4
+
5
+ test("reasoning defaults to collapsed and only accepts the saved true value", () => {
6
+ for (const value of [null, "", "false", "1", "{}", "TRUE"]) {
7
+ assert.equal(createReasoningPreference({ getItem: () => value, setItem() {} }).getSnapshot(), false);
8
+ }
9
+ assert.equal(createReasoningPreference({ getItem: () => "true", setItem() {} }).getSnapshot(), true);
10
+ });
11
+
12
+ test("reasoning preference survives recreation and cleans up subscribers", () => {
13
+ const data = new Map<string, string>();
14
+ const storage = { getItem: (key: string) => data.get(key) ?? null, setItem: (key: string, value: string) => { data.set(key, value); } };
15
+ const preference = createReasoningPreference(storage);
16
+ const notifications: boolean[] = [];
17
+ const unsubscribe = preference.subscribe(() => notifications.push(preference.getSnapshot()));
18
+ preference.set(true);
19
+ preference.set(true);
20
+ assert.deepEqual(notifications, [true]);
21
+ assert.equal(data.get(REASONING_EXPANDED_STORAGE_KEY), "true");
22
+ assert.equal(createReasoningPreference(storage).getSnapshot(), true);
23
+ unsubscribe();
24
+ preference.set(false);
25
+ assert.deepEqual(notifications, [true]);
26
+ assert.equal(createReasoningPreference(storage).getSnapshot(), false);
27
+ });
28
+
29
+ test("blocked storage retains the setting in memory and cross-tab changes do not write back", () => {
30
+ let writes = 0;
31
+ const preference = createReasoningPreference({ getItem() { throw new Error("blocked"); }, setItem() { writes++; throw new Error("blocked"); } });
32
+ preference.set(true);
33
+ assert.equal(preference.getSnapshot(), true);
34
+ preference.restore(null);
35
+ assert.equal(preference.getSnapshot(), false);
36
+ preference.restore("true");
37
+ assert.equal(preference.getSnapshot(), true);
38
+ assert.equal(writes, 1);
39
+ });
@@ -0,0 +1,42 @@
1
+ import { useSyncExternalStore } from "react";
2
+
3
+ export const REASONING_EXPANDED_STORAGE_KEY = "claude-ide:reasoning-expanded";
4
+ type PreferenceStorage = Pick<Storage, "getItem" | "setItem">;
5
+
6
+ /** Display preference only; independent of session and transcript lifetimes. */
7
+ export function createReasoningPreference(storage?: PreferenceStorage) {
8
+ let expanded = false;
9
+ try { expanded = storage?.getItem(REASONING_EXPANDED_STORAGE_KEY) === "true"; } catch { /* Keep the collapsed default. */ }
10
+ const listeners = new Set<() => void>();
11
+ const update = (next: boolean) => {
12
+ if (next === expanded) return;
13
+ expanded = next;
14
+ for (const listener of listeners) listener();
15
+ };
16
+ return {
17
+ getSnapshot: () => expanded,
18
+ subscribe(listener: () => void) {
19
+ listeners.add(listener);
20
+ return () => { listeners.delete(listener); };
21
+ },
22
+ set(next: boolean) {
23
+ try { storage?.setItem(REASONING_EXPANDED_STORAGE_KEY, String(next)); } catch { /* Keep a working in-memory setting. */ }
24
+ update(next);
25
+ },
26
+ restore(value: unknown) { update(value === "true"); },
27
+ };
28
+ }
29
+
30
+ let storage: PreferenceStorage | undefined;
31
+ try { storage = typeof localStorage === "undefined" ? undefined : localStorage; } catch { /* Storage may be disabled. */ }
32
+ const preference = createReasoningPreference(storage);
33
+ if (typeof window !== "undefined") window.addEventListener("storage", (event) => {
34
+ if (event.storageArea && event.storageArea !== storage) return;
35
+ if (event.key === REASONING_EXPANDED_STORAGE_KEY || event.key === null) preference.restore(event.newValue);
36
+ });
37
+
38
+ export const getReasoningExpanded = preference.getSnapshot;
39
+ export const setReasoningExpanded = preference.set;
40
+ export function useReasoningExpanded(): boolean {
41
+ return useSyncExternalStore(preference.subscribe, preference.getSnapshot, () => false);
42
+ }
@@ -0,0 +1,36 @@
1
+ import { useConfirm } from "./components/Confirmation.tsx";
2
+ import type { ImageInput } from "@shared/protocol.ts";
3
+ import { sendClient } from "./live.ts";
4
+ import { uuid } from "./uuid.ts";
5
+
6
+ /** Both the context popover and /compact use the same confirmation and dispatch. */
7
+ export function useCompactSession(projectId: string, sessionId: string) {
8
+ const confirm = useConfirm();
9
+ return async (instructions = "", images?: ImageInput[]) => {
10
+ if (!(await confirm({
11
+ title: "压缩上下文?",
12
+ description: `后续推理将使用压缩摘要,部分讨论细节可能不再包含在上下文中。历史对话仍保留。${instructions.trim() ? `\n\n压缩说明:${instructions.trim()}` : ""}`,
13
+ confirmLabel: "确认压缩",
14
+ })).confirmed) return false;
15
+ sendClient({
16
+ t: "send", projectId, sessionId, uuid: uuid(),
17
+ text: `/compact${instructions.trim() ? ` ${instructions.trim()}` : ""}`,
18
+ ...(images?.length ? { images } : {}),
19
+ });
20
+ return true;
21
+ };
22
+ }
23
+
24
+ export function useClearGoal(projectId: string, sessionId: string) {
25
+ const confirm = useConfirm();
26
+ return async (condition?: string) => {
27
+ if (!(await confirm({
28
+ title: "清除当前目标?",
29
+ description: `${condition ? `当前目标:${condition}。` : ""}将取消持续推进这个目标,已经完成的操作不会撤销。`,
30
+ confirmLabel: "清除目标",
31
+ danger: true,
32
+ })).confirmed) return false;
33
+ sendClient({ t: "send", projectId, sessionId, uuid: uuid(), text: "/goal clear" });
34
+ return true;
35
+ };
36
+ }
@@ -0,0 +1,163 @@
1
+ import { test } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import type { LiveEvent, SdkMessageLike } from "@shared/protocol.ts";
4
+ import { StreamingReplay, boundedReplay, compactReplayEvents, compactStreamEvents, contentBytes, displayEvent, displayMessage, SESSION_BYTES } from "@shared/message-window.ts";
5
+ import { Transcript } from "./transcript.ts";
6
+ import { groupTurns } from "./turns.ts";
7
+ import { ItemViewState } from "./item-view-state.ts";
8
+
9
+ const delta = (text: string, index = 0): LiveEvent => ({ k: "delta", msgId: "m", index, kind: "text", text, parent: null });
10
+ const assistant = (uuid: string, text: string): LiveEvent => ({ k: "sdk", m: { type: "assistant", uuid, message: { id: "m", content: [{ type: "text", text }] } } });
11
+
12
+ test("CACHE-002 replay restores the complete active prefix after both ring and browser count cuts", () => {
13
+ const streaming = new StreamingReplay();
14
+ const events = Array.from({ length: 6100 }, (_, i) => ({ seq: i + 1, ev: delta(`${i},`) }));
15
+ for (const event of events) streaming.push(event);
16
+ const replay = boundedReplay(events.slice(-5000), streaming.checkpoints);
17
+ const expected = events.map(({ ev }) => (ev as Extract<LiveEvent, { k: "delta" }>).text).join("");
18
+ assert.equal(replay.length, 1);
19
+ const transcript = new Transcript();
20
+ const pending = transcript.update(undefined, replay.map(({ ev }) => ev));
21
+ assert.equal((pending[0] as { text: string }).text, expected);
22
+ const final = transcript.update(undefined, [...replay.map(({ ev }) => ev), assistant("a", expected)]);
23
+ assert.equal(final.length, 1);
24
+ assert.equal(final.some((item) => "streaming" in item && item.streaming), false);
25
+ assert.equal((final[0] as { text: string }).text, expected);
26
+ const compact = compactStreamEvents(events.map(({ ev }) => ev));
27
+ assert.equal((compact[0] as { text: string }).text, expected);
28
+ assert.deepEqual(compactStreamEvents(compact), compact);
29
+ assert.deepEqual(compactStreamEvents([...compact, delta("next")]), compactStreamEvents([...events.map(({ ev }) => ev), delta("next")]));
30
+ });
31
+
32
+ test("CACHE-002 replayed completion UUID cannot settle a later block with identical text", () => {
33
+ const events = [delta("same"), assistant("first", "same"), delta("same", 1), assistant("first", "same")];
34
+ const compact = compactStreamEvents(events);
35
+ const drafts = compact.filter((ev) => ev.k === "delta");
36
+ assert.equal(drafts.length, 1);
37
+ assert.equal((drafts[0] as Extract<LiveEvent, { k: "delta" }>).index, 1);
38
+ const items = new Transcript().update(undefined, compact);
39
+ assert.equal(items.filter((item) => "streaming" in item && item.streaming).length, 1);
40
+ });
41
+
42
+ test("CACHE-002 checkpoint sequence prevents completed blocks from reviving and preserves the next active block", () => {
43
+ const checkpoint = { seq: 1, ev: { ...delta("same"), checkpoint: true } as LiveEvent };
44
+ const first = { seq: 2, ev: assistant("first", "same") };
45
+ assert.equal(compactReplayEvents([first], [checkpoint]).some(({ ev }) => ev.k === "delta"), false);
46
+ const next = { seq: 3, ev: { ...delta("same", 1), checkpoint: true } as LiveEvent };
47
+ const replay = compactReplayEvents([first], [next]);
48
+ assert.equal(replay.filter(({ ev }) => ev.k === "delta").length, 1);
49
+ });
50
+
51
+ test("CACHE-002 completion identities survive compaction and reject late fragments", () => {
52
+ const compact = compactStreamEvents([delta("done"), assistant("first", "done")]);
53
+ const late = compactStreamEvents([...compact, delta("late")]);
54
+ assert.equal(late.some((ev) => ev.k === "delta"), false);
55
+ const items = new Transcript().update(undefined, [...compact, delta("late")]);
56
+ assert.equal(items.some((item) => "streaming" in item && item.streaming), false);
57
+ });
58
+
59
+ test("thinking fold identity survives partial stream settlement and repeated replay compaction", () => {
60
+ const state = new ItemViewState();
61
+ const thinking: LiveEvent = { k: "delta", msgId: "api", index: 7, kind: "thinking", text: "thought", parent: "agent" };
62
+ const before = new Transcript({ parent: "agent" }).update(undefined, [thinking])[0]!;
63
+ const choice = state.write(before.key, false);
64
+ // Text and tool blocks need not have been streamed. The settlement list is a
65
+ // subset, so the block-to-stream association must survive beside that list.
66
+ const complete: LiveEvent = { k: "sdk", m: { type: "assistant", uuid: "frame", parent_tool_use_id: "agent", message: { id: "api", content: [
67
+ { type: "text", text: "unstreamed text" }, { type: "thinking", thinking: "thought" },
68
+ { type: "tool_use", id: "read", name: "Read", input: {} },
69
+ ] } } };
70
+ const compacted = compactStreamEvents([thinking, complete]);
71
+ assert.equal(compacted.length, 1);
72
+ assert.deepEqual(compacted[0]?.k === "sdk" && compacted[0].m._streamThinking, { 1: 7 });
73
+ assert.deepEqual(compactStreamEvents(compacted), compacted);
74
+ const displayed = compacted.map((event) => event.k === "sdk" ? { ...event, m: displayMessage(event.m, 500_000) } : event);
75
+ const items = new Transcript({ parent: "agent" }).update(undefined, displayed);
76
+ const after = items.find((item) => item.kind === "thinking")!;
77
+ assert.equal(after.key, "frame:1");
78
+ assert.equal(after.streamStateKey, before.key);
79
+ assert.equal(state.read(after.key, true, true, [after.streamStateKey!]), choice);
80
+ });
81
+
82
+ test("CACHE-002 cumulative checkpoint replaces an already displayed prefix without doubling it", () => {
83
+ const transcript = new Transcript();
84
+ transcript.update(undefined, [delta("hello")]);
85
+ const events = [delta("hello"), { ...delta("hello world"), checkpoint: true } as LiveEvent];
86
+ assert.equal((transcript.update(undefined, events)[0] as { text: string }).text, "hello world");
87
+ });
88
+
89
+ test("CACHE-002 oversized unfinished streams stay bounded and report preview truncation", () => {
90
+ const streaming = new StreamingReplay();
91
+ for (let i = 0; i < 20; i++) streaming.push({ seq: i, ev: delta("x".repeat(1024 * 1024)) });
92
+ assert.ok(contentBytes(streaming.checkpoints) < SESSION_BYTES / 2);
93
+ const checkpoint = streaming.checkpoints[0]!.ev as Extract<LiveEvent, { k: "delta" }>;
94
+ assert.equal(checkpoint.truncated, true);
95
+ assert.ok(checkpoint.text.startsWith("x"));
96
+ const items = new Transcript().update(undefined, streaming.checkpoints.map(({ ev }) => ev));
97
+ assert.match((items[0] as { text: string }).text, /流式预览已截断/);
98
+ });
99
+
100
+ test("CACHE-003 oversized result retains owner, failure and turn accounting", () => {
101
+ const message = displayMessage({ type: "result", uuid: "r", subtype: "error_during_execution", is_error: true,
102
+ user_message_uuids: ["u1"], total_cost_usd: 2, turn_cost_usd: 0.75, duration_ms: 4321, duration_api_ms: 1234,
103
+ errors: ["request failed"], result: "x".repeat(500_000) });
104
+ assert.deepEqual(message.user_message_uuids, ["u1"]);
105
+ assert.equal(message.total_cost_usd, 2);
106
+ assert.equal(message.duration_api_ms, 1234);
107
+ const history: SdkMessageLike[] = [{ type: "user", uuid: "u1", message: { content: "first" } }];
108
+ const items = new Transcript().update(history, [assistant("a", "answer"), { k: "user", uuid: "u2", text: "queued", ts: 1 }, { k: "sdk", m: message }]);
109
+ const turns = groupTurns(items, true);
110
+ assert.equal(turns[0]?.result?.ok, false);
111
+ assert.equal(turns[0]?.result?.turnCostUsd, 0.75);
112
+ assert.equal(turns[0]?.result?.durationMs, 4321);
113
+ assert.equal(turns[0]?.result?.error, "request failed");
114
+ assert.equal(turns[1]?.result, undefined);
115
+ assert.equal(turns[1]?.active, true);
116
+ });
117
+
118
+ test("CACHE-003 assistant error and task control fields survive payload abbreviation", () => {
119
+ const failed = displayMessage({ type: "assistant", uuid: "a", error: "authentication_failed", aborted: true,
120
+ message: { content: [{ type: "text", text: "failure ".repeat(100_000) }] } });
121
+ assert.equal(failed.aborted, true);
122
+ assert.equal(new Transcript().update([failed], [])[0]?.kind, "notice");
123
+ const task = displayMessage({ type: "system", subtype: "task_notification", task_id: "t", tool_use_id: "tu", run_id: "run",
124
+ status: "failed", usage: { duration_ms: 12 }, summary: "long ".repeat(100_000) });
125
+ assert.equal(task.task_id, "t");
126
+ assert.equal(task.status, "failed");
127
+ assert.equal(task.tool_use_id, "tu");
128
+ assert.deepEqual(task.usage, { duration_ms: 12 });
129
+ });
130
+
131
+
132
+ test("CACHE-017 oversized real shell completion settles same UUID and exposes output plus hydration", async () => {
133
+ const { runBang, bangMessage } = await import("../../server/bang.ts");
134
+ const command = "printf '%30000s' ''; printf '%30000s' '' >&2 # " + "c".repeat(90000);
135
+ const result = await runBang(command, process.cwd());
136
+ assert.equal(result.code, 0);
137
+ assert.equal(result.stdout.length, 30000);
138
+ assert.equal(result.stderr.length, 30000);
139
+ const full = { type: "user", uuid: "shell", message: { content: bangMessage(command, result) } };
140
+ const completion = displayEvent({ k: "user", uuid: "shell", text: full.message.content, ts: 2 });
141
+ assert.equal(completion.k, "sdk");
142
+ assert.ok(contentBytes(completion) < 30000);
143
+ const transcript = new Transcript();
144
+ const start: LiveEvent = { k: "bash", uuid: "shell", command, ts: 1 };
145
+ const events = [start, completion];
146
+ const items = transcript.update(undefined, events);
147
+ assert.equal(items.length, 1);
148
+ const shell = items[0]!;
149
+ assert.equal(shell.kind, "bash");
150
+ if (shell.kind !== "bash") return;
151
+ assert.equal(shell.running, false);
152
+ assert.equal(shell.stdout?.length, 2000);
153
+ assert.equal(shell.stderr?.length, 2000);
154
+ assert.equal(shell.deferred?.uuid, "shell");
155
+ assert.deepEqual(new Transcript().update(undefined, [...events, completion]), items);
156
+ const hydrated = new Transcript().update([{ ...full, _hydrated: true }], events);
157
+ const expanded = hydrated.find((item) => item.kind === "bash");
158
+ assert.equal(expanded?.kind === "bash" && expanded.stdout?.length, 30000);
159
+ assert.equal(expanded?.kind === "bash" && expanded.command.length, command.length);
160
+ const historyOnly = new Transcript().update([displayMessage(full)], []);
161
+ assert.equal(historyOnly[0]?.kind === "bash" && historyOnly[0].running, false);
162
+ assert.equal(historyOnly[0]?.deferred?.uuid, "shell");
163
+ });
@@ -0,0 +1,231 @@
1
+ @import "tailwindcss";
2
+
3
+ /* Design tokens: Claude desktop look — warm neutrals, clay accent, serif headings. */
4
+ :root {
5
+ --bg: #faf9f5;
6
+ --surface: #ffffff;
7
+ --surface-2: #f0eee6;
8
+ --border: #e5e2d9;
9
+ --text: #141413;
10
+ --text-muted: #6b6a65;
11
+ --accent: #c96442;
12
+ --accent-hover: #b4573a;
13
+ --accent-text: #ffffff;
14
+ --danger: #b3261e;
15
+ --ok: #3d7a4a;
16
+ color-scheme: light;
17
+ }
18
+
19
+ @media (prefers-color-scheme: dark) {
20
+ :root:not([data-theme="light"]) {
21
+ --bg: #1f1e1d;
22
+ --surface: #262624;
23
+ --surface-2: #30302e;
24
+ --border: #3d3d3a;
25
+ --text: #f5f4ed;
26
+ --text-muted: #a6a39b;
27
+ --accent: #d97757;
28
+ --accent-hover: #e08a6d;
29
+ --accent-text: #1f1e1d;
30
+ --danger: #f2b8b5;
31
+ --ok: #8fc79a;
32
+ color-scheme: dark;
33
+ }
34
+ }
35
+
36
+ :root[data-theme="dark"] {
37
+ --bg: #1f1e1d;
38
+ --surface: #262624;
39
+ --surface-2: #30302e;
40
+ --border: #3d3d3a;
41
+ --text: #f5f4ed;
42
+ --text-muted: #a6a39b;
43
+ --accent: #d97757;
44
+ --accent-hover: #e08a6d;
45
+ --accent-text: #1f1e1d;
46
+ --danger: #f2b8b5;
47
+ --ok: #8fc79a;
48
+ color-scheme: dark;
49
+ }
50
+
51
+ @theme inline {
52
+ --color-bg: var(--bg);
53
+ --color-surface: var(--surface);
54
+ --color-surface-2: var(--surface-2);
55
+ --color-border: var(--border);
56
+ --color-text: var(--text);
57
+ --color-muted: var(--text-muted);
58
+ --color-accent: var(--accent);
59
+ --color-accent-hover: var(--accent-hover);
60
+ --color-accent-text: var(--accent-text);
61
+ --color-danger: var(--danger);
62
+ --color-ok: var(--ok);
63
+ --font-serif: "Tiempos Headline", ui-serif, Georgia, "Noto Serif SC", "Songti SC", serif;
64
+ --font-sans: ui-sans-serif, system-ui, -apple-system, "Segoe UI", "PingFang SC", "Noto Sans SC", sans-serif;
65
+ --font-mono: ui-monospace, "SF Mono", "JetBrains Mono", Menlo, monospace;
66
+ }
67
+
68
+ html, body, #root {
69
+ height: 100%;
70
+ }
71
+
72
+ body {
73
+ background: var(--bg);
74
+ color: var(--text);
75
+ font-family: var(--font-sans);
76
+ -webkit-font-smoothing: antialiased;
77
+ -webkit-tap-highlight-color: transparent;
78
+ }
79
+
80
+ h1, h2, h3 {
81
+ font-family: var(--font-serif);
82
+ font-weight: 500;
83
+ letter-spacing: -0.01em;
84
+ }
85
+
86
+ /* Markdown body */
87
+ .prose-claude {
88
+ line-height: 1.65;
89
+ overflow-wrap: anywhere;
90
+ }
91
+ .prose-claude > * + * { margin-top: 0.75em; }
92
+ .prose-claude h1, .prose-claude h2, .prose-claude h3 { margin-top: 1.2em; }
93
+ .prose-claude h1 { font-size: 1.4em; }
94
+ .prose-claude h2 { font-size: 1.25em; }
95
+ .prose-claude h3 { font-size: 1.1em; }
96
+ .prose-claude ul { list-style: disc; padding-left: 1.4em; }
97
+ .prose-claude ol { list-style: decimal; padding-left: 1.4em; }
98
+ .prose-claude li + li { margin-top: 0.25em; }
99
+ .prose-claude a { color: var(--accent); text-decoration: underline; text-underline-offset: 2px; }
100
+ .prose-claude code { font-family: var(--font-mono); font-size: 0.88em; background: var(--surface-2); padding: 0.1em 0.35em; border-radius: 4px; }
101
+ .prose-claude pre { background: var(--surface-2); border: 1px solid var(--border); border-radius: 8px; padding: 0.75em 1em; overflow-x: auto; font-size: 0.85em; line-height: 1.5; }
102
+ .prose-claude pre code { background: none; padding: 0; font-size: inherit; }
103
+ .prose-claude blockquote { border-left: 3px solid var(--border); padding-left: 1em; color: var(--text-muted); }
104
+ .prose-claude table { border-collapse: collapse; font-size: 0.92em; }
105
+ .prose-claude th, .prose-claude td { border: 1px solid var(--border); padding: 0.35em 0.7em; text-align: left; }
106
+ .prose-claude hr { border-color: var(--border); }
107
+ .markdown-copy-block { position: relative; min-width: 0; }
108
+ .markdown-copy-button { position: absolute; z-index: 1; top: 0.25rem; right: 0.375rem; }
109
+ .prose-claude .markdown-code-block pre { padding-top: 2.75rem; }
110
+ .markdown-table-block { padding-top: 2.5rem; }
111
+ @media (hover: hover) and (pointer: fine) {
112
+ .markdown-copy-button { opacity: 0; }
113
+ .markdown-copy-block:hover > .markdown-copy-button,
114
+ .markdown-copy-block:focus-within > .markdown-copy-button,
115
+ .markdown-copy-button[data-copied] { opacity: 1; }
116
+ }
117
+
118
+ /* Local type scale for transcripts only. Large preserves the existing sizes;
119
+ dense output keeps a 12px floor so commands and diffs remain readable. */
120
+ .message-transcript {
121
+ --message-body-size: 1rem;
122
+ --text-sm: 0.875rem;
123
+ --text-xs: 0.75rem;
124
+ font-size: var(--message-body-size);
125
+ }
126
+ .message-transcript[data-message-font-size="medium"] {
127
+ --message-body-size: 0.9375rem;
128
+ --text-sm: 0.84375rem;
129
+ }
130
+ .message-transcript[data-message-font-size="small"] {
131
+ --message-body-size: 0.875rem;
132
+ --text-sm: 0.8125rem;
133
+ }
134
+ .message-transcript .prose-claude code { font-size: max(0.75rem, 0.88em); }
135
+ .message-transcript .prose-claude pre { font-size: max(0.75rem, 0.85em); }
136
+ .message-transcript .prose-claude pre code { font-size: inherit; }
137
+
138
+ .app-height { height: 100dvh; }
139
+ /* Shared scrollbars cover page content and portals; utilities can still hide them. */
140
+ @layer base {
141
+ :where(*) {
142
+ --scrollbar-thumb: color-mix(in srgb, var(--text-muted) 30%, transparent);
143
+ scrollbar-width: thin;
144
+ scrollbar-color: var(--scrollbar-thumb) transparent;
145
+ }
146
+ :where(*:hover, *:focus-within) {
147
+ --scrollbar-thumb: color-mix(in srgb, var(--text-muted) 50%, transparent);
148
+ }
149
+ @supports selector(::-webkit-scrollbar) {
150
+ /* Chromium's standard scrollbar properties otherwise override pixel sizing. */
151
+ :where(*) { scrollbar-width: auto; scrollbar-color: auto; }
152
+ :where(*)::-webkit-scrollbar { width: 8px; height: 8px; }
153
+ :where(*)::-webkit-scrollbar-track,
154
+ :where(*)::-webkit-scrollbar-corner { background: transparent; }
155
+ :where(*)::-webkit-scrollbar-thumb {
156
+ background: var(--scrollbar-thumb);
157
+ border-radius: 999px;
158
+ }
159
+ :where(*)::-webkit-scrollbar-button { display: none; }
160
+ }
161
+ }
162
+ @media (pointer: fine) {
163
+ .sidebar-session-list { scrollbar-gutter: stable; }
164
+ }
165
+ @media (min-width: 768px) {
166
+ .desktop-resizable-pane {
167
+ width: var(--pane-width);
168
+ min-width: var(--pane-min);
169
+ max-width: min(var(--pane-max), max(var(--pane-min), calc(100% - var(--pane-remaining))));
170
+ }
171
+ }
172
+ /* Floating panels leave a visible gap as well as room for notches and the home indicator. */
173
+ .safe-overlay {
174
+ --overlay-gap: 0.75rem;
175
+ --overlay-top: max(var(--overlay-gap), env(safe-area-inset-top, 0px));
176
+ --overlay-bottom: max(var(--overlay-gap), env(safe-area-inset-bottom, 0px));
177
+ padding: var(--overlay-top) max(var(--overlay-gap), env(safe-area-inset-right, 0px)) var(--overlay-bottom) max(var(--overlay-gap), env(safe-area-inset-left, 0px));
178
+ }
179
+ .safe-overlay-panel { max-height: calc(100dvh - var(--overlay-top) - var(--overlay-bottom)); }
180
+ .goal-dialog { height: min(40rem, calc(100dvh - var(--overlay-top) - var(--overlay-bottom))); }
181
+ .safe-sheet-panel { max-height: min(70dvh, calc(100dvh - var(--overlay-top) - var(--overlay-bottom))); }
182
+ @media (min-width: 640px) { .safe-overlay { --overlay-gap: 1rem; } }
183
+ /* On very short screens or with the keyboard open, keep all picker controls reachable. */
184
+ @media (max-height: 480px) {
185
+ .directory-picker-body { overflow-y: auto; overscroll-behavior: contain; }
186
+ .directory-picker-list { flex: none; height: 6rem; }
187
+ }
188
+ @supports (-webkit-touch-callout: none) {
189
+ .app-height { height: -webkit-fill-available; height: 100dvh; }
190
+ }
191
+
192
+ /* Code viewer (files page): line numbers via counters, shiki dual-theme colors. */
193
+ .code-view {
194
+ font-family: var(--font-mono);
195
+ font-size: 12.5px;
196
+ line-height: 1.6;
197
+ padding: 0.5rem 0;
198
+ width: max-content;
199
+ min-width: 100%;
200
+ tab-size: 4;
201
+ }
202
+ .code-view code { counter-reset: line; display: block; }
203
+ .code-view .line { display: block; min-height: 1.6em; padding-right: 1rem; white-space: pre; }
204
+ .code-view .line::before {
205
+ counter-increment: line;
206
+ content: counter(line);
207
+ display: inline-block;
208
+ width: 3.5em;
209
+ margin-right: 1em;
210
+ padding-right: 0.5em;
211
+ text-align: right;
212
+ color: var(--text-muted);
213
+ opacity: 0.7;
214
+ user-select: none;
215
+ }
216
+ .code-view .line-target { background: color-mix(in srgb, var(--accent) 18%, transparent); }
217
+ .code-view .line span { color: var(--shiki-light); }
218
+ @media (prefers-color-scheme: dark) {
219
+ :root:not([data-theme="light"]) .code-view .line span { color: var(--shiki-dark); }
220
+ }
221
+ :root[data-theme="dark"] .code-view .line span { color: var(--shiki-dark); }
222
+
223
+ .diff-table .diff-ln {
224
+ width: 1%;
225
+ min-width: 2.5em;
226
+ padding: 0 0.5em;
227
+ text-align: right;
228
+ color: var(--text-muted);
229
+ user-select: none;
230
+ white-space: nowrap;
231
+ }
@@ -0,0 +1,115 @@
1
+ import { test } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import type { SdkMessageLike } from "@shared/protocol.ts";
4
+ import { SESSION_BYTES } from "@shared/message-window.ts";
5
+ import { boundSession, sessionWeight } from "./message-cache.ts";
6
+ import { hydrateMessageBody, preserveHydratedBodies } from "./message-bodies.ts";
7
+ import { buildItems, type ToolResult } from "./transcript.ts";
8
+
9
+ const result = (uuid: string, entries: [string, string][], deferred = false): SdkMessageLike => ({
10
+ type: "user", uuid, message: { content: entries.map(([tool_use_id, content]) => ({ type: "tool_result", tool_use_id, content })) },
11
+ tool_use_result: { details: uuid }, ...(deferred ? { _deferred: { bytes: 500000 } } : {}),
12
+ });
13
+ const owner = (links: Record<string, ToolResult>): SdkMessageLike => ({
14
+ type: "assistant", uuid: "owner", message: { content: Object.keys(links).map((id) => ({ type: "tool_use", id, name: "Read", input: {} })) },
15
+ linkedToolResults: links,
16
+ });
17
+ const preview = (uuid: string): ToolResult => ({ content: "preview", isError: false, deferredUuid: uuid });
18
+ const tools = (messages: SdkMessageLike[]) => buildItems(messages, []).filter((item) => item.kind === "tool");
19
+
20
+ test("loaded results follow their own UUID between linked and standalone forms after the owner leaves the window", () => {
21
+ const linked = hydrateMessageBody(owner({ first: preview("r1") }), result("r1", [["first", "complete"]]), "r1");
22
+ const incoming = result("r1", [["first", "preview"]], true);
23
+ const moved = preserveHydratedBodies([incoming], [linked]);
24
+ assert.equal(moved[0]!._deferred, incoming._deferred, "unread text/image body must remain deferred");
25
+ assert.deepEqual((moved[0]!.linkedToolResults as Record<string, ToolResult>).first,
26
+ { content: "complete", isError: false, structured: { details: "r1" } });
27
+ assert.equal(incoming.linkedToolResults, undefined, "frozen page records must not be mutated");
28
+ const back = preserveHydratedBodies([owner({ first: preview("r1") })], moved);
29
+ assert.equal(tools(back)[0]!.result?.content, "complete");
30
+ assert.equal(tools([back[0]!, ...moved])[0]!.result?.content, "complete");
31
+ });
32
+
33
+ test("several hydrated result messages preserve the exact tool ID and source UUID pairing", () => {
34
+ let linked = owner({ first: preview("r1"), second: preview("r2"), third: preview("r2") });
35
+ linked = hydrateMessageBody(linked, result("r1", [["first", "full one"]]), "r1");
36
+ linked = hydrateMessageBody(linked, result("r2", [["second", "full two"], ["third", "full three"]]), "r2");
37
+ const records = preserveHydratedBodies([
38
+ owner({ first: preview("r1"), second: preview("r2"), third: preview("r2") }),
39
+ result("r1", [["first", "short one"]], true), result("r2", [["second", "short two"], ["third", "short three"]], true),
40
+ ], [linked]);
41
+ assert.deepEqual(tools(records).map((item) => item.result?.content), ["full one", "full two", "full three"]);
42
+ assert.deepEqual(records[0]!._hydratedToolResultUuids, { first: "r1", second: "r2", third: "r2" });
43
+ const wrongIdentity = preserveHydratedBodies([result("r1", [["second", "different"]], true)], [linked]);
44
+ assert.equal((wrongIdentity[0]!.linkedToolResults as Record<string, ToolResult>).second, undefined, "owner-level alias membership alone cannot identify a different tool result");
45
+ assert.equal((wrongIdentity[0]!.linkedToolResults as Record<string, ToolResult>).first?.content, "full one", "known results survive when their blocks are absent from the preview");
46
+ });
47
+
48
+ test("a partially retained tool-result record never promotes its other preview blocks to complete", () => {
49
+ const loaded = hydrateMessageBody(owner({ first: preview("r1") }), result("r1", [["first", "complete"]]), "r1");
50
+ const incoming = result("r1", [["first", "short"], ["second", "unloaded preview"]], true);
51
+ const moved = preserveHydratedBodies([incoming], [loaded]);
52
+ const calls = owner({ first: preview("r1"), second: preview("r1") });
53
+ const items = tools(preserveHydratedBodies([calls, ...moved], moved));
54
+ assert.equal(items[0]!.result?.content, "complete");
55
+ assert.equal(items[0]!.result?.deferredUuid, undefined);
56
+ assert.equal(items[1]!.result?.content, "unloaded preview");
57
+ assert.equal(items[1]!.result?.deferredUuid, "r1");
58
+ });
59
+
60
+ test("a standalone full hydration upgrades linked previews without retaining its original record", () => {
61
+ const full = { ...result("r1", [["first", "complete"]]), _hydrated: true };
62
+ const records = preserveHydratedBodies([owner({ first: preview("r1") })], [full]);
63
+ assert.equal(tools(records)[0]!.result?.content, "complete");
64
+ });
65
+
66
+ test("a different result UUID or an incoming complete result is not overwritten by an older hydrated result", () => {
67
+ const linked = hydrateMessageBody(owner({ first: preview("r1") }), result("r1", [["first", "old full"]]), "r1");
68
+ const newResult = preserveHydratedBodies([owner({ first: preview("r2") }), result("r2", [["first", "new preview"]], true)], [linked]);
69
+ assert.equal(tools(newResult)[0]!.result?.content, "new preview");
70
+ assert.equal(tools(newResult)[0]!.result?.deferredUuid, "r2");
71
+ const complete = preserveHydratedBodies([owner({ first: { content: "incoming complete", isError: true } })], [linked]);
72
+ assert.deepEqual(tools(complete)[0]!.result, { content: "incoming complete", isError: true });
73
+ const freshView = [owner({ first: preview("r1") }), result("r1", [["first", "fresh view"]], true)];
74
+ assert.equal(tools(preserveHydratedBodies(freshView, []))[0]!.result?.content, "fresh view");
75
+ });
76
+
77
+ test("projection prefers full content only for the same source result, independent of representation order", () => {
78
+ const linked = hydrateMessageBody(owner({ first: preview("r1") }), result("r1", [["first", "complete"]]), "r1");
79
+ const deferred = result("r1", [["first", "preview"]], true);
80
+ for (const messages of [[linked, deferred], [deferred, linked]]) assert.equal(tools(messages)[0]!.result?.content, "complete");
81
+ const newer = result("r2", [["first", "new result"]], true);
82
+ assert.equal(tools([linked, newer])[0]!.result?.content, "new result");
83
+ });
84
+
85
+ test("ordinary complete pages retain hydration provenance through a later deferred page", () => {
86
+ const loaded = hydrateMessageBody(owner({ first: preview("r1") }), result("r1", [["first", "complete"]]), "r1");
87
+ const ordinaryOwner = { ...owner({ first: { content: "complete", isError: false } }), _toolResultUuids: { first: "r1" } };
88
+ const second = preserveHydratedBodies([ordinaryOwner], [loaded]);
89
+ const third = preserveHydratedBodies([owner({ first: preview("r1") }), result("r1", [["first", "short"]], true)], second);
90
+ assert.equal(tools(third)[0]!.result?.content, "complete");
91
+ const standalone = preserveHydratedBodies([result("r1", [["first", "complete"]])], [loaded]);
92
+ const back = preserveHydratedBodies([owner({ first: preview("r1") })], standalone);
93
+ assert.equal(tools(back)[0]!.result?.content, "complete");
94
+ const genuinelyNew = { ...owner({ first: { content: "new complete", isError: false } }), _toolResultUuids: { first: "r2" } };
95
+ const fresh = preserveHydratedBodies([genuinelyNew], [loaded]);
96
+ assert.equal(tools(fresh)[0]!.result?.content, "new complete");
97
+ assert.equal(fresh[0]!._hydratedToolResultUuids, undefined);
98
+ assert.equal(tools(preserveHydratedBodies([owner({ first: preview("r1") })], fresh))[0]!.result?.content, "preview");
99
+ });
100
+
101
+
102
+ test("migrated result bodies count toward the normal budget and survive trimming their original owner", () => {
103
+ const text = "x".repeat(SESSION_BYTES / 5);
104
+ const loaded = hydrateMessageBody(owner({ first: preview("r1") }), result("r1", [["first", text]]), "r1");
105
+ const records = preserveHydratedBodies([owner({ first: preview("r1") }),
106
+ { type: "user", uuid: "padding", message: { content: "p".repeat(SESSION_BYTES / 7) } },
107
+ result("r1", [["first", "preview"]], true),
108
+ ], [loaded]);
109
+ const bounded = boundSession({ seq: 0, resetCount: 0, events: [], snapshot: { state: "idle", pending: [], planMode: false, crons: [], tasks: [] },
110
+ history: { source: "main", mode: "latest", generation: "g", request: 1, items: records } });
111
+ assert.ok(sessionWeight(bounded) <= SESSION_BYTES);
112
+ assert.equal(bounded.history!.items.some((m) => m.uuid === "owner"), false);
113
+ const returned = preserveHydratedBodies([owner({ first: preview("r1") })], bounded.history!.items);
114
+ assert.equal(tools(returned)[0]!.result?.content, text);
115
+ });