pi-web-ui 0.29.0 → 0.29.2

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();
package/package.json CHANGED
@@ -1,96 +1,96 @@
1
- {
2
- "name": "pi-web-ui",
3
- "version": "0.29.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.83.0",
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.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
+ }