@rexvane/piweb 0.3.20 → 0.3.22

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 (317) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/app-path-routes-manifest.json +6 -4
  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 +12 -12
  8. package/.next/required-server-files.js +18 -2
  9. package/.next/required-server-files.json +18 -2
  10. package/.next/routes-manifest.json +12 -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 +3 -3
  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]/tree/route.js +1 -1
  34. package/.next/server/app/api/agent/[id]/tree/route.js.nft.json +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/config/route.js +1 -1
  38. package/.next/server/app/api/config/route.js.nft.json +1 -1
  39. package/.next/server/app/api/files/route.js +1 -1
  40. package/.next/server/app/api/files/route.js.nft.json +1 -1
  41. package/.next/server/app/api/fs/route.js +1 -1
  42. package/.next/server/app/api/fs/route.js.nft.json +1 -1
  43. package/.next/server/app/api/git/route.js +4 -4
  44. package/.next/server/app/api/git/route.js.nft.json +1 -1
  45. package/.next/server/app/api/github/route.js +5 -0
  46. package/.next/server/app/api/github/route.js.nft.json +1 -0
  47. package/.next/server/app/api/github/route_client-reference-manifest.js +1 -0
  48. package/.next/server/app/api/growth/route.js +1 -1
  49. package/.next/server/app/api/growth/route.js.nft.json +1 -1
  50. package/.next/server/app/api/health/route.js +1 -1
  51. package/.next/server/app/api/health/route.js.nft.json +1 -1
  52. package/.next/server/app/api/models/defaults/route.js +1 -0
  53. package/.next/server/app/api/models/defaults/route.js.nft.json +1 -0
  54. package/.next/server/app/api/models/defaults/route_client-reference-manifest.js +1 -0
  55. package/.next/server/app/api/models/route.js +1 -1
  56. package/.next/server/app/api/models/route.js.nft.json +1 -1
  57. package/.next/server/app/api/pi-settings/route.js +1 -1
  58. package/.next/server/app/api/pi-settings/route.js.nft.json +1 -1
  59. package/.next/server/app/api/plugins/route.js +1 -1
  60. package/.next/server/app/api/plugins/route.js.nft.json +1 -1
  61. package/.next/server/app/api/prompts/route.js +1 -1
  62. package/.next/server/app/api/prompts/route.js.nft.json +1 -1
  63. package/.next/server/app/api/remote/route.js +3 -3
  64. package/.next/server/app/api/remote/route.js.nft.json +1 -1
  65. package/.next/server/app/api/security/route.js +1 -1
  66. package/.next/server/app/api/security/route.js.nft.json +1 -1
  67. package/.next/server/app/api/sessions/[id]/export/route.js +1 -1
  68. package/.next/server/app/api/sessions/[id]/export/route.js.nft.json +1 -1
  69. package/.next/server/app/api/sessions/[id]/route.js +1 -1
  70. package/.next/server/app/api/sessions/[id]/route.js.nft.json +1 -1
  71. package/.next/server/app/api/sessions/import/route.js +2 -2
  72. package/.next/server/app/api/sessions/import/route.js.nft.json +1 -1
  73. package/.next/server/app/api/sessions/route.js +1 -1
  74. package/.next/server/app/api/sessions/route.js.nft.json +1 -1
  75. package/.next/server/app/api/skills/route.js +2 -2
  76. package/.next/server/app/api/skills/route.js.nft.json +1 -1
  77. package/.next/server/app/api/update/route.js +2 -2
  78. package/.next/server/app/api/update/route.js.nft.json +1 -1
  79. package/.next/server/app/api/version/route.js +1 -1
  80. package/.next/server/app/api/version/route.js.nft.json +1 -1
  81. package/.next/server/app/api/web-auth/route.js +1 -1
  82. package/.next/server/app/api/web-auth/route.js.nft.json +1 -1
  83. package/.next/server/app/api/workspaces/route.js +1 -1
  84. package/.next/server/app/api/workspaces/route.js.nft.json +1 -1
  85. package/.next/server/app/icon.svg/route.js +1 -1
  86. package/.next/server/app/icon.svg/route.js.nft.json +1 -1
  87. package/.next/server/app/icon.svg.meta +1 -1
  88. package/.next/server/app/index.html +1 -1
  89. package/.next/server/app/index.meta +8 -0
  90. package/.next/server/app/index.rsc +19 -13
  91. package/.next/server/app/index.segments/__PAGE__.segment.rsc +25 -19
  92. package/.next/server/app/index.segments/_full.segment.rsc +19 -13
  93. package/.next/server/app/index.segments/_tree.segment.rsc +3 -2
  94. package/.next/server/app/login/page.js +1 -1
  95. package/.next/server/app/login/page.js.nft.json +1 -1
  96. package/.next/server/app/login/page_client-reference-manifest.js +1 -1
  97. package/.next/server/app/login.html +1 -1
  98. package/.next/server/app/login.meta +8 -0
  99. package/.next/server/app/login.rsc +23 -17
  100. package/.next/server/app/login.segments/_full.segment.rsc +23 -17
  101. package/.next/server/app/login.segments/_tree.segment.rsc +4 -2
  102. package/.next/server/app/login.segments/login/__PAGE__.segment.rsc +30 -24
  103. package/.next/server/app/page.js +1 -1
  104. package/.next/server/app/page.js.nft.json +1 -1
  105. package/.next/server/app/page_client-reference-manifest.js +1 -1
  106. package/.next/server/app-paths-manifest.json +6 -4
  107. package/.next/server/chunks/229.js +1 -1
  108. package/.next/server/chunks/421.js +1 -0
  109. package/.next/server/chunks/430.js +215 -31
  110. package/.next/server/chunks/496.js +1 -1
  111. package/.next/server/chunks/538.js +1 -1
  112. package/.next/server/chunks/662.js +2 -0
  113. package/.next/server/chunks/686.js +1 -1
  114. package/.next/server/chunks/807.js +1 -1
  115. package/.next/server/chunks/813.js +23 -1
  116. package/.next/server/chunks/920.js +2 -2
  117. package/.next/server/chunks/924.js +1 -1
  118. package/.next/server/chunks/970.js +305 -0
  119. package/.next/server/instrumentation.js +1 -1
  120. package/.next/server/instrumentation.js.nft.json +1 -1
  121. package/.next/server/middleware-build-manifest.js +1 -1
  122. package/.next/server/middleware-react-loadable-manifest.js +1 -1
  123. package/.next/server/middleware.js +185 -23
  124. package/.next/server/middleware.js.nft.json +1 -1
  125. package/.next/server/pages/404.html +1 -1
  126. package/.next/server/pages/500.html +1 -1
  127. package/.next/server/preview-props.json +5 -0
  128. package/.next/server/server-reference-manifest.json +1 -1
  129. package/.next/static/WMazYvD-gbeLsCUfUxvvM/_buildManifest.js +1 -0
  130. package/.next/static/chunks/316-698d10eb465bd8ef.js +1 -0
  131. package/.next/static/chunks/3794-0978865cda773008.js +52 -0
  132. package/.next/static/chunks/4673.4cf66cbdd8364c57.js +1 -0
  133. package/.next/static/chunks/4686-09fcd0028079006c.js +1 -0
  134. package/.next/static/chunks/4778.eddd3a5813f85824.js +1 -0
  135. package/.next/static/chunks/4bd1b696-e199e788e58acf79.js +1 -0
  136. package/.next/static/chunks/{5173.91ddb803785009f8.js → 5173.dfd1cbb795b9a21c.js} +1 -1
  137. package/.next/static/chunks/618.949981da99be7182.js +4 -0
  138. package/.next/static/chunks/7411.67366dc5032a141d.js +1 -0
  139. package/.next/static/chunks/8552.25c39ef4c995eba0.js +1 -0
  140. package/.next/static/chunks/app/_global-error/page-f977af7f858990a2.js +1 -0
  141. package/.next/static/chunks/app/api/agent/[id]/events/route-f977af7f858990a2.js +1 -0
  142. package/.next/static/chunks/app/api/agent/[id]/route-f977af7f858990a2.js +1 -0
  143. package/.next/static/chunks/app/api/agent/[id]/tree/route-f977af7f858990a2.js +1 -0
  144. package/.next/static/chunks/app/api/agent/new/route-f977af7f858990a2.js +1 -0
  145. package/.next/static/chunks/app/api/config/route-f977af7f858990a2.js +1 -0
  146. package/.next/static/chunks/app/api/files/route-f977af7f858990a2.js +1 -0
  147. package/.next/static/chunks/app/api/fs/route-f977af7f858990a2.js +1 -0
  148. package/.next/static/chunks/app/api/git/route-f977af7f858990a2.js +1 -0
  149. package/.next/static/chunks/app/api/github/route-f977af7f858990a2.js +1 -0
  150. package/.next/static/chunks/app/api/growth/route-f977af7f858990a2.js +1 -0
  151. package/.next/static/chunks/app/api/health/route-f977af7f858990a2.js +1 -0
  152. package/.next/static/chunks/app/api/models/defaults/route-f977af7f858990a2.js +1 -0
  153. package/.next/static/chunks/app/api/models/route-f977af7f858990a2.js +1 -0
  154. package/.next/static/chunks/app/api/pi-settings/route-f977af7f858990a2.js +1 -0
  155. package/.next/static/chunks/app/api/plugins/route-f977af7f858990a2.js +1 -0
  156. package/.next/static/chunks/app/api/prompts/route-f977af7f858990a2.js +1 -0
  157. package/.next/static/chunks/app/api/remote/route-f977af7f858990a2.js +1 -0
  158. package/.next/static/chunks/app/api/security/route-f977af7f858990a2.js +1 -0
  159. package/.next/static/chunks/app/api/sessions/[id]/export/route-f977af7f858990a2.js +1 -0
  160. package/.next/static/chunks/app/api/sessions/[id]/route-f977af7f858990a2.js +1 -0
  161. package/.next/static/chunks/app/api/sessions/import/route-f977af7f858990a2.js +1 -0
  162. package/.next/static/chunks/app/api/sessions/route-f977af7f858990a2.js +1 -0
  163. package/.next/static/chunks/app/api/skills/route-f977af7f858990a2.js +1 -0
  164. package/.next/static/chunks/app/api/update/route-f977af7f858990a2.js +1 -0
  165. package/.next/static/chunks/app/api/version/route-f977af7f858990a2.js +1 -0
  166. package/.next/static/chunks/app/api/web-auth/route-f977af7f858990a2.js +1 -0
  167. package/.next/static/chunks/app/api/workspaces/route-f977af7f858990a2.js +1 -0
  168. package/.next/static/chunks/app/page-fc4ed2ff29b6235e.js +1 -0
  169. package/.next/static/chunks/framework-47b723d27f4a4963.js +1 -0
  170. package/.next/static/chunks/main-5403f0764579e5fe.js +5 -0
  171. package/.next/static/chunks/next/dist/client/components/builtin/app-error-f977af7f858990a2.js +1 -0
  172. package/.next/static/chunks/next/dist/client/components/builtin/forbidden-f977af7f858990a2.js +1 -0
  173. package/.next/static/chunks/next/dist/client/components/builtin/not-found-f977af7f858990a2.js +1 -0
  174. package/.next/static/chunks/next/dist/client/components/builtin/unauthorized-f977af7f858990a2.js +1 -0
  175. package/.next/static/chunks/webpack-22ebffc20adb44ae.js +1 -0
  176. package/.next/static/css/6671d0d1589fdc0e.css +1 -0
  177. package/.next/static/css/{2959bd6b626b7ffe.css → f15a853af8b5127f.css} +1 -1
  178. package/.next/types/app/api/agent/[id]/events/route.ts +3 -1
  179. package/.next/types/app/api/agent/[id]/route.ts +3 -1
  180. package/.next/types/app/api/agent/[id]/tree/route.ts +3 -1
  181. package/.next/types/app/api/agent/new/route.ts +3 -1
  182. package/.next/types/app/api/config/route.ts +3 -1
  183. package/.next/types/app/api/files/route.ts +3 -1
  184. package/.next/types/app/api/fs/route.ts +3 -1
  185. package/.next/types/app/api/git/route.ts +3 -1
  186. package/.next/types/app/api/github/route.ts +355 -0
  187. package/.next/types/app/api/growth/route.ts +3 -1
  188. package/.next/types/app/api/health/route.ts +3 -1
  189. package/.next/types/app/api/models/defaults/route.ts +355 -0
  190. package/.next/types/app/api/models/route.ts +3 -1
  191. package/.next/types/app/api/pi-settings/route.ts +3 -1
  192. package/.next/types/app/api/plugins/route.ts +3 -1
  193. package/.next/types/app/api/prompts/route.ts +3 -1
  194. package/.next/types/app/api/remote/route.ts +3 -1
  195. package/.next/types/app/api/security/route.ts +3 -1
  196. package/.next/types/app/api/sessions/[id]/export/route.ts +3 -1
  197. package/.next/types/app/api/sessions/[id]/route.ts +3 -1
  198. package/.next/types/app/api/sessions/import/route.ts +3 -1
  199. package/.next/types/app/api/sessions/route.ts +3 -1
  200. package/.next/types/app/api/skills/route.ts +3 -1
  201. package/.next/types/app/api/update/route.ts +3 -1
  202. package/.next/types/app/api/version/route.ts +3 -1
  203. package/.next/types/app/api/web-auth/route.ts +3 -1
  204. package/.next/types/app/api/workspaces/route.ts +3 -1
  205. package/.next/types/app/layout.ts +4 -1
  206. package/.next/types/app/login/page.ts +4 -1
  207. package/.next/types/app/page.ts +4 -1
  208. package/.next/types/cache-life.d.ts +11 -43
  209. package/.next/types/routes.d.ts +3 -1
  210. package/.next/types/validator.ts +76 -2
  211. package/README.md +14 -5
  212. package/next.config.ts +4 -2
  213. package/package.json +2 -2
  214. package/scripts/release.mjs +1 -1
  215. package/src/app/api/agent/[id]/events/route.ts +42 -13
  216. package/src/app/api/files/route.ts +5 -2
  217. package/src/app/api/github/route.ts +26 -0
  218. package/src/app/api/models/defaults/route.ts +18 -0
  219. package/src/app/api/models/route.ts +24 -1
  220. package/src/app/api/update/route.ts +18 -2
  221. package/src/app/globals.css +275 -0
  222. package/src/components/AppShell.tsx +89 -33
  223. package/src/components/ChatInput.tsx +245 -63
  224. package/src/components/ChatWindow.tsx +123 -108
  225. package/src/components/ContextMeter.tsx +204 -123
  226. package/src/components/DrawerPanel.tsx +19 -26
  227. package/src/components/ExtensionUI.tsx +8 -2
  228. package/src/components/GitPanel.tsx +110 -40
  229. package/src/components/MarkdownBlock.tsx +333 -37
  230. package/src/components/ProjectPanel.tsx +55 -30
  231. package/src/components/PromptPanel.tsx +2 -4
  232. package/src/components/ProviderSetupModal.module.css +20 -0
  233. package/src/components/ProviderSetupModal.tsx +134 -29
  234. package/src/components/SettingsPanel.tsx +365 -80
  235. package/src/components/TrajectoryView.tsx +4 -6
  236. package/src/components/chat/chat-scroll.ts +18 -0
  237. package/src/components/composer/ComposerImages.tsx +306 -0
  238. package/src/components/update/CopyCommand.tsx +34 -0
  239. package/src/components/update/UpdateNotice.tsx +130 -0
  240. package/src/components/update/update-store.ts +155 -0
  241. package/src/hooks/useGrowth.ts +3 -0
  242. package/src/hooks/usePiWeb.ts +54 -27
  243. package/src/hooks/useWorkspaceFiles.ts +93 -0
  244. package/src/i18n.tsx +182 -6
  245. package/src/instrumentation-node.ts +3 -0
  246. package/src/lib/agent-manager.ts +193 -119
  247. package/src/lib/chat/event-stream.ts +113 -0
  248. package/src/lib/chat/stream-settings.ts +192 -0
  249. package/src/lib/files-service.ts +100 -1
  250. package/src/lib/format.ts +56 -0
  251. package/src/lib/git-service.ts +45 -13
  252. package/src/lib/github-identity.ts +245 -0
  253. package/src/lib/growth-service.ts +6 -2
  254. package/src/lib/hero-model-memory.ts +28 -0
  255. package/src/lib/model-defaults.ts +105 -0
  256. package/src/lib/model-draft.ts +257 -9
  257. package/src/lib/model-endpoint.ts +23 -0
  258. package/src/lib/model-files.ts +33 -0
  259. package/src/lib/models-service.ts +240 -78
  260. package/src/lib/pi.ts +27 -2
  261. package/src/lib/process-format.ts +3 -2
  262. package/src/lib/session/metrics.ts +31 -0
  263. package/src/lib/thinking-level.ts +39 -0
  264. package/src/lib/trajectory.ts +17 -3
  265. package/src/lib/types.ts +19 -1
  266. package/src/lib/update/startup-check.ts +117 -0
  267. package/src/lib/update-service.ts +23 -3
  268. package/src/lib/workspace-index.ts +270 -0
  269. package/.next/server/chunks/131.js +0 -1
  270. package/.next/server/chunks/243.js +0 -1
  271. package/.next/server/chunks/741.js +0 -165
  272. package/.next/static/_Y3Lq0lI5RJ1tFSOnDi0L/_buildManifest.js +0 -1
  273. package/.next/static/chunks/316-5ea443ee6b512aa9.js +0 -1
  274. package/.next/static/chunks/3794-3679f1c05160977c.js +0 -52
  275. package/.next/static/chunks/4673.8a5a9e6855350e34.js +0 -1
  276. package/.next/static/chunks/4686-b1e9fa876d0ea671.js +0 -1
  277. package/.next/static/chunks/4778.e4c9ed093a413bc4.js +0 -1
  278. package/.next/static/chunks/4bd1b696-93bf506b236d6248.js +0 -1
  279. package/.next/static/chunks/618.ee5d5bf1c647e483.js +0 -4
  280. package/.next/static/chunks/839.d3fe337a9117c325.js +0 -1
  281. package/.next/static/chunks/8552.6bfe3e101250b20f.js +0 -1
  282. package/.next/static/chunks/app/_global-error/page-b179d9f7f6f2daf9.js +0 -1
  283. package/.next/static/chunks/app/api/agent/[id]/events/route-b179d9f7f6f2daf9.js +0 -1
  284. package/.next/static/chunks/app/api/agent/[id]/route-b179d9f7f6f2daf9.js +0 -1
  285. package/.next/static/chunks/app/api/agent/[id]/tree/route-b179d9f7f6f2daf9.js +0 -1
  286. package/.next/static/chunks/app/api/agent/new/route-b179d9f7f6f2daf9.js +0 -1
  287. package/.next/static/chunks/app/api/config/route-b179d9f7f6f2daf9.js +0 -1
  288. package/.next/static/chunks/app/api/files/route-b179d9f7f6f2daf9.js +0 -1
  289. package/.next/static/chunks/app/api/fs/route-b179d9f7f6f2daf9.js +0 -1
  290. package/.next/static/chunks/app/api/git/route-b179d9f7f6f2daf9.js +0 -1
  291. package/.next/static/chunks/app/api/growth/route-b179d9f7f6f2daf9.js +0 -1
  292. package/.next/static/chunks/app/api/health/route-b179d9f7f6f2daf9.js +0 -1
  293. package/.next/static/chunks/app/api/models/route-b179d9f7f6f2daf9.js +0 -1
  294. package/.next/static/chunks/app/api/pi-settings/route-b179d9f7f6f2daf9.js +0 -1
  295. package/.next/static/chunks/app/api/plugins/route-b179d9f7f6f2daf9.js +0 -1
  296. package/.next/static/chunks/app/api/prompts/route-b179d9f7f6f2daf9.js +0 -1
  297. package/.next/static/chunks/app/api/remote/route-b179d9f7f6f2daf9.js +0 -1
  298. package/.next/static/chunks/app/api/security/route-b179d9f7f6f2daf9.js +0 -1
  299. package/.next/static/chunks/app/api/sessions/[id]/export/route-b179d9f7f6f2daf9.js +0 -1
  300. package/.next/static/chunks/app/api/sessions/[id]/route-b179d9f7f6f2daf9.js +0 -1
  301. package/.next/static/chunks/app/api/sessions/import/route-b179d9f7f6f2daf9.js +0 -1
  302. package/.next/static/chunks/app/api/sessions/route-b179d9f7f6f2daf9.js +0 -1
  303. package/.next/static/chunks/app/api/skills/route-b179d9f7f6f2daf9.js +0 -1
  304. package/.next/static/chunks/app/api/update/route-b179d9f7f6f2daf9.js +0 -1
  305. package/.next/static/chunks/app/api/version/route-b179d9f7f6f2daf9.js +0 -1
  306. package/.next/static/chunks/app/api/web-auth/route-b179d9f7f6f2daf9.js +0 -1
  307. package/.next/static/chunks/app/api/workspaces/route-b179d9f7f6f2daf9.js +0 -1
  308. package/.next/static/chunks/app/page-2e425ef83fba0782.js +0 -1
  309. package/.next/static/chunks/framework-c4e651fe89edfa91.js +0 -1
  310. package/.next/static/chunks/main-6159b22a8cb0b7c5.js +0 -5
  311. package/.next/static/chunks/next/dist/client/components/builtin/app-error-b179d9f7f6f2daf9.js +0 -1
  312. package/.next/static/chunks/next/dist/client/components/builtin/forbidden-b179d9f7f6f2daf9.js +0 -1
  313. package/.next/static/chunks/next/dist/client/components/builtin/not-found-b179d9f7f6f2daf9.js +0 -1
  314. package/.next/static/chunks/next/dist/client/components/builtin/unauthorized-b179d9f7f6f2daf9.js +0 -1
  315. package/.next/static/chunks/webpack-3614a8337843456b.js +0 -1
  316. package/.next/static/css/7391c42e1ae617f9.css +0 -1
  317. /package/.next/static/{_Y3Lq0lI5RJ1tFSOnDi0L → WMazYvD-gbeLsCUfUxvvM}/_ssgManifest.js +0 -0
@@ -1,174 +1,255 @@
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
- * 无对话时不渲染(由父组件经 contextVisible 控制)。
11
- */
3
+ /** 上下文总量是 SDK 估算;API 输入用量与内容字符分类分别展示,不能按比例分摊 token。 */
12
4
  import { useEffect, useMemo, useRef, useState } from "react";
13
5
  import { useI18n } from "@/i18n";
6
+ import { fmtPercent, fmtTokens } from "@/lib/format";
7
+ import { formatCapacity, parseCapacity } from "@/lib/model-draft";
14
8
  import { classifyMessageChars } from "@/lib/process-format";
15
- import type { ContextBreakdown } from "@/lib/types";
16
-
17
- function fmtTok(n: number): string {
18
- return n >= 1000 ? `${(n / 1000).toFixed(1)}K` : String(Math.round(n));
19
- }
9
+ import type { ContextBreakdown, TrajTokens } from "@/lib/types";
20
10
 
21
- /** 13 类分段的定义(顺序即渲染顺序) */
22
- interface Segment {
23
- key: string;
24
- label: string;
25
- tokens: number;
26
- color: string;
27
- }
28
-
29
- export function ContextMeter({
30
- percent,
31
- tokens,
32
- contextWindow,
33
- source,
34
- }: {
11
+ export function ContextMeter({ percent, tokens, contextWindow, model, source }: {
35
12
  percent: number | null;
36
13
  tokens: number | null;
37
14
  contextWindow: number | null;
38
- /** 分段数据源:消息字符统计只在弹窗打开时执行(流式期间消息每 token 都变,不能常驻扫描) */
15
+ /** 当前会话的模型:有它才能在弹窗里改上下文窗口 */
16
+ model?: { provider: string; id: string } | null;
39
17
  source?: {
40
18
  systemChars: number;
41
- messages: { role?: string; content: Array<{ type: string; text?: string; thinking?: string; arguments?: unknown }> }[];
19
+ messages: Array<{ role?: string; content: Array<{ type: string; text?: string; thinking?: string; arguments?: unknown; name?: string }>; usage?: TrajTokens; stopReason?: string }>;
42
20
  breakdown?: ContextBreakdown;
21
+ loadFull?: () => Promise<unknown> | unknown;
43
22
  };
44
23
  }) {
45
24
  const [open, setOpen] = useState(false);
25
+ const [counting, setCounting] = useState(false);
46
26
  const ref = useRef<HTMLDivElement>(null);
47
27
  const { t } = useI18n();
28
+ const cold = Boolean(source?.breakdown?.cold);
48
29
 
30
+ useEffect(() => { if (!cold) setCounting(false); }, [cold]);
49
31
  useEffect(() => {
50
32
  if (!open) return;
51
- const h = (e: MouseEvent) => {
52
- if (!ref.current?.contains(e.target as Node)) setOpen(false);
53
- };
33
+ const h = (e: MouseEvent) => { if (!ref.current?.contains(e.target as Node)) setOpen(false); };
54
34
  document.addEventListener("mousedown", h);
55
35
  return () => document.removeEventListener("mousedown", h);
56
36
  }, [open]);
57
37
 
58
- const used = tokens ?? 0;
59
- const window_ = contextWindow ?? 0;
60
- const b = source?.breakdown;
38
+ // 流式期间只在打开弹窗后扫描,分类是实际文本字符数,不是假设的 token 数。
39
+ const chars = useMemo(() => classifyMessageChars(open ? source?.messages ?? [] : []), [open, source]);
40
+ const lastUsage = useMemo(() => open ? source?.messages.findLast((m) => m.role === "assistant" && m.stopReason !== "error" && m.stopReason !== "aborted" && m.stopReason !== "pending" &&
41
+ (m.usage?.input ?? 0) + (m.usage?.cacheRead ?? 0) + (m.usage?.cacheWrite ?? 0) + (m.usage?.output ?? 0) > 0)?.usage : undefined, [open, source]);
42
+ const measuredInput = lastUsage ? (lastUsage.input ?? 0) + (lastUsage.cacheRead ?? 0) + (lastUsage.cacheWrite ?? 0) : null;
61
43
 
62
- // 消息分项只在弹窗打开时统计(≈4 字符/token,与 SDK 一致)
63
- const messageSegments = useMemo(() => {
64
- if (!open) return { user: 0, agentText: 0, agentThinking: 0, agentToolCall: 0, toolOutput: 0 };
65
- const chars = classifyMessageChars(source?.messages ?? []);
66
- return {
67
- user: Math.max(0, Math.round(chars.user / 4)),
68
- agentText: Math.max(0, Math.round(chars.agentText / 4)),
69
- agentThinking: Math.max(0, Math.round(chars.agentThinking / 4)),
70
- agentToolCall: Math.max(0, Math.round(chars.agentToolCall / 4)),
71
- toolOutput: Math.max(0, Math.round(chars.toolOutput / 4)),
72
- };
73
- }, [open, source]);
44
+ // 刚改过窗口:服务端已落盘,但页面上的 contextWindow 还是改之前的值(冷会话要等下一次快照)。
45
+ // 在它变之前先显示刚保存的值;contextWindow 一变(快照 / 用量追上来,或别处改了)就以 props 为准。
46
+ const modelKey = model ? `${model.provider}/${model.id}` : "";
47
+ const [applied, setApplied] = useState<{ key: string; from: number; value: number } | null>(null);
48
+ useEffect(() => {
49
+ if (applied && (applied.key !== modelKey || contextWindow !== applied.from)) setApplied(null);
50
+ }, [applied, modelKey, contextWindow]);
51
+ const override = applied && applied.key === modelKey && contextWindow === applied.from ? applied.value : null;
74
52
 
75
- const p = percent ?? 0;
76
- const usageLabel = percent === null ? t.contextUnknown : `${Math.round(p)}% ${t.contextUsed}`;
53
+ const known = tokens !== null && Number.isFinite(tokens) && tokens >= 0;
54
+ const used = known ? tokens : 0;
55
+ const window_ = override ?? contextWindow ?? 0;
56
+ const recomputed = override !== null && known;
57
+ const p = recomputed ? used / override * 100 : percent ?? 0;
58
+ const usageLabel = percent === null && !recomputed ? t.contextUnknown : `${fmtPercent(p)} ${t.contextEstimated}`;
77
59
  const r = 5.5;
78
- const c = 2 * Math.PI * r;
79
- const shown = Math.max(0, Math.min(100, p));
80
-
81
- const segments: Segment[] = b
82
- ? [
83
- { key: "systemPrompt", label: t.contextSystemPrompt, tokens: b.systemPrompt, color: "var(--dsw-accent)" },
84
- { key: "systemTools", label: t.contextSystemTools, tokens: b.systemTools, color: "#8B7EC8" },
85
- { key: "customTools", label: t.contextCustomTools, tokens: b.customTools, color: "#B39DDB" },
86
- { key: "memory", label: t.contextMemory, tokens: b.memory, color: "#5FA8D3" },
87
- { key: "skills", label: t.contextSkills, tokens: b.skills, color: "#4FB286" },
88
- { key: "user", label: t.contextUserMessages, tokens: messageSegments.user, color: "var(--dsw-success)" },
89
- { key: "agentText", label: t.contextAgentText, tokens: messageSegments.agentText, color: "#7BC96F" },
90
- { key: "agentThinking", label: t.contextAgentThinking, tokens: messageSegments.agentThinking, color: "#C9A66B" },
91
- { key: "agentToolCall", label: t.contextAgentToolCall, tokens: messageSegments.agentToolCall, color: "#D3A15F" },
92
- { key: "toolOutput", label: t.contextToolOutput, tokens: messageSegments.toolOutput, color: "var(--dsw-warn)" },
93
- { key: "compacted", label: t.contextCompacted, tokens: b.compacted, color: "#A05A78" },
94
- { key: "buffer", label: t.contextAutoCompactBuffer, tokens: b.autoCompactBuffer, color: "var(--dsw-border-l3)" },
95
- ]
96
- : // 服务端分项缺失(旧快照):退回旧三段口径
97
- [
98
- { key: "system", label: t.contextSystem, tokens: Math.max(0, Math.round((source?.systemChars ?? 0) / 4)), color: "var(--dsw-accent)" },
99
- { key: "messages", label: t.contextMessages, tokens: messageSegments.user + messageSegments.agentText + messageSegments.agentThinking, color: "var(--dsw-success)" },
100
- { 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)" },
101
- ];
102
-
103
- // Free Space:窗口余量(窗口未知则不显示);分段条不归一化到已用之和
104
- const usedSum = segments.reduce((sum, s) => sum + s.tokens, 0);
105
- const free = window_ > 0 ? Math.max(0, window_ - usedSum) : 0;
106
- const barTotal = window_ > 0 ? window_ : usedSum || 1;
107
- const bar = (n: number) => `${(n / barTotal) * 100}%`;
60
+ const circumference = 2 * Math.PI * r;
61
+ const reserved = Math.max(0, source?.breakdown?.autoCompactBuffer ?? 0);
62
+ // 超过窗口时预留不再画到窗口外;未知用量不能将整扇窗口显示为 Free Space。
63
+ const visibleReserved = known && window_ > 0 ? Math.min(reserved, Math.max(0, window_ - used)) : 0;
64
+ const free = known && window_ > 0 ? Math.max(0, window_ - used - reserved) : null;
65
+ const b = source?.breakdown?.characters;
66
+ const rows = [
67
+ { key: "system", label: t.contextSystemPrompt, value: b?.systemPrompt, color: "var(--dsw-accent)" },
68
+ { key: "builtin", label: t.contextSystemTools, value: b?.systemTools, color: "#8B7EC8" },
69
+ { key: "custom", label: t.contextCustomTools, value: b?.customTools, color: "#B39DDB" },
70
+ { key: "memory", label: t.contextMemory, value: b?.memory, color: "#5FA8D3" },
71
+ { key: "skills", label: t.contextSkills, value: b?.skills, color: "#4FB286" },
72
+ { key: "user", label: t.contextUserMessages, value: chars.user, color: "var(--dsw-success)" },
73
+ { key: "text", label: t.contextAgentText, value: chars.agentText, color: "#7BC96F" },
74
+ { key: "thinking", label: t.contextAgentThinking, value: chars.agentThinking, color: "#C9A66B" },
75
+ { key: "call", label: t.contextAgentToolCall, value: chars.agentToolCall, color: "#D3A15F" },
76
+ { key: "output", label: t.contextToolOutput, value: chars.toolOutput, color: "var(--dsw-warn)" },
77
+ { key: "compacted", label: t.contextCompacted, value: b?.compacted, color: "#A05A78" },
78
+ ].filter((row) => row.value !== undefined && row.value > 0);
108
79
 
109
80
  return (
110
81
  <div ref={ref} className="relative">
111
- <button
112
- className="icon-btn"
113
- style={{ width: 28, height: 28 }}
114
- title={usageLabel}
115
- onClick={() => setOpen((o) => !o)}
116
- >
82
+ <button className="icon-btn" style={{ width: 28, height: 28 }} title={usageLabel} aria-label={usageLabel} onClick={() => setOpen((o) => !o)}>
117
83
  <svg width="14" height="14" viewBox="0 0 14 14">
118
84
  <circle cx="7" cy="7" r={r} fill="none" stroke="var(--dsw-border-l3)" strokeWidth="2" />
119
- <circle
120
- cx="7" cy="7" r={r}
121
- fill="none"
122
- stroke={p != null && p > 85 ? "var(--dsw-warn)" : "var(--dsw-accent)"}
123
- strokeWidth="2"
124
- strokeDasharray={`${(c * shown) / 100} ${c}`}
125
- transform="rotate(-90 7 7)"
126
- strokeLinecap="round"
127
- />
85
+ <circle cx="7" cy="7" r={r} fill="none" stroke={p > 85 ? "var(--dsw-warn)" : "var(--dsw-accent)"} strokeWidth="2"
86
+ strokeDasharray={`${circumference * Math.max(0, Math.min(100, p)) / 100} ${circumference}`} transform="rotate(-90 7 7)" strokeLinecap="round" />
128
87
  </svg>
129
88
  </button>
130
89
  {open && (
131
- <div
132
- className="popover absolute bottom-9 right-0 z-50 w-64 p-4"
133
- style={{ fontSize: "var(--dsh-content-font-size-secondary)" }}
134
- >
135
- <div className="font-medium" style={{ color: "var(--dsw-label-primary)" }}>
136
- {usageLabel}
137
- </div>
138
- <div style={{ color: "var(--dsw-label-tertiary)" }} className="mt-1">
139
- {tokens === null ? "—" : `~${fmtTok(used)}`} / {window_ > 0 ? fmtTok(window_) : "—"}
90
+ <div className="popover absolute bottom-9 right-0 z-50 w-72 p-4" style={{ fontSize: "var(--dsh-content-font-size-secondary)" }}>
91
+ <div className="font-medium" style={{ color: "var(--dsw-label-primary)" }}>{usageLabel}</div>
92
+ <div className="mt-1" style={{ color: "var(--dsw-label-tertiary)" }}>
93
+ {known ? `~${fmtTokens(used)}` : "—"} / {window_ > 0 ? fmtTokens(window_) : "—"} tok
140
94
  </div>
141
-
142
- {/* 分段占用条(mac 磁盘存储条式多色分段,含 Free Space) */}
143
- {usedSum > 0 && (
95
+ {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>}
96
+ {known && window_ > 0 && (
144
97
  <div className="mt-3 flex h-2 w-full overflow-hidden rounded-full" style={{ background: "var(--dsw-selector)" }} aria-hidden>
145
- {segments.filter((s) => s.tokens > 0).map((s) => (
146
- <span key={s.key} style={{ width: bar(s.tokens), background: s.color }} />
147
- ))}
148
- {free > 0 && <span style={{ width: bar(free), background: "transparent" }} />}
98
+ <span className="flex-none" style={{ width: `${Math.min(100, used / window_ * 100)}%`, background: "var(--dsw-accent)" }} />
99
+ {visibleReserved > 0 && <span className="flex-none" style={{ width: `${visibleReserved / window_ * 100}%`, background: "var(--dsw-border-l3)" }} />}
149
100
  </div>
150
101
  )}
151
102
  <div className="mt-2 flex flex-col gap-1" style={{ color: "var(--dsw-label-tertiary)" }}>
152
- {segments.filter((s) => s.tokens > 0).map((s) => (
153
- <SegmentRow key={s.key} label={s.label} tokens={s.tokens} color={s.color} />
154
- ))}
155
- {window_ > 0 && <SegmentRow label={t.contextFreeSpace} tokens={free} color="var(--dsw-selector)" />}
103
+ {reserved > 0 && <DataRow label={t.contextAutoCompactBuffer} value={`${fmtTokens(reserved)} tok`} exact={`${reserved} tok`} color="var(--dsw-border-l3)" />}
104
+ {window_ > 0 && <DataRow label={t.contextFreeSpace} value={free === null ? "—" : `~${fmtTokens(free)} tok`} color="var(--dsw-selector)" />}
105
+ </div>
106
+ <div className="mt-3 font-medium">{t.contextCharacterBreakdown}</div>
107
+ <div className="mt-1" style={{ fontSize: 11.5, color: "var(--dsw-label-tertiary)" }}>{t.contextCharacterHint}</div>
108
+ <div className="mt-2 flex flex-col gap-1" style={{ color: "var(--dsw-label-tertiary)" }}>
109
+ {rows.map((row) => <DataRow key={row.key} label={row.label} value={`${fmtTokens(row.value!)} ${t.contextChars}`} exact={`${row.value} ${t.contextChars}`} color={row.color} />)}
156
110
  </div>
111
+ {cold && source?.loadFull && (
112
+ <div className="mt-3 flex items-center justify-between gap-2" style={{ color: "var(--dsw-label-tertiary)" }}>
113
+ <span className="min-w-0" style={{ fontSize: 11.5 }}>{t.contextColdHint}</span>
114
+ <button type="button" className="btn-outline flex-none" style={{ height: 24, padding: "0 10px", fontSize: 12 }} disabled={counting}
115
+ onClick={() => {
116
+ setCounting(true);
117
+ void Promise.resolve(source.loadFull?.()).then((result) => {
118
+ if ((result as { success?: boolean } | undefined)?.success === false) setCounting(false);
119
+ }, () => setCounting(false));
120
+ }}>{counting ? t.contextCounting : t.contextCountNow}</button>
121
+ </div>
122
+ )}
123
+ {model && window_ > 0 && (
124
+ <WindowEditor key={modelKey} model={model} value={window_} onSaved={(value) => setApplied({ key: modelKey, from: contextWindow ?? 0, value })} />
125
+ )}
157
126
  </div>
158
127
  )}
159
128
  </div>
160
129
  );
161
130
  }
162
131
 
163
- function SegmentRow({ label, tokens, color }: { label: string; tokens: number; color: string }) {
164
- if (tokens <= 0) return null;
132
+ async function postModels<T>(body: Record<string, unknown>): Promise<T> {
133
+ const response = await fetch("/api/models", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) });
134
+ const payload = (await response.json().catch(() => null)) as { success?: boolean; data?: T; error?: string } | null;
135
+ if (!response.ok || !payload?.success) throw new Error(payload?.error || `request failed (${response.status})`);
136
+ return payload.data as T;
137
+ }
138
+
139
+ /**
140
+ * 弹窗底部的上下文窗口:平时显示精确值,「修改」后填数字保存。
141
+ * 不用滑块:模型的真实上限是 128000、200000 这类精确值,拖出来的近似值多一点上游就 400。
142
+ * 内置目录有原值就并排给出、填大了提醒;改的是该模型的全局配置,这一点写在输入框下面。
143
+ */
144
+ function WindowEditor({ model, value, onSaved }: { model: { provider: string; id: string }; value: number; onSaved: (value: number) => void }) {
145
+ const { t } = useI18n();
146
+ const [editing, setEditing] = useState(false);
147
+ const [text, setText] = useState("");
148
+ /** undefined = 还在查(不显示上限提示,免得先闪一句「没有内置值」) */
149
+ const [builtin, setBuiltin] = useState<number | null | undefined>(undefined);
150
+ const [saving, setSaving] = useState(false);
151
+ const [error, setError] = useState<string | null>(null);
152
+ const infoSeq = useRef(0);
153
+ const parsed = parseCapacity(text);
154
+ const target = parsed !== undefined && !Number.isNaN(parsed) ? parsed : null;
155
+ const valid = target !== null;
156
+ const over = target !== null && typeof builtin === "number" && target > builtin;
157
+ const exact = (n: number) => n.toLocaleString("en-US");
158
+
159
+ const begin = () => {
160
+ setEditing(true);
161
+ setText(formatCapacity(value));
162
+ setError(null);
163
+ setBuiltin(undefined);
164
+ const seq = ++infoSeq.current;
165
+ void postModels<{ builtin: number | null }>({ action: "contextWindowInfo", providerId: model.provider, modelId: model.id }).then(
166
+ (info) => { if (seq === infoSeq.current) setBuiltin(info.builtin); },
167
+ () => { if (seq === infoSeq.current) setBuiltin(null); },
168
+ );
169
+ };
170
+ const cancel = () => {
171
+ infoSeq.current += 1;
172
+ setEditing(false);
173
+ };
174
+ const save = async () => {
175
+ if (target === null || saving) return;
176
+ setSaving(true);
177
+ setError(null);
178
+ try {
179
+ const info = await postModels<{ contextWindow: number }>({ action: "setContextWindow", providerId: model.provider, modelId: model.id, contextWindow: target });
180
+ onSaved(info.contextWindow);
181
+ setEditing(false);
182
+ } catch (err) {
183
+ setError(err instanceof Error ? err.message : String(err));
184
+ } finally {
185
+ setSaving(false);
186
+ }
187
+ };
188
+
189
+ if (!editing) {
190
+ return (
191
+ <div className="hairline-t mt-3 flex items-center justify-between gap-2 pt-3" style={{ color: "var(--dsw-label-tertiary)" }}>
192
+ <span>{t.contextWindowLabel}</span>
193
+ <span className="flex items-center gap-2">
194
+ <span style={{ color: "var(--dsw-label-secondary)", fontFamily: "var(--font-mono)" }}>{exact(value)}</span>
195
+ <button type="button" className="btn-outline" style={{ height: 22, padding: "0 8px", fontSize: 11.5 }} onClick={begin}>{t.contextWindowEdit}</button>
196
+ </span>
197
+ </div>
198
+ );
199
+ }
165
200
  return (
166
- <div className="flex items-center justify-between gap-2">
167
- <span className="flex items-center gap-1.5">
168
- <span className="inline-block rounded-full" style={{ width: 7, height: 7, background: color }} />
169
- {label}
170
- </span>
171
- <span>~{fmtTok(tokens)}</span>
201
+ <div className="hairline-t mt-3 pt-3" style={{ color: "var(--dsw-label-tertiary)" }}>
202
+ <div>{t.contextWindowLabel}</div>
203
+ <div className="mt-1.5 flex items-center gap-1.5">
204
+ <input
205
+ autoFocus
206
+ value={text}
207
+ onChange={(e) => setText(e.target.value)}
208
+ onKeyDown={(e) => {
209
+ if (e.key === "Enter") {
210
+ e.preventDefault();
211
+ void save();
212
+ } else if (e.key === "Escape") {
213
+ e.preventDefault();
214
+ e.stopPropagation();
215
+ cancel();
216
+ }
217
+ }}
218
+ aria-label={t.contextWindowLabel}
219
+ aria-invalid={!valid}
220
+ spellCheck={false}
221
+ className="min-w-0 flex-1 rounded-md px-2"
222
+ 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)"}` }}
223
+ />
224
+ <button type="button" className="btn-primary-white flex-none" style={{ height: 26, padding: "0 10px", fontSize: 12 }} disabled={!valid || saving} onClick={() => void save()}>
225
+ {saving ? t.saving : t.save}
226
+ </button>
227
+ <button type="button" className="btn-outline flex-none" style={{ height: 26, padding: "0 10px", fontSize: 12 }} disabled={saving} onClick={cancel}>{t.cancel}</button>
228
+ </div>
229
+ {typeof builtin === "number" && (
230
+ <div className="mt-1.5 flex items-center gap-1.5" style={{ fontSize: 11.5 }}>
231
+ <span>{t.contextWindowBuiltin.replace("{value}", exact(builtin))}</span>
232
+ {target !== builtin && (
233
+ <button type="button" className="underline" style={{ color: "var(--dsw-label-secondary)" }} onClick={() => setText(formatCapacity(builtin))}>{t.contextWindowUseBuiltin}</button>
234
+ )}
235
+ </div>
236
+ )}
237
+ {!valid ? (
238
+ <div className="mt-1" style={{ fontSize: 11.5, color: "var(--dsw-danger)" }}>{t.contextWindowInvalid}</div>
239
+ ) : over ? (
240
+ <div className="mt-1" style={{ fontSize: 11.5, color: "var(--dsw-warn)" }}>{t.contextWindowOverBuiltin}</div>
241
+ ) : builtin === null ? (
242
+ <div className="mt-1" style={{ fontSize: 11.5 }}>{t.contextWindowNoBuiltin}</div>
243
+ ) : null}
244
+ <div className="mt-1" style={{ fontSize: 11.5 }}>{t.contextWindowScope}</div>
245
+ {error && <div className="mt-1" role="alert" style={{ fontSize: 11.5, color: "var(--dsw-danger)" }}>{error}</div>}
172
246
  </div>
173
247
  );
174
248
  }
249
+
250
+ function DataRow({ label, value, exact, color }: { label: string; value: string; exact?: string; color: string }) {
251
+ return <div className="flex items-center justify-between gap-2">
252
+ <span className="flex items-center gap-1.5"><span className="inline-block rounded-full" style={{ width: 7, height: 7, background: color }} />{label}</span>
253
+ <span title={exact}>{value}</span>
254
+ </div>;
255
+ }
@@ -87,22 +87,10 @@ export function DrawerPanel({
87
87
 
88
88
  return (
89
89
  <div className="flex h-full min-h-0 flex-col" data-testid="drawer-panel">
90
- {/* 头部:与 PromptPanel / ProjectPanel 同一套排布。
91
- 提示词 / Git 两个面板自带同款标题行(含关闭),这里就不再叠一层;
92
- 「导入会话」段落没有标题行,抽屉自己出这行,并关掉整个抽屉。 */}
93
- {section === "import" && (
94
- <div className="hairline-b flex items-center gap-1.5 px-3 pb-2 pt-3">
95
- <span className="min-w-0 flex-1 truncate" style={{ fontSize: 13, fontWeight: 600 }}>
96
- {t.drawerPanel}
97
- </span>
98
- <button className="icon-btn" style={{ width: 22, height: 22 }} title={t.close} aria-label={t.close} onClick={onClose}>
99
- <IconCloseOutline14 size={13} />
100
- </button>
101
- </div>
102
- )}
103
-
104
90
  {/* 入口列表:横向一排图标,不显示文字(名字放 title / aria-label)。
105
- 激活态复用设置页那套下划线(.settings-tab-underline),不自创样式。 */}
91
+ 激活态复用设置页那套下划线(.settings-tab-underline),不自创样式。
92
+ 悬停底色交给 CSS(.drawer-tab:hover):以前用 onMouseEnter/Leave 直接改 style,
93
+ 悬停时点下去这一项变成选中,移开时「选中就不复原」,底色就永远留在上面。 */}
106
94
  <div className="hairline-b flex items-stretch gap-1 px-2" role="tablist" aria-label={t.drawerSections}>
107
95
  {entries.map((entry) => (
108
96
  <button
@@ -110,16 +98,10 @@ export function DrawerPanel({
110
98
  type="button"
111
99
  role="tab"
112
100
  aria-selected={section === entry.id}
113
- className="settings-tab-underline flex flex-1 items-center justify-center rounded-t-lg"
101
+ className="settings-tab-underline drawer-tab flex flex-1 items-center justify-center rounded-t-lg"
114
102
  data-active={section === entry.id}
115
103
  title={entry.label}
116
104
  aria-label={entry.label}
117
- onMouseEnter={(e) => {
118
- if (section !== entry.id) e.currentTarget.style.background = "var(--dsw-hover)";
119
- }}
120
- onMouseLeave={(e) => {
121
- if (section !== entry.id) e.currentTarget.style.background = "transparent";
122
- }}
123
105
  onClick={() => onSectionChange(entry.id)}
124
106
  data-testid={`drawer-entry-${entry.id}`}
125
107
  >
@@ -128,12 +110,23 @@ export function DrawerPanel({
128
110
  ))}
129
111
  </div>
130
112
 
131
- {/* 所选入口的内容:项目 / 提示词 / Git 三个面板自带标题行与滚动,导入段落沿用抽屉的留白 */}
113
+ {/* 所选入口的内容:项目 / 提示词 / Git 三个面板自带标题行与滚动。
114
+ 「导入会话」段落没有标题行,这里补一行,排布照抄 PromptPanel(同样在入口列表下面、关掉整个抽屉) */}
132
115
  <div className="flex min-h-0 flex-1 flex-col">
133
116
  {section === "import" ? (
134
- <div className="min-h-0 flex-1 overflow-y-auto px-3 py-3">
135
- <SessionImportSection cwd={cwd} />
136
- </div>
117
+ <>
118
+ <div className="hairline-b flex items-center gap-2 px-4 py-2.5">
119
+ <span className="min-w-0 flex-1 truncate" style={{ fontSize: 13.5, fontWeight: 600 }}>
120
+ {t.importSessions}
121
+ </span>
122
+ <button type="button" className="icon-btn" style={{ width: 26, height: 26 }} title={t.close} aria-label={t.close} onClick={onClose}>
123
+ <IconCloseOutline14 size={14} />
124
+ </button>
125
+ </div>
126
+ <div className="min-h-0 flex-1 overflow-y-auto px-3 py-3">
127
+ <SessionImportSection cwd={cwd} />
128
+ </div>
129
+ </>
137
130
  ) : section === "project" ? (
138
131
  <ProjectPanel
139
132
  growth={growth}
@@ -152,9 +152,12 @@ export function ExtensionDialogHost({
152
152
  export function ExtensionNotices({
153
153
  notices,
154
154
  onDismiss,
155
+ children,
155
156
  }: {
156
157
  notices: { id: string; message: string; type: "info" | "warning" | "error"; ts: number }[];
157
158
  onDismiss: (id: string) => void;
159
+ /** 同一摞里的其它常驻提示(如更新提示条),排在扩展通知上面,免得两处浮层叠在一起 */
160
+ children?: React.ReactNode;
158
161
  }) {
159
162
  // 通知 6 秒后自动消失
160
163
  useEffect(() => {
@@ -162,10 +165,13 @@ export function ExtensionNotices({
162
165
  const timers = notices.map((n) => setTimeout(() => onDismiss(n.id), Math.max(500, 6000 - (Date.now() - n.ts))));
163
166
  return () => timers.forEach(clearTimeout);
164
167
  }, [notices, onDismiss]);
165
- if (!notices.length) return null;
168
+ // 有 children 时容器常驻:children 自己决定显示与否,容器里没东西时什么也看不见(pointer-events-none)
169
+ if (!notices.length && !children) return null;
166
170
  const color = (type: string) => (type === "error" ? "var(--dsw-danger)" : type === "warning" ? "var(--dsw-warn)" : "var(--dsw-accent)");
171
+ // 手机上输入卡占满底部,放底部会盖住发送按钮:窄屏改挂到顶栏下面
167
172
  return (
168
- <div className="pointer-events-none fixed bottom-24 right-4 z-[110] flex w-[min(92vw,360px)] flex-col gap-2">
173
+ <div className="pointer-events-none fixed right-4 top-16 z-[110] flex w-[min(92vw,360px)] flex-col gap-2 sm:top-auto sm:bottom-24">
174
+ {children}
169
175
  {notices.map((n) => (
170
176
  <button
171
177
  key={n.id}