@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
@@ -9,6 +9,7 @@ import { PiMark } from "@/components/PiMark";
9
9
  import { ChatInput, EMPTY_CHAT_DRAFT, type ChatDraft, type ChatDraftUpdate } from "@/components/ChatInput";
10
10
  import { ChatWindow, SessionStatsBar, type RoundBadge } from "@/components/ChatWindow";
11
11
  import { ExtensionDialogHost, ExtensionNotices } from "@/components/ExtensionUI";
12
+ import { UpdateNotice } from "@/components/update/UpdateNotice";
12
13
  import dynamic from "next/dynamic";
13
14
  import { SessionSidebar } from "@/components/SessionSidebar";
14
15
  import type { DrawerSection } from "@/components/DrawerPanel";
@@ -40,6 +41,9 @@ import { useFileViewer } from "@/hooks/useFileViewer";
40
41
  import type { TreeNode } from "@/lib/growth-tree";
41
42
  import { syncPebrelTheme } from "@/lib/theme";
42
43
  import type { ModelChoice } from "@/components/ModelSelector";
44
+ import type { NewSessionDefaults } from "@/lib/model-defaults";
45
+ import { readRememberedHeroModel, writeRememberedHeroModel } from "@/lib/hero-model-memory";
46
+ import { newSessionThinkingLevel } from "@/lib/thinking-level";
43
47
  import type { ImageAttachment, TrajEntry } from "@/lib/types";
44
48
 
45
49
  // dsh ui-layout columns.ts 几何常量
@@ -163,6 +167,8 @@ export function AppShell() {
163
167
  const growthSelectLatest = useRef<() => void>(() => {});
164
168
  const [heroCwd, setHeroCwd] = useState("");
165
169
  const [heroModel, setHeroModel] = useState<{ provider: string; id: string } | null>(null);
170
+ /** 当前工作区的新会话默认值(pi 的初始模型规则 + 设置里的思考档位),切换工作区时重新取 */
171
+ const [heroDefaults, setHeroDefaults] = useState<NewSessionDefaults | null>(null);
166
172
  const [heroThinking, setHeroThinking] = useState("");
167
173
  const [isNarrow, setIsNarrow] = useState(false);
168
174
  const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false);
@@ -172,6 +178,29 @@ export function AppShell() {
172
178
  setSidebarCollapsed(localStorage.getItem("piweb.sidebarCollapsed") === "1");
173
179
  setDrawerOpen(localStorage.getItem("piweb.drawerOpen") === "1");
174
180
  }, []);
181
+ /** 新会话时加载用户之前的选择,或使用系统建议(第一个认证的) */
182
+ useEffect(() => {
183
+ if (!heroCwd) {
184
+ setHeroModel(null);
185
+ return;
186
+ }
187
+ // 只认这个工作区自己记住的模型;没有就不预选,输入框等用户在新会话页选
188
+ setHeroModel(readRememberedHeroModel(localStorage, heroCwd));
189
+ }, [heroCwd]);
190
+ useEffect(() => {
191
+ setHeroDefaults(null);
192
+ if (!heroCwd) return;
193
+ let stale = false;
194
+ fetch(`/api/models/defaults?cwd=${encodeURIComponent(heroCwd)}`)
195
+ .then((r) => r.json() as Promise<{ success?: boolean; data?: NewSessionDefaults }>)
196
+ .then((json) => {
197
+ if (!stale && json.success && json.data) setHeroDefaults(json.data);
198
+ })
199
+ .catch(() => undefined);
200
+ return () => {
201
+ stale = true;
202
+ };
203
+ }, [heroCwd]);
175
204
  /** 右侧抽屉开关(项目栏降级成抽屉入口后,它是"最右一列是否打开"唯一的开关) */
176
205
  const toggleDrawer = useCallback((next?: boolean) => {
177
206
  setDrawerOpen((cur) => {
@@ -279,11 +308,28 @@ export function AppShell() {
279
308
  })),
280
309
  [models],
281
310
  );
282
- const defaultModel = useMemo(() => modelChoices.find((m) => authByProvider[m.provider]), [modelChoices, authByProvider]);
311
+ // 新会话页当前选中的模型:只有在这个工作区选过(或记住过)才有;没有就是 undefined,输入框保持禁用
312
+ const heroChoice = useMemo(
313
+ () => (heroModel ? modelChoices.find((m) => m.provider === heroModel.provider && m.id === heroModel.id) : undefined),
314
+ [heroModel, modelChoices],
315
+ );
283
316
  const heroModelLevels = useMemo(
284
- () => (heroModel ? modelChoices.find((m) => m.provider === heroModel.provider && m.id === heroModel.id) : defaultModel)?.thinkingLevels ?? ["off", "minimal", "low", "medium", "high", "xhigh", "max"],
285
- [heroModel, modelChoices, defaultModel],
317
+ () => heroChoice?.thinkingLevels ?? ["off", "minimal", "low", "medium", "high", "xhigh", "max"],
318
+ [heroChoice],
286
319
  );
320
+ /**
321
+ * 新会话实际会用的思考档位:用户显式选过才算选择(下发时才带上);界面平时只显示这里算出的生效值,
322
+ * 不把它当作选择发给服务端。
323
+ */
324
+ const heroEffectiveThinking = useMemo(() => {
325
+ const choice = heroChoice;
326
+ return newSessionThinkingLevel({
327
+ supported: heroModelLevels,
328
+ requested: heroThinking,
329
+ modelOverride: choice ? heroDefaults?.modelThinkingLevels[`${choice.provider}/${choice.id}`] : undefined,
330
+ defaultLevel: heroDefaults?.defaultThinkingLevel,
331
+ });
332
+ }, [heroChoice, heroModelLevels, heroThinking, heroDefaults]);
287
333
 
288
334
 
289
335
  // ---------- 拖拽 ----------
@@ -461,12 +507,14 @@ export function AppShell() {
461
507
  setError(t.pickWorkspaceFirst);
462
508
  return { success: false };
463
509
  }
464
- // 用户没手动选过模型时也要把界面显示的默认模型显式下发,
465
- // 否则后端不 setModel、SDK 自选的默认与界面显示不一致。
466
- const effective = heroModel ?? (defaultModel ? { provider: defaultModel.provider, id: defaultModel.id } : null);
510
+ // 没选模型不建会话:输入框本来就禁用,这里再拦一次;模型一律显式下发,不让 SDK 自选
511
+ if (!heroChoice) {
512
+ setError(t.blockedComposer);
513
+ return { success: false };
514
+ }
467
515
  const p = await newSession(cwd, {
468
- provider: effective?.provider,
469
- modelId: effective?.id,
516
+ provider: heroChoice.provider,
517
+ modelId: heroChoice.id,
470
518
  thinking: heroThinking || undefined,
471
519
  });
472
520
  if (!p) return { success: false };
@@ -518,7 +566,7 @@ export function AppShell() {
518
566
  void sendCommand({ cmd: "fork" }).then((r) => {
519
567
  if (r.success && r.data?.sessionPath) openSession(r.data.sessionPath);
520
568
  });
521
- } else if (name === "reload") void sendCommand({ cmd: "reload" });
569
+ } else if (name === "reload") void sendCommand({ cmd: "reload" }).then(() => refreshModels());
522
570
  else setError(t.cmdUnknown.replace("{name}", name));
523
571
  },
524
572
  [sendCommand, currentId, closeSession, openSession, setError, t],
@@ -587,7 +635,9 @@ export function AppShell() {
587
635
  if (!path) return;
588
636
  updateDraft(draftKey, (previous) => {
589
637
  const references = previous.references ?? [];
590
- if (references.includes(path)) return previous;
638
+ // 目录在 @ 选择器里存成 `src/lib/`:去重按去掉尾斜杠比较,避免同一目录两条
639
+ const samePath = (a: string, b: string) => a.replace(/[\\/]+$/, "") === b.replace(/[\\/]+$/, "");
640
+ if (references.some((existing) => samePath(existing, path))) return previous;
591
641
  return { ...previous, references: [...references, path] };
592
642
  });
593
643
  requestAnimationFrame(() => document.querySelector<HTMLTextAreaElement>('[data-testid="composer"] textarea')?.focus());
@@ -726,7 +776,7 @@ export function AppShell() {
726
776
 
727
777
  return (
728
778
  <div
729
- className="grid h-screen w-screen overflow-hidden"
779
+ className="pw-app-shell grid w-screen overflow-hidden"
730
780
  style={{
731
781
  gridTemplateColumns: gridCols,
732
782
  // 收起/展开不做过渡:grid-template-columns 是布局属性,动画它会让浏览器逐帧
@@ -822,15 +872,17 @@ export function AppShell() {
822
872
  providerNames={providerNames}
823
873
  authByProvider={authByProvider}
824
874
  addWorkspaceByPicker={addWorkspaceByPicker}
825
- heroModel={heroModel}
875
+ heroChoice={heroChoice}
826
876
  onSelectHeroModel={(provider, id) => {
827
- setHeroModel({ provider, id });
877
+ const choice = { provider, id };
878
+ setHeroModel(choice);
879
+ // 只记在当前工作区名下
880
+ if (heroCwd) writeRememberedHeroModel(localStorage, heroCwd, choice);
828
881
  const levels = modelChoices.find((model) => model.provider === provider && model.id === id)?.thinkingLevels ?? [];
829
882
  if (heroThinking && !levels.includes(heroThinking)) setHeroThinking("");
830
883
  }}
831
- defaultModel={defaultModel}
832
884
  heroModelLevels={heroModelLevels}
833
- heroThinking={heroThinking}
885
+ heroEffectiveThinking={heroEffectiveThinking}
834
886
  onSelectHeroThinking={setHeroThinking}
835
887
  />
836
888
  {/* 草稿阶段的错误行内显示(不再弹右下角) */}
@@ -951,7 +1003,7 @@ export function AppShell() {
951
1003
  contextPercent={snapshot?.contextUsage?.percent ?? null}
952
1004
  contextTokens={snapshot?.contextUsage?.tokens ?? null}
953
1005
  contextWindow={snapshot?.contextUsage?.contextWindow ?? null}
954
- contextSource={{ systemChars: contextSystemChars, messages: state.messages, breakdown: snapshot?.contextBreakdown }}
1006
+ contextSource={{ systemChars: contextSystemChars, messages: state.messages, breakdown: snapshot?.contextBreakdown, loadFull: () => sendCommand({ cmd: "prepare" }) }}
955
1007
  contextVisible={(snapshot?.messages?.length ?? 0) > 0}
956
1008
  model={snapshot?.model}
957
1009
  thinkingLevel={snapshot?.thinkingLevel}
@@ -961,6 +1013,7 @@ export function AppShell() {
961
1013
  authByProvider={authByProvider}
962
1014
  queue={snapshot?.queue ?? { steering: [], followUp: [] }}
963
1015
  commands={slashCommands}
1016
+ cwd={panelCwd}
964
1017
  onCommand={runSlashCommand}
965
1018
  onSend={sendPrompt}
966
1019
  onSteer={(text, images) => sendCommand({ cmd: "prompt", text, images, behavior: "steer" })}
@@ -1037,7 +1090,10 @@ export function AppShell() {
1037
1090
  )}
1038
1091
  {/* 只有远程设备(手机走 tailnet)会走到这里:本机仍然用系统原生对话框 */}
1039
1092
  <DirectoryPicker open={dirPickerOpen} onConfirm={(path) => void confirmDirPicker(path)} onCancel={cancelDirPicker} />
1040
- <ExtensionNotices notices={state.extensionNotices} onDismiss={dismissExtensionNotice} />
1093
+ <ExtensionNotices notices={state.extensionNotices} onDismiss={dismissExtensionNotice}>
1094
+ {/* 每次启动提醒一次有没有新版本(PiWeb / pi),和扩展通知摞在同一处 */}
1095
+ <UpdateNotice />
1096
+ </ExtensionNotices>
1041
1097
  <SettingsPanel
1042
1098
  open={settingsOpen}
1043
1099
  onClose={() => {
@@ -1109,11 +1165,10 @@ function Hero({
1109
1165
  providerNames,
1110
1166
  authByProvider,
1111
1167
  addWorkspaceByPicker,
1112
- heroModel,
1168
+ heroChoice,
1113
1169
  onSelectHeroModel,
1114
- defaultModel,
1115
1170
  heroModelLevels,
1116
- heroThinking,
1171
+ heroEffectiveThinking,
1117
1172
  onSelectHeroThinking,
1118
1173
  }: {
1119
1174
  draft: ChatDraft;
@@ -1135,11 +1190,10 @@ function Hero({
1135
1190
  providerNames: Record<string, string>;
1136
1191
  authByProvider: Record<string, boolean>;
1137
1192
  addWorkspaceByPicker: () => Promise<string | null>;
1138
- heroModel: { provider: string; id: string } | null;
1193
+ heroChoice?: ModelChoice;
1139
1194
  onSelectHeroModel: (provider: string, id: string) => void;
1140
- defaultModel?: ModelChoice;
1141
1195
  heroModelLevels: string[];
1142
- heroThinking: string;
1196
+ heroEffectiveThinking: string;
1143
1197
  onSelectHeroThinking: (level: string) => void;
1144
1198
  }) {
1145
1199
  const { t } = useI18n();
@@ -1233,14 +1287,16 @@ function Hero({
1233
1287
 
1234
1288
  {/* 输入卡 */}
1235
1289
  <ChatInput
1236
- draft={draft}
1237
- onDraftChange={onDraftChange}
1238
- onUploadError={onUploadError}
1239
- onUploadProgress={onUploadProgress}
1240
- uploadFailure={uploadFailure}
1241
- pendingUploadCount={pendingUploadCount}
1242
- pendingSend={pendingSend}
1243
- onSendPendingChange={onSendPendingChange}
1290
+ draft={draft}
1291
+ onDraftChange={onDraftChange}
1292
+ onUploadError={onUploadError}
1293
+ onUploadProgress={onUploadProgress}
1294
+ uploadFailure={uploadFailure}
1295
+ pendingUploadCount={pendingUploadCount}
1296
+ pendingSend={pendingSend}
1297
+ onSendPendingChange={onSendPendingChange}
1298
+ // 新会话也能 @ 引用:候选来自上面选中的工作区
1299
+ cwd={cwd}
1244
1300
  commands={commands}
1245
1301
  onCommand={onCommand}
1246
1302
  isStreaming={false}
@@ -1248,8 +1304,8 @@ function Hero({
1248
1304
  contextTokens={null}
1249
1305
  contextWindow={null}
1250
1306
  contextVisible={false}
1251
- model={heroModel ? models.find((m) => m.provider === heroModel.provider && m.id === heroModel.id) ?? { provider: heroModel.provider, id: heroModel.id, name: heroModel.id, reasoning: false, contextWindow: 0 } : defaultModel}
1252
- thinkingLevel={heroThinking || undefined}
1307
+ model={heroChoice}
1308
+ thinkingLevel={heroEffectiveThinking}
1253
1309
  thinkingLevels={heroModelLevels}
1254
1310
  models={models}
1255
1311
  providerNames={providerNames}
@@ -6,18 +6,23 @@
6
6
  * 运行中:Enter 按设置插话 / 排队,Ctrl(⌘)+Enter 用另一种;停止按钮会把排队消息取回输入框(pi 终端同款);
7
7
  * 排队中的消息列在输入卡上方,↑(空输入框时)取回编辑。Enter 发送 / Shift+Enter 换行。
8
8
  */
9
- import { useEffect, useRef, useState } from "react";
9
+ import { useEffect, useMemo, useRef, useState } from "react";
10
10
  import {
11
11
  IconAtOutline16,
12
12
  IconFileOutline16,
13
+ IconFolderClose16,
13
14
  IconSendArrowUp14,
14
15
  IconStopFill16,
15
16
  IconTerminalOutline14,
16
17
  } from "@/components/icons";
17
18
  import { ContextMeter } from "@/components/ContextMeter";
19
+ import { COLLAPSE_AT, ComposerImages } from "@/components/composer/ComposerImages";
18
20
  import { ModelSelector, type ModelChoice } from "@/components/ModelSelector";
19
21
  import { useI18n } from "@/i18n";
22
+ import { useWorkspaceFiles } from "@/hooks/useWorkspaceFiles";
23
+ import { fmtBytes } from "@/lib/format";
20
24
  import { otherBehavior, useEnterBehavior } from "@/lib/enter-behavior";
25
+ import { atToken, candidateText, suggestPaths, type PathCandidate } from "@/lib/workspace-index";
21
26
  import type { ImageAttachment } from "@/lib/types";
22
27
 
23
28
  export interface SlashCommand {
@@ -55,6 +60,7 @@ export function ChatInput({
55
60
  contextWindow,
56
61
  contextSource,
57
62
  contextVisible = true,
63
+ cwd,
58
64
  model,
59
65
  thinkingLevel,
60
66
  thinkingLevels,
@@ -89,11 +95,14 @@ export function ChatInput({
89
95
  /** 上下文分段数据源:ContextMeter 弹窗打开时才做字符统计 */
90
96
  contextSource?: {
91
97
  systemChars: number;
92
- messages: { role?: string; content: Array<{ type: string; text?: string; thinking?: string }> }[];
98
+ messages: { role?: string; content: Array<{ type: string; text?: string; thinking?: string; arguments?: unknown; name?: string }>; usage?: import("@/lib/types").TrajTokens; stopReason?: string }[];
93
99
  breakdown?: import("@/lib/types").ContextBreakdown;
100
+ loadFull?: () => Promise<unknown> | unknown;
94
101
  };
95
102
  /** 还没有对话时隐藏上下文计量 */
96
103
  contextVisible?: boolean;
104
+ /** 工作区根:@ 引用的候选清单从这里取(不传就不启用 @ 引用) */
105
+ cwd?: string;
97
106
  model?: { provider: string; id: string; name: string };
98
107
  thinkingLevel?: string;
99
108
  thinkingLevels: string[];
@@ -148,10 +157,18 @@ export function ChatInput({
148
157
  const uploadBusy = uploadCount > 0 || pendingUploadCount > 0;
149
158
  const sendBusy = sending || pendingSend;
150
159
  const [dragActive, setDragActive] = useState(false);
160
+ /** 引用 chip 超过 COLLAPSE_AT 就先折叠:手机上折行会把输入框顶掉(图片的折叠在 ComposerImages 里) */
161
+ const [refsExpanded, setRefsExpanded] = useState(false);
151
162
  const dragDepth = useRef(0);
152
163
  const [modelOpen, setModelOpen] = useState(false);
153
164
  const [cmdIdx, setCmdIdx] = useState(0);
154
165
  const [cmdDismissed, setCmdDismissed] = useState(false);
166
+ // @ 引用:打字触发候选,选中后落成引用 chip(发送时统一拼成 `@路径` 行)
167
+ const [mention, setMention] = useState<{ query: string; start: number; tokenEnd: number } | null>(null);
168
+ const [mentionIdx, setMentionIdx] = useState(0);
169
+ /** 被 Esc 关掉的那个 token(位置 + 原文);光标在它里面移动不再弹,改了字、或换成别的 token 重新弹 */
170
+ const mentionDismissed = useRef<string | null>(null);
171
+ const mentionMenuRef = useRef<HTMLDivElement>(null);
155
172
  const taRef = useRef<HTMLTextAreaElement>(null);
156
173
  const wrapRef = useRef<HTMLDivElement>(null);
157
174
  const { t } = useI18n();
@@ -178,6 +195,79 @@ export function ChatInput({
178
195
  const filteredCommands = (commands ?? []).filter((c) => c.name.toLowerCase().startsWith(slash.split(" ")[0].toLowerCase()));
179
196
  const visibleCommands = filteredCommands;
180
197
  const commandMode = typedCommandMode;
198
+
199
+ // ---------- @ 引用:输入 @ 弹候选,选中落成引用 chip ----------
200
+ // 候选清单一次拉全量(工作区 git 可见文件),之后每个字符都在本地匹配;规则与 pi 终端的 @ 补全一致(见 workspace-index)。
201
+ const mentionMode = mention !== null;
202
+ const mentionQuery = mention?.query ?? null;
203
+ const files = useWorkspaceFiles(cwd ?? "", mentionMode);
204
+ // 只在候选清单或查询变了时重算:光标移动、其它状态变化引起的重渲染不该每次扫一遍全量清单
205
+ const suggestions = useMemo(
206
+ () => (mentionQuery === null ? [] : suggestPaths(files.candidates, mentionQuery)),
207
+ [files.candidates, mentionQuery],
208
+ );
209
+ const effectiveMentionIdx = Math.min(mentionIdx, Math.max(0, suggestions.length - 1));
210
+
211
+ useEffect(() => {
212
+ mentionMenuRef.current?.querySelector('[aria-selected="true"]')?.scrollIntoView({ block: "nearest" });
213
+ }, [mentionIdx, suggestions.length]);
214
+
215
+ /**
216
+ * 文本 / 光标变了就重新判是否在 @ token 里(Esc 关过的同一个 token 不再弹)。
217
+ * 没有工作区就不弹:没有候选来源,菜单只会空转还拦着方向键和 Esc。
218
+ */
219
+ const syncMention = (value: string, caret: number | null) => {
220
+ const token = caret === null || !cwd ? null : atToken(value, caret);
221
+ if (!token) {
222
+ // 离开 token 就忘掉 Esc:之后在同一位置打新的 @ 照常弹
223
+ mentionDismissed.current = null;
224
+ setMention(null);
225
+ return;
226
+ }
227
+ if (mentionDismissed.current === `${token.start}:${value.slice(token.start, token.tokenEnd)}`) {
228
+ setMention(null);
229
+ return;
230
+ }
231
+ if (!mention || mention.start !== token.start || mention.query !== token.query) setMentionIdx(0);
232
+ if (!mention || mention.start !== token.start || mention.query !== token.query || mention.tokenEnd !== token.tokenEnd) {
233
+ setMention({ query: token.query, start: token.start, tokenEnd: token.tokenEnd });
234
+ }
235
+ };
236
+
237
+ const closeMention = (dismiss: boolean) => {
238
+ if (dismiss && mention) mentionDismissed.current = `${mention.start}:${text.slice(mention.start, mention.tokenEnd)}`;
239
+ setMention(null);
240
+ setMentionIdx(0);
241
+ };
242
+
243
+ /** 选中候选:目录引用文件夹本身(要往下钻就继续打 `@src/lib/`),token 从正文里播掉 */
244
+ const applyMention = (candidate: PathCandidate) => {
245
+ if (!mention) return;
246
+ const { start, tokenEnd } = mention;
247
+ // 目录存成 `src/lib/`:chip 靠尾斜杠判断文件夹(去重会把尾斜杠归一化掉)
248
+ const path = candidate.dir ? `${candidate.path}/` : candidate.path;
249
+ changeDraft((previous) => {
250
+ const refs = previous.references ?? [];
251
+ // 目录带尾斜杠、文件不带:去重按去掉尾斜杠比较,
252
+ // 否则「文件面板引用的 src/lib」与「@ 选的 src/lib/」会变成两个 chip
253
+ const samePath = (a: string, b: string) => a.replace(/[\\/]+$/, "") === b.replace(/[\\/]+$/, "");
254
+ return {
255
+ ...previous,
256
+ text: previous.text.slice(0, start) + previous.text.slice(tokenEnd),
257
+ references: refs.some((existing) => samePath(existing, path)) ? refs : [...refs, path],
258
+ };
259
+ });
260
+ setMention(null);
261
+ setMentionIdx(0);
262
+ requestAnimationFrame(() => {
263
+ autoSize();
264
+ const ta = taRef.current;
265
+ if (ta) {
266
+ ta.focus();
267
+ ta.setSelectionRange(start, start);
268
+ }
269
+ });
270
+ };
181
271
  // 命令菜单滚动:.popover 的 overflow:hidden 会压掉 overflow-y-auto(unlayered CSS 优先),
182
272
  // 内联 overflowY 强制可滚;键盘导航时把选中项滚进视野
183
273
  const cmdMenuRef = useRef<HTMLDivElement>(null);
@@ -444,6 +534,63 @@ export function ChatInput({
444
534
  {attachmentError}
445
535
  </div>
446
536
  )}
537
+ {/* @ 引用候选(与斜杠菜单同一处锚点:输入卡上方) */}
538
+ {mentionMode && (
539
+ <div
540
+ ref={mentionMenuRef}
541
+ data-testid="mention-menu"
542
+ className="popover absolute bottom-full left-0 right-0 z-50 mb-2 max-h-[340px] overflow-y-auto py-1.5"
543
+ role="listbox"
544
+ aria-label={t.mentionFiles}
545
+ style={{ overflowY: "auto" }}
546
+ >
547
+ <div className="flex items-center gap-2 px-4 pb-1 pt-1" style={{ fontSize: 11.5, color: "var(--dsw-label-caption)" }}>
548
+ <span>{t.mentionFiles}</span>
549
+ {files.truncated && <span style={{ color: "var(--dsw-warn)" }}>{t.mentionTruncated}</span>}
550
+ {files.source === "disk" && <span title={t.mentionDiskSourceHint}>{t.mentionDiskSource}</span>}
551
+ <span className="flex-1" />
552
+ <span>{t.mentionHint}</span>
553
+ </div>
554
+ {files.status === "loading" && (
555
+ <div className="px-4 py-2" style={{ fontSize: 12.5, color: "var(--dsw-label-caption)" }}>{t.panelLoading}</div>
556
+ )}
557
+ {files.status === "error" && (
558
+ <div className="px-4 py-2" style={{ fontSize: 12.5, color: "var(--dsw-danger)" }} role="alert">{files.error}</div>
559
+ )}
560
+ {files.status === "ready" && suggestions.length === 0 && (
561
+ <div className="px-4 py-2" style={{ fontSize: 12.5, color: "var(--dsw-label-caption)" }}>{t.mentionNoMatch}</div>
562
+ )}
563
+ {suggestions.map(({ candidate, label, description }, i) => (
564
+ <button
565
+ key={`${candidate.dir ? "d" : "f"}:${candidate.path}`}
566
+ type="button"
567
+ role="option"
568
+ aria-selected={i === effectiveMentionIdx}
569
+ className="flex w-full items-center gap-2 px-4 py-1.5 text-left transition-colors"
570
+ style={{ background: i === effectiveMentionIdx ? "var(--dsw-hover)" : "transparent" }}
571
+ onMouseEnter={() => setMentionIdx(i)}
572
+ onClick={() => applyMention(candidate)}
573
+ title={candidateText(candidate)}
574
+ >
575
+ {candidate.dir ? (
576
+ <IconFolderClose16 size={13} style={{ flex: "none", color: "var(--dsw-warn)" }} />
577
+ ) : (
578
+ <IconFileOutline16 size={13} style={{ flex: "none", color: "var(--dsw-label-caption)" }} />
579
+ )}
580
+ {/* 和 pi 终端一样:名字在前,路径在后;根目录下的条目两者相同,不重复显示 */}
581
+ <span className="flex-none truncate" style={{ maxWidth: "55%", fontSize: 12.5, fontFamily: "var(--font-mono)", color: "var(--dsw-label-primary)" }}>
582
+ {label}
583
+ </span>
584
+ {description.replace(/\/$/, "") !== label.replace(/\/$/, "") && (
585
+ <span className="min-w-0 flex-1 truncate" style={{ fontSize: 11.5, fontFamily: "var(--font-mono)", color: "var(--dsw-label-caption)" }}>
586
+ {description}
587
+ </span>
588
+ )}
589
+ </button>
590
+ ))}
591
+ </div>
592
+ )}
593
+
447
594
  {/* 斜杠命令菜单(dsh:悬浮于输入卡上方,同宽) */}
448
595
  {commandMode && visibleCommands.length > 0 && (
449
596
  <div ref={cmdMenuRef} className="popover absolute bottom-full left-0 right-0 z-50 mb-2 max-h-[340px] overflow-y-auto py-1.5" role="listbox" aria-label={t.slashCatalog} style={{ overflowY: "auto" }}>
@@ -477,65 +624,62 @@ export function ChatInput({
477
624
  </div>
478
625
  )}
479
626
 
480
- {/* 附件轨:图片 */}
627
+ {/* 附件轨:图片(超过 4 张折叠成牌堆,展开沿弧线扇开;点开可放大、切换、删除) */}
481
628
  {images.length > 0 && (
482
- <div className="mb-2 flex flex-wrap gap-2 px-1">
483
- {images.map((img, i) => (
484
- <div key={i} className="relative">
485
- <img
486
- src={`data:${img.mimeType};base64,${img.data}`}
487
- alt=""
488
- className="h-14 w-14 rounded-lg object-cover"
489
- style={{ border: "0.5px solid var(--dsw-border-l2)" }}
490
- />
491
- <button
492
- className="absolute -right-1.5 -top-1.5 flex h-5 w-5 items-center justify-center rounded-full text-[10px]"
493
- style={{ background: "var(--dsw-label-primary)", color: "var(--dsw-bg-base)" }}
494
- disabled={sendBusy}
495
- onClick={() => changeDraft((previous) => ({ ...previous, images: previous.images.filter((_, index) => index !== i) }))}
496
- >
497
- ✕
498
- </button>
499
- </div>
500
- ))}
501
- </div>
629
+ <ComposerImages
630
+ images={images}
631
+ disabled={sendBusy}
632
+ onRemove={(index) => changeDraft((previous) => ({ ...previous, images: previous.images.filter((_, at) => at !== index) }))}
633
+ />
502
634
  )}
503
635
 
504
- {/* 附件轨:上传的普通文件(压缩包/文档等,路径引用) */}
505
636
  {/* 路径引用(项目结构图/文件面板的「引用到输入框」):独立于正文,发送时拼成 @路径 */}
506
637
  {references.length > 0 && (
507
638
  <div className="mb-2 flex flex-wrap gap-1.5 px-1" data-testid="composer-references">
508
- {references.map((path) => (
509
- <span
510
- key={path}
511
- className="flex items-center gap-1 rounded-md px-2 py-1"
512
- style={{
513
- maxWidth: 240,
514
- fontSize: 12,
515
- fontFamily: "var(--font-mono)",
516
- color: "var(--dsw-label-secondary)",
517
- background: "var(--dsw-selector)",
518
- border: "0.5px solid var(--dsw-border-l3)",
519
- }}
520
- title={path}
521
- >
522
- <IconAtOutline16 size={11} style={{ flex: "none", color: "var(--dsw-accent)" }} />
523
- <span className="truncate">{referenceLabelOf(path)}</span>
524
- <button
525
- type="button"
526
- className="flex h-4 w-4 flex-none items-center justify-center rounded-full text-[10px]"
527
- style={{ color: "var(--dsw-label-caption)" }}
528
- disabled={sendBusy}
529
- onClick={() => changeDraft((previous) => ({ ...previous, references: (previous.references ?? []).filter((p) => p !== path) }))}
530
- aria-label={`${t.delete} ${path}`}
639
+ {(refsExpanded ? references : references.slice(0, COLLAPSE_AT)).map((path) => {
640
+ const chip = referenceChipOf(path);
641
+ return (
642
+ <span
643
+ key={path}
644
+ className="flex items-center gap-1 rounded-md px-2 py-1"
645
+ style={{
646
+ maxWidth: 260,
647
+ fontSize: 12,
648
+ fontFamily: "var(--font-mono)",
649
+ color: "var(--dsw-label-secondary)",
650
+ background: "var(--dsw-selector)",
651
+ border: "0.5px solid var(--dsw-border-l3)",
652
+ }}
653
+ title={path}
531
654
  >
532
- ✕
533
- </button>
534
- </span>
535
- ))}
655
+ {chip.dir ? (
656
+ <IconFolderClose16 size={12} style={{ flex: "none", color: "var(--dsw-warn)" }} />
657
+ ) : (
658
+ <IconFileOutline16 size={12} style={{ flex: "none", color: "var(--dsw-accent)" }} />
659
+ )}
660
+ <span className="truncate">{chip.text}</span>
661
+ <button
662
+ type="button"
663
+ className="flex h-4 w-4 flex-none items-center justify-center rounded-full text-[10px]"
664
+ style={{ color: "var(--dsw-label-caption)" }}
665
+ disabled={sendBusy}
666
+ onClick={() => changeDraft((previous) => ({ ...previous, references: (previous.references ?? []).filter((p) => p !== path) }))}
667
+ aria-label={`${t.delete} ${path}`}
668
+ >
669
+ ✕
670
+ </button>
671
+ </span>
672
+ );
673
+ })}
674
+ {references.length > COLLAPSE_AT ? (
675
+ <button type="button" className="pw-chip" data-testid="references-toggle" onClick={() => setRefsExpanded((open) => !open)}>
676
+ {refsExpanded ? t.chipCollapse : t.chipExpandMore.replace("{n}", String(references.length - COLLAPSE_AT))}
677
+ </button>
678
+ ) : null}
536
679
  </div>
537
680
  )}
538
681
 
682
+ {/* 附件轨:上传的普通文件(压缩包/文档等,路径引用) */}
539
683
  {uploads.length > 0 && (
540
684
  <div className="mb-2 flex flex-wrap gap-2 px-1">
541
685
  {uploads.map((f, i) => (
@@ -604,12 +748,19 @@ export function ChatInput({
604
748
  suppressHydrationWarning
605
749
  className="min-w-0 flex-1 resize-none"
606
750
  style={{ fontSize: "var(--piweb-chat-font-size, var(--dsh-content-font-size))", lineHeight: 1.55 }}
607
- onChange={(e) => {
608
- setText(e.target.value);
609
- setCmdDismissed(false);
751
+ onChange={(e) => {
752
+ const value = e.target.value;
753
+ setText(value);
754
+ setCmdDismissed(false);
610
755
  setCmdIdx(0);
756
+ syncMention(value, e.target.selectionStart);
611
757
  autoSize();
612
758
  }}
759
+ onSelect={(e) => {
760
+ // 选中了一段文字时不弹:那时 Enter / Tab 应该照常工作
761
+ const ta = e.target as HTMLTextAreaElement;
762
+ syncMention(ta.value, ta.selectionStart === ta.selectionEnd ? ta.selectionStart : null);
763
+ }}
613
764
  onPaste={(e) => {
614
765
  const files = Array.from(e.clipboardData.files);
615
766
  if (files.length) {
@@ -618,6 +769,34 @@ export function ChatInput({
618
769
  }
619
770
  }}
620
771
  onKeyDown={(e) => {
772
+ // 输入法组字时的按键归输入法(Esc 取消组字、↑↓ 选字、Enter 上屏),一律不当候选操作
773
+ if (mentionMode && !e.nativeEvent.isComposing) {
774
+ // 候选开着时优先于发送:Esc 关掉、↑↓ 选、Enter / Tab 采纳。
775
+ // 没有候选时不拦 Enter——`@某人` 这种不成候选的文字要能直接发出去。
776
+ if (e.key === "Escape") {
777
+ e.preventDefault();
778
+ closeMention(true);
779
+ return;
780
+ }
781
+ if (suggestions.length > 0) {
782
+ if (e.key === "ArrowDown") {
783
+ e.preventDefault();
784
+ setMentionIdx((i) => Math.min(i + 1, suggestions.length - 1));
785
+ return;
786
+ }
787
+ if (e.key === "ArrowUp") {
788
+ e.preventDefault();
789
+ setMentionIdx((i) => Math.max(i - 1, 0));
790
+ return;
791
+ }
792
+ if (e.key === "Enter" || e.key === "Tab") {
793
+ e.preventDefault();
794
+ const picked = suggestions[effectiveMentionIdx];
795
+ if (picked) applyMention(picked.candidate);
796
+ return;
797
+ }
798
+ }
799
+ }
621
800
  if (adopted && text === "" && (e.key === "Backspace" || e.key === "Escape")) {
622
801
  // 空参数时退格/Esc:摘掉命令胶囊
623
802
  e.preventDefault();
@@ -710,6 +889,7 @@ export function ChatInput({
710
889
  percent={contextPercent}
711
890
  tokens={contextTokens}
712
891
  contextWindow={contextWindow}
892
+ model={model}
713
893
  source={contextSource}
714
894
  />
715
895
  )}
@@ -772,17 +952,19 @@ export function ChatInput({
772
952
  }
773
953
 
774
954
  function fmtUploadSize(bytes: number): string {
775
- if (bytes >= 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
776
- if (bytes >= 1024) return `${Math.round(bytes / 1024)}KB`;
777
- return `${bytes}B`;
955
+ return fmtBytes(bytes).replace(/ /g, "");
778
956
  }
779
957
 
780
958
  /**
781
- * 引用 chip 上的显示文本:只要最后一段(文件名/目录名),太挤就靠 CSS 截断。
782
- * 目录引用带尾斜杠,和文件区分开(与项目结构图里的展示一致)。
959
+ * 引用 chip 上显示的短标签。只取 basename 会分不清两件事:目录与同名文件
960
+ * (`src/lib` 与 `src/lib.ts` 都显示 `lib`;`a/index.ts` 与 `b/index.ts` 都显示 `index.ts`)。
961
+ * 规则:目录带尾斜杠(`lib/`,配文件夹图标);其余最多保留**尾部两段**
962
+ * (`src/lib/index.ts` → `…/lib/index.ts`)。完整路径仍在 title 里。
783
963
  */
784
- function referenceLabelOf(path: string): string {
785
- const trimmed = path.replace(/[\\/]+$/, "");
786
- const base = trimmed.split(/[\\/]/).filter(Boolean).pop() ?? path;
787
- return /[\\/]$/.test(path) ? `${base}/` : base;
964
+ function referenceChipOf(path: string): { text: string; dir: boolean } {
965
+ const dir = /[\\/]$/.test(path);
966
+ const parts = path.replace(/[\\/]+$/, "").split(/[\\/]/).filter(Boolean);
967
+ if (!parts.length) return { text: path, dir };
968
+ const tail = parts.slice(-2).join("/");
969
+ return { text: dir ? `${tail}/` : parts.length > 2 ? `…/${tail}` : tail, dir };
788
970
  }