@rexvane/piweb 0.3.24 → 0.3.26

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 (285) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/app-path-routes-manifest.json +11 -11
  3. package/.next/build-manifest.json +3 -3
  4. package/.next/prerender-manifest.json +5 -5
  5. package/.next/react-loadable-manifest.json +13 -20
  6. package/.next/required-server-files.js +2 -1
  7. package/.next/required-server-files.json +2 -1
  8. package/.next/routes-manifest.json +6 -8
  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 +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]/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/agent/resources/route.js +1 -0
  32. package/.next/server/app/api/agent/resources/route.js.nft.json +1 -0
  33. package/.next/server/app/api/agent/resources/route_client-reference-manifest.js +1 -0
  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 +4 -4
  41. package/.next/server/app/api/git/route.js.nft.json +1 -1
  42. package/.next/server/app/api/github/route.js +4 -4
  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/models/defaults/route.js +1 -1
  48. package/.next/server/app/api/models/defaults/route.js.nft.json +1 -1
  49. package/.next/server/app/api/models/route.js +1 -1
  50. package/.next/server/app/api/models/route.js.nft.json +1 -1
  51. package/.next/server/app/api/pi-settings/route.js +1 -1
  52. package/.next/server/app/api/pi-settings/route.js.nft.json +1 -1
  53. package/.next/server/app/api/plugins/route.js +1 -1
  54. package/.next/server/app/api/plugins/route.js.nft.json +1 -1
  55. package/.next/server/app/api/prompts/route.js +1 -1
  56. package/.next/server/app/api/prompts/route.js.nft.json +1 -1
  57. package/.next/server/app/api/remote/route.js +4 -3
  58. package/.next/server/app/api/security/route.js +1 -1
  59. package/.next/server/app/api/security/route.js.nft.json +1 -1
  60. package/.next/server/app/api/sessions/[id]/export/route.js +1 -1
  61. package/.next/server/app/api/sessions/[id]/export/route.js.nft.json +1 -1
  62. package/.next/server/app/api/sessions/[id]/route.js +1 -1
  63. package/.next/server/app/api/sessions/[id]/route.js.nft.json +1 -1
  64. package/.next/server/app/api/sessions/import/route.js +2 -2
  65. package/.next/server/app/api/sessions/import/route.js.nft.json +1 -1
  66. package/.next/server/app/api/sessions/route.js +1 -1
  67. package/.next/server/app/api/sessions/route.js.nft.json +1 -1
  68. package/.next/server/app/api/skills/route.js +2 -2
  69. package/.next/server/app/api/skills/route.js.nft.json +1 -1
  70. package/.next/server/app/api/update/route.js +2 -2
  71. package/.next/server/app/api/update/route.js.nft.json +1 -1
  72. package/.next/server/app/api/version/route.js +1 -1
  73. package/.next/server/app/api/version/route.js.nft.json +1 -1
  74. package/.next/server/app/api/web-auth/route.js +2 -2
  75. package/.next/server/app/api/workspaces/route.js +1 -1
  76. package/.next/server/app/api/workspaces/route.js.nft.json +1 -1
  77. package/.next/server/app/icon.svg/route.js +1 -1
  78. package/.next/server/app/index.html +1 -1
  79. package/.next/server/app/index.rsc +6 -7
  80. package/.next/server/app/index.segments/__PAGE__.segment.rsc +6 -7
  81. package/.next/server/app/index.segments/_full.segment.rsc +6 -7
  82. package/.next/server/app/index.segments/_tree.segment.rsc +2 -3
  83. package/.next/server/app/login/page.js +1 -1
  84. package/.next/server/app/login/page_client-reference-manifest.js +1 -1
  85. package/.next/server/app/login.html +1 -1
  86. package/.next/server/app/login.rsc +3 -3
  87. package/.next/server/app/login.segments/_full.segment.rsc +3 -3
  88. package/.next/server/app/login.segments/_tree.segment.rsc +2 -2
  89. package/.next/server/app/login.segments/login/__PAGE__.segment.rsc +3 -3
  90. package/.next/server/app/page.js +1 -1
  91. package/.next/server/app/page.js.nft.json +1 -1
  92. package/.next/server/app/page_client-reference-manifest.js +1 -1
  93. package/.next/server/app-paths-manifest.json +11 -11
  94. package/.next/server/chunks/3924.js +3 -3
  95. package/.next/server/chunks/538.js +1 -1
  96. package/.next/server/chunks/620.js +1 -0
  97. package/.next/server/chunks/6229.js +1 -1
  98. package/.next/server/chunks/7496.js +1 -1
  99. package/.next/server/chunks/7662.js +2 -2
  100. package/.next/server/chunks/8686.js +1 -1
  101. package/.next/server/functions-config-manifest.json +1 -2
  102. package/.next/server/instrumentation.js.nft.json +1 -1
  103. package/.next/server/middleware-build-manifest.js +1 -1
  104. package/.next/server/middleware-react-loadable-manifest.js +1 -1
  105. package/.next/server/pages/404.html +1 -1
  106. package/.next/server/pages/500.html +1 -1
  107. package/.next/server/preview-props.json +3 -3
  108. package/.next/server/server-reference-manifest.json +1 -1
  109. package/.next/static/QJCtqgDKzGS2BdKABd9ch/_buildManifest.js +1 -0
  110. package/.next/static/chunks/2577.86c8538fbf05a8b2.js +1 -0
  111. package/.next/static/chunks/2716.fa561519675a3ff1.js +1 -0
  112. package/.next/static/chunks/2906.8a0e09bb47605cf1.js +1 -0
  113. package/.next/static/chunks/4673.ea36e55005c0f82f.js +1 -0
  114. package/.next/static/chunks/4686-4dc565817a160351.js +1 -0
  115. package/.next/static/chunks/4778.0d2c5d75b791d352.js +1 -0
  116. package/.next/static/chunks/{618.949981da99be7182.js → 618.9a22656b863f242b.js} +2 -2
  117. package/.next/static/chunks/app/_global-error/page-f2d93969c0bcf406.js +1 -0
  118. package/.next/static/chunks/app/api/agent/[id]/events/route-f2d93969c0bcf406.js +1 -0
  119. package/.next/static/chunks/app/api/agent/[id]/route-f2d93969c0bcf406.js +1 -0
  120. package/.next/static/chunks/app/api/agent/[id]/tree/route-f2d93969c0bcf406.js +1 -0
  121. package/.next/static/chunks/app/api/agent/new/route-f2d93969c0bcf406.js +1 -0
  122. package/.next/static/chunks/app/api/agent/resources/route-f2d93969c0bcf406.js +1 -0
  123. package/.next/static/chunks/app/api/config/route-f2d93969c0bcf406.js +1 -0
  124. package/.next/static/chunks/app/api/files/route-f2d93969c0bcf406.js +1 -0
  125. package/.next/static/chunks/app/api/fs/route-f2d93969c0bcf406.js +1 -0
  126. package/.next/static/chunks/app/api/git/route-f2d93969c0bcf406.js +1 -0
  127. package/.next/static/chunks/app/api/github/route-f2d93969c0bcf406.js +1 -0
  128. package/.next/static/chunks/app/api/growth/route-f2d93969c0bcf406.js +1 -0
  129. package/.next/static/chunks/app/api/health/route-f2d93969c0bcf406.js +1 -0
  130. package/.next/static/chunks/app/api/models/defaults/route-f2d93969c0bcf406.js +1 -0
  131. package/.next/static/chunks/app/api/models/route-f2d93969c0bcf406.js +1 -0
  132. package/.next/static/chunks/app/api/pi-settings/route-f2d93969c0bcf406.js +1 -0
  133. package/.next/static/chunks/app/api/plugins/route-f2d93969c0bcf406.js +1 -0
  134. package/.next/static/chunks/app/api/prompts/route-f2d93969c0bcf406.js +1 -0
  135. package/.next/static/chunks/app/api/remote/route-f2d93969c0bcf406.js +1 -0
  136. package/.next/static/chunks/app/api/security/route-f2d93969c0bcf406.js +1 -0
  137. package/.next/static/chunks/app/api/sessions/[id]/export/route-f2d93969c0bcf406.js +1 -0
  138. package/.next/static/chunks/app/api/sessions/[id]/route-f2d93969c0bcf406.js +1 -0
  139. package/.next/static/chunks/app/api/sessions/import/route-f2d93969c0bcf406.js +1 -0
  140. package/.next/static/chunks/app/api/sessions/route-f2d93969c0bcf406.js +1 -0
  141. package/.next/static/chunks/app/api/skills/route-f2d93969c0bcf406.js +1 -0
  142. package/.next/static/chunks/app/api/update/route-f2d93969c0bcf406.js +1 -0
  143. package/.next/static/chunks/app/api/version/route-f2d93969c0bcf406.js +1 -0
  144. package/.next/static/chunks/app/api/web-auth/route-f2d93969c0bcf406.js +1 -0
  145. package/.next/static/chunks/app/api/workspaces/route-f2d93969c0bcf406.js +1 -0
  146. package/.next/static/chunks/app/page-71f2d3b3ed864b3d.js +1 -0
  147. package/.next/static/chunks/next/dist/client/components/builtin/app-error-f2d93969c0bcf406.js +1 -0
  148. package/.next/static/chunks/next/dist/client/components/builtin/forbidden-f2d93969c0bcf406.js +1 -0
  149. package/.next/static/chunks/next/dist/client/components/builtin/not-found-f2d93969c0bcf406.js +1 -0
  150. package/.next/static/chunks/next/dist/client/components/builtin/unauthorized-f2d93969c0bcf406.js +1 -0
  151. package/.next/static/chunks/webpack-ffc09b5fcc8aaa6d.js +1 -0
  152. package/.next/static/css/014eb1f21414e615.css +1 -0
  153. package/.next/static/css/56b129a13637fcfd.css +1 -0
  154. package/.next/static/css/7e3caf797e4cb19a.css +1 -0
  155. package/.next/types/app/api/agent/{[id]/side-chat → resources}/route.ts +3 -3
  156. package/.next/types/routes.d.ts +2 -2
  157. package/.next/types/validator.ts +9 -9
  158. package/README.md +1 -1
  159. package/next.config.ts +1 -1
  160. package/package.json +9 -9
  161. package/scripts/apply-global.mjs +87 -0
  162. package/scripts/launcher.mjs +21 -0
  163. package/scripts/testing/prepare-sdk-tools.mjs +19 -0
  164. package/src/app/api/agent/new/route.ts +2 -1
  165. package/src/app/api/agent/resources/route.ts +18 -0
  166. package/src/app/api/models/route.ts +22 -1
  167. package/src/app/api/plugins/route.ts +3 -2
  168. package/src/app/api/prompts/route.ts +3 -2
  169. package/src/app/api/remote/route.ts +22 -4
  170. package/src/app/api/sessions/[id]/export/route.ts +3 -1
  171. package/src/app/api/skills/route.ts +3 -2
  172. package/src/app/globals.css +1002 -1765
  173. package/src/components/AppShell.tsx +128 -93
  174. package/src/components/ChatInput.tsx +177 -98
  175. package/src/components/ChatWindow.tsx +189 -56
  176. package/src/components/ContextMeter.tsx +33 -9
  177. package/src/components/CustomSelect.module.css +142 -0
  178. package/src/components/CustomSelect.tsx +157 -0
  179. package/src/components/DrawerPanel.tsx +1 -1
  180. package/src/components/ExtensionUI.tsx +9 -3
  181. package/src/components/FileViewer.tsx +24 -21
  182. package/src/components/GitPanel.tsx +35 -13
  183. package/src/components/GrowthTree.tsx +6 -2
  184. package/src/components/ImageViewer.tsx +72 -0
  185. package/src/components/MarkdownBlock.tsx +67 -96
  186. package/src/components/ModelSelector.tsx +36 -39
  187. package/src/components/PiMark.tsx +76 -41
  188. package/src/components/PreviewImage.tsx +33 -0
  189. package/src/components/ProjectPanel.tsx +8 -5
  190. package/src/components/PromptPanel.tsx +85 -20
  191. package/src/components/ProviderSetupModal.module.css +12 -11
  192. package/src/components/ProviderSetupModal.tsx +107 -36
  193. package/src/components/RemoteSection.tsx +53 -2
  194. package/src/components/SessionImportSection.tsx +30 -15
  195. package/src/components/SessionSidebar.tsx +72 -62
  196. package/src/components/SettingsPanel.tsx +49 -151
  197. package/src/components/TrajectoryAnalyzer.module.css +26 -11
  198. package/src/components/TrajectoryView.tsx +11 -3
  199. package/src/components/chat/markdown-actions.tsx +23 -0
  200. package/src/components/composer/ComposerFiles.tsx +224 -0
  201. package/src/components/composer/ComposerImages.tsx +59 -127
  202. package/src/hooks/usePiWeb.ts +47 -2
  203. package/src/i18n.tsx +62 -80
  204. package/src/lib/agent-manager.ts +365 -33
  205. package/src/lib/chat/file-path.ts +54 -0
  206. package/src/lib/chat/skill-expansion.ts +51 -0
  207. package/src/lib/extension-ui.ts +25 -1
  208. package/src/lib/font.ts +37 -39
  209. package/src/lib/git-service.ts +4 -2
  210. package/src/lib/growth-turns.ts +3 -1
  211. package/src/lib/model-draft.ts +69 -2
  212. package/src/lib/models-service.ts +53 -3
  213. package/src/lib/pi.ts +2 -2
  214. package/src/lib/remote/assets.json +1 -1
  215. package/src/lib/remote/supervisor.ts +48 -5
  216. package/src/lib/security-service.ts +2 -1
  217. package/src/lib/session-reader.ts +25 -13
  218. package/src/lib/session-retention.ts +11 -8
  219. package/src/lib/skills-service.ts +2 -1
  220. package/src/lib/slash-command-token.ts +14 -0
  221. package/src/lib/trajectory.ts +111 -15
  222. package/src/lib/types.ts +28 -3
  223. package/src/lib/update-service.ts +29 -9
  224. package/src/lib/workspace-store.ts +36 -1
  225. package/.next/server/app/api/agent/[id]/side-chat/route.js +0 -5
  226. package/.next/server/app/api/agent/[id]/side-chat/route.js.nft.json +0 -1
  227. package/.next/server/app/api/agent/[id]/side-chat/route_client-reference-manifest.js +0 -1
  228. package/.next/server/chunks/3421.js +0 -1
  229. package/.next/static/chunks/2716.44fdf1b21454b961.js +0 -1
  230. package/.next/static/chunks/4673.e261707dce391be7.js +0 -1
  231. package/.next/static/chunks/4686-0cd87513f9ea3538.js +0 -1
  232. package/.next/static/chunks/4778.eddd3a5813f85824.js +0 -1
  233. package/.next/static/chunks/7411.5f79671208209b13.js +0 -1
  234. package/.next/static/chunks/8552.25c39ef4c995eba0.js +0 -1
  235. package/.next/static/chunks/app/_global-error/page-cbaaee0d7aacb85a.js +0 -1
  236. package/.next/static/chunks/app/api/agent/[id]/events/route-cbaaee0d7aacb85a.js +0 -1
  237. package/.next/static/chunks/app/api/agent/[id]/route-cbaaee0d7aacb85a.js +0 -1
  238. package/.next/static/chunks/app/api/agent/[id]/side-chat/route-cbaaee0d7aacb85a.js +0 -1
  239. package/.next/static/chunks/app/api/agent/[id]/tree/route-cbaaee0d7aacb85a.js +0 -1
  240. package/.next/static/chunks/app/api/agent/new/route-cbaaee0d7aacb85a.js +0 -1
  241. package/.next/static/chunks/app/api/config/route-cbaaee0d7aacb85a.js +0 -1
  242. package/.next/static/chunks/app/api/files/route-cbaaee0d7aacb85a.js +0 -1
  243. package/.next/static/chunks/app/api/fs/route-cbaaee0d7aacb85a.js +0 -1
  244. package/.next/static/chunks/app/api/git/route-cbaaee0d7aacb85a.js +0 -1
  245. package/.next/static/chunks/app/api/github/route-cbaaee0d7aacb85a.js +0 -1
  246. package/.next/static/chunks/app/api/growth/route-cbaaee0d7aacb85a.js +0 -1
  247. package/.next/static/chunks/app/api/health/route-cbaaee0d7aacb85a.js +0 -1
  248. package/.next/static/chunks/app/api/models/defaults/route-cbaaee0d7aacb85a.js +0 -1
  249. package/.next/static/chunks/app/api/models/route-cbaaee0d7aacb85a.js +0 -1
  250. package/.next/static/chunks/app/api/pi-settings/route-cbaaee0d7aacb85a.js +0 -1
  251. package/.next/static/chunks/app/api/plugins/route-cbaaee0d7aacb85a.js +0 -1
  252. package/.next/static/chunks/app/api/prompts/route-cbaaee0d7aacb85a.js +0 -1
  253. package/.next/static/chunks/app/api/remote/route-cbaaee0d7aacb85a.js +0 -1
  254. package/.next/static/chunks/app/api/security/route-cbaaee0d7aacb85a.js +0 -1
  255. package/.next/static/chunks/app/api/sessions/[id]/export/route-cbaaee0d7aacb85a.js +0 -1
  256. package/.next/static/chunks/app/api/sessions/[id]/route-cbaaee0d7aacb85a.js +0 -1
  257. package/.next/static/chunks/app/api/sessions/import/route-cbaaee0d7aacb85a.js +0 -1
  258. package/.next/static/chunks/app/api/sessions/route-cbaaee0d7aacb85a.js +0 -1
  259. package/.next/static/chunks/app/api/skills/route-cbaaee0d7aacb85a.js +0 -1
  260. package/.next/static/chunks/app/api/update/route-cbaaee0d7aacb85a.js +0 -1
  261. package/.next/static/chunks/app/api/version/route-cbaaee0d7aacb85a.js +0 -1
  262. package/.next/static/chunks/app/api/web-auth/route-cbaaee0d7aacb85a.js +0 -1
  263. package/.next/static/chunks/app/api/workspaces/route-cbaaee0d7aacb85a.js +0 -1
  264. package/.next/static/chunks/app/page-dc3cb41d91c7432e.js +0 -1
  265. package/.next/static/chunks/next/dist/client/components/builtin/app-error-cbaaee0d7aacb85a.js +0 -1
  266. package/.next/static/chunks/next/dist/client/components/builtin/forbidden-cbaaee0d7aacb85a.js +0 -1
  267. package/.next/static/chunks/next/dist/client/components/builtin/not-found-cbaaee0d7aacb85a.js +0 -1
  268. package/.next/static/chunks/next/dist/client/components/builtin/unauthorized-cbaaee0d7aacb85a.js +0 -1
  269. package/.next/static/chunks/webpack-2ac3d76b81895f5b.js +0 -1
  270. package/.next/static/css/08815507594e8b8d.css +0 -1
  271. package/.next/static/css/9fc559c2c4ad1d0f.css +0 -1
  272. package/.next/static/css/c6e2227c36ec2fd6.css +0 -1
  273. package/.next/static/css/f15a853af8b5127f.css +0 -1
  274. package/.next/static/lvKCJmjFj8LuqSojRVLRF/_buildManifest.js +0 -1
  275. package/src/app/api/agent/[id]/side-chat/route.ts +0 -77
  276. package/src/components/side-chat/SideChatPanel.module.css +0 -131
  277. package/src/components/side-chat/SideChatPanel.tsx +0 -189
  278. package/src/components/side-chat/icons.tsx +0 -8
  279. package/src/components/side-chat/request-history.ts +0 -29
  280. package/src/components/side-chat/store.ts +0 -248
  281. package/src/lib/chat/stream-settings.ts +0 -192
  282. package/src/lib/side-chat/context.ts +0 -123
  283. package/src/lib/side-chat/service.ts +0 -191
  284. package/src/lib/side-chat/types.ts +0 -61
  285. /package/.next/static/{lvKCJmjFj8LuqSojRVLRF → QJCtqgDKzGS2BdKABd9ch}/_ssgManifest.js +0 -0
@@ -0,0 +1,54 @@
1
+ /**
2
+ * 正文里的文件路径识别(纯函数)。
3
+ *
4
+ * 模型回答里经常出现 `src/lib/pi.ts`、`./scripts/dev.mjs` 这类路径,把它们变成可点击的、
5
+ * 直接用 PiWeb 的文件查看器打开,省掉手动去项目栏里翻。
6
+ *
7
+ * 判定刻意保守——宁可少认,也不要把普通文字变成链接(猜错会让用户点开一个「文件不存在」):
8
+ * - 必须有路径分隔符,或看起来像「名字.扩展名」;
9
+ * - 排除 URL(http/https/协议相对)、邮箱、纯版本号(v1.2.3 / 1.2.3)、纯数字;
10
+ * - 排除以 / 或 ~ 开头的绝对路径与家目录路径:它们不属于当前工作区,点开多半是错的;
11
+ * - 排除含空格、通配符、模板占位(< > { } * ?)的片段。
12
+ *
13
+ * 只做识别,不校验存在性:校验要为每个候选发一次请求,代价远大于收益;
14
+ * 查看器本身对不存在的路径会体面地报错(见 FileViewer 的目录兜底与「文件不存在」)。
15
+ */
16
+
17
+ /** 有扩展名的相对路径:至少一段目录 + 文件名.扩展名,如 src/lib/pi.ts、scripts/dev.mjs。
18
+ * 目录段允许方括号——Next.js 的动态路由目录就是 [id] 这种写法,在 PiWeb 自己的代码里很常见。 */
19
+ const RELATIVE_WITH_DIR = /^(?:\.{1,2}\/)?(?:[\w.@[\]-]+\/)+[\w.@[\]-]+\.[A-Za-z][\w]{0,9}$/;
20
+ /** 只有文件名但带扩展名:如 agents.md、vite.config.ts、package.json —— 需要能对上常见代码/文档扩展名 */
21
+ const BARE_FILENAME = /^[\w.@-]+\.[A-Za-z][\w]{0,9}$/;
22
+ /**
23
+ * 裸文件名允许的扩展名。这里刻意收窄:像 v1.2.3、1.24 这类「数字.数字」在形状上与
24
+ * 文件名无法区分,靠扩展名列表把它们挡掉。
25
+ */
26
+ const BARE_EXTENSIONS = new Set([
27
+ "ts", "tsx", "js", "jsx", "mjs", "cjs", "mts", "cts",
28
+ "json", "jsonc", "json5", "yml", "yaml", "toml", "ini", "env",
29
+ "css", "scss", "sass", "less", "html", "htm", "vue", "svelte",
30
+ "md", "mdx", "txt", "rst", "adoc",
31
+ "py", "rb", "go", "rs", "java", "kt", "kts", "swift", "c", "h", "cc", "cpp", "hpp", "cs", "php", "lua", "sh", "bash", "zsh", "ps1", "psm1", "bat", "cmd",
32
+ "sql", "graphql", "gql", "proto", "lock", "patch", "diff", "csv", "tsv", "xml", "svg",
33
+ ]);
34
+ /** 明确不是路径的:URL、邮箱、协议相对地址 */
35
+ const NOT_A_PATH = /^(?:[a-z][a-z0-9+.-]*:\/\/|\/\/|mailto:|www\.)/i;
36
+ const EMAIL = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
37
+ /** 纯版本号:v1、1.2、v1.2.3、1.2.3-beta.1 */
38
+ const VERSION = /^v?\d+(?:\.\d+)*(?:[-+][\w.]+)?$/;
39
+
40
+ /** 单个片段是否像项目内的相对文件路径 */
41
+ export function looksLikeFilePath(raw: string): boolean {
42
+ const value = raw.trim();
43
+ if (!value || value.length > 260) return false;
44
+ if (NOT_A_PATH.test(value) || EMAIL.test(value) || VERSION.test(value)) return false;
45
+ // 绝对路径与家目录路径不属于当前工作区,点开多半是错的
46
+ if (value.startsWith("/") || value.startsWith("~") || /^[A-Za-z]:[\\/]/.test(value)) return false;
47
+ // 通配符 / 模板占位 / 反斜杠(Windows 路径在回答里少见,且与转义混淆)。
48
+ // 方括号不在此列:Next.js 的动态路由目录([id])是合法目录名,见上面对 RELATIVE_WITH_DIR 的说明。
49
+ if (/[*?<>{}|\\]/.test(value)) return false;
50
+ if (RELATIVE_WITH_DIR.test(value)) return true;
51
+ if (!BARE_FILENAME.test(value)) return false;
52
+ const ext = value.slice(value.lastIndexOf(".") + 1).toLowerCase();
53
+ return BARE_EXTENSIONS.has(ext);
54
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * 技能命令展开的识别。
3
+ *
4
+ * 用户在输入框里发 `/skill:name args` 时,pi 会在 `_expandSkillCommand` 里把 SKILL.md 正文
5
+ * 展开成提示词,格式是:
6
+ *
7
+ * <skill name="computer-use" location="/path/SKILL.md">
8
+ * References are relative to /path.
9
+ *
10
+ * ...正文...
11
+ * </skill>
12
+ * \n\n参数(有参数时才会追加)
13
+ *
14
+ * 这段展开文本会作为用户消息存进会话,所以重新打开会话时它依然在。直接渲染就是一大段
15
+ * SKILL.md 正文;这里把它认出来,只留技能名给界面显示(正文与参数都不展示)。
16
+ *
17
+ * 保守起见只在整条消息「以 <skill 开头」时才认,避免把正文里恰好引用了这个标签的普通消息误判。
18
+ */
19
+
20
+ export interface ExpandedSkill {
21
+ /** 技能名(pi 展开时写入的 name 属性) */
22
+ name: string;
23
+ /** 用户在技能名后面打的正文(展开后追加在 `</skill>` 之后;没有则为空串) */
24
+ args: string;
25
+ }
26
+
27
+ /** 整条消息就是一个技能展开时返回技能名与参数,否则返回 null */
28
+ export function parseExpandedSkill(text: string): ExpandedSkill | null {
29
+ const trimmed = text.trimStart();
30
+ if (!trimmed.startsWith("<skill ")) return null;
31
+ const open = /^<skill\s+([^>]*)>/.exec(trimmed);
32
+ if (!open) return null;
33
+ const attrs = open[1];
34
+ const name = /\bname\s*=\s*"([^"]*)"/.exec(attrs)?.[1]?.trim();
35
+ if (!name) return null;
36
+ // 必须能找到对应的闭合标签,才认定这是一次完整的展开(半截内容不认)
37
+ const close = trimmed.lastIndexOf("</skill>");
38
+ if (close < 0) return null;
39
+ // 参数在闭合标签之后,与技能正文同属一条用户消息(pi 不单独发)
40
+ return { name, args: trimmed.slice(close + "</skill>".length).trim() };
41
+ }
42
+
43
+ /**
44
+ * 拿用户消息当标题 / 摘要时用的文本:技能展开还原成用户当初打的 `/skill:name 参数`,
45
+ * 别的原样返回。会话列表、窗口标题、增长树的轮次标题都走它,不再露出 `<skill name=…>` 和 SKILL.md 正文。
46
+ */
47
+ export function promptTitleText(text: string): string {
48
+ const skill = parseExpandedSkill(text);
49
+ if (!skill) return text;
50
+ return skill.args ? `/skill:${skill.name} ${skill.args}` : `/skill:${skill.name}`;
51
+ }
@@ -31,6 +31,28 @@ export interface ExtensionUiBridge {
31
31
 
32
32
  const DEFAULT_TIMEOUT_MS = 10 * 60 * 1000;
33
33
 
34
+ const plainText = (text: unknown): string => (typeof text === "string" ? text : String(text ?? ""));
35
+ /**
36
+ * 扩展常写 `ctx.ui.theme.fg("accent", text)` 给状态文字上色;pi 的 noOp 上下文也带 theme。
37
+ * 缺了它扩展一调就抛 TypeError。Web 不渲染 ANSI,所以给一个原样返回文字、不加任何转义序列的主题。
38
+ */
39
+ const PLAIN_THEME = {
40
+ name: "piweb-plain",
41
+ style: (text: string) => plainText(text),
42
+ fg: (_color: string, text: string) => plainText(text),
43
+ bg: (_color: string, text: string) => plainText(text),
44
+ bold: plainText,
45
+ italic: plainText,
46
+ underline: plainText,
47
+ inverse: plainText,
48
+ strikethrough: plainText,
49
+ getFgAnsi: () => "",
50
+ getBgAnsi: () => "",
51
+ getColorMode: () => "truecolor",
52
+ getThinkingBorderColor: () => plainText,
53
+ getBashModeBorderColor: () => plainText,
54
+ };
55
+
34
56
  export function createExtensionUiBridge(opts: {
35
57
  publish: (evt: WebEvent) => void;
36
58
  hasViewers: () => boolean;
@@ -106,9 +128,11 @@ export function createExtensionUiBridge(opts: {
106
128
  addAutocompleteProvider: () => {},
107
129
  setEditorComponent: () => {},
108
130
  getEditorComponent: () => undefined,
131
+ theme: PLAIN_THEME,
109
132
  getAllThemes: () => [],
110
133
  getTheme: () => undefined,
111
- setTheme: () => {},
134
+ // 与 pi 的 noOp 同一返回形状:扩展会读 result.success,返回 undefined 会让它们解构时出错
135
+ setTheme: () => ({ success: false, error: "UI not available" }),
112
136
  getToolsExpanded: () => false,
113
137
  setToolsExpanded: () => {},
114
138
  };
package/src/lib/font.ts CHANGED
@@ -1,20 +1,18 @@
1
1
  /**
2
- * 字体族偏好:界面字体与代码字体各自独立选择。
2
+ * 界面字体族偏好。
3
3
  * 与字号(piweb.fontSize / piweb.chatFontSize,见 theme.ts applyFontSize)分开,
4
- * 存 localStorage:piweb.fontUi / piweb.fontMono,
5
- * 通过覆盖 CSS 变量 --font-ui / --font-mono 生效(变量默认值见 globals.css)。
4
+ * 存 localStorage:piweb.fontUi,通过覆盖 CSS 变量 --font-ui 生效(默认值见 globals.css)。
5
+ *
6
+ * 代码字体(--font-mono)不再可配:固定为系统等宽(见 globals.css 的 --font-mono)。
7
+ * 原先的 piweb.fontMono 偏好会被清掉,避免旧值残留却已无入口可改。
6
8
  *
7
9
  * 值有两种写法:
8
- * - 预设 id(如 "maple" / "system" / "jetbrains")
10
+ * - 预设 id(如 "maple" / "system" / "noto")
9
11
  * - 自定义字体栈,前缀 "custom:"(如 "custom:Fira Code, monospace")
10
12
  */
11
13
 
12
- export type FontScope = "ui" | "mono";
13
-
14
- export const DEFAULT_FONT_STACK: Record<FontScope, string> = {
15
- ui: '"Maple Mono CN", ui-monospace, SFMono-Regular, Menlo, Consolas, "PingFang SC", "Microsoft YaHei", monospace',
16
- mono: '"Maple Mono CN", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace',
17
- };
14
+ export const DEFAULT_FONT_STACK =
15
+ '"Maple Mono CN", ui-monospace, SFMono-Regular, Menlo, Consolas, "PingFang SC", "Microsoft YaHei", monospace';
18
16
 
19
17
  export interface FontPreset {
20
18
  id: string;
@@ -22,63 +20,63 @@ export interface FontPreset {
22
20
  label: { zh: string; en: string };
23
21
  }
24
22
 
25
- export const FONT_PRESETS: Record<FontScope, FontPreset[]> = {
26
- ui: [
27
- { id: "maple", stack: DEFAULT_FONT_STACK.ui, label: { zh: "Maple Mono CN(默认)", en: "Maple Mono CN (default)" } },
28
- { id: "system", stack: 'ui-sans-serif, system-ui, -apple-system, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif', label: { zh: "系统默认", en: "System default" } },
29
- { id: "noto", stack: '"Noto Sans SC", ui-sans-serif, system-ui, "PingFang SC", "Microsoft YaHei", sans-serif', label: { zh: "思源黑体", en: "Noto Sans SC" } },
30
- ],
31
- mono: [
32
- { id: "maple", stack: DEFAULT_FONT_STACK.mono, label: { zh: "Maple Mono CN(默认)", en: "Maple Mono CN (default)" } },
33
- { id: "system", stack: "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace", label: { zh: "系统等宽", en: "System monospace" } },
34
- { id: "jetbrains", stack: '"JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace', label: { zh: "JetBrains Mono", en: "JetBrains Mono" } },
35
- { id: "fira", stack: '"Fira Code", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace', label: { zh: "Fira Code", en: "Fira Code" } },
36
- ],
37
- };
23
+ export const FONT_PRESETS: FontPreset[] = [
24
+ { id: "maple", stack: DEFAULT_FONT_STACK, label: { zh: "Maple Mono CN(默认)", en: "Maple Mono CN (default)" } },
25
+ { id: "system", stack: 'ui-sans-serif, system-ui, -apple-system, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif', label: { zh: "系统默认", en: "System default" } },
26
+ { id: "noto", stack: '"Noto Sans SC", ui-sans-serif, system-ui, "PingFang SC", "Microsoft YaHei", sans-serif', label: { zh: "思源黑体", en: "Noto Sans SC" } },
27
+ ];
38
28
 
39
29
  export const CUSTOM_PREFIX = "custom:";
40
- const KEY: Record<FontScope, string> = { ui: "piweb.fontUi", mono: "piweb.fontMono" };
41
- const CSS_VAR: Record<FontScope, string> = { ui: "--font-ui", mono: "--font-mono" };
30
+ const KEY = "piweb.fontUi";
31
+ /** 已废弃的代码字体偏好键:保留常量只为在启动时清掉历史残留 */
32
+ const RETIRED_MONO_KEY = "piweb.fontMono";
33
+ const CSS_VAR = "--font-ui";
42
34
 
43
35
  /** localStorage 里的原始偏好(预设 id 或 custom: 前缀的字体栈) */
44
- export function loadFontChoice(scope: FontScope): string | null {
36
+ export function loadFontChoice(): string | null {
45
37
  if (typeof window === "undefined") return null;
46
38
  try {
47
- return localStorage.getItem(KEY[scope]);
39
+ return localStorage.getItem(KEY);
48
40
  } catch {
49
41
  return null;
50
42
  }
51
43
  }
52
44
 
53
45
  /** 把偏好解析成可直接写进 CSS 变量的字体栈;无法识别时回落默认 */
54
- export function resolveFontStack(scope: FontScope, stored: string | null): string {
55
- if (!stored) return DEFAULT_FONT_STACK[scope];
46
+ export function resolveFontStack(stored: string | null): string {
47
+ if (!stored) return DEFAULT_FONT_STACK;
56
48
  if (stored.startsWith(CUSTOM_PREFIX)) {
57
49
  const custom = stored.slice(CUSTOM_PREFIX.length).trim();
58
- return custom || DEFAULT_FONT_STACK[scope];
50
+ return custom || DEFAULT_FONT_STACK;
59
51
  }
60
- return FONT_PRESETS[scope].find((p) => p.id === stored)?.stack ?? DEFAULT_FONT_STACK[scope];
52
+ return FONT_PRESETS.find((p) => p.id === stored)?.stack ?? DEFAULT_FONT_STACK;
61
53
  }
62
54
 
63
55
  /** 应用并持久化;value 传 null 表示恢复默认(移除内联覆盖 + 清 localStorage) */
64
- export function applyFontChoice(scope: FontScope, value: string | null): void {
56
+ export function applyFontChoice(value: string | null): void {
65
57
  if (typeof window === "undefined") return;
66
58
  const root = document.documentElement;
67
59
  try {
68
60
  if (!value) {
69
- root.style.removeProperty(CSS_VAR[scope]);
70
- localStorage.removeItem(KEY[scope]);
61
+ root.style.removeProperty(CSS_VAR);
62
+ localStorage.removeItem(KEY);
71
63
  return;
72
64
  }
73
- root.style.setProperty(CSS_VAR[scope], resolveFontStack(scope, value));
74
- localStorage.setItem(KEY[scope], value);
65
+ root.style.setProperty(CSS_VAR, resolveFontStack(value));
66
+ localStorage.setItem(KEY, value);
75
67
  } catch {
76
68
  /* ignore */
77
69
  }
78
70
  }
79
71
 
80
- /** 启动 / 系统主题变化 / 跨标签同步时重放两处字体偏好 */
72
+ /** 启动 / 系统主题变化 / 跨标签同步时重放字体偏好 */
81
73
  export function applyAllFonts(): void {
82
- applyFontChoice("ui", loadFontChoice("ui"));
83
- applyFontChoice("mono", loadFontChoice("mono"));
74
+ // 代码字体设置已移除:清掉旧偏好,并移除可能残留的内联覆盖(--font-mono 现在由 CSS 决定)
75
+ try {
76
+ localStorage.removeItem(RETIRED_MONO_KEY);
77
+ document.documentElement.style.removeProperty("--font-mono");
78
+ } catch {
79
+ /* ignore */
80
+ }
81
+ applyFontChoice(loadFontChoice());
84
82
  }
@@ -9,6 +9,7 @@ import path from "node:path";
9
9
  import { promisify } from "node:util";
10
10
  import { parseNumstat, resolveGitBin } from "./growth-service";
11
11
  import { BoundaryError, isPathInside, resolveWorkspacePath } from "./path-security";
12
+ import { utf8Prefix } from "./text-sanitize";
12
13
 
13
14
  const exec = promisify(execFile);
14
15
 
@@ -213,9 +214,10 @@ async function fullDiffStats(root: string, args: string[], noIndex = false): Pro
213
214
  return { stats: { add, del }, binary };
214
215
  }
215
216
 
217
+ /** 按字节截断时退到字符边界:直接 subarray 会把多字节字符切成半个,patch 末尾多出一个 U+FFFD */
216
218
  function truncate(patch: string, stats: GitDiffResult["stats"]): GitDiffResult {
217
219
  if (Buffer.byteLength(patch) <= MAX_DIFF_BYTES) return { patch, truncated: false, binary: false, stats };
218
- return { patch: Buffer.from(patch).subarray(0, MAX_DIFF_BYTES).toString("utf8"), truncated: true, binary: false, stats };
220
+ return { patch: utf8Prefix(Buffer.from(patch), MAX_DIFF_BYTES).toString("utf8"), truncated: true, binary: false, stats };
219
221
  }
220
222
 
221
223
  function assertRepoRelative(root: string, relPath: unknown): string {
@@ -254,7 +256,7 @@ export async function gitDiff(cwdValue: unknown, relPath: unknown, mode: unknown
254
256
  await handle.close();
255
257
  }
256
258
  const head = buffer.subarray(0, bytesRead);
257
- const text = head.subarray(0, MAX_DIFF_BYTES).toString("utf8").replace(/\r\n/g, "\n");
259
+ const text = utf8Prefix(head, MAX_DIFF_BYTES).toString("utf8").replace(/\r\n/g, "\n");
258
260
  const lines = text.split("\n");
259
261
  if (lines[lines.length - 1] === "") lines.pop();
260
262
  const body = lines.map((l) => `+${l}`).join("\n");
@@ -1,3 +1,5 @@
1
+ import { promptTitleText } from "./chat/skill-expansion";
2
+
1
3
  export interface SessionTurn {
2
4
  id: string;
3
5
  ts: number;
@@ -47,7 +49,7 @@ export function promptsFromEntries(entries: readonly SessionEntry[], fromIndex =
47
49
  for (const entry of entries.slice(Math.max(0, fromIndex))) {
48
50
  if (entry.type !== "message" || entry.message?.role !== "user" || !entry.id) continue;
49
51
  ids.push(entry.id);
50
- if (!title) title = textOf(entry.message.content).split("\n").map((line) => line.trim()).find(Boolean) ?? "";
52
+ if (!title) title = promptTitleText(textOf(entry.message.content)).split("\n").map((line) => line.trim()).find(Boolean) ?? "";
51
53
  }
52
54
  return { ids, title };
53
55
  }
@@ -7,11 +7,26 @@ export interface ModelDraft {
7
7
  maxTokens?: number;
8
8
  /** 输入模态里有 image(models.json 的 `input`)。undefined = 源里没写:pi 会当成只文本、把贴图换成占位文字 */
9
9
  vision?: boolean;
10
+ /**
11
+ * 是否推理模型(models.json 的 `reasoning`)。undefined = 源里没写:
12
+ * pi 按 false 处理(`definition.reasoning ?? false`),思考档位会整个消失。
13
+ */
14
+ reasoning?: boolean;
10
15
  source?: Readonly<Record<string, unknown>>;
11
16
  /** 来自 modelOverrides:只覆盖内置模型的部分字段。缺省 = models[] 里的定义或新加的行 */
12
17
  origin?: "override";
13
18
  }
14
19
 
20
+ /** models.json 里 provider 级的自定义请求头:名字 → 值 */
21
+ export function providerHeadersFromConfig(value: unknown): Record<string, string> {
22
+ if (!value || typeof value !== "object" || Array.isArray(value)) return {};
23
+ const out: Record<string, string> = {};
24
+ for (const [name, raw] of Object.entries(value as Record<string, unknown>)) {
25
+ if (typeof raw === "string") out[name] = raw;
26
+ }
27
+ return out;
28
+ }
29
+
15
30
  /**
16
31
  * models.json 里模型的 `input` 字段(pi 用它判断能不能收图)。
17
32
  * 没写 → pi 按只文本处理,而且提示语会说是「模型不支持图像」,很容易被当成模型能力问题。
@@ -46,6 +61,8 @@ export function modelDraftFromConfig(value: unknown): ModelDraft {
46
61
  maxTokens: typeof source.maxTokens === "number" ? source.maxTokens : undefined,
47
62
  // 只有源里写了 input 才算「声明过」:没写 ≠ 只文本,而是「会被 pi 当只文本」
48
63
  vision: input ? input.includes("image") : undefined,
64
+ // 同理:没写 reasoning 不是 false,而是「没声明」——pi 会按 false 处理,体检卡片据此提醒
65
+ reasoning: typeof source.reasoning === "boolean" ? source.reasoning : undefined,
49
66
  source,
50
67
  };
51
68
  }
@@ -55,7 +72,7 @@ function positiveInt(value: unknown): number | undefined {
55
72
  }
56
73
 
57
74
  /** 「获取可用模型」返回的一条 → 表单行:端点报了容量与视觉就带上,没报的留空(不猜) */
58
- export function modelDraftFromCatalog(entry: { id: string; name?: string; contextWindow?: number; maxTokens?: number; vision?: boolean }): ModelDraft {
75
+ export function modelDraftFromCatalog(entry: { id: string; name?: string; contextWindow?: number; maxTokens?: number; vision?: boolean; reasoning?: boolean }): ModelDraft {
59
76
  const contextWindow = positiveInt(entry.contextWindow);
60
77
  const maxTokens = positiveInt(entry.maxTokens);
61
78
  return {
@@ -64,6 +81,8 @@ export function modelDraftFromCatalog(entry: { id: string; name?: string; contex
64
81
  ...(contextWindow === undefined ? {} : { contextWindow }),
65
82
  ...(maxTokens === undefined ? {} : { maxTokens }),
66
83
  ...(typeof entry.vision === "boolean" ? { vision: entry.vision } : {}),
84
+ // 端点报了推理能力就带上:不写的话 pi 按 false 处理,思考档位会整个消失
85
+ ...(typeof entry.reasoning === "boolean" ? { reasoning: entry.reasoning } : {}),
67
86
  };
68
87
  }
69
88
 
@@ -99,6 +118,24 @@ export function formatCapacity(value: number | undefined): string {
99
118
  return String(value);
100
119
  }
101
120
 
121
+ /**
122
+ * 请求头名字的合法字符集与长度上限(HTTP 令牌字符)。
123
+ * 前端与 API 路由共用同一套规则,避免一边放行、一边 400。
124
+ */
125
+ export const HEADER_NAME_RE = /^[A-Za-z0-9!#$%&'*+.^_`|~-]{1,64}$/;
126
+ /** 请求头条数上限:表单里手填的头不会很多,超过基本是异常输入 */
127
+ export const HEADER_MAX_COUNT = 32;
128
+
129
+ /** 校验一组自定义请求头;返回错误文案(中文,直接给用户看),没问题返回 null */
130
+ export function validateHeaderEntries(entries: Array<[string, string]>): string | null {
131
+ if (entries.length > HEADER_MAX_COUNT) return `请求头最多 ${HEADER_MAX_COUNT} 条`;
132
+ for (const [name, value] of entries) {
133
+ if (!HEADER_NAME_RE.test(name)) return `请求头名字不合法:${name}`;
134
+ if (value.length > 4096 || /[\r\n]/.test(value)) return `请求头内容不合法:${name}`;
135
+ }
136
+ return null;
137
+ }
138
+
102
139
  export function validateModelDrafts(models: ModelDraft[]): { index: number; reason: "id" | "capacity" } | undefined {
103
140
  const ids = new Set<string>();
104
141
  for (let index = 0; index < models.length; index += 1) {
@@ -139,6 +176,12 @@ export function serializeModelDraft(model: Omit<ModelDraft, "name"> & { name?: s
139
176
  } else if (!Object.is(vision, original?.vision)) {
140
177
  result.input = modelInputFor(vision);
141
178
  }
179
+ // reasoning 是三态:true/false 是用户或端点明确表态,undefined 表示不表态(沿用源值)。
180
+ // pi 缺省 false,所以只有显式 true 才有意义,但 false 也要能写下去(推翻源里的 true)。
181
+ const reasoning = model.reasoning;
182
+ if (reasoning !== undefined && !Object.is(reasoning, original?.reasoning)) {
183
+ result.reasoning = reasoning;
184
+ }
142
185
  return result;
143
186
  }
144
187
 
@@ -149,6 +192,8 @@ export interface ProviderDraft {
149
192
  api?: string;
150
193
  /** Blank means unchanged. Redacted secrets are restored by the server, never invented here. */
151
194
  apiKey?: string;
195
+ /** 自定义请求头(models.json 的 `headers`)。undefined = 未加载/不表态,{} = 明确清空 */
196
+ headers?: Record<string, string>;
152
197
  }
153
198
 
154
199
  export function serializeProviderDraft(
@@ -166,6 +211,19 @@ export function serializeProviderDraft(
166
211
  else delete result[field];
167
212
  }
168
213
  if (draft.apiKey?.trim()) result.apiKey = draft.apiKey.trim();
214
+ // 请求头:只在表单确实载入过(headers !== undefined)时按整体替换写回。
215
+ // 空对象 = 用户删光了,要把键去掉;否则保留原文件里没在表单里呈现的头(不认得的字段一律不动)。
216
+ if (draft.headers !== undefined) {
217
+ const cleaned: Record<string, string> = {};
218
+ for (const [name, value] of Object.entries(draft.headers)) {
219
+ if (typeof value === "string" && value) cleaned[name] = value;
220
+ }
221
+ const existing = isRecord(source.headers) ? source.headers : {};
222
+ const unknown = Object.fromEntries(Object.entries(existing).filter(([, v]) => typeof v !== "string"));
223
+ const merged = { ...cleaned, ...unknown };
224
+ if (Object.keys(merged).length) result.headers = merged;
225
+ else delete result.headers;
226
+ }
169
227
  const { definitions, overrides } = catalogIds
170
228
  ? splitBuiltinModels(draft.models, catalogIds)
171
229
  : { definitions: draft.models.map((model) => serializeModelDraft(model)), overrides: undefined };
@@ -234,7 +292,7 @@ export function isInertProviderConfig(config: Readonly<Record<string, unknown>>)
234
292
  return Object.keys(config).every((field) => INERT_PROVIDER_FIELDS.has(field));
235
293
  }
236
294
 
237
- export type CustomModelField = "contextWindow" | "maxTokens" | "input";
295
+ export type CustomModelField = "contextWindow" | "maxTokens" | "input" | "reasoning";
238
296
 
239
297
  /** 体检的一条:models[] 里有字段没写的模型(已写的值一并带上,界面要显示) */
240
298
  export interface CustomModelGap {
@@ -246,6 +304,7 @@ export interface CustomModelGap {
246
304
  contextWindow?: number;
247
305
  maxTokens?: number;
248
306
  vision?: boolean;
307
+ reasoning?: boolean;
249
308
  missing: CustomModelField[];
250
309
  }
251
310
 
@@ -266,6 +325,8 @@ export function customModelGaps(providers: Readonly<Record<string, unknown>>): C
266
325
  if (model.contextWindow === undefined) missing.push("contextWindow");
267
326
  if (model.maxTokens === undefined) missing.push("maxTokens");
268
327
  if (!Array.isArray(model.input)) missing.push("input");
328
+ // reasoning 没写时 pi 按 false 处理,推理模型的思考档位会整个消失,所以也要提醒
329
+ if (typeof model.reasoning !== "boolean") missing.push("reasoning");
269
330
  if (!missing.length) return;
270
331
  const declared = modelDraftFromConfig(model);
271
332
  gaps.push({
@@ -276,6 +337,7 @@ export function customModelGaps(providers: Readonly<Record<string, unknown>>): C
276
337
  ...(declared.contextWindow === undefined ? {} : { contextWindow: declared.contextWindow }),
277
338
  ...(declared.maxTokens === undefined ? {} : { maxTokens: declared.maxTokens }),
278
339
  ...(declared.vision === undefined ? {} : { vision: declared.vision }),
340
+ ...(declared.reasoning === undefined ? {} : { reasoning: declared.reasoning }),
279
341
  missing,
280
342
  });
281
343
  });
@@ -291,6 +353,7 @@ export interface ModelFieldFill {
291
353
  contextWindow?: number;
292
354
  maxTokens?: number;
293
355
  vision?: boolean;
356
+ reasoning?: boolean;
294
357
  }
295
358
 
296
359
  /**
@@ -316,6 +379,10 @@ export function fillModelFields(providers: Readonly<Record<string, unknown>>, fi
316
379
  model.input = modelInputFor(fill.vision);
317
380
  touched = true;
318
381
  }
382
+ if (typeof fill.reasoning === "boolean" && typeof model.reasoning !== "boolean") {
383
+ model.reasoning = fill.reasoning;
384
+ touched = true;
385
+ }
319
386
  if (touched) filled += 1;
320
387
  }
321
388
  return { providers: next, filled };
@@ -68,19 +68,52 @@ export interface DiscoveredModel {
68
68
  maxTokens?: number;
69
69
  /** 端点自报的输入模态里有 image */
70
70
  vision?: boolean;
71
+ /**
72
+ * 端点自报「这是推理模型」。对应 models.json 的 `reasoning`。
73
+ * 不写时 pi 按 false 处理(provider-composer 的 `definition.reasoning ?? false`),
74
+ * 思考档位就整个消失,所以能读到就带上。
75
+ */
76
+ reasoning?: boolean;
71
77
  }
72
78
 
73
79
  function positiveInt(value: unknown): number | undefined {
74
80
  return typeof value === "number" && Number.isSafeInteger(value) && value > 0 ? value : undefined;
75
81
  }
76
82
 
83
+ /**
84
+ * 从模型目录条目里挑出「是否推理模型」。
85
+ *
86
+ * 各家命名不同,能读到就带上、读不到留空(不猜——猜错会让非推理模型多出思考档位,
87
+ * 选中后上游直接 400)。已知的几种形状:
88
+ * - `reasoning: true` / `supports_reasoning: true`:部分网关直接给布尔;
89
+ * - `capabilities.reasoning` / `capabilities.thinking.supported`:Anthropic Messages 系的 /models;
90
+ * - `supported_parameters` 数组里出现 reasoning 相关项:OpenRouter 系。
91
+ */
92
+ function catalogReasoning(value: Record<string, unknown>): boolean | undefined {
93
+ const direct = value.reasoning ?? value.supports_reasoning;
94
+ if (typeof direct === "boolean") return direct;
95
+ const capabilities = isRecord(value.capabilities) ? value.capabilities : undefined;
96
+ const fromCapabilities = capabilities?.reasoning;
97
+ if (typeof fromCapabilities === "boolean") return fromCapabilities;
98
+ const thinking = isRecord(capabilities?.thinking) ? capabilities.thinking : undefined;
99
+ if (typeof thinking?.supported === "boolean") return thinking.supported;
100
+ if (Array.isArray(value.supported_parameters)) {
101
+ const hit = value.supported_parameters.some(
102
+ (item) => typeof item === "string" && /^(include_)?reasoning/i.test(item.trim()),
103
+ );
104
+ if (hit) return true;
105
+ }
106
+ return undefined;
107
+ }
108
+
77
109
  /**
78
110
  * 从模型目录条目里挑出能力字段。各家命名不同,能给就读、读不到就留空(不猜,缺的由体检卡片提醒用户填):
79
111
  * - 上下文:`context_window`(本机中转)/ `context_length`(OpenRouter)/ `max_input_tokens`;
80
112
  * - 输出上限:`max_output_tokens` / `max_completion_tokens` / `top_provider.max_completion_tokens`;
81
- * - 视觉:`modalities.input`(本机中转)/ `architecture.input_modalities`(OpenRouter / llama.cpp)。
113
+ * - 视觉:`modalities.input`(本机中转)/ `architecture.input_modalities`(OpenRouter / llama.cpp);
114
+ * - 推理:见 catalogReasoning。
82
115
  */
83
- export function catalogCapabilities(value: Record<string, unknown>): Pick<DiscoveredModel, "contextWindow" | "maxTokens" | "vision"> {
116
+ export function catalogCapabilities(value: Record<string, unknown>): Pick<DiscoveredModel, "contextWindow" | "maxTokens" | "vision" | "reasoning"> {
84
117
  const architecture = isRecord(value.architecture) ? value.architecture : undefined;
85
118
  const modalities = value.modalities ?? architecture?.input_modalities ?? value.input_modalities;
86
119
  const inputList = Array.isArray(modalities)
@@ -99,10 +132,12 @@ export function catalogCapabilities(value: Record<string, unknown>): Pick<Discov
99
132
  const vision = inputList
100
133
  ? inputList.some((item) => typeof item === "string" && item.toLowerCase() === "image")
101
134
  : undefined;
135
+ const reasoning = catalogReasoning(value);
102
136
  return {
103
137
  ...(contextWindow === undefined ? {} : { contextWindow }),
104
138
  ...(maxTokens === undefined ? {} : { maxTokens }),
105
139
  ...(vision === undefined ? {} : { vision }),
140
+ ...(reasoning === undefined ? {} : { reasoning }),
106
141
  };
107
142
  }
108
143
 
@@ -517,7 +552,7 @@ async function readModelCatalog(response: Response): Promise<unknown> {
517
552
  }
518
553
 
519
554
  /** Fetch a provider's conventional /models catalog without persisting credentials. */
520
- export async function discoverModels(input: { baseUrl: string; api?: string; apiKey?: string; providerId?: string; allowPrivate?: boolean }): Promise<DiscoveredModel[]> {
555
+ export async function discoverModels(input: { baseUrl: string; api?: string; apiKey?: string; providerId?: string; allowPrivate?: boolean; headers?: Record<string, string> }): Promise<DiscoveredModel[]> {
521
556
  const rawBaseUrl = input.baseUrl.trim();
522
557
  if (!rawBaseUrl) throw new Error("缺少 API 地址");
523
558
  const base = new URL(rawBaseUrl);
@@ -539,6 +574,21 @@ export async function discoverModels(input: { baseUrl: string; api?: string; api
539
574
  throw new Error("已配置的凭证只能用于该 Provider 已配置的 API 地址;新地址请显式填写密钥");
540
575
  }
541
576
  }
577
+ // 自定义请求头可能带密钥,所以与凭证同一条规矩:只发往显式填了密钥的新地址,
578
+ // 或该 provider 已配置过的地址。否则一个新主机名就能把用户存好的头骗走。
579
+ if (input.headers && Object.keys(input.headers).length) {
580
+ if (!input.apiKey?.trim()) {
581
+ const rt = await getModelRuntime();
582
+ const provider = rt.getProviders().find((p) => p.id === input.providerId);
583
+ const configuredUrls = [provider?.baseUrl, credential?.baseUrl, ...(provider ? rt.getModels(provider.id).map((model) => model.baseUrl) : [])];
584
+ if (!isTrustedCredentialEndpoint(endpoint, configuredUrls)) {
585
+ throw new Error("自定义请求头只能用于该 Provider 已配置的 API 地址;新地址请显式填写密钥");
586
+ }
587
+ }
588
+ for (const [name, value] of Object.entries(input.headers)) {
589
+ if (typeof value === "string" && value) headers[name] = value;
590
+ }
591
+ }
542
592
  const apiKey = credential?.key;
543
593
  // OpenAI Codex 的模型目录由 SDK 内置(chatgpt.com 后端无公开 /models),OAuth 令牌无法用于目录探测
544
594
  if (!input.apiKey?.trim() && credential?.oauth && input.providerId === "openai-codex") {
package/src/lib/pi.ts CHANGED
@@ -292,8 +292,8 @@ function loaderKey(cwd: string): string {
292
292
  }
293
293
 
294
294
  /** 每个热会话必须拥有自己的扩展 runtime;这里只创建,不加入发现缓存。 */
295
- export async function createSessionResourceLoader(cwd: string): Promise<DefaultResourceLoader> {
296
- const settingsManager = getSettingsManager(cwd);
295
+ export async function createSessionResourceLoader(cwd: string, settingsOverride?: SettingsManager): Promise<DefaultResourceLoader> {
296
+ const settingsManager = settingsOverride ?? getSettingsManager(cwd);
297
297
  settingsManager.setProjectTrusted(resolveProjectTrust(cwd).trusted);
298
298
  const loader = new DefaultResourceLoader({ cwd: path.resolve(cwd), agentDir: getAgentDir(), settingsManager, extensionFactories: BUILTIN_EXTENSIONS });
299
299
  const reload = loader.reload.bind(loader);