@baique/pi-web-sky 0.1.35 → 0.1.36

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 (294) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/app-path-routes-manifest.json +16 -16
  3. package/.next/build-manifest.json +3 -3
  4. package/.next/react-loadable-manifest.json +1 -1
  5. package/.next/required-server-files.js +1 -1
  6. package/.next/required-server-files.json +1 -1
  7. package/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  8. package/.next/server/app/_global-error.html +1 -1
  9. package/.next/server/app/_global-error.rsc +1 -1
  10. package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  11. package/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  12. package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  13. package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  14. package/.next/server/app/_not-found.html +1 -1
  15. package/.next/server/app/_not-found.rsc +3 -3
  16. package/.next/server/app/_not-found.segments/_full.segment.rsc +3 -3
  17. package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +3 -3
  18. package/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  19. package/.next/server/app/api/agent/[id]/bash-output/route.js +1 -1
  20. package/.next/server/app/api/agent/[id]/events/route.js +1 -1
  21. package/.next/server/app/api/agent/[id]/route.js +1 -1
  22. package/.next/server/app/api/app-update/route.js +1 -1
  23. package/.next/server/app/api/auth/login/[provider]/route.js +1 -1
  24. package/.next/server/app/api/cwd/validate/route.js +1 -1
  25. package/.next/server/app/api/default-cwd/route.js +1 -1
  26. package/.next/server/app/api/file-index/route.js +1 -1
  27. package/.next/server/app/api/files/[...path]/route.js +1 -1
  28. package/.next/server/app/api/mcp/route.js +1 -1
  29. package/.next/server/app/api/models/route.js +1 -1
  30. package/.next/server/app/api/plugins/route.js +1 -1
  31. package/.next/server/app/api/project-trust/route.js +1 -1
  32. package/.next/server/app/api/search/route.js +1 -1
  33. package/.next/server/app/api/sessions/[id]/auto-name/route.js +1 -1
  34. package/.next/server/app/api/sessions/[id]/context/route.js +1 -1
  35. package/.next/server/app/api/sessions/[id]/entries/[entryId]/thinking/route.js +1 -1
  36. package/.next/server/app/api/sessions/[id]/export/route.js +1 -1
  37. package/.next/server/app/api/sessions/[id]/state/route.js +1 -1
  38. package/.next/server/app/api/sessions/[id]/todos/route.js +1 -1
  39. package/.next/server/app/api/sessions/route.js +1 -1
  40. package/.next/server/app/api/skills/check/route.js +1 -1
  41. package/.next/server/app/api/skills/check/route.js.nft.json +1 -1
  42. package/.next/server/app/api/skills/install/route.js +1 -1
  43. package/.next/server/app/api/skills/route.js +2 -31
  44. package/.next/server/app/api/skills/route.js.nft.json +1 -1
  45. package/.next/server/app/api/skills/update/route.js +1 -1
  46. package/.next/server/app/api/skills/update/route.js.nft.json +1 -1
  47. package/.next/server/app/api/worktrees/route.js +1 -1
  48. package/.next/server/app/index.html +1 -1
  49. package/.next/server/app/index.rsc +5 -5
  50. package/.next/server/app/index.segments/__PAGE__.segment.rsc +5 -5
  51. package/.next/server/app/index.segments/_full.segment.rsc +5 -5
  52. package/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  53. package/.next/server/app/page.js +2 -2
  54. package/.next/server/app/page_client-reference-manifest.js +1 -1
  55. package/.next/server/app-paths-manifest.json +16 -16
  56. package/.next/server/chunks/2404.js +1 -1
  57. package/.next/server/chunks/3500.js +30 -0
  58. package/.next/server/chunks/4226.js +2 -2
  59. package/.next/server/chunks/542.js +10 -5
  60. package/.next/server/chunks/5794.js +1 -1
  61. package/.next/server/chunks/6429.js +1 -1
  62. package/.next/server/chunks/898.js +1 -1
  63. package/.next/server/middleware-build-manifest.js +1 -1
  64. package/.next/server/middleware-react-loadable-manifest.js +1 -1
  65. package/.next/server/pages/404.html +1 -1
  66. package/.next/server/pages/500.html +1 -1
  67. package/.next/static/chunks/5315.7e72d26a3ad3bd9a.js +4 -0
  68. package/.next/static/chunks/app/{layout-1d9a7a44fbc92d2e.js → layout-ebbc0d287267b07b.js} +1 -1
  69. package/.next/static/chunks/app/{page-6d6779a81a0a7639.js → page-e568623a44c67606.js} +2 -2
  70. package/.next/static/chunks/{webpack-fa4f40085b1ec1ac.js → webpack-2e4e348867a555cd.js} +1 -1
  71. package/.next/static/css/422c7dc0faa04d89.css +1 -0
  72. package/.next/trace +8 -9
  73. package/.next/trace-build +1 -1
  74. package/bin/pi-web-sky.js +15 -20
  75. package/lib/agent-client.test.mjs +64 -0
  76. package/lib/agent-client.ts +53 -0
  77. package/lib/agent-event-connection.test.mjs +193 -0
  78. package/lib/agent-event-connection.ts +202 -0
  79. package/lib/agent-event-stream.test.mjs +155 -0
  80. package/lib/agent-event-stream.ts +132 -0
  81. package/lib/agent-event-wire.test.mjs +278 -0
  82. package/lib/agent-event-wire.ts +107 -0
  83. package/lib/agent-phase.ts +24 -0
  84. package/lib/allowed-roots.ts +31 -0
  85. package/lib/ansi.test.mjs +52 -0
  86. package/lib/ansi.ts +211 -0
  87. package/lib/api-types.ts +139 -0
  88. package/lib/app-update.test.mjs +27 -0
  89. package/lib/app-update.ts +28 -0
  90. package/lib/atomic-file.test.mjs +37 -0
  91. package/lib/atomic-file.ts +34 -0
  92. package/lib/audit-session.ts +235 -0
  93. package/lib/bash-output.test.mjs +63 -0
  94. package/lib/bash-output.ts +53 -0
  95. package/lib/bg-image.ts +129 -0
  96. package/lib/board-events.ts +83 -0
  97. package/lib/board-purge.test.mjs +66 -0
  98. package/lib/board-purge.ts +65 -0
  99. package/lib/board-search.test.mjs +110 -0
  100. package/lib/board-search.ts +63 -0
  101. package/lib/board-store.test.mjs +542 -0
  102. package/lib/board-store.ts +794 -0
  103. package/lib/board-types.ts +96 -0
  104. package/lib/board-utils.ts +17 -0
  105. package/lib/bounded-form-data.test.mjs +54 -0
  106. package/lib/bounded-form-data.ts +49 -0
  107. package/lib/browser-notifications.test.mjs +167 -0
  108. package/lib/browser-notifications.ts +104 -0
  109. package/lib/chat-lazy-load.test.mjs +140 -0
  110. package/lib/chat-lazy-load.ts +64 -0
  111. package/lib/clipboard-paths.test.mjs +140 -0
  112. package/lib/clipboard-paths.ts +192 -0
  113. package/lib/clipboard.ts +18 -0
  114. package/lib/compaction-summary.test.mjs +44 -0
  115. package/lib/compaction-summary.ts +34 -0
  116. package/lib/custom-ui-terminal.test.mjs +32 -0
  117. package/lib/custom-ui-terminal.ts +27 -0
  118. package/lib/deepseek-pricing.test.mjs +120 -0
  119. package/lib/deepseek-pricing.ts +71 -0
  120. package/lib/directory-browser.test.mjs +60 -0
  121. package/lib/directory-browser.ts +83 -0
  122. package/lib/draft-stash.test.mjs +91 -0
  123. package/lib/draft-stash.ts +102 -0
  124. package/lib/draft-store.ts +105 -0
  125. package/lib/dropdown-direction.ts +27 -0
  126. package/lib/extension-command.test.mjs +63 -0
  127. package/lib/extension-command.ts +81 -0
  128. package/lib/file-access.test.mjs +31 -0
  129. package/lib/file-access.ts +59 -0
  130. package/lib/file-dirent.test.mjs +56 -0
  131. package/lib/file-dirent.ts +17 -0
  132. package/lib/file-explorer-state.test.mjs +63 -0
  133. package/lib/file-explorer-state.ts +36 -0
  134. package/lib/file-fuzzy.test.mjs +29 -0
  135. package/lib/file-fuzzy.ts +189 -0
  136. package/lib/file-links.test.mjs +92 -0
  137. package/lib/file-links.ts +148 -0
  138. package/lib/file-paths.ts +43 -0
  139. package/lib/file-types.test.mjs +34 -0
  140. package/lib/file-types.ts +73 -0
  141. package/lib/file-upload.test.mjs +70 -0
  142. package/lib/file-upload.ts +59 -0
  143. package/lib/file-viewer-state.test.mjs +20 -0
  144. package/lib/file-viewer-state.ts +15 -0
  145. package/lib/frontmatter.test.mjs +55 -0
  146. package/lib/frontmatter.ts +87 -0
  147. package/lib/git-changes.test.mjs +52 -0
  148. package/lib/git-changes.ts +232 -0
  149. package/lib/git-status.ts +47 -0
  150. package/lib/git-types.ts +29 -0
  151. package/lib/http-dispatcher.test.mjs +83 -0
  152. package/lib/http-dispatcher.ts +86 -0
  153. package/lib/i18n/format.test.mjs +19 -0
  154. package/lib/i18n/format.ts +60 -0
  155. package/lib/i18n/messages/en.ts +662 -0
  156. package/lib/i18n/messages/zh-CN.ts +661 -0
  157. package/lib/i18n/registry.test.mjs +32 -0
  158. package/lib/i18n/registry.ts +43 -0
  159. package/lib/i18n/types.ts +15 -0
  160. package/lib/image-attachments.test.mjs +27 -0
  161. package/lib/image-attachments.ts +56 -0
  162. package/lib/initial-navigation.test.mjs +65 -0
  163. package/lib/initial-navigation.ts +15 -0
  164. package/lib/markdown.test.mjs +104 -0
  165. package/lib/markdown.ts +347 -0
  166. package/lib/message-display.test.mjs +130 -0
  167. package/lib/message-display.ts +47 -0
  168. package/lib/model-catalog.test.mjs +327 -0
  169. package/lib/model-catalog.ts +414 -0
  170. package/lib/model-discovery-auth.ts +58 -0
  171. package/lib/model-discovery.test.mjs +50 -0
  172. package/lib/model-discovery.ts +75 -0
  173. package/lib/model-id-preference.ts +46 -0
  174. package/lib/model-scope.test.mjs +193 -0
  175. package/lib/model-scope.ts +198 -0
  176. package/lib/models-cache.test.mjs +122 -0
  177. package/lib/models-cache.ts +90 -0
  178. package/lib/models-config-store.test.mjs +200 -0
  179. package/lib/models-config-store.ts +84 -0
  180. package/lib/next-config-esm.test.mjs +14 -0
  181. package/lib/next-config.test.mjs +13 -0
  182. package/lib/node-version.test.mjs +33 -0
  183. package/lib/normalize.test.mjs +54 -0
  184. package/lib/normalize.ts +63 -0
  185. package/lib/npx.ts +62 -0
  186. package/lib/panel-layout.test.mjs +56 -0
  187. package/lib/panel-layout.ts +52 -0
  188. package/lib/patch.test.mjs +110 -0
  189. package/lib/patch.ts +134 -0
  190. package/lib/path-security.ts +42 -0
  191. package/lib/paths.test.mjs +58 -0
  192. package/lib/paths.ts +71 -0
  193. package/lib/pi-types.ts +183 -0
  194. package/lib/pi-web-options.test.mjs +63 -0
  195. package/lib/process-lifecycle.test.mjs +83 -0
  196. package/lib/project-command-env.test.mjs +298 -0
  197. package/lib/project-command-env.ts +129 -0
  198. package/lib/project-groups.test.mjs +60 -0
  199. package/lib/project-groups.ts +53 -0
  200. package/lib/project-identity.test.mjs +37 -0
  201. package/lib/project-identity.ts +26 -0
  202. package/lib/project-tree.test.mjs +115 -0
  203. package/lib/project-tree.ts +176 -0
  204. package/lib/project-trust.test.mjs +117 -0
  205. package/lib/project-trust.ts +48 -0
  206. package/lib/prompt-recovery.test.mjs +33 -0
  207. package/lib/prompt-recovery.ts +47 -0
  208. package/lib/provider-api-key-route.test.mjs +12 -0
  209. package/lib/provider-credential-store.test.mjs +126 -0
  210. package/lib/provider-credential-store.ts +114 -0
  211. package/lib/provider-listing-runtime.ts +37 -0
  212. package/lib/provider-listing.test.mjs +114 -0
  213. package/lib/provider-listing.ts +118 -0
  214. package/lib/provider-services.ts +64 -0
  215. package/lib/request-security.test.mjs +256 -0
  216. package/lib/request-security.ts +136 -0
  217. package/lib/rpc-manager-shutdown.test.mjs +372 -0
  218. package/lib/rpc-manager-widgets.test.mjs +342 -0
  219. package/lib/rpc-manager.test.mjs +139 -0
  220. package/lib/rpc-manager.ts +1703 -0
  221. package/lib/rpc-session-info.test.mjs +93 -0
  222. package/lib/session-delete.test.mjs +53 -0
  223. package/lib/session-delete.ts +86 -0
  224. package/lib/session-file-references-core.ts +82 -0
  225. package/lib/session-file-references.test.mjs +91 -0
  226. package/lib/session-file-references.ts +29 -0
  227. package/lib/session-path.test.mjs +24 -0
  228. package/lib/session-path.ts +11 -0
  229. package/lib/session-reader.pagination.test.mjs +115 -0
  230. package/lib/session-reader.test.mjs +446 -0
  231. package/lib/session-reader.ts +548 -0
  232. package/lib/session-resolve.test.mjs +65 -0
  233. package/lib/session-row-context-menu.test.mjs +46 -0
  234. package/lib/session-row-context-menu.ts +29 -0
  235. package/lib/session-scanner.test.mjs +151 -0
  236. package/lib/session-scanner.ts +238 -0
  237. package/lib/session-search.test.mjs +173 -0
  238. package/lib/session-search.ts +284 -0
  239. package/lib/session-stats.ts +26 -0
  240. package/lib/session-timing.test.mjs +107 -0
  241. package/lib/session-timing.ts +45 -0
  242. package/lib/session-title.test.mjs +284 -0
  243. package/lib/session-title.ts +255 -0
  244. package/lib/skill-frontmatter.test.mjs +75 -0
  245. package/lib/skill-frontmatter.ts +40 -0
  246. package/lib/skill-lock.test.mjs +182 -0
  247. package/lib/skill-lock.ts +146 -0
  248. package/lib/skill-updates.test.mjs +182 -0
  249. package/lib/skill-updates.ts +263 -0
  250. package/lib/skills-service.ts +16 -0
  251. package/lib/slash-display.test.mjs +122 -0
  252. package/lib/slash-display.ts +30 -0
  253. package/lib/sqlite-db.test.mjs +140 -0
  254. package/lib/sqlite-db.ts +205 -0
  255. package/lib/startup-preferences.test.mjs +130 -0
  256. package/lib/startup-preferences.ts +65 -0
  257. package/lib/streaming-message.test.mjs +212 -0
  258. package/lib/streaming-message.ts +151 -0
  259. package/lib/subagent-widget.test.mjs +87 -0
  260. package/lib/subagent-widget.ts +109 -0
  261. package/lib/sync-room-server.mjs +137 -0
  262. package/lib/task-card-store.test.mjs +309 -0
  263. package/lib/task-card-store.ts +559 -0
  264. package/lib/task-scheduler.ts +498 -0
  265. package/lib/task-store.test.mjs +216 -0
  266. package/lib/task-store.ts +271 -0
  267. package/lib/terminal-input.test.mjs +46 -0
  268. package/lib/terminal-input.ts +63 -0
  269. package/lib/terminal-manager.test.mjs +71 -0
  270. package/lib/terminal-manager.ts +245 -0
  271. package/lib/tool-execution-progress.test.mjs +32 -0
  272. package/lib/tool-execution-progress.ts +29 -0
  273. package/lib/tool-names.ts +25 -0
  274. package/lib/tool-preset-preference.test.mjs +72 -0
  275. package/lib/tool-preset-preference.ts +41 -0
  276. package/lib/tool-presets.test.mjs +56 -0
  277. package/lib/tool-presets.ts +42 -0
  278. package/lib/turn-written-files.test.mjs +137 -0
  279. package/lib/turn-written-files.ts +60 -0
  280. package/lib/types.ts +349 -0
  281. package/lib/wallpaper-settings.ts +191 -0
  282. package/lib/web-auth.test.mjs +50 -0
  283. package/lib/web-auth.ts +45 -0
  284. package/lib/workspace-memory.test.mjs +129 -0
  285. package/lib/workspace-memory.ts +98 -0
  286. package/lib/worktree.test.mjs +50 -0
  287. package/lib/worktree.ts +250 -0
  288. package/package.json +3 -1
  289. package/server.mjs +34 -0
  290. package/.next/server/chunks/7513.js +0 -30
  291. package/.next/static/chunks/5315.03de3c6abd8b283e.js +0 -4
  292. package/.next/static/css/cf25164cb7c04d95.css +0 -1
  293. /package/.next/static/{Ud6Y2ONkGuPVFbo1xZQvk → EoBw3VYlygpr1EqlVOa5b}/_buildManifest.js +0 -0
  294. /package/.next/static/{Ud6Y2ONkGuPVFbo1xZQvk → EoBw3VYlygpr1EqlVOa5b}/_ssgManifest.js +0 -0
@@ -0,0 +1,60 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdtemp, mkdir, rm, symlink, writeFile } from "node:fs/promises";
3
+ import { homedir, tmpdir } from "node:os";
4
+ import path from "node:path";
5
+ import test from "node:test";
6
+
7
+ async function loadSubject() {
8
+ return import("./directory-browser.ts");
9
+ }
10
+
11
+ test("lists directories and directory symlinks without returning files", async () => {
12
+ const root = await mkdtemp(path.join(tmpdir(), "pi-web-browse-"));
13
+ try {
14
+ await mkdir(path.join(root, "project"));
15
+ await writeFile(path.join(root, "notes.txt"), "test", "utf8");
16
+ await symlink(path.join(root, "project"), path.join(root, "linked-project"));
17
+
18
+ const { listDirectories } = await loadSubject();
19
+ const directories = await listDirectories(root);
20
+
21
+ assert.deepEqual(directories.map((entry) => entry.name), ["linked-project", "project"]);
22
+ } finally {
23
+ await rm(root, { recursive: true, force: true });
24
+ }
25
+ });
26
+
27
+ test("expands home-relative paths and rejects missing directories", async () => {
28
+ const {
29
+ getBrowseStartDirectory,
30
+ normalizeDirectory,
31
+ resolveDirectory,
32
+ shouldShowWindowsDrivePicker,
33
+ } = await loadSubject();
34
+ assert.equal(getBrowseStartDirectory(), homedir());
35
+ assert.equal(getBrowseStartDirectory("/project"), "/project");
36
+ assert.equal(shouldShowWindowsDrivePicker(undefined, "win32"), true);
37
+ assert.equal(shouldShowWindowsDrivePicker(undefined, "darwin"), false);
38
+ assert.equal(shouldShowWindowsDrivePicker(undefined, "linux"), false);
39
+ assert.equal(shouldShowWindowsDrivePicker("C:\\Projects", "win32"), false);
40
+ assert.equal(normalizeDirectory("~/project"), path.join(homedir(), "project"));
41
+ await assert.rejects(resolveDirectory(path.join(tmpdir(), `pi-web-missing-${Date.now()}`)));
42
+ });
43
+
44
+ test("builds every Windows drive-letter candidate", async () => {
45
+ const { getWindowsDriveCandidates } = await loadSubject();
46
+ const drives = getWindowsDriveCandidates();
47
+
48
+ assert.equal(drives.length, 26);
49
+ assert.deepEqual(drives[0], { name: "A:", path: "A:\\" });
50
+ assert.deepEqual(drives.at(-1), { name: "Z:", path: "Z:\\" });
51
+ });
52
+
53
+ test("finds parent directories across POSIX and Windows paths", async () => {
54
+ const { getParentDirectory } = await loadSubject();
55
+
56
+ assert.equal(getParentDirectory("/Users/alex/project"), "/Users/alex");
57
+ assert.equal(getParentDirectory("/"), null);
58
+ assert.equal(getParentDirectory("C:\\Users\\Alex\\project"), "C:\\Users\\Alex");
59
+ assert.equal(getParentDirectory("C:\\"), null);
60
+ });
@@ -0,0 +1,83 @@
1
+ import { readdir, realpath, stat } from "fs/promises";
2
+ import { homedir } from "os";
3
+ import path from "path";
4
+
5
+ export interface BrowsableDirectory {
6
+ name: string;
7
+ path: string;
8
+ }
9
+
10
+ export function shouldShowWindowsDrivePicker(
11
+ directory?: string,
12
+ platform: NodeJS.Platform = process.platform,
13
+ ): boolean {
14
+ return platform === "win32" && !directory;
15
+ }
16
+
17
+ export function getBrowseStartDirectory(directory?: string): string {
18
+ return directory || homedir();
19
+ }
20
+
21
+ export function getWindowsDriveCandidates(): BrowsableDirectory[] {
22
+ return "ABCDEFGHIJKLMNOPQRSTUVWXYZ".split("").map((letter) => ({
23
+ name: `${letter}:`,
24
+ path: `${letter}:\\`,
25
+ }));
26
+ }
27
+
28
+ export async function listWindowsDrives(): Promise<BrowsableDirectory[]> {
29
+ const candidates = await Promise.all(getWindowsDriveCandidates().map(async (drive) => {
30
+ try {
31
+ const driveStat = await stat(drive.path);
32
+ return driveStat.isDirectory() ? drive : null;
33
+ } catch {
34
+ return null;
35
+ }
36
+ }));
37
+
38
+ return candidates.filter((drive): drive is BrowsableDirectory => drive !== null);
39
+ }
40
+
41
+ export function normalizeDirectory(directory: string): string {
42
+ if (directory === "~") return homedir();
43
+ if (directory.startsWith("~/")) return path.resolve(homedir(), directory.slice(2));
44
+ return path.resolve(directory);
45
+ }
46
+
47
+ export function getParentDirectory(directory: string): string | null {
48
+ const pathApi = /^[a-zA-Z]:[\\/]/.test(directory) || directory.startsWith("\\\\")
49
+ ? path.win32
50
+ : path.posix;
51
+ const normalized = pathApi.normalize(directory);
52
+ const parent = pathApi.dirname(normalized);
53
+ return parent === normalized ? null : parent;
54
+ }
55
+
56
+ export async function resolveDirectory(directory: string): Promise<string> {
57
+ return realpath(normalizeDirectory(directory));
58
+ }
59
+
60
+ export async function listDirectories(directory: string): Promise<BrowsableDirectory[]> {
61
+ const entries = await readdir(directory, { withFileTypes: true });
62
+ // 忽略损坏、不可访问或不指向目录的符号链接。
63
+ const candidates = await Promise.all(entries.map(async (entry) => {
64
+ if (entry.isDirectory()) {
65
+ return { name: entry.name, path: path.join(directory, entry.name) };
66
+ }
67
+ if (!entry.isSymbolicLink()) return null;
68
+
69
+ try {
70
+ const entryPath = path.join(directory, entry.name);
71
+ const realEntryPath = await realpath(entryPath);
72
+ const entryStat = await stat(realEntryPath);
73
+ if (!entryStat.isDirectory()) return null;
74
+ return { name: entry.name, path: entryPath };
75
+ } catch {
76
+ return null;
77
+ }
78
+ }));
79
+
80
+ return candidates
81
+ .filter((entry): entry is BrowsableDirectory => entry !== null)
82
+ .sort((left, right) => left.name.localeCompare(right.name));
83
+ }
@@ -0,0 +1,91 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { createJiti } from "jiti";
4
+
5
+ const jiti = createJiti(import.meta.url, { tsconfigPaths: true });
6
+
7
+ // Mock localStorage before importing the module-under-test.
8
+ const store = new Map();
9
+ globalThis.localStorage = {
10
+ getItem: (k) => (store.has(k) ? store.get(k) : null),
11
+ setItem: (k, v) => store.set(k, String(v)),
12
+ removeItem: (k) => store.delete(k),
13
+ clear: () => store.clear(),
14
+ key: (i) => Array.from(store.keys())[i] ?? null,
15
+ get length() { return store.size; },
16
+ };
17
+
18
+ const {
19
+ addDraft, updateDraft, removeDraft, loadDrafts, formatRelativeTime, newDraftId,
20
+ } = await jiti.import("../lib/draft-stash.ts");
21
+
22
+ test.beforeEach(() => store.clear());
23
+
24
+ test("addDraft creates an item, persists it, and sorts newest first", () => {
25
+ const a = addDraft("第一条", 1000);
26
+ const b = addDraft("第二条", 2000);
27
+
28
+ const drafts = loadDrafts();
29
+ assert.equal(drafts.length, 2);
30
+ assert.equal(drafts[0].id, b.id);
31
+ assert.equal(drafts[0].content, "第二条");
32
+ assert.equal(drafts[1].content, "第一条");
33
+ assert.equal(typeof a.id, "string");
34
+ assert.ok(a.id.length > 0);
35
+ });
36
+
37
+ test("updateDraft rewrites content and moves the item to the top", () => {
38
+ const a = addDraft("原始内容", 1000);
39
+ addDraft("另一条", 2000);
40
+
41
+ const updated = updateDraft(a.id, "编辑后的内容", 3000);
42
+ assert.ok(updated);
43
+ assert.equal(updated.content, "编辑后的内容");
44
+ assert.equal(updated.updatedAt, 3000);
45
+
46
+ const drafts = loadDrafts();
47
+ assert.equal(drafts[0].id, a.id);
48
+ assert.equal(drafts[0].content, "编辑后的内容");
49
+ });
50
+
51
+ test("updateDraft returns null for an unknown id", () => {
52
+ assert.equal(updateDraft("missing-id", "x"), null);
53
+ });
54
+
55
+ test("removeDraft deletes only the target item", () => {
56
+ const a = addDraft("第一条", 1000);
57
+ const b = addDraft("第二条", 2000);
58
+
59
+ removeDraft(a.id);
60
+ const drafts = loadDrafts();
61
+ assert.equal(drafts.length, 1);
62
+ assert.equal(drafts[0].id, b.id);
63
+ });
64
+
65
+ test("loadDrafts survives corrupted storage", () => {
66
+ store.set("pi-web.drafts.v1", "{{{not json");
67
+ assert.deepEqual(loadDrafts(), []);
68
+ });
69
+
70
+ test("loadDrafts survives non-array / malformed entries", () => {
71
+ store.set("pi-web.drafts.v1", JSON.stringify([{ id: 1, content: "bad" }, "junk", { id: "ok", content: "fine", updatedAt: 5 }]));
72
+ const drafts = loadDrafts();
73
+ assert.equal(drafts.length, 1);
74
+ assert.equal(drafts[0].id, "ok");
75
+ });
76
+
77
+ test("formatRelativeTime produces compact labels", () => {
78
+ // 本地时区构造(实现按本地时间显示,断言保持一致)
79
+ const now = new Date(2026, 7, 18, 12, 0, 0).getTime(); // 2026-08-18 12:00 本地
80
+ assert.equal(formatRelativeTime(now - 5_000, now), "刚刚");
81
+ assert.equal(formatRelativeTime(now - 5 * 60_000, now), "5分钟前");
82
+ assert.equal(formatRelativeTime(now - 3 * 3_600_000, now), "09:00");
83
+ assert.equal(formatRelativeTime(now - 26 * 3_600_000, now), "昨天");
84
+ assert.equal(formatRelativeTime(new Date(2026, 7, 3, 9, 30).getTime(), now), "8月3日");
85
+ assert.equal(formatRelativeTime(new Date(2025, 11, 25, 9, 30).getTime(), now), "2025年12月25日");
86
+ });
87
+
88
+ test("newDraftId returns unique ids", () => {
89
+ const ids = new Set([newDraftId(), newDraftId(), newDraftId()]);
90
+ assert.equal(ids.size, 3);
91
+ });
@@ -0,0 +1,102 @@
1
+ // 用户级草稿暂存区:存储层(纯逻辑 + localStorage 封装)。
2
+ // 跨会话、浏览器全局存在;内容只增不自动删,删除仅靠手动。
3
+ // 与 lib/draft-store.ts(会话级单条输入框草稿)完全独立,互不干扰。
4
+
5
+ export interface DraftItem {
6
+ id: string;
7
+ content: string;
8
+ updatedAt: number; // epoch ms
9
+ }
10
+
11
+ const STORAGE_KEY = "pi-web.drafts.v1";
12
+
13
+ function getStorage(): Storage | null {
14
+ return typeof localStorage !== "undefined" ? localStorage : null;
15
+ }
16
+
17
+ function isDraftItem(value: unknown): value is DraftItem {
18
+ if (typeof value !== "object" || value === null) return false;
19
+ const v = value as Record<string, unknown>;
20
+ return (
21
+ typeof v.id === "string" &&
22
+ typeof v.content === "string" &&
23
+ typeof v.updatedAt === "number"
24
+ );
25
+ }
26
+
27
+ /** 生成草稿 id(优先 crypto.randomUUID,退化到时间戳+随机) */
28
+ export function newDraftId(): string {
29
+ if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
30
+ return crypto.randomUUID();
31
+ }
32
+ return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
33
+ }
34
+
35
+ /** 读取全部草稿,按最近活动(updatedAt)降序;存储缺失/损坏时返回空数组 */
36
+ export function loadDrafts(): DraftItem[] {
37
+ const storage = getStorage();
38
+ if (!storage) return [];
39
+ try {
40
+ const raw = storage.getItem(STORAGE_KEY);
41
+ if (!raw) return [];
42
+ const parsed: unknown = JSON.parse(raw);
43
+ if (!Array.isArray(parsed)) return [];
44
+ return parsed
45
+ .filter(isDraftItem)
46
+ .sort((a, b) => b.updatedAt - a.updatedAt);
47
+ } catch {
48
+ return [];
49
+ }
50
+ }
51
+
52
+ function saveDrafts(items: DraftItem[]): void {
53
+ const storage = getStorage();
54
+ if (!storage) return;
55
+ try {
56
+ storage.setItem(STORAGE_KEY, JSON.stringify(items));
57
+ } catch {
58
+ // 配额不足等——静默失败,草稿仅本次内存可见
59
+ }
60
+ }
61
+
62
+ /** 新增一条草稿,置顶 */
63
+ export function addDraft(content: string, now: number = Date.now()): DraftItem {
64
+ const item: DraftItem = { id: newDraftId(), content, updatedAt: now };
65
+ saveDrafts([item, ...loadDrafts()]);
66
+ return item;
67
+ }
68
+
69
+ /** 更新一条草稿内容并置顶(最新编辑在最上方);id 不存在返回 null */
70
+ export function updateDraft(id: string, content: string, now: number = Date.now()): DraftItem | null {
71
+ const items = loadDrafts();
72
+ const idx = items.findIndex((d) => d.id === id);
73
+ if (idx < 0) return null;
74
+ const updated: DraftItem = { ...items[idx], content, updatedAt: now };
75
+ items.splice(idx, 1);
76
+ items.unshift(updated);
77
+ saveDrafts(items);
78
+ return updated;
79
+ }
80
+
81
+ /** 删除一条草稿(仅手动删除,无自动删除逻辑) */
82
+ export function removeDraft(id: string): void {
83
+ saveDrafts(loadDrafts().filter((d) => d.id !== id));
84
+ }
85
+
86
+ function pad(n: number): string {
87
+ return n < 10 ? `0${n}` : String(n);
88
+ }
89
+
90
+ /** 相对时间简写:刚刚 / N分钟前 / HH:MM / 昨天 / M月D日 / YYYY年M月D日 */
91
+ export function formatRelativeTime(ts: number, now: number = Date.now()): string {
92
+ const diff = now - ts;
93
+ if (diff < 60_000) return "刚刚";
94
+ if (diff < 3_600_000) return `${Math.floor(diff / 60_000)}分钟前`;
95
+ const d = new Date(ts);
96
+ const n = new Date(now);
97
+ const startOfToday = new Date(n.getFullYear(), n.getMonth(), n.getDate()).getTime();
98
+ if (ts >= startOfToday) return `${pad(d.getHours())}:${pad(d.getMinutes())}`;
99
+ if (ts >= startOfToday - 86_400_000) return "昨天";
100
+ if (d.getFullYear() === n.getFullYear()) return `${d.getMonth() + 1}月${d.getDate()}日`;
101
+ return `${d.getFullYear()}年${d.getMonth() + 1}月${d.getDate()}日`;
102
+ }
@@ -0,0 +1,105 @@
1
+ import {
2
+ MAX_ATTACHED_IMAGES,
3
+ isBase64ImageWithinLimits,
4
+ } from "./image-attachments";
5
+
6
+ export interface ChatDraftImage {
7
+ data: string;
8
+ mimeType: string;
9
+ }
10
+
11
+ export interface ChatDraft {
12
+ value: string;
13
+ images: ChatDraftImage[];
14
+ }
15
+
16
+ const drafts = new Map<string, ChatDraft>();
17
+
18
+ function cloneDraft(draft: ChatDraft): ChatDraft {
19
+ return {
20
+ value: draft.value,
21
+ images: draft.images.map((image) => ({ ...image })),
22
+ };
23
+ }
24
+
25
+ function isEmptyDraft(draft: ChatDraft): boolean {
26
+ return !draft.value && draft.images.length === 0;
27
+ }
28
+
29
+ export function getDraft(key: string): ChatDraft | null {
30
+ const draft = drafts.get(key);
31
+ return draft ? cloneDraft(draft) : null;
32
+ }
33
+
34
+ export function setDraft(key: string, draft: ChatDraft): void {
35
+ if (isEmptyDraft(draft)) {
36
+ drafts.delete(key);
37
+ return;
38
+ }
39
+ drafts.set(key, cloneDraft(draft));
40
+ }
41
+
42
+ export function clearDraft(key: string): void {
43
+ drafts.delete(key);
44
+ }
45
+
46
+ export function mergeRestoredSubmissionText(submitted: string, current: string): string {
47
+ if (!submitted.trim()) return current;
48
+ if (!current.trim()) return submitted;
49
+ return `${submitted}\n\n${current}`;
50
+ }
51
+
52
+ export function mergeRestoredSubmissionDraft(
53
+ submittedText: string,
54
+ submittedImages: ChatDraftImage[] | undefined,
55
+ currentText: string,
56
+ currentImages: ChatDraftImage[],
57
+ ): ChatDraft {
58
+ const images = [...(submittedImages ?? []), ...currentImages]
59
+ .filter(isBase64ImageWithinLimits)
60
+ .slice(0, MAX_ATTACHED_IMAGES)
61
+ .map(({ data, mimeType }) => ({ data, mimeType }));
62
+
63
+ return {
64
+ value: mergeRestoredSubmissionText(submittedText, currentText),
65
+ images,
66
+ };
67
+ }
68
+
69
+ export function restoreDraftSubmission(
70
+ key: string,
71
+ text: string,
72
+ images?: ChatDraftImage[],
73
+ ): ChatDraft {
74
+ const current = getDraft(key) ?? { value: "", images: [] };
75
+ const restored = mergeRestoredSubmissionDraft(
76
+ text,
77
+ images,
78
+ current.value,
79
+ current.images,
80
+ );
81
+ setDraft(key, restored);
82
+ return restored;
83
+ }
84
+
85
+ export function rekeyDraft(
86
+ previousKey: string,
87
+ nextKey: string,
88
+ currentDraft?: ChatDraft,
89
+ ): ChatDraft | null {
90
+ if (previousKey === nextKey) return currentDraft ? cloneDraft(currentDraft) : getDraft(nextKey);
91
+
92
+ const storedPrevious = getDraft(previousKey);
93
+ const previous = currentDraft && !isEmptyDraft(currentDraft)
94
+ ? cloneDraft(currentDraft)
95
+ : (storedPrevious ?? (currentDraft ? cloneDraft(currentDraft) : null));
96
+ const next = getDraft(nextKey);
97
+ clearDraft(previousKey);
98
+ if (!previous) return next;
99
+
100
+ const merged = next
101
+ ? mergeRestoredSubmissionDraft(next.value, next.images, previous.value, previous.images)
102
+ : previous;
103
+ setDraft(nextKey, merged);
104
+ return cloneDraft(merged);
105
+ }
@@ -0,0 +1,27 @@
1
+ // 下拉/气泡方向探测:距最近滚动容器下缘空间不足时向上展开(返回 true)。
2
+ // 解决 absolute 定位弹层在滚动容器边缘被 overflow 裁剪的问题。
3
+
4
+ export function dropdownDirection(
5
+ trigger: HTMLElement,
6
+ estimatedHeight: number,
7
+ offset = 4,
8
+ ): boolean {
9
+ let node = trigger.parentElement;
10
+ let container: Element | null = null;
11
+ while (node) {
12
+ const st = getComputedStyle(node);
13
+ if (st.overflowY === "auto" || st.overflowY === "scroll" || st.overflowY === "overlay") {
14
+ container = node;
15
+ break;
16
+ }
17
+ node = node.parentElement;
18
+ }
19
+ const t = trigger.getBoundingClientRect();
20
+ const c = container
21
+ ? container.getBoundingClientRect()
22
+ : { top: 0, bottom: window.innerHeight };
23
+ const spaceBelow = c.bottom - t.bottom;
24
+ const spaceAbove = t.top - c.top;
25
+ // 下方放不下且上方放得下 → 向上
26
+ return spaceBelow < estimatedHeight + offset && spaceAbove >= estimatedHeight + offset;
27
+ }
@@ -0,0 +1,63 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+
4
+ async function loadSubject() {
5
+ try {
6
+ const { createJiti } = await import("jiti");
7
+ return createJiti(import.meta.url).import("./extension-command.ts");
8
+ } catch {
9
+ return import("./extension-command.ts");
10
+ }
11
+ }
12
+
13
+ const {
14
+ nextInspectRequestId,
15
+ subscribeInspectReplies,
16
+ dispatchInspectReply,
17
+ clearPendingInspectReplies,
18
+ } = await loadSubject();
19
+
20
+ test("requestId 唯一且符合 [A-Za-z0-9_-]{1,64}", () => {
21
+ const a = nextInspectRequestId();
22
+ const b = nextInspectRequestId();
23
+ assert.notEqual(a, b);
24
+ assert.match(a, /^[A-Za-z0-9_-]{1,64}$/);
25
+ });
26
+
27
+ test("订阅-分发回包", () => {
28
+ clearPendingInspectReplies();
29
+ const received = [];
30
+ const unsubscribe = subscribeInspectReplies((reply, requestId) => {
31
+ received.push([reply, requestId]);
32
+ });
33
+ dispatchInspectReply({ kind: "pi-subagents.inspect-reply" }, "req-1");
34
+ assert.equal(received.length, 1);
35
+ assert.equal(received[0][1], "req-1");
36
+ unsubscribe();
37
+ dispatchInspectReply({}, "req-2");
38
+ assert.equal(received.length, 1, "退订后不再收到");
39
+ clearPendingInspectReplies();
40
+ });
41
+
42
+ test("单个监听器抛错不影响其他", () => {
43
+ clearPendingInspectReplies();
44
+ const received = [];
45
+ subscribeInspectReplies(() => {
46
+ throw new Error("boom");
47
+ });
48
+ subscribeInspectReplies((_, requestId) => received.push(requestId));
49
+ dispatchInspectReply({}, "req-3");
50
+ assert.deepEqual(received, ["req-3"]);
51
+ clearPendingInspectReplies();
52
+ });
53
+
54
+ test("迟到订阅者补收 pending 回包", () => {
55
+ clearPendingInspectReplies();
56
+ // 先分发(无订阅者),再订阅 → 应立即补收
57
+ dispatchInspectReply({ kind: "pi-subagents.inspect-reply" }, "late-req");
58
+ const received = [];
59
+ subscribeInspectReplies((reply, requestId) => received.push([reply, requestId]));
60
+ assert.equal(received.length, 1);
61
+ assert.equal(received[0][1], "late-req");
62
+ clearPendingInspectReplies();
63
+ });
@@ -0,0 +1,81 @@
1
+ import { sendAgentCommand } from "./agent-client";
2
+
3
+ /**
4
+ * inspect 命令封装:复用现有 prompt 通道执行 pi-subagents 的
5
+ * `/subagents-inspect-rpc` 扩展命令(SDK 的 _tryExecuteExtensionCommand 会
6
+ * 直接执行 handler,不走模型轮次)。
7
+ *
8
+ * 回包经 `subagent-inspect` widget emit-then-retract 两帧回传,由
9
+ * ExtensionStatusBar 拦截(见 components/ExtensionStatusBar.tsx),通过
10
+ * 模块级订阅分发。这里只负责构造命令 + 生成 requestId。
11
+ */
12
+
13
+ let seq = 0;
14
+
15
+ /** 生成符合插件侧 [A-Za-z0-9_-]{1,64} 约束的 requestId */
16
+ export function nextInspectRequestId(): string {
17
+ seq = (seq + 1) % 0x7fffffff;
18
+ return `pw-${Date.now().toString(36)}-${seq.toString(36)}`;
19
+ }
20
+
21
+ /**
22
+ * 触发一次 inspect 查询。
23
+ * @param sessionId 当前会话 id
24
+ * @param requestId 本次请求的 requestId(与回包匹配)
25
+ * @param asyncId 子任务 run id(快照节点 id)
26
+ * @param childId 可选,workflow step / 嵌套子任务 id
27
+ * @param lines 可选,消息条数上限(1-200)
28
+ */
29
+ export async function invokeSubagentInspect(
30
+ sessionId: string,
31
+ requestId: string,
32
+ asyncId: string,
33
+ childId?: string,
34
+ lines?: number,
35
+ ): Promise<void> {
36
+ const parts = ["/subagents-inspect-rpc", requestId, asyncId];
37
+ if (childId) parts.push(childId);
38
+ if (lines !== undefined) parts.push("--lines", String(lines));
39
+ await sendAgentCommand(sessionId, { type: "prompt", message: parts.join(" ") });
40
+ }
41
+
42
+ /** 模块级回包订阅:ExtensionStatusBar 收到 subagent-inspect 帧时调用。
43
+ * 回包先存 pending 缓存;订阅者订阅时立即补收匹配的回包,避免命令排队期间
44
+ * 卡片卸载导致回包丢失(emit-then-retract 帧是瞬时的,不缓存就会丢)。 */
45
+ type InspectReplyListener = (reply: unknown, requestId: string) => void;
46
+ const inspectReplyListeners = new Set<InspectReplyListener>();
47
+ const pendingInspectReplies = new Map<string, unknown>();
48
+ const PENDING_REPLY_TTL_MS = 60_000;
49
+
50
+ export function subscribeInspectReplies(listener: InspectReplyListener): () => void {
51
+ inspectReplyListeners.add(listener);
52
+ // 补收订阅前已到达、尚未消费的回包
53
+ for (const [requestId, reply] of pendingInspectReplies) {
54
+ try {
55
+ listener(reply, requestId);
56
+ } catch {
57
+ // 单个监听器失败不影响其他监听器
58
+ }
59
+ }
60
+ return () => inspectReplyListeners.delete(listener);
61
+ }
62
+
63
+ /** 由 ExtensionStatusBar 调用,分发一次回包(并缓存,供迟到的订阅者补收) */
64
+ export function dispatchInspectReply(reply: unknown, requestId: string): void {
65
+ pendingInspectReplies.set(requestId, reply);
66
+ setTimeout(() => {
67
+ if (pendingInspectReplies.get(requestId) === reply) pendingInspectReplies.delete(requestId);
68
+ }, PENDING_REPLY_TTL_MS);
69
+ for (const listener of inspectReplyListeners) {
70
+ try {
71
+ listener(reply, requestId);
72
+ } catch {
73
+ // 单个监听器失败不影响其他监听器
74
+ }
75
+ }
76
+ }
77
+
78
+ /** 清空 pending 缓存(测试用;生产不调用) */
79
+ export function clearPendingInspectReplies(): void {
80
+ pendingInspectReplies.clear();
81
+ }
@@ -0,0 +1,31 @@
1
+ import assert from "node:assert/strict";
2
+ import fs from "node:fs";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import test from "node:test";
6
+
7
+ // Loaded through jiti so the module's own extensionless imports resolve the way
8
+ // the app resolves them (tsconfig moduleResolution: "bundler"); bare
9
+ // `import("./path-security.ts")` only works while that file has no imports.
10
+ async function loadSubject() {
11
+ const { createJiti } = await import("jiti");
12
+ return createJiti(import.meta.url).import("./path-security.ts");
13
+ }
14
+
15
+ test("rejects an existing path that escapes an allowed root through a symlink", async (t) => {
16
+ const { isExistingPathWithinRoots, isPathWithinRoots } = await loadSubject();
17
+ const base = fs.mkdtempSync(path.join(os.tmpdir(), "pi-web-file-access-"));
18
+ t.after(() => fs.rmSync(base, { recursive: true, force: true }));
19
+ const allowed = path.join(base, "allowed");
20
+ const outside = path.join(base, "outside");
21
+ fs.mkdirSync(allowed);
22
+ fs.mkdirSync(outside);
23
+ fs.writeFileSync(path.join(outside, "secret.txt"), "secret");
24
+ const link = path.join(allowed, "link");
25
+ fs.symlinkSync(outside, link, process.platform === "win32" ? "junction" : "dir");
26
+ const target = path.join(link, "secret.txt");
27
+ const roots = new Set([allowed]);
28
+
29
+ assert.equal(isPathWithinRoots(target, roots), true);
30
+ assert.equal(isExistingPathWithinRoots(target, roots), false);
31
+ });