@rexvane/piweb 0.3.21 → 0.3.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (343) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/app-path-routes-manifest.json +9 -6
  3. package/.next/build-manifest.json +5 -5
  4. package/.next/next-minimal-server.js.nft.json +1 -1
  5. package/.next/next-server.js.nft.json +1 -1
  6. package/.next/prerender-manifest.json +7 -7
  7. package/.next/react-loadable-manifest.json +18 -11
  8. package/.next/required-server-files.js +16 -1
  9. package/.next/required-server-files.json +16 -1
  10. package/.next/routes-manifest.json +20 -0
  11. package/.next/server/app/_global-error/page.js +2 -2
  12. package/.next/server/app/_global-error/page.js.nft.json +1 -1
  13. package/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  14. package/.next/server/app/_global-error.html +1 -1
  15. package/.next/server/app/_global-error.meta +8 -0
  16. package/.next/server/app/_global-error.rsc +15 -11
  17. package/.next/server/app/_global-error.segments/_full.segment.rsc +15 -11
  18. package/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +20 -16
  19. package/.next/server/app/_global-error.segments/_tree.segment.rsc +3 -1
  20. package/.next/server/app/_not-found/page.js +1 -1
  21. package/.next/server/app/_not-found/page.js.nft.json +1 -1
  22. package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  23. package/.next/server/app/_not-found.html +1 -1
  24. package/.next/server/app/_not-found.meta +8 -0
  25. package/.next/server/app/_not-found.rsc +17 -13
  26. package/.next/server/app/_not-found.segments/_full.segment.rsc +17 -13
  27. package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +24 -20
  28. package/.next/server/app/_not-found.segments/_tree.segment.rsc +4 -2
  29. package/.next/server/app/api/agent/[id]/events/route.js +2 -2
  30. package/.next/server/app/api/agent/[id]/events/route.js.nft.json +1 -1
  31. package/.next/server/app/api/agent/[id]/route.js +1 -1
  32. package/.next/server/app/api/agent/[id]/route.js.nft.json +1 -1
  33. package/.next/server/app/api/agent/[id]/side-chat/route.js +5 -0
  34. package/.next/server/app/api/agent/[id]/side-chat/route.js.nft.json +1 -0
  35. package/.next/server/app/api/agent/[id]/side-chat/route_client-reference-manifest.js +1 -0
  36. package/.next/server/app/api/agent/[id]/tree/route.js +1 -1
  37. package/.next/server/app/api/agent/[id]/tree/route.js.nft.json +1 -1
  38. package/.next/server/app/api/agent/new/route.js +1 -1
  39. package/.next/server/app/api/agent/new/route.js.nft.json +1 -1
  40. package/.next/server/app/api/config/route.js +1 -1
  41. package/.next/server/app/api/config/route.js.nft.json +1 -1
  42. package/.next/server/app/api/files/route.js +1 -1
  43. package/.next/server/app/api/files/route.js.nft.json +1 -1
  44. package/.next/server/app/api/fs/route.js +1 -1
  45. package/.next/server/app/api/fs/route.js.nft.json +1 -1
  46. package/.next/server/app/api/git/route.js +4 -4
  47. package/.next/server/app/api/git/route.js.nft.json +1 -1
  48. package/.next/server/app/api/github/route.js +5 -0
  49. package/.next/server/app/api/github/route.js.nft.json +1 -0
  50. package/.next/server/app/api/github/route_client-reference-manifest.js +1 -0
  51. package/.next/server/app/api/growth/route.js +1 -1
  52. package/.next/server/app/api/growth/route.js.nft.json +1 -1
  53. package/.next/server/app/api/health/route.js +1 -1
  54. package/.next/server/app/api/health/route.js.nft.json +1 -1
  55. package/.next/server/app/api/models/defaults/route.js +1 -0
  56. package/.next/server/app/api/models/defaults/route.js.nft.json +1 -0
  57. package/.next/server/app/api/models/defaults/route_client-reference-manifest.js +1 -0
  58. package/.next/server/app/api/models/route.js +1 -1
  59. package/.next/server/app/api/models/route.js.nft.json +1 -1
  60. package/.next/server/app/api/pi-settings/route.js +1 -1
  61. package/.next/server/app/api/pi-settings/route.js.nft.json +1 -1
  62. package/.next/server/app/api/plugins/route.js +1 -1
  63. package/.next/server/app/api/plugins/route.js.nft.json +1 -1
  64. package/.next/server/app/api/prompts/route.js +1 -1
  65. package/.next/server/app/api/prompts/route.js.nft.json +1 -1
  66. package/.next/server/app/api/remote/route.js +4 -4
  67. package/.next/server/app/api/remote/route.js.nft.json +1 -1
  68. package/.next/server/app/api/security/route.js +1 -1
  69. package/.next/server/app/api/security/route.js.nft.json +1 -1
  70. package/.next/server/app/api/sessions/[id]/export/route.js +1 -1
  71. package/.next/server/app/api/sessions/[id]/export/route.js.nft.json +1 -1
  72. package/.next/server/app/api/sessions/[id]/route.js +1 -1
  73. package/.next/server/app/api/sessions/[id]/route.js.nft.json +1 -1
  74. package/.next/server/app/api/sessions/import/route.js +2 -2
  75. package/.next/server/app/api/sessions/import/route.js.nft.json +1 -1
  76. package/.next/server/app/api/sessions/route.js +1 -1
  77. package/.next/server/app/api/sessions/route.js.nft.json +1 -1
  78. package/.next/server/app/api/skills/route.js +2 -2
  79. package/.next/server/app/api/skills/route.js.nft.json +1 -1
  80. package/.next/server/app/api/update/route.js +2 -2
  81. package/.next/server/app/api/update/route.js.nft.json +1 -1
  82. package/.next/server/app/api/version/route.js +1 -1
  83. package/.next/server/app/api/version/route.js.nft.json +1 -1
  84. package/.next/server/app/api/web-auth/route.js +2 -2
  85. package/.next/server/app/api/web-auth/route.js.nft.json +1 -1
  86. package/.next/server/app/api/workspaces/route.js +1 -1
  87. package/.next/server/app/api/workspaces/route.js.nft.json +1 -1
  88. package/.next/server/app/icon.svg/route.js +1 -1
  89. package/.next/server/app/icon.svg/route.js.nft.json +1 -1
  90. package/.next/server/app/icon.svg.meta +1 -1
  91. package/.next/server/app/index.html +1 -1
  92. package/.next/server/app/index.meta +8 -0
  93. package/.next/server/app/index.rsc +20 -13
  94. package/.next/server/app/index.segments/__PAGE__.segment.rsc +26 -19
  95. package/.next/server/app/index.segments/_full.segment.rsc +20 -13
  96. package/.next/server/app/index.segments/_tree.segment.rsc +4 -2
  97. package/.next/server/app/login/page.js +1 -1
  98. package/.next/server/app/login/page.js.nft.json +1 -1
  99. package/.next/server/app/login/page_client-reference-manifest.js +1 -1
  100. package/.next/server/app/login.html +1 -1
  101. package/.next/server/app/login.meta +8 -0
  102. package/.next/server/app/login.rsc +23 -17
  103. package/.next/server/app/login.segments/_full.segment.rsc +23 -17
  104. package/.next/server/app/login.segments/_tree.segment.rsc +4 -2
  105. package/.next/server/app/login.segments/login/__PAGE__.segment.rsc +30 -24
  106. package/.next/server/app/page.js +1 -1
  107. package/.next/server/app/page.js.nft.json +1 -1
  108. package/.next/server/app/page_client-reference-manifest.js +1 -1
  109. package/.next/server/app-paths-manifest.json +9 -6
  110. package/.next/server/chunks/1813.js +23 -0
  111. package/.next/server/chunks/2430.js +377 -0
  112. package/.next/server/chunks/2970.js +305 -0
  113. package/.next/server/chunks/3421.js +1 -0
  114. package/.next/server/chunks/3924.js +6 -0
  115. package/.next/server/chunks/4807.js +1 -0
  116. package/.next/server/chunks/538.js +1 -1
  117. package/.next/server/chunks/5393.js +1 -0
  118. package/.next/server/chunks/5802.js +2 -0
  119. package/.next/server/chunks/6229.js +1 -0
  120. package/.next/server/chunks/7496.js +1 -0
  121. package/.next/server/chunks/7662.js +2 -0
  122. package/.next/server/chunks/8686.js +1 -0
  123. package/.next/server/chunks/{593.js → 974.js} +1 -1
  124. package/.next/server/chunks/{920.js → 9920.js} +2 -2
  125. package/.next/server/functions-config-manifest.json +2 -1
  126. package/.next/server/instrumentation.js +1 -1
  127. package/.next/server/instrumentation.js.nft.json +1 -1
  128. package/.next/server/middleware-build-manifest.js +1 -1
  129. package/.next/server/middleware-react-loadable-manifest.js +1 -1
  130. package/.next/server/middleware.js +185 -23
  131. package/.next/server/middleware.js.nft.json +1 -1
  132. package/.next/server/pages/404.html +1 -1
  133. package/.next/server/pages/500.html +1 -1
  134. package/.next/server/preview-props.json +5 -0
  135. package/.next/server/server-reference-manifest.json +1 -1
  136. package/.next/server/webpack-runtime.js +1 -1
  137. package/.next/static/chunks/2716.44fdf1b21454b961.js +1 -0
  138. package/.next/static/chunks/316-698d10eb465bd8ef.js +1 -0
  139. package/.next/static/chunks/3794-0978865cda773008.js +52 -0
  140. package/.next/static/chunks/4673.e261707dce391be7.js +1 -0
  141. package/.next/static/chunks/4686-0cd87513f9ea3538.js +1 -0
  142. package/.next/static/chunks/4778.eddd3a5813f85824.js +1 -0
  143. package/.next/static/chunks/4bd1b696-e199e788e58acf79.js +1 -0
  144. package/.next/static/chunks/{5173.91ddb803785009f8.js → 5173.dfd1cbb795b9a21c.js} +1 -1
  145. package/.next/static/chunks/618.949981da99be7182.js +4 -0
  146. package/.next/static/chunks/7411.5f79671208209b13.js +1 -0
  147. package/.next/static/chunks/app/_global-error/page-cbaaee0d7aacb85a.js +1 -0
  148. package/.next/static/chunks/app/api/agent/[id]/events/route-cbaaee0d7aacb85a.js +1 -0
  149. package/.next/static/chunks/app/api/agent/[id]/route-cbaaee0d7aacb85a.js +1 -0
  150. package/.next/static/chunks/app/api/agent/[id]/side-chat/route-cbaaee0d7aacb85a.js +1 -0
  151. package/.next/static/chunks/app/api/agent/[id]/tree/route-cbaaee0d7aacb85a.js +1 -0
  152. package/.next/static/chunks/app/api/agent/new/route-cbaaee0d7aacb85a.js +1 -0
  153. package/.next/static/chunks/app/api/config/route-cbaaee0d7aacb85a.js +1 -0
  154. package/.next/static/chunks/app/api/files/route-cbaaee0d7aacb85a.js +1 -0
  155. package/.next/static/chunks/app/api/fs/route-cbaaee0d7aacb85a.js +1 -0
  156. package/.next/static/chunks/app/api/git/route-cbaaee0d7aacb85a.js +1 -0
  157. package/.next/static/chunks/app/api/github/route-cbaaee0d7aacb85a.js +1 -0
  158. package/.next/static/chunks/app/api/growth/route-cbaaee0d7aacb85a.js +1 -0
  159. package/.next/static/chunks/app/api/health/route-cbaaee0d7aacb85a.js +1 -0
  160. package/.next/static/chunks/app/api/models/defaults/route-cbaaee0d7aacb85a.js +1 -0
  161. package/.next/static/chunks/app/api/models/route-cbaaee0d7aacb85a.js +1 -0
  162. package/.next/static/chunks/app/api/pi-settings/route-cbaaee0d7aacb85a.js +1 -0
  163. package/.next/static/chunks/app/api/plugins/route-cbaaee0d7aacb85a.js +1 -0
  164. package/.next/static/chunks/app/api/prompts/route-cbaaee0d7aacb85a.js +1 -0
  165. package/.next/static/chunks/app/api/remote/route-cbaaee0d7aacb85a.js +1 -0
  166. package/.next/static/chunks/app/api/security/route-cbaaee0d7aacb85a.js +1 -0
  167. package/.next/static/chunks/app/api/sessions/[id]/export/route-cbaaee0d7aacb85a.js +1 -0
  168. package/.next/static/chunks/app/api/sessions/[id]/route-cbaaee0d7aacb85a.js +1 -0
  169. package/.next/static/chunks/app/api/sessions/import/route-cbaaee0d7aacb85a.js +1 -0
  170. package/.next/static/chunks/app/api/sessions/route-cbaaee0d7aacb85a.js +1 -0
  171. package/.next/static/chunks/app/api/skills/route-cbaaee0d7aacb85a.js +1 -0
  172. package/.next/static/chunks/app/api/update/route-cbaaee0d7aacb85a.js +1 -0
  173. package/.next/static/chunks/app/api/version/route-cbaaee0d7aacb85a.js +1 -0
  174. package/.next/static/chunks/app/api/web-auth/route-cbaaee0d7aacb85a.js +1 -0
  175. package/.next/static/chunks/app/api/workspaces/route-cbaaee0d7aacb85a.js +1 -0
  176. package/.next/static/chunks/app/page-dc3cb41d91c7432e.js +1 -0
  177. package/.next/static/chunks/framework-47b723d27f4a4963.js +1 -0
  178. package/.next/static/chunks/main-5403f0764579e5fe.js +5 -0
  179. package/.next/static/chunks/next/dist/client/components/builtin/app-error-cbaaee0d7aacb85a.js +1 -0
  180. package/.next/static/chunks/next/dist/client/components/builtin/forbidden-cbaaee0d7aacb85a.js +1 -0
  181. package/.next/static/chunks/next/dist/client/components/builtin/not-found-cbaaee0d7aacb85a.js +1 -0
  182. package/.next/static/chunks/next/dist/client/components/builtin/unauthorized-cbaaee0d7aacb85a.js +1 -0
  183. package/.next/static/chunks/webpack-2ac3d76b81895f5b.js +1 -0
  184. package/.next/static/css/9fc559c2c4ad1d0f.css +1 -0
  185. package/.next/static/css/c6e2227c36ec2fd6.css +1 -0
  186. package/.next/static/css/{2959bd6b626b7ffe.css → f15a853af8b5127f.css} +1 -1
  187. package/.next/static/hYPL1-dmhKFjLS3f5yoqN/_buildManifest.js +1 -0
  188. package/.next/types/app/api/agent/[id]/events/route.ts +3 -1
  189. package/.next/types/app/api/agent/[id]/route.ts +3 -1
  190. package/.next/types/app/api/agent/[id]/side-chat/route.ts +355 -0
  191. package/.next/types/app/api/agent/[id]/tree/route.ts +3 -1
  192. package/.next/types/app/api/agent/new/route.ts +3 -1
  193. package/.next/types/app/api/config/route.ts +3 -1
  194. package/.next/types/app/api/files/route.ts +3 -1
  195. package/.next/types/app/api/fs/route.ts +3 -1
  196. package/.next/types/app/api/git/route.ts +3 -1
  197. package/.next/types/app/api/github/route.ts +355 -0
  198. package/.next/types/app/api/growth/route.ts +3 -1
  199. package/.next/types/app/api/health/route.ts +3 -1
  200. package/.next/types/app/api/models/defaults/route.ts +355 -0
  201. package/.next/types/app/api/models/route.ts +3 -1
  202. package/.next/types/app/api/pi-settings/route.ts +3 -1
  203. package/.next/types/app/api/plugins/route.ts +3 -1
  204. package/.next/types/app/api/prompts/route.ts +3 -1
  205. package/.next/types/app/api/remote/route.ts +3 -1
  206. package/.next/types/app/api/security/route.ts +3 -1
  207. package/.next/types/app/api/sessions/[id]/export/route.ts +3 -1
  208. package/.next/types/app/api/sessions/[id]/route.ts +3 -1
  209. package/.next/types/app/api/sessions/import/route.ts +3 -1
  210. package/.next/types/app/api/sessions/route.ts +3 -1
  211. package/.next/types/app/api/skills/route.ts +3 -1
  212. package/.next/types/app/api/update/route.ts +3 -1
  213. package/.next/types/app/api/version/route.ts +3 -1
  214. package/.next/types/app/api/web-auth/route.ts +3 -1
  215. package/.next/types/app/api/workspaces/route.ts +3 -1
  216. package/.next/types/app/layout.ts +4 -1
  217. package/.next/types/app/login/page.ts +4 -1
  218. package/.next/types/app/page.ts +4 -1
  219. package/.next/types/cache-life.d.ts +11 -43
  220. package/.next/types/routes.d.ts +4 -1
  221. package/.next/types/validator.ts +86 -2
  222. package/next.config.ts +2 -0
  223. package/package.json +8 -2
  224. package/scripts/release.mjs +3 -7
  225. package/scripts/runtime/check-installed.mjs +66 -0
  226. package/scripts/runtime/pi-packages.mjs +51 -0
  227. package/scripts/verify-package.mjs +29 -7
  228. package/src/app/api/agent/[id]/side-chat/route.ts +77 -0
  229. package/src/app/api/files/route.ts +5 -2
  230. package/src/app/api/github/route.ts +26 -0
  231. package/src/app/api/models/defaults/route.ts +18 -0
  232. package/src/app/api/models/route.ts +24 -1
  233. package/src/app/globals.css +1748 -0
  234. package/src/components/AppShell.tsx +145 -43
  235. package/src/components/ChatInput.tsx +264 -79
  236. package/src/components/ChatWindow.tsx +13 -23
  237. package/src/components/ContextMeter.tsx +213 -144
  238. package/src/components/DrawerPanel.tsx +1 -1
  239. package/src/components/FileViewer.tsx +89 -8
  240. package/src/components/GitPanel.tsx +110 -40
  241. package/src/components/GrowthTree.tsx +30 -5
  242. package/src/components/ModelSelector.tsx +21 -5
  243. package/src/components/ProjectPanel.tsx +55 -30
  244. package/src/components/PromptPanel.tsx +2 -4
  245. package/src/components/ProviderSetupModal.module.css +20 -0
  246. package/src/components/ProviderSetupModal.tsx +134 -29
  247. package/src/components/SettingsPanel.tsx +252 -27
  248. package/src/components/TrajectoryView.tsx +4 -6
  249. package/src/components/chat/reading-anchor.ts +81 -0
  250. package/src/components/composer/ComposerImages.tsx +326 -0
  251. package/src/components/side-chat/SideChatPanel.module.css +131 -0
  252. package/src/components/side-chat/SideChatPanel.tsx +189 -0
  253. package/src/components/side-chat/icons.tsx +8 -0
  254. package/src/components/side-chat/request-history.ts +29 -0
  255. package/src/components/side-chat/store.ts +248 -0
  256. package/src/hooks/useFileViewer.ts +6 -4
  257. package/src/hooks/useGrowth.ts +3 -0
  258. package/src/hooks/usePiWeb.ts +12 -2
  259. package/src/hooks/useWorkspaceFiles.ts +93 -0
  260. package/src/i18n.tsx +150 -2
  261. package/src/lib/agent-manager.ts +135 -104
  262. package/src/lib/files-service.ts +166 -3
  263. package/src/lib/format.ts +56 -0
  264. package/src/lib/git-service.ts +45 -13
  265. package/src/lib/github-identity.ts +245 -0
  266. package/src/lib/growth-service.ts +6 -2
  267. package/src/lib/hero-model-memory.ts +28 -0
  268. package/src/lib/model-defaults.ts +105 -0
  269. package/src/lib/model-draft.ts +257 -9
  270. package/src/lib/model-endpoint.ts +23 -0
  271. package/src/lib/model-files.ts +33 -0
  272. package/src/lib/models-service.ts +240 -78
  273. package/src/lib/path-security.ts +24 -32
  274. package/src/lib/pi.ts +27 -2
  275. package/src/lib/process-format.ts +3 -2
  276. package/src/lib/session/metrics.ts +31 -0
  277. package/src/lib/session-reader.ts +1 -1
  278. package/src/lib/side-chat/context.ts +123 -0
  279. package/src/lib/side-chat/service.ts +191 -0
  280. package/src/lib/side-chat/types.ts +61 -0
  281. package/src/lib/thinking-level.ts +39 -0
  282. package/src/lib/trajectory.ts +17 -3
  283. package/src/lib/types.ts +12 -1
  284. package/src/lib/update-service.ts +4 -6
  285. package/src/lib/workspace-index.ts +270 -0
  286. package/.next/server/chunks/229.js +0 -1
  287. package/.next/server/chunks/393.js +0 -1
  288. package/.next/server/chunks/421.js +0 -1
  289. package/.next/server/chunks/430.js +0 -193
  290. package/.next/server/chunks/496.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/741.js +0 -165
  294. package/.next/server/chunks/802.js +0 -2
  295. package/.next/server/chunks/807.js +0 -1
  296. package/.next/server/chunks/813.js +0 -1
  297. package/.next/server/chunks/924.js +0 -6
  298. package/.next/static/chunks/2716.7ad073038e94a802.js +0 -1
  299. package/.next/static/chunks/316-5ea443ee6b512aa9.js +0 -1
  300. package/.next/static/chunks/3794-3679f1c05160977c.js +0 -52
  301. package/.next/static/chunks/4673.8a5a9e6855350e34.js +0 -1
  302. package/.next/static/chunks/4686-882531aef1d2e250.js +0 -1
  303. package/.next/static/chunks/4778.305dc6abb9889627.js +0 -1
  304. package/.next/static/chunks/4bd1b696-93bf506b236d6248.js +0 -1
  305. package/.next/static/chunks/618.ee5d5bf1c647e483.js +0 -4
  306. package/.next/static/chunks/839.bfd2db36483faa3e.js +0 -1
  307. package/.next/static/chunks/app/_global-error/page-b179d9f7f6f2daf9.js +0 -1
  308. package/.next/static/chunks/app/api/agent/[id]/events/route-b179d9f7f6f2daf9.js +0 -1
  309. package/.next/static/chunks/app/api/agent/[id]/route-b179d9f7f6f2daf9.js +0 -1
  310. package/.next/static/chunks/app/api/agent/[id]/tree/route-b179d9f7f6f2daf9.js +0 -1
  311. package/.next/static/chunks/app/api/agent/new/route-b179d9f7f6f2daf9.js +0 -1
  312. package/.next/static/chunks/app/api/config/route-b179d9f7f6f2daf9.js +0 -1
  313. package/.next/static/chunks/app/api/files/route-b179d9f7f6f2daf9.js +0 -1
  314. package/.next/static/chunks/app/api/fs/route-b179d9f7f6f2daf9.js +0 -1
  315. package/.next/static/chunks/app/api/git/route-b179d9f7f6f2daf9.js +0 -1
  316. package/.next/static/chunks/app/api/growth/route-b179d9f7f6f2daf9.js +0 -1
  317. package/.next/static/chunks/app/api/health/route-b179d9f7f6f2daf9.js +0 -1
  318. package/.next/static/chunks/app/api/models/route-b179d9f7f6f2daf9.js +0 -1
  319. package/.next/static/chunks/app/api/pi-settings/route-b179d9f7f6f2daf9.js +0 -1
  320. package/.next/static/chunks/app/api/plugins/route-b179d9f7f6f2daf9.js +0 -1
  321. package/.next/static/chunks/app/api/prompts/route-b179d9f7f6f2daf9.js +0 -1
  322. package/.next/static/chunks/app/api/remote/route-b179d9f7f6f2daf9.js +0 -1
  323. package/.next/static/chunks/app/api/security/route-b179d9f7f6f2daf9.js +0 -1
  324. package/.next/static/chunks/app/api/sessions/[id]/export/route-b179d9f7f6f2daf9.js +0 -1
  325. package/.next/static/chunks/app/api/sessions/[id]/route-b179d9f7f6f2daf9.js +0 -1
  326. package/.next/static/chunks/app/api/sessions/import/route-b179d9f7f6f2daf9.js +0 -1
  327. package/.next/static/chunks/app/api/sessions/route-b179d9f7f6f2daf9.js +0 -1
  328. package/.next/static/chunks/app/api/skills/route-b179d9f7f6f2daf9.js +0 -1
  329. package/.next/static/chunks/app/api/update/route-b179d9f7f6f2daf9.js +0 -1
  330. package/.next/static/chunks/app/api/version/route-b179d9f7f6f2daf9.js +0 -1
  331. package/.next/static/chunks/app/api/web-auth/route-b179d9f7f6f2daf9.js +0 -1
  332. package/.next/static/chunks/app/api/workspaces/route-b179d9f7f6f2daf9.js +0 -1
  333. package/.next/static/chunks/app/page-c04af72e549695bd.js +0 -1
  334. package/.next/static/chunks/framework-c4e651fe89edfa91.js +0 -1
  335. package/.next/static/chunks/main-6159b22a8cb0b7c5.js +0 -5
  336. package/.next/static/chunks/next/dist/client/components/builtin/app-error-b179d9f7f6f2daf9.js +0 -1
  337. package/.next/static/chunks/next/dist/client/components/builtin/forbidden-b179d9f7f6f2daf9.js +0 -1
  338. package/.next/static/chunks/next/dist/client/components/builtin/not-found-b179d9f7f6f2daf9.js +0 -1
  339. package/.next/static/chunks/next/dist/client/components/builtin/unauthorized-b179d9f7f6f2daf9.js +0 -1
  340. package/.next/static/chunks/webpack-9b4bb574e5817654.js +0 -1
  341. package/.next/static/css/ea27b9e30f76303f.css +0 -1
  342. package/.next/static/sNX5GQkOWuK5YEID1hT-C/_buildManifest.js +0 -1
  343. /package/.next/static/{sNX5GQkOWuK5YEID1hT-C → hYPL1-dmhKFjLS3f5yoqN}/_ssgManifest.js +0 -0
@@ -1,48 +1,25 @@
1
1
  "use client";
2
2
 
3
- /**
4
- * 上下文占用:
5
- * 圆环常驻按钮 + 弹窗显示「百分比 / ~已用 / 窗口」与 13 类分段占用条
6
- * (mac 磁盘存储条式的多色分段,含 Free Space 段)。
7
- * 分段口径:全部为字符/4 启发式估算(与 SDK estimateTokens 一致),
8
- * 服务端分项(System Prompt / Tools / Memory / Skills / Compacted / Buffer)来自快照,
9
- * 消息分项(User / Agent Text / Thinking / Tool Call / Tool Output)弹窗打开时前端统计。
10
- * 冷会话(只浏览历史、agent 没启动)拿不到组装后的 System Prompt 和工具定义:
11
- * 其余分项照常显示,另给「现在统计」按需启动会话,分项随后经 context_breakdown 事件刷新。
12
- * 无对话时不渲染(由父组件经 contextVisible 控制)。
13
- */
3
+ /** 上下文总量是 SDK 估算;API 输入用量与内容字符分类分别展示,不能按比例分摊 token。 */
14
4
  import { useEffect, useMemo, useRef, useState } from "react";
15
5
  import { useI18n } from "@/i18n";
6
+ import { fmtPercent, fmtTokens } from "@/lib/format";
7
+ import { formatCapacity, parseCapacity } from "@/lib/model-draft";
16
8
  import { classifyMessageChars } from "@/lib/process-format";
17
- import type { ContextBreakdown } from "@/lib/types";
18
-
19
- function fmtTok(n: number): string {
20
- return n >= 1000 ? `${(n / 1000).toFixed(1)}K` : String(Math.round(n));
21
- }
9
+ import type { ContextBreakdown, TrajTokens } from "@/lib/types";
22
10
 
23
- /** 13 类分段的定义(顺序即渲染顺序) */
24
- interface Segment {
25
- key: string;
26
- label: string;
27
- tokens: number;
28
- color: string;
29
- }
30
-
31
- export function ContextMeter({
32
- percent,
33
- tokens,
34
- contextWindow,
35
- source,
36
- }: {
11
+ export function ContextMeter({ percent, tokens, contextWindow, model, source, label }: {
37
12
  percent: number | null;
38
13
  tokens: number | null;
39
14
  contextWindow: number | null;
40
- /** 分段数据源:消息字符统计只在弹窗打开时执行(流式期间消息每 token 都变,不能常驻扫描) */
15
+ /** 当前会话的模型:有它才能在弹窗里改上下文窗口 */
16
+ model?: { provider: string; id: string } | null;
17
+ /** 覆盖「上下文估算」这个措辞:真实用量(如旁聊)不该叫估算 */
18
+ label?: string;
41
19
  source?: {
42
20
  systemChars: number;
43
- messages: { role?: string; content: Array<{ type: string; text?: string; thinking?: string; arguments?: unknown }> }[];
21
+ messages: Array<{ role?: string; content: Array<{ type: string; text?: string; thinking?: string; arguments?: unknown; name?: string }>; usage?: TrajTokens; stopReason?: string }>;
44
22
  breakdown?: ContextBreakdown;
45
- /** 冷会话里点「现在统计」:启动会话(prepare),System Prompt 与工具定义才算得出来 */
46
23
  loadFull?: () => Promise<unknown> | unknown;
47
24
  };
48
25
  }) {
@@ -52,154 +29,246 @@ export function ContextMeter({
52
29
  const { t } = useI18n();
53
30
  const cold = Boolean(source?.breakdown?.cold);
54
31
 
55
- // 新分项到了(不再是冷的)就收起「统计中」
56
- useEffect(() => {
57
- if (!cold) setCounting(false);
58
- }, [cold]);
59
-
32
+ useEffect(() => { if (!cold) setCounting(false); }, [cold]);
60
33
  useEffect(() => {
61
34
  if (!open) return;
62
- const h = (e: MouseEvent) => {
63
- if (!ref.current?.contains(e.target as Node)) setOpen(false);
64
- };
35
+ const h = (e: MouseEvent) => { if (!ref.current?.contains(e.target as Node)) setOpen(false); };
65
36
  document.addEventListener("mousedown", h);
66
37
  return () => document.removeEventListener("mousedown", h);
67
38
  }, [open]);
68
39
 
69
- const used = tokens ?? 0;
70
- const window_ = contextWindow ?? 0;
71
- const b = source?.breakdown;
40
+ // 流式期间只在打开弹窗后扫描,分类是实际文本字符数,不是假设的 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]);
44
+ const measuredInput = lastUsage ? (lastUsage.input ?? 0) + (lastUsage.cacheRead ?? 0) + (lastUsage.cacheWrite ?? 0) : null;
72
45
 
73
- // 消息分项只在弹窗打开时统计(≈4 字符/token,与 SDK 一致)
74
- const messageSegments = useMemo(() => {
75
- if (!open) return { user: 0, agentText: 0, agentThinking: 0, agentToolCall: 0, toolOutput: 0 };
76
- const chars = classifyMessageChars(source?.messages ?? []);
77
- return {
78
- user: Math.max(0, Math.round(chars.user / 4)),
79
- agentText: Math.max(0, Math.round(chars.agentText / 4)),
80
- agentThinking: Math.max(0, Math.round(chars.agentThinking / 4)),
81
- agentToolCall: Math.max(0, Math.round(chars.agentToolCall / 4)),
82
- toolOutput: Math.max(0, Math.round(chars.toolOutput / 4)),
83
- };
84
- }, [open, source]);
46
+ // 刚改过窗口:服务端已落盘,但页面上的 contextWindow 还是改之前的值(冷会话要等下一次快照)。
47
+ // 在它变之前先显示刚保存的值;contextWindow 一变(快照 / 用量追上来,或别处改了)就以 props 为准。
48
+ const modelKey = model ? `${model.provider}/${model.id}` : "";
49
+ const [applied, setApplied] = useState<{ key: string; from: number; value: number } | null>(null);
50
+ useEffect(() => {
51
+ if (applied && (applied.key !== modelKey || contextWindow !== applied.from)) setApplied(null);
52
+ }, [applied, modelKey, contextWindow]);
53
+ const override = applied && applied.key === modelKey && contextWindow === applied.from ? applied.value : null;
85
54
 
86
- const p = percent ?? 0;
87
- const usageLabel = percent === null ? t.contextUnknown : `${Math.round(p)}% ${t.contextUsed}`;
55
+ const known = tokens !== null && Number.isFinite(tokens) && tokens >= 0;
56
+ const used = known ? tokens : 0;
57
+ const window_ = override ?? contextWindow ?? 0;
58
+ const recomputed = override !== null && known;
59
+ const p = recomputed ? used / override * 100 : percent ?? 0;
60
+ /** 圆环与悬停提示仍用 SDK 估算(那是「占窗口多少」),弹窗正文只留 API 出来的真实数字 */
61
+ const usageLabel = percent === null && !recomputed ? t.contextUnknown : `${fmtPercent(p)} ${label ?? t.contextEstimated}`;
88
62
  const r = 5.5;
89
- const c = 2 * Math.PI * r;
90
- const shown = Math.max(0, Math.min(100, p));
91
-
92
- const segments: Segment[] = b
93
- ? [
94
- { key: "systemPrompt", label: t.contextSystemPrompt, tokens: b.systemPrompt, color: "var(--dsw-accent)" },
95
- { key: "systemTools", label: t.contextSystemTools, tokens: b.systemTools, color: "#8B7EC8" },
96
- { key: "customTools", label: t.contextCustomTools, tokens: b.customTools, color: "#B39DDB" },
97
- { key: "memory", label: t.contextMemory, tokens: b.memory, color: "#5FA8D3" },
98
- { key: "skills", label: t.contextSkills, tokens: b.skills, color: "#4FB286" },
99
- { key: "user", label: t.contextUserMessages, tokens: messageSegments.user, color: "var(--dsw-success)" },
100
- { key: "agentText", label: t.contextAgentText, tokens: messageSegments.agentText, color: "#7BC96F" },
101
- { key: "agentThinking", label: t.contextAgentThinking, tokens: messageSegments.agentThinking, color: "#C9A66B" },
102
- { key: "agentToolCall", label: t.contextAgentToolCall, tokens: messageSegments.agentToolCall, color: "#D3A15F" },
103
- { key: "toolOutput", label: t.contextToolOutput, tokens: messageSegments.toolOutput, color: "var(--dsw-warn)" },
104
- { key: "compacted", label: t.contextCompacted, tokens: b.compacted, color: "#A05A78" },
105
- { key: "buffer", label: t.contextAutoCompactBuffer, tokens: b.autoCompactBuffer, color: "var(--dsw-border-l3)" },
106
- ]
107
- : // 服务端分项缺失(旧快照):退回旧三段口径
108
- [
109
- { key: "system", label: t.contextSystem, tokens: Math.max(0, Math.round((source?.systemChars ?? 0) / 4)), color: "var(--dsw-accent)" },
110
- { key: "messages", label: t.contextMessages, tokens: messageSegments.user + messageSegments.agentText + messageSegments.agentThinking, color: "var(--dsw-success)" },
111
- { key: "tools", label: t.contextTools, tokens: Math.max(0, used - Math.max(0, Math.round((source?.systemChars ?? 0) / 4)) - messageSegments.user - messageSegments.agentText - messageSegments.agentThinking), color: "var(--dsw-warn)" },
112
- ];
113
-
114
- // Free Space:窗口余量(窗口未知则不显示);分段条不归一化到已用之和
115
- const usedSum = segments.reduce((sum, s) => sum + s.tokens, 0);
116
- const free = window_ > 0 ? Math.max(0, window_ - usedSum) : 0;
117
- const barTotal = window_ > 0 ? window_ : usedSum || 1;
118
- const bar = (n: number) => `${(n / barTotal) * 100}%`;
63
+ const circumference = 2 * Math.PI * r;
64
+ const reserved = Math.max(0, source?.breakdown?.autoCompactBuffer ?? 0);
65
+ /**
66
+ * 进度条与 Free Space 的「已用」基准:优先最近一次请求的真实输入(API 报的),
67
+ * 没测到才退回 SDK 估算;两者都没有就不画(未知不能当空闲)。
68
+ */
69
+ const anchor = window_ > 0 ? (measuredInput !== null ? Math.min(measuredInput, window_) : known ? Math.min(used, window_) : null) : null;
70
+ // 超过窗口时预留不再画到窗口外;用量未知不能将整扇窗口显示为 Free Space。
71
+ const visibleReserved = anchor === null ? 0 : Math.min(reserved, Math.max(0, window_ - anchor));
72
+ const free = anchor === null ? null : Math.max(0, window_ - anchor - reserved);
73
+ const b = source?.breakdown?.characters;
74
+ const rows = [
75
+ { key: "system", label: t.contextSystemPrompt, value: b?.systemPrompt, color: "var(--dsw-accent)" },
76
+ { key: "builtin", label: t.contextSystemTools, value: b?.systemTools, color: "#8B7EC8" },
77
+ { key: "custom", label: t.contextCustomTools, value: b?.customTools, color: "#B39DDB" },
78
+ { key: "background", label: t.contextBackground, value: b?.background, color: "#5B8DEF" },
79
+ { key: "memory", label: t.contextMemory, value: b?.memory, color: "#5FA8D3" },
80
+ { key: "skills", label: t.contextSkills, value: b?.skills, color: "#4FB286" },
81
+ { key: "user", label: t.contextUserMessages, value: chars.user, color: "var(--dsw-success)" },
82
+ { key: "text", label: t.contextAgentText, value: chars.agentText, color: "#7BC96F" },
83
+ { key: "thinking", label: t.contextAgentThinking, value: chars.agentThinking, color: "#C9A66B" },
84
+ { key: "call", label: t.contextAgentToolCall, value: chars.agentToolCall, color: "#D3A15F" },
85
+ { key: "output", label: t.contextToolOutput, value: chars.toolOutput, color: "var(--dsw-warn)" },
86
+ { key: "compacted", label: t.contextCompacted, value: b?.compacted, color: "#A05A78" },
87
+ ].filter((row) => row.value !== undefined && row.value > 0);
88
+ const charTotal = rows.reduce((sum, row) => sum + (row.value ?? 0), 0);
89
+ /**
90
+ * 进度条整条 = 全部上下文窗口:已用那一截按各类**字符数比例**分段(API 不给分类 token,
91
+ * 所以只摊比例、不报每类的 token 数),剩下的部分就是背景色的 Free Space。
92
+ */
93
+ const segments = anchor === null || charTotal === 0 ? [] : rows.map((row) => ({
94
+ key: row.key,
95
+ label: row.label,
96
+ value: row.value as number,
97
+ color: row.color,
98
+ width: anchor / window_ * 100 * ((row.value as number) / charTotal),
99
+ }));
119
100
 
120
101
  return (
121
102
  <div ref={ref} className="relative">
122
- <button
123
- className="icon-btn"
124
- style={{ width: 28, height: 28 }}
125
- title={usageLabel}
126
- onClick={() => setOpen((o) => !o)}
127
- >
103
+ <button className="icon-btn" style={{ width: 28, height: 28 }} title={usageLabel} aria-label={usageLabel} onClick={() => setOpen((o) => !o)}>
128
104
  <svg width="14" height="14" viewBox="0 0 14 14">
129
105
  <circle cx="7" cy="7" r={r} fill="none" stroke="var(--dsw-border-l3)" strokeWidth="2" />
130
- <circle
131
- cx="7" cy="7" r={r}
132
- fill="none"
133
- stroke={p != null && p > 85 ? "var(--dsw-warn)" : "var(--dsw-accent)"}
134
- strokeWidth="2"
135
- strokeDasharray={`${(c * shown) / 100} ${c}`}
136
- transform="rotate(-90 7 7)"
137
- strokeLinecap="round"
138
- />
106
+ <circle cx="7" cy="7" r={r} fill="none" stroke={p > 85 ? "var(--dsw-warn)" : "var(--dsw-accent)"} strokeWidth="2"
107
+ strokeDasharray={`${circumference * Math.max(0, Math.min(100, p)) / 100} ${circumference}`} transform="rotate(-90 7 7)" strokeLinecap="round" />
139
108
  </svg>
140
109
  </button>
141
110
  {open && (
142
- <div
143
- className="popover absolute bottom-9 right-0 z-50 w-64 p-4"
144
- style={{ fontSize: "var(--dsh-content-font-size-secondary)" }}
145
- >
146
- <div className="font-medium" style={{ color: "var(--dsw-label-primary)" }}>
147
- {usageLabel}
148
- </div>
149
- <div style={{ color: "var(--dsw-label-tertiary)" }} className="mt-1">
150
- {tokens === null ? "—" : `~${fmtTok(used)}`} / {window_ > 0 ? fmtTok(window_) : "—"}
151
- </div>
152
-
153
- {/* 分段占用条(mac 磁盘存储条式多色分段,含 Free Space) */}
154
- {usedSum > 0 && (
155
- <div className="mt-3 flex h-2 w-full overflow-hidden rounded-full" style={{ background: "var(--dsw-selector)" }} aria-hidden>
156
- {segments.filter((s) => s.tokens > 0).map((s) => (
157
- <span key={s.key} style={{ width: bar(s.tokens), background: s.color }} />
111
+ <div className="popover absolute bottom-9 right-0 z-50 w-72 p-4" style={{ fontSize: "var(--dsh-content-font-size-secondary)" }}>
112
+ {measuredInput !== null && <div className="mt-2 flex items-center justify-between gap-2"><span>{t.contextApiInput}</span><span title={`${measuredInput} tok`}>{fmtTokens(measuredInput)} tok</span></div>}
113
+ {window_ > 0 && (
114
+ <div className="mt-3 flex h-2 w-full overflow-hidden rounded-full" style={{ background: "var(--dsw-selector)" }} data-testid="context-bar" aria-hidden>
115
+ {segments.map((segment) => (
116
+ <span key={segment.key} data-segment={segment.key} className="flex-none" title={`${segment.label}:${fmtTokens(segment.value)} ${t.contextChars}`}
117
+ style={{ width: `${segment.width}%`, background: segment.color }} />
158
118
  ))}
159
- {free > 0 && <span style={{ width: bar(free), background: "transparent" }} />}
119
+ {visibleReserved > 0 && <span data-segment="reserved" className="flex-none" style={{ width: `${visibleReserved / window_ * 100}%`, background: "var(--dsw-border-l3)" }} />}
160
120
  </div>
161
121
  )}
162
122
  <div className="mt-2 flex flex-col gap-1" style={{ color: "var(--dsw-label-tertiary)" }}>
163
- {segments.filter((s) => s.tokens > 0).map((s) => (
164
- <SegmentRow key={s.key} label={s.label} tokens={s.tokens} color={s.color} />
165
- ))}
166
- {window_ > 0 && <SegmentRow label={t.contextFreeSpace} tokens={free} color="var(--dsw-selector)" />}
123
+ {reserved > 0 && <DataRow label={t.contextAutoCompactBuffer} value={`${fmtTokens(reserved)} tok`} exact={`${reserved} tok`} color="var(--dsw-border-l3)" />}
124
+ {window_ > 0 && <DataRow label={t.contextFreeSpace} value={free === null ? "—" : `~${fmtTokens(free)} tok`} color="var(--dsw-selector)" />}
167
125
  </div>
168
- {cold && source?.loadFull ? (
126
+ {/* 进度条的图例:颜色跟上面的分段一一对应,数字是真实字符数 */}
127
+ <div className="mt-3 flex flex-col gap-1" style={{ color: "var(--dsw-label-tertiary)" }}>
128
+ {rows.map((row) => <DataRow key={row.key} label={row.label} value={`${fmtTokens(row.value!)} ${t.contextChars}`} exact={`${row.value} ${t.contextChars}`} color={row.color} />)}
129
+ </div>
130
+ {cold && source?.loadFull && (
169
131
  <div className="mt-3 flex items-center justify-between gap-2" style={{ color: "var(--dsw-label-tertiary)" }}>
170
132
  <span className="min-w-0" style={{ fontSize: 11.5 }}>{t.contextColdHint}</span>
171
- <button
172
- type="button"
173
- className="btn-outline flex-none"
174
- style={{ height: 24, padding: "0 10px", fontSize: 12 }}
175
- disabled={counting}
133
+ <button type="button" className="btn-outline flex-none" style={{ height: 24, padding: "0 10px", fontSize: 12 }} disabled={counting}
176
134
  onClick={() => {
177
135
  setCounting(true);
178
- // 启动失败(例如没配模型)就把按钮还回来;成功时等 context_breakdown 事件把冷标记去掉
179
136
  void Promise.resolve(source.loadFull?.()).then((result) => {
180
137
  if ((result as { success?: boolean } | undefined)?.success === false) setCounting(false);
181
138
  }, () => setCounting(false));
182
- }}
183
- >
184
- {counting ? t.contextCounting : t.contextCountNow}
185
- </button>
139
+ }}>{counting ? t.contextCounting : t.contextCountNow}</button>
186
140
  </div>
187
- ) : null}
141
+ )}
142
+ {model && window_ > 0 && (
143
+ <WindowEditor key={modelKey} model={model} value={window_} onSaved={(value) => setApplied({ key: modelKey, from: contextWindow ?? 0, value })} />
144
+ )}
188
145
  </div>
189
146
  )}
190
147
  </div>
191
148
  );
192
149
  }
193
150
 
194
- function SegmentRow({ label, tokens, color }: { label: string; tokens: number; color: string }) {
195
- if (tokens <= 0) return null;
151
+ async function postModels<T>(body: Record<string, unknown>): Promise<T> {
152
+ const response = await fetch("/api/models", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) });
153
+ const payload = (await response.json().catch(() => null)) as { success?: boolean; data?: T; error?: string } | null;
154
+ if (!response.ok || !payload?.success) throw new Error(payload?.error || `request failed (${response.status})`);
155
+ return payload.data as T;
156
+ }
157
+
158
+ /**
159
+ * 弹窗底部的上下文窗口:平时显示精确值,「修改」后填数字保存。
160
+ * 不用滑块:模型的真实上限是 128000、200000 这类精确值,拖出来的近似值多一点上游就 400。
161
+ * 内置目录有原值就并排给出、填大了提醒;改的是该模型的全局配置,这一点写在输入框下面。
162
+ */
163
+ function WindowEditor({ model, value, onSaved }: { model: { provider: string; id: string }; value: number; onSaved: (value: number) => void }) {
164
+ const { t } = useI18n();
165
+ const [editing, setEditing] = useState(false);
166
+ const [text, setText] = useState("");
167
+ /** undefined = 还在查(不显示上限提示,免得先闪一句「没有内置值」) */
168
+ const [builtin, setBuiltin] = useState<number | null | undefined>(undefined);
169
+ const [saving, setSaving] = useState(false);
170
+ const [error, setError] = useState<string | null>(null);
171
+ const infoSeq = useRef(0);
172
+ const parsed = parseCapacity(text);
173
+ const target = parsed !== undefined && !Number.isNaN(parsed) ? parsed : null;
174
+ const valid = target !== null;
175
+ const over = target !== null && typeof builtin === "number" && target > builtin;
176
+ const exact = (n: number) => n.toLocaleString("en-US");
177
+
178
+ const begin = () => {
179
+ setEditing(true);
180
+ setText(formatCapacity(value));
181
+ setError(null);
182
+ setBuiltin(undefined);
183
+ const seq = ++infoSeq.current;
184
+ void postModels<{ builtin: number | null }>({ action: "contextWindowInfo", providerId: model.provider, modelId: model.id }).then(
185
+ (info) => { if (seq === infoSeq.current) setBuiltin(info.builtin); },
186
+ () => { if (seq === infoSeq.current) setBuiltin(null); },
187
+ );
188
+ };
189
+ const cancel = () => {
190
+ infoSeq.current += 1;
191
+ setEditing(false);
192
+ };
193
+ const save = async () => {
194
+ if (target === null || saving) return;
195
+ setSaving(true);
196
+ setError(null);
197
+ try {
198
+ const info = await postModels<{ contextWindow: number }>({ action: "setContextWindow", providerId: model.provider, modelId: model.id, contextWindow: target });
199
+ onSaved(info.contextWindow);
200
+ setEditing(false);
201
+ } catch (err) {
202
+ setError(err instanceof Error ? err.message : String(err));
203
+ } finally {
204
+ setSaving(false);
205
+ }
206
+ };
207
+
208
+ if (!editing) {
209
+ return (
210
+ <div className="hairline-t mt-3 flex items-center justify-between gap-2 pt-3" style={{ color: "var(--dsw-label-tertiary)" }}>
211
+ <span>{t.contextWindowLabel}</span>
212
+ <span className="flex items-center gap-2">
213
+ <span style={{ color: "var(--dsw-label-secondary)", fontFamily: "var(--font-mono)" }}>{exact(value)}</span>
214
+ <button type="button" className="btn-outline" style={{ height: 22, padding: "0 8px", fontSize: 11.5 }} onClick={begin}>{t.contextWindowEdit}</button>
215
+ </span>
216
+ </div>
217
+ );
218
+ }
196
219
  return (
197
- <div className="flex items-center justify-between gap-2">
198
- <span className="flex items-center gap-1.5">
199
- <span className="inline-block rounded-full" style={{ width: 7, height: 7, background: color }} />
200
- {label}
201
- </span>
202
- <span>~{fmtTok(tokens)}</span>
220
+ <div className="hairline-t mt-3 pt-3" style={{ color: "var(--dsw-label-tertiary)" }}>
221
+ <div>{t.contextWindowLabel}</div>
222
+ <div className="mt-1.5 flex items-center gap-1.5">
223
+ <input
224
+ autoFocus
225
+ value={text}
226
+ onChange={(e) => setText(e.target.value)}
227
+ onKeyDown={(e) => {
228
+ if (e.key === "Enter") {
229
+ e.preventDefault();
230
+ void save();
231
+ } else if (e.key === "Escape") {
232
+ e.preventDefault();
233
+ e.stopPropagation();
234
+ cancel();
235
+ }
236
+ }}
237
+ aria-label={t.contextWindowLabel}
238
+ aria-invalid={!valid}
239
+ spellCheck={false}
240
+ className="min-w-0 flex-1 rounded-md px-2"
241
+ style={{ height: 26, fontSize: 12.5, fontFamily: "var(--font-mono)", color: "var(--dsw-label-primary)", background: "var(--dsw-hover)", border: `0.5px solid ${valid ? "var(--dsw-border-l2)" : "var(--dsw-danger)"}` }}
242
+ />
243
+ <button type="button" className="btn-primary-white flex-none" style={{ height: 26, padding: "0 10px", fontSize: 12 }} disabled={!valid || saving} onClick={() => void save()}>
244
+ {saving ? t.saving : t.save}
245
+ </button>
246
+ <button type="button" className="btn-outline flex-none" style={{ height: 26, padding: "0 10px", fontSize: 12 }} disabled={saving} onClick={cancel}>{t.cancel}</button>
247
+ </div>
248
+ {typeof builtin === "number" && (
249
+ <div className="mt-1.5 flex items-center gap-1.5" style={{ fontSize: 11.5 }}>
250
+ <span>{t.contextWindowBuiltin.replace("{value}", exact(builtin))}</span>
251
+ {target !== builtin && (
252
+ <button type="button" className="underline" style={{ color: "var(--dsw-label-secondary)" }} onClick={() => setText(formatCapacity(builtin))}>{t.contextWindowUseBuiltin}</button>
253
+ )}
254
+ </div>
255
+ )}
256
+ {!valid ? (
257
+ <div className="mt-1" style={{ fontSize: 11.5, color: "var(--dsw-danger)" }}>{t.contextWindowInvalid}</div>
258
+ ) : over ? (
259
+ <div className="mt-1" style={{ fontSize: 11.5, color: "var(--dsw-warn)" }}>{t.contextWindowOverBuiltin}</div>
260
+ ) : builtin === null ? (
261
+ <div className="mt-1" style={{ fontSize: 11.5 }}>{t.contextWindowNoBuiltin}</div>
262
+ ) : null}
263
+ <div className="mt-1" style={{ fontSize: 11.5 }}>{t.contextWindowScope}</div>
264
+ {error && <div className="mt-1" role="alert" style={{ fontSize: 11.5, color: "var(--dsw-danger)" }}>{error}</div>}
203
265
  </div>
204
266
  );
205
267
  }
268
+
269
+ function DataRow({ label, value, exact, color }: { label: string; value: string; exact?: string; color: string }) {
270
+ return <div className="flex items-center justify-between gap-2">
271
+ <span className="flex items-center gap-1.5"><span className="inline-block rounded-full" style={{ width: 7, height: 7, background: color }} />{label}</span>
272
+ <span title={exact}>{value}</span>
273
+ </div>;
274
+ }
@@ -18,7 +18,7 @@ import type { GrowthApi } from "@/hooks/useGrowth";
18
18
  import { useI18n } from "@/i18n";
19
19
  import type { TreeNode } from "@/lib/growth-tree";
20
20
 
21
- /** 抽屉里的入口,顺序即顶部入口列表的显示顺序:项目 → 提示词 → Git 详情 → 会话导入 */
21
+ /** 抽屉入口:项目 → 提示词 → Git 详情 → 会话导入(旁聊已独立成右侧面板,不再共用这一列)。 */
22
22
  export type DrawerSection = "project" | "prompts" | "git" | "import";
23
23
 
24
24
  export function DrawerPanel({
@@ -6,7 +6,7 @@
6
6
  * 文件页签:面包屑 + 模式(变更 / 内容 / 渲染)+ 折叠未改动 + 上一处 / 下一处 + 引用 / 编辑器打开。
7
7
  * 「变更」= 整文件行内 diff(FullDiff);「内容」= 所选步的全文(被删文件显示删除前);「渲染」= Markdown。
8
8
  */
9
- import { useEffect, useMemo, useRef, useState } from "react";
9
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
10
10
  import ReactMarkdown from "react-markdown";
11
11
  import rehypeHighlight from "rehype-highlight";
12
12
  import remarkGfm from "remark-gfm";
@@ -46,6 +46,10 @@ function cachedJson<T>(url: string): Promise<T> {
46
46
  return p;
47
47
  }
48
48
 
49
+ /** 文件夹预览:一页目录条目(分页;后端已按 .gitignore 过滤) */
50
+ interface DirEntry { name: string; kind: "dir" | "file"; size?: number }
51
+ interface DirPage { dir: string; entries: DirEntry[]; nextOffset: number | null; truncated: boolean }
52
+
49
53
  interface Loaded {
50
54
  key: string;
51
55
  diff: DiffLine[] | null;
@@ -94,7 +98,7 @@ export function FileViewer({
94
98
  onClose: () => void;
95
99
  onSelectTab: (path: string | null) => void;
96
100
  onCloseTab: (path: string) => void;
97
- onOpen: (path: string, opts?: { from?: string; lazy?: boolean }) => void;
101
+ onOpen: (path: string, opts?: { from?: string; lazy?: boolean; kind?: "dir" | "file" }) => void;
98
102
  onReference?: (path: string) => void;
99
103
  onOpenEditor?: (path: string) => void;
100
104
  }) {
@@ -109,6 +113,8 @@ export function FileViewer({
109
113
  const [treeFilter, setTreeFilter] = useState("");
110
114
  const [reveal, setReveal] = useState<{ path: string; key: number } | null>(null);
111
115
  const [loaded, setLoaded] = useState<Loaded | null>(null);
116
+ const [dirPage, setDirPage] = useState<(DirPage & { key: string }) | null>(null);
117
+ const [dirMore, setDirMore] = useState<DirEntry[]>([]);
112
118
  const diffRef = useRef<FullDiffHandle>(null);
113
119
  const rootRef = useRef<HTMLDivElement>(null);
114
120
  const quickInputRef = useRef<HTMLInputElement>(null);
@@ -121,15 +127,56 @@ export function FileViewer({
121
127
  // 切页签:模式按是否有改动自动选,用户手动切过才保留
122
128
  useEffect(() => {
123
129
  setModeTouched(false);
130
+ setDirFallback(null);
124
131
  }, [state.active]);
125
132
  useEffect(() => {
126
133
  if (modeTouched || !activeTab) return;
127
134
  setMode(change && !activeTab.lazy ? "diff" : isMarkdown ? "render" : "content");
128
135
  }, [activeTab, change, isMarkdown, modeTouched]);
129
136
 
137
+ /** 目录兜底:文件读取报 400(不存在 / 不是文件)时探一下目录接口,成功就当文件夹显示 */
138
+ const [dirFallback, setDirFallback] = useState<string | null>(null);
139
+ const dirPath = activeTab?.kind === "dir" ? activeTab.path : dirFallback;
140
+ const isDir = Boolean(dirPath);
141
+ // 文件夹:列目录(分页,后端已按 .gitignore 过滤),不读文件内容
142
+ useEffect(() => {
143
+ if (!dirPath || !cwd || !activeTab) {
144
+ setDirPage(null);
145
+ setDirMore([]);
146
+ return;
147
+ }
148
+ // @src/ 这类带尾斜杠的引用:列目录时去掉
149
+ const path_ = dirPath.replace(/\/+$/, "");
150
+ const key = `dir|${path_}`;
151
+ let alive = true;
152
+ setDirMore([]);
153
+ void (async () => {
154
+ try {
155
+ const data = await requestJson<DirPage>(`/api/files?cwd=${encodeURIComponent(cwd)}&path=${encodeURIComponent(path_)}&offset=0`);
156
+ if (alive) setDirPage({ key, ...data });
157
+ } 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
+ }
160
+ })();
161
+ return () => {
162
+ alive = false;
163
+ };
164
+ }, [activeTab, dirPath, cwd, t.viewerLoadFailed]);
165
+
166
+ /** 目录还有下一页时接着拉(大目录:node_modules 这种已被忽略,剩下的也不会太多) */
167
+ const loadMore = useCallback(async () => {
168
+ if (!activeTab || !cwd || !dirPage?.nextOffset) return;
169
+ 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
+ } catch {
173
+ // 下一页失败就留在当前这页,用户可以再点
174
+ }
175
+ }, [activeTab, cwd, dirPage]);
176
+
130
177
  // 取数据
131
178
  useEffect(() => {
132
- if (!activeTab) {
179
+ if (!activeTab || activeTab.kind === "dir") {
133
180
  setLoaded(null);
134
181
  return;
135
182
  }
@@ -157,6 +204,15 @@ export function FileViewer({
157
204
  done({ content: data.content, binary: data.binary, truncated: data.truncated, notice });
158
205
  } catch (error) {
159
206
  if ((error as { status?: number }).status !== 400) throw error;
207
+ // 400 有两种:文件不存在 / 路径其实是目录。探一下目录接口,能列出条目就当文件夹打开
208
+ try {
209
+ await requestJson<DirPage>(`/api/files?${q}&path=${encodeURIComponent(tab.path)}&offset=0`);
210
+ done({ notice: null });
211
+ setDirFallback(tab.path);
212
+ return;
213
+ } catch {
214
+ // 目录也列不出来:照旧报「文件不存在」
215
+ }
160
216
  done({ notice: "missing" });
161
217
  }
162
218
  };
@@ -379,7 +435,7 @@ export function FileViewer({
379
435
  {stepBadge}
380
436
  </span>
381
437
  )}
382
- {!activeTab.lazy && (
438
+ {!activeTab.lazy && !isDir && (
383
439
  <span className="pw-seg" role="radiogroup">
384
440
  {(["diff", "content", ...(isMarkdown ? ["render" as const] : [])] as Mode[]).map((m) => (
385
441
  <button
@@ -398,7 +454,7 @@ export function FileViewer({
398
454
  ))}
399
455
  </span>
400
456
  )}
401
- {activeTab.lazy && isMarkdown && (
457
+ {!isDir && activeTab.lazy && isMarkdown && (
402
458
  <span className="pw-seg" role="radiogroup">
403
459
  {(["content", "render"] as Mode[]).map((m) => (
404
460
  <button key={m} type="button" role="radio" aria-checked={mode === m} data-active={mode === m} onClick={() => { setModeTouched(true); setMode(m); }}>
@@ -407,7 +463,7 @@ export function FileViewer({
407
463
  ))}
408
464
  </span>
409
465
  )}
410
- {isDiffMode && (
466
+ {isDiffMode && !isDir && (
411
467
  <>
412
468
  <button type="button" className="pw-chip" data-active={folded} onClick={() => setFolded((f) => !f)}>
413
469
  {folded ? t.viewerFold : t.viewerUnfold}
@@ -439,7 +495,32 @@ export function FileViewer({
439
495
  </div>
440
496
  )}
441
497
  {loaded?.truncated && <div className="pw-viewer-notice" data-kind="warn">{mode === "diff" ? t.viewerPatchTruncated : t.filePreviewTruncated}</div>}
442
- {!loaded || loaded.key !== `${activeTab.path}|${mode}|${range?.from ?? ""}|${range?.to ?? ""}|${change?.status ?? ""}` ? (
498
+ {isDir ? (
499
+ <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))}
502
+ </div>
503
+ {dirPage?.entries.map((entry) => (
504
+ <button
505
+ key={entry.name}
506
+ type="button"
507
+ className="flex w-full items-center gap-2 px-3 py-1.5 text-left hover:bg-[var(--dsw-hover)]"
508
+ onClick={() => (entry.kind === "dir"
509
+ ? onOpen(`${dirPath!.replace(/\/+$/, "")}/${entry.name}`, { kind: "dir", lazy: true })
510
+ : onOpen(`${dirPath!.replace(/\/+$/, "")}/${entry.name}`, { lazy: true }))}
511
+ >
512
+ <img className="pw-tree-icon" src={entry.kind === "dir" ? "/file-icons/folder-close.svg" : fileIconSrc(entry.name)} alt="" />
513
+ <span className="min-w-0 flex-1 truncate" style={{ fontSize: 12.5, fontFamily: entry.kind === "dir" ? "inherit" : "var(--font-mono)" }}>{entry.name}</span>
514
+ {entry.kind === "file" && entry.size !== undefined && (
515
+ <span style={{ fontSize: 11, color: "var(--dsw-label-caption)", fontVariantNumeric: "tabular-nums" }}>{entry.size} B</span>
516
+ )}
517
+ </button>
518
+ ))}
519
+ {dirPage?.nextOffset ? (
520
+ <button type="button" className="pw-chip mx-3 my-2" onClick={() => void loadMore()}>{t.viewerDirMore}</button>
521
+ ) : null}
522
+ </div>
523
+ ) : !loaded || loaded.key !== `${activeTab.path}|${mode}|${range?.from ?? ""}|${range?.to ?? ""}|${change?.status ?? ""}` ? (
443
524
  <div className="flex min-h-0 flex-1 items-center justify-center" style={{ fontSize: 12.5, color: "var(--dsw-label-caption)" }}>{t.panelLoading}</div>
444
525
  ) : loaded.error ? (
445
526
  <div className="flex min-h-0 flex-1 items-center justify-center" style={{ fontSize: 12.5, color: "var(--dsw-danger)" }}>{loaded.error}</div>
@@ -480,7 +561,7 @@ export function FileViewer({
480
561
  selectedPath={null}
481
562
  onToggleDir={growth.toggleDir}
482
563
  onExpandLazy={growth.expandLazy}
483
- onOpenFile={(node: TreeNode) => onOpen(node.path, { from: node.from, lazy: node.lazy })}
564
+ onOpenFile={(node: TreeNode) => onOpen(node.path, { from: node.from, lazy: node.lazy, kind: node.kind })}
484
565
  onReference={onReference}
485
566
  onOpenEditor={onOpenEditor}
486
567
  revealPath={reveal?.path ?? null}