pi-web-ui 0.29.2 → 0.30.0

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.
@@ -22,7 +22,7 @@ import { GoalService } from "./goal-service.js";
22
22
  import { SlashCommandsService, parseSlash } from "./slash-commands.js";
23
23
  import { ModelAdminService } from "./model-admin.js";
24
24
  import { FilesService, workspacePath } from "./files-service.js";
25
- import { extensionKey, ClientStateStore, } from "./client-state.js";
25
+ import { isExtensionDisabled, ClientStateStore, } from "./client-state.js";
26
26
  import { saveUpload } from "./uploads.js";
27
27
  import { makePersistentTerminalTools } from "./terminals.js";
28
28
  import { WebUIContext } from "./webui-context.js";
@@ -528,9 +528,11 @@ export class ClientSession {
528
528
  skills: res.skills.filter((s) => !this.settingsSvc.current.disabledSkills.includes(s.name)),
529
529
  }),
530
530
  // 插件开关:禁用的扩展整个卸载(工具 / 命令随之消失)。
531
+ // 注意 SDK 在 extensionsOverride 之后才补 sourceInfo,包扩展此处只能靠路径
532
+ // 匹配 —— isExtensionDisabled 同时比对 npm:<pkg> 候选键。
531
533
  extensionsOverride: (res) => ({
532
534
  ...res,
533
- extensions: res.extensions.filter((e) => !this.settingsSvc.current.disabledExtensions.includes(extensionKey(e))),
535
+ extensions: res.extensions.filter((e) => !isExtensionDisabled(e, this.settingsSvc.current.disabledExtensions)),
534
536
  }),
535
537
  },
536
538
  });
@@ -17,6 +17,35 @@ export function extensionKey(e) {
17
17
  return src.source;
18
18
  return src?.path ?? e.path;
19
19
  }
20
+ /** All identities an extension may be disabled by. The SDK applies
21
+ * `sourceInfo` only AFTER extensionsOverride runs (resource-loader reload():
22
+ * override first, applyExtensionSourceInfo second), so inside the override a
23
+ * package extension still has no sourceInfo and extensionKey() falls back to
24
+ * the raw entry path — which never matches the "npm:<pkg>" id the settings
25
+ * panel stores. Derive the package name from the entry path
26
+ * (.../node_modules/<pkg>/... or .../node_modules/@scope/<pkg>/...) so both
27
+ * sides agree. */
28
+ export function extensionKeyCandidates(e) {
29
+ const keys = new Set([extensionKey(e)]);
30
+ const norm = e.path.replace(/\\/g, "/");
31
+ const marker = "/node_modules/";
32
+ const idx = norm.lastIndexOf(marker);
33
+ if (idx !== -1) {
34
+ const segs = norm.slice(idx + marker.length).split("/");
35
+ // Scoped package @scope/name spans two segments.
36
+ const name = segs[0]?.startsWith("@") && segs[1] ? `${segs[0]}/${segs[1]}` : segs[0];
37
+ if (name)
38
+ keys.add(`npm:${name}`);
39
+ }
40
+ return [...keys];
41
+ }
42
+ /** Whether an extension is covered by the disabled list (any identity match). */
43
+ export function isExtensionDisabled(e, disabled) {
44
+ if (disabled.length === 0)
45
+ return false;
46
+ const keys = extensionKeyCandidates(e);
47
+ return disabled.some((d) => keys.includes(d));
48
+ }
20
49
  /**
21
50
  * Persists which workspace each browser client last used + which workspaces it
22
51
  * has opened, so a server restart / page reload restores the same project and
@@ -405,9 +405,11 @@ function scheduleQuit() {
405
405
  return true;
406
406
  }
407
407
  service.onQuit = scheduleQuit;
408
- /** 背压阈值:socket 未发送积压超过此值时丢弃 snapshot(issue #11)。
409
- * 1MB 两三份全量 snapshot 的量,足够吸收网络抖动,又远低于 OOM 级堆积。 */
410
- const SNAPSHOT_BACKPRESSURE_BYTES = 1_000_000;
408
+ /** 背压相对倍数:socket 未发送积压超过「最近一份 snapshot 大小 × 此倍数」时丢弃
409
+ * (issue #11 及其评论区的自适应建议)。固定 1MB 阈值在长会话下单份 snapshot 可达
410
+ * ~10MB——连半份都没发完就丢,前端频繁跳帧;短会话又太迟钝。相对阈值语义稳定在
411
+ * 「缓冲堆了约 N 份快照」,不随会话长短漂移。 */
412
+ const SNAPSHOT_BACKPRESSURE_FACTOR = 3;
411
413
  /**
412
414
  * Multi-tab serialization sharing: emit() hands the SAME message object to
413
415
  * every socket of a client, but each send() used to JSON.stringify it
@@ -430,6 +432,8 @@ wss.on("connection", (ws) => {
430
432
  service.noteSocketOpen();
431
433
  let clientId = null;
432
434
  let closed = false;
435
+ /** 最近一份全量 snapshot 的估算字节数(UTF-16 ×2),供背压相对阈值用(issue #11)。 */
436
+ let lastSnapshotBytes = 0;
433
437
  /** Commands received while the session is still being created — replayed after attach. */
434
438
  let pending = [];
435
439
  // 协议层错误(非法帧/未 masked 帧等):不注册 handler 会作为 uncaught
@@ -447,14 +451,20 @@ wss.on("connection", (ws) => {
447
451
  if (closed || ws.readyState !== WebSocket.OPEN)
448
452
  return;
449
453
  // 发送背压(issue #11):socket 消费不过来时(前端慢/网络差),堆里会堆积
450
- // 每份 ~10MB 的全量 snapshot 字符串,低内存主机直接 OOM。snapshot 是全量
454
+ // 每份可达 ~10MB 的全量 snapshot 字符串,低内存主机直接 OOM。snapshot 是全量
451
455
  // 幂等的且 60ms 后必有更新的一份,可以安全丢弃——在序列化之前丢,连
452
456
  // stringify 的分配都省掉。ready/notice/error/tool_delta 等消息必须送达。
457
+ // 阈值相对化(评论区建议):用「最近一份 snapshot 的字节数 × 倍数」做基准,
458
+ // 首份无基准不丢(首次必达)。wire.length 是 UTF-16 字符数,×2 估算字节。
453
459
  if ((msg.type === "snapshot" || msg.type === "snapshot_delta") &&
454
- ws.bufferedAmount > SNAPSHOT_BACKPRESSURE_BYTES) {
460
+ lastSnapshotBytes > 0 &&
461
+ ws.bufferedAmount > SNAPSHOT_BACKPRESSURE_FACTOR * lastSnapshotBytes) {
455
462
  return;
456
463
  }
457
- ws.send(serializeShared(msg));
464
+ const wire = serializeShared(msg);
465
+ if (msg.type === "snapshot")
466
+ lastSnapshotBytes = wire.length * 2;
467
+ ws.send(wire);
458
468
  };
459
469
  const dispatch = (msg) => {
460
470
  if (!clientId) {
@@ -1,4 +1,11 @@
1
1
  const WIDGET_WIDTH = 80;
2
+ /** ANSI 转义序列(CSI + OSC 两类):扩展 widget/status 文本里常混有 TUI 颜色码
3
+ * (如 pi-powerline-footer),浏览器会把它渲染成字面 `[38;5;244m` 乱码(issue #16)。 */
4
+ const ANSI_RE = /\[[0-9:;<=>?]*[ -/]*[@-~]|\][^\u0007\u001b]*(?:\u0007|\u001b\\)/g;
5
+ /** Strip all ANSI escape sequences (CSI/OSC) from a string. */
6
+ export function stripAnsi(s) {
7
+ return s.replace(ANSI_RE, "");
8
+ }
2
9
  /** Mock theme: TUI color functions degrade to identity so widget text survives. */
3
10
  const mockTheme = new Proxy({
4
11
  fg: (_color, text) => text,
@@ -101,8 +108,10 @@ export class WebUIContext {
101
108
  catch {
102
109
  lines = undefined;
103
110
  }
104
- this.lastLines.set(key, lines ?? []);
105
- return { key, lines: lines ?? [] };
111
+ // 浏览器不是终端:ANSI 颜色码剥掉再下发/比对(issue #16)。
112
+ const clean = lines?.map(stripAnsi) ?? undefined;
113
+ this.lastLines.set(key, clean ?? []);
114
+ return { key, lines: clean ?? [] };
106
115
  });
107
116
  }
108
117
  // -- notifications --------------------------------------------------------
@@ -116,7 +125,12 @@ export class WebUIContext {
116
125
  this.statuses.delete(key);
117
126
  }
118
127
  else {
119
- this.statuses.set(key, text);
128
+ // 入口处剥 ANSI:pushStatuses / statusSnapshot 两条路径都拿到干净文本。
129
+ const clean = stripAnsi(text);
130
+ if (clean === "")
131
+ this.statuses.delete(key);
132
+ else
133
+ this.statuses.set(key, clean);
120
134
  }
121
135
  this.pushStatuses();
122
136
  }
package/package.json CHANGED
@@ -1,96 +1,96 @@
1
- {
2
- "name": "pi-web-ui",
3
- "version": "0.29.2",
4
- "description": "Web chat interface for the pi coding agent, powered by the pi SDK (@earendil-works/pi-coding-agent) — one-command run, Docker/systemd/launchd deployable",
5
- "license": "MIT",
6
- "type": "module",
7
- "bin": {
8
- "pi-web-ui": "bin/pi-web-ui.mjs"
9
- },
10
- "main": "dist/server/index.js",
11
- "files": [
12
- "bin/",
13
- "dist/",
14
- "web/dist/",
15
- "web/public/",
16
- "themes/",
17
- "deploy/",
18
- "extensions/",
19
- "README.md",
20
- "LICENSE"
21
- ],
22
- "keywords": [
23
- "pi",
24
- "pi-package",
25
- "coding-agent",
26
- "ai",
27
- "chat",
28
- "web-ui",
29
- "terminal",
30
- "llm"
31
- ],
32
- "repository": {
33
- "type": "git",
34
- "url": "git+https://github.com/xing-shuyin/pi-web-ui.git"
35
- },
36
- "pi": {
37
- "extensions": [
38
- "./extensions"
39
- ]
40
- },
41
- "engines": {
42
- "node": ">=22.19.0"
43
- },
44
- "scripts": {
45
- "prepublishOnly": "npm run build",
46
- "dev": "concurrently -k -n server,web -c blue,green \"npm:dev:server\" \"npm:dev:web\"",
47
- "dev:server": "cross-env PORT=8788 PI_WEB_ALLOW_ORIGINS=http://localhost:5173,http://127.0.0.1:5173 node --watch --import tsx server/index.ts",
48
- "dev:web": "vite --config web/vite.config.ts",
49
- "build": "npm run build:web && npm run build:server",
50
- "build:web": "vite build --config web/vite.config.ts",
51
- "build:server": "tsc -p tsconfig.server.json",
52
- "typecheck": "tsc -p tsconfig.server.json --noEmit && tsc -p web/tsconfig.json --noEmit && tsc -p tsconfig.tests.json --noEmit",
53
- "test": "vitest run",
54
- "test:unit": "vitest run",
55
- "test:smoke": "node tests/run-smoke.mjs",
56
- "check:protocol": "node scripts/check-protocol-sync.mjs",
57
- "test:freeze": "node tests/freeze-test.mjs",
58
- "start": "node dist/server/index.js"
59
- },
60
- "dependencies": {
61
- "@earendil-works/pi-coding-agent": "^0.84.2",
62
- "@xterm/addon-fit": "^0.11.0",
63
- "@xterm/xterm": "^6.0.0",
64
- "compression": "^1.8.1",
65
- "express": "^4.21.2",
66
- "highlight.js": "^11.10.0",
67
- "node-pty": "^1.1.0",
68
- "react": "^18.3.1",
69
- "react-dom": "^18.3.1",
70
- "react-icons": "^5.7.0",
71
- "react-markdown": "^9.0.1",
72
- "rehype-highlight": "^7.0.1",
73
- "remark-gfm": "^4.0.0",
74
- "typebox": "^1.3.14",
75
- "ws": "^8.18.0"
76
- },
77
- "devDependencies": {
78
- "@types/compression": "^1.8.1",
79
- "@types/express": "^4.17.21",
80
- "@types/node": "^22.10.0",
81
- "@types/react": "^18.3.12",
82
- "@types/react-dom": "^18.3.1",
83
- "@types/ws": "^8.5.13",
84
- "@vitejs/plugin-react": "^4.3.4",
85
- "concurrently": "^9.1.0",
86
- "cross-env": "^10.1.0",
87
- "playwright-core": "^1.62.1",
88
- "tsx": "^4.19.2",
89
- "typescript": "^5.7.2",
90
- "vite": "^6.0.3",
91
- "vitest": "^4.1.11"
92
- },
93
- "allowScripts": {
94
- "node-pty@1.1.0": true
95
- }
96
- }
1
+ {
2
+ "name": "pi-web-ui",
3
+ "version": "0.30.0",
4
+ "description": "Web chat interface for the pi coding agent, powered by the pi SDK (@earendil-works/pi-coding-agent) — one-command run, Docker/systemd/launchd deployable",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "bin": {
8
+ "pi-web-ui": "bin/pi-web-ui.mjs"
9
+ },
10
+ "main": "dist/server/index.js",
11
+ "files": [
12
+ "bin/",
13
+ "dist/",
14
+ "web/dist/",
15
+ "web/public/",
16
+ "themes/",
17
+ "deploy/",
18
+ "extensions/",
19
+ "README.md",
20
+ "LICENSE"
21
+ ],
22
+ "keywords": [
23
+ "pi",
24
+ "pi-package",
25
+ "coding-agent",
26
+ "ai",
27
+ "chat",
28
+ "web-ui",
29
+ "terminal",
30
+ "llm"
31
+ ],
32
+ "repository": {
33
+ "type": "git",
34
+ "url": "git+https://github.com/xing-shuyin/pi-web-ui.git"
35
+ },
36
+ "pi": {
37
+ "extensions": [
38
+ "./extensions"
39
+ ]
40
+ },
41
+ "engines": {
42
+ "node": ">=22.19.0"
43
+ },
44
+ "scripts": {
45
+ "prepublishOnly": "npm run build",
46
+ "dev": "concurrently -k -n server,web -c blue,green \"npm:dev:server\" \"npm:dev:web\"",
47
+ "dev:server": "cross-env PORT=8788 PI_WEB_ALLOW_ORIGINS=http://localhost:5173,http://127.0.0.1:5173 node --watch --import tsx server/index.ts",
48
+ "dev:web": "vite --config web/vite.config.ts",
49
+ "build": "npm run build:web && npm run build:server",
50
+ "build:web": "vite build --config web/vite.config.ts",
51
+ "build:server": "tsc -p tsconfig.server.json",
52
+ "typecheck": "tsc -p tsconfig.server.json --noEmit && tsc -p web/tsconfig.json --noEmit && tsc -p tsconfig.tests.json --noEmit",
53
+ "test": "vitest run",
54
+ "test:unit": "vitest run",
55
+ "test:smoke": "node tests/run-smoke.mjs",
56
+ "check:protocol": "node scripts/check-protocol-sync.mjs",
57
+ "test:freeze": "node tests/freeze-test.mjs",
58
+ "start": "node dist/server/index.js"
59
+ },
60
+ "dependencies": {
61
+ "@earendil-works/pi-coding-agent": "^0.84.2",
62
+ "@xterm/addon-fit": "^0.11.0",
63
+ "@xterm/xterm": "^6.0.0",
64
+ "compression": "^1.8.1",
65
+ "express": "^4.21.2",
66
+ "highlight.js": "^11.10.0",
67
+ "node-pty": "^1.1.0",
68
+ "react": "^18.3.1",
69
+ "react-dom": "^18.3.1",
70
+ "react-icons": "^5.7.0",
71
+ "react-markdown": "^9.0.1",
72
+ "rehype-highlight": "^7.0.1",
73
+ "remark-gfm": "^4.0.0",
74
+ "typebox": "^1.3.14",
75
+ "ws": "^8.18.0"
76
+ },
77
+ "devDependencies": {
78
+ "@types/compression": "^1.8.1",
79
+ "@types/express": "^4.17.21",
80
+ "@types/node": "^22.10.0",
81
+ "@types/react": "^18.3.12",
82
+ "@types/react-dom": "^18.3.1",
83
+ "@types/ws": "^8.5.13",
84
+ "@vitejs/plugin-react": "^4.3.4",
85
+ "concurrently": "^9.1.0",
86
+ "cross-env": "^10.1.0",
87
+ "playwright-core": "^1.62.1",
88
+ "tsx": "^4.19.2",
89
+ "typescript": "^5.7.2",
90
+ "vite": "^6.0.3",
91
+ "vitest": "^4.1.11"
92
+ },
93
+ "allowScripts": {
94
+ "node-pty@1.1.0": true
95
+ }
96
+ }