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 +84 -74
- package/README.zh-CN.md +79 -71
- package/dist/server/agent-service.js +116 -13
- package/dist/server/index.js +9 -0
- package/dist/server/patch-node-pty.js +80 -0
- package/dist/server/terminals.js +23 -0
- package/package.json +3 -2
- package/web/dist/assets/index-BehovRZ4.css +41 -0
- package/web/dist/assets/index-CFXSK8-7.js +128 -0
- package/web/dist/index.html +2 -2
- package/web/dist/assets/index-Cx5p0OLy.js +0 -126
- package/web/dist/assets/index-DkYHV6iM.css +0 -41
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,
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
pi-web-ui
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
pi-web
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
-
|
|
65
|
-
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
+

|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
pi-web
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
+

|
|
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).
|
|
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
|
|
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?.();
|
package/dist/server/index.js
CHANGED
|
@@ -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();
|
package/dist/server/terminals.js
CHANGED
|
@@ -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
|