@rexvane/piweb 0.3.22 → 0.3.24

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 (251) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/app-path-routes-manifest.json +8 -7
  3. package/.next/build-manifest.json +3 -3
  4. package/.next/prerender-manifest.json +4 -4
  5. package/.next/react-loadable-manifest.json +11 -4
  6. package/.next/routes-manifest.json +8 -0
  7. package/.next/server/app/_global-error/page.js +2 -2
  8. package/.next/server/app/_global-error/page.js.nft.json +1 -1
  9. package/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  10. package/.next/server/app/_global-error.html +1 -1
  11. package/.next/server/app/_global-error.rsc +1 -1
  12. package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  13. package/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  14. package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  15. package/.next/server/app/_not-found/page.js +1 -1
  16. package/.next/server/app/_not-found/page.js.nft.json +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 +2 -2
  20. package/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
  21. package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
  22. package/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  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]/side-chat/route.js +5 -0
  28. package/.next/server/app/api/agent/[id]/side-chat/route.js.nft.json +1 -0
  29. package/.next/server/app/api/agent/[id]/side-chat/route_client-reference-manifest.js +1 -0
  30. package/.next/server/app/api/agent/[id]/tree/route.js +1 -1
  31. package/.next/server/app/api/agent/[id]/tree/route.js.nft.json +1 -1
  32. package/.next/server/app/api/agent/new/route.js +1 -1
  33. package/.next/server/app/api/agent/new/route.js.nft.json +1 -1
  34. package/.next/server/app/api/config/route.js +1 -1
  35. package/.next/server/app/api/config/route.js.nft.json +1 -1
  36. package/.next/server/app/api/files/route.js +1 -1
  37. package/.next/server/app/api/files/route.js.nft.json +1 -1
  38. package/.next/server/app/api/fs/route.js +1 -1
  39. package/.next/server/app/api/fs/route.js.nft.json +1 -1
  40. package/.next/server/app/api/git/route.js +2 -2
  41. package/.next/server/app/api/git/route.js.nft.json +1 -1
  42. package/.next/server/app/api/github/route.js +2 -2
  43. package/.next/server/app/api/github/route.js.nft.json +1 -1
  44. package/.next/server/app/api/growth/route.js +1 -1
  45. package/.next/server/app/api/growth/route.js.nft.json +1 -1
  46. package/.next/server/app/api/health/route.js +1 -1
  47. package/.next/server/app/api/health/route.js.nft.json +1 -1
  48. package/.next/server/app/api/models/defaults/route.js +1 -1
  49. package/.next/server/app/api/models/defaults/route.js.nft.json +1 -1
  50. package/.next/server/app/api/models/route.js +1 -1
  51. package/.next/server/app/api/models/route.js.nft.json +1 -1
  52. package/.next/server/app/api/pi-settings/route.js +1 -1
  53. package/.next/server/app/api/pi-settings/route.js.nft.json +1 -1
  54. package/.next/server/app/api/plugins/route.js +1 -1
  55. package/.next/server/app/api/plugins/route.js.nft.json +1 -1
  56. package/.next/server/app/api/prompts/route.js +1 -1
  57. package/.next/server/app/api/prompts/route.js.nft.json +1 -1
  58. package/.next/server/app/api/remote/route.js +3 -3
  59. package/.next/server/app/api/remote/route.js.nft.json +1 -1
  60. package/.next/server/app/api/security/route.js +1 -1
  61. package/.next/server/app/api/security/route.js.nft.json +1 -1
  62. package/.next/server/app/api/sessions/[id]/export/route.js +1 -1
  63. package/.next/server/app/api/sessions/[id]/export/route.js.nft.json +1 -1
  64. package/.next/server/app/api/sessions/[id]/route.js +1 -1
  65. package/.next/server/app/api/sessions/[id]/route.js.nft.json +1 -1
  66. package/.next/server/app/api/sessions/import/route.js +2 -2
  67. package/.next/server/app/api/sessions/import/route.js.nft.json +1 -1
  68. package/.next/server/app/api/sessions/route.js +1 -1
  69. package/.next/server/app/api/sessions/route.js.nft.json +1 -1
  70. package/.next/server/app/api/skills/route.js +2 -2
  71. package/.next/server/app/api/skills/route.js.nft.json +1 -1
  72. package/.next/server/app/api/update/route.js +2 -2
  73. package/.next/server/app/api/update/route.js.nft.json +1 -1
  74. package/.next/server/app/api/version/route.js +1 -1
  75. package/.next/server/app/api/version/route.js.nft.json +1 -1
  76. package/.next/server/app/api/web-auth/route.js +2 -2
  77. package/.next/server/app/api/web-auth/route.js.nft.json +1 -1
  78. package/.next/server/app/api/workspaces/route.js +1 -1
  79. package/.next/server/app/api/workspaces/route.js.nft.json +1 -1
  80. package/.next/server/app/icon.svg/route.js +1 -1
  81. package/.next/server/app/icon.svg/route.js.nft.json +1 -1
  82. package/.next/server/app/index.html +1 -1
  83. package/.next/server/app/index.rsc +7 -6
  84. package/.next/server/app/index.segments/__PAGE__.segment.rsc +7 -6
  85. package/.next/server/app/index.segments/_full.segment.rsc +7 -6
  86. package/.next/server/app/index.segments/_tree.segment.rsc +3 -2
  87. package/.next/server/app/login/page.js +1 -1
  88. package/.next/server/app/login/page.js.nft.json +1 -1
  89. package/.next/server/app/login/page_client-reference-manifest.js +1 -1
  90. package/.next/server/app/login.html +1 -1
  91. package/.next/server/app/login.rsc +3 -3
  92. package/.next/server/app/login.segments/_full.segment.rsc +3 -3
  93. package/.next/server/app/login.segments/_tree.segment.rsc +2 -2
  94. package/.next/server/app/login.segments/login/__PAGE__.segment.rsc +3 -3
  95. package/.next/server/app/page.js +1 -1
  96. package/.next/server/app/page.js.nft.json +1 -1
  97. package/.next/server/app/page_client-reference-manifest.js +1 -1
  98. package/.next/server/app-paths-manifest.json +8 -7
  99. package/.next/server/chunks/{813.js → 1813.js} +1 -1
  100. package/.next/server/chunks/{430.js → 2430.js} +1 -1
  101. package/.next/server/chunks/{970.js → 2970.js} +1 -1
  102. package/.next/server/chunks/3421.js +1 -0
  103. package/.next/server/chunks/3924.js +6 -0
  104. package/.next/server/chunks/4807.js +1 -0
  105. package/.next/server/chunks/5393.js +1 -0
  106. package/.next/server/chunks/5802.js +2 -0
  107. package/.next/server/chunks/6229.js +1 -0
  108. package/.next/server/chunks/7496.js +1 -0
  109. package/.next/server/chunks/7662.js +2 -0
  110. package/.next/server/chunks/8686.js +1 -0
  111. package/.next/server/chunks/{593.js → 974.js} +1 -1
  112. package/.next/server/chunks/{920.js → 9920.js} +1 -1
  113. package/.next/server/functions-config-manifest.json +2 -1
  114. package/.next/server/instrumentation.js +1 -1
  115. package/.next/server/instrumentation.js.nft.json +1 -1
  116. package/.next/server/middleware-build-manifest.js +1 -1
  117. package/.next/server/middleware-react-loadable-manifest.js +1 -1
  118. package/.next/server/middleware.js +1 -1
  119. package/.next/server/pages/404.html +1 -1
  120. package/.next/server/pages/500.html +1 -1
  121. package/.next/server/preview-props.json +3 -3
  122. package/.next/server/server-reference-manifest.json +1 -1
  123. package/.next/server/webpack-runtime.js +1 -1
  124. package/.next/static/chunks/2716.44fdf1b21454b961.js +1 -0
  125. package/.next/static/chunks/4673.e261707dce391be7.js +1 -0
  126. package/.next/static/chunks/4686-0cd87513f9ea3538.js +1 -0
  127. package/.next/static/chunks/7411.5f79671208209b13.js +1 -0
  128. package/.next/static/chunks/app/_global-error/page-cbaaee0d7aacb85a.js +1 -0
  129. package/.next/static/chunks/app/api/agent/[id]/events/route-cbaaee0d7aacb85a.js +1 -0
  130. package/.next/static/chunks/app/api/agent/[id]/route-cbaaee0d7aacb85a.js +1 -0
  131. package/.next/static/chunks/app/api/agent/[id]/side-chat/route-cbaaee0d7aacb85a.js +1 -0
  132. package/.next/static/chunks/app/api/agent/[id]/tree/route-cbaaee0d7aacb85a.js +1 -0
  133. package/.next/static/chunks/app/api/agent/new/route-cbaaee0d7aacb85a.js +1 -0
  134. package/.next/static/chunks/app/api/config/route-cbaaee0d7aacb85a.js +1 -0
  135. package/.next/static/chunks/app/api/files/route-cbaaee0d7aacb85a.js +1 -0
  136. package/.next/static/chunks/app/api/fs/route-cbaaee0d7aacb85a.js +1 -0
  137. package/.next/static/chunks/app/api/git/route-cbaaee0d7aacb85a.js +1 -0
  138. package/.next/static/chunks/app/api/github/route-cbaaee0d7aacb85a.js +1 -0
  139. package/.next/static/chunks/app/api/growth/route-cbaaee0d7aacb85a.js +1 -0
  140. package/.next/static/chunks/app/api/health/route-cbaaee0d7aacb85a.js +1 -0
  141. package/.next/static/chunks/app/api/models/defaults/route-cbaaee0d7aacb85a.js +1 -0
  142. package/.next/static/chunks/app/api/models/route-cbaaee0d7aacb85a.js +1 -0
  143. package/.next/static/chunks/app/api/pi-settings/route-cbaaee0d7aacb85a.js +1 -0
  144. package/.next/static/chunks/app/api/plugins/route-cbaaee0d7aacb85a.js +1 -0
  145. package/.next/static/chunks/app/api/prompts/route-cbaaee0d7aacb85a.js +1 -0
  146. package/.next/static/chunks/app/api/remote/route-cbaaee0d7aacb85a.js +1 -0
  147. package/.next/static/chunks/app/api/security/route-cbaaee0d7aacb85a.js +1 -0
  148. package/.next/static/chunks/app/api/sessions/[id]/export/route-cbaaee0d7aacb85a.js +1 -0
  149. package/.next/static/chunks/app/api/sessions/[id]/route-cbaaee0d7aacb85a.js +1 -0
  150. package/.next/static/chunks/app/api/sessions/import/route-cbaaee0d7aacb85a.js +1 -0
  151. package/.next/static/chunks/app/api/sessions/route-cbaaee0d7aacb85a.js +1 -0
  152. package/.next/static/chunks/app/api/skills/route-cbaaee0d7aacb85a.js +1 -0
  153. package/.next/static/chunks/app/api/update/route-cbaaee0d7aacb85a.js +1 -0
  154. package/.next/static/chunks/app/api/version/route-cbaaee0d7aacb85a.js +1 -0
  155. package/.next/static/chunks/app/api/web-auth/route-cbaaee0d7aacb85a.js +1 -0
  156. package/.next/static/chunks/app/api/workspaces/route-cbaaee0d7aacb85a.js +1 -0
  157. package/.next/static/chunks/app/page-dc3cb41d91c7432e.js +1 -0
  158. package/.next/static/chunks/next/dist/client/components/builtin/app-error-cbaaee0d7aacb85a.js +1 -0
  159. package/.next/static/chunks/next/dist/client/components/builtin/forbidden-cbaaee0d7aacb85a.js +1 -0
  160. package/.next/static/chunks/next/dist/client/components/builtin/not-found-cbaaee0d7aacb85a.js +1 -0
  161. package/.next/static/chunks/next/dist/client/components/builtin/unauthorized-cbaaee0d7aacb85a.js +1 -0
  162. package/.next/static/chunks/webpack-2ac3d76b81895f5b.js +1 -0
  163. package/.next/static/css/9fc559c2c4ad1d0f.css +1 -0
  164. package/.next/static/css/c6e2227c36ec2fd6.css +1 -0
  165. package/.next/types/app/api/agent/[id]/side-chat/route.ts +355 -0
  166. package/.next/types/routes.d.ts +2 -1
  167. package/.next/types/validator.ts +10 -0
  168. package/next.config.ts +2 -0
  169. package/package.json +9 -3
  170. package/scripts/release.mjs +3 -7
  171. package/scripts/runtime/check-installed.mjs +66 -0
  172. package/scripts/runtime/pi-packages.mjs +52 -0
  173. package/scripts/verify-package.mjs +29 -7
  174. package/src/app/api/agent/[id]/side-chat/route.ts +77 -0
  175. package/src/app/globals.css +1576 -14
  176. package/src/components/AppShell.tsx +63 -13
  177. package/src/components/ChatInput.tsx +33 -29
  178. package/src/components/ChatWindow.tsx +6 -7
  179. package/src/components/ContextMeter.tsx +35 -16
  180. package/src/components/DrawerPanel.tsx +1 -1
  181. package/src/components/FileViewer.tsx +89 -8
  182. package/src/components/GrowthTree.tsx +30 -5
  183. package/src/components/ModelSelector.tsx +21 -5
  184. package/src/components/SettingsPanel.tsx +25 -16
  185. package/src/components/chat/reading-anchor.ts +81 -0
  186. package/src/components/composer/ComposerImages.tsx +31 -11
  187. package/src/components/side-chat/SideChatPanel.module.css +131 -0
  188. package/src/components/side-chat/SideChatPanel.tsx +189 -0
  189. package/src/components/side-chat/icons.tsx +8 -0
  190. package/src/components/side-chat/request-history.ts +29 -0
  191. package/src/components/side-chat/store.ts +248 -0
  192. package/src/hooks/useFileViewer.ts +6 -4
  193. package/src/i18n.tsx +50 -8
  194. package/src/lib/files-service.ts +66 -2
  195. package/src/lib/path-security.ts +24 -32
  196. package/src/lib/session-reader.ts +1 -1
  197. package/src/lib/side-chat/context.ts +123 -0
  198. package/src/lib/side-chat/service.ts +191 -0
  199. package/src/lib/side-chat/types.ts +61 -0
  200. package/src/lib/types.ts +1 -1
  201. package/src/lib/update-service.ts +4 -6
  202. package/.next/server/chunks/229.js +0 -1
  203. package/.next/server/chunks/393.js +0 -1
  204. package/.next/server/chunks/421.js +0 -1
  205. package/.next/server/chunks/496.js +0 -1
  206. package/.next/server/chunks/662.js +0 -2
  207. package/.next/server/chunks/686.js +0 -1
  208. package/.next/server/chunks/802.js +0 -2
  209. package/.next/server/chunks/807.js +0 -1
  210. package/.next/server/chunks/924.js +0 -6
  211. package/.next/static/chunks/2716.7ad073038e94a802.js +0 -1
  212. package/.next/static/chunks/4673.4cf66cbdd8364c57.js +0 -1
  213. package/.next/static/chunks/4686-09fcd0028079006c.js +0 -1
  214. package/.next/static/chunks/7411.67366dc5032a141d.js +0 -1
  215. package/.next/static/chunks/app/_global-error/page-f977af7f858990a2.js +0 -1
  216. package/.next/static/chunks/app/api/agent/[id]/events/route-f977af7f858990a2.js +0 -1
  217. package/.next/static/chunks/app/api/agent/[id]/route-f977af7f858990a2.js +0 -1
  218. package/.next/static/chunks/app/api/agent/[id]/tree/route-f977af7f858990a2.js +0 -1
  219. package/.next/static/chunks/app/api/agent/new/route-f977af7f858990a2.js +0 -1
  220. package/.next/static/chunks/app/api/config/route-f977af7f858990a2.js +0 -1
  221. package/.next/static/chunks/app/api/files/route-f977af7f858990a2.js +0 -1
  222. package/.next/static/chunks/app/api/fs/route-f977af7f858990a2.js +0 -1
  223. package/.next/static/chunks/app/api/git/route-f977af7f858990a2.js +0 -1
  224. package/.next/static/chunks/app/api/github/route-f977af7f858990a2.js +0 -1
  225. package/.next/static/chunks/app/api/growth/route-f977af7f858990a2.js +0 -1
  226. package/.next/static/chunks/app/api/health/route-f977af7f858990a2.js +0 -1
  227. package/.next/static/chunks/app/api/models/defaults/route-f977af7f858990a2.js +0 -1
  228. package/.next/static/chunks/app/api/models/route-f977af7f858990a2.js +0 -1
  229. package/.next/static/chunks/app/api/pi-settings/route-f977af7f858990a2.js +0 -1
  230. package/.next/static/chunks/app/api/plugins/route-f977af7f858990a2.js +0 -1
  231. package/.next/static/chunks/app/api/prompts/route-f977af7f858990a2.js +0 -1
  232. package/.next/static/chunks/app/api/remote/route-f977af7f858990a2.js +0 -1
  233. package/.next/static/chunks/app/api/security/route-f977af7f858990a2.js +0 -1
  234. package/.next/static/chunks/app/api/sessions/[id]/export/route-f977af7f858990a2.js +0 -1
  235. package/.next/static/chunks/app/api/sessions/[id]/route-f977af7f858990a2.js +0 -1
  236. package/.next/static/chunks/app/api/sessions/import/route-f977af7f858990a2.js +0 -1
  237. package/.next/static/chunks/app/api/sessions/route-f977af7f858990a2.js +0 -1
  238. package/.next/static/chunks/app/api/skills/route-f977af7f858990a2.js +0 -1
  239. package/.next/static/chunks/app/api/update/route-f977af7f858990a2.js +0 -1
  240. package/.next/static/chunks/app/api/version/route-f977af7f858990a2.js +0 -1
  241. package/.next/static/chunks/app/api/web-auth/route-f977af7f858990a2.js +0 -1
  242. package/.next/static/chunks/app/api/workspaces/route-f977af7f858990a2.js +0 -1
  243. package/.next/static/chunks/app/page-fc4ed2ff29b6235e.js +0 -1
  244. package/.next/static/chunks/next/dist/client/components/builtin/app-error-f977af7f858990a2.js +0 -1
  245. package/.next/static/chunks/next/dist/client/components/builtin/forbidden-f977af7f858990a2.js +0 -1
  246. package/.next/static/chunks/next/dist/client/components/builtin/not-found-f977af7f858990a2.js +0 -1
  247. package/.next/static/chunks/next/dist/client/components/builtin/unauthorized-f977af7f858990a2.js +0 -1
  248. package/.next/static/chunks/webpack-22ebffc20adb44ae.js +0 -1
  249. package/.next/static/css/6671d0d1589fdc0e.css +0 -1
  250. /package/.next/static/{WMazYvD-gbeLsCUfUxvvM → lvKCJmjFj8LuqSojRVLRF}/_buildManifest.js +0 -0
  251. /package/.next/static/{WMazYvD-gbeLsCUfUxvvM → lvKCJmjFj8LuqSojRVLRF}/_ssgManifest.js +0 -0
@@ -6,7 +6,7 @@
6
6
  * 生成中呼吸态、新步入场动画、窗口化渲染、键盘导航(↑↓ 移动,←→ 收展,Enter 打开)。
7
7
  */
8
8
  import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
9
- import { IconAtOutline16, IconFileOutline16, IconTriangleRightFill14 } from "@/components/icons";
9
+ import { IconAtOutline16, IconFileOutline16, IconFolderClose16, IconTriangleRightFill14 } from "@/components/icons";
10
10
  import { useI18n } from "@/i18n";
11
11
  import { fileIconSrc, folderIconSrc } from "@/lib/file-icons";
12
12
  import { flattenTree, treeNodeKey, type FlatRow, type TreeNode } from "@/lib/growth-tree";
@@ -182,14 +182,16 @@ export const GrowthTree = memo(function GrowthTree({
182
182
  selected={row.node.kind === "file" && selectedPath === row.node.path}
183
183
  focused={focused === treeNodeKey(row.node)}
184
184
  onActivate={activate}
185
+ onOpenFolder={onOpenFile}
185
186
  onFocusRow={setFocused}
186
187
  onReference={onReference}
187
188
  onOpenEditor={onOpenEditor}
188
189
  pendingLabel={t.growthPending}
189
- lazyLabel={t.growthLazyDir}
190
- deletedLabel={t.growthStatD}
191
- referenceLabel={t.fileReference}
190
+ lazyLabel={t.growthLazyDir}
191
+ deletedLabel={t.growthStatD}
192
+ referenceLabel={t.fileReference}
192
193
  editorLabel={t.fileOpenEditor}
194
+ openFolderLabel={t.viewerOpenFolder}
193
195
  />
194
196
  ))}
195
197
  </div>
@@ -205,6 +207,7 @@ const TreeRow = memo(function TreeRow({
205
207
  selected,
206
208
  focused,
207
209
  onActivate,
210
+ onOpenFolder,
208
211
  onFocusRow,
209
212
  onReference,
210
213
  onOpenEditor,
@@ -213,6 +216,7 @@ const TreeRow = memo(function TreeRow({
213
216
  deletedLabel,
214
217
  referenceLabel,
215
218
  editorLabel,
219
+ openFolderLabel,
216
220
  }: {
217
221
  row: FlatRow;
218
222
  top: number;
@@ -220,6 +224,7 @@ const TreeRow = memo(function TreeRow({
220
224
  selected: boolean;
221
225
  focused: boolean;
222
226
  onActivate: (row: FlatRow) => void;
227
+ onOpenFolder: (node: TreeNode) => void;
223
228
  onFocusRow: (path: string) => void;
224
229
  onReference?: (path: string) => void;
225
230
  onOpenEditor?: (path: string) => void;
@@ -228,6 +233,7 @@ const TreeRow = memo(function TreeRow({
228
233
  deletedLabel: string;
229
234
  referenceLabel: string;
230
235
  editorLabel: string;
236
+ openFolderLabel: string;
231
237
  }) {
232
238
  const { node, depth, expanded } = row;
233
239
  const isDir = node.kind === "dir";
@@ -253,6 +259,10 @@ const TreeRow = memo(function TreeRow({
253
259
  onFocusRow(treeNodeKey(node));
254
260
  onActivate(row);
255
261
  }}
262
+ onDoubleClick={() => {
263
+ // 目录双击 = 打开文件夹预览(单击仍是展开/收起,不动既有习惯)
264
+ if (isDir) onOpenFolder(node);
265
+ }}
256
266
  >
257
267
  <span className="pw-tree-chevron" aria-hidden>
258
268
  {isDir && <IconTriangleRightFill14 size={10} style={{ transform: expanded ? "rotate(90deg)" : undefined, transition: "transform var(--ds-duration-fast)" }} />}
@@ -264,8 +274,23 @@ const TreeRow = memo(function TreeRow({
264
274
  {node.status === "R" && node.from && <span className="pw-tree-from">← {node.from.split("/").pop()}</span>}
265
275
  </span>
266
276
  {node.pending && <span className="pw-tree-pulse" aria-label={pendingLabel} />}
267
- {!node.lazy && (onReference || (!isDir && onOpenEditor)) && (
277
+ {!node.lazy && (onReference || (!isDir && onOpenEditor) || isDir) && (
268
278
  <span className="pw-tree-actions">
279
+ {isDir && (
280
+ <button
281
+ type="button"
282
+ className="icon-btn"
283
+ style={{ width: 18, height: 18, borderRadius: 5 }}
284
+ title={openFolderLabel}
285
+ aria-label={`${openFolderLabel}: ${node.path}`}
286
+ onClick={(e) => {
287
+ e.stopPropagation();
288
+ onOpenFolder(node);
289
+ }}
290
+ >
291
+ <IconFolderClose16 size={11} />
292
+ </button>
293
+ )}
269
294
  {onReference && (
270
295
  <button
271
296
  type="button"
@@ -27,6 +27,9 @@ export function ModelSelector({
27
27
  onSelectLevel,
28
28
  open: openProp,
29
29
  onOpenChange,
30
+ modelOnly = false,
31
+ disabled = false,
32
+ defaultOption,
30
33
  }: {
31
34
  model?: { provider: string; id: string; name: string };
32
35
  thinkingLevel?: string;
@@ -38,6 +41,10 @@ export function ModelSelector({
38
41
  onSelectLevel: (level: string) => void;
39
42
  open?: boolean;
40
43
  onOpenChange?: (open: boolean) => void;
44
+ /** Compact consumers can show just the model list. */
45
+ modelOnly?: boolean;
46
+ disabled?: boolean;
47
+ defaultOption?: { label: string; selected: boolean; onSelect: () => void };
41
48
  }) {
42
49
  const [openState, setOpenState] = useState(false);
43
50
  const open = openProp ?? openState;
@@ -104,10 +111,10 @@ export function ModelSelector({
104
111
 
105
112
  const hasModel = Boolean(model?.id);
106
113
  const modelName = hasModel ? (model?.name || model?.id || "") : "";
107
- const effortLabel = hasModel && thinkingLevel && thinkingLevel !== "off" ? thinkingLevel : undefined;
108
- const triggerTitle = hasModel
114
+ const effortLabel = !modelOnly && hasModel && thinkingLevel && thinkingLevel !== "off" ? thinkingLevel : undefined;
115
+ const triggerTitle = (defaultOption?.selected ? `${defaultOption.label} · ` : "") + (hasModel
109
116
  ? `${providerNames[model?.provider ?? ""] ?? model?.provider}/${modelName}${effortLabel ? ` · ${effortLabel}` : ""}`
110
- : t.selectModel;
117
+ : t.selectModel);
111
118
 
112
119
  return (
113
120
  <div ref={ref} className="relative">
@@ -119,6 +126,7 @@ export function ModelSelector({
119
126
  aria-label={triggerTitle}
120
127
  aria-haspopup="menu"
121
128
  aria-expanded={open}
129
+ disabled={disabled}
122
130
  onClick={() => {
123
131
  setOpen(!open);
124
132
  setPane("model");
@@ -198,6 +206,14 @@ export function ModelSelector({
198
206
  {pane === "model" && (
199
207
  <div className="flex min-h-0 flex-col">
200
208
  <div ref={listRef} className="max-h-[340px] overflow-y-auto py-1">
209
+ {defaultOption && (
210
+ <button type="button" role="menuitemradio" aria-checked={defaultOption.selected} data-selected={defaultOption.selected}
211
+ className="flex min-h-10 w-full items-center justify-between gap-3 rounded-[10px] px-3 py-2 text-left"
212
+ style={{ fontSize: 13, background: defaultOption.selected ? "var(--dsw-active)" : undefined }}
213
+ onClick={() => { defaultOption.onSelect(); setOpen(false); }}>
214
+ {defaultOption.label}{defaultOption.selected && <IconCheckOutline14 size={14} />}
215
+ </button>
216
+ )}
201
217
  {groups.length === 0 && (
202
218
  <div className="px-3 py-3" style={{ fontSize: 13, color: "var(--dsw-label-caption)" }}>
203
219
  {t.emptyModels}
@@ -213,7 +229,7 @@ export function ModelSelector({
213
229
  {g.label}
214
230
  </div>
215
231
  {g.models.map((m) => {
216
- const selected = model?.provider === m.provider && model?.id === m.id;
232
+ const selected = !defaultOption?.selected && model?.provider === m.provider && model?.id === m.id;
217
233
  return (
218
234
  <button
219
235
  key={`${m.provider}/${m.id}`}
@@ -250,7 +266,7 @@ export function ModelSelector({
250
266
  </div>
251
267
  {/* 思考档位入口:模型只有一档(非推理模型 / models.json 没写 reasoning: true)时也显示,
252
268
  但置灰并说明是模型不支持,而不是让入口凭空消失 */}
253
- {hasModel && (thinkingLevels.length > 1 ? (
269
+ {hasModel && !modelOnly && (thinkingLevels.length > 1 ? (
254
270
  <button
255
271
  type="button"
256
272
  className="mt-1 flex w-full items-center justify-between gap-3 border-t px-3 pb-1 pt-2 text-left transition-colors"
@@ -999,21 +999,25 @@ function ModelFieldAudit({ gaps, providerLabel, busy, onSave }: {
999
999
  <div role="cell" className="min-w-0">{capacityCell(gap, "contextWindow", t.modelAuditColContext)}</div>
1000
1000
  <div role="cell" className="min-w-0">{capacityCell(gap, "maxTokens", t.modelAuditColOutput)}</div>
1001
1001
  <div role="cell" className="min-w-0">
1002
- {gap.missing.includes("input") ? (
1003
- <select
1004
- className="pw-audit-field"
1005
- value={rowOf(gap).vision}
1006
- onChange={(event) => edit(gap, { vision: event.target.value as AuditRow["vision"] })}
1007
- aria-label={`${gap.id} ${t.modelAuditColVision}`}
1008
- disabled={busy}
1009
- >
1010
- <option value="">{t.visionUnset}</option>
1011
- <option value="image">{t.visionOn}</option>
1012
- <option value="text">{t.visionOff}</option>
1013
- </select>
1014
- ) : (
1015
- <span style={{ color: "var(--dsw-label-secondary)" }}>{gap.vision ? t.visionOn : t.visionOff}</span>
1016
- )}
1002
+ {/* 输入类型:文本 pi 总是接受(勾上不可点),图片这一勾就是 models.json 的 input 里加 image */}
1003
+ {gap.missing.includes("input") || true ? (
1004
+ <div className="flex items-center gap-3" role="group" aria-label={`${gap.id} ${t.modelAuditColVision}`}>
1005
+ <label className="flex cursor-default items-center gap-1" style={{ color: "var(--dsw-label-tertiary)" }} title={t.visionInputTextHint}>
1006
+ <input type="checkbox" checked disabled readOnly />
1007
+ {t.visionInputText}
1008
+ </label>
1009
+ <label className="flex items-center gap-1" style={{ color: "var(--dsw-label-secondary)" }} title={t.visionInputImageHint}>
1010
+ <input
1011
+ type="checkbox"
1012
+ checked={gap.missing.includes("input") ? rowOf(gap).vision === "image" : gap.vision === true}
1013
+ disabled={busy || !gap.missing.includes("input")}
1014
+ onChange={(event) => edit(gap, { vision: event.target.checked ? "image" : "text" })}
1015
+ aria-label={`${gap.id} ${t.visionInputImage}`}
1016
+ />
1017
+ {t.visionInputImage}
1018
+ </label>
1019
+ </div>
1020
+ ) : null}
1017
1021
  </div>
1018
1022
  </div>
1019
1023
  ))}
@@ -1360,7 +1364,12 @@ function ModelsSection() {
1360
1364
  if (!filled) return true;
1361
1365
  setAuditBusy(true);
1362
1366
  try {
1363
- return Boolean((await saveCustom(next)).success);
1367
+ const result = await saveCustom(next);
1368
+ // 失败(典型是 revision 冲突:写入期间 pi 自己动过 models.json)也必须刷新本地副本与 revision。
1369
+ // 否则本地一直拿着旧 revision,之后每次保存都撞同一个 409——用户看到的就是「反复填、永远不成」。
1370
+ // call() 只在成功时 load(),这里把失败那一半补上。
1371
+ if (!result.success) await load();
1372
+ return Boolean(result.success);
1364
1373
  } finally {
1365
1374
  setAuditBusy(false);
1366
1375
  }
@@ -0,0 +1,81 @@
1
+ "use client";
2
+
3
+ import { useEffect, useRef, type RefObject } from "react";
4
+
5
+ const BEFORE_LAYOUT = "piweb:before-chat-layout";
6
+
7
+ /** Call before changing the columns, while the currently visible text still has its old geometry. */
8
+ export function preserveChatReadingPosition(): void {
9
+ window.dispatchEvent(new Event(BEFORE_LAYOUT));
10
+ }
11
+
12
+ interface ReadingAnchor {
13
+ element?: Element;
14
+ range?: Range;
15
+ top: number;
16
+ bottom: boolean;
17
+ width: number;
18
+ capturedAt: number;
19
+ }
20
+
21
+ export function useChatReadingAnchor(rootRef: RefObject<HTMLElement | null>, scrollRef: RefObject<HTMLElement | null>, shouldPin: () => boolean): void {
22
+ const anchor = useRef<ReadingAnchor | null>(null);
23
+ useEffect(() => {
24
+ const root = rootRef.current;
25
+ const scroller = scrollRef.current;
26
+ if (!root || !scroller) return;
27
+ let frame = 0;
28
+ const restore = () => {
29
+ const saved = anchor.current;
30
+ anchor.current = null;
31
+ if (!saved || Date.now() - saved.capturedAt > 500 || root.getBoundingClientRect().width === saved.width) return;
32
+ if (saved.bottom) {
33
+ scroller.scrollTop = scroller.scrollHeight;
34
+ return;
35
+ }
36
+ const rect = saved.range?.startContainer.isConnected ? saved.range.getBoundingClientRect() : saved.element?.isConnected ? saved.element.getBoundingClientRect() : null;
37
+ if (rect) scroller.scrollTop += rect.top - scroller.getBoundingClientRect().top - saved.top;
38
+ };
39
+ const capture = () => {
40
+ if (frame) cancelAnimationFrame(frame);
41
+ // A new drag frame may arrive before the previous restore frame. Finish it first.
42
+ restore();
43
+ const geometry = { width: root.getBoundingClientRect().width, capturedAt: Date.now() };
44
+ if (shouldPin()) {
45
+ anchor.current = { top: 0, bottom: true, ...geometry };
46
+ return;
47
+ }
48
+ const viewport = scroller.getBoundingClientRect();
49
+ const element = [...scroller.querySelectorAll("p, pre, li, h1, h2, h3, h4, table, [data-role='user']")].find((node) => {
50
+ const rect = node.getBoundingClientRect();
51
+ return rect.bottom > viewport.top + 8 && rect.top < viewport.bottom;
52
+ });
53
+ if (!element) return;
54
+ const rect = element.getBoundingClientRect();
55
+ const x = Math.max(viewport.left + 8, rect.left + 8);
56
+ const y = Math.max(viewport.top + 8, rect.top + 4);
57
+ const range = document.caretRangeFromPoint?.(x, y);
58
+ if (range && scroller.contains(range.startContainer) && range.startContainer.nodeType === Node.TEXT_NODE && range.startOffset < (range.startContainer.textContent?.length ?? 0)) {
59
+ range.setEnd(range.startContainer, range.startOffset + 1);
60
+ anchor.current = { element, range, top: range.getBoundingClientRect().top - viewport.top, bottom: false, ...geometry };
61
+ } else {
62
+ anchor.current = { element, top: rect.top - viewport.top, bottom: false, ...geometry };
63
+ }
64
+ };
65
+ const observer = new ResizeObserver(() => {
66
+ if (!anchor.current) return;
67
+ if (frame) cancelAnimationFrame(frame);
68
+ frame = requestAnimationFrame(() => {
69
+ frame = 0;
70
+ restore();
71
+ });
72
+ });
73
+ observer.observe(root);
74
+ window.addEventListener(BEFORE_LAYOUT, capture);
75
+ return () => {
76
+ observer.disconnect();
77
+ window.removeEventListener(BEFORE_LAYOUT, capture);
78
+ if (frame) cancelAnimationFrame(frame);
79
+ };
80
+ }, [rootRef, scrollRef, shouldPin]);
81
+ }
@@ -99,7 +99,7 @@ function keyOf(image: ImageAttachment): string {
99
99
 
100
100
  const srcOf = (image: ImageAttachment) => `data:${image.mimeType};base64,${image.data}`;
101
101
 
102
- export function ComposerImages({ images, disabled, onRemove }: { images: ImageAttachment[]; disabled?: boolean; onRemove: (index: number) => void }) {
102
+ export function ComposerImages({ images, disabled, onRemove, onClear }: { images: ImageAttachment[]; disabled?: boolean; onRemove: (index: number) => void; onClear?: () => void }) {
103
103
  const { t } = useI18n();
104
104
  const collapsible = images.length > COLLAPSE_AT;
105
105
  const [expanded, setExpanded] = useState(false);
@@ -140,14 +140,19 @@ export function ComposerImages({ images, disabled, onRemove }: { images: ImageAt
140
140
 
141
141
  return (
142
142
  <>
143
- <div ref={railRef} className="mb-2 flex flex-wrap items-center gap-2 px-1" data-testid="composer-images">
143
+ <div ref={railRef} className="pointer-events-auto flex flex-wrap items-center gap-2 px-1" data-testid="composer-images">
144
144
  {collapsible ? (
145
145
  <div
146
146
  className="pw-image-fan"
147
147
  data-testid="image-fan"
148
148
  data-expanded={fanned}
149
149
  data-crowded={fanned && layout.gap < ROOMY_GAP ? true : undefined}
150
- style={{ width: fanned ? "100%" : layout.width, height: layout.height }}
150
+ // 容器(composer-attachments)已经浮在输入卡上方、不占布局高度,这里不用再抬
151
+ style={{
152
+ position: "relative",
153
+ width: fanned ? "100%" : layout.width,
154
+ height: layout.height,
155
+ }}
151
156
  >
152
157
  {images.map((image, i) => {
153
158
  const pose = layout.cards[i];
@@ -201,6 +206,21 @@ export function ComposerImages({ images, disabled, onRemove }: { images: ImageAt
201
206
  </div>
202
207
  ))
203
208
  )}
209
+ {images.length > 1 && onClear ? (
210
+ <button
211
+ type="button"
212
+ className="pw-chip"
213
+ data-testid="images-clear"
214
+ disabled={disabled}
215
+ onClick={() => {
216
+ onClear();
217
+ setLifted(null);
218
+ setPreview(null);
219
+ }}
220
+ >
221
+ {t.imageClearAll}
222
+ </button>
223
+ ) : null}
204
224
  {collapsible ? (
205
225
  <button
206
226
  type="button"
@@ -280,23 +300,23 @@ function ImageViewer({
280
300
  }, [count, index]);
281
301
 
282
302
  return (
283
- <div ref={dialogRef} className="pw-viewer" role="dialog" aria-modal="true" aria-label={t.imageViewer} onClick={onClose}>
284
- <div className="pw-viewer-bar" onClick={(event) => event.stopPropagation()}>
285
- <span className="pw-viewer-count">{count > 1 ? `${index + 1} / ${count}` : ""}</span>
286
- <button type="button" className="pw-viewer-btn" disabled={disabled} onClick={() => onRemove(index)}>
303
+ <div ref={dialogRef} className="pw-lightbox" role="dialog" aria-modal="true" aria-label={t.imageViewer} onClick={onClose}>
304
+ <div className="pw-lightbox-bar" onClick={(event) => event.stopPropagation()}>
305
+ <span className="pw-lightbox-count">{count > 1 ? `${index + 1} / ${count}` : ""}</span>
306
+ <button type="button" className="pw-lightbox-btn" disabled={disabled} onClick={() => onRemove(index)}>
287
307
  {t.delete}
288
308
  </button>
289
- <button ref={closeRef} type="button" className="pw-viewer-btn" aria-label={t.close} onClick={onClose}>
309
+ <button ref={closeRef} type="button" className="pw-lightbox-btn" aria-label={t.close} onClick={onClose}>
290
310
  ✕
291
311
  </button>
292
312
  </div>
293
- <img src={srcOf(images[index])} alt="" className="pw-viewer-image" onClick={(event) => event.stopPropagation()} />
313
+ <img src={srcOf(images[index])} alt="" className="pw-lightbox-image" onClick={(event) => event.stopPropagation()} />
294
314
  {count > 1 ? (
295
315
  <>
296
- <button type="button" className="pw-viewer-nav" data-side="prev" aria-label={t.imagePrev} onClick={(event) => { event.stopPropagation(); go(-1); }}>
316
+ <button type="button" className="pw-lightbox-nav" data-side="prev" aria-label={t.imagePrev} onClick={(event) => { event.stopPropagation(); go(-1); }}>
297
317
  ‹
298
318
  </button>
299
- <button type="button" className="pw-viewer-nav" data-side="next" aria-label={t.imageNext} onClick={(event) => { event.stopPropagation(); go(1); }}>
319
+ <button type="button" className="pw-lightbox-nav" data-side="next" aria-label={t.imageNext} onClick={(event) => { event.stopPropagation(); go(1); }}>
300
320
  ›
301
321
  </button>
302
322
  </>
@@ -0,0 +1,131 @@
1
+ .panel {
2
+ display: flex;
3
+ flex-direction: column;
4
+ height: 100%;
5
+ min-height: 0;
6
+ min-width: 0;
7
+ }
8
+ .header {
9
+ display: flex;
10
+ align-items: center;
11
+ gap: 6px;
12
+ padding: 10px 14px;
13
+ border-bottom: 0.5px solid var(--dsw-border-l2);
14
+ }
15
+ .header button { width: 28px; height: 28px; flex-shrink: 0; }
16
+ .title { flex: 1; font-size: 13.5px; font-weight: 600; }
17
+ /* 历史记录:按钮 + 浮层菜单(原生 select 不跟深色主题) */
18
+ .history { position: relative; padding: 8px 14px 0; }
19
+ .historyButton {
20
+ display: flex;
21
+ align-items: center;
22
+ gap: 6px;
23
+ width: 100%;
24
+ min-width: 0;
25
+ height: 30px;
26
+ padding: 0 8px;
27
+ border: 0.5px solid var(--dsw-border-l2);
28
+ border-radius: 8px;
29
+ background: var(--dsw-hover);
30
+ color: var(--dsw-label-primary);
31
+ font-size: 12px;
32
+ text-align: left;
33
+ transition: background var(--ds-duration-fast) var(--ds-ease-in-out), border-color var(--ds-duration-fast) var(--ds-ease-in-out);
34
+ }
35
+ .historyButton:hover { background: var(--dsw-selector); }
36
+ .historyButton[aria-expanded="true"] { border-color: var(--dsw-border-l3); background: var(--dsw-selector); }
37
+ .historyMenu {
38
+ position: absolute;
39
+ left: 14px;
40
+ right: 14px;
41
+ top: calc(100% + 4px);
42
+ z-index: 30;
43
+ max-height: 260px;
44
+ overflow-y: auto;
45
+ padding: 4px;
46
+ border: 0.5px solid var(--dsw-border-l2);
47
+ border-radius: 10px;
48
+ background: var(--dsw-glass-popover);
49
+ box-shadow: var(--dsw-elevation-soft);
50
+ }
51
+ .historyItem {
52
+ display: flex;
53
+ align-items: center;
54
+ gap: 6px;
55
+ width: 100%;
56
+ padding: 6px 8px;
57
+ border-radius: 7px;
58
+ font-size: 12px;
59
+ color: var(--dsw-label-secondary);
60
+ text-align: left;
61
+ }
62
+ .historyItem:hover { background: var(--dsw-hover); color: var(--dsw-label-primary); }
63
+ .historyItem[aria-checked="true"] { background: var(--dsw-accent-soft); color: var(--dsw-accent); }
64
+ .historyMeta { flex: none; font-size: 11px; color: var(--dsw-label-caption); }
65
+ .messages { flex: 1; min-height: 0; overflow-y: auto; overscroll-behavior: contain; }
66
+ .messageContent { display: flex; flex-direction: column; gap: 20px; padding: 20px 16px; }
67
+ .empty { margin: 36px 8px; color: var(--dsw-label-secondary); text-align: center; line-height: 1.8; }
68
+ .empty p { margin: 0 0 8px; font-size: 14px; }
69
+ .empty span { font-size: 12px; color: var(--dsw-label-tertiary); }
70
+ .question {
71
+ align-self: flex-end;
72
+ max-width: 94%;
73
+ padding: 9px 12px;
74
+ border-radius: 14px 14px 4px 14px;
75
+ background: var(--dsw-accent-soft);
76
+ font-size: var(--piweb-chat-font-size, var(--dsh-content-font-size));
77
+ line-height: 1.65;
78
+ white-space: pre-wrap;
79
+ overflow-wrap: anywhere;
80
+ }
81
+ .answer { min-width: 0; font-size: var(--piweb-chat-font-size, var(--dsh-content-font-size)); line-height: 1.75; overflow-wrap: anywhere; }
82
+ .answer :global(.md) { font-size: inherit; }
83
+ .answer :global(.md pre), .answer :global(.md table) { max-width: 100%; }
84
+ .notice, .waiting { font-size: 12px; line-height: 1.7; color: var(--dsw-label-tertiary); }
85
+ .notice { margin: 4px 0 10px; }
86
+ .error, .storageError { font-size: 12px; line-height: 1.7; color: var(--dsw-danger); overflow-wrap: anywhere; }
87
+ .error button { margin-left: 10px; text-decoration: underline; }
88
+ .storageError { margin: 0; padding: 4px 14px; }
89
+ .composer {
90
+ margin: 10px 12px 12px;
91
+ padding: 11px 12px 9px;
92
+ border: 0.5px solid var(--dsw-border-l2);
93
+ border-radius: 14px;
94
+ background: var(--dsw-bg-base);
95
+ flex-shrink: 0;
96
+ }
97
+ .composer:focus-within { border-color: var(--dsw-border-l3); }
98
+ .composer textarea {
99
+ display: block;
100
+ width: 100%;
101
+ min-height: 64px;
102
+ max-height: 180px;
103
+ resize: none;
104
+ border: 0;
105
+ outline: none;
106
+ background: transparent;
107
+ color: inherit;
108
+ font-family: inherit;
109
+ font-size: var(--piweb-chat-font-size, var(--dsh-content-font-size));
110
+ line-height: 1.65;
111
+ }
112
+ .composer textarea::placeholder { color: var(--dsw-label-tertiary); }
113
+ .actions { display: flex; align-items: center; gap: 8px; justify-content: space-between; padding-top: 5px; }
114
+ .model { flex: 1; min-width: 0; }
115
+ /* 右下角的上下文环:夹在模型选择器与发送键之间 */
116
+ .meter { display: flex; align-items: center; flex: none; }
117
+ .model :global(.chip) { max-width: 100%; font-size: 12px; }
118
+ .model :global(.chip:disabled) { opacity: 0.6; }
119
+ .model :global([role="menu"]) { left: 0; right: auto; width: min(312px, calc(100% + 38px)); }
120
+ .send {
121
+ display: flex;
122
+ align-items: center;
123
+ justify-content: center;
124
+ width: 30px;
125
+ height: 30px;
126
+ flex-shrink: 0;
127
+ border-radius: 50%;
128
+ background: var(--dsw-label-primary);
129
+ color: var(--dsw-bg-base);
130
+ }
131
+ .send:disabled { opacity: 0.3; cursor: default; }