pi-web-ui 0.21.2 → 0.22.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.
package/README.md CHANGED
@@ -1,74 +1,84 @@
1
- # pi-web-ui
2
-
3
- **English** | [简体中文](https://github.com/xing-shuyin/pi-web-ui/blob/main/README.zh-CN.md)
4
-
5
- A web chat interface for the [pi coding agent](https://pi.dev) — the agent runs
6
- in-process via the pi SDK and streams events to the browser over WebSocket. Chat
7
- with thinking blocks and tool calls, attach files, ask about images, use a
8
- built-in terminal, manage models, and more. Requires Node.js 22.19 and a
9
- configured pi install.
10
-
11
- ## Screenshots
12
-
13
- ![pi-web-ui main interface](https://cdn.jsdelivr.net/gh/xing-shuyin/pi-web-ui@main/assets/shot.jpeg)
14
-
15
- ## Install
16
-
17
- ```bash
18
- npm i -g pi-web-ui # global install (recommended)
19
- npx pi-web-ui # or run without installing (latest, starts on :8787)
20
- npm i -g . # or install the local checkout
21
- ```
22
-
23
- ## Start
24
-
25
- ```bash
26
- pi-web-ui # foreground, http://localhost:8787
27
- PORT=9000 PI_WEB_CWD=/path/to/project pi-web-ui # custom port / workspace
28
- ```
29
-
30
- ## Stop
31
-
32
- - **Foreground**: press `Ctrl+C` in the terminal running it.
33
- - **As a service**: `pi-web-ui server stop` (stops the instance; auto-start stays until `server uninstall`).
34
-
35
- ## Update
36
-
37
- ```bash
38
- npm i -g pi-web-ui@latest # upgrade to the latest published version
39
- pi-web-ui server restart # restart the service to apply it (foreground: restart manually)
40
- ```
41
-
42
- ## Uninstall
43
-
44
- ```bash
45
- npm uninstall -g pi-web-ui
46
- ```
47
-
48
- Uninstalling does **not** delete your chats session data lives in
49
- `<cwd>/.pi-web` (or `PI_WEB_DATA_DIR`) and survives uninstall/upgrade.
50
-
51
- ## Run as a system service (auto-start on boot)
52
-
53
- ```bash
54
- pi-web-ui server install --port 9000 --cwd /path/to/project # install + start
55
- pi-web-ui server status # running? auto-start?
56
- pi-web-ui server restart # restart (applies config/version changes)
57
- pi-web-ui server stop # stop (auto-start stays)
58
- pi-web-ui server start # start again
59
- pi-web-ui server uninstall # remove the service entirely
60
- pi-web-ui server shortcut # desktop one-click launch icon
61
- ```
62
-
63
- - **macOS** → launchd agent (no sudo), logs to `/tmp/pi-web-ui.log` / `.err`
64
- - **Linux** systemd unit (`systemctl enable --now`), logs via `journalctl -u pi-web-ui -f`
65
- - **Windows** Task Scheduler logon task (hidden PowerShell window, no black console)
66
-
67
- Options: `--port` (default 8787), `--cwd` (workspace), `--data-dir` (sessions),
68
- `--name` (custom service name). Rerunning `server install` with new options
69
- regenerates the config and restarts the service — that's how you change its
70
- port/cwd.
71
-
72
- ## License
73
-
74
- MIT
1
+ # pi-web-ui
2
+
3
+ **English** | [简体中文](https://github.com/xing-shuyin/pi-web-ui/blob/main/README.zh-CN.md)
4
+
5
+ A web chat interface for the [pi coding agent](https://pi.dev) — the agent runs
6
+ in-process via the pi SDK and streams events to the browser over WebSocket. Chat
7
+ with thinking blocks and tool calls, attach files, ask about images, use a
8
+ built-in terminal, manage models, tweak the system prompt, toggle skills and
9
+ extensions on/off, and save/apply settings presets — all from a settings panel.
10
+ Requires Node.js ≥ 22.19 and a configured pi install.
11
+
12
+ ## Screenshots
13
+
14
+ ![pi-web-ui main interface](https://cdn.jsdelivr.net/gh/xing-shuyin/pi-web-ui@main/assets/shot.jpeg)
15
+
16
+ ## Install
17
+
18
+ ```bash
19
+ npm i -g pi-web-ui # global install (recommended)
20
+ npx pi-web-ui # or run without installing (latest, starts on :8787)
21
+ npm i -g . # or install the local checkout
22
+ ```
23
+
24
+ **npm ≥ 12?** npm 12+ blocks dependency install scripts by default (you'll see
25
+ `npm warn install-scripts … blocked`). node-pty is a native module, so allow its
26
+ script (the other two packages it lists are harmless no-ops allowing them just
27
+ silences the warning):
28
+
29
+ ```bash
30
+ npm i -g --allow-scripts=node-pty,@google/genai,protobufjs pi-web-ui@latest
31
+ ```
32
+
33
+ ## Start
34
+
35
+ ```bash
36
+ pi-web-ui # foreground, http://localhost:8787
37
+ PORT=9000 PI_WEB_CWD=/path/to/project pi-web-ui # custom port / workspace
38
+ ```
39
+
40
+ ## Stop
41
+
42
+ - **Foreground**: press `Ctrl+C` in the terminal running it.
43
+ - **As a service**: `pi-web-ui server stop` (stops the instance; auto-start stays until `server uninstall`).
44
+
45
+ ## Update
46
+
47
+ ```bash
48
+ npm i -g pi-web-ui@latest # upgrade to the latest published version
49
+ pi-web-ui server restart # restart the service to apply it (foreground: restart manually)
50
+ ```
51
+
52
+ ## Uninstall
53
+
54
+ ```bash
55
+ npm uninstall -g pi-web-ui
56
+ ```
57
+
58
+ Uninstalling does **not** delete your chats — session data lives in
59
+ `<cwd>/.pi-web` (or `PI_WEB_DATA_DIR`) and survives uninstall/upgrade.
60
+
61
+ ## Run as a system service (auto-start on boot)
62
+
63
+ ```bash
64
+ pi-web-ui server install --port 9000 --cwd /path/to/project # install + start
65
+ pi-web-ui server status # running? auto-start?
66
+ pi-web-ui server restart # restart (applies config/version changes)
67
+ pi-web-ui server stop # stop (auto-start stays)
68
+ pi-web-ui server start # start again
69
+ pi-web-ui server uninstall # remove the service entirely
70
+ pi-web-ui server shortcut # desktop one-click launch icon
71
+ ```
72
+
73
+ - **macOS** → launchd agent (no sudo), logs to `/tmp/pi-web-ui.log` / `.err`
74
+ - **Linux** → systemd unit (`systemctl enable --now`), logs via `journalctl -u pi-web-ui -f`
75
+ - **Windows** → Task Scheduler logon task (hidden PowerShell window, no black console)
76
+
77
+ Options: `--port` (default 8787), `--cwd` (workspace), `--data-dir` (sessions),
78
+ `--name` (custom service name). Rerunning `server install` with new options
79
+ regenerates the config and restarts the service — that's how you change its
80
+ port/cwd.
81
+
82
+ ## License
83
+
84
+ MIT
package/README.zh-CN.md CHANGED
@@ -1,71 +1,79 @@
1
- # pi-web-ui
2
-
3
- [English](https://github.com/xing-shuyin/pi-web-ui/blob/main/README.md) | **简体中文**
4
-
5
- [pi 编码智能体](https://pi.dev) 的 Web 聊天界面 —— 智能体通过 pi SDK 在服务端进程内运行,
6
- 事件经 WebSocket 流式推送到浏览器。支持思考块与工具调用、附件与图片问答、内置终端、
7
- 模型管理等功能。需要 Node.js ≥ 22.19 及配置好的 pi 环境。
8
-
9
- ## 界面截图
10
-
11
- ![pi-web-ui 主界面](https://cdn.jsdelivr.net/gh/xing-shuyin/pi-web-ui@main/assets/shot.jpeg)
12
-
13
- ## 安装
14
-
15
- ```bash
16
- npm i -g pi-web-ui # 全局安装(推荐)
17
- npx pi-web-ui # 或免安装直接跑(拉取最新版,启动在 :8787)
18
- npm i -g . # 或安装本地 checkout
19
- ```
20
-
21
- ## 启动
22
-
23
- ```bash
24
- pi-web-ui # 前台,http://localhost:8787
25
- PORT=9000 PI_WEB_CWD=/path/to/project pi-web-ui # 自定义端口 / 工作目录
26
- ```
27
-
28
- ## 停止
29
-
30
- - **前台**:在运行它的终端里按 `Ctrl+C`。
31
- - **作为服务**:`pi-web-ui server stop`(停止实例;开机自启保留,直到 `server uninstall`)。
32
-
33
- ## 更新
34
-
35
- ```bash
36
- npm i -g pi-web-ui@latest # 升级到最新发布版本
37
- pi-web-ui server restart # 重启服务使新版本生效(前台运行则手动重启)
38
- ```
39
-
40
- ## 卸载
41
-
42
- ```bash
43
- npm uninstall -g pi-web-ui
44
- ```
45
-
46
- 卸载**不会**删除你的聊天记录 —— 会话数据存放在 `<cwd>/.pi-web`(或 `PI_WEB_DATA_DIR`),
47
- 卸载/升级后依然保留。
48
-
49
- ## 作为系统服务(开机自启)
50
-
51
- ```bash
52
- pi-web-ui server install --port 9000 --cwd /path/to/project # 安装 + 启动
53
- pi-web-ui server status # 运行中?开机自启?
54
- pi-web-ui server restart # 重启(应用配置/版本变更)
55
- pi-web-ui server stop # 停止(开机自启保留)
56
- pi-web-ui server start # 再次启动
57
- pi-web-ui server uninstall # 彻底移除服务
58
- pi-web-ui server shortcut # 桌面一键启动图标
59
- ```
60
-
61
- - **macOS** → launchd 代理(无需 sudo),日志 `/tmp/pi-web-ui.log` / `.err`
62
- - **Linux** → systemd unit(`systemctl enable --now`),日志 `journalctl -u pi-web-ui -f`
63
- - **Windows** 计划任务(登录自启,隐藏 PowerShell 窗口,无黑窗)
64
-
65
- 选项:`--port`(默认 8787)、`--cwd`(工作目录)、`--data-dir`(会话目录)、
66
- `--name`(自定义服务名)。重复执行 `server install` 并传入新选项即可重新生成配置
67
- 并重启服务 —— 这就是修改已装服务端口/工作目录的方式。
68
-
69
- ## License
70
-
71
- MIT
1
+ # pi-web-ui
2
+
3
+ [English](https://github.com/xing-shuyin/pi-web-ui/blob/main/README.md) | **简体中文**
4
+
5
+ [pi 编码智能体](https://pi.dev) 的 Web 聊天界面 —— 智能体通过 pi SDK 在服务端进程内运行,
6
+ 事件经 WebSocket 流式推送到浏览器。支持思考块与工具调用、附件与图片问答、内置终端、
7
+ 模型管理,以及设置面板(自定义系统提示词、技能/插件开关、设置预设一键应用)等功能。
8
+ 需要 Node.js ≥ 22.19 及配置好的 pi 环境。
9
+
10
+ ## 界面截图
11
+
12
+ ![pi-web-ui 主界面](https://cdn.jsdelivr.net/gh/xing-shuyin/pi-web-ui@main/assets/shot.jpeg)
13
+
14
+ ## 安装
15
+
16
+ ```bash
17
+ npm i -g pi-web-ui # 全局安装(推荐)
18
+ npx pi-web-ui # 或免安装直接跑(拉取最新版,启动在 :8787)
19
+ npm i -g . # 或安装本地 checkout
20
+ ```
21
+
22
+ **npm ≥ 12?** npm 12+ 默认阻止依赖安装脚本(会看到 `npm warn install-scripts … blocked` 警告)。
23
+ node-pty 是原生模块,需要放行其脚本(其余两个包只是 no-op/纯提示,一并放行可消除警告):
24
+
25
+ ```bash
26
+ npm i -g --allow-scripts=node-pty,@google/genai,protobufjs pi-web-ui@latest
27
+ ```
28
+
29
+ ## 启动
30
+
31
+ ```bash
32
+ pi-web-ui # 前台,http://localhost:8787
33
+ PORT=9000 PI_WEB_CWD=/path/to/project pi-web-ui # 自定义端口 / 工作目录
34
+ ```
35
+
36
+ ## 停止
37
+
38
+ - **前台**:在运行它的终端里按 `Ctrl+C`。
39
+ - **作为服务**:`pi-web-ui server stop`(停止实例;开机自启保留,直到 `server uninstall`)。
40
+
41
+ ## 更新
42
+
43
+ ```bash
44
+ npm i -g pi-web-ui@latest # 升级到最新发布版本
45
+ pi-web-ui server restart # 重启服务使新版本生效(前台运行则手动重启)
46
+ ```
47
+
48
+ ## 卸载
49
+
50
+ ```bash
51
+ npm uninstall -g pi-web-ui
52
+ ```
53
+
54
+ 卸载**不会**删除你的聊天记录 —— 会话数据存放在 `<cwd>/.pi-web`(或 `PI_WEB_DATA_DIR`),
55
+ 卸载/升级后依然保留。
56
+
57
+ ## 作为系统服务(开机自启)
58
+
59
+ ```bash
60
+ pi-web-ui server install --port 9000 --cwd /path/to/project # 安装 + 启动
61
+ pi-web-ui server status # 运行中?开机自启?
62
+ pi-web-ui server restart # 重启(应用配置/版本变更)
63
+ pi-web-ui server stop # 停止(开机自启保留)
64
+ pi-web-ui server start # 再次启动
65
+ pi-web-ui server uninstall # 彻底移除服务
66
+ pi-web-ui server shortcut # 桌面一键启动图标
67
+ ```
68
+
69
+ - **macOS** → launchd 代理(无需 sudo),日志 `/tmp/pi-web-ui.log` / `.err`
70
+ - **Linux** → systemd unit(`systemctl enable --now`),日志 `journalctl -u pi-web-ui -f`
71
+ - **Windows** → 计划任务(登录自启,隐藏 PowerShell 窗口,无黑窗)
72
+
73
+ 选项:`--port`(默认 8787)、`--cwd`(工作目录)、`--data-dir`(会话目录)、
74
+ `--name`(自定义服务名)。重复执行 `server install` 并传入新选项即可重新生成配置
75
+ 并重启服务 —— 这就是修改已装服务端口/工作目录的方式。
76
+
77
+ ## License
78
+
79
+ MIT
@@ -334,6 +334,25 @@ function killPidTree(pid) {
334
334
  // already dead
335
335
  }
336
336
  }
337
+ /** Best-effort process name for a pid (tasklist on win32, ps on POSIX).
338
+ * Returns undefined when the process is gone or the lookup fails. */
339
+ async function lookupProcessName(pid) {
340
+ try {
341
+ const { execFile } = await import("node:child_process");
342
+ if (process.platform === "win32") {
343
+ const out = await new Promise((resolve, reject) => execFile("tasklist", ["/FI", `PID eq ${pid}`, "/FO", "CSV", "/NH"], { windowsHide: true, timeout: 4000 }, (err, stdout) => (err ? reject(err) : resolve(stdout))));
344
+ // CSV: "node.exe","12345",...
345
+ const m = out.match(/"([^"]+)"/);
346
+ return m ? m[1] : undefined;
347
+ }
348
+ const out = await new Promise((resolve, reject) => execFile("ps", ["-o", "comm=", "-p", String(pid)], { timeout: 4000 }, (err, stdout) => (err ? reject(err) : resolve(stdout))));
349
+ const name = out.trim();
350
+ return name || undefined;
351
+ }
352
+ catch {
353
+ return undefined;
354
+ }
355
+ }
337
356
  /**
338
357
  * Cheap per-message discriminator for the serialization cache key. Persisted
339
358
  * message content never changes, so this is stable across snapshots, while
@@ -1001,8 +1020,15 @@ export class ClientSession {
1001
1020
  * end-of-execution diff reveals servers the agent left running in the
1002
1021
  * background (e.g. `npm run dev &`). Keyed by port → pid. */
1003
1022
  bashListenBefore = null;
1004
- /** Background servers the agent started (port → pid). 「中断」kills them. */
1023
+ /** Background servers the agent started (port → pid). Managed from the
1024
+ * 后台任务 panel: each can be stopped individually or all at once. The
1025
+ * map lives on the CLIENT (not a conversation), so it survives
1026
+ * conversation switches/ends and only empties when a task is stopped or
1027
+ * its process exits on its own (pruned by refreshBgServers). */
1005
1028
  bgServers = new Map();
1029
+ /** Periodic pruner: re-snapshots listening ports and drops tracked entries
1030
+ * whose process died on its own, keeping the panel honest. */
1031
+ bgTimer = null;
1006
1032
  /** The active conversation (all session operations target it). */
1007
1033
  get conv() {
1008
1034
  const conv = this.convs.get(this.activeId);
@@ -1052,6 +1078,10 @@ export class ClientSession {
1052
1078
  this.stateStore = stateStore;
1053
1079
  this.settings = stateStore.getSettings(clientId);
1054
1080
  this.presets = stateStore.getPresets(clientId);
1081
+ // Prune dead background tasks every 30s (only spawns netstat/lsof while
1082
+ // the list is non-empty). unref: must not keep the process alive.
1083
+ this.bgTimer = setInterval(() => void this.refreshBgServers(), 30_000);
1084
+ this.bgTimer.unref?.();
1055
1085
  }
1056
1086
  static async create(clientId, cwd, stateStore) {
1057
1087
  const agentDir = process.env.PI_CODING_AGENT_DIR ?? getAgentDir();
@@ -1204,6 +1234,9 @@ export class ClientSession {
1204
1234
  // Reconnect: push the settings panel state (prompt text/mode, skill &
1205
1235
  // extension toggles, saved presets).
1206
1236
  this.pushSettings();
1237
+ // Reconnect: push the background-task list — it must survive reconnects
1238
+ // and outlive the conversation that started the tasks.
1239
+ this.emitBgServers();
1207
1240
  }
1208
1241
  detachSink(send) {
1209
1242
  this.sinks.delete(send);
@@ -2998,17 +3031,9 @@ export class ClientSession {
2998
3031
  * overnight. The notice fires only on the forced-reset path.
2999
3032
  */
3000
3033
  async abort() {
3034
+ // 只停止智能体运行本身;AI 在后台启动的服务由「后台任务」面板单独
3035
+ // 管理(可逐个停止或全部关闭),不会在停止对话时被连带杀掉。
3001
3036
  await this.interruptRun(this.conv, "已停止");
3002
- // 中断同时清理 AI 在后台启动的服务(npm run dev & 等)——避免用户
3003
- // 测试时发现端口被占用而不知道是什么进程。
3004
- const killed = await this.killBackgroundServers();
3005
- if (killed.length > 0) {
3006
- this.emit({
3007
- type: "notice",
3008
- level: "info",
3009
- text: `已停止 AI 后台服务:端口 ${killed.join("、")}(进程已结束)`,
3010
- });
3011
- }
3012
3037
  this.flushSnapshot();
3013
3038
  }
3014
3039
  /** After a bash tool run, wait briefly for background servers to bind,
@@ -3021,19 +3046,92 @@ export class ClientSession {
3021
3046
  return;
3022
3047
  await new Promise((r) => setTimeout(r, 1500));
3023
3048
  const after = await snapshotListeningPorts();
3049
+ let added = false;
3024
3050
  for (const [port, pid] of after) {
3025
3051
  if (!before.has(port) && !this.bgServers.has(port)) {
3026
3052
  this.bgServers.set(port, { pid, since: Date.now() });
3053
+ added = true;
3054
+ // Best-effort process name so the panel shows something readable.
3055
+ void lookupProcessName(pid).then((name) => {
3056
+ const cur = this.bgServers.get(port);
3057
+ if (cur && cur.pid === pid && name) {
3058
+ cur.name = name;
3059
+ this.emitBgServers();
3060
+ }
3061
+ });
3027
3062
  this.emit({
3028
3063
  type: "notice",
3029
3064
  level: "info",
3030
- text: `检测到 AI 启动的后台服务:端口 ${port}(pid ${pid})——点顶栏「中断」可停止`,
3065
+ text: `检测到 AI 启动的后台服务:端口 ${port}(pid ${pid})——可在顶栏「后台任务」里单独停止或全部关闭`,
3031
3066
  });
3032
3067
  }
3033
3068
  }
3069
+ if (added)
3070
+ this.emitBgServers();
3071
+ }
3072
+ /** The current background-server list, oldest first. */
3073
+ bgServerList() {
3074
+ return [...this.bgServers.entries()]
3075
+ .map(([port, v]) => ({
3076
+ port,
3077
+ pid: v.pid,
3078
+ since: v.since,
3079
+ ...(v.name ? { name: v.name } : {}),
3080
+ }))
3081
+ .sort((a, b) => a.since - b.since);
3082
+ }
3083
+ /** Push the current background-task list to every connected socket. */
3084
+ emitBgServers() {
3085
+ this.emit({ type: "bg_servers", servers: this.bgServerList() });
3086
+ }
3087
+ /** Re-snapshot listening ports and drop tracked entries that are no longer
3088
+ * listening — the process exited on its own, so it must leave the panel.
3089
+ * Port AND pid must both match: a port reused by an unrelated process is
3090
+ * not our server anymore. Silent (the list just updates). */
3091
+ async refreshBgServers() {
3092
+ if (this.disposed || this.bgServers.size === 0)
3093
+ return;
3094
+ const now = await snapshotListeningPorts();
3095
+ let changed = false;
3096
+ for (const [port, v] of [...this.bgServers]) {
3097
+ if (now.get(port) !== v.pid) {
3098
+ this.bgServers.delete(port);
3099
+ changed = true;
3100
+ }
3101
+ }
3102
+ if (changed)
3103
+ this.emitBgServers();
3104
+ }
3105
+ /** Re-push the current list on request (panel opened); prunes dead entries first. */
3106
+ async listBgServers() {
3107
+ await this.refreshBgServers();
3108
+ this.emitBgServers();
3109
+ }
3110
+ /** Kill ONE background server (by port); returns whether anything was killed. */
3111
+ async killBackgroundServer(port) {
3112
+ const entry = this.bgServers.get(port);
3113
+ if (!entry) {
3114
+ this.emit({
3115
+ type: "notice",
3116
+ level: "info",
3117
+ text: `端口 ${port} 不在后台任务列表中`,
3118
+ });
3119
+ this.flushSnapshot();
3120
+ return false;
3121
+ }
3122
+ killPidTree(entry.pid);
3123
+ this.bgServers.delete(port);
3124
+ this.emitBgServers();
3125
+ this.emit({
3126
+ type: "notice",
3127
+ level: "info",
3128
+ text: `已停止后台任务:端口 ${port}(pid ${entry.pid})`,
3129
+ });
3130
+ this.flushSnapshot();
3131
+ return true;
3034
3132
  }
3035
3133
  /** Kill every background server the agent started; returns the freed ports. */
3036
- async killBackgroundServers() {
3134
+ async killAllBackgroundServers() {
3037
3135
  if (this.bgServers.size === 0)
3038
3136
  return [];
3039
3137
  const killed = [];
@@ -3042,6 +3140,7 @@ export class ClientSession {
3042
3140
  killed.push(String(port));
3043
3141
  }
3044
3142
  this.bgServers.clear();
3143
+ this.emitBgServers();
3045
3144
  return killed;
3046
3145
  }
3047
3146
  /** Kill only the running bash command(s) — the agent run itself continues
@@ -4608,6 +4707,10 @@ export class ClientSession {
4608
4707
  }
4609
4708
  this.unwatchDir();
4610
4709
  this.webUi.dispose();
4710
+ if (this.bgTimer) {
4711
+ clearInterval(this.bgTimer);
4712
+ this.bgTimer = null;
4713
+ }
4611
4714
  for (const conv of this.convs.values()) {
4612
4715
  this.clearAllToolWatchdogs(conv);
4613
4716
  conv.unsubscribe?.();
@@ -240,6 +240,15 @@ wss.on("connection", (ws) => {
240
240
  case "abort_bash":
241
241
  void cs.abortBash();
242
242
  break;
243
+ case "kill_background_server":
244
+ void cs.killBackgroundServer(msg.port);
245
+ break;
246
+ case "kill_background_servers":
247
+ void cs.killAllBackgroundServers();
248
+ break;
249
+ case "list_bg_servers":
250
+ void cs.listBgServers();
251
+ break;
243
252
  case "new_chat":
244
253
  void cs.newChat();
245
254
  break;
@@ -0,0 +1,80 @@
1
+ /**
2
+ * node-pty × Node `--watch` compatibility patches (dev-server noise).
3
+ *
4
+ * Node's `--watch` mode (the dev script runs `node --watch --import tsx`)
5
+ * pushes `watch:require` / `watch:import` messages over the IPC channels of
6
+ * spawned worker threads AND forked child processes to track module
7
+ * dependencies. node-pty 1.1.0 does not recognize these and mishandles them in
8
+ * two places:
9
+ *
10
+ * · windowsConoutConnection.js — logs every unknown worker message via
11
+ * `console.warn`, producing hundreds of "Unexpected ConoutWorkerMessage"
12
+ * lines per terminal (the SCM panel's hidden query PTY triggers it on every
13
+ * git-view open).
14
+ * · windowsPtyAgent.js — the kill path's `_getConsoleProcessList()` treats ANY
15
+ * message from the forked console-list agent as its reply; a watch message
16
+ * has no `consoleProcessList` property, so `message.consoleProcessList` is
17
+ * undefined and the `.forEach` on it crashes the server.
18
+ *
19
+ * Both are benign protocol noise: the ConPTY worker only ever sends its READY
20
+ * sentinel and the agent only sends `{ consoleProcessList }` — both of which
21
+ * node-pty handles. These patches make node-pty ignore unrelated IPC traffic.
22
+ *
23
+ * IMPORTANT: this module MUST be imported before node-pty (see terminals.ts) —
24
+ * it rewrites the installed copies on disk so the subsequently-compiled
25
+ * node-pty modules contain the fixed handlers. Best-effort and idempotent
26
+ * (checks before rewriting), mirroring the spawn-helper chmod self-heal; a
27
+ * read-only node_modules or a different node-pty version just skips the patch
28
+ * (the runtime console.warn filter in terminals.ts still guards the flood).
29
+ */
30
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
31
+ import { createRequire } from "node:module";
32
+ import { dirname, join } from "node:path";
33
+ const require = createRequire(import.meta.url);
34
+ /** Absolute path of the installed node-pty package root, or null. */
35
+ function ptyPackageDir() {
36
+ try {
37
+ // require.resolve("node-pty") → <pkg>/lib/index.js → package root is two up.
38
+ return dirname(dirname(require.resolve("node-pty")));
39
+ }
40
+ catch {
41
+ return null;
42
+ }
43
+ }
44
+ /** Idempotently replace one code fragment in a node-pty source file. */
45
+ function patchFile(pkgDir, rel, find, replace) {
46
+ try {
47
+ const file = join(pkgDir, rel);
48
+ if (!existsSync(file))
49
+ return;
50
+ const src = readFileSync(file, "utf8");
51
+ if (src.includes(replace))
52
+ return; // already patched
53
+ if (!src.includes(find))
54
+ return; // unexpected content — leave alone
55
+ writeFileSync(file, src.replace(find, replace), "utf8");
56
+ }
57
+ catch {
58
+ // best-effort — the runtime console.warn filter still guards the flood
59
+ }
60
+ }
61
+ function applyPatches() {
62
+ const pkgDir = ptyPackageDir();
63
+ if (!pkgDir)
64
+ return;
65
+ // 1. ConoutConnection: ignore unknown worker messages instead of warning.
66
+ patchFile(pkgDir, "lib/windowsConoutConnection.js", "console.warn('Unexpected ConoutWorkerMessage', message);", "break; // pi-web-ui: ignore Node --watch worker messages");
67
+ // 2. windowsPtyAgent kill path: only accept a real console-list reply
68
+ // (watch messages arrive first on the fork channel and carry no
69
+ // consoleProcessList — previously resolved undefined and crashed).
70
+ patchFile(pkgDir, "lib/windowsPtyAgent.js", "agent.on('message', function (message) {\n clearTimeout(timeout);\n resolve(message.consoleProcessList);\n });", [
71
+ " agent.on('message', function (message) {",
72
+ " // pi-web-ui: ignore Node --watch messages on the fork channel",
73
+ " if (message && Array.isArray(message.consoleProcessList)) {",
74
+ " clearTimeout(timeout);",
75
+ " resolve(message.consoleProcessList);",
76
+ " }",
77
+ " });",
78
+ ].join("\n"));
79
+ }
80
+ applyPatches();
@@ -18,6 +18,10 @@ import { mkdir, readFile, writeFile } from "node:fs/promises";
18
18
  import { createRequire } from "node:module";
19
19
  import { homedir } from "node:os";
20
20
  import { dirname, isAbsolute, join, resolve } from "node:path";
21
+ // MUST run before node-pty is required: rewrites the installed node-pty copies
22
+ // so their worker/agent handlers tolerate Node `--watch`'s IPC traffic (see the
23
+ // module itself for details).
24
+ import "./patch-node-pty.js";
21
25
  import { spawn } from "node-pty";
22
26
  /** Location of the command list for a project: <workspaceRoot>/.pi/commands.json */
23
27
  export function commandsFilePath(workspaceRoot) {
@@ -164,6 +168,25 @@ function shellEnv() {
164
168
  return env;
165
169
  }
166
170
  // ---------------------------------------------------------------------------
171
+ // node-pty ConoutConnection warning noise (Node --watch)
172
+ // ---------------------------------------------------------------------------
173
+ // node-pty's ConoutConnection warns about every unknown message from its ConPTY
174
+ // worker thread. Under `node --watch` (the dev server: `node --watch --import
175
+ // tsx`), Node's watch mode pushes `watch:require` / `watch:import` messages over
176
+ // the worker's message channel to track module dependencies; node-pty doesn't
177
+ // recognize them and logs one `Unexpected ConoutWorkerMessage { … }` per message
178
+ // — hundreds of lines per terminal (the SCM panel's hidden query PTY triggers it
179
+ // on every git-view open). The messages are harmless: the worker only ever sends
180
+ // its READY sentinel, which the handler does process. Filter that exact warning
181
+ // at the console boundary so dev output stays readable. Production runs without
182
+ // --watch and never produces these.
183
+ const originalWarn = console.warn.bind(console);
184
+ console.warn = (...args) => {
185
+ if (args[0] === "Unexpected ConoutWorkerMessage")
186
+ return;
187
+ originalWarn(...args);
188
+ };
189
+ // ---------------------------------------------------------------------------
167
190
  // spawn-helper permission repair (node-pty macOS prebuilds)
168
191
  // ---------------------------------------------------------------------------
169
192
  // node-pty 1.1.0 publishes its macOS prebuilds with `spawn-helper` lacking the