@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,31 @@
1
+ import { toSlashPath } from "./paths";
2
+
3
+ // In-memory roots that should be browsable in addition to roots derived from
4
+ // persisted sessions. Stored on globalThis so Next.js hot-reload keeps them.
5
+ declare global {
6
+ var __piAllowedRootsCache: { roots: Set<string>; expiresAt: number } | undefined;
7
+ var __piAdditionalAllowedRoots: Set<string> | undefined;
8
+ }
9
+
10
+ /**
11
+ * Allowed roots are internal bookkeeping keys that are never displayed, so they
12
+ * are stored slash-normalized for consistent Set membership. Correctness does
13
+ * not depend on it — isPathWithinRoots() re-normalizes whatever it is given.
14
+ */
15
+ export function normalizeSlashes(filePath: string): string {
16
+ return toSlashPath(filePath);
17
+ }
18
+
19
+ export function getAdditionalAllowedRoots(): Set<string> {
20
+ if (!globalThis.__piAdditionalAllowedRoots) {
21
+ globalThis.__piAdditionalAllowedRoots = new Set();
22
+ }
23
+ return globalThis.__piAdditionalAllowedRoots;
24
+ }
25
+
26
+ export function allowFileRoot(root: string): void {
27
+ if (!root) return;
28
+ const normalizedRoot = normalizeSlashes(root);
29
+ getAdditionalAllowedRoots().add(normalizedRoot);
30
+ globalThis.__piAllowedRootsCache?.roots.add(normalizedRoot);
31
+ }
@@ -0,0 +1,52 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+
4
+ async function loadSubject() {
5
+ return import("./ansi.ts");
6
+ }
7
+
8
+ test("strips ANSI escape sequences", async () => {
9
+ const { stripAnsi } = await loadSubject();
10
+
11
+ assert.equal(stripAnsi("\x1b[31mred\x1b[0m plain"), "red plain");
12
+ assert.equal(stripAnsi("answer\x1b_pi:c\x07"), "answer");
13
+ });
14
+
15
+ test("normalizes boxed custom panel lines while preserving ANSI codes", async () => {
16
+ const { normalizeCustomPanelLines, stripAnsi } = await loadSubject();
17
+ const lines = [
18
+ "┌──────┐",
19
+ "│ \x1b[32mOK\x1b[0m │",
20
+ "└──────┘",
21
+ ];
22
+
23
+ const normalized = normalizeCustomPanelLines(lines);
24
+
25
+ assert.equal(normalized.length, 1);
26
+ assert.equal(stripAnsi(normalized[0]), "OK");
27
+ assert.match(normalized[0], /\x1b\[32m/);
28
+ });
29
+
30
+ test("removes pi-tui cursor markers from custom panel output", async () => {
31
+ const { normalizeCustomPanelLines } = await loadSubject();
32
+
33
+ assert.deepEqual(normalizeCustomPanelLines(["> value\x1b_pi:c\x07"]), ["> value"]);
34
+ });
35
+
36
+ test("parses ANSI style segments and reset codes", async () => {
37
+ const { parseAnsiLine } = await loadSubject();
38
+
39
+ assert.deepEqual(parseAnsiLine("\x1b[31;1mhot\x1b[0m cold"), [
40
+ { text: "hot", style: { color: "#dc2626", fontWeight: 700 } },
41
+ { text: " cold", style: {} },
42
+ ]);
43
+ });
44
+
45
+ test("maps 256-color SGR codes", async () => {
46
+ const { ansi256Color, parseAnsiLine } = await loadSubject();
47
+
48
+ assert.equal(ansi256Color(196), "rgb(255, 0, 0)");
49
+ assert.deepEqual(parseAnsiLine("\x1b[38;5;196mred"), [
50
+ { text: "red", style: { color: "rgb(255, 0, 0)" } },
51
+ ]);
52
+ });
package/lib/ansi.ts ADDED
@@ -0,0 +1,211 @@
1
+ import type { CSSProperties } from "react";
2
+
3
+ const ANSI_ESCAPE_RE = /\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~]|\][^\x07]*(?:\x07|\x1B\\))/g;
4
+ const ANSI_ESCAPE_AT_START_RE = /^\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~]|\][^\x07]*(?:\x07|\x1B\\))/;
5
+ const ANSI_SGR_RE = /\x1B\[([0-9;]*)m/g;
6
+ const TUI_CURSOR_MARKER_RE = /\x1B_pi:c\x07/g;
7
+
8
+ const ANSI_8_COLORS = [
9
+ "#1f2937",
10
+ "#dc2626",
11
+ "#16a34a",
12
+ "#d97706",
13
+ "#2563eb",
14
+ "#9333ea",
15
+ "#0891b2",
16
+ "#6b7280",
17
+ ];
18
+
19
+ const ANSI_BRIGHT_COLORS = [
20
+ "#9ca3af",
21
+ "#ef4444",
22
+ "#22c55e",
23
+ "#f59e0b",
24
+ "#3b82f6",
25
+ "#a855f7",
26
+ "#06b6d4",
27
+ "#e5e7eb",
28
+ ];
29
+
30
+ export interface AnsiSegment {
31
+ text: string;
32
+ style: CSSProperties;
33
+ }
34
+
35
+ export function stripAnsi(text: string): string {
36
+ return text.replace(TUI_CURSOR_MARKER_RE, "").replace(ANSI_ESCAPE_RE, "");
37
+ }
38
+
39
+ function visibleCharPositions(text: string): Array<{ start: number; end: number; char: string }> {
40
+ const positions: Array<{ start: number; end: number; char: string }> = [];
41
+ let i = 0;
42
+ while (i < text.length) {
43
+ if (text.charCodeAt(i) === 0x1b) {
44
+ const match = text.slice(i).match(ANSI_ESCAPE_AT_START_RE);
45
+ if (match) {
46
+ i += match[0].length;
47
+ continue;
48
+ }
49
+ }
50
+ const codePoint = text.codePointAt(i);
51
+ if (codePoint === undefined) break;
52
+ const char = String.fromCodePoint(codePoint);
53
+ positions.push({ start: i, end: i + char.length, char });
54
+ i += char.length;
55
+ }
56
+ return positions;
57
+ }
58
+
59
+ function removeVisibleCharAt(text: string, index: number): string {
60
+ const positions = visibleCharPositions(text);
61
+ const pos = positions[index];
62
+ if (!pos) return text;
63
+ return text.slice(0, pos.start) + text.slice(pos.end);
64
+ }
65
+
66
+ function firstVisibleChar(text: string): string | undefined {
67
+ return visibleCharPositions(text)[0]?.char;
68
+ }
69
+
70
+ function lastNonSpaceVisibleCharIndex(text: string): number {
71
+ const positions = visibleCharPositions(text);
72
+ for (let i = positions.length - 1; i >= 0; i--) {
73
+ if (positions[i].char.trim() !== "") return i;
74
+ }
75
+ return -1;
76
+ }
77
+
78
+ function trimEndVisibleSpaces(text: string): string {
79
+ let next = text;
80
+ while (true) {
81
+ const positions = visibleCharPositions(next);
82
+ const last = positions[positions.length - 1];
83
+ if (!last || last.char.trim() !== "") return next;
84
+ next = next.slice(0, last.start) + next.slice(last.end);
85
+ }
86
+ }
87
+
88
+ export function normalizeCustomPanelLines(lines: string[]): string[] {
89
+ const horizontalFrameLine = /^[┌├└╭╰][─┬┴┼]+[┐┤┘╮╯]$/;
90
+ const normalized: string[] = [];
91
+
92
+ for (const rawLine of lines) {
93
+ const lineWithoutCursor = rawLine.replace(TUI_CURSOR_MARKER_RE, "");
94
+ const plain = stripAnsi(lineWithoutCursor).trimEnd();
95
+ if (horizontalFrameLine.test(plain)) continue;
96
+
97
+ let line = lineWithoutCursor;
98
+ const first = firstVisibleChar(line);
99
+ if (first === "│" || first === "┃") {
100
+ line = removeVisibleCharAt(line, 0);
101
+ if (firstVisibleChar(line) === " ") line = removeVisibleCharAt(line, 0);
102
+ }
103
+
104
+ const rightBorderIndex = lastNonSpaceVisibleCharIndex(line);
105
+ const rightBorder = rightBorderIndex >= 0 ? visibleCharPositions(line)[rightBorderIndex]?.char : undefined;
106
+ if (rightBorder === "│" || rightBorder === "┃") {
107
+ line = removeVisibleCharAt(line, rightBorderIndex);
108
+ }
109
+
110
+ normalized.push(trimEndVisibleSpaces(line));
111
+ }
112
+
113
+ while (normalized.length > 0 && stripAnsi(normalized[0]).trim() === "") normalized.shift();
114
+ while (normalized.length > 0 && stripAnsi(normalized[normalized.length - 1]).trim() === "") normalized.pop();
115
+ return normalized.length ? normalized : lines;
116
+ }
117
+
118
+ export function ansi256Color(index: number): string | undefined {
119
+ if (index >= 0 && index < 8) return ANSI_8_COLORS[index];
120
+ if (index >= 8 && index < 16) return ANSI_BRIGHT_COLORS[index - 8];
121
+ if (index >= 16 && index <= 231) {
122
+ const n = index - 16;
123
+ const r = Math.floor(n / 36);
124
+ const g = Math.floor((n % 36) / 6);
125
+ const b = n % 6;
126
+ const scale = (v: number) => v === 0 ? 0 : 55 + v * 40;
127
+ return `rgb(${scale(r)}, ${scale(g)}, ${scale(b)})`;
128
+ }
129
+ if (index >= 232 && index <= 255) {
130
+ const gray = 8 + (index - 232) * 10;
131
+ return `rgb(${gray}, ${gray}, ${gray})`;
132
+ }
133
+ return undefined;
134
+ }
135
+
136
+ function applyAnsiCodes(style: CSSProperties, codes: number[]): CSSProperties {
137
+ const next: CSSProperties = { ...style };
138
+ for (let i = 0; i < codes.length; i++) {
139
+ const code = codes[i];
140
+ if (code === 0) {
141
+ for (const key of Object.keys(next) as Array<keyof CSSProperties>) delete next[key];
142
+ } else if (code === 1) {
143
+ next.fontWeight = 700;
144
+ } else if (code === 2) {
145
+ next.opacity = 0.65;
146
+ } else if (code === 3) {
147
+ next.fontStyle = "italic";
148
+ } else if (code === 4) {
149
+ next.textDecoration = "underline";
150
+ } else if (code === 22) {
151
+ delete next.fontWeight;
152
+ delete next.opacity;
153
+ } else if (code === 23) {
154
+ delete next.fontStyle;
155
+ } else if (code === 24) {
156
+ delete next.textDecoration;
157
+ } else if (code === 39) {
158
+ delete next.color;
159
+ } else if (code === 49) {
160
+ delete next.backgroundColor;
161
+ } else if (code >= 30 && code <= 37) {
162
+ next.color = ANSI_8_COLORS[code - 30];
163
+ } else if (code >= 90 && code <= 97) {
164
+ next.color = ANSI_BRIGHT_COLORS[code - 90];
165
+ } else if (code >= 40 && code <= 47) {
166
+ next.backgroundColor = ANSI_8_COLORS[code - 40];
167
+ } else if (code >= 100 && code <= 107) {
168
+ next.backgroundColor = ANSI_BRIGHT_COLORS[code - 100];
169
+ } else if ((code === 38 || code === 48) && codes[i + 1] === 2) {
170
+ const [r, g, b] = [codes[i + 2], codes[i + 3], codes[i + 4]];
171
+ if ([r, g, b].every((value) => typeof value === "number" && Number.isFinite(value))) {
172
+ if (code === 38) next.color = `rgb(${r}, ${g}, ${b})`;
173
+ else next.backgroundColor = `rgb(${r}, ${g}, ${b})`;
174
+ }
175
+ i += 4;
176
+ } else if ((code === 38 || code === 48) && codes[i + 1] === 5) {
177
+ const color = ansi256Color(codes[i + 2]);
178
+ if (color) {
179
+ if (code === 38) next.color = color;
180
+ else next.backgroundColor = color;
181
+ }
182
+ i += 2;
183
+ }
184
+ }
185
+ return next;
186
+ }
187
+
188
+ export function parseAnsiLine(line: string): AnsiSegment[] {
189
+ const segments: AnsiSegment[] = [];
190
+ let style: CSSProperties = {};
191
+ let lastIndex = 0;
192
+ let match: RegExpExecArray | null;
193
+ ANSI_SGR_RE.lastIndex = 0;
194
+
195
+ while ((match = ANSI_SGR_RE.exec(line)) !== null) {
196
+ if (match.index > lastIndex) {
197
+ segments.push({ text: line.slice(lastIndex, match.index), style });
198
+ }
199
+ const codes = match[1]
200
+ ? match[1].split(";").map((part) => Number(part || "0"))
201
+ : [0];
202
+ style = applyAnsiCodes(style, codes);
203
+ lastIndex = match.index + match[0].length;
204
+ }
205
+
206
+ if (lastIndex < line.length) {
207
+ segments.push({ text: line.slice(lastIndex), style });
208
+ }
209
+
210
+ return segments;
211
+ }
@@ -0,0 +1,139 @@
1
+ import type { ResourceDiagnostic } from "@earendil-works/pi-coding-agent";
2
+
3
+ export interface SkillSearchResult {
4
+ package: string;
5
+ installs: string;
6
+ url: string;
7
+ }
8
+
9
+ export type SkillInstallScope = "global" | "project";
10
+
11
+ export interface SkillInstallInfo {
12
+ package: string;
13
+ scope: SkillInstallScope;
14
+ source: string;
15
+ sourceType?: string;
16
+ skillsShUrl?: string;
17
+ skillPath?: string;
18
+ ref?: string;
19
+ versionHash?: string;
20
+ canCheckForUpdates: boolean;
21
+ }
22
+
23
+ export type SkillUpdateState =
24
+ | "up-to-date"
25
+ | "update-available"
26
+ | "unsupported"
27
+ | "error";
28
+
29
+ export interface SkillUpdateResult {
30
+ package: string;
31
+ scope: SkillInstallScope;
32
+ state: SkillUpdateState;
33
+ currentVersion?: string;
34
+ latestVersion?: string;
35
+ message?: string;
36
+ }
37
+
38
+ export interface SkillInfo {
39
+ name: string;
40
+ description: string;
41
+ filePath: string;
42
+ baseDir: string;
43
+ disableModelInvocation: boolean;
44
+ sourceInfo: {
45
+ source?: string;
46
+ scope?: string;
47
+ };
48
+ install?: SkillInstallInfo;
49
+ }
50
+
51
+ export interface SkillsResponse {
52
+ skills: SkillInfo[];
53
+ diagnostics: ResourceDiagnostic[];
54
+ projectResourcesLoaded: boolean;
55
+ }
56
+
57
+ export interface ProjectTrustStatus {
58
+ requiresTrust: boolean;
59
+ trusted: boolean;
60
+ }
61
+
62
+ export interface AppUpdateResponse {
63
+ currentVersion: string;
64
+ latestVersion: string;
65
+ updateAvailable: boolean;
66
+ releaseUrl: string;
67
+ }
68
+
69
+ export type PluginScope = "global" | "project";
70
+ export type PluginResourceKind = "extension" | "skill" | "prompt" | "theme";
71
+
72
+ export interface PluginResourceCounts {
73
+ extensions: number;
74
+ skills: number;
75
+ prompts: number;
76
+ themes: number;
77
+ }
78
+
79
+ export interface PluginDiagnostic {
80
+ type: "warning" | "error";
81
+ message: string;
82
+ source?: string;
83
+ path?: string;
84
+ }
85
+
86
+ export interface PluginResourceInfo {
87
+ kind: PluginResourceKind;
88
+ name: string;
89
+ path: string;
90
+ relativePath: string;
91
+ }
92
+
93
+ export interface PluginPackageInfo {
94
+ source: string;
95
+ scope: PluginScope;
96
+ filtered: boolean;
97
+ disabled: boolean;
98
+ installedPath?: string;
99
+ packageName?: string;
100
+ version?: string;
101
+ configuredVersion?: string;
102
+ counts: PluginResourceCounts;
103
+ resources: PluginResourceInfo[];
104
+ status: "loaded" | "installed" | "missing" | "disabled";
105
+ }
106
+
107
+ export interface PluginsResponse {
108
+ packages: PluginPackageInfo[];
109
+ totals: PluginResourceCounts;
110
+ diagnostics: PluginDiagnostic[];
111
+ projectResourcesLoaded: boolean;
112
+ }
113
+
114
+ export type McpScope = "global" | "project";
115
+
116
+ export interface McpServerInfo {
117
+ name: string;
118
+ scope: McpScope;
119
+ disabled: boolean;
120
+ kind: "command" | "url" | "socket";
121
+ command?: string;
122
+ args: string[];
123
+ url?: string;
124
+ socket?: string;
125
+ /** 环境变量名列表(不含值,避免泄露) */
126
+ envKeys: string[];
127
+ /** Remaining server options (lifecycle / directTools / timeout, etc.) */
128
+ options: Record<string, unknown>;
129
+ /** Path of the config file that defines this server */
130
+ source: string;
131
+ }
132
+
133
+ export interface McpResponse {
134
+ servers: McpServerInfo[];
135
+ /** Settings merged from global + project */
136
+ settings: Record<string, unknown>;
137
+ diagnostics: string[];
138
+ projectResourcesLoaded: boolean;
139
+ }
@@ -0,0 +1,27 @@
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);
6
+ const { getPiWebReleaseUrl, isNewerStableVersion } = await jiti.import("./app-update.ts");
7
+
8
+ test("detects newer stable Pi Web versions", () => {
9
+ assert.equal(isNewerStableVersion("0.8.8", "0.8.7"), true);
10
+ assert.equal(isNewerStableVersion("0.9.0", "0.8.7"), true);
11
+ assert.equal(isNewerStableVersion("1.0.0", "0.9.9"), true);
12
+ });
13
+
14
+ test("does not report equal, older, or unsupported versions as updates", () => {
15
+ assert.equal(isNewerStableVersion("0.8.7", "0.8.7"), false);
16
+ assert.equal(isNewerStableVersion("0.8.6", "0.8.7"), false);
17
+ assert.equal(isNewerStableVersion("0.8.8-beta.1", "0.8.7"), false);
18
+ assert.equal(isNewerStableVersion("invalid", "0.8.7"), false);
19
+ });
20
+
21
+ test("builds a release-notes URL only for stable versions", () => {
22
+ assert.equal(
23
+ getPiWebReleaseUrl("0.8.8"),
24
+ "https://github.com/agegr/pi-web/releases/tag/v0.8.8",
25
+ );
26
+ assert.equal(getPiWebReleaseUrl("0.8.8-beta.1"), null);
27
+ });
@@ -0,0 +1,28 @@
1
+ const STABLE_VERSION_PATTERN = /^(\d+)\.(\d+)\.(\d+)$/;
2
+
3
+ function parseStableVersion(version: string): [number, number, number] | null {
4
+ const match = STABLE_VERSION_PATTERN.exec(version);
5
+ if (!match) return null;
6
+
7
+ const parts = match.slice(1).map(Number);
8
+ if (parts.some((part) => !Number.isSafeInteger(part))) return null;
9
+ return parts as [number, number, number];
10
+ }
11
+
12
+ export function isNewerStableVersion(candidate: string, current: string): boolean {
13
+ const candidateParts = parseStableVersion(candidate);
14
+ const currentParts = parseStableVersion(current);
15
+ if (!candidateParts || !currentParts) return false;
16
+
17
+ for (let index = 0; index < candidateParts.length; index += 1) {
18
+ if (candidateParts[index] !== currentParts[index]) {
19
+ return candidateParts[index] > currentParts[index];
20
+ }
21
+ }
22
+ return false;
23
+ }
24
+
25
+ export function getPiWebReleaseUrl(version: string): string | null {
26
+ if (!parseStableVersion(version)) return null;
27
+ return `https://github.com/agegr/pi-web/releases/tag/v${version}`;
28
+ }
@@ -0,0 +1,37 @@
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
+ const { writePrivateFileAtomicSync } = await import("./atomic-file.ts");
8
+
9
+ function createTempRoot(t) {
10
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-web-atomic-file-"));
11
+ t.after(() => fs.rmSync(root, { recursive: true, force: true }));
12
+ return root;
13
+ }
14
+
15
+ test("atomically replaces a file with restrictive permissions", (t) => {
16
+ const root = createTempRoot(t);
17
+ const destination = path.join(root, "models.json");
18
+ fs.writeFileSync(destination, "old", { mode: 0o644 });
19
+
20
+ writePrivateFileAtomicSync(destination, "new");
21
+
22
+ assert.equal(fs.readFileSync(destination, "utf8"), "new");
23
+ assert.deepEqual(fs.readdirSync(root), ["models.json"]);
24
+ if (process.platform !== "win32") {
25
+ assert.equal(fs.statSync(destination).mode & 0o777, 0o600);
26
+ }
27
+ });
28
+
29
+ test("keeps the destination and removes the temporary file when replacement fails", (t) => {
30
+ const root = createTempRoot(t);
31
+ const destination = path.join(root, "models.json");
32
+ fs.mkdirSync(destination);
33
+
34
+ assert.throws(() => writePrivateFileAtomicSync(destination, "new"));
35
+ assert.equal(fs.statSync(destination).isDirectory(), true);
36
+ assert.deepEqual(fs.readdirSync(root), ["models.json"]);
37
+ });
@@ -0,0 +1,34 @@
1
+ import { randomUUID } from "crypto";
2
+ import { renameSync, unlinkSync, writeFileSync } from "fs";
3
+ import { basename, dirname, join } from "path";
4
+
5
+ /**
6
+ * Replace a file atomically without exposing credentials through default
7
+ * process permissions. The caller must create the parent directory first.
8
+ */
9
+ export function writePrivateFileAtomicSync(path: string, contents: string): void {
10
+ const dir = dirname(path);
11
+ const tempPath = join(dir, `.${basename(path)}-${randomUUID()}.tmp`);
12
+ let operationFailed = false;
13
+
14
+ try {
15
+ writeFileSync(tempPath, contents, {
16
+ encoding: "utf8",
17
+ flag: "wx",
18
+ mode: 0o600,
19
+ flush: true,
20
+ });
21
+ renameSync(tempPath, path);
22
+ } catch (error) {
23
+ operationFailed = true;
24
+ throw error;
25
+ } finally {
26
+ try {
27
+ unlinkSync(tempPath);
28
+ } catch (error) {
29
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT" && !operationFailed) {
30
+ throw error;
31
+ }
32
+ }
33
+ }
34
+ }