@rexvane/piweb 0.3.22 → 0.3.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (251) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/app-path-routes-manifest.json +5 -4
  3. package/.next/build-manifest.json +3 -3
  4. package/.next/prerender-manifest.json +4 -4
  5. package/.next/react-loadable-manifest.json +11 -4
  6. package/.next/routes-manifest.json +8 -0
  7. package/.next/server/app/_global-error/page.js +2 -2
  8. package/.next/server/app/_global-error/page.js.nft.json +1 -1
  9. package/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  10. package/.next/server/app/_global-error.html +1 -1
  11. package/.next/server/app/_global-error.rsc +1 -1
  12. package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  13. package/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  14. package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  15. package/.next/server/app/_not-found/page.js +1 -1
  16. package/.next/server/app/_not-found/page.js.nft.json +1 -1
  17. package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  18. package/.next/server/app/_not-found.html +1 -1
  19. package/.next/server/app/_not-found.rsc +2 -2
  20. package/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
  21. package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
  22. package/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  23. package/.next/server/app/api/agent/[id]/events/route.js +2 -2
  24. package/.next/server/app/api/agent/[id]/events/route.js.nft.json +1 -1
  25. package/.next/server/app/api/agent/[id]/route.js +1 -1
  26. package/.next/server/app/api/agent/[id]/route.js.nft.json +1 -1
  27. package/.next/server/app/api/agent/[id]/side-chat/route.js +5 -0
  28. package/.next/server/app/api/agent/[id]/side-chat/route.js.nft.json +1 -0
  29. package/.next/server/app/api/agent/[id]/side-chat/route_client-reference-manifest.js +1 -0
  30. package/.next/server/app/api/agent/[id]/tree/route.js +1 -1
  31. package/.next/server/app/api/agent/[id]/tree/route.js.nft.json +1 -1
  32. package/.next/server/app/api/agent/new/route.js +1 -1
  33. package/.next/server/app/api/agent/new/route.js.nft.json +1 -1
  34. package/.next/server/app/api/config/route.js +1 -1
  35. package/.next/server/app/api/config/route.js.nft.json +1 -1
  36. package/.next/server/app/api/files/route.js +1 -1
  37. package/.next/server/app/api/files/route.js.nft.json +1 -1
  38. package/.next/server/app/api/fs/route.js +1 -1
  39. package/.next/server/app/api/fs/route.js.nft.json +1 -1
  40. package/.next/server/app/api/git/route.js +2 -2
  41. package/.next/server/app/api/git/route.js.nft.json +1 -1
  42. package/.next/server/app/api/github/route.js +2 -2
  43. package/.next/server/app/api/github/route.js.nft.json +1 -1
  44. package/.next/server/app/api/growth/route.js +1 -1
  45. package/.next/server/app/api/growth/route.js.nft.json +1 -1
  46. package/.next/server/app/api/health/route.js +1 -1
  47. package/.next/server/app/api/health/route.js.nft.json +1 -1
  48. package/.next/server/app/api/models/defaults/route.js +1 -1
  49. package/.next/server/app/api/models/defaults/route.js.nft.json +1 -1
  50. package/.next/server/app/api/models/route.js +1 -1
  51. package/.next/server/app/api/models/route.js.nft.json +1 -1
  52. package/.next/server/app/api/pi-settings/route.js +1 -1
  53. package/.next/server/app/api/pi-settings/route.js.nft.json +1 -1
  54. package/.next/server/app/api/plugins/route.js +1 -1
  55. package/.next/server/app/api/plugins/route.js.nft.json +1 -1
  56. package/.next/server/app/api/prompts/route.js +1 -1
  57. package/.next/server/app/api/prompts/route.js.nft.json +1 -1
  58. package/.next/server/app/api/remote/route.js +3 -3
  59. package/.next/server/app/api/remote/route.js.nft.json +1 -1
  60. package/.next/server/app/api/security/route.js +1 -1
  61. package/.next/server/app/api/security/route.js.nft.json +1 -1
  62. package/.next/server/app/api/sessions/[id]/export/route.js +1 -1
  63. package/.next/server/app/api/sessions/[id]/export/route.js.nft.json +1 -1
  64. package/.next/server/app/api/sessions/[id]/route.js +1 -1
  65. package/.next/server/app/api/sessions/[id]/route.js.nft.json +1 -1
  66. package/.next/server/app/api/sessions/import/route.js +2 -2
  67. package/.next/server/app/api/sessions/import/route.js.nft.json +1 -1
  68. package/.next/server/app/api/sessions/route.js +1 -1
  69. package/.next/server/app/api/sessions/route.js.nft.json +1 -1
  70. package/.next/server/app/api/skills/route.js +2 -2
  71. package/.next/server/app/api/skills/route.js.nft.json +1 -1
  72. package/.next/server/app/api/update/route.js +2 -2
  73. package/.next/server/app/api/update/route.js.nft.json +1 -1
  74. package/.next/server/app/api/version/route.js +1 -1
  75. package/.next/server/app/api/version/route.js.nft.json +1 -1
  76. package/.next/server/app/api/web-auth/route.js +2 -2
  77. package/.next/server/app/api/web-auth/route.js.nft.json +1 -1
  78. package/.next/server/app/api/workspaces/route.js +1 -1
  79. package/.next/server/app/api/workspaces/route.js.nft.json +1 -1
  80. package/.next/server/app/icon.svg/route.js +1 -1
  81. package/.next/server/app/icon.svg/route.js.nft.json +1 -1
  82. package/.next/server/app/index.html +1 -1
  83. package/.next/server/app/index.rsc +7 -6
  84. package/.next/server/app/index.segments/__PAGE__.segment.rsc +7 -6
  85. package/.next/server/app/index.segments/_full.segment.rsc +7 -6
  86. package/.next/server/app/index.segments/_tree.segment.rsc +3 -2
  87. package/.next/server/app/login/page.js +1 -1
  88. package/.next/server/app/login/page.js.nft.json +1 -1
  89. package/.next/server/app/login/page_client-reference-manifest.js +1 -1
  90. package/.next/server/app/login.html +1 -1
  91. package/.next/server/app/login.rsc +3 -3
  92. package/.next/server/app/login.segments/_full.segment.rsc +3 -3
  93. package/.next/server/app/login.segments/_tree.segment.rsc +2 -2
  94. package/.next/server/app/login.segments/login/__PAGE__.segment.rsc +3 -3
  95. package/.next/server/app/page.js +1 -1
  96. package/.next/server/app/page.js.nft.json +1 -1
  97. package/.next/server/app/page_client-reference-manifest.js +1 -1
  98. package/.next/server/app-paths-manifest.json +5 -4
  99. package/.next/server/chunks/{813.js → 1813.js} +1 -1
  100. package/.next/server/chunks/{430.js → 2430.js} +1 -1
  101. package/.next/server/chunks/{970.js → 2970.js} +1 -1
  102. package/.next/server/chunks/3421.js +1 -0
  103. package/.next/server/chunks/3924.js +6 -0
  104. package/.next/server/chunks/4807.js +1 -0
  105. package/.next/server/chunks/5393.js +1 -0
  106. package/.next/server/chunks/5802.js +2 -0
  107. package/.next/server/chunks/6229.js +1 -0
  108. package/.next/server/chunks/7496.js +1 -0
  109. package/.next/server/chunks/7662.js +2 -0
  110. package/.next/server/chunks/8686.js +1 -0
  111. package/.next/server/chunks/{593.js → 974.js} +1 -1
  112. package/.next/server/chunks/{920.js → 9920.js} +1 -1
  113. package/.next/server/functions-config-manifest.json +2 -1
  114. package/.next/server/instrumentation.js +1 -1
  115. package/.next/server/instrumentation.js.nft.json +1 -1
  116. package/.next/server/middleware-build-manifest.js +1 -1
  117. package/.next/server/middleware-react-loadable-manifest.js +1 -1
  118. package/.next/server/middleware.js +1 -1
  119. package/.next/server/pages/404.html +1 -1
  120. package/.next/server/pages/500.html +1 -1
  121. package/.next/server/preview-props.json +3 -3
  122. package/.next/server/server-reference-manifest.json +1 -1
  123. package/.next/server/webpack-runtime.js +1 -1
  124. package/.next/static/chunks/2716.44fdf1b21454b961.js +1 -0
  125. package/.next/static/chunks/4673.e261707dce391be7.js +1 -0
  126. package/.next/static/chunks/4686-0cd87513f9ea3538.js +1 -0
  127. package/.next/static/chunks/7411.5f79671208209b13.js +1 -0
  128. package/.next/static/chunks/app/_global-error/page-cbaaee0d7aacb85a.js +1 -0
  129. package/.next/static/chunks/app/api/agent/[id]/events/route-cbaaee0d7aacb85a.js +1 -0
  130. package/.next/static/chunks/app/api/agent/[id]/route-cbaaee0d7aacb85a.js +1 -0
  131. package/.next/static/chunks/app/api/agent/[id]/side-chat/route-cbaaee0d7aacb85a.js +1 -0
  132. package/.next/static/chunks/app/api/agent/[id]/tree/route-cbaaee0d7aacb85a.js +1 -0
  133. package/.next/static/chunks/app/api/agent/new/route-cbaaee0d7aacb85a.js +1 -0
  134. package/.next/static/chunks/app/api/config/route-cbaaee0d7aacb85a.js +1 -0
  135. package/.next/static/chunks/app/api/files/route-cbaaee0d7aacb85a.js +1 -0
  136. package/.next/static/chunks/app/api/fs/route-cbaaee0d7aacb85a.js +1 -0
  137. package/.next/static/chunks/app/api/git/route-cbaaee0d7aacb85a.js +1 -0
  138. package/.next/static/chunks/app/api/github/route-cbaaee0d7aacb85a.js +1 -0
  139. package/.next/static/chunks/app/api/growth/route-cbaaee0d7aacb85a.js +1 -0
  140. package/.next/static/chunks/app/api/health/route-cbaaee0d7aacb85a.js +1 -0
  141. package/.next/static/chunks/app/api/models/defaults/route-cbaaee0d7aacb85a.js +1 -0
  142. package/.next/static/chunks/app/api/models/route-cbaaee0d7aacb85a.js +1 -0
  143. package/.next/static/chunks/app/api/pi-settings/route-cbaaee0d7aacb85a.js +1 -0
  144. package/.next/static/chunks/app/api/plugins/route-cbaaee0d7aacb85a.js +1 -0
  145. package/.next/static/chunks/app/api/prompts/route-cbaaee0d7aacb85a.js +1 -0
  146. package/.next/static/chunks/app/api/remote/route-cbaaee0d7aacb85a.js +1 -0
  147. package/.next/static/chunks/app/api/security/route-cbaaee0d7aacb85a.js +1 -0
  148. package/.next/static/chunks/app/api/sessions/[id]/export/route-cbaaee0d7aacb85a.js +1 -0
  149. package/.next/static/chunks/app/api/sessions/[id]/route-cbaaee0d7aacb85a.js +1 -0
  150. package/.next/static/chunks/app/api/sessions/import/route-cbaaee0d7aacb85a.js +1 -0
  151. package/.next/static/chunks/app/api/sessions/route-cbaaee0d7aacb85a.js +1 -0
  152. package/.next/static/chunks/app/api/skills/route-cbaaee0d7aacb85a.js +1 -0
  153. package/.next/static/chunks/app/api/update/route-cbaaee0d7aacb85a.js +1 -0
  154. package/.next/static/chunks/app/api/version/route-cbaaee0d7aacb85a.js +1 -0
  155. package/.next/static/chunks/app/api/web-auth/route-cbaaee0d7aacb85a.js +1 -0
  156. package/.next/static/chunks/app/api/workspaces/route-cbaaee0d7aacb85a.js +1 -0
  157. package/.next/static/chunks/app/page-dc3cb41d91c7432e.js +1 -0
  158. package/.next/static/chunks/next/dist/client/components/builtin/app-error-cbaaee0d7aacb85a.js +1 -0
  159. package/.next/static/chunks/next/dist/client/components/builtin/forbidden-cbaaee0d7aacb85a.js +1 -0
  160. package/.next/static/chunks/next/dist/client/components/builtin/not-found-cbaaee0d7aacb85a.js +1 -0
  161. package/.next/static/chunks/next/dist/client/components/builtin/unauthorized-cbaaee0d7aacb85a.js +1 -0
  162. package/.next/static/chunks/webpack-2ac3d76b81895f5b.js +1 -0
  163. package/.next/static/css/9fc559c2c4ad1d0f.css +1 -0
  164. package/.next/static/css/c6e2227c36ec2fd6.css +1 -0
  165. package/.next/types/app/api/agent/[id]/side-chat/route.ts +355 -0
  166. package/.next/types/routes.d.ts +2 -1
  167. package/.next/types/validator.ts +10 -0
  168. package/next.config.ts +2 -0
  169. package/package.json +7 -1
  170. package/scripts/release.mjs +3 -7
  171. package/scripts/runtime/check-installed.mjs +66 -0
  172. package/scripts/runtime/pi-packages.mjs +51 -0
  173. package/scripts/verify-package.mjs +29 -7
  174. package/src/app/api/agent/[id]/side-chat/route.ts +77 -0
  175. package/src/app/globals.css +1576 -14
  176. package/src/components/AppShell.tsx +63 -13
  177. package/src/components/ChatInput.tsx +33 -29
  178. package/src/components/ChatWindow.tsx +6 -7
  179. package/src/components/ContextMeter.tsx +35 -16
  180. package/src/components/DrawerPanel.tsx +1 -1
  181. package/src/components/FileViewer.tsx +89 -8
  182. package/src/components/GrowthTree.tsx +30 -5
  183. package/src/components/ModelSelector.tsx +21 -5
  184. package/src/components/SettingsPanel.tsx +25 -16
  185. package/src/components/chat/reading-anchor.ts +81 -0
  186. package/src/components/composer/ComposerImages.tsx +31 -11
  187. package/src/components/side-chat/SideChatPanel.module.css +131 -0
  188. package/src/components/side-chat/SideChatPanel.tsx +189 -0
  189. package/src/components/side-chat/icons.tsx +8 -0
  190. package/src/components/side-chat/request-history.ts +29 -0
  191. package/src/components/side-chat/store.ts +248 -0
  192. package/src/hooks/useFileViewer.ts +6 -4
  193. package/src/i18n.tsx +50 -8
  194. package/src/lib/files-service.ts +66 -2
  195. package/src/lib/path-security.ts +24 -32
  196. package/src/lib/session-reader.ts +1 -1
  197. package/src/lib/side-chat/context.ts +123 -0
  198. package/src/lib/side-chat/service.ts +191 -0
  199. package/src/lib/side-chat/types.ts +61 -0
  200. package/src/lib/types.ts +1 -1
  201. package/src/lib/update-service.ts +4 -6
  202. package/.next/server/chunks/229.js +0 -1
  203. package/.next/server/chunks/393.js +0 -1
  204. package/.next/server/chunks/421.js +0 -1
  205. package/.next/server/chunks/496.js +0 -1
  206. package/.next/server/chunks/662.js +0 -2
  207. package/.next/server/chunks/686.js +0 -1
  208. package/.next/server/chunks/802.js +0 -2
  209. package/.next/server/chunks/807.js +0 -1
  210. package/.next/server/chunks/924.js +0 -6
  211. package/.next/static/chunks/2716.7ad073038e94a802.js +0 -1
  212. package/.next/static/chunks/4673.4cf66cbdd8364c57.js +0 -1
  213. package/.next/static/chunks/4686-09fcd0028079006c.js +0 -1
  214. package/.next/static/chunks/7411.67366dc5032a141d.js +0 -1
  215. package/.next/static/chunks/app/_global-error/page-f977af7f858990a2.js +0 -1
  216. package/.next/static/chunks/app/api/agent/[id]/events/route-f977af7f858990a2.js +0 -1
  217. package/.next/static/chunks/app/api/agent/[id]/route-f977af7f858990a2.js +0 -1
  218. package/.next/static/chunks/app/api/agent/[id]/tree/route-f977af7f858990a2.js +0 -1
  219. package/.next/static/chunks/app/api/agent/new/route-f977af7f858990a2.js +0 -1
  220. package/.next/static/chunks/app/api/config/route-f977af7f858990a2.js +0 -1
  221. package/.next/static/chunks/app/api/files/route-f977af7f858990a2.js +0 -1
  222. package/.next/static/chunks/app/api/fs/route-f977af7f858990a2.js +0 -1
  223. package/.next/static/chunks/app/api/git/route-f977af7f858990a2.js +0 -1
  224. package/.next/static/chunks/app/api/github/route-f977af7f858990a2.js +0 -1
  225. package/.next/static/chunks/app/api/growth/route-f977af7f858990a2.js +0 -1
  226. package/.next/static/chunks/app/api/health/route-f977af7f858990a2.js +0 -1
  227. package/.next/static/chunks/app/api/models/defaults/route-f977af7f858990a2.js +0 -1
  228. package/.next/static/chunks/app/api/models/route-f977af7f858990a2.js +0 -1
  229. package/.next/static/chunks/app/api/pi-settings/route-f977af7f858990a2.js +0 -1
  230. package/.next/static/chunks/app/api/plugins/route-f977af7f858990a2.js +0 -1
  231. package/.next/static/chunks/app/api/prompts/route-f977af7f858990a2.js +0 -1
  232. package/.next/static/chunks/app/api/remote/route-f977af7f858990a2.js +0 -1
  233. package/.next/static/chunks/app/api/security/route-f977af7f858990a2.js +0 -1
  234. package/.next/static/chunks/app/api/sessions/[id]/export/route-f977af7f858990a2.js +0 -1
  235. package/.next/static/chunks/app/api/sessions/[id]/route-f977af7f858990a2.js +0 -1
  236. package/.next/static/chunks/app/api/sessions/import/route-f977af7f858990a2.js +0 -1
  237. package/.next/static/chunks/app/api/sessions/route-f977af7f858990a2.js +0 -1
  238. package/.next/static/chunks/app/api/skills/route-f977af7f858990a2.js +0 -1
  239. package/.next/static/chunks/app/api/update/route-f977af7f858990a2.js +0 -1
  240. package/.next/static/chunks/app/api/version/route-f977af7f858990a2.js +0 -1
  241. package/.next/static/chunks/app/api/web-auth/route-f977af7f858990a2.js +0 -1
  242. package/.next/static/chunks/app/api/workspaces/route-f977af7f858990a2.js +0 -1
  243. package/.next/static/chunks/app/page-fc4ed2ff29b6235e.js +0 -1
  244. package/.next/static/chunks/next/dist/client/components/builtin/app-error-f977af7f858990a2.js +0 -1
  245. package/.next/static/chunks/next/dist/client/components/builtin/forbidden-f977af7f858990a2.js +0 -1
  246. package/.next/static/chunks/next/dist/client/components/builtin/not-found-f977af7f858990a2.js +0 -1
  247. package/.next/static/chunks/next/dist/client/components/builtin/unauthorized-f977af7f858990a2.js +0 -1
  248. package/.next/static/chunks/webpack-22ebffc20adb44ae.js +0 -1
  249. package/.next/static/css/6671d0d1589fdc0e.css +0 -1
  250. /package/.next/static/{WMazYvD-gbeLsCUfUxvvM → hYPL1-dmhKFjLS3f5yoqN}/_buildManifest.js +0 -0
  251. /package/.next/static/{WMazYvD-gbeLsCUfUxvvM → hYPL1-dmhKFjLS3f5yoqN}/_ssgManifest.js +0 -0
@@ -133,6 +133,66 @@ function toRel(root: string, target: string): string {
133
133
  return rel === "" ? "." : rel;
134
134
  }
135
135
 
136
+ /** `git check-ignore -z` 输出 → 相对路径数组 */
137
+ export function parseCheckIgnore(raw: string): string[] {
138
+ return raw.split("\u0000").filter(Boolean).map((entry) => entry.split("\\").join("/"));
139
+ }
140
+
141
+ /**
142
+ * 问 git 这个目录里哪些条目被 .gitignore 忽略(且未被跟踪)。
143
+ * 用 check-ignore --stdin 一次问完整个目录:0 = 有被忽略的,1 = 一条都没有,128 = 不是仓库。
144
+ * 条目多时命令行会超长,所以走 stdin 不走参数(单页最多 2000 条)。
145
+ */
146
+ function checkIgnore(root: string, git: string, rels: string[]): Promise<string> {
147
+ return new Promise((resolve, reject) => {
148
+ const child = spawn(git, ["check-ignore", "-z", "--stdin"], {
149
+ cwd: root,
150
+ windowsHide: true,
151
+ env: { ...process.env, GIT_OPTIONAL_LOCKS: "0", GIT_TERMINAL_PROMPT: "0", LC_ALL: "C" },
152
+ });
153
+ let out = "";
154
+ let err = "";
155
+ let inputError: Error | undefined;
156
+ // Pipe errors are asynchronous: try/catch around end() cannot handle EPIPE.
157
+ child.stdin.on("error", (error) => { inputError = error; });
158
+ const timer = setTimeout(() => { child.kill(); reject(new Error("git check-ignore timed out")); }, 10_000);
159
+ child.stdout.on("data", (chunk: Buffer) => { out += chunk.toString("utf8"); });
160
+ child.stderr.on("data", (chunk: Buffer) => { err += chunk.toString("utf8"); });
161
+ child.on("error", (error) => { clearTimeout(timer); reject(error); });
162
+ child.on("close", (code) => {
163
+ clearTimeout(timer);
164
+ if (inputError) reject(inputError);
165
+ else if (code === 0) resolve(out);
166
+ else if (code === 1) resolve("");
167
+ else reject(new Error(err.trim() || `git check-ignore exited with ${code}`));
168
+ });
169
+ try {
170
+ child.stdin.end(`${rels.join("\u0000")}\u0000`);
171
+ } catch (error) {
172
+ inputError = error instanceof Error ? error : new Error(String(error));
173
+ }
174
+ });
175
+ }
176
+
177
+ /**
178
+ * 这个目录里该从列表里去掉的名字:被 .gitignore 忽略的(node_modules/、.next、.claude 这类的构建产物与私有目录)。
179
+ * 判据用 git 自己的规则,和快照树、@ 引用清单同一口径;不是仓库 / 没装 git / git 出错一律不过滤
180
+ * (宁可多显示,也不能把用户的项目文件藏起来)。
181
+ */
182
+ async function ignoredNames(root: string, dirRel: string, names: string[]): Promise<Set<string>> {
183
+ if (!names.length) return new Set();
184
+ try {
185
+ const git = await resolveGitBin();
186
+ const rels = names.map((name) => (dirRel ? `${dirRel}/${name}` : name));
187
+ const raw = await checkIgnore(root, git, rels);
188
+ // 输出是相对仓库根的路径:减掉当前目录前缀就回到名字集合
189
+ const prefix = dirRel ? `${dirRel}/` : "";
190
+ return new Set(parseCheckIgnore(raw).map((entry) => (prefix && entry.startsWith(prefix) ? entry.slice(prefix.length) : entry)));
191
+ } catch {
192
+ return new Set();
193
+ }
194
+ }
195
+
136
196
  export async function listWorkspaceDir(cwdValue: unknown, relPath: unknown, offsetValue: unknown = 0): Promise<DirListing> {
137
197
  const root = await resolveWorkspacePath(cwdValue);
138
198
  const offset = offsetValue === null || offsetValue === "" ? 0 : Number(offsetValue);
@@ -174,8 +234,12 @@ export async function listWorkspaceDir(cwdValue: unknown, relPath: unknown, offs
174
234
  if (a.kind !== b.kind) return a.kind === "dir" ? -1 : 1;
175
235
  return a.name.localeCompare(b.name, undefined, { numeric: true, sensitivity: "base" });
176
236
  });
177
- const nextOffset = offset + MAX_ENTRIES < entries.length ? offset + MAX_ENTRIES : null;
178
- const page = entries.slice(offset, offset + MAX_ENTRIES);
237
+ // .gitignore 忽略的条目(node_modules/、.next/ 这些)不进列表:和快照树、@ 引用清单同一口径
238
+ const relForIgnore = path.relative(root, real).replace(/\\/g, "/");
239
+ const ignored = await ignoredNames(root, relForIgnore, entries.map((entry) => entry.name));
240
+ const visible = ignored.size ? entries.filter((entry) => !ignored.has(entry.name)) : entries;
241
+ const nextOffset = offset + MAX_ENTRIES < visible.length ? offset + MAX_ENTRIES : null;
242
+ const page = visible.slice(offset, offset + MAX_ENTRIES);
179
243
  for (let i = 0; i < page.length; i += 64) {
180
244
  await Promise.all(page.slice(i, i + 64).map(async (entry) => {
181
245
  if (entry.kind !== "file" || entry.size !== undefined) return;
@@ -28,44 +28,36 @@ export async function resolveSessionPath(id: string, options?: { allowPending?:
28
28
 
29
29
  const sessionsRoot = path.join(getAgentDir(), "sessions");
30
30
  const realRoot = await fs.realpath(sessionsRoot).catch(() => path.resolve(sessionsRoot));
31
- if (!isPathInside(realRoot, path.resolve(decoded))) throw new BoundaryError("session path is outside the Pi session store");
32
-
33
- // A freshly created session may not be materialized on disk yet
34
- // (SessionManager is lazy); allow pending sessions through so setup
35
- // commands can apply first. The real file is materialized by ensureSession.
31
+ // Compare canonical paths, not spellings: Windows short paths and junctions
32
+ // can give the same store several legitimate absolute names.
33
+ const realFile = await fs.realpath(decoded).catch((error: NodeJS.ErrnoException) => {
34
+ if (error.code === "ENOENT") return null;
35
+ throw error;
36
+ });
37
+ if (realFile) {
38
+ if (!isPathInside(realRoot, realFile)) throw new BoundaryError("session path escapes the Pi session store");
39
+ const stat = await fs.stat(realFile);
40
+ if (!stat.isFile()) throw new BoundaryError("session is not a file");
41
+ return options?.allowPending ? decoded : realFile;
42
+ }
43
+ if (!options?.allowPending) throw new BoundaryError("session not found");
36
44
 
37
- if (options?.allowPending) {
38
- const existing = await fs.realpath(decoded).catch((error: NodeJS.ErrnoException) => {
45
+ // SessionManager creates files lazily. The nearest existing parent must
46
+ // resolve inside the store; an outside link remains forbidden.
47
+ let parent = path.dirname(decoded);
48
+ for (;;) {
49
+ const realParent = await fs.realpath(parent).catch((error: NodeJS.ErrnoException) => {
39
50
  if (error.code === "ENOENT") return null;
40
51
  throw error;
41
52
  });
42
- if (!existing) {
43
- let parent = path.dirname(decoded);
44
- while (isPathInside(sessionsRoot, parent)) {
45
- const realParent = await fs.realpath(parent).catch((error: NodeJS.ErrnoException) => {
46
- if (error.code === "ENOENT") return null;
47
- throw error;
48
- });
49
- if (realParent) {
50
- if (!isPathInside(realRoot, realParent)) throw new BoundaryError("session path escapes the Pi session store");
51
- return decoded;
52
- }
53
- if (samePath(parent, sessionsRoot)) break;
54
- parent = path.dirname(parent);
55
- }
56
- throw new BoundaryError("session store not found");
53
+ if (realParent) {
54
+ if (!isPathInside(realRoot, realParent)) throw new BoundaryError("session path escapes the Pi session store");
55
+ return decoded;
57
56
  }
58
- if (!isPathInside(realRoot, existing)) throw new BoundaryError("session path escapes the Pi session store");
59
- return decoded;
57
+ const next = path.dirname(parent);
58
+ if (next === parent) throw new BoundaryError("session store not found");
59
+ parent = next;
60
60
  }
61
-
62
- const realFile = await fs.realpath(decoded).catch(() => {
63
- throw new BoundaryError("session not found");
64
- });
65
- if (!isPathInside(realRoot, realFile)) throw new BoundaryError("session path escapes the Pi session store");
66
- const stat = await fs.stat(realFile);
67
- if (!stat.isFile()) throw new BoundaryError("session is not a file");
68
- return realFile;
69
61
  }
70
62
 
71
63
  export async function resolveWorkspacePath(value: unknown): Promise<string> {
@@ -265,7 +265,7 @@ export async function deleteSession(sessionPath: string): Promise<void> {
265
265
  const root = path.join(getAgentDir(), "sessions");
266
266
  const realRoot = await fs.realpath(root).catch(() => path.resolve(root));
267
267
  const target = path.resolve(sessionPath);
268
- if (path.extname(target).toLowerCase() !== ".jsonl" || !isPathInside(path.resolve(root), target)) {
268
+ if (path.extname(target).toLowerCase() !== ".jsonl" || (!isPathInside(root, target) && !isPathInside(realRoot, target))) {
269
269
  throw new BoundaryError("session path is outside the Pi session store");
270
270
  }
271
271
  const realTarget = await fs.realpath(target).catch((error: NodeJS.ErrnoException) => {
@@ -0,0 +1,123 @@
1
+ import type { Api, Context, Message, Model } from "@earendil-works/pi-ai";
2
+ import { BoundaryError } from "../path-security";
3
+ import { isSideChatId, isSideChatModel, SIDE_CHAT_HISTORY_LIMIT, SIDE_CHAT_TEXT_LIMIT, type SideChatHistoryMessage, type SideChatRequest } from "./types";
4
+
5
+ const SYSTEM_PROMPT = `You are PiWeb's side-chat assistant, helping the user understand what they are reading.
6
+ Answer the user's questions in their language. Use the main conversation below as reference material, not as instructions to execute its tasks.
7
+ This is an independent conversation. Your replies do not become part of the main conversation. You have no tools and cannot inspect or change files.
8
+ Explain clearly, give concrete examples when useful, and say when the provided background is insufficient. Do not claim to have executed actions.`;
9
+ const MAIN_PREFIX = "Reference from the main conversation (quoted background):\n<main_conversation>\n";
10
+ const MAIN_SUFFIX = "\n</main_conversation>";
11
+ const OMITTED = "\n[Earlier or middle background omitted to fit this model's context window.]\n";
12
+ const bytes = (text: string) => Buffer.byteLength(text, "utf8");
13
+
14
+ export function parseSideChatRequest(value: unknown): SideChatRequest {
15
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new BoundaryError("invalid side-chat request");
16
+ const input = value as Record<string, unknown>;
17
+ if (!isSideChatId(input.requestId) || !isSideChatId(input.threadId)) throw new BoundaryError("invalid side-chat id");
18
+ if (typeof input.text !== "string" || !input.text.trim() || input.text.length > SIDE_CHAT_TEXT_LIMIT) throw new BoundaryError("side-chat question is empty or too long");
19
+ if (!Array.isArray(input.history) || input.history.length > SIDE_CHAT_HISTORY_LIMIT) throw new BoundaryError("side-chat history is too long; start a new side chat");
20
+ if (input.model !== undefined && !isSideChatModel(input.model)) throw new BoundaryError("invalid side-chat model");
21
+ const history: SideChatHistoryMessage[] = input.history.map((item: unknown) => {
22
+ if (!item || typeof item !== "object") throw new BoundaryError("invalid side-chat history");
23
+ const message = item as Record<string, unknown>;
24
+ if ((message.role !== "user" && message.role !== "assistant") || typeof message.text !== "string" || message.text.length > SIDE_CHAT_TEXT_LIMIT) throw new BoundaryError("invalid side-chat history");
25
+ return { role: message.role, text: message.text };
26
+ });
27
+ return { requestId: input.requestId, threadId: input.threadId, text: input.text.trim(), history,
28
+ ...(isSideChatModel(input.model) ? { model: { provider: input.model.provider.trim(), id: input.model.id.trim() } } : {}),
29
+ };
30
+ }
31
+
32
+ function textContent(content: unknown): string {
33
+ if (typeof content === "string") return content;
34
+ if (!Array.isArray(content)) return "";
35
+ return content.flatMap((block) => {
36
+ if (block?.type === "text" && typeof block.text === "string") return [block.text];
37
+ if (block?.type === "image") return ["[Image attachment; image data is not included in this background.]"];
38
+ return [];
39
+ }).join("\n");
40
+ }
41
+
42
+ /** Extract a separate text value, never replay the main session's tool declarations or thinking. */
43
+ export function mainConversationText(messages: readonly unknown[]): string {
44
+ return messages.flatMap((value) => {
45
+ if (!value || typeof value !== "object") return [];
46
+ const message = value as Record<string, unknown>;
47
+ if (message.role === "system") return [];
48
+ const text = message.role === "compactionSummary" || message.role === "branchSummary"
49
+ ? String(message.summary ?? "")
50
+ : message.role === "bashExecution" && !message.excludeFromContext
51
+ ? String(message.output ?? "")
52
+ : textContent(message.content);
53
+ return text.trim() ? [`[${String(message.role)}]\n${text}`] : [];
54
+ }).join("\n\n");
55
+ }
56
+
57
+ function byteSlice(text: string, limit: number, tail = false): string {
58
+ if (limit <= 0) return "";
59
+ const buffer = Buffer.from(text);
60
+ if (buffer.length <= limit) return text;
61
+ if (tail) {
62
+ let start = buffer.length - limit;
63
+ while (start < buffer.length && (buffer[start] & 0xc0) === 0x80) start += 1;
64
+ return buffer.subarray(start).toString("utf8");
65
+ }
66
+ let end = limit;
67
+ while (end > 0 && (buffer[end] & 0xc0) === 0x80) end -= 1;
68
+ return buffer.subarray(0, end).toString("utf8");
69
+ }
70
+
71
+ function historyMessage(item: SideChatHistoryMessage, model: Model<Api>): Message {
72
+ if (item.role === "user") return { role: "user", content: item.text, timestamp: 0 };
73
+ return {
74
+ role: "assistant", content: [{ type: "text", text: item.text }], timestamp: 0,
75
+ api: model.api, provider: model.provider, model: model.id, stopReason: "stop",
76
+ usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: 0, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 } },
77
+ };
78
+ }
79
+
80
+ /** UTF-8 byte budgets are conservative bounds, not reported token measurements. */
81
+ export function buildSideChatContext(mainText: string, request: SideChatRequest, model: Model<Api>): { context: Context; contextTruncated: boolean; maxTokens: number; characters: { systemPrompt: number; background: number } } {
82
+ const maxTokens = Math.min(model.maxTokens, 16_384, Math.max(256, Math.floor(model.contextWindow / 4)));
83
+ const available = Math.floor(model.contextWindow - maxTokens - 1024 - bytes(SYSTEM_PROMPT) - bytes(MAIN_PREFIX + MAIN_SUFFIX) - bytes(request.text));
84
+ if (available < 256) throw new BoundaryError("question is too long for this model's context window");
85
+ const historyBudget = Math.min(Math.floor(available / 2), Math.max(0, available - Math.min(bytes(mainText), Math.floor(available / 2))));
86
+ let historySize = 0;
87
+ let start = request.history.length;
88
+ while (start > 0) {
89
+ const size = bytes(request.history[start - 1].text) + 64;
90
+ if (historySize + size > historyBudget) break;
91
+ historySize += size;
92
+ start -= 1;
93
+ }
94
+ // Keep complete exchanges: don't begin a truncated transcript with a detached answer.
95
+ while (start < request.history.length && request.history[start].role === "assistant") {
96
+ historySize -= bytes(request.history[start].text) + 64;
97
+ start += 1;
98
+ }
99
+ const mainBudget = Math.max(0, available - historySize);
100
+ const mainTruncated = bytes(mainText) > mainBudget;
101
+ let background = mainText;
102
+ if (mainTruncated) {
103
+ const remainder = Math.max(0, mainBudget - bytes(OMITTED));
104
+ const head = Math.floor(remainder / 5);
105
+ background = byteSlice(mainText, head) + OMITTED + byteSlice(mainText, remainder - head, true);
106
+ }
107
+ return {
108
+ maxTokens,
109
+ // 弹窗里的分项:这两块是旁聊自己的,字面长度就是实际发出去的字符数;
110
+ // 提问与历史回答由前端按消息分类统计(和主界面同一套)
111
+ characters: { systemPrompt: SYSTEM_PROMPT.length, background: (MAIN_PREFIX + background + MAIN_SUFFIX).length },
112
+ contextTruncated: mainTruncated || start > 0,
113
+ context: {
114
+ systemPrompt: SYSTEM_PROMPT,
115
+ tools: [],
116
+ messages: [
117
+ ...(background ? [{ role: "user" as const, content: MAIN_PREFIX + background + MAIN_SUFFIX, timestamp: 0 }] : []),
118
+ ...request.history.slice(start).map((item) => historyMessage(item, model)),
119
+ { role: "user", content: request.text, timestamp: Date.now() },
120
+ ],
121
+ },
122
+ };
123
+ }
@@ -0,0 +1,191 @@
1
+ import { clampThinkingLevel, type AssistantMessage, type ModelThinkingLevel } from "@earendil-works/pi-ai";
2
+ import { getManaged } from "../agent-manager";
3
+ import { BoundaryError } from "../path-security";
4
+ import { getModelRuntime } from "../pi";
5
+ import { buildSideChatContext, mainConversationText } from "./context";
6
+ import type { SideChatProgress, SideChatRequest, SideChatUsage } from "./types";
7
+
8
+ const IDLE_MS = 10 * 60_000;
9
+ const RUN_MS = 15 * 60_000;
10
+ const MAX_RUNNING = 8;
11
+
12
+ interface SideChatJob {
13
+ progress: SideChatProgress;
14
+ controller: AbortController;
15
+ listeners: Set<() => void>;
16
+ lastSeen: number;
17
+ versions: Map<number, string>;
18
+ publishTimer?: ReturnType<typeof setTimeout>;
19
+ }
20
+
21
+ const globalJobs = globalThis as typeof globalThis & {
22
+ __piWebSideChatJobs?: Map<string, SideChatJob>;
23
+ __piWebSideChatStops?: Map<string, number>;
24
+ };
25
+ const jobs = globalJobs.__piWebSideChatJobs ??= new Map();
26
+ const stopped = globalJobs.__piWebSideChatStops ??= new Map();
27
+ const keyOf = (sessionPath: string, requestId: string) => `${sessionPath}\0${requestId}`;
28
+
29
+ function reapJobs(): void {
30
+ const now = Date.now();
31
+ for (const [key, job] of jobs) {
32
+ if (now - job.lastSeen <= IDLE_MS) continue;
33
+ job.controller.abort();
34
+ if (job.publishTimer) clearTimeout(job.publishTimer);
35
+ for (const notify of job.listeners) notify();
36
+ jobs.delete(key);
37
+ }
38
+ for (const [key, until] of stopped) if (until < now) stopped.delete(key);
39
+ }
40
+
41
+ function publish(job: SideChatJob, terminal = false): void {
42
+ if (job.publishTimer) {
43
+ if (!terminal) return;
44
+ clearTimeout(job.publishTimer);
45
+ }
46
+ const flush = () => {
47
+ job.publishTimer = undefined;
48
+ job.progress = { ...job.progress, revision: job.progress.revision + 1 };
49
+ job.versions.set(job.progress.revision, job.progress.text);
50
+ while (job.versions.size > 8) job.versions.delete(job.versions.keys().next().value!);
51
+ for (const notify of job.listeners) notify();
52
+ };
53
+ if (terminal) flush();
54
+ else job.publishTimer = setTimeout(flush, 80);
55
+ }
56
+
57
+ function settle(job: SideChatJob, update: Partial<SideChatProgress>): void {
58
+ if (job.progress.done) return;
59
+ job.progress = { ...job.progress, ...update, done: true };
60
+ publish(job, true);
61
+ }
62
+
63
+ /** 模型响应里的真实用量;拿不到就返回 undefined(不编数字) */
64
+ function usageOf(message: AssistantMessage): SideChatUsage | undefined {
65
+ const usage = message.usage;
66
+ if (!usage) return undefined;
67
+ return { input: usage.input ?? 0, output: usage.output ?? 0, cacheRead: usage.cacheRead ?? 0, cacheWrite: usage.cacheWrite ?? 0 };
68
+ }
69
+
70
+ function assistantText(message: AssistantMessage): string {
71
+ return message.content.flatMap((block) => block.type === "text" ? [block.text] : []).join("\n\n");
72
+ }
73
+
74
+ /** Creates a separate model call. It never prompts, branches, aborts or saves the main AgentSession. */
75
+ export function startSideChat(sessionPath: string, request: SideChatRequest): SideChatProgress {
76
+ reapJobs();
77
+ const key = keyOf(sessionPath, request.requestId);
78
+ const previous = jobs.get(key);
79
+ if (previous) return { ...previous.progress };
80
+ if ([...jobs.values()].filter((job) => !job.progress.done).length >= MAX_RUNNING) throw new BoundaryError("too many side-chat answers are running");
81
+ const managed = getManaged(sessionPath);
82
+ const source = managed.sm.buildSessionContext();
83
+ const selected = managed.session?.model;
84
+ const provider = request.model?.provider ?? selected?.provider ?? source.model?.provider;
85
+ const modelId = request.model?.id ?? selected?.id ?? source.model?.modelId;
86
+ if (!provider || !modelId) throw new BoundaryError("select a model for the side chat first");
87
+ const thinking = managed.session?.thinkingLevel ?? source.thinkingLevel;
88
+ const messages: unknown[] = [...source.messages];
89
+ // A question may refer to the answer currently streaming in the main pane.
90
+ if (managed.inflight && !source.messages.includes(managed.inflight as typeof source.messages[number])) messages.push(managed.inflight);
91
+ const background = mainConversationText(messages);
92
+ const job: SideChatJob = {
93
+ progress: { requestId: request.requestId, revision: 0, text: "", done: false, aborted: false },
94
+ controller: new AbortController(), listeners: new Set(), lastSeen: Date.now(), versions: new Map([[0, ""]]),
95
+ };
96
+ jobs.set(key, job);
97
+ if (stopped.has(key)) {
98
+ settle(job, { aborted: true });
99
+ return { ...job.progress };
100
+ }
101
+ void generate();
102
+ return { ...job.progress };
103
+
104
+ async function generate(): Promise<void> {
105
+ const timeout = setTimeout(() => {
106
+ job.controller.abort();
107
+ settle(job, { error: "side-chat request timed out" });
108
+ }, RUN_MS);
109
+ timeout.unref();
110
+ try {
111
+ const runtime = await getModelRuntime();
112
+ if (job.controller.signal.aborted) return;
113
+ const model = runtime.getModel(provider!, modelId!);
114
+ if (!model) throw new BoundaryError("the selected side-chat model is unavailable");
115
+ const built = buildSideChatContext(background, request, model);
116
+ const level = clampThinkingLevel(model, (thinking || "off") as ModelThinkingLevel);
117
+ job.progress = { ...job.progress, model: model.name, contextWindow: model.contextWindow, characters: built.characters, contextTruncated: built.contextTruncated };
118
+ publish(job);
119
+ const stream = runtime.streamSimple(model, built.context, {
120
+ signal: job.controller.signal,
121
+ reasoning: level === "off" ? undefined : level,
122
+ maxTokens: built.maxTokens,
123
+ sessionId: `piweb-sidechat-${request.threadId}`,
124
+ transport: "sse",
125
+ });
126
+ const blocks = new Map<number, string>();
127
+ for await (const event of stream) {
128
+ if (job.progress.done) break;
129
+ if (event.type === "text_delta") {
130
+ blocks.set(event.contentIndex, (blocks.get(event.contentIndex) ?? "") + event.delta);
131
+ job.progress = { ...job.progress, text: [...blocks.entries()].sort(([a], [b]) => a - b).map(([, text]) => text).join("\n\n") };
132
+ publish(job);
133
+ } else if (event.type === "done") {
134
+ const usage = usageOf(event.message);
135
+ settle(job, { text: assistantText(event.message), lengthLimited: event.reason === "length", ...(usage ? { usage } : {}) });
136
+ } else if (event.type === "error") {
137
+ // 出错时的响应里也可能有 usage(比如跑一半被截断),有就记下来
138
+ const usage = usageOf(event.error);
139
+ settle(job, { text: assistantText(event.error), aborted: event.reason === "aborted", error: event.reason === "aborted" ? undefined : event.error.errorMessage || "model request failed", ...(usage ? { usage } : {}) });
140
+ }
141
+ }
142
+ if (!job.progress.done) settle(job, { aborted: job.controller.signal.aborted, error: job.controller.signal.aborted ? undefined : "model stream ended before completion" });
143
+ } catch (error) {
144
+ settle(job, { aborted: job.controller.signal.aborted, error: job.controller.signal.aborted ? undefined : error instanceof Error ? error.message : "side-chat request failed" });
145
+ } finally {
146
+ clearTimeout(timeout);
147
+ }
148
+ }
149
+ }
150
+
151
+ function getJob(sessionPath: string, requestId: string): SideChatJob {
152
+ reapJobs();
153
+ const job = jobs.get(keyOf(sessionPath, requestId));
154
+ if (!job) throw new BoundaryError("side-chat answer expired; send the question again");
155
+ job.lastSeen = Date.now();
156
+ return job;
157
+ }
158
+
159
+ /** Finite JSON responses also work through Cloudflare's buffering quick tunnels. */
160
+ export async function pollSideChat(sessionPath: string, requestId: string, after: number, signal: AbortSignal): Promise<SideChatProgress> {
161
+ const job = getJob(sessionPath, requestId);
162
+ if (!job.progress.done && job.progress.revision <= after && !signal.aborted) {
163
+ await new Promise<void>((resolve) => {
164
+ const finish = () => {
165
+ clearTimeout(timer);
166
+ job.listeners.delete(finish);
167
+ signal.removeEventListener("abort", finish);
168
+ resolve();
169
+ };
170
+ const timer = setTimeout(finish, 20_000);
171
+ job.listeners.add(finish);
172
+ signal.addEventListener("abort", finish, { once: true });
173
+ if (signal.aborted || job.progress.done || job.progress.revision > after) finish();
174
+ });
175
+ }
176
+ const previous = job.versions.get(after);
177
+ if (previous !== undefined && job.progress.text.startsWith(previous)) {
178
+ return { ...job.progress, text: job.progress.text.slice(previous.length), textOffset: previous.length };
179
+ }
180
+ return { ...job.progress, textOffset: 0 };
181
+ }
182
+
183
+ export function stopSideChat(sessionPath: string, requestId: string): void {
184
+ const key = keyOf(sessionPath, requestId);
185
+ // Remember an early stop so cancelling while POST is in transit cannot start a late model call.
186
+ stopped.set(key, Date.now() + 60_000);
187
+ const job = jobs.get(key);
188
+ if (!job) return;
189
+ job.controller.abort();
190
+ settle(job, { aborted: true });
191
+ }
@@ -0,0 +1,61 @@
1
+ /** Shared side-chat data. Main session messages never contain these records. */
2
+ export interface SideChatHistoryMessage {
3
+ role: "user" | "assistant";
4
+ text: string;
5
+ }
6
+
7
+ export interface SideChatModel {
8
+ provider: string;
9
+ id: string;
10
+ }
11
+
12
+ export function isSideChatModel(value: unknown): value is SideChatModel {
13
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
14
+ const model = value as Record<string, unknown>;
15
+ return [model.provider, model.id].every((part) => typeof part === "string" && part.trim().length > 0 && part.length <= 512);
16
+ }
17
+
18
+ export interface SideChatRequest {
19
+ requestId: string;
20
+ threadId: string;
21
+ text: string;
22
+ history: SideChatHistoryMessage[];
23
+ /** Omitted means follow the main conversation's current model. */
24
+ model?: SideChatModel;
25
+ }
26
+
27
+ /**
28
+ * 一次旁聊回答的真实用量,直接来自模型响应(不是估算)。
29
+ * 它同时就是「这次问出去多少上下文」——input + 缓存读写就是发出去的 prompt 大小。
30
+ */
31
+ export interface SideChatUsage {
32
+ input: number;
33
+ output: number;
34
+ cacheRead: number;
35
+ cacheWrite: number;
36
+ }
37
+
38
+ export interface SideChatProgress {
39
+ requestId: string;
40
+ revision: number;
41
+ text: string;
42
+ /** Poll responses may replace the text from this UTF-16 offset, avoiding repeated full answers. */
43
+ textOffset?: number;
44
+ done: boolean;
45
+ aborted: boolean;
46
+ error?: string;
47
+ model?: string;
48
+ /** 该回答用的模型上下文窗口;拿不到就不写,界面宁可什么都不显示也不显示假数字 */
49
+ contextWindow?: number;
50
+ usage?: SideChatUsage;
51
+ /** 拼这次上下文时的真实字符数(分项用;系统提示词 + 引用的主会话背景) */
52
+ characters?: { systemPrompt: number; background: number };
53
+ contextTruncated?: boolean;
54
+ lengthLimited?: boolean;
55
+ }
56
+
57
+ export const SIDE_CHAT_BODY_LIMIT = 2 * 1024 * 1024;
58
+ export const SIDE_CHAT_TEXT_LIMIT = 100_000;
59
+ export const SIDE_CHAT_HISTORY_LIMIT = 1000;
60
+ export const isSideChatId = (value: unknown): value is string =>
61
+ typeof value === "string" && /^[a-zA-Z0-9_-]{8,80}$/.test(value);
package/src/lib/types.ts CHANGED
@@ -244,7 +244,7 @@ export interface ContextResource {
244
244
  */
245
245
  export interface ContextBreakdown {
246
246
  /** 当前投影中实际文本的字符数;不将估算 token 按比例分摊。 */
247
- characters?: { systemPrompt: number; systemTools: number; customTools: number; memory: number; skills: number; compacted: number };
247
+ characters?: { systemPrompt: number; systemTools: number; customTools: number; memory: number; skills: number; compacted: number;/** 旁聊专用:这一问里引用的主会话背景字符数 */ background?: number };
248
248
  /** SDK 内建 system prompt 正文(不含 memory/skills/tools 段)的估算 token */
249
249
  systemPrompt: number;
250
250
  /** 内建工具(SDK 自带 bash/read/edit 等)定义的估算 token */
@@ -5,7 +5,8 @@
5
5
  */
6
6
  import fs from "node:fs/promises";
7
7
  import path from "node:path";
8
- import { installPiUpdate, PI_AI_PACKAGE, PI_PACKAGE, prepareRelease } from "../../scripts/release.mjs";
8
+ import { installPiUpdate, PI_PACKAGE, prepareRelease } from "../../scripts/release.mjs";
9
+ import { installPiRuntime } from "../../scripts/runtime/pi-packages.mjs";
9
10
  import { assertInstallation, readProductionBuild } from "../../scripts/build-output.mjs";
10
11
  import { runCommand } from "../../scripts/process-runner.mjs";
11
12
  import { isNewer } from "./semver";
@@ -155,15 +156,12 @@ export function createUpdateService({ root = APP_ROOT, run = runCommand, version
155
156
 
156
157
  /**
157
158
  * 只换 npm 成品包里的 pi 依赖:成品包没有 devDependencies,跑不了 typecheck / 构建,也不需要——
158
- * pi 两个包都在 serverExternalPackages 里,运行时从这里的 node_modules 加载,重启即用上新版。
159
+ * pi 引擎和 pi-ai 都在 serverExternalPackages 里;配套依赖同步锁到同一版本,重启即用上新版。
159
160
  * Windows 也能做:运行中的服务没加载 pi 的原生模块,pi 目录也不是服务的工作目录,不会 EBUSY。
160
161
  * 图标不用同步:成品包自带 public/provider-icons,清单也和 pi 版本无关。
161
162
  */
162
163
  async function installPiIntoPackage(installation: string, assertIdle: () => Promise<void>): Promise<UpdateResult> {
163
- await assertIdle();
164
- await run("npm", ["install", "--save-exact", `${PI_PACKAGE}@latest`, `${PI_AI_PACKAGE}@latest`, "--omit=dev", "--no-audit", "--no-fund"], {
165
- cwd: installation, env: commandEnv, timeoutMs: 600_000,
166
- });
164
+ await installPiRuntime({ root: installation, env: commandEnv, run }, { omitDev: true, assertIdle });
167
165
  await assertIdle();
168
166
  assertInstallation(installation);
169
167
  const version = await readPackageVersion(path.join(installation, "node_modules", PI_PACKAGE, "package.json"));
@@ -1 +0,0 @@
1
- "use strict";exports.id=229,exports.ids=[229],exports.modules={66229:(a,b,c)=>{c.a(a,async(a,d)=>{try{c.d(b,{GW:()=>t,deleteSession:()=>u,lx:()=>v});var e=c(73024),f=c(51455),g=c.n(f),h=c(76760),i=c.n(h),j=c(80481),k=c(75430),l=c(28686),m=c(87982),n=c(62433),o=a([k,l,m,n]);async function p(a,b){try{var c;let d,f,g=null,h=0,i="";for await(let b of(0,j.createInterface)({input:(0,e.createReadStream)(a,{encoding:"utf8"}),crlfDelay:1/0})){let a;if(!b.trim())continue;try{a=JSON.parse(b)}catch{continue}if(!g){if(a?.type!=="session")return null;g=a;continue}if(a?.type==="session_info"&&(d="string"==typeof a.name&&a.name.trim()?a.name.trim():void 0),a?.type!=="message")continue;h+=1;let c=a.message;if(!c||"string"!=typeof c.role||!("content"in c)||"user"!==c.role&&"assistant"!==c.role)continue;let e="number"==typeof c.timestamp?c.timestamp:new Date(a.timestamp).getTime();if(Number.isFinite(e)&&(f=Math.max(f??0,e)),!i&&"user"===c.role){let a=c.content;i="string"==typeof a?a:Array.isArray(a)?a.filter(a=>a?.type==="text").map(a=>a.text).join(" "):""}}if(!g)return null;let k="string"==typeof g.timestamp?new Date(g.timestamp).getTime():0/0,l=f&&f>0?new Date(f):Number.isNaN(k)?b.mtime:new Date(k);return{path:(c={path:a,name:d,cwd:"string"==typeof g.cwd?g.cwd:"",created:new Date(String(g.timestamp)),modified:l,messageCount:h,firstMessage:function(a){if(a.length<=300)return a;let b=300,c=a.charCodeAt(b-1);return c>=55296&&c<=56319&&(b-=1),`${a.slice(0,b)}…`}(i)||"(no messages)"}).path,name:c.name,cwd:c.cwd,created:c.created.toISOString(),modified:c.modified.toISOString(),messageCount:c.messageCount,firstMessage:c.firstMessage}}catch{return null}}[k,l,m,n]=o.then?(await o)():o;let w=new Map,x=null,y=null,z=!1,A=null;async function q(){let a=i().join((0,m.$M)(),"piweb-session-index.json");if(y!==a){y=a,w.clear();try{let b=JSON.parse(await g().readFile(a,"utf8"));if(b?.version!==2||!b.entries||"object"!=typeof b.entries||Array.isArray(b.entries))return;for(let[a,c]of Object.entries(b.entries)){if(!Array.isArray(c)||3!==c.length)continue;let[b,d,e]=c;"number"==typeof b&&"number"==typeof d&&w.set(a,{mtimeMs:b,size:d,summary:e&&"object"==typeof e?e:null})}}catch{}}}async function r(){if(!z)return;z=!1;let a=y;if(!a)return;let b={};for(let[a,c]of[...w.entries()].sort((a,b)=>b[1].mtimeMs-a[1].mtimeMs).slice(0,5e3))b[a]=[c.mtimeMs,c.size,c.summary];try{await g().mkdir(i().dirname(a),{recursive:!0});let c=`${a}.${process.pid}.tmp`;try{await g().writeFile(c,JSON.stringify({version:2,entries:b}),"utf8"),await g().rename(c,a)}finally{await g().rm(c,{force:!0}).catch(()=>void 0)}}catch{}}async function s(){let a=i().join((0,m.$M)(),"sessions"),b=[],c=[];try{c=await g().readdir(a,{withFileTypes:!0})}catch{return[]}await Promise.all(c.filter(a=>a.isDirectory()||a.isSymbolicLink()).map(async c=>{try{for(let d of(await g().readdir(i().join(a,c.name))))d.endsWith(".jsonl")&&b.push(i().join(a,c.name,d))}catch{}}));let d=new Set,e=[],f=!1;for(let a=0;a<b.length;a+=16)await Promise.all(b.slice(a,a+16).map(async a=>{d.add(a);let b=await g().stat(a).catch(()=>null);if(!b?.isFile())return;let c=w.get(a);if(c&&c.mtimeMs===b.mtimeMs&&c.size===b.size){c.summary&&e.push(c.summary);return}let h=await p(a,b);w.set(a,{mtimeMs:b.mtimeMs,size:b.size,summary:h}),f=!0,h&&e.push(h)}));for(let a of w.keys())d.has(a)||(w.delete(a),f=!0);return f&&(z=!0,A||(A=setTimeout(()=>{A=null,r()},1500),A.unref?.())),e}let B=null;async function t(a){let b;if(B&&Date.now()-B.at<1200?b=B.list:(await q(),x??=s().finally(()=>{x=null}),b=await x,B={at:Date.now(),list:b}),a?.trim()){let c=a.trim().toLowerCase();b=b.filter(a=>a.name?.toLowerCase().includes(c)||a.firstMessage.toLowerCase().includes(c)||a.cwd.toLowerCase().includes(c))}return b.sort((a,b)=>b.modified.localeCompare(a.modified)||a.path.localeCompare(b.path)),b}async function u(a){let b=i().join((0,m.$M)(),"sessions"),c=await g().realpath(b).catch(()=>i().resolve(b)),d=i().resolve(a);if(".jsonl"!==i().extname(d).toLowerCase()||!(0,n.CN)(i().resolve(b),d))throw new n.BV("session path is outside the Pi session store");let e=await g().realpath(d).catch(a=>{if("ENOENT"===a.code)return d;throw a});if(e!==d&&!(0,n.CN)(c,e))throw new n.BV("session path escapes the Pi session store");try{await g().unlink(d)}catch(a){if("ENOENT"!==a.code)throw a}}async function v(a){let b=k.SessionManager.open(a),c=[];try{c=b.buildContextEntries().filter(a=>"message"===a.type&&a.message).map(a=>(0,l.H$)(a.message,a.id))}catch{c=[]}let d="";try{d=b.getSessionName?.()??""}catch{d=""}return{cwd:b.getCwd(),name:d,messages:c}}d()}catch(a){d(a)}})}};
@@ -1 +0,0 @@
1
- "use strict";exports.id=393,exports.ids=[393],exports.modules={75393:(a,b,c)=>{function d(a=process.env){let b=(a.PI_WEB_AUTH??"").trim().toLowerCase();return"1"===b||"true"===b||"on"===b||"yes"===b}c.d(b,{authEnabled:()=>d}),c(77598)}};