@rexvane/piweb 0.3.25 → 0.3.26

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 (345) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/app-path-routes-manifest.json +6 -5
  3. package/.next/build-manifest.json +4 -4
  4. package/.next/next-server.js.nft.json +1 -1
  5. package/.next/prerender-manifest.json +4 -4
  6. package/.next/react-loadable-manifest.json +14 -26
  7. package/.next/required-server-files.js +26 -26
  8. package/.next/required-server-files.json +26 -26
  9. package/.next/routes-manifest.json +6 -0
  10. package/.next/server/app/_global-error/page.js +2 -2
  11. package/.next/server/app/_global-error/page.js.nft.json +1 -1
  12. package/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  13. package/.next/server/app/_global-error.html +1 -1
  14. package/.next/server/app/_global-error.rsc +1 -1
  15. package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  16. package/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  17. package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  18. package/.next/server/app/_not-found/page.js +1 -1
  19. package/.next/server/app/_not-found/page.js.nft.json +1 -1
  20. package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  21. package/.next/server/app/_not-found.html +1 -1
  22. package/.next/server/app/_not-found.rsc +2 -2
  23. package/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
  24. package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
  25. package/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  26. package/.next/server/app/api/agent/[id]/events/route.js +2 -2
  27. package/.next/server/app/api/agent/[id]/events/route.js.nft.json +1 -1
  28. package/.next/server/app/api/agent/[id]/events/route_client-reference-manifest.js +1 -1
  29. package/.next/server/app/api/agent/[id]/route.js +1 -1
  30. package/.next/server/app/api/agent/[id]/route.js.nft.json +1 -1
  31. package/.next/server/app/api/agent/[id]/route_client-reference-manifest.js +1 -1
  32. package/.next/server/app/api/agent/[id]/tree/route.js +1 -1
  33. package/.next/server/app/api/agent/[id]/tree/route.js.nft.json +1 -1
  34. package/.next/server/app/api/agent/[id]/tree/route_client-reference-manifest.js +1 -1
  35. package/.next/server/app/api/agent/new/route.js +1 -1
  36. package/.next/server/app/api/agent/new/route.js.nft.json +1 -1
  37. package/.next/server/app/api/agent/new/route_client-reference-manifest.js +1 -1
  38. package/.next/server/app/api/agent/resources/route.js +1 -0
  39. package/.next/server/app/api/agent/resources/route.js.nft.json +1 -0
  40. package/.next/server/app/api/agent/resources/route_client-reference-manifest.js +1 -0
  41. package/.next/server/app/api/config/route.js +1 -1
  42. package/.next/server/app/api/config/route.js.nft.json +1 -1
  43. package/.next/server/app/api/config/route_client-reference-manifest.js +1 -1
  44. package/.next/server/app/api/files/route.js +1 -1
  45. package/.next/server/app/api/files/route.js.nft.json +1 -1
  46. package/.next/server/app/api/files/route_client-reference-manifest.js +1 -1
  47. package/.next/server/app/api/fs/route.js +1 -1
  48. package/.next/server/app/api/fs/route.js.nft.json +1 -1
  49. package/.next/server/app/api/fs/route_client-reference-manifest.js +1 -1
  50. package/.next/server/app/api/git/route.js +4 -4
  51. package/.next/server/app/api/git/route.js.nft.json +1 -1
  52. package/.next/server/app/api/git/route_client-reference-manifest.js +1 -1
  53. package/.next/server/app/api/github/route.js +4 -4
  54. package/.next/server/app/api/github/route.js.nft.json +1 -1
  55. package/.next/server/app/api/github/route_client-reference-manifest.js +1 -1
  56. package/.next/server/app/api/growth/route.js +1 -1
  57. package/.next/server/app/api/growth/route.js.nft.json +1 -1
  58. package/.next/server/app/api/growth/route_client-reference-manifest.js +1 -1
  59. package/.next/server/app/api/health/route.js +1 -1
  60. package/.next/server/app/api/health/route.js.nft.json +1 -1
  61. package/.next/server/app/api/health/route_client-reference-manifest.js +1 -1
  62. package/.next/server/app/api/models/defaults/route.js +1 -1
  63. package/.next/server/app/api/models/defaults/route.js.nft.json +1 -1
  64. package/.next/server/app/api/models/defaults/route_client-reference-manifest.js +1 -1
  65. package/.next/server/app/api/models/route.js +1 -1
  66. package/.next/server/app/api/models/route.js.nft.json +1 -1
  67. package/.next/server/app/api/models/route_client-reference-manifest.js +1 -1
  68. package/.next/server/app/api/pi-settings/route.js +1 -1
  69. package/.next/server/app/api/pi-settings/route.js.nft.json +1 -1
  70. package/.next/server/app/api/pi-settings/route_client-reference-manifest.js +1 -1
  71. package/.next/server/app/api/plugins/route.js +1 -1
  72. package/.next/server/app/api/plugins/route.js.nft.json +1 -1
  73. package/.next/server/app/api/plugins/route_client-reference-manifest.js +1 -1
  74. package/.next/server/app/api/prompts/route.js +1 -1
  75. package/.next/server/app/api/prompts/route.js.nft.json +1 -1
  76. package/.next/server/app/api/prompts/route_client-reference-manifest.js +1 -1
  77. package/.next/server/app/api/remote/route.js +2 -2
  78. package/.next/server/app/api/remote/route.js.nft.json +1 -1
  79. package/.next/server/app/api/remote/route_client-reference-manifest.js +1 -1
  80. package/.next/server/app/api/security/route.js +1 -1
  81. package/.next/server/app/api/security/route.js.nft.json +1 -1
  82. package/.next/server/app/api/security/route_client-reference-manifest.js +1 -1
  83. package/.next/server/app/api/sessions/[id]/export/route.js +1 -1
  84. package/.next/server/app/api/sessions/[id]/export/route.js.nft.json +1 -1
  85. package/.next/server/app/api/sessions/[id]/export/route_client-reference-manifest.js +1 -1
  86. package/.next/server/app/api/sessions/[id]/route.js +1 -1
  87. package/.next/server/app/api/sessions/[id]/route.js.nft.json +1 -1
  88. package/.next/server/app/api/sessions/[id]/route_client-reference-manifest.js +1 -1
  89. package/.next/server/app/api/sessions/import/route.js +2 -2
  90. package/.next/server/app/api/sessions/import/route.js.nft.json +1 -1
  91. package/.next/server/app/api/sessions/import/route_client-reference-manifest.js +1 -1
  92. package/.next/server/app/api/sessions/route.js +1 -1
  93. package/.next/server/app/api/sessions/route.js.nft.json +1 -1
  94. package/.next/server/app/api/sessions/route_client-reference-manifest.js +1 -1
  95. package/.next/server/app/api/skills/route.js +2 -2
  96. package/.next/server/app/api/skills/route.js.nft.json +1 -1
  97. package/.next/server/app/api/skills/route_client-reference-manifest.js +1 -1
  98. package/.next/server/app/api/update/route.js +2 -2
  99. package/.next/server/app/api/update/route.js.nft.json +1 -1
  100. package/.next/server/app/api/update/route_client-reference-manifest.js +1 -1
  101. package/.next/server/app/api/version/route.js +1 -1
  102. package/.next/server/app/api/version/route.js.nft.json +1 -1
  103. package/.next/server/app/api/version/route_client-reference-manifest.js +1 -1
  104. package/.next/server/app/api/web-auth/route.js +2 -2
  105. package/.next/server/app/api/web-auth/route.js.nft.json +1 -1
  106. package/.next/server/app/api/web-auth/route_client-reference-manifest.js +1 -1
  107. package/.next/server/app/api/workspaces/route.js +1 -1
  108. package/.next/server/app/api/workspaces/route.js.nft.json +1 -1
  109. package/.next/server/app/api/workspaces/route_client-reference-manifest.js +1 -1
  110. package/.next/server/app/icon.svg/route.js +1 -1
  111. package/.next/server/app/icon.svg/route.js.nft.json +1 -1
  112. package/.next/server/app/index.html +1 -1
  113. package/.next/server/app/index.rsc +5 -5
  114. package/.next/server/app/index.segments/__PAGE__.segment.rsc +5 -5
  115. package/.next/server/app/index.segments/_full.segment.rsc +5 -5
  116. package/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  117. package/.next/server/app/login/page.js +1 -1
  118. package/.next/server/app/login/page.js.nft.json +1 -1
  119. package/.next/server/app/login/page_client-reference-manifest.js +1 -1
  120. package/.next/server/app/login.html +1 -1
  121. package/.next/server/app/login.rsc +4 -4
  122. package/.next/server/app/login.segments/_full.segment.rsc +4 -4
  123. package/.next/server/app/login.segments/_tree.segment.rsc +2 -2
  124. package/.next/server/app/login.segments/login/__PAGE__.segment.rsc +4 -4
  125. package/.next/server/app/page.js +1 -1
  126. package/.next/server/app/page.js.nft.json +1 -1
  127. package/.next/server/app/page_client-reference-manifest.js +1 -1
  128. package/.next/server/app-paths-manifest.json +6 -5
  129. package/.next/server/chunks/{813.js → 1813.js} +1 -1
  130. package/.next/server/chunks/{430.js → 2430.js} +5 -5
  131. package/.next/server/chunks/{970.js → 2970.js} +1 -1
  132. package/.next/server/chunks/3924.js +6 -0
  133. package/.next/server/chunks/4807.js +1 -0
  134. package/.next/server/chunks/5393.js +1 -0
  135. package/.next/server/chunks/5802.js +2 -0
  136. package/.next/server/chunks/620.js +1 -1
  137. package/.next/server/chunks/6229.js +1 -0
  138. package/.next/server/chunks/7496.js +1 -0
  139. package/.next/server/chunks/7662.js +2 -0
  140. package/.next/server/chunks/8686.js +1 -0
  141. package/.next/server/chunks/{593.js → 974.js} +1 -1
  142. package/.next/server/chunks/{920.js → 9920.js} +1 -1
  143. package/.next/server/instrumentation.js +1 -1
  144. package/.next/server/instrumentation.js.nft.json +1 -1
  145. package/.next/server/middleware-build-manifest.js +1 -1
  146. package/.next/server/middleware-react-loadable-manifest.js +1 -1
  147. package/.next/server/middleware.js +5 -5
  148. package/.next/server/pages/404.html +1 -1
  149. package/.next/server/pages/500.html +1 -1
  150. package/.next/server/preview-props.json +3 -3
  151. package/.next/server/server-reference-manifest.json +1 -1
  152. package/.next/server/webpack-runtime.js +1 -1
  153. package/.next/static/QJCtqgDKzGS2BdKABd9ch/_buildManifest.js +1 -0
  154. package/.next/static/chunks/2577.86c8538fbf05a8b2.js +1 -0
  155. package/.next/static/chunks/2716.fa561519675a3ff1.js +1 -0
  156. package/.next/static/chunks/2906.8a0e09bb47605cf1.js +1 -0
  157. package/.next/static/chunks/4673.ea36e55005c0f82f.js +1 -0
  158. package/.next/static/chunks/4686-4dc565817a160351.js +1 -0
  159. package/.next/static/chunks/4778.0d2c5d75b791d352.js +1 -0
  160. package/.next/static/chunks/app/_global-error/page-f2d93969c0bcf406.js +1 -0
  161. package/.next/static/chunks/app/_not-found/page-2d40d1d7d916666a.js +1 -0
  162. package/.next/static/chunks/app/api/agent/[id]/events/route-f2d93969c0bcf406.js +1 -0
  163. package/.next/static/chunks/app/api/agent/[id]/route-f2d93969c0bcf406.js +1 -0
  164. package/.next/static/chunks/app/api/agent/[id]/tree/route-f2d93969c0bcf406.js +1 -0
  165. package/.next/static/chunks/app/api/agent/new/route-f2d93969c0bcf406.js +1 -0
  166. package/.next/static/chunks/app/api/agent/resources/route-f2d93969c0bcf406.js +1 -0
  167. package/.next/static/chunks/app/api/config/route-f2d93969c0bcf406.js +1 -0
  168. package/.next/static/chunks/app/api/files/route-f2d93969c0bcf406.js +1 -0
  169. package/.next/static/chunks/app/api/fs/route-f2d93969c0bcf406.js +1 -0
  170. package/.next/static/chunks/app/api/git/route-f2d93969c0bcf406.js +1 -0
  171. package/.next/static/chunks/app/api/github/route-f2d93969c0bcf406.js +1 -0
  172. package/.next/static/chunks/app/api/growth/route-f2d93969c0bcf406.js +1 -0
  173. package/.next/static/chunks/app/api/health/route-f2d93969c0bcf406.js +1 -0
  174. package/.next/static/chunks/app/api/models/defaults/route-f2d93969c0bcf406.js +1 -0
  175. package/.next/static/chunks/app/api/models/route-f2d93969c0bcf406.js +1 -0
  176. package/.next/static/chunks/app/api/pi-settings/route-f2d93969c0bcf406.js +1 -0
  177. package/.next/static/chunks/app/api/plugins/route-f2d93969c0bcf406.js +1 -0
  178. package/.next/static/chunks/app/api/prompts/route-f2d93969c0bcf406.js +1 -0
  179. package/.next/static/chunks/app/api/remote/route-f2d93969c0bcf406.js +1 -0
  180. package/.next/static/chunks/app/api/security/route-f2d93969c0bcf406.js +1 -0
  181. package/.next/static/chunks/app/api/sessions/[id]/export/route-f2d93969c0bcf406.js +1 -0
  182. package/.next/static/chunks/app/api/sessions/[id]/route-f2d93969c0bcf406.js +1 -0
  183. package/.next/static/chunks/app/api/sessions/import/route-f2d93969c0bcf406.js +1 -0
  184. package/.next/static/chunks/app/api/sessions/route-f2d93969c0bcf406.js +1 -0
  185. package/.next/static/chunks/app/api/skills/route-f2d93969c0bcf406.js +1 -0
  186. package/.next/static/chunks/app/api/update/route-f2d93969c0bcf406.js +1 -0
  187. package/.next/static/chunks/app/api/version/route-f2d93969c0bcf406.js +1 -0
  188. package/.next/static/chunks/app/api/web-auth/route-f2d93969c0bcf406.js +1 -0
  189. package/.next/static/chunks/app/api/workspaces/route-f2d93969c0bcf406.js +1 -0
  190. package/.next/static/chunks/app/{layout-7f64fa67ea6ce980.js → layout-50d06e2078d6ce76.js} +1 -1
  191. package/.next/static/chunks/app/login/{page-00dbd2f395b364d5.js → page-5e276a1eeb8867df.js} +1 -1
  192. package/.next/static/chunks/app/page-71f2d3b3ed864b3d.js +1 -0
  193. package/.next/static/chunks/main-5403f0764579e5fe.js +5 -0
  194. package/.next/static/chunks/{main-app-56863ace24eb152f.js → main-app-e9b53f0e1c6591b2.js} +1 -1
  195. package/.next/static/chunks/next/dist/client/components/builtin/app-error-f2d93969c0bcf406.js +1 -0
  196. package/.next/static/chunks/next/dist/client/components/builtin/forbidden-f2d93969c0bcf406.js +1 -0
  197. package/.next/static/chunks/next/dist/client/components/builtin/global-error-a7e260f8cc2582d7.js +1 -0
  198. package/.next/static/chunks/next/dist/client/components/builtin/not-found-f2d93969c0bcf406.js +1 -0
  199. package/.next/static/chunks/next/dist/client/components/builtin/unauthorized-f2d93969c0bcf406.js +1 -0
  200. package/.next/static/chunks/webpack-ffc09b5fcc8aaa6d.js +1 -0
  201. package/.next/static/css/7e3caf797e4cb19a.css +1 -0
  202. package/.next/types/app/api/agent/[id]/events/route.ts +1 -1
  203. package/.next/types/app/api/agent/[id]/route.ts +1 -1
  204. package/.next/types/app/api/agent/[id]/tree/route.ts +1 -1
  205. package/.next/types/app/api/agent/new/route.ts +1 -1
  206. package/.next/types/app/api/agent/resources/route.ts +355 -0
  207. package/.next/types/app/api/config/route.ts +1 -1
  208. package/.next/types/app/api/files/route.ts +1 -1
  209. package/.next/types/app/api/fs/route.ts +1 -1
  210. package/.next/types/app/api/git/route.ts +1 -1
  211. package/.next/types/app/api/github/route.ts +1 -1
  212. package/.next/types/app/api/growth/route.ts +1 -1
  213. package/.next/types/app/api/health/route.ts +1 -1
  214. package/.next/types/app/api/models/defaults/route.ts +1 -1
  215. package/.next/types/app/api/models/route.ts +1 -1
  216. package/.next/types/app/api/pi-settings/route.ts +1 -1
  217. package/.next/types/app/api/plugins/route.ts +1 -1
  218. package/.next/types/app/api/prompts/route.ts +1 -1
  219. package/.next/types/app/api/remote/route.ts +1 -1
  220. package/.next/types/app/api/security/route.ts +1 -1
  221. package/.next/types/app/api/sessions/[id]/export/route.ts +1 -1
  222. package/.next/types/app/api/sessions/[id]/route.ts +1 -1
  223. package/.next/types/app/api/sessions/import/route.ts +1 -1
  224. package/.next/types/app/api/sessions/route.ts +1 -1
  225. package/.next/types/app/api/skills/route.ts +1 -1
  226. package/.next/types/app/api/update/route.ts +1 -1
  227. package/.next/types/app/api/version/route.ts +1 -1
  228. package/.next/types/app/api/web-auth/route.ts +1 -1
  229. package/.next/types/app/api/workspaces/route.ts +1 -1
  230. package/.next/types/app/layout.ts +1 -1
  231. package/.next/types/app/login/page.ts +1 -1
  232. package/.next/types/app/page.ts +1 -1
  233. package/.next/types/routes.d.ts +2 -1
  234. package/.next/types/validator.ts +10 -0
  235. package/LICENSE +1 -1
  236. package/README.md +1 -1
  237. package/bin/pi.js +0 -0
  238. package/bin/piweb.js +0 -0
  239. package/package.json +1 -1
  240. package/scripts/build-output.mjs +0 -0
  241. package/scripts/dev.mjs +0 -0
  242. package/scripts/entrypoint.mjs +0 -0
  243. package/scripts/install-build.mjs +0 -0
  244. package/scripts/launcher.mjs +0 -0
  245. package/scripts/pick-folder.ps1 +0 -0
  246. package/scripts/postinstall.mjs +0 -0
  247. package/scripts/process-runner.mjs +0 -0
  248. package/scripts/release.mjs +0 -0
  249. package/scripts/sync-file-icons.mjs +0 -0
  250. package/scripts/sync-provider-icons.mjs +0 -0
  251. package/src/app/api/agent/resources/route.ts +18 -0
  252. package/src/app/api/remote/route.ts +5 -7
  253. package/src/app/api/sessions/[id]/export/route.ts +3 -1
  254. package/src/app/globals.css +16 -0
  255. package/src/components/AppShell.tsx +68 -21
  256. package/src/components/ChatInput.tsx +98 -27
  257. package/src/components/ChatWindow.tsx +68 -11
  258. package/src/components/ContextMeter.tsx +24 -4
  259. package/src/components/ExtensionUI.tsx +9 -3
  260. package/src/components/FileViewer.tsx +24 -21
  261. package/src/components/GitPanel.tsx +449 -449
  262. package/src/components/GrowthTree.tsx +3 -1
  263. package/src/components/ImageViewer.tsx +72 -0
  264. package/src/components/MarkdownBlock.tsx +6 -3
  265. package/src/components/PreviewImage.tsx +33 -0
  266. package/src/components/ProjectPanel.tsx +5 -3
  267. package/src/components/PromptPanel.tsx +322 -313
  268. package/src/components/RemoteSection.tsx +2 -1
  269. package/src/components/SessionSidebar.tsx +22 -4
  270. package/src/components/SettingsPanel.tsx +2654 -2639
  271. package/src/components/composer/ComposerImages.tsx +1 -79
  272. package/src/hooks/usePiWeb.ts +47 -2
  273. package/src/i18n.tsx +24 -0
  274. package/src/lib/agent-manager.ts +275 -18
  275. package/src/lib/chat/skill-expansion.ts +10 -0
  276. package/src/lib/extension-ui.ts +25 -1
  277. package/src/lib/git-service.ts +291 -289
  278. package/src/lib/github-identity.ts +245 -245
  279. package/src/lib/growth-turns.ts +3 -1
  280. package/src/lib/session-reader.ts +25 -13
  281. package/src/lib/session-retention.ts +11 -8
  282. package/src/lib/slash-command-token.ts +14 -0
  283. package/src/lib/trajectory.ts +17 -6
  284. package/src/lib/types.ts +24 -2
  285. package/src/lib/workspace-store.ts +36 -1
  286. package/.next/diagnostics/build-diagnostics.json +0 -6
  287. package/.next/diagnostics/framework.json +0 -1
  288. package/.next/server/chunks/229.js +0 -1
  289. package/.next/server/chunks/393.js +0 -1
  290. package/.next/server/chunks/612.js +0 -1
  291. package/.next/server/chunks/662.js +0 -2
  292. package/.next/server/chunks/686.js +0 -1
  293. package/.next/server/chunks/802.js +0 -2
  294. package/.next/server/chunks/807.js +0 -1
  295. package/.next/server/chunks/924.js +0 -6
  296. package/.next/static/4snUelZTEIiQYCkB5nTW7/_buildManifest.js +0 -1
  297. package/.next/static/chunks/1966.1560d0f43ac9a41a.js +0 -1
  298. package/.next/static/chunks/2577.eebf2cf291a811ad.js +0 -1
  299. package/.next/static/chunks/2716.a5228b0eeedbe4c8.js +0 -1
  300. package/.next/static/chunks/2906.546a64377e12dfbc.js +0 -1
  301. package/.next/static/chunks/3899.55a0943bf5e7fec7.js +0 -1
  302. package/.next/static/chunks/4673.8eff281cb63e2799.js +0 -1
  303. package/.next/static/chunks/4686-f03275819a719fd8.js +0 -1
  304. package/.next/static/chunks/4778.2dae0c6a9e396326.js +0 -1
  305. package/.next/static/chunks/app/_global-error/page-f977af7f858990a2.js +0 -1
  306. package/.next/static/chunks/app/_not-found/page-851dc3883a1937aa.js +0 -1
  307. package/.next/static/chunks/app/api/agent/[id]/events/route-f977af7f858990a2.js +0 -1
  308. package/.next/static/chunks/app/api/agent/[id]/route-f977af7f858990a2.js +0 -1
  309. package/.next/static/chunks/app/api/agent/[id]/tree/route-f977af7f858990a2.js +0 -1
  310. package/.next/static/chunks/app/api/agent/new/route-f977af7f858990a2.js +0 -1
  311. package/.next/static/chunks/app/api/config/route-f977af7f858990a2.js +0 -1
  312. package/.next/static/chunks/app/api/files/route-f977af7f858990a2.js +0 -1
  313. package/.next/static/chunks/app/api/fs/route-f977af7f858990a2.js +0 -1
  314. package/.next/static/chunks/app/api/git/route-f977af7f858990a2.js +0 -1
  315. package/.next/static/chunks/app/api/github/route-f977af7f858990a2.js +0 -1
  316. package/.next/static/chunks/app/api/growth/route-f977af7f858990a2.js +0 -1
  317. package/.next/static/chunks/app/api/health/route-f977af7f858990a2.js +0 -1
  318. package/.next/static/chunks/app/api/models/defaults/route-f977af7f858990a2.js +0 -1
  319. package/.next/static/chunks/app/api/models/route-f977af7f858990a2.js +0 -1
  320. package/.next/static/chunks/app/api/pi-settings/route-f977af7f858990a2.js +0 -1
  321. package/.next/static/chunks/app/api/plugins/route-f977af7f858990a2.js +0 -1
  322. package/.next/static/chunks/app/api/prompts/route-f977af7f858990a2.js +0 -1
  323. package/.next/static/chunks/app/api/remote/route-f977af7f858990a2.js +0 -1
  324. package/.next/static/chunks/app/api/security/route-f977af7f858990a2.js +0 -1
  325. package/.next/static/chunks/app/api/sessions/[id]/export/route-f977af7f858990a2.js +0 -1
  326. package/.next/static/chunks/app/api/sessions/[id]/route-f977af7f858990a2.js +0 -1
  327. package/.next/static/chunks/app/api/sessions/import/route-f977af7f858990a2.js +0 -1
  328. package/.next/static/chunks/app/api/sessions/route-f977af7f858990a2.js +0 -1
  329. package/.next/static/chunks/app/api/skills/route-f977af7f858990a2.js +0 -1
  330. package/.next/static/chunks/app/api/update/route-f977af7f858990a2.js +0 -1
  331. package/.next/static/chunks/app/api/version/route-f977af7f858990a2.js +0 -1
  332. package/.next/static/chunks/app/api/web-auth/route-f977af7f858990a2.js +0 -1
  333. package/.next/static/chunks/app/api/workspaces/route-f977af7f858990a2.js +0 -1
  334. package/.next/static/chunks/app/page-0159987118376f42.js +0 -1
  335. package/.next/static/chunks/main-22ccebc2da4d291e.js +0 -5
  336. package/.next/static/chunks/next/dist/client/components/builtin/app-error-f977af7f858990a2.js +0 -1
  337. package/.next/static/chunks/next/dist/client/components/builtin/forbidden-f977af7f858990a2.js +0 -1
  338. package/.next/static/chunks/next/dist/client/components/builtin/global-error-c37b7707077f50ce.js +0 -1
  339. package/.next/static/chunks/next/dist/client/components/builtin/not-found-f977af7f858990a2.js +0 -1
  340. package/.next/static/chunks/next/dist/client/components/builtin/unauthorized-f977af7f858990a2.js +0 -1
  341. package/.next/static/chunks/webpack-c1b43c269867b64f.js +0 -1
  342. package/.next/static/css/93ed3daa6f3fbc35.css +0 -1
  343. package/.next/trace +0 -58
  344. package/.next/trace-build +0 -1
  345. /package/.next/static/{4snUelZTEIiQYCkB5nTW7 → QJCtqgDKzGS2BdKABd9ch}/_ssgManifest.js +0 -0
@@ -9,6 +9,7 @@
9
9
  import { useEffect, useMemo, useRef, useState } from "react";
10
10
  import {
11
11
  IconAtOutline16,
12
+ IconCloseOutline14,
12
13
  IconFileOutline16,
13
14
  IconFolderClose16,
14
15
  IconSendArrowUp14,
@@ -24,6 +25,7 @@ import { useWorkspaceFiles } from "@/hooks/useWorkspaceFiles";
24
25
  import { fmtBytes } from "@/lib/format";
25
26
  import { otherBehavior, useEnterBehavior } from "@/lib/enter-behavior";
26
27
  import { atToken, candidateText, suggestPaths, type PathCandidate } from "@/lib/workspace-index";
28
+ import { slashCommandToken } from "@/lib/slash-command-token";
27
29
  import type { ImageAttachment } from "@/lib/types";
28
30
 
29
31
  export interface SlashCommand {
@@ -87,6 +89,7 @@ export function ChatInput({
87
89
  pendingUploadCount = 0,
88
90
  pendingSend = false,
89
91
  onSendPendingChange,
92
+ attachmentLayout = "overlay",
90
93
  }: {
91
94
  disabled?: boolean;
92
95
  isStreaming: boolean;
@@ -132,6 +135,8 @@ export function ChatInput({
132
135
  pendingUploadCount?: number;
133
136
  pendingSend?: boolean;
134
137
  onSendPendingChange?: (pending: boolean) => void;
138
+ /** Hero 在输入卡上方预留空间;聊天中仍让附件悬浮、不占布局高度 */
139
+ attachmentLayout?: "overlay" | "flow";
135
140
  }) {
136
141
  const [localDraft, setLocalDraft] = useState<ChatDraft>(initialDraft ?? EMPTY_CHAT_DRAFT);
137
142
  const currentDraft = draft ?? localDraft;
@@ -164,6 +169,7 @@ export function ChatInput({
164
169
  const [modelOpen, setModelOpen] = useState(false);
165
170
  const [cmdIdx, setCmdIdx] = useState(0);
166
171
  const [cmdDismissed, setCmdDismissed] = useState(false);
172
+ const [commandCaret, setCommandCaret] = useState<number | null>(currentDraft.text.length);
167
173
  // @ 引用:打字触发候选,选中后落成引用 chip(发送时统一拼成 `@路径` 行)
168
174
  const [mention, setMention] = useState<{ query: string; start: number; tokenEnd: number } | null>(null);
169
175
  const [mentionIdx, setMentionIdx] = useState(0);
@@ -189,11 +195,11 @@ export function ChatInput({
189
195
  autoSize();
190
196
  }, [text]);
191
197
 
192
- // 斜杠命令模式:文本以 / 开头且未输入参数空格
193
- const slash = text.startsWith("/") ? text.slice(1) : "";
194
- const hasArgs = slash.includes(" ");
195
- const typedCommandMode = !!commands?.length && text.startsWith("/") && !cmdDismissed && !hasArgs;
196
- const filteredCommands = (commands ?? []).filter((c) => c.name.toLowerCase().startsWith(slash.split(" ")[0].toLowerCase()));
198
+ // 补全光标处的 token,而不是只看草稿的第一个字符
199
+ const commandToken = slashCommandToken(text, commandCaret);
200
+ const slash = commandToken?.query ?? "";
201
+ const typedCommandMode = !adopted && !!commands?.length && !!commandToken && !cmdDismissed;
202
+ const filteredCommands = (commands ?? []).filter((c) => c.name.toLowerCase().startsWith(slash.toLowerCase()));
197
203
  const visibleCommands = filteredCommands;
198
204
  const commandMode = typedCommandMode;
199
205
 
@@ -277,7 +283,67 @@ export function ChatInput({
277
283
  }, [cmdIdx, visibleCommands.length]);
278
284
  const effectiveIdx = Math.min(cmdIdx, Math.max(0, visibleCommands.length - 1));
279
285
 
286
+ const withoutCommandToken = () => commandToken ? text.slice(0, commandToken.start) + text.slice(commandToken.tokenEnd) : text;
287
+ const adoptCommand = (entry: SlashCommand) => {
288
+ // SDK 只展开开头的一个命令:已经采纳时不再覆盖(先 Backspace 摘掉)
289
+ if (draftRef.current.adopted) return;
290
+ const remaining = withoutCommandToken();
291
+ const caret = commandToken?.start ?? remaining.length;
292
+ setAdopted(entry);
293
+ setText(remaining);
294
+ setCommandCaret(caret);
295
+ setCmdDismissed(true);
296
+ setCmdIdx(0);
297
+ requestAnimationFrame(() => {
298
+ autoSize();
299
+ taRef.current?.focus();
300
+ taRef.current?.setSelectionRange(caret, caret);
301
+ });
302
+ };
303
+
304
+ // 行内扩展命令(如 /fast)自己执行:不能为了切换它把周围的草稿、图片或引用一起发出去
305
+ const runInlineExtension = async (entry: SlashCommand) => {
306
+ if (disabled || isBlocked || sendingRef.current || pendingSend || uploadBusy) return;
307
+ const original = text;
308
+ const remaining = withoutCommandToken();
309
+ sendingRef.current = true;
310
+ setSending(true);
311
+ onSendPendingChange?.(true);
312
+ setText(remaining);
313
+ setCommandCaret(remaining.length);
314
+ setCmdDismissed(true);
315
+ try {
316
+ const result = await onSend(`/${entry.name}`, []);
317
+ if (!result?.success) throw new Error(result?.error || "Command failed");
318
+ } catch (error) {
319
+ setText((previous) => previous === remaining ? original : previous);
320
+ if (mountedRef.current) {
321
+ setCommandCaret(commandToken?.tokenEnd ?? original.length);
322
+ setCmdDismissed(false);
323
+ setAttachmentError(error instanceof Error ? error.message : String(error));
324
+ }
325
+ } finally {
326
+ sendingRef.current = false;
327
+ if (mountedRef.current) setSending(false);
328
+ onSendPendingChange?.(false);
329
+ }
330
+ };
331
+
280
332
  const runEntry = (entry: SlashCommand) => {
333
+ if (draftRef.current.adopted) return;
334
+ // 命令出现在正文中间:只替换这一段 token,前后文字原样保留
335
+ if (commandToken && (commandToken.start > 0 || commandToken.tokenEnd < text.length)) {
336
+ if (entry.kind === "builtin") {
337
+ if (entry.name === "model") setModelOpen(true);
338
+ else onCommand?.(entry.name, "");
339
+ const remaining = withoutCommandToken();
340
+ setText(remaining);
341
+ setCommandCaret(remaining.length);
342
+ setCmdDismissed(true);
343
+ } else if (entry.kind === "extension") void runInlineExtension(entry);
344
+ else adoptCommand(entry);
345
+ return;
346
+ }
281
347
  const args = slash.slice(entry.name.length).trim();
282
348
  if (entry.kind === "builtin" && entry.name === "model") {
283
349
  setModelOpen(true);
@@ -505,7 +571,7 @@ export function ChatInput({
505
571
  // eslint-disable-next-line react-hooks/exhaustive-deps
506
572
  }, []);
507
573
 
508
- const hasDraft = text.trim().length > 0 || images.length > 0 || uploads.length > 0 || references.length > 0;
574
+ const hasDraft = adopted !== null || text.trim().length > 0 || images.length > 0 || uploads.length > 0 || references.length > 0;
509
575
 
510
576
  return (
511
577
  <div ref={wrapRef} className="relative w-full" data-testid="composer">
@@ -650,11 +716,12 @@ export function ChatInput({
650
716
  )}
651
717
 
652
718
  {/*
653
- * 附件轨(图片 / 引用 / 上传文件):绝对定位浮在输入卡上方,**不占布局高度** ——
719
+ * 附件轨(图片 / 引用 / 上传文件):聊天中悬浮于输入卡上方,不占布局高度 ——
654
720
  * 输入框位置不动,多出来的附件直接盖住后面的聊天记录(用户要的就是这个)。
721
+ * 新对话页用 flow 占位,避免 @ 引用和图片盖住上方的工作区选择器。
655
722
  * 外层 pointer-events-none,只有附件本身接收点击,免得挡住聊天区的交互。
656
723
  */}
657
- <div className="pointer-events-none absolute inset-x-0 bottom-full z-20 flex flex-col gap-1.5 pb-2" data-testid="composer-attachments">
724
+ {(images.length > 0 || references.length > 0 || uploads.length > 0) && <div className={`pointer-events-none z-20 flex flex-col gap-1.5 pb-2 ${attachmentLayout === "flow" ? "relative" : "absolute inset-x-0 bottom-full"}`} data-testid="composer-attachments">
658
725
  {/* 附件轨:图片(超过 4 张折叠成牌堆,展开沿弧线扇开;点开可放大、切换、删除) */}
659
726
  {images.length > 0 && (
660
727
  <ComposerImages
@@ -714,16 +781,18 @@ export function ChatInput({
714
781
  />
715
782
  )}
716
783
 
717
- </div>
784
+ </div>}
718
785
 
719
786
  {/* 卡片(Iconsax .isx-composer:24px 圆角 + 聚焦高对比描边 + 底部流光短横条)—— 拖放由 document 级监听统一接管 */}
720
787
  <div className="isx-composer relative w-full">
721
- <div className="px-4 pt-3 flex items-start gap-1.5">
788
+ <div className="px-4 pt-3 flex flex-col items-stretch gap-1.5">
722
789
  {adopted && (
723
790
  <button
724
791
  type="button"
725
- className="flex-none flex items-center gap-1 self-start"
726
- title={`${t.delete} (Backspace)`}
792
+ className="flex-none flex max-w-full items-center gap-1 self-start"
793
+ disabled={disabled || sendBusy}
794
+ aria-label={`${t.delete} /${adopted.name}`}
795
+ title={`${t.delete} /${adopted.name} (Backspace)`}
727
796
  style={{
728
797
  fontSize: "var(--piweb-chat-font-size, var(--dsh-content-font-size))",
729
798
  lineHeight: "var(--piweb-chat-line-height, 1.55)",
@@ -738,7 +807,8 @@ export function ChatInput({
738
807
  }}
739
808
  >
740
809
  <IconTerminalOutline14 size={14} style={{ color: "var(--dsw-accent)", flex: "none" }} />
741
- <span style={{ color: "var(--dsw-accent)", fontWeight: 600 }}>/{adopted.name}</span>
810
+ <span className="min-w-0 truncate" style={{ color: "var(--dsw-accent)", fontWeight: 600 }}>/{adopted.name}</span>
811
+ <IconCloseOutline14 size={12} style={{ flex: "none", color: "var(--dsw-label-caption)" }} />
742
812
  </button>
743
813
  )}
744
814
  <textarea
@@ -748,11 +818,12 @@ export function ChatInput({
748
818
  rows={1}
749
819
  placeholder={adopted ? (adopted.argumentHint ?? t.inputPlaceholder) : !disabled && isBlocked ? t.blockedComposer : t.inputPlaceholder}
750
820
  suppressHydrationWarning
751
- className="min-w-0 flex-1 resize-none"
821
+ className="min-w-0 w-full resize-none"
752
822
  style={{ fontSize: "var(--piweb-chat-font-size, var(--dsh-content-font-size))", lineHeight: 1.55 }}
753
823
  onChange={(e) => {
754
824
  const value = e.target.value;
755
825
  setText(value);
826
+ setCommandCaret(e.target.selectionStart);
756
827
  setCmdDismissed(false);
757
828
  setCmdIdx(0);
758
829
  syncMention(value, e.target.selectionStart);
@@ -761,7 +832,10 @@ export function ChatInput({
761
832
  onSelect={(e) => {
762
833
  // 选中了一段文字时不弹:那时 Enter / Tab 应该照常工作
763
834
  const ta = e.target as HTMLTextAreaElement;
764
- syncMention(ta.value, ta.selectionStart === ta.selectionEnd ? ta.selectionStart : null);
835
+ const caret = ta.selectionStart === ta.selectionEnd ? ta.selectionStart : null;
836
+ setCommandCaret(caret);
837
+ if (slashCommandToken(ta.value, caret)?.start !== commandToken?.start) setCmdDismissed(false);
838
+ syncMention(ta.value, caret);
765
839
  }}
766
840
  onPaste={(e) => {
767
841
  const files = Array.from(e.clipboardData.files);
@@ -799,13 +873,16 @@ export function ChatInput({
799
873
  }
800
874
  }
801
875
  }
802
- if (adopted && text === "" && (e.key === "Backspace" || e.key === "Escape")) {
803
- // 空参数时退格/Esc:摘掉命令胶囊
876
+ if (adopted && !e.nativeEvent.isComposing && (
877
+ (e.key === "Backspace" && e.currentTarget.selectionStart === 0 && e.currentTarget.selectionEnd === 0) ||
878
+ (e.key === "Escape" && text === "")
879
+ )) {
880
+ // 光标在开头时退格 / 空参数时 Esc:只摘掉命令标记,不碰任何正文
804
881
  e.preventDefault();
805
882
  setAdopted(null);
806
883
  return;
807
884
  }
808
- if (commandMode && visibleCommands.length > 0) {
885
+ if (commandMode && visibleCommands.length > 0 && !e.nativeEvent.isComposing) {
809
886
  if (e.key === "ArrowDown") {
810
887
  e.preventDefault();
811
888
  setCmdIdx((i) => Math.min(i + 1, visibleCommands.length - 1));
@@ -818,10 +895,10 @@ export function ChatInput({
818
895
  }
819
896
  if (e.key === "Escape") {
820
897
  e.preventDefault();
821
- setCmdDismissed(true);
898
+ setCmdDismissed(true);
822
899
  return;
823
900
  }
824
- if (e.key === "Enter" && !e.nativeEvent.isComposing) {
901
+ if (e.key === "Enter") {
825
902
  e.preventDefault();
826
903
  runEntry(visibleCommands[effectiveIdx]);
827
904
  return;
@@ -831,13 +908,7 @@ export function ChatInput({
831
908
  e.preventDefault();
832
909
  const entry = visibleCommands[effectiveIdx];
833
910
  if (!entry) return;
834
- setAdopted(entry);
835
- setText("");
836
- setCmdIdx(0);
837
- requestAnimationFrame(() => {
838
- autoSize();
839
- taRef.current?.focus();
840
- });
911
+ adoptCommand(entry);
841
912
  return;
842
913
  }
843
914
  }
@@ -30,6 +30,7 @@ import { languageForPath } from "@/lib/highlight";
30
30
  import { parseExpandedSkill } from "@/lib/chat/skill-expansion";
31
31
  import { copyText } from "@/lib/clipboard";
32
32
  import { OutlineRail } from "@/components/OutlineRail";
33
+ import { PreviewImage } from "@/components/PreviewImage";
33
34
  import { ChatScrollContext, type ChatScroll } from "@/components/chat/chat-scroll";
34
35
  import { MarkdownActionsProvider, type MarkdownActions } from "@/components/chat/markdown-actions";
35
36
  import { useChatReadingAnchor } from "@/components/chat/reading-anchor";
@@ -879,7 +880,7 @@ function ItemView({ item, cwd, onInspectTool, onOpenFile }: { item: Item; cwd?:
879
880
  case "image":
880
881
  return (
881
882
  <div className="pw-narr">
882
- <img src={`data:${item.mimeType};base64,${item.data}`} alt="" className="my-1 max-h-96 max-w-full rounded-2xl object-contain" />
883
+ <PreviewImage image={{ type: "image", mimeType: item.mimeType, data: item.data }} className="my-1 max-h-96 max-w-full rounded-2xl object-contain" />
883
884
  </div>
884
885
  );
885
886
  case "tool":
@@ -916,11 +917,12 @@ const FinalAnswer = memo(function FinalAnswer({
916
917
  .filter((c): c is { type: "text"; text: string } => c.type === "text")
917
918
  .map((c) => c.text)
918
919
  .join("");
920
+ const images = message.content.filter((c) => c.type === "image");
919
921
  return (
920
922
  <div className="group w-full" style={{ marginTop: 14 }} data-role="assistant">
921
923
  {message.content.map((c, i) => {
922
924
  if (c.type === "text" && c.text.trim()) return <Markdown key={i} text={c.text} />;
923
- if (c.type === "image") return <img key={i} src={`data:${c.mimeType};base64,${c.data}`} alt="" className="my-2 max-h-96 max-w-full rounded-2xl object-contain" />;
925
+ if (c.type === "image") return <PreviewImage key={i} image={c} images={images} index={images.indexOf(c)} className="my-2 max-h-96 max-w-full rounded-2xl object-contain" />;
924
926
  return null;
925
927
  })}
926
928
  {showActions && (
@@ -980,10 +982,11 @@ function UserImages({ images }: { images: Array<{ type: "image"; data: string; m
980
982
  return (
981
983
  <div className="mb-2 flex max-w-[85%] flex-wrap justify-end gap-2 self-end">
982
984
  {images.map((image, index) => (
983
- <img
985
+ <PreviewImage
984
986
  key={index}
985
- src={`data:${image.mimeType};base64,${image.data}`}
986
- alt=""
987
+ image={image}
988
+ images={images}
989
+ index={index}
987
990
  className="max-h-72 max-w-full rounded-2xl object-contain"
988
991
  style={{ border: "0.5px solid var(--dsw-border-l2)" }}
989
992
  />
@@ -1003,6 +1006,8 @@ const UserMessage = memo(function UserMessage({ message, onEditMessage }: { mess
1003
1006
  if (editing) {
1004
1007
  return (
1005
1008
  <div className="group mt-7 flex w-full flex-col items-end first:mt-0" data-role="user" data-message-id={message.id}>
1009
+ {/* 编辑只改文字,图片由服务端从原消息里带进重发;照样显示,免得以为图被丢了 */}
1010
+ <UserImages images={images} />
1006
1011
  <div className="msg-user-bubble w-full">
1007
1012
  <textarea
1008
1013
  className="w-full resize-none bg-transparent outline-none"
@@ -1064,7 +1069,8 @@ const UserMessage = memo(function UserMessage({ message, onEditMessage }: { mess
1064
1069
 
1065
1070
  // ---------- 回合:用户消息 → 过程步骤 → 最终回答 ----------
1066
1071
 
1067
- type Turn = { key: string; userIndex: number; assistantIndexes: number[] };
1072
+ /** summaryIndex:压缩分隔卡 / 分支摘要卡单独占一格,没有用户消息和过程 */
1073
+ type Turn = { key: string; userIndex: number; assistantIndexes: number[]; summaryIndex?: number };
1068
1074
 
1069
1075
  function buildTurns(messages: WebMessage[]): Turn[] {
1070
1076
  const out: Turn[] = [];
@@ -1073,6 +1079,10 @@ function buildTurns(messages: WebMessage[]): Turn[] {
1073
1079
  if (m.role === "user") {
1074
1080
  cur = { key: `u${i}`, userIndex: i, assistantIndexes: [] };
1075
1081
  out.push(cur);
1082
+ } else if (m.role === "summary") {
1083
+ // 压缩可能切在回合中间:卡片后面的助手消息另起一个回合,不并回卡片上方的用户消息
1084
+ out.push({ key: `s${i}`, userIndex: -1, assistantIndexes: [], summaryIndex: i });
1085
+ cur = null;
1076
1086
  } else if (m.role === "assistant") {
1077
1087
  if (!cur) {
1078
1088
  cur = { key: `a${i}`, userIndex: -1, assistantIndexes: [] };
@@ -1311,6 +1321,45 @@ function TurnLite({ turn, messages }: { turn: Turn; messages: WebMessage[] }) {
1311
1321
  );
1312
1322
  }
1313
1323
 
1324
+ /**
1325
+ * 压缩分隔卡:卡片以上的消息照常显示(可编辑、可分叉),但模型只看得到它们的摘要;摘要可展开。
1326
+ * 分支摘要卡:切换分支时离开的那条分支被总结成摘要发给模型。
1327
+ */
1328
+ function SummaryCard({ message }: { message: WebMessage }) {
1329
+ const { t } = useI18n();
1330
+ const [open, setOpen] = useState(false);
1331
+ const branch = message.summaryKind === "branch";
1332
+ const summary = message.summary?.trim();
1333
+ const title = branch
1334
+ ? t.branchSummaryTitle
1335
+ : message.tokensBefore
1336
+ ? t.compactedTitleTokens.replace("{tokens}", fmtTokens(message.tokensBefore))
1337
+ : t.compactedTitle;
1338
+ const rule = <span aria-hidden="true" style={{ flex: 1, borderTop: "1px dashed var(--dsw-border-l3)" }} />;
1339
+ return (
1340
+ <div className="w-full" data-role="summary" data-message-id={message.id} style={{ fontSize: "var(--piweb-chat-font-s)", color: "var(--dsw-label-caption)" }}>
1341
+ <div className="flex items-center gap-3">
1342
+ {rule}
1343
+ <span style={{ flex: "none", color: "var(--dsw-label-secondary)" }}>{title}</span>
1344
+ {rule}
1345
+ </div>
1346
+ <div className="mt-1 flex flex-wrap items-center justify-center gap-x-2 text-center">
1347
+ <span>{branch ? t.branchSummaryHint : t.compactedHint}</span>
1348
+ {summary && (
1349
+ <button type="button" className="hover:underline" aria-expanded={open} onClick={() => setOpen((value) => !value)} style={{ color: "var(--dsw-label-secondary)" }}>
1350
+ {open ? t.hideSummary : t.showSummary}
1351
+ </button>
1352
+ )}
1353
+ </div>
1354
+ {open && summary && (
1355
+ <div className="mt-2 rounded-xl px-3 py-2" style={{ background: "var(--dsw-hover)", color: "var(--dsw-label-primary)" }}>
1356
+ <Markdown text={summary} />
1357
+ </div>
1358
+ )}
1359
+ </div>
1360
+ );
1361
+ }
1362
+
1314
1363
  export function SessionStatsBar({
1315
1364
  stats,
1316
1365
  }: {
@@ -1455,6 +1504,7 @@ export function ChatWindow({
1455
1504
  stats,
1456
1505
  onOpenTrajectory,
1457
1506
  onOpenFile,
1507
+ onOpenPath,
1458
1508
  roundBadges,
1459
1509
  onShowRound,
1460
1510
  }: {
@@ -1483,6 +1533,8 @@ export function ChatWindow({
1483
1533
  /** 在本机编辑器打开工具行涉及的文件 */
1484
1534
  onEditMessage?: (entryId: string, text: string) => void;
1485
1535
  onOpenFile?: (path: string) => void;
1536
+ /** 正文行内代码里的路径:用页内文件查看器打开;没给就退回 onOpenFile(本机编辑器) */
1537
+ onOpenPath?: (path: string) => void;
1486
1538
  roundBadges?: ReadonlyMap<string, RoundBadge>;
1487
1539
  onShowRound?: (commit: string) => void;
1488
1540
  }) {
@@ -1502,8 +1554,9 @@ export function ChatWindow({
1502
1554
  },
1503
1555
  }), []);
1504
1556
  useChatReadingAnchor(rootRef, scrollRef, chatScroll.shouldPin);
1505
- // 正文里可点击的文件路径(见 chat/markdown-actions):只用查看器打开当前工作区内的相对路径
1506
- const markdownActions = useMemo<MarkdownActions>(() => ({ openFile: onOpenFile }), [onOpenFile]);
1557
+ // 正文里可点击的文件路径(见 chat/markdown-actions):只认当前工作区内的相对路径,
1558
+ // 点开走页内文件查看器(不离开页面,路径不存在时查看器自己会报「文件不存在」)。
1559
+ const markdownActions = useMemo<MarkdownActions>(() => ({ openFile: onOpenPath ?? onOpenFile }), [onOpenPath, onOpenFile]);
1507
1560
  const { t } = useI18n();
1508
1561
  useEffect(() => {
1509
1562
  const root = rootRef.current;
@@ -1541,6 +1594,8 @@ export function ChatWindow({
1541
1594
  return -1;
1542
1595
  }, [messages]);
1543
1596
  const turns = useMemo(() => buildTurns(messages), [messages]);
1597
+ // 工作指示 / 重试行挂在最后一个真正的回合上,压缩卡追加在末尾时也不抢走
1598
+ const lastContentTurn = useMemo(() => turns.findLastIndex((turn) => turn.summaryIndex === undefined), [turns]);
1544
1599
  const firstUser = useMemo(() => messages.findIndex((m) => m.role === "user"), [messages]);
1545
1600
 
1546
1601
  // ---------- 回合懒渲染 ----------
@@ -1742,14 +1797,16 @@ export function ChatWindow({
1742
1797
  {turns.map((turn, ti) => (
1743
1798
  // 回合间 28px 的分隔挂在槽上:用户消息现在总是槽里的第一个子元素,它自己的 mt-7 会被 first:mt-0 吃掉
1744
1799
  <div key={turn.key} data-turn-slot={ti} ref={slotRef(ti)} className="mt-7 first:mt-0">
1745
- {isNear(ti) ? (
1800
+ {turn.summaryIndex !== undefined ? (
1801
+ <SummaryCard message={messages[turn.summaryIndex]} />
1802
+ ) : isNear(ti) ? (
1746
1803
  <TurnBlock
1747
1804
  turn={turn}
1748
1805
  messages={messages}
1749
1806
  tools={tools}
1750
1807
  cwd={cwd}
1751
1808
  isStreaming={isStreaming}
1752
- isLast={ti === turns.length - 1}
1809
+ isLast={ti === lastContentTurn}
1753
1810
  lastAssistantIndex={lastAssistantIndex}
1754
1811
  contextFiles={turn.userIndex >= 0 && turn.userIndex === firstUser ? contextFiles : undefined}
1755
1812
  retryNotice={retryNotice}
@@ -1770,7 +1827,7 @@ export function ChatWindow({
1770
1827
  </div>
1771
1828
  ))}
1772
1829
  {/* 还没有任何消息就已在流式(极少见):单独给一条工作指示 */}
1773
- {isStreaming && streamStartedAt && turns.length === 0 && (
1830
+ {isStreaming && streamStartedAt && lastContentTurn < 0 && (
1774
1831
  <div className="pw-turn">
1775
1832
  <WorkingStep startedAt={streamStartedAt} outputTokens={stats?.tokens.output} workingMessage={workingMessage} hasLiveStep={Boolean(runningTool)} />
1776
1833
  </div>
@@ -8,6 +8,24 @@ import { formatCapacity, parseCapacity } from "@/lib/model-draft";
8
8
  import { classifyMessageChars } from "@/lib/process-format";
9
9
  import type { ContextBreakdown, TrajTokens } from "@/lib/types";
10
10
 
11
+ type ContextMessage = {
12
+ role?: string;
13
+ content: Array<{ type: string; text?: string; thinking?: string; arguments?: unknown; name?: string }>;
14
+ usage?: TrajTokens;
15
+ stopReason?: string;
16
+ timestamp?: number;
17
+ summaryKind?: string;
18
+ };
19
+
20
+ /**
21
+ * 消息列表会照常显示压缩前的历史,但模型上下文只从最近一张压缩卡往后算(卡片以上只剩摘要,已计入 compacted)。
22
+ * 保留下来的旧助手消息用量是压缩前那次请求的,也不能当「最近一次输入」。
23
+ */
24
+ function inContext(messages: ContextMessage[]): { messages: ContextMessage[]; since?: number } {
25
+ const at = messages.findLastIndex((m) => m.role === "summary" && m.summaryKind === "compaction");
26
+ return at < 0 ? { messages } : { messages: messages.slice(at + 1), since: messages[at].timestamp };
27
+ }
28
+
11
29
  export function ContextMeter({ percent, tokens, contextWindow, model, source, label }: {
12
30
  percent: number | null;
13
31
  tokens: number | null;
@@ -18,7 +36,7 @@ export function ContextMeter({ percent, tokens, contextWindow, model, source, la
18
36
  label?: string;
19
37
  source?: {
20
38
  systemChars: number;
21
- messages: Array<{ role?: string; content: Array<{ type: string; text?: string; thinking?: string; arguments?: unknown; name?: string }>; usage?: TrajTokens; stopReason?: string }>;
39
+ messages: ContextMessage[];
22
40
  breakdown?: ContextBreakdown;
23
41
  loadFull?: () => Promise<unknown> | unknown;
24
42
  };
@@ -38,9 +56,11 @@ export function ContextMeter({ percent, tokens, contextWindow, model, source, la
38
56
  }, [open]);
39
57
 
40
58
  // 流式期间只在打开弹窗后扫描,分类是实际文本字符数,不是假设的 token 数。
41
- const chars = useMemo(() => classifyMessageChars(open ? source?.messages ?? [] : []), [open, source]);
42
- const lastUsage = useMemo(() => open ? source?.messages.findLast((m) => m.role === "assistant" && m.stopReason !== "error" && m.stopReason !== "aborted" && m.stopReason !== "pending" &&
43
- (m.usage?.input ?? 0) + (m.usage?.cacheRead ?? 0) + (m.usage?.cacheWrite ?? 0) + (m.usage?.output ?? 0) > 0)?.usage : undefined, [open, source]);
59
+ const context = useMemo(() => inContext(open ? source?.messages ?? [] : []), [open, source]);
60
+ const chars = useMemo(() => classifyMessageChars(context.messages), [context]);
61
+ const lastUsage = useMemo(() => context.messages.findLast((m) => m.role === "assistant" && m.stopReason !== "error" && m.stopReason !== "aborted" && m.stopReason !== "pending" &&
62
+ (context.since === undefined || (m.timestamp ?? 0) >= context.since) &&
63
+ (m.usage?.input ?? 0) + (m.usage?.cacheRead ?? 0) + (m.usage?.cacheWrite ?? 0) + (m.usage?.output ?? 0) > 0)?.usage, [context]);
44
64
  const measuredInput = lastUsage ? (lastUsage.input ?? 0) + (lastUsage.cacheRead ?? 0) + (lastUsage.cacheWrite ?? 0) : null;
45
65
 
46
66
  // 刚改过窗口:服务端已落盘,但页面上的 contextWindow 还是改之前的值(冷会话要等下一次快照)。
@@ -7,7 +7,7 @@
7
7
  import { useEffect, useMemo, useRef, useState } from "react";
8
8
  import { IconShieldOutline16 } from "@/components/icons";
9
9
  import { useI18n } from "@/i18n";
10
- import type { ExtensionDialog } from "@/hooks/usePiWeb";
10
+ import type { ExtensionDialog, PiWebNotice } from "@/hooks/usePiWeb";
11
11
 
12
12
  /** message 若全是 "key : value" 行(权限对话框的常见形态),解析成键值表渲染 */
13
13
  function parseKeyValueRows(message: string): { key: string; value: string }[] | null {
@@ -154,11 +154,17 @@ export function ExtensionNotices({
154
154
  onDismiss,
155
155
  children,
156
156
  }: {
157
- notices: { id: string; message: string; type: "info" | "warning" | "error"; ts: number }[];
157
+ notices: PiWebNotice[];
158
158
  onDismiss: (id: string) => void;
159
159
  /** 同一摞里的其它常驻提示(如更新提示条),排在扩展通知上面,免得两处浮层叠在一起 */
160
160
  children?: React.ReactNode;
161
161
  }) {
162
+ const { t } = useI18n();
163
+ const text = (n: PiWebNotice) => {
164
+ if (n.code === "external_reload") return t.externalSessionReloaded.replace("{n}", String(n.count ?? 0));
165
+ if (n.code === "external_write") return t.externalSessionWriting.replace("{n}", String(n.count ?? 0));
166
+ return n.message;
167
+ };
162
168
  // 通知 6 秒后自动消失
163
169
  useEffect(() => {
164
170
  if (!notices.length) return;
@@ -179,7 +185,7 @@ export function ExtensionNotices({
179
185
  style={{ fontSize: 12.5, background: "var(--dsw-glass-popover)", border: "0.5px solid var(--dsw-border-l2)", boxShadow: "var(--dsw-elevation-soft)", borderLeft: `3px solid ${color(n.type)}` }}
180
186
  onClick={() => onDismiss(n.id)}
181
187
  >
182
- {n.message}
188
+ {text(n)}
183
189
  </button>
184
190
  ))}
185
191
  </div>
@@ -17,6 +17,7 @@ import { IconAtOutline16, IconChevronDown14, IconChevronUp14, IconCloseOutline14
17
17
  import type { GrowthApi } from "@/hooks/useGrowth";
18
18
  import { useI18n } from "@/i18n";
19
19
  import { fileIconSrc } from "@/lib/file-icons";
20
+ import { fmtBytes } from "@/lib/format";
20
21
  import { fuzzyScore, type TreeNode } from "@/lib/growth-tree";
21
22
  import type { ViewerState } from "@/hooks/useFileViewer";
22
23
  import { highlightCode, languageForPath } from "@/lib/highlight";
@@ -113,8 +114,7 @@ export function FileViewer({
113
114
  const [treeFilter, setTreeFilter] = useState("");
114
115
  const [reveal, setReveal] = useState<{ path: string; key: number } | null>(null);
115
116
  const [loaded, setLoaded] = useState<Loaded | null>(null);
116
- const [dirPage, setDirPage] = useState<(DirPage & { key: string }) | null>(null);
117
- const [dirMore, setDirMore] = useState<DirEntry[]>([]);
117
+ const [dirPage, setDirPage] = useState<(DirPage & { key: string; error?: string }) | null>(null);
118
118
  const diffRef = useRef<FullDiffHandle>(null);
119
119
  const rootRef = useRef<HTMLDivElement>(null);
120
120
  const quickInputRef = useRef<HTMLInputElement>(null);
@@ -138,41 +138,44 @@ export function FileViewer({
138
138
  const [dirFallback, setDirFallback] = useState<string | null>(null);
139
139
  const dirPath = activeTab?.kind === "dir" ? activeTab.path : dirFallback;
140
140
  const isDir = Boolean(dirPath);
141
+ // @src/ 这类带尾斜杠的引用:列目录时去掉
142
+ const dirRel = dirPath ? dirPath.replace(/\/+$/, "") : null;
143
+ const dirKey = dirRel !== null && cwd ? `dir|${cwd}|${dirRel}` : null;
144
+ // 只认当前目录的那一页:从一个文件夹页签切到另一个时,上一个目录的条目不能在新请求回来前冒充新目录
145
+ // (点上去会拼出不存在的路径),「加载更多」也不能接着上一个目录的偏移拉
146
+ const page = dirPage && dirPage.key === dirKey ? dirPage : null;
141
147
  // 文件夹:列目录(分页,后端已按 .gitignore 过滤),不读文件内容
142
148
  useEffect(() => {
143
- if (!dirPath || !cwd || !activeTab) {
149
+ if (dirRel === null || !cwd || !dirKey) {
144
150
  setDirPage(null);
145
- setDirMore([]);
146
151
  return;
147
152
  }
148
- // @src/ 这类带尾斜杠的引用:列目录时去掉
149
- const path_ = dirPath.replace(/\/+$/, "");
150
- const key = `dir|${path_}`;
151
153
  let alive = true;
152
- setDirMore([]);
153
154
  void (async () => {
154
155
  try {
155
- const data = await requestJson<DirPage>(`/api/files?cwd=${encodeURIComponent(cwd)}&path=${encodeURIComponent(path_)}&offset=0`);
156
- if (alive) setDirPage({ key, ...data });
156
+ const data = await requestJson<DirPage>(`/api/files?cwd=${encodeURIComponent(cwd)}&path=${encodeURIComponent(dirRel)}&offset=0`);
157
+ if (alive) setDirPage({ key: dirKey, ...data });
157
158
  } catch (error) {
158
- if (alive) setDirPage({ key, dir: path_, entries: [], nextOffset: null, truncated: false, error: error instanceof Error ? error.message : t.viewerLoadFailed } as DirPage & { key: string; error?: string });
159
+ if (alive) setDirPage({ key: dirKey, dir: dirRel, entries: [], nextOffset: null, truncated: false, error: error instanceof Error ? error.message : t.viewerLoadFailed });
159
160
  }
160
161
  })();
161
162
  return () => {
162
163
  alive = false;
163
164
  };
164
- }, [activeTab, dirPath, cwd, t.viewerLoadFailed]);
165
+ }, [dirRel, dirKey, cwd, t.viewerLoadFailed]);
165
166
 
166
167
  /** 目录还有下一页时接着拉(大目录:node_modules 这种已被忽略,剩下的也不会太多) */
167
168
  const loadMore = useCallback(async () => {
168
- if (!activeTab || !cwd || !dirPage?.nextOffset) return;
169
+ if (!page?.nextOffset || dirRel === null || !cwd) return;
170
+ const base = page;
169
171
  try {
170
- const data = await requestJson<DirPage>(`/api/files?cwd=${encodeURIComponent(cwd)}&path=${encodeURIComponent(activeTab.path)}&offset=${dirPage.nextOffset}`);
171
- setDirPage({ key: `dir|${activeTab.path}`, ...data, entries: [...dirPage.entries, ...data.entries] });
172
+ const data = await requestJson<DirPage>(`/api/files?cwd=${encodeURIComponent(cwd)}&path=${encodeURIComponent(dirRel)}&offset=${base.nextOffset}`);
173
+ // 请求途中切走了(或别的请求先换了这一页)就丢掉结果
174
+ setDirPage((current) => (current === base ? { ...base, ...data, key: base.key, entries: [...base.entries, ...data.entries] } : current));
172
175
  } catch {
173
176
  // 下一页失败就留在当前这页,用户可以再点
174
177
  }
175
- }, [activeTab, cwd, dirPage]);
178
+ }, [page, dirRel, cwd]);
176
179
 
177
180
  // 取数据
178
181
  useEffect(() => {
@@ -497,10 +500,10 @@ export function FileViewer({
497
500
  {loaded?.truncated && <div className="pw-viewer-notice" data-kind="warn">{mode === "diff" ? t.viewerPatchTruncated : t.filePreviewTruncated}</div>}
498
501
  {isDir ? (
499
502
  <div className="min-h-0 flex-1 overflow-auto" data-testid="viewer-dir">
500
- <div className="px-3 py-2" style={{ fontSize: 11.5, color: "var(--dsw-label-caption)" }}>
501
- {t.viewerDirHint.replace("{n}", String(dirPage?.entries.length ?? 0))}
503
+ <div className="px-3 py-2" style={{ fontSize: 11.5, color: page?.error ? "var(--dsw-danger)" : "var(--dsw-label-caption)" }}>
504
+ {!page ? t.panelLoading : page.error ? page.error : t.viewerDirHint.replace("{n}", String(page.entries.length))}
502
505
  </div>
503
- {dirPage?.entries.map((entry) => (
506
+ {page?.entries.map((entry) => (
504
507
  <button
505
508
  key={entry.name}
506
509
  type="button"
@@ -512,11 +515,11 @@ export function FileViewer({
512
515
  <img className="pw-tree-icon" src={entry.kind === "dir" ? "/file-icons/folder-close.svg" : fileIconSrc(entry.name)} alt="" />
513
516
  <span className="min-w-0 flex-1 truncate" style={{ fontSize: 12.5, fontFamily: entry.kind === "dir" ? "inherit" : "var(--font-mono)" }}>{entry.name}</span>
514
517
  {entry.kind === "file" && entry.size !== undefined && (
515
- <span style={{ fontSize: 11, color: "var(--dsw-label-caption)", fontVariantNumeric: "tabular-nums" }}>{entry.size} B</span>
518
+ <span style={{ fontSize: 11, color: "var(--dsw-label-caption)", fontVariantNumeric: "tabular-nums" }}>{fmtBytes(entry.size)}</span>
516
519
  )}
517
520
  </button>
518
521
  ))}
519
- {dirPage?.nextOffset ? (
522
+ {page?.nextOffset ? (
520
523
  <button type="button" className="pw-chip mx-3 my-2" onClick={() => void loadMore()}>{t.viewerDirMore}</button>
521
524
  ) : null}
522
525
  </div>