pi-web-ui 0.29.1 → 0.29.3

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.
@@ -109,6 +109,8 @@ export async function saveCommandsFile(workspaceRoot, commands) {
109
109
  }
110
110
  const isWindows = process.platform === "win32";
111
111
  const MAX_TERMINALS = 16;
112
+ /** Coalescing window for terminal_output WS messages (one animation frame). */
113
+ const OUTPUT_FLUSH_MS = 16;
112
114
  const MAX_TERMINAL_HISTORY = 32;
113
115
  const MAX_OUTPUT = 200_000;
114
116
  const MAX_INPUT = 64 * 1024;
@@ -429,6 +431,7 @@ export class TerminalManager {
429
431
  // process group) and start a fresh shell with the same id. Keep the
430
432
  // last known size so the replacement matches the xterm's dimensions.
431
433
  if (!existing.exited) {
434
+ this.flushPending(existing);
432
435
  existing.exited = true;
433
436
  try {
434
437
  existing.pty.kill();
@@ -509,6 +512,8 @@ export class TerminalManager {
509
512
  output: "",
510
513
  outputOffset: 0,
511
514
  waiters: new Set(),
515
+ pendingOut: "",
516
+ flushTimer: null,
512
517
  };
513
518
  this.terms.set(id, entry);
514
519
  // The closures capture `entry`: after a restart the map points at the
@@ -517,7 +522,13 @@ export class TerminalManager {
517
522
  if (this.terms.get(id) !== entry)
518
523
  return;
519
524
  this.appendOutput(entry, data);
520
- this.writeOut(id, data);
525
+ // Coalesce instead of emitting per chunk: node-pty onData fires per
526
+ // ConPTY read buffer (dozens of bytes to a few KB each), so a build or
527
+ // `cat` of a big file used to produce hundreds/thousands of WS frames
528
+ // per second — each paying stringify + frame + parse + dispatch cost.
529
+ // A one-frame micro-batch cuts the frame rate 10-50× at ≤16ms latency
530
+ // (imperceptible; xterm writes batch data more efficiently anyway).
531
+ this.queueOut(entry, data);
521
532
  });
522
533
  pty.onExit(({ exitCode }) => {
523
534
  if (this.terms.get(id) !== entry)
@@ -526,12 +537,33 @@ export class TerminalManager {
526
537
  });
527
538
  return true;
528
539
  }
540
+ /** Emit output immediately, bypassing the coalescing window (rare paths:
541
+ * one-shot hints/banners — not per-chunk data). */
529
542
  writeOut(id, data) {
530
- const entry = this.terms.get(id);
531
- if (!entry)
532
- return;
533
543
  this.emit({ type: "terminal_output", terminalId: id, data });
534
544
  }
545
+ /** Queue output for the coalescing window; flushes via flushPending. */
546
+ queueOut(entry, data) {
547
+ entry.pendingOut += data;
548
+ if (entry.flushTimer)
549
+ return;
550
+ entry.flushTimer = setTimeout(() => {
551
+ entry.flushTimer = null;
552
+ this.flushPending(entry);
553
+ }, OUTPUT_FLUSH_MS);
554
+ }
555
+ /** Emit everything queued for this terminal (no-op when nothing pending). */
556
+ flushPending(entry) {
557
+ if (entry.flushTimer) {
558
+ clearTimeout(entry.flushTimer);
559
+ entry.flushTimer = null;
560
+ }
561
+ if (!entry.pendingOut)
562
+ return;
563
+ const pending = entry.pendingOut;
564
+ entry.pendingOut = "";
565
+ this.emit({ type: "terminal_output", terminalId: entry.id, data: pending });
566
+ }
535
567
  appendOutput(entry, data) {
536
568
  entry.output += data;
537
569
  if (entry.output.length > MAX_OUTPUT) {
@@ -668,9 +700,11 @@ export class TerminalManager {
668
700
  const entry = this.terms.get(id);
669
701
  if (!entry || entry.exited)
670
702
  return;
703
+ // Flush queued output BEFORE the exit banner so ordering is preserved.
704
+ this.flushPending(entry);
671
705
  const banner = `\r\n\x1b[90m[进程已退出,退出码 ${exitCode}]\x1b[0m\r\n`;
672
706
  this.appendOutput(entry, banner);
673
- this.writeOut(id, banner);
707
+ this.emit({ type: "terminal_output", terminalId: id, data: banner });
674
708
  entry.exited = true;
675
709
  entry.exitCode = exitCode;
676
710
  this.terms.delete(id);
@@ -705,6 +739,7 @@ export class TerminalManager {
705
739
  kill(id) {
706
740
  const entry = this.terms.get(id);
707
741
  if (entry) {
742
+ this.flushPending(entry);
708
743
  entry.exited = true;
709
744
  try {
710
745
  entry.pty.kill();
@@ -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.1",
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.29.3",
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
+ }