@rexvane/piweb 0.3.16 → 0.3.17

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 (288) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/app-path-routes-manifest.json +8 -11
  3. package/.next/build-manifest.json +3 -3
  4. package/.next/prerender-manifest.json +6 -6
  5. package/.next/react-loadable-manifest.json +4 -16
  6. package/.next/required-server-files.js +1 -3
  7. package/.next/required-server-files.json +1 -3
  8. package/.next/routes-manifest.json +0 -20
  9. package/.next/server/app/_global-error/page.js +2 -2
  10. package/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  11. package/.next/server/app/_global-error.html +1 -1
  12. package/.next/server/app/_global-error.rsc +1 -1
  13. package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  14. package/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  15. package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  16. package/.next/server/app/_not-found/page.js +1 -1
  17. package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  18. package/.next/server/app/_not-found.html +1 -1
  19. package/.next/server/app/_not-found.rsc +4 -7
  20. package/.next/server/app/_not-found.segments/_full.segment.rsc +4 -7
  21. package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -5
  22. package/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -5
  23. package/.next/server/app/api/agent/[id]/events/route.js +2 -2
  24. package/.next/server/app/api/agent/[id]/events/route.js.nft.json +1 -1
  25. package/.next/server/app/api/agent/[id]/route.js +1 -1
  26. package/.next/server/app/api/agent/[id]/route.js.nft.json +1 -1
  27. package/.next/server/app/api/agent/[id]/tree/route.js +1 -1
  28. package/.next/server/app/api/agent/[id]/tree/route.js.nft.json +1 -1
  29. package/.next/server/app/api/agent/new/route.js +1 -1
  30. package/.next/server/app/api/agent/new/route.js.nft.json +1 -1
  31. package/.next/server/app/api/config/route.js +1 -1
  32. package/.next/server/app/api/config/route.js.nft.json +1 -1
  33. package/.next/server/app/api/files/route.js +1 -1
  34. package/.next/server/app/api/files/route.js.nft.json +1 -1
  35. package/.next/server/app/api/git/route.js +2 -2
  36. package/.next/server/app/api/git/route.js.nft.json +1 -1
  37. package/.next/server/app/api/growth/route.js +1 -1
  38. package/.next/server/app/api/growth/route.js.nft.json +1 -1
  39. package/.next/server/app/api/health/route.js +1 -1
  40. package/.next/server/app/api/models/route.js +1 -1
  41. package/.next/server/app/api/models/route.js.nft.json +1 -1
  42. package/.next/server/app/api/pi-settings/route.js +1 -1
  43. package/.next/server/app/api/pi-settings/route.js.nft.json +1 -1
  44. package/.next/server/app/api/plugins/route.js +1 -1
  45. package/.next/server/app/api/plugins/route.js.nft.json +1 -1
  46. package/.next/server/app/api/prompts/route.js +1 -1
  47. package/.next/server/app/api/prompts/route.js.nft.json +1 -1
  48. package/.next/server/app/api/security/route.js +1 -1
  49. package/.next/server/app/api/security/route.js.nft.json +1 -1
  50. package/.next/server/app/api/sessions/[id]/export/route.js +1 -1
  51. package/.next/server/app/api/sessions/[id]/export/route.js.nft.json +1 -1
  52. package/.next/server/app/api/sessions/[id]/route.js +1 -1
  53. package/.next/server/app/api/sessions/[id]/route.js.nft.json +1 -1
  54. package/.next/server/app/api/sessions/import/route.js +3 -3
  55. package/.next/server/app/api/sessions/import/route.js.nft.json +1 -1
  56. package/.next/server/app/api/sessions/route.js +1 -1
  57. package/.next/server/app/api/sessions/route.js.nft.json +1 -1
  58. package/.next/server/app/api/skills/route.js +2 -2
  59. package/.next/server/app/api/skills/route.js.nft.json +1 -1
  60. package/.next/server/app/api/update/route.js +2 -2
  61. package/.next/server/app/api/update/route.js.nft.json +1 -1
  62. package/.next/server/app/api/version/route.js +1 -1
  63. package/.next/server/app/api/version/route.js.nft.json +1 -1
  64. package/.next/server/app/api/web-auth/route.js +1 -1
  65. package/.next/server/app/api/workspaces/route.js +1 -1
  66. package/.next/server/app/api/workspaces/route.js.nft.json +1 -1
  67. package/.next/server/app/icon.svg/route.js +1 -1
  68. package/.next/server/app/index.html +1 -1
  69. package/.next/server/app/index.rsc +6 -9
  70. package/.next/server/app/index.segments/__PAGE__.segment.rsc +4 -7
  71. package/.next/server/app/index.segments/_full.segment.rsc +6 -9
  72. package/.next/server/app/index.segments/_tree.segment.rsc +2 -5
  73. package/.next/server/app/login/page.js +1 -1
  74. package/.next/server/app/login/page_client-reference-manifest.js +1 -1
  75. package/.next/server/app/login.html +1 -1
  76. package/.next/server/app/login.rsc +5 -8
  77. package/.next/server/app/login.segments/_full.segment.rsc +5 -8
  78. package/.next/server/app/login.segments/_tree.segment.rsc +2 -5
  79. package/.next/server/app/login.segments/login/__PAGE__.segment.rsc +3 -6
  80. package/.next/server/app/page.js +1 -1
  81. package/.next/server/app/page_client-reference-manifest.js +1 -1
  82. package/.next/server/app-paths-manifest.json +8 -11
  83. package/.next/server/chunks/496.js +1 -1
  84. package/.next/server/chunks/532.js +1 -1
  85. package/.next/server/chunks/686.js +4 -8
  86. package/.next/server/middleware-build-manifest.js +1 -1
  87. package/.next/server/middleware-react-loadable-manifest.js +1 -1
  88. package/.next/server/pages/404.html +1 -1
  89. package/.next/server/pages/500.html +1 -1
  90. package/.next/server/server-reference-manifest.json +1 -1
  91. package/.next/static/chunks/2608.ec8ff1f5767c9056.js +1 -0
  92. package/.next/static/chunks/2716.93be8ad5c970c516.js +1 -0
  93. package/.next/static/chunks/4632.112d2e893d48d188.js +1 -0
  94. package/.next/static/chunks/4673.c1fb1580efc6ace7.js +1 -0
  95. package/.next/static/chunks/4686-f4a037d609cea3f2.js +1 -0
  96. package/.next/static/chunks/app/_global-error/page-f77e9ede9808b18e.js +1 -0
  97. package/.next/static/chunks/app/api/agent/[id]/events/route-f77e9ede9808b18e.js +1 -0
  98. package/.next/static/chunks/app/api/agent/[id]/route-f77e9ede9808b18e.js +1 -0
  99. package/.next/static/chunks/app/api/agent/[id]/tree/route-f77e9ede9808b18e.js +1 -0
  100. package/.next/static/chunks/app/api/agent/new/route-f77e9ede9808b18e.js +1 -0
  101. package/.next/static/chunks/app/api/config/route-f77e9ede9808b18e.js +1 -0
  102. package/.next/static/chunks/app/api/files/route-f77e9ede9808b18e.js +1 -0
  103. package/.next/static/chunks/app/api/git/route-f77e9ede9808b18e.js +1 -0
  104. package/.next/static/chunks/app/api/growth/route-f77e9ede9808b18e.js +1 -0
  105. package/.next/static/chunks/app/api/health/route-f77e9ede9808b18e.js +1 -0
  106. package/.next/static/chunks/app/api/models/route-f77e9ede9808b18e.js +1 -0
  107. package/.next/static/chunks/app/api/pi-settings/route-f77e9ede9808b18e.js +1 -0
  108. package/.next/static/chunks/app/api/plugins/route-f77e9ede9808b18e.js +1 -0
  109. package/.next/static/chunks/app/api/prompts/route-f77e9ede9808b18e.js +1 -0
  110. package/.next/static/chunks/app/api/security/route-f77e9ede9808b18e.js +1 -0
  111. package/.next/static/chunks/app/api/sessions/[id]/export/route-f77e9ede9808b18e.js +1 -0
  112. package/.next/static/chunks/app/api/sessions/[id]/route-f77e9ede9808b18e.js +1 -0
  113. package/.next/static/chunks/app/api/sessions/import/route-f77e9ede9808b18e.js +1 -0
  114. package/.next/static/chunks/app/api/sessions/route-f77e9ede9808b18e.js +1 -0
  115. package/.next/static/chunks/app/api/skills/route-f77e9ede9808b18e.js +1 -0
  116. package/.next/static/chunks/app/api/update/route-f77e9ede9808b18e.js +1 -0
  117. package/.next/static/chunks/app/api/version/route-f77e9ede9808b18e.js +1 -0
  118. package/.next/static/chunks/app/api/web-auth/route-f77e9ede9808b18e.js +1 -0
  119. package/.next/static/chunks/app/api/workspaces/route-f77e9ede9808b18e.js +1 -0
  120. package/.next/static/chunks/app/page-b51087831582ee37.js +1 -0
  121. package/.next/static/chunks/next/dist/client/components/builtin/app-error-f77e9ede9808b18e.js +1 -0
  122. package/.next/static/chunks/next/dist/client/components/builtin/forbidden-f77e9ede9808b18e.js +1 -0
  123. package/.next/static/chunks/next/dist/client/components/builtin/not-found-f77e9ede9808b18e.js +1 -0
  124. package/.next/static/chunks/next/dist/client/components/builtin/unauthorized-f77e9ede9808b18e.js +1 -0
  125. package/.next/static/chunks/webpack-c31263fd83d75ce6.js +1 -0
  126. package/.next/static/css/850c2fd130ffa2fc.css +1 -0
  127. package/.next/static/zsynuO_9Kx6nrXgkTUJ04/_buildManifest.js +1 -0
  128. package/.next/types/routes.d.ts +1 -4
  129. package/.next/types/validator.ts +0 -27
  130. package/README.md +17 -29
  131. package/assets/archive.jpg +0 -0
  132. package/assets/growth-1.jpg +0 -0
  133. package/assets/growth-2.jpg +0 -0
  134. package/assets/import.jpg +0 -0
  135. package/assets/main.jpg +0 -0
  136. package/assets/outline.jpg +0 -0
  137. package/assets/prompts-1.jpg +0 -0
  138. package/assets/prompts-2.jpg +0 -0
  139. package/assets/providers-1.jpg +0 -0
  140. package/assets/providers-2.jpg +0 -0
  141. package/assets/providers-3.jpg +0 -0
  142. package/assets/trace-1.jpg +0 -0
  143. package/assets/trace-2.png +0 -0
  144. package/assets/trace-3.png +0 -0
  145. package/assets/trace-4.png +0 -0
  146. package/assets/viewer-1.jpg +0 -0
  147. package/assets/viewer-2.jpg +0 -0
  148. package/bin/pi.js +50 -0
  149. package/next.config.ts +32 -7
  150. package/package.json +4 -9
  151. package/scripts/launcher.mjs +1 -1
  152. package/scripts/pick-folder.ps1 +2 -11
  153. package/scripts/release.mjs +31 -11
  154. package/scripts/verify-package.mjs +0 -53
  155. package/src/app/api/config/route.ts +1 -4
  156. package/src/app/api/growth/route.ts +14 -14
  157. package/src/app/api/models/route.ts +2 -1
  158. package/src/app/api/security/route.ts +3 -8
  159. package/src/app/api/update/route.ts +2 -8
  160. package/src/app/globals.css +90 -777
  161. package/src/app/layout.tsx +6 -13
  162. package/src/components/AppShell.tsx +229 -521
  163. package/src/components/ChatInput.tsx +26 -153
  164. package/src/components/ChatWindow.tsx +231 -51
  165. package/src/components/ContextMeter.tsx +17 -11
  166. package/src/components/FileViewer.tsx +11 -10
  167. package/src/components/GitPanel.tsx +11 -13
  168. package/src/components/ModelSelector.tsx +6 -19
  169. package/src/components/PiMark.tsx +38 -5
  170. package/src/components/ProjectPanel.tsx +111 -95
  171. package/src/components/PromptPanel.tsx +51 -132
  172. package/src/components/SessionImportSection.tsx +37 -53
  173. package/src/components/SessionSidebar.tsx +57 -159
  174. package/src/components/SettingsPanel.tsx +193 -330
  175. package/src/components/icons.tsx +0 -23
  176. package/src/hooks/useGrowth.ts +123 -156
  177. package/src/hooks/usePiWeb.ts +15 -122
  178. package/src/i18n.tsx +42 -98
  179. package/src/lib/agent-manager.ts +21 -187
  180. package/src/lib/command-validation.ts +1 -9
  181. package/src/lib/growth-service.ts +390 -166
  182. package/src/lib/growth-tracker.ts +64 -264
  183. package/src/lib/growth-turns.ts +31 -1
  184. package/src/lib/models/model-scope.ts +15 -0
  185. package/src/lib/models-service.ts +16 -0
  186. package/src/lib/pi-settings.ts +7 -0
  187. package/src/lib/pi.ts +2 -3
  188. package/src/lib/session-import/claude.ts +2 -13
  189. package/src/lib/session-import/codex.ts +8 -44
  190. package/src/lib/session-import/files.ts +0 -14
  191. package/src/lib/session-import/grok.ts +24 -54
  192. package/src/lib/session-import/index.ts +0 -5
  193. package/src/lib/session-import/types.ts +0 -12
  194. package/src/lib/session-import/writer.ts +3 -8
  195. package/src/lib/skills-service.ts +9 -16
  196. package/src/lib/theme.ts +1 -68
  197. package/src/lib/trajectory.ts +0 -1
  198. package/src/lib/types.ts +27 -36
  199. package/tsconfig.json +2 -1
  200. package/.next/server/app/api/swarm/[id]/route.js +0 -1
  201. package/.next/server/app/api/swarm/[id]/route.js.nft.json +0 -1
  202. package/.next/server/app/api/swarm/[id]/route_client-reference-manifest.js +0 -1
  203. package/.next/server/app/api/swarm/route.js +0 -1
  204. package/.next/server/app/api/swarm/route.js.nft.json +0 -1
  205. package/.next/server/app/api/swarm/route_client-reference-manifest.js +0 -1
  206. package/.next/server/app/api/tests/route.js +0 -2
  207. package/.next/server/app/api/tests/route.js.nft.json +0 -1
  208. package/.next/server/app/api/tests/route_client-reference-manifest.js +0 -1
  209. package/.next/server/chunks/757.js +0 -1
  210. package/.next/server/chunks/775.js +0 -3
  211. package/.next/static/0t1jBUAuVaTFTXxRyIo3q/_buildManifest.js +0 -1
  212. package/.next/static/chunks/2608.43c58236c9cce8e6.js +0 -1
  213. package/.next/static/chunks/2716.4ec42df1d51354c9.js +0 -1
  214. package/.next/static/chunks/4632.62139b565b657aac.js +0 -1
  215. package/.next/static/chunks/4673.7796104395deb8d7.js +0 -1
  216. package/.next/static/chunks/4686-17f21a006739a596.js +0 -1
  217. package/.next/static/chunks/7577.10af9bb661652da8.js +0 -1
  218. package/.next/static/chunks/8866.38ec4ac4ee5a1b74.js +0 -1
  219. package/.next/static/chunks/app/_global-error/page-5148173b3f55e415.js +0 -1
  220. package/.next/static/chunks/app/api/agent/[id]/events/route-5148173b3f55e415.js +0 -1
  221. package/.next/static/chunks/app/api/agent/[id]/route-5148173b3f55e415.js +0 -1
  222. package/.next/static/chunks/app/api/agent/[id]/tree/route-5148173b3f55e415.js +0 -1
  223. package/.next/static/chunks/app/api/agent/new/route-5148173b3f55e415.js +0 -1
  224. package/.next/static/chunks/app/api/config/route-5148173b3f55e415.js +0 -1
  225. package/.next/static/chunks/app/api/files/route-5148173b3f55e415.js +0 -1
  226. package/.next/static/chunks/app/api/git/route-5148173b3f55e415.js +0 -1
  227. package/.next/static/chunks/app/api/growth/route-5148173b3f55e415.js +0 -1
  228. package/.next/static/chunks/app/api/health/route-5148173b3f55e415.js +0 -1
  229. package/.next/static/chunks/app/api/models/route-5148173b3f55e415.js +0 -1
  230. package/.next/static/chunks/app/api/pi-settings/route-5148173b3f55e415.js +0 -1
  231. package/.next/static/chunks/app/api/plugins/route-5148173b3f55e415.js +0 -1
  232. package/.next/static/chunks/app/api/prompts/route-5148173b3f55e415.js +0 -1
  233. package/.next/static/chunks/app/api/security/route-5148173b3f55e415.js +0 -1
  234. package/.next/static/chunks/app/api/sessions/[id]/export/route-5148173b3f55e415.js +0 -1
  235. package/.next/static/chunks/app/api/sessions/[id]/route-5148173b3f55e415.js +0 -1
  236. package/.next/static/chunks/app/api/sessions/import/route-5148173b3f55e415.js +0 -1
  237. package/.next/static/chunks/app/api/sessions/route-5148173b3f55e415.js +0 -1
  238. package/.next/static/chunks/app/api/skills/route-5148173b3f55e415.js +0 -1
  239. package/.next/static/chunks/app/api/swarm/[id]/route-5148173b3f55e415.js +0 -1
  240. package/.next/static/chunks/app/api/swarm/route-5148173b3f55e415.js +0 -1
  241. package/.next/static/chunks/app/api/tests/route-5148173b3f55e415.js +0 -1
  242. package/.next/static/chunks/app/api/update/route-5148173b3f55e415.js +0 -1
  243. package/.next/static/chunks/app/api/version/route-5148173b3f55e415.js +0 -1
  244. package/.next/static/chunks/app/api/web-auth/route-5148173b3f55e415.js +0 -1
  245. package/.next/static/chunks/app/api/workspaces/route-5148173b3f55e415.js +0 -1
  246. package/.next/static/chunks/app/page-14379b8060d5cf07.js +0 -1
  247. package/.next/static/chunks/next/dist/client/components/builtin/app-error-5148173b3f55e415.js +0 -1
  248. package/.next/static/chunks/next/dist/client/components/builtin/forbidden-5148173b3f55e415.js +0 -1
  249. package/.next/static/chunks/next/dist/client/components/builtin/not-found-5148173b3f55e415.js +0 -1
  250. package/.next/static/chunks/next/dist/client/components/builtin/unauthorized-5148173b3f55e415.js +0 -1
  251. package/.next/static/chunks/webpack-d1a69ad9edb38b11.js +0 -1
  252. package/.next/static/css/d0a0162cb481abe8.css +0 -1
  253. package/.next/types/app/api/swarm/[id]/route.ts +0 -353
  254. package/.next/types/app/api/swarm/route.ts +0 -353
  255. package/.next/types/app/api/tests/route.ts +0 -353
  256. package/assets/archive.png +0 -0
  257. package/assets/growth-1.png +0 -0
  258. package/assets/growth-2.png +0 -0
  259. package/assets/import.png +0 -0
  260. package/assets/main.png +0 -0
  261. package/assets/outline.png +0 -0
  262. package/assets/prompts-1.png +0 -0
  263. package/assets/prompts-2.png +0 -0
  264. package/assets/providers-1.png +0 -0
  265. package/assets/providers-2.png +0 -0
  266. package/assets/providers-3.png +0 -0
  267. package/assets/trace-1.png +0 -0
  268. package/assets/viewer-1.png +0 -0
  269. package/assets/viewer-2.png +0 -0
  270. package/scripts/managed-niubash-assets.mjs +0 -146
  271. package/scripts/prepare-niubash-packages.mjs +0 -182
  272. package/scripts/run-test-file.mjs +0 -34
  273. package/scripts/smoke-managed-niubash.mjs +0 -62
  274. package/scripts/verify-release-config.mjs +0 -24
  275. package/src/app/api/swarm/[id]/route.ts +0 -28
  276. package/src/app/api/swarm/route.ts +0 -24
  277. package/src/app/api/tests/route.ts +0 -27
  278. package/src/components/SwarmCoordinatorPod.tsx +0 -1021
  279. package/src/components/TestInspectorPod.tsx +0 -549
  280. package/src/lib/delegation.ts +0 -93
  281. package/src/lib/growth-rounds.ts +0 -84
  282. package/src/lib/managed-niubash.ts +0 -231
  283. package/src/lib/runtime-activity.ts +0 -27
  284. package/src/lib/session-import/lock.ts +0 -25
  285. package/src/lib/swarm-service.ts +0 -371
  286. package/src/lib/test-run-service.ts +0 -122
  287. package/src/lib/workflow.ts +0 -55
  288. /package/.next/static/{0t1jBUAuVaTFTXxRyIo3q → zsynuO_9Kx6nrXgkTUJ04}/_ssgManifest.js +0 -0
@@ -22,7 +22,6 @@ import {
22
22
  IconFileOutline16,
23
23
  IconRefreshOutline14,
24
24
  IconSearchOutline16,
25
- IconSendArrowUp14,
26
25
  IconTerminalOutline14,
27
26
  IconThinkOutline14,
28
27
  IconWarningOutline16,
@@ -77,13 +76,54 @@ function formatMessageClock(time: number, t: Dict): string {
77
76
  return `${md} ${clock}`;
78
77
  }
79
78
 
79
+ type MarkdownTextNode = { value?: unknown; children?: MarkdownTextNode[] };
80
+ function markdownNodeText(node: MarkdownTextNode | undefined): string {
81
+ if (!node) return "";
82
+ if (typeof node.value === "string") return node.value;
83
+ return node.children?.map(markdownNodeText).join("") ?? "";
84
+ }
85
+
86
+ function CopyButton({ text }: { text: string }) {
87
+ const [copied, setCopied] = useState(false);
88
+ const [copyFailed, setCopyFailed] = useState(false);
89
+ const { t } = useI18n();
90
+ return (
91
+ <>
92
+ <button
93
+ type="button"
94
+ className="icon-btn"
95
+ title={copyFailed ? t.copyFailed : t.copy}
96
+ aria-label={t.copy}
97
+ onClick={() => {
98
+ void copyText(text).then((ok) => {
99
+ setCopied(ok);
100
+ setCopyFailed(!ok);
101
+ setTimeout(() => { setCopied(false); setCopyFailed(false); }, 1200);
102
+ });
103
+ }}
104
+ >
105
+ {copied ? <IconCheckOutline14 size={14} /> : <IconCopyOutline16 size={14} />}
106
+ </button>
107
+ {copyFailed && <span role="alert" style={{ fontSize: 11, color: "var(--dsw-danger)" }}>{t.copyFailed}</span>}
108
+ </>
109
+ );
110
+ }
111
+
80
112
  const Markdown = memo(function Markdown({ text }: { text: string }) {
81
113
  return (
82
- <div className="md select-text">
114
+ <div className="md">
83
115
  <ReactMarkdown
84
116
  remarkPlugins={[remarkGfm]}
85
117
  rehypePlugins={[[rehypeHighlight, { detect: false, ignoreMissing: true }]]}
86
- components={{ a: ({ node: _node, ...props }) => <a {...props} target="_blank" rel="noopener noreferrer" /> }}
118
+ components={{
119
+ a: ({ node: _node, ...props }) => <a {...props} target="_blank" rel="noopener noreferrer" />,
120
+ pre: ({ node, children, ...props }) => (
121
+ <div className="md-code-block">
122
+ <div className="md-code-copy"><CopyButton text={markdownNodeText(node)} /></div>
123
+ <pre {...props}>{children}</pre>
124
+ </div>
125
+ ),
126
+ }}
87
127
  >
88
128
  {text}
89
129
  </ReactMarkdown>
@@ -888,8 +928,16 @@ const FinalAnswer = memo(function FinalAnswer({
888
928
  );
889
929
  });
890
930
 
931
+ /** 对话里提问下方的「本轮改了 N 个文件」:来自项目生长这一轮的 commit */
932
+ export interface RoundBadge {
933
+ commit: string;
934
+ files: number;
935
+ add: number;
936
+ del: number;
937
+ }
938
+
891
939
  /** 行级 memo:流式期间只有最后一条消息变化,历史行全部跳过重渲染 */
892
- const UserMessage = memo(function UserMessage({ message, onEditMessage }: { message: WebMessage; onEditMessage?: (entryId: string, text: string) => void }) {
940
+ const UserMessage = memo(function UserMessage({ message, badge, onShowRound, onEditMessage }: { message: WebMessage; badge?: RoundBadge; onShowRound?: (commit: string) => void; onEditMessage?: (entryId: string, text: string) => void }) {
893
941
  const { t } = useI18n();
894
942
  const text = message.content
895
943
  .filter((c): c is { type: "text"; text: string } => c.type === "text")
@@ -898,77 +946,89 @@ const UserMessage = memo(function UserMessage({ message, onEditMessage }: { mess
898
946
  const images = message.content.filter((content) => content.type === "image");
899
947
  const [editing, setEditing] = useState(false);
900
948
  const [draft, setDraft] = useState(text);
901
- const [editorWidth, setEditorWidth] = useState(132);
902
- const editRef = useRef<HTMLTextAreaElement>(null);
903
- const bubbleRef = useRef<HTMLDivElement>(null);
904
949
  const canEdit = Boolean(onEditMessage && message.id && text);
905
- useEffect(() => {
906
- if (!editing || !editRef.current) return;
907
- editRef.current.style.height = "0px";
908
- editRef.current.style.height = `${Math.min(320, editRef.current.scrollHeight)}px`;
909
- }, [draft, editing]);
910
- const imageStrip = images.length > 0 && (
911
- <div className="mb-2 flex max-w-[85%] flex-wrap justify-end gap-2">
912
- {images.map((image, index) => (
913
- <img key={index} src={`data:${image.mimeType};base64,${image.data}`} alt="" className="max-h-72 max-w-full rounded-2xl object-contain" style={{ border: "0.5px solid var(--dsw-border-l2)" }} />
914
- ))}
915
- </div>
916
- );
917
- function beginEdit() {
918
- if (!canEdit) return;
919
- setEditorWidth(Math.max(132, Math.ceil(bubbleRef.current?.getBoundingClientRect().width ?? 132)));
920
- setDraft(text);
921
- setEditing(true);
922
- }
923
950
  if (editing) {
924
951
  return (
925
- <div className="group mt-7 flex w-full flex-col items-end first:mt-0" data-role="user">
926
- {imageStrip}
927
- <div className="msg-user-bubble pw-user-editor" style={{ width: editorWidth }}>
952
+ <div className="group mt-7 flex w-full flex-col items-end first:mt-0" data-role="user" data-message-id={message.id}>
953
+ <div className="msg-user-bubble w-full">
928
954
  <textarea
929
- ref={editRef}
930
- className="pw-user-editor-input"
955
+ className="w-full resize-none bg-transparent outline-none"
956
+ style={{ minHeight: 68, font: "inherit", color: "inherit" }}
931
957
  value={draft}
932
958
  aria-label={t.editMessage}
933
959
  autoFocus
934
960
  onChange={(e) => setDraft(e.target.value)}
935
961
  onKeyDown={(e) => {
936
962
  if (e.key === "Escape") { e.preventDefault(); setEditing(false); }
937
- if (e.key === "Enter" && !e.shiftKey && !e.nativeEvent.isComposing) { e.preventDefault(); submit(); }
963
+ if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); submit(); }
938
964
  }}
939
965
  />
940
- <div className="pw-user-editor-actions">
941
- <button type="button" className="pw-user-editor-cancel" aria-label={t.cancel} title={t.cancel} onClick={() => setEditing(false)}><IconCloseOutline14 size={14} /></button>
942
- <button type="button" className="pw-user-editor-send" aria-label={t.sendEdit} title={t.sendEdit} disabled={!draft.trim()} onClick={submit}><IconSendArrowUp14 size={15} /></button>
943
- </div>
966
+ </div>
967
+ <div className="mt-1.5 flex items-center gap-2">
968
+ <button type="button" className="btn-outline" style={{ height: 26, padding: "0 10px", fontSize: 12 }} onClick={() => setEditing(false)}>
969
+ {t.cancel}
970
+ </button>
971
+ <button
972
+ type="button"
973
+ className="btn-primary-white"
974
+ style={{ height: 26, padding: "0 12px", fontSize: 12 }}
975
+ disabled={!draft.trim() || draft.trim() === text.trim()}
976
+ onClick={submit}
977
+ >
978
+ {t.sendEdit}
979
+ </button>
944
980
  </div>
945
981
  </div>
946
982
  );
947
983
  }
948
984
  function submit() {
949
985
  const next = draft.trim();
950
- if (!next || !onEditMessage || !message.id) return;
986
+ if (!next || next === text.trim() || !onEditMessage || !message.id) return;
951
987
  setEditing(false);
952
988
  onEditMessage(message.id, next);
953
989
  }
954
990
  return (
955
991
  // 回合边界:用户消息前留 28px(比回合内 8px 大得多),长对话里一眼找到“这一轮从哪开始”
956
- <div className="group mt-7 flex w-full flex-col items-end first:mt-0" data-role="user">
957
- {imageStrip}
992
+ <div className="group mt-7 flex w-full flex-col items-end first:mt-0" data-role="user" data-message-id={message.id}>
993
+ {images.length > 0 && (
994
+ <div className="mb-2 flex max-w-[85%] flex-wrap justify-end gap-2">
995
+ {images.map((image, index) => (
996
+ <img
997
+ key={index}
998
+ src={`data:${image.mimeType};base64,${image.data}`}
999
+ alt=""
1000
+ className="max-h-72 max-w-full rounded-2xl object-contain"
1001
+ style={{ border: "0.5px solid var(--dsw-border-l2)" }}
1002
+ />
1003
+ ))}
1004
+ </div>
1005
+ )}
958
1006
  {/* 用户消息也走 Markdown:贴进来的代码块/列表不再是一坨纯文本 */}
959
- {text && <div ref={bubbleRef} className={`msg-user-bubble clay-bubble-user${canEdit ? " pw-user-message-clickable" : ""}`} onClick={canEdit ? (event) => {
960
- if ((event.target as HTMLElement).closest("a, button, input, textarea, [role='button']")) return;
961
- const selection = window.getSelection();
962
- if (selection && !selection.isCollapsed) return;
963
- beginEdit();
964
- } : undefined}><Markdown text={text} /></div>}
1007
+ {text && <div className="msg-user-bubble"><Markdown text={text} /></div>}
1008
+ {badge && (
1009
+ <button
1010
+ type="button"
1011
+ className="pw-chip mt-1.5"
1012
+ title={t.chatRoundShow}
1013
+ onClick={() => onShowRound?.(badge.commit)}
1014
+ data-testid="round-badge"
1015
+ >
1016
+ {badge.files ? t.chatRoundChanged.replace("{n}", String(badge.files)) : t.chatRoundNoChanges}
1017
+ {badge.files > 0 && (
1018
+ <>
1019
+ <span style={{ color: "var(--dsw-success)" }}> +{badge.add}</span>
1020
+ <span style={{ color: "var(--dsw-danger)" }}> −{badge.del}</span>
1021
+ </>
1022
+ )}
1023
+ </button>
1024
+ )}
965
1025
  {/* 用户消息:复制 + 原地编辑重发(编辑后模型从这条消息重新回答);时钟在图标左侧(dsh clock=start) */}
966
1026
  {text && (
967
1027
  <MessageActions
968
1028
  text={text}
969
1029
  clockStart={message.timestamp !== undefined}
970
1030
  time={message.timestamp}
971
- onEdit={canEdit ? beginEdit : undefined}
1031
+ onEdit={canEdit ? () => { setDraft(text); setEditing(true); } : undefined}
972
1032
  />
973
1033
  )}
974
1034
  </div>
@@ -1021,6 +1081,8 @@ function TurnBlockView({
1021
1081
  workingMessage,
1022
1082
  onFork,
1023
1083
  onEditMessage,
1084
+ roundBadge,
1085
+ onShowRound,
1024
1086
  onInspectTool,
1025
1087
  onOpenFile,
1026
1088
  }: {
@@ -1040,6 +1102,8 @@ function TurnBlockView({
1040
1102
  onFork?: (entryId: string) => void;
1041
1103
  /** 编辑该回合的用户消息并重新发送(模型从这条消息重新回答) */
1042
1104
  onEditMessage?: (entryId: string, text: string) => void;
1105
+ roundBadge?: RoundBadge;
1106
+ onShowRound?: (commit: string) => void;
1043
1107
  onInspectTool?: (toolCallId: string) => void;
1044
1108
  onOpenFile?: (path: string) => void;
1045
1109
  }) {
@@ -1090,8 +1154,8 @@ function TurnBlockView({
1090
1154
  const showProcess = items.length > 0 || Boolean(contextFiles?.length) || turnLive || (isLast && Boolean(retryNotice));
1091
1155
 
1092
1156
  return (
1093
- <div className="pw-conversation-turn">
1094
- {user && <UserMessage message={user} onEditMessage={onEditMessage} />}
1157
+ <>
1158
+ {user && <UserMessage message={user} badge={roundBadge} onShowRound={onShowRound} onEditMessage={onEditMessage} />}
1095
1159
  {showProcess && (
1096
1160
  <div className="pw-turn">
1097
1161
  {contextFiles?.map((resource, i) => (
@@ -1121,7 +1185,7 @@ function TurnBlockView({
1121
1185
  </div>
1122
1186
  )}
1123
1187
  {finalIdx >= 0 && <FinalAnswer message={messages[finalIdx]} onFork={onFork} showActions={!isStreaming || !isLast} turnStartMs={user?.timestamp} />}
1124
- </div>
1188
+ </>
1125
1189
  );
1126
1190
  }
1127
1191
 
@@ -1206,6 +1270,11 @@ function turnBlockPropsEqual(prev: TurnBlockProps, next: TurnBlockProps): boolea
1206
1270
  prev.contextFiles !== next.contextFiles ||
1207
1271
  prev.onFork !== next.onFork ||
1208
1272
  prev.onEditMessage !== next.onEditMessage ||
1273
+ prev.onShowRound !== next.onShowRound ||
1274
+ prev.roundBadge?.commit !== next.roundBadge?.commit ||
1275
+ prev.roundBadge?.files !== next.roundBadge?.files ||
1276
+ prev.roundBadge?.add !== next.roundBadge?.add ||
1277
+ prev.roundBadge?.del !== next.roundBadge?.del ||
1209
1278
  prev.onInspectTool !== next.onInspectTool ||
1210
1279
  prev.onOpenFile !== next.onOpenFile
1211
1280
  ) return false;
@@ -1228,6 +1297,28 @@ function turnBlockPropsEqual(prev: TurnBlockProps, next: TurnBlockProps): boolea
1228
1297
 
1229
1298
  const TurnBlock = memo(TurnBlockView, turnBlockPropsEqual);
1230
1299
 
1300
+ const CHAT_WIDTH_KEY = "piweb.chatContentWidth";
1301
+ const CHAT_WIDTH_MIN = 480;
1302
+ const CHAT_WIDTH_MAX = 1200;
1303
+
1304
+ function resolveChatWidth(width: number, viewportWidth: number): number {
1305
+ const max = Math.max(320, Math.min(CHAT_WIDTH_MAX, viewportWidth - 64));
1306
+ return Math.max(Math.min(CHAT_WIDTH_MIN, max), Math.min(width, max));
1307
+ }
1308
+
1309
+ function setChatWidth(root: HTMLElement, width: number | null): void {
1310
+ const scope = root.closest<HTMLElement>(".pi-main");
1311
+ if (!scope) return;
1312
+ if (width === null) {
1313
+ scope.style.removeProperty("--dsh-chat-content-width");
1314
+ scope.style.removeProperty("--dsh-composer-card-max-width");
1315
+ return;
1316
+ }
1317
+ const resolved = resolveChatWidth(width, root.clientWidth);
1318
+ scope.style.setProperty("--dsh-chat-content-width", `${resolved}px`);
1319
+ scope.style.setProperty("--dsh-composer-card-max-width", `${resolved + 32}px`);
1320
+ }
1321
+
1231
1322
  export function ChatWindow({
1232
1323
  messages,
1233
1324
  tools,
@@ -1246,6 +1337,8 @@ export function ChatWindow({
1246
1337
  stats,
1247
1338
  onOpenTrajectory,
1248
1339
  onOpenFile,
1340
+ roundBadges,
1341
+ onShowRound,
1249
1342
  }: {
1250
1343
  messages: WebMessage[];
1251
1344
  tools: Record<string, ToolCardState>;
@@ -1272,10 +1365,31 @@ export function ChatWindow({
1272
1365
  /** 在本机编辑器打开工具行涉及的文件 */
1273
1366
  onEditMessage?: (entryId: string, text: string) => void;
1274
1367
  onOpenFile?: (path: string) => void;
1368
+ roundBadges?: ReadonlyMap<string, RoundBadge>;
1369
+ onShowRound?: (commit: string) => void;
1275
1370
  }) {
1276
1371
  const scrollRef = useRef<HTMLDivElement>(null);
1372
+ const rootRef = useRef<HTMLDivElement>(null);
1373
+ const widthPreference = useRef<number | null>(null);
1374
+ const widthDrag = useRef<{ side: "left" | "right"; origin: number; width: number; pointerId: number } | null>(null);
1277
1375
  const stickToBottom = useRef(true);
1278
1376
  const { t } = useI18n();
1377
+ useEffect(() => {
1378
+ const root = rootRef.current;
1379
+ if (!root) return;
1380
+ const saved = Number(localStorage.getItem(CHAT_WIDTH_KEY));
1381
+ widthPreference.current = Number.isFinite(saved) && saved >= CHAT_WIDTH_MIN ? saved : null;
1382
+ const syncWidth = () => setChatWidth(root, window.innerWidth <= 840 ? null : widthPreference.current);
1383
+ const observer = new ResizeObserver(syncWidth);
1384
+ observer.observe(root);
1385
+ window.addEventListener("resize", syncWidth);
1386
+ syncWidth();
1387
+ return () => {
1388
+ observer.disconnect();
1389
+ window.removeEventListener("resize", syncWidth);
1390
+ setChatWidth(root, null);
1391
+ };
1392
+ }, []);
1279
1393
  // 脱离底部后累计的新内容条数(回到底部按钮上的角标)
1280
1394
  const [unseen, setUnseen] = useState(0);
1281
1395
  /** 是否已离开底部(state 版:ref 变化不会触发渲染,静态会话翻页时按钮出不来) */
@@ -1321,10 +1435,74 @@ export function ChatWindow({
1321
1435
  };
1322
1436
 
1323
1437
  return (
1324
- <div className="pw-chat-window relative flex h-full min-h-0 flex-col">
1438
+ <div ref={rootRef} className="relative flex h-full min-h-0 flex-col">
1439
+ {(["left", "right"] as const).map((side) => (
1440
+ <div
1441
+ key={side}
1442
+ className="piweb-conversation-width-handle"
1443
+ data-side={side}
1444
+ role="separator"
1445
+ aria-orientation="vertical"
1446
+ aria-label={t.chatWidthHandle}
1447
+ tabIndex={0}
1448
+ onPointerDown={(event) => {
1449
+ if (event.button !== 0 || !rootRef.current) return;
1450
+ event.preventDefault();
1451
+ const width = widthPreference.current ?? Math.max(544, Math.min(rootRef.current.clientWidth * 0.64, 736));
1452
+ widthDrag.current = { side, origin: event.clientX, width, pointerId: event.pointerId };
1453
+ event.currentTarget.setPointerCapture(event.pointerId);
1454
+ event.currentTarget.dataset.dragging = "true";
1455
+ }}
1456
+ onPointerMove={(event) => {
1457
+ const drag = widthDrag.current;
1458
+ if (!drag || drag.pointerId !== event.pointerId || !rootRef.current) return;
1459
+ const delta = (event.clientX - drag.origin) * (side === "right" ? 2 : -2);
1460
+ drag.width = resolveChatWidth((widthPreference.current ?? drag.width) + delta, rootRef.current.clientWidth);
1461
+ drag.origin = event.clientX;
1462
+ widthPreference.current = drag.width;
1463
+ setChatWidth(rootRef.current, drag.width);
1464
+ }}
1465
+ onPointerUp={(event) => {
1466
+ const drag = widthDrag.current;
1467
+ if (!drag || drag.pointerId !== event.pointerId) return;
1468
+ widthDrag.current = null;
1469
+ event.currentTarget.removeAttribute("data-dragging");
1470
+ if (event.currentTarget.hasPointerCapture(event.pointerId)) event.currentTarget.releasePointerCapture(event.pointerId);
1471
+ if (rootRef.current) {
1472
+ const width = resolveChatWidth(drag.width, rootRef.current.clientWidth);
1473
+ widthPreference.current = width;
1474
+ localStorage.setItem(CHAT_WIDTH_KEY, String(width));
1475
+ setChatWidth(rootRef.current, width);
1476
+ }
1477
+ }}
1478
+ onPointerCancel={(event) => {
1479
+ event.currentTarget.removeAttribute("data-dragging");
1480
+ widthDrag.current = null;
1481
+ if (rootRef.current) setChatWidth(rootRef.current, widthPreference.current);
1482
+ }}
1483
+ onLostPointerCapture={(event) => {
1484
+ event.currentTarget.removeAttribute("data-dragging");
1485
+ if (widthDrag.current) {
1486
+ widthDrag.current = null;
1487
+ if (rootRef.current) setChatWidth(rootRef.current, widthPreference.current);
1488
+ }
1489
+ }}
1490
+ onWheel={(event) => scrollRef.current?.scrollBy({ top: event.deltaY })}
1491
+ onKeyDown={(event) => {
1492
+ if ((event.key === "ArrowLeft" || event.key === "ArrowRight") && rootRef.current) {
1493
+ event.preventDefault();
1494
+ const delta = event.key === "ArrowRight" ? 24 : -24;
1495
+ const width = resolveChatWidth((widthPreference.current ?? rootRef.current.clientWidth * 0.64) + delta, rootRef.current.clientWidth);
1496
+ widthPreference.current = width;
1497
+ localStorage.setItem(CHAT_WIDTH_KEY, String(width));
1498
+ setChatWidth(rootRef.current, width);
1499
+ }
1500
+ }}
1501
+ />
1502
+ ))}
1325
1503
  <div
1326
1504
  ref={scrollRef}
1327
- className="pw-chat-scroll min-h-0 flex-1 overflow-y-auto"
1505
+ className="min-h-0 flex-1 overflow-y-auto"
1328
1506
  role="log"
1329
1507
  aria-live="polite"
1330
1508
  aria-relevant="additions"
@@ -1336,7 +1514,7 @@ export function ChatWindow({
1336
1514
  if (atBottom) setUnseen(0);
1337
1515
  }}
1338
1516
  >
1339
- <div className="pw-chat-flow mx-auto flex w-full flex-col px-4 py-6" style={{ maxWidth: "var(--dsh-chat-content-width)" }}>
1517
+ <div className="mx-auto flex w-full flex-col px-4 py-6" style={{ maxWidth: "var(--dsh-chat-content-width)" }}>
1340
1518
  {turns.map((turn, ti) => (
1341
1519
  <TurnBlock
1342
1520
  key={turn.key}
@@ -1355,6 +1533,8 @@ export function ChatWindow({
1355
1533
  workingMessage={workingMessage}
1356
1534
  onFork={onFork}
1357
1535
  onEditMessage={onEditMessage}
1536
+ roundBadge={turn.userIndex >= 0 ? roundBadges?.get(messages[turn.userIndex].id ?? "") : undefined}
1537
+ onShowRound={onShowRound}
1358
1538
  onInspectTool={onOpenTrajectory}
1359
1539
  onOpenFile={onOpenFile}
1360
1540
  />
@@ -2,8 +2,9 @@
2
2
 
3
3
  /**
4
4
  * 上下文占用:
5
- * 圆环常驻按钮 + 弹窗显示 Pi 的上下文用量与内容构成估算。
6
- * 总量由 SDK 提供;分类是字符/4 启发式估算,不能与总量相加。
5
+ * 圆环常驻按钮 + 弹窗显示「百分比 / ~已用 / 窗口」与 13 类分段占用条
6
+ * (mac 磁盘存储条式的多色分段,含 Free Space 段)。
7
+ * 分段口径:全部为字符/4 启发式估算(与 SDK estimateTokens 一致),
7
8
  * 服务端分项(System Prompt / Tools / Memory / Skills / Compacted / Buffer)来自快照,
8
9
  * 消息分项(User / Agent Text / Thinking / Tool Call / Tool Output)弹窗打开时前端统计。
9
10
  * 无对话时不渲染(由父组件经 contextVisible 控制)。
@@ -17,7 +18,7 @@ function fmtTok(n: number): string {
17
18
  return n >= 1000 ? `${(n / 1000).toFixed(1)}K` : String(Math.round(n));
18
19
  }
19
20
 
20
- /** 估算分类的定义(顺序即渲染顺序) */
21
+ /** 13 类分段的定义(顺序即渲染顺序) */
21
22
  interface Segment {
22
23
  key: string;
23
24
  label: string;
@@ -90,6 +91,7 @@ export function ContextMeter({
90
91
  { key: "agentToolCall", label: t.contextAgentToolCall, tokens: messageSegments.agentToolCall, color: "#D3A15F" },
91
92
  { key: "toolOutput", label: t.contextToolOutput, tokens: messageSegments.toolOutput, color: "var(--dsw-warn)" },
92
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)" },
93
95
  ]
94
96
  : // 服务端分项缺失(旧快照):退回旧三段口径
95
97
  [
@@ -98,9 +100,11 @@ export function ContextMeter({
98
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)" },
99
101
  ];
100
102
 
101
- // 容量只能按 SDK 总量计算;分类有遗漏与重叠,不能用于推导剩余空间。
102
- const free = window_ > 0 && tokens !== null ? Math.max(0, window_ - used) : null;
103
- const usedWidth = window_ > 0 && tokens !== null ? `${Math.max(0, Math.min(100, (used / window_) * 100))}%` : "0%";
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}%`;
104
108
 
105
109
  return (
106
110
  <div ref={ref} className="relative">
@@ -135,19 +139,21 @@ export function ContextMeter({
135
139
  {tokens === null ? "—" : `~${fmtTok(used)}`} / {window_ > 0 ? fmtTok(window_) : "—"}
136
140
  </div>
137
141
 
138
- {free !== null && (
142
+ {/* 分段占用条(mac 磁盘存储条式多色分段,含 Free Space) */}
143
+ {usedSum > 0 && (
139
144
  <div className="mt-3 flex h-2 w-full overflow-hidden rounded-full" style={{ background: "var(--dsw-selector)" }} aria-hidden>
140
- <span style={{ width: usedWidth, background: "var(--dsw-accent)" }} />
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" }} />}
141
149
  </div>
142
150
  )}
143
- <div className="mt-3" style={{ color: "var(--dsw-label-caption)", fontSize: 11 }}>{t.contextEstimateNote}</div>
144
151
  <div className="mt-2 flex flex-col gap-1" style={{ color: "var(--dsw-label-tertiary)" }}>
145
152
  {segments.filter((s) => s.tokens > 0).map((s) => (
146
153
  <SegmentRow key={s.key} label={s.label} tokens={s.tokens} color={s.color} />
147
154
  ))}
155
+ {window_ > 0 && <SegmentRow label={t.contextFreeSpace} tokens={free} color="var(--dsw-selector)" />}
148
156
  </div>
149
- {b && b.autoCompactBuffer > 0 && <div className="mt-2 flex justify-between gap-2" style={{ color: "var(--dsw-label-tertiary)" }}><span>{t.contextAutoCompactBuffer}</span><span>{fmtTok(b.autoCompactBuffer)}</span></div>}
150
- {window_ > 0 && <div className="mt-2 flex justify-between gap-2" style={{ color: "var(--dsw-label-secondary)" }}><span>{t.contextFreeSpace}</span><span>{free === null ? "—" : `~${fmtTok(free)}`}</span></div>}
151
157
  </div>
152
158
  )}
153
159
  </div>
@@ -167,11 +167,12 @@ export function FileViewer({
167
167
  } catch (error) {
168
168
  if ((error as { status?: number }).status === 404) {
169
169
  // 跟随中的文件可能在本轮后续工具里被删掉;从最近一次涉及它的快照取回最后内容。
170
- const prior = [...growth.steps].reverse().find((step) =>
171
- step.seq <= (growth.selected?.seq ?? Infinity) && step.changes.some((item) => item.path === tab.path || item.from === tab.path),
170
+ const upto = growth.selected ? growth.rounds.findIndex((round) => round.commit === growth.selected!.commit) : growth.rounds.length - 1;
171
+ const prior = growth.rounds.slice(0, upto + 1).reverse().find((round) =>
172
+ round.changes.some((item) => item.path === tab.path || item.from === tab.path),
172
173
  );
173
174
  const priorTree = prior?.changes.some((item) => (item.status === "D" && item.path === tab.path) || (item.status === "R" && item.from === tab.path))
174
- ? prior.parent
175
+ ? prior.parentTree
175
176
  : prior?.tree;
176
177
  if (priorTree && priorTree !== tree) {
177
178
  try {
@@ -192,7 +193,7 @@ export function FileViewer({
192
193
  try {
193
194
  if (tab.lazy || !range) {
194
195
  // 会话还没有任何快照时整棵树都是磁盘条目,「不在快照里」的提示会误导
195
- await diskContent(tab.lazy && growth.steps.length > 0 ? "disk" : null);
196
+ await diskContent(tab.lazy && growth.rounds.length > 0 ? "disk" : null);
196
197
  return;
197
198
  }
198
199
  if (mode === "diff") {
@@ -223,7 +224,7 @@ export function FileViewer({
223
224
  return () => {
224
225
  alive = false;
225
226
  };
226
- }, [activeTab, cwd, mode, range, change, growth.steps, growth.selected, growth.pending, t.viewerLoadFailed]);
227
+ }, [activeTab, cwd, mode, range, change, growth.rounds, growth.selected, t.viewerLoadFailed]);
227
228
 
228
229
  // 键盘:Esc 关(先关快速打开)、Ctrl/⌘+W 关页签、Ctrl/⌘+P 快速打开、[ ] 上一处下一处
229
230
  useEffect(() => {
@@ -289,9 +290,9 @@ export function FileViewer({
289
290
  setReveal({ path: dir, key: Date.now() });
290
291
  onSelectTab(null);
291
292
  };
292
- const roundNumber = growth.selectedRound?.id ?? 0;
293
- const stepBadge = roundNumber
294
- ? `${t.growthRoundLabel.replace("{n}", String(roundNumber))}${change ? ` · +${change.add ?? 0} −${change.del ?? 0}` : ""}`
293
+ const roundName = growth.selected ? (growth.selected.n ? t.growthRoundLabel.replace("{n}", String(growth.selected.n)) : t.growthKindUser) : "";
294
+ const stepBadge = roundName
295
+ ? `${roundName}${change ? ` · +${change.add ?? 0} −${change.del ?? 0}` : ""}`
295
296
  : "";
296
297
  const isDiffMode = mode === "diff" && Boolean(loaded?.diff);
297
298
  const noticeText = loaded?.notice === "deleted" ? t.viewerDeleted : loaded?.notice === "historical" ? t.viewerHistorical : loaded?.notice === "missing" ? t.viewerMissing : loaded?.notice === "disk" ? t.viewerNotInSnapshot : loaded?.notice === "nochange" ? t.viewerNoChanges : "";
@@ -463,10 +464,10 @@ export function FileViewer({
463
464
  <div className="pw-viewer-toolbar hairline-b">
464
465
  <div className="flex min-w-0 flex-1 items-center gap-1.5 rounded-lg px-2" style={{ background: "var(--dsw-hover)", height: 26, maxWidth: 360 }}>
465
466
  <IconSearchOutline16 size={12} style={{ flex: "none", color: "var(--dsw-label-caption)" }} />
466
- <input value={treeFilter} onChange={(e) => setTreeFilter(e.target.value)} aria-label={t.growthFilter} placeholder={t.growthFilter} className="min-w-0 flex-1 bg-transparent" style={{ fontSize: 12 }} />
467
+ <input value={treeFilter} onChange={(e) => setTreeFilter(e.target.value)} placeholder={t.growthFilter} className="min-w-0 flex-1 bg-transparent" style={{ fontSize: 12 }} />
467
468
  </div>
468
469
  <span style={{ fontSize: 11.5, color: "var(--dsw-label-caption)" }}>
469
- {roundNumber ? t.growthRoundLabel.replace("{n}", String(roundNumber)) : ""}
470
+ {roundName}
470
471
  </span>
471
472
  <button type="button" className="pw-chip" onClick={growth.expandAll}>{t.growthExpandAll}</button>
472
473
  <button type="button" className="pw-chip" onClick={growth.collapseAll}>{t.growthCollapseAll}</button>
@@ -181,29 +181,27 @@ export function GitPanel({
181
181
  : t.gitPanel;
182
182
 
183
183
  return (
184
- <div className="flex h-full min-h-0 flex-col p-4 select-none">
185
- <div className="mb-3 flex items-center gap-2 pb-2 border-b border-slate-200/60 dark:border-slate-800/60">
184
+ <div className="flex h-full min-h-0 flex-col p-4">
185
+ <div className="mb-3 flex items-center gap-2">
186
186
  {view ? (
187
- <button className="w-7 h-7 rounded-full clay-btn-soft flex items-center justify-center text-slate-600 hover:text-slate-900 transition-transform active:scale-95" title={t.gitBack} onClick={() => changeView(null)}>
187
+ <button className="icon-btn" style={{ width: 22, height: 22 }} title={t.gitBack} onClick={() => changeView(null)}>
188
188
  <IconChevronLeft14 size={13} />
189
189
  </button>
190
190
  ) : (
191
- <div className="w-7 h-7 rounded-full clay-inset flex items-center justify-center text-emerald-600">
192
- <IconGitOutline16 size={15} />
193
- </div>
191
+ <IconGitOutline16 size={15} style={{ flex: "none" }} />
194
192
  )}
195
- <span className="min-w-0 flex-1 truncate font-bold text-sm text-slate-800 dark:text-slate-100" title={view?.kind === "file" ? view.path : undefined}>
193
+ <span className="min-w-0 flex-1 truncate" style={{ fontSize: 13, fontWeight: 600 }} title={view?.kind === "file" ? view.path : undefined}>
196
194
  {headerTitle}
197
195
  </span>
198
196
  {view?.kind === "file" && onOpenFile && info?.root && (
199
- <button className="w-7 h-7 rounded-full clay-btn-soft flex items-center justify-center text-slate-600 hover:text-slate-900 transition-transform active:scale-95" title={t.fileOpenEditor} onClick={() => onOpenFile(`${info.root}/${view.path}`)}>
197
+ <button className="icon-btn" style={{ width: 22, height: 22 }} title={t.fileOpenEditor} onClick={() => onOpenFile(`${info.root}/${view.path}`)}>
200
198
  <IconFileOutline16 size={13} />
201
199
  </button>
202
200
  )}
203
- <button className="w-7 h-7 rounded-full clay-btn-soft flex items-center justify-center text-slate-600 hover:text-slate-900 transition-transform active:scale-95" title={t.panelRefresh} onClick={() => (view ? void loadDiff(view) : void load())}>
204
- <IconRefreshOutline14 size={13} className={loading || diffLoading ? "piweb-spin" : undefined} />
201
+ <button className="icon-btn" style={{ width: 22, height: 22 }} title={t.panelRefresh} onClick={() => (view ? void loadDiff(view) : void load())}>
202
+ <IconRefreshOutline14 size={13} />
205
203
  </button>
206
- <button className="w-7 h-7 rounded-full clay-btn-soft flex items-center justify-center text-slate-600 hover:text-rose-600 transition-colors" title={t.close} onClick={onClose}>
204
+ <button className="icon-btn" style={{ width: 22, height: 22 }} title={t.close} onClick={onClose}>
207
205
  <IconCloseOutline14 size={13} />
208
206
  </button>
209
207
  </div>
@@ -216,7 +214,6 @@ export function GitPanel({
216
214
  )}
217
215
 
218
216
  <div className="min-h-0 flex-1 overflow-y-auto">
219
- {!cwd && <div className="rounded-xl px-3 py-4 text-sm" style={{ color: "var(--dsw-label-caption)", background: "var(--dsw-hover)" }}>请先在工作台选择工作区,再查看 Git 差异。</div>}
220
217
  {/* ---------- 差异视图 ---------- */}
221
218
  {view && (
222
219
  <div>
@@ -270,7 +267,8 @@ export function GitPanel({
270
267
  {!error && info.files.length > 0 && onAskCommit && (
271
268
  <button
272
269
  type="button"
273
- className="clay-btn clay-btn-mint text-white font-semibold px-3.5 py-1.5 rounded-full text-xs self-start shadow-sm active:scale-95"
270
+ className="btn-outline self-start"
271
+ style={{ height: 28, padding: "0 12px", fontSize: 12 }}
274
272
  onClick={onAskCommit}
275
273
  title={t.gitAskCommitPrompt}
276
274
  >