pi-web-ui 0.2.0 → 0.2.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.
- package/README.md +59 -23
- package/bin/pi-web-ui.mjs +237 -8
- package/deploy/pi-web-ui-task.xml +69 -0
- package/dist/server/agent-service.js +45 -5
- package/dist/server/terminals.js +11 -2
- package/package.json +1 -1
- package/web/dist/assets/{index-DLMNi58O.js → index-C6A69YHZ.js} +43 -43
- package/web/dist/assets/{index-B882SGfI.css → index-DQ2mX6v5.css} +1 -1
- package/web/dist/index.html +2 -2
package/README.md
CHANGED
|
@@ -72,7 +72,7 @@ PORT=9000 PI_WEB_CWD=/path/to/project pi-web-ui # custom port / workspace
|
|
|
72
72
|
```
|
|
73
73
|
|
|
74
74
|
To run it in the background or auto-start on boot, use a system service —
|
|
75
|
-
see [Deploy & auto-start on boot](#deploy--auto-start-on-boot) (systemd /
|
|
75
|
+
see [Deploy & auto-start on boot](#deploy--auto-start-on-boot) (systemd /
|
|
76
76
|
launchd / Docker).
|
|
77
77
|
|
|
78
78
|
The `pi-web-ui` command serves the built frontend and the WebSocket API from
|
|
@@ -85,6 +85,9 @@ wherever the package is installed — no repo checkout needed. It uses **your**
|
|
|
85
85
|
- **Foreground**: press `Ctrl+C` in the terminal running it.
|
|
86
86
|
- **systemd**: `sudo systemctl stop pi-web-ui`
|
|
87
87
|
- **launchd**: `launchctl bootout gui/$(id -u)/com.xingshuyin.pi-web-ui`
|
|
88
|
+
- **Windows (scheduled task)**: `pi-web-ui server stop` (or
|
|
89
|
+
`schtasks /End /TN pi-web-ui`; stops the running instance, auto-start stays
|
|
90
|
+
until `server uninstall`)
|
|
88
91
|
- **Docker**: `docker compose stop` (stop + remove the container: `docker compose down`)
|
|
89
92
|
|
|
90
93
|
(Background processes should be managed by a system service, not `nohup` —
|
|
@@ -134,23 +137,28 @@ pi-web-ui server uninstall # remove the service entirely
|
|
|
134
137
|
- **Linux** → systemd unit (auto-sudo): writes
|
|
135
138
|
`/etc/systemd/system/pi-web-ui.service` and runs `systemctl enable --now`,
|
|
136
139
|
logs via `journalctl -u pi-web-ui -f`.
|
|
140
|
+
- **Windows** → Task Scheduler (no admin): creates a user task that starts at
|
|
141
|
+
logon (same as a launchd agent). It runs a `.cmd` wrapper generated at
|
|
142
|
+
`%APPDATA%\pi-web-ui\pi-web-ui.cmd` (sets env, cd's to the workspace,
|
|
143
|
+
launches node, appends logs to `%USERPROFILE%\pi-web-ui.log`). The task XML
|
|
144
|
+
is saved next to it; restarts on failure.
|
|
137
145
|
- Options: `--port` (default 8787 or `$PORT`), `--cwd` (default `$PI_WEB_CWD`
|
|
138
146
|
or the current directory), `--data-dir` (sessions), `--name` (custom service
|
|
139
147
|
name; on macOS the label is `com.xingshuyin.pi-web-ui`, custom names become
|
|
140
|
-
`com.<name>.server`). `--print` previews the generated unit/plist
|
|
141
|
-
applying it.
|
|
148
|
+
`com.<name>.server`). `--print` previews the generated unit/plist/task files
|
|
149
|
+
without applying it.
|
|
142
150
|
- Rerunning `install` with new options regenerates the config and restarts the
|
|
143
151
|
service — that's how you change the port/cwd of an installed service.
|
|
144
152
|
|
|
145
153
|
## Configuration
|
|
146
154
|
|
|
147
|
-
| Env var
|
|
148
|
-
|
|
|
149
|
-
| `PORT`
|
|
150
|
-
| `PI_WEB_CWD`
|
|
151
|
-
| `PI_WEB_DATA_DIR`
|
|
152
|
-
| `PI_WEB_INLINE_FILE_MAX` | `12288` (12KB)
|
|
153
|
-
| `PI_CODING_AGENT_DIR`
|
|
155
|
+
| Env var | Default | Description |
|
|
156
|
+
| -------------------------- | ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
157
|
+
| `PORT` | `8787` | HTTP/WebSocket port |
|
|
158
|
+
| `PI_WEB_CWD` | server's cwd | The workspace directory the agent operates in (read/edit/bash/write) |
|
|
159
|
+
| `PI_WEB_DATA_DIR` | `<cwd>/.pi-web` | Where per-client session dirs are stored |
|
|
160
|
+
| `PI_WEB_INLINE_FILE_MAX` | `12288` (12KB) | Text attachments at or below this size are inlined into the model context; larger files are passed as path references and the model reads them on demand (saves tokens for small edits) |
|
|
161
|
+
| `PI_CODING_AGENT_DIR` | `~/.pi/agent` | pi config dir (auth.json, models.json, skills, extensions) |
|
|
154
162
|
|
|
155
163
|
Example — point the agent at a project:
|
|
156
164
|
|
|
@@ -228,10 +236,10 @@ with three panes:
|
|
|
228
236
|
`${pwd}` resolves to the agent's current working directory (the one shown in the chat
|
|
229
237
|
view's file panel, changeable via set_cwd); `~` and relative paths also work. The `+`
|
|
230
238
|
button at the top of the command panel creates a new entry.
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
and you can type, Ctrl+C, resize, etc. exactly like a desktop
|
|
234
|
-
|
|
239
|
+
- **Middle — the terminal**: each tab is a real PTY (your `$SHELL` on
|
|
240
|
+
macOS/Linux; PowerShell or cmd.exe — `$COMSPEC` — on Windows); output
|
|
241
|
+
streams live and you can type, Ctrl+C, resize, etc. exactly like a desktop
|
|
242
|
+
terminal. Git Bash users on Windows get their `$SHELL` automatically.
|
|
235
243
|
- **Right — 终端 (tabs)**: VSCode-style vertical tab strip. `+` opens a plain shell in the
|
|
236
244
|
current directory. Closing a tab kills its process.
|
|
237
245
|
|
|
@@ -254,13 +262,13 @@ Server → client: `ready`, `snapshot` (full `UiState`), `tool_delta`, `notice`,
|
|
|
254
262
|
|
|
255
263
|
## Scripts
|
|
256
264
|
|
|
257
|
-
| Script
|
|
258
|
-
|
|
|
259
|
-
| `npm run dev`
|
|
260
|
-
| `npm run build`
|
|
261
|
-
| `npm start`
|
|
262
|
-
| `npm run typecheck`
|
|
263
|
-
| `node terminal-smoke-test.mjs`
|
|
265
|
+
| Script | What it does |
|
|
266
|
+
| ---------------------------------- | ------------------------------------------------------- |
|
|
267
|
+
| `npm run dev` | server (tsx watch) + Vite dev server with WS proxy |
|
|
268
|
+
| `npm run build` | type-check + build frontend and server |
|
|
269
|
+
| `npm start` | run the production server (serves`web/dist`) |
|
|
270
|
+
| `npm run typecheck` | `tsc --noEmit` for both server and web |
|
|
271
|
+
| `node terminal-smoke-test.mjs` | WS-level terminal/commands protocol test (build first) |
|
|
264
272
|
| `node terminal-browser-test.mjs` | headless-browser E2E of the terminal view (build first) |
|
|
265
273
|
|
|
266
274
|
## Deploy & auto-start on boot
|
|
@@ -306,8 +314,36 @@ launchctl bootout gui/$(id -u)/com.xingshuyin.pi-web-ui # stop + remove auto-s
|
|
|
306
314
|
# logs: /tmp/pi-web-ui.log, /tmp/pi-web-ui.err
|
|
307
315
|
```
|
|
308
316
|
|
|
309
|
-
|
|
310
|
-
|
|
317
|
+
### Windows — Task Scheduler
|
|
318
|
+
|
|
319
|
+
Easiest path (no admin, generates everything):
|
|
320
|
+
|
|
321
|
+
```bat
|
|
322
|
+
npm i -g pi-web-ui
|
|
323
|
+
pi-web-ui server install --port 8787 --cwd C:\path\to\project
|
|
324
|
+
pi-web-ui server status
|
|
325
|
+
pi-web-ui server restart
|
|
326
|
+
pi-web-ui server stop :: stop the running instance (auto-start stays)
|
|
327
|
+
pi-web-ui server uninstall :: remove the task entirely
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
What it does: writes `%APPDATA%\pi-web-ui\pi-web-ui.cmd` (a `.cmd` wrapper
|
|
331
|
+
that sets `PORT`/`PI_WEB_CWD`, cd's to the workspace, launches node and
|
|
332
|
+
appends output to `%USERPROFILE%\pi-web-ui.log`) plus the Task Scheduler XML,
|
|
333
|
+
then registers a **logon** task (`schtasks /Create /XML` — the task runs when
|
|
334
|
+
you log in, same as a launchd agent; no admin needed). Preview both generated
|
|
335
|
+
files without installing: `pi-web-ui server install --print`.
|
|
336
|
+
|
|
337
|
+
Manual alternative with `deploy/pi-web-ui-task.xml`: edit the paths, save the
|
|
338
|
+
file as **UTF-16 LE** (schtasks requires it), then
|
|
339
|
+
`schtasks /Create /TN "pi-web-ui" /XML pi-web-ui-task.xml /F` and
|
|
340
|
+
`schtasks /Run /TN "pi-web-ui"`.
|
|
341
|
+
|
|
342
|
+
> **Boot-start without login?** A logon task needs an interactive session, just
|
|
343
|
+
> like a launchd agent. For headless/always-on Windows use Docker (see above).
|
|
344
|
+
|
|
345
|
+
The three templates use `KeepAlive` / `Restart=on-failure` / `RestartOnFailure`
|
|
346
|
+
so the server survives crashes, and start at login/boot automatically.
|
|
311
347
|
|
|
312
348
|
## License
|
|
313
349
|
|
package/bin/pi-web-ui.mjs
CHANGED
|
@@ -10,9 +10,11 @@
|
|
|
10
10
|
* pi-web-ui server start|stop|restart|status [选项]
|
|
11
11
|
*
|
|
12
12
|
* 系统服务:
|
|
13
|
-
* - macOS
|
|
14
|
-
*
|
|
15
|
-
* - Linux
|
|
13
|
+
* - macOS → launchd 用户代理,label 默认 com.xingshuyin.pi-web-ui
|
|
14
|
+
* (--name 自定义时 com.<name>.server),无需 sudo
|
|
15
|
+
* - Linux → systemd 单元 <name>.service(/etc/systemd/system/,自动 sudo)
|
|
16
|
+
* - Windows → 计划任务(Task Scheduler / schtasks,登录后自启,无需管理员),
|
|
17
|
+
* 包装脚本与任务 XML 生成在 %APPDATA%\pi-web-ui\
|
|
16
18
|
*
|
|
17
19
|
* 环境变量(前台与系统服务均适用):PORT / PI_WEB_CWD / PI_WEB_DATA_DIR /
|
|
18
20
|
* PI_CODING_AGENT_DIR。
|
|
@@ -59,6 +61,9 @@ server 选项:
|
|
|
59
61
|
为 com.xingshuyin.pi-web-ui,自定义名时为 com.<name>.server)
|
|
60
62
|
--print 只打印将生成的配置文件,不实际安装
|
|
61
63
|
|
|
64
|
+
平台: macOS → launchd 用户代理 · Linux → systemd · Windows → 计划任务(schtasks)
|
|
65
|
+
(Windows 任务登录后自启、无需管理员;stop 停止,uninstall 移除)
|
|
66
|
+
|
|
62
67
|
环境变量(前台与系统服务均适用):
|
|
63
68
|
PORT / PI_WEB_CWD / PI_WEB_DATA_DIR / PI_CODING_AGENT_DIR
|
|
64
69
|
`;
|
|
@@ -146,6 +151,7 @@ async function startForeground(opts) {
|
|
|
146
151
|
|
|
147
152
|
const isMac = process.platform === "darwin";
|
|
148
153
|
const isLinux = process.platform === "linux";
|
|
154
|
+
const isWin = process.platform === "win32";
|
|
149
155
|
|
|
150
156
|
function uid() {
|
|
151
157
|
try {
|
|
@@ -155,7 +161,7 @@ function uid() {
|
|
|
155
161
|
}
|
|
156
162
|
}
|
|
157
163
|
|
|
158
|
-
/** launchd label / systemd unit name for a service name. */
|
|
164
|
+
/** launchd label / systemd unit name / Windows task name for a service name. */
|
|
159
165
|
function serviceLabel(name) {
|
|
160
166
|
if (isMac) {
|
|
161
167
|
return name === "pi-web-ui"
|
|
@@ -178,6 +184,110 @@ function systemdUnitPath(name) {
|
|
|
178
184
|
return `/etc/systemd/system/${name}.service`;
|
|
179
185
|
}
|
|
180
186
|
|
|
187
|
+
/** Windows: per-user config dir (%APPDATA%\pi-web-ui) holding the .cmd wrapper + task XML. */
|
|
188
|
+
function winServiceDir() {
|
|
189
|
+
return join(
|
|
190
|
+
process.env.APPDATA ?? join(homedir(), "AppData", "Roaming"),
|
|
191
|
+
"pi-web-ui",
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function winCmdPath(name) {
|
|
196
|
+
return join(winServiceDir(), `${name}.cmd`);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function winTaskXmlPath(name) {
|
|
200
|
+
return join(winServiceDir(), `${name}.xml`);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function winLogPath() {
|
|
204
|
+
return join(homedir(), "pi-web-ui.log");
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** True when a scheduled task with this name exists (schtasks exits 0). */
|
|
208
|
+
function winTaskExists(name) {
|
|
209
|
+
return (
|
|
210
|
+
spawnSync("schtasks", ["/Query", "/TN", name], { stdio: "ignore" })
|
|
211
|
+
.status === 0
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** Build the .cmd wrapper the scheduled task runs (set env → cd → launch node → log). */
|
|
216
|
+
function buildWinCmd(cwd, env, logPath) {
|
|
217
|
+
const sets = Object.entries(env)
|
|
218
|
+
.map(([k, v]) => `set "${k}=${v}"`)
|
|
219
|
+
.join("\r\n");
|
|
220
|
+
return [
|
|
221
|
+
"@echo off",
|
|
222
|
+
"rem Generated by: pi-web-ui server install (rerun to change)",
|
|
223
|
+
sets,
|
|
224
|
+
`cd /d "${cwd}"`,
|
|
225
|
+
`"${NODE}" "${SERVER_ENTRY}" >> "${logPath}" 2>&1`,
|
|
226
|
+
"",
|
|
227
|
+
].join("\r\n");
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Build the Task Scheduler XML for a user task: LogonTrigger (starts at logon,
|
|
232
|
+
* like a launchd agent — no admin needed), InteractiveToken, auto-restart on
|
|
233
|
+
* failure. The task runs the .cmd wrapper via cmd.exe.
|
|
234
|
+
*/
|
|
235
|
+
function buildWinTaskXml(cmdPath, cwd) {
|
|
236
|
+
const cmdExe = join(
|
|
237
|
+
process.env.SystemRoot ?? "C:\\Windows",
|
|
238
|
+
"System32",
|
|
239
|
+
"cmd.exe",
|
|
240
|
+
);
|
|
241
|
+
return `<?xml version="1.0" encoding="UTF-16"?>
|
|
242
|
+
<Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
|
|
243
|
+
<RegistrationInfo>
|
|
244
|
+
<Description>pi-web-ui — web chat for the pi coding agent (auto-start at logon)</Description>
|
|
245
|
+
</RegistrationInfo>
|
|
246
|
+
<Triggers>
|
|
247
|
+
<LogonTrigger>
|
|
248
|
+
<Enabled>true</Enabled>
|
|
249
|
+
</LogonTrigger>
|
|
250
|
+
</Triggers>
|
|
251
|
+
<Principals>
|
|
252
|
+
<Principal id="Author">
|
|
253
|
+
<LogonType>InteractiveToken</LogonType>
|
|
254
|
+
<RunLevel>LeastPrivilege</RunLevel>
|
|
255
|
+
</Principal>
|
|
256
|
+
</Principals>
|
|
257
|
+
<Settings>
|
|
258
|
+
<MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
|
|
259
|
+
<DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>
|
|
260
|
+
<StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>
|
|
261
|
+
<AllowHardTerminate>true</AllowHardTerminate>
|
|
262
|
+
<StartWhenAvailable>false</StartWhenAvailable>
|
|
263
|
+
<RunOnlyIfNetworkAvailable>false</RunOnlyIfNetworkAvailable>
|
|
264
|
+
<IdleSettings>
|
|
265
|
+
<StopOnIdleEnd>false</StopOnIdleEnd>
|
|
266
|
+
<RestartOnIdle>false</RestartOnIdle>
|
|
267
|
+
</IdleSettings>
|
|
268
|
+
<AllowStartOnDemand>true</AllowStartOnDemand>
|
|
269
|
+
<Enabled>true</Enabled>
|
|
270
|
+
<Hidden>false</Hidden>
|
|
271
|
+
<RunOnlyIfIdle>false</RunOnlyIfIdle>
|
|
272
|
+
<WakeToRun>false</WakeToRun>
|
|
273
|
+
<ExecutionTimeLimit>PT0S</ExecutionTimeLimit>
|
|
274
|
+
<Priority>7</Priority>
|
|
275
|
+
<RestartOnFailure>
|
|
276
|
+
<Interval>PT1M</Interval>
|
|
277
|
+
<Count>3</Count>
|
|
278
|
+
</RestartOnFailure>
|
|
279
|
+
</Settings>
|
|
280
|
+
<Actions Context="Author">
|
|
281
|
+
<Exec>
|
|
282
|
+
<Command>${esc(cmdExe)}</Command>
|
|
283
|
+
<Arguments>/c ""${esc(cmdPath)}""</Arguments>
|
|
284
|
+
<WorkingDirectory>${esc(cwd)}</WorkingDirectory>
|
|
285
|
+
</Exec>
|
|
286
|
+
</Actions>
|
|
287
|
+
</Task>
|
|
288
|
+
`;
|
|
289
|
+
}
|
|
290
|
+
|
|
181
291
|
function esc(s) {
|
|
182
292
|
return s
|
|
183
293
|
.replace(/&/g, "&")
|
|
@@ -288,8 +398,10 @@ function serviceEnv(port, cwd, dataDir) {
|
|
|
288
398
|
const env = {
|
|
289
399
|
PORT: port,
|
|
290
400
|
PI_WEB_CWD: cwd,
|
|
291
|
-
PATH: process.env.PATH ?? "/usr/local/bin:/usr/bin:/bin",
|
|
292
401
|
};
|
|
402
|
+
// Interactive Windows tasks inherit the user's PATH; only systemd/launchd
|
|
403
|
+
// run with a minimal environment that needs an explicit PATH.
|
|
404
|
+
if (!isWin) env.PATH = process.env.PATH ?? "/usr/local/bin:/usr/bin:/bin";
|
|
293
405
|
if (dataDir) env.PI_WEB_DATA_DIR = dataDir;
|
|
294
406
|
return env;
|
|
295
407
|
}
|
|
@@ -363,6 +475,52 @@ function uninstallSystemd(opts) {
|
|
|
363
475
|
console.log(`🗑 已卸载 ${name}.service(不再开机自启)`);
|
|
364
476
|
}
|
|
365
477
|
|
|
478
|
+
function installWindows(opts) {
|
|
479
|
+
const { name, port, cwd, dataDir } = serviceOptions(opts);
|
|
480
|
+
const env = serviceEnv(port, cwd, dataDir);
|
|
481
|
+
const cmdPath = winCmdPath(name);
|
|
482
|
+
const xmlPath = winTaskXmlPath(name);
|
|
483
|
+
const cmd = buildWinCmd(cwd, env, winLogPath());
|
|
484
|
+
const xml = buildWinTaskXml(cmdPath, cwd);
|
|
485
|
+
if (opts.print) {
|
|
486
|
+
console.log(`# ${cmdPath}\n${cmd}`);
|
|
487
|
+
console.log(`# ${xmlPath}\n${xml}`);
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
mkdirSync(dirname(cmdPath), { recursive: true });
|
|
491
|
+
writeFileSync(cmdPath, cmd);
|
|
492
|
+
// schtasks /Create /XML requires a UTF-16 file (with BOM).
|
|
493
|
+
writeFileSync(xmlPath, "\uFEFF" + xml, "utf16le");
|
|
494
|
+
if (winTaskExists(name)) {
|
|
495
|
+
run("schtasks", ["/Delete", "/TN", name, "/F"], {
|
|
496
|
+
ignoreError: true,
|
|
497
|
+
silent: true,
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
run("schtasks", ["/Create", "/TN", name, "/XML", xmlPath, "/F"]);
|
|
501
|
+
run("schtasks", ["/Run", "/TN", name], { ignoreError: true });
|
|
502
|
+
console.log(`✅ 已安装并启动计划任务 ${name}`);
|
|
503
|
+
console.log(` 端口 : ${port}`);
|
|
504
|
+
console.log(` 目录 : ${cwd}`);
|
|
505
|
+
console.log(` 访问 : http://localhost:${port}`);
|
|
506
|
+
console.log(` 日志 : ${winLogPath()}`);
|
|
507
|
+
console.log(
|
|
508
|
+
` 说明 : 登录后自启(与 launchd 用户代理一致);stop 停止,uninstall 移除`,
|
|
509
|
+
);
|
|
510
|
+
console.log(` 管理 : pi-web-ui server status|restart|stop|uninstall`);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function uninstallWindows(opts) {
|
|
514
|
+
const name = opts.name ?? "pi-web-ui";
|
|
515
|
+
if (winTaskExists(name)) {
|
|
516
|
+
run("schtasks", ["/Delete", "/TN", name, "/F"], { ignoreError: true });
|
|
517
|
+
}
|
|
518
|
+
for (const f of [winCmdPath(name), winTaskXmlPath(name)]) {
|
|
519
|
+
if (existsSync(f)) rmSync(f);
|
|
520
|
+
}
|
|
521
|
+
console.log(`🗑 已卸载 ${name}(计划任务已删除,不再自启)`);
|
|
522
|
+
}
|
|
523
|
+
|
|
366
524
|
function controlService(action, opts) {
|
|
367
525
|
const name = opts.name ?? "pi-web-ui";
|
|
368
526
|
|
|
@@ -430,7 +588,74 @@ function controlService(action, opts) {
|
|
|
430
588
|
return;
|
|
431
589
|
}
|
|
432
590
|
|
|
433
|
-
|
|
591
|
+
if (isWin) {
|
|
592
|
+
const exists = winTaskExists(name);
|
|
593
|
+
|
|
594
|
+
if (action === "status") {
|
|
595
|
+
if (!exists) {
|
|
596
|
+
console.log(`${name}: 未安装(运行 pi-web-ui server install 安装)`);
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
// Get-ScheduledTask outputs English state enums — locale-independent,
|
|
600
|
+
// unlike `schtasks /Query` tables on localized Windows.
|
|
601
|
+
const ps = spawnSync(
|
|
602
|
+
"powershell.exe",
|
|
603
|
+
[
|
|
604
|
+
"-NoProfile",
|
|
605
|
+
"-NonInteractive",
|
|
606
|
+
"-Command",
|
|
607
|
+
"$t=Get-ScheduledTask -TaskName '" +
|
|
608
|
+
name +
|
|
609
|
+
"' -ErrorAction SilentlyContinue;" +
|
|
610
|
+
"if(!$t){'NOT_INSTALLED';exit}" +
|
|
611
|
+
"$i=$t|Get-ScheduledTaskInfo;" +
|
|
612
|
+
"'State: '+$t.State;" +
|
|
613
|
+
"'LastRunTime: '+$i.LastRunTime;" +
|
|
614
|
+
"'LastTaskResult: '+$i.LastTaskResult",
|
|
615
|
+
],
|
|
616
|
+
{ encoding: "utf8" },
|
|
617
|
+
);
|
|
618
|
+
if (ps.status !== 0 || (ps.stdout ?? "").includes("NOT_INSTALLED")) {
|
|
619
|
+
console.log(`${name}: 未安装(运行 pi-web-ui server install 安装)`);
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
622
|
+
console.log(`${name}: 计划任务\n${(ps.stdout ?? "").trim()}`);
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
if (action === "start") {
|
|
627
|
+
if (!exists) fail(`${name} 不存在,请先运行 pi-web-ui server install`);
|
|
628
|
+
run("schtasks", ["/Run", "/TN", name]);
|
|
629
|
+
console.log(`✅ 已启动 ${name}`);
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
if (action === "restart") {
|
|
634
|
+
if (!exists) fail(`${name} 不存在,请先运行 pi-web-ui server install`);
|
|
635
|
+
run("schtasks", ["/End", "/TN", name], {
|
|
636
|
+
ignoreError: true,
|
|
637
|
+
silent: true,
|
|
638
|
+
});
|
|
639
|
+
run("schtasks", ["/Run", "/TN", name]);
|
|
640
|
+
console.log(`✅ 已重启 ${name}`);
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
if (action === "stop") {
|
|
645
|
+
run("schtasks", ["/End", "/TN", name], {
|
|
646
|
+
ignoreError: true,
|
|
647
|
+
silent: true,
|
|
648
|
+
});
|
|
649
|
+
console.log(`⏹ 已停止 ${name}(自启保留;uninstall 移除)`);
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
fail(`未知操作: ${action}`);
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
fail(
|
|
657
|
+
`不支持的系统服务平台: ${process.platform}(仅 macOS / Linux / Windows)`,
|
|
658
|
+
);
|
|
434
659
|
}
|
|
435
660
|
|
|
436
661
|
async function serverCmd(argv) {
|
|
@@ -454,8 +679,10 @@ async function serverCmd(argv) {
|
|
|
454
679
|
installLaunchd(opts);
|
|
455
680
|
} else if (isLinux) {
|
|
456
681
|
installSystemd(opts);
|
|
682
|
+
} else if (isWin) {
|
|
683
|
+
installWindows(opts);
|
|
457
684
|
} else {
|
|
458
|
-
fail(`不支持的系统服务平台: ${process.platform}
|
|
685
|
+
fail(`不支持的系统服务平台: ${process.platform}`);
|
|
459
686
|
}
|
|
460
687
|
break;
|
|
461
688
|
}
|
|
@@ -464,8 +691,10 @@ async function serverCmd(argv) {
|
|
|
464
691
|
uninstallLaunchd(opts);
|
|
465
692
|
} else if (isLinux) {
|
|
466
693
|
uninstallSystemd(opts);
|
|
694
|
+
} else if (isWin) {
|
|
695
|
+
uninstallWindows(opts);
|
|
467
696
|
} else {
|
|
468
|
-
fail(`不支持的系统服务平台: ${process.platform}
|
|
697
|
+
fail(`不支持的系统服务平台: ${process.platform}`);
|
|
469
698
|
}
|
|
470
699
|
break;
|
|
471
700
|
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-16"?>
|
|
2
|
+
<!--
|
|
3
|
+
pi-web-ui Task Scheduler task — Windows auto-start at logon.
|
|
4
|
+
|
|
5
|
+
The easy way (no admin needed, generates everything for you):
|
|
6
|
+
npm i -g pi-web-ui
|
|
7
|
+
pi-web-ui server install --port 8787 --cwd C:\path\to\project
|
|
8
|
+
pi-web-ui server status | restart | stop | uninstall
|
|
9
|
+
|
|
10
|
+
Manual install with this template (edit the paths below first):
|
|
11
|
+
schtasks /Create /TN "pi-web-ui" /XML pi-web-ui-task.xml /F
|
|
12
|
+
schtasks /Run /TN "pi-web-ui"
|
|
13
|
+
|
|
14
|
+
Notes:
|
|
15
|
+
- The task points at the .cmd wrapper the CLI generates at
|
|
16
|
+
%APPDATA%\pi-web-ui\pi-web-ui.cmd (it sets PORT/PI_WEB_CWD, cd's to the
|
|
17
|
+
workspace, launches node, and appends output to %USERPROFILE%\pi-web-ui.log).
|
|
18
|
+
Preview both generated files with: pi-web-ui server install --print
|
|
19
|
+
- Save this file as UTF-16 LE (schtasks requires it; the CLI does this
|
|
20
|
+
automatically when it writes the task XML).
|
|
21
|
+
- LogonTrigger = starts when you log in, same as a launchd user agent.
|
|
22
|
+
For boot-start without login, use Docker instead (see README).
|
|
23
|
+
-->
|
|
24
|
+
<Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
|
|
25
|
+
<RegistrationInfo>
|
|
26
|
+
<Description>pi-web-ui — web chat for the pi coding agent (auto-start at logon)</Description>
|
|
27
|
+
</RegistrationInfo>
|
|
28
|
+
<Triggers>
|
|
29
|
+
<LogonTrigger>
|
|
30
|
+
<Enabled>true</Enabled>
|
|
31
|
+
</LogonTrigger>
|
|
32
|
+
</Triggers>
|
|
33
|
+
<Principals>
|
|
34
|
+
<Principal id="Author">
|
|
35
|
+
<LogonType>InteractiveToken</LogonType>
|
|
36
|
+
<RunLevel>LeastPrivilege</RunLevel>
|
|
37
|
+
</Principal>
|
|
38
|
+
</Principals>
|
|
39
|
+
<Settings>
|
|
40
|
+
<MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
|
|
41
|
+
<DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>
|
|
42
|
+
<StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>
|
|
43
|
+
<AllowHardTerminate>true</AllowHardTerminate>
|
|
44
|
+
<StartWhenAvailable>false</StartWhenAvailable>
|
|
45
|
+
<RunOnlyIfNetworkAvailable>false</RunOnlyIfNetworkAvailable>
|
|
46
|
+
<IdleSettings>
|
|
47
|
+
<StopOnIdleEnd>false</StopOnIdleEnd>
|
|
48
|
+
<RestartOnIdle>false</RestartOnIdle>
|
|
49
|
+
</IdleSettings>
|
|
50
|
+
<AllowStartOnDemand>true</AllowStartOnDemand>
|
|
51
|
+
<Enabled>true</Enabled>
|
|
52
|
+
<Hidden>false</Hidden>
|
|
53
|
+
<RunOnlyIfIdle>false</RunOnlyIfIdle>
|
|
54
|
+
<WakeToRun>false</WakeToRun>
|
|
55
|
+
<ExecutionTimeLimit>PT0S</ExecutionTimeLimit>
|
|
56
|
+
<Priority>7</Priority>
|
|
57
|
+
<RestartOnFailure>
|
|
58
|
+
<Interval>PT1M</Interval>
|
|
59
|
+
<Count>3</Count>
|
|
60
|
+
</RestartOnFailure>
|
|
61
|
+
</Settings>
|
|
62
|
+
<Actions Context="Author">
|
|
63
|
+
<Exec>
|
|
64
|
+
<Command>C:\Windows\System32\cmd.exe</Command>
|
|
65
|
+
<Arguments>/c ""%APPDATA%\pi-web-ui\pi-web-ui.cmd""</Arguments>
|
|
66
|
+
<WorkingDirectory>%USERPROFILE%</WorkingDirectory>
|
|
67
|
+
</Exec>
|
|
68
|
+
</Actions>
|
|
69
|
+
</Task>
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
import { existsSync } from "node:fs";
|
|
13
13
|
import { join } from "node:path";
|
|
14
14
|
import { createAgentSessionFromServices, createAgentSessionRuntime, createAgentSessionServices, getAgentDir, SessionManager, } from "@earendil-works/pi-coding-agent";
|
|
15
|
-
import { serializeMessage, serializeStreamingMessage } from "./serialize.js";
|
|
15
|
+
import { serializeMessage, serializeStreamingMessage, } from "./serialize.js";
|
|
16
16
|
import { loadCommands, saveCommandsFile, TerminalManager, } from "./terminals.js";
|
|
17
17
|
const SNAPSHOT_INTERVAL_MS = 60;
|
|
18
18
|
const WIDGET_REFRESH_MS = 2000;
|
|
@@ -286,7 +286,20 @@ export class ClientSession {
|
|
|
286
286
|
snapshotTimer = null;
|
|
287
287
|
sessionsTimer = null;
|
|
288
288
|
version = 0;
|
|
289
|
-
|
|
289
|
+
/**
|
|
290
|
+
* Stable per-message ids: assigned once per (role, timestamp) so snapshot ids
|
|
291
|
+
* don't change every 60ms — changing ids would remount the whole list in
|
|
292
|
+
* React (collapse open thinking blocks, reset scroll, jank on long chats).
|
|
293
|
+
*/
|
|
294
|
+
msgIds = new Map();
|
|
295
|
+
nextMsgId = 1;
|
|
296
|
+
/** Serialized UiMessage cache — object-reference-stable across snapshots, so
|
|
297
|
+
* the frontend's React.memo can skip unchanged messages entirely. */
|
|
298
|
+
uiMessageCache = new Map();
|
|
299
|
+
/** Reused when the message set didn't change, keeping state.messages
|
|
300
|
+
* reference-stable so the frontend can memoize derived maps. */
|
|
301
|
+
lastMessagesSig = "";
|
|
302
|
+
lastMessagesArray = [];
|
|
290
303
|
queueSteering = 0;
|
|
291
304
|
queueFollowUp = 0;
|
|
292
305
|
disposed = false;
|
|
@@ -434,6 +447,25 @@ export class ClientSession {
|
|
|
434
447
|
void this.pushSessions();
|
|
435
448
|
}, 800);
|
|
436
449
|
}
|
|
450
|
+
/** Serialize a persisted message with a STABLE id + cached object reference. */
|
|
451
|
+
serializeCached(m) {
|
|
452
|
+
const key = m.role === "toolResult"
|
|
453
|
+
? `t:${m.toolCallId}`
|
|
454
|
+
: `${m.role}:${m.timestamp}`;
|
|
455
|
+
let n = this.msgIds.get(key);
|
|
456
|
+
if (n === undefined) {
|
|
457
|
+
n = this.nextMsgId++;
|
|
458
|
+
this.msgIds.set(key, n);
|
|
459
|
+
}
|
|
460
|
+
const cacheKey = `${key}#${n}`;
|
|
461
|
+
const cached = this.uiMessageCache.get(cacheKey);
|
|
462
|
+
if (cached)
|
|
463
|
+
return cached;
|
|
464
|
+
const msg = serializeMessage(m, n);
|
|
465
|
+
if (msg)
|
|
466
|
+
this.uiMessageCache.set(cacheKey, msg);
|
|
467
|
+
return msg;
|
|
468
|
+
}
|
|
437
469
|
snapshot() {
|
|
438
470
|
const state = this.session.agent.state;
|
|
439
471
|
const model = state.model;
|
|
@@ -461,14 +493,22 @@ export class ClientSession {
|
|
|
461
493
|
catch {
|
|
462
494
|
// stats are best-effort
|
|
463
495
|
}
|
|
496
|
+
const rawMessages = state.messages
|
|
497
|
+
.map((m) => this.serializeCached(m))
|
|
498
|
+
.filter((m) => m !== null);
|
|
499
|
+
// Reuse the previous array when nothing changed: the element objects are
|
|
500
|
+
// cached (reference-stable) anyway, and a stable array reference lets the
|
|
501
|
+
// frontend memoize derived maps instead of rebuilding them every 60ms.
|
|
502
|
+
const sig = rawMessages.map((m) => m.id).join("\u0001");
|
|
503
|
+
const messages = sig === this.lastMessagesSig ? this.lastMessagesArray : rawMessages;
|
|
504
|
+
this.lastMessagesSig = sig;
|
|
505
|
+
this.lastMessagesArray = rawMessages;
|
|
464
506
|
return {
|
|
465
507
|
clientId: this.clientId,
|
|
466
508
|
cwd: this.cwd,
|
|
467
509
|
sessionId: this.session.sessionId,
|
|
468
510
|
sessionFile: this.session.sessionFile,
|
|
469
|
-
messages
|
|
470
|
-
.map((m) => serializeMessage(m, ++this.seq))
|
|
471
|
-
.filter((m) => m !== null),
|
|
511
|
+
messages,
|
|
472
512
|
// The in-progress assistant message lives in state.streamingMessage
|
|
473
513
|
// (the SDK only pushes it into state.messages at message_end). Surfacing
|
|
474
514
|
// it here is what makes thinking + text stream into the browser at
|
package/dist/server/terminals.js
CHANGED
|
@@ -101,8 +101,17 @@ export async function saveCommandsFile(workspaceRoot, commands) {
|
|
|
101
101
|
return { path, error: `保存命令文件失败:${err.message}` };
|
|
102
102
|
}
|
|
103
103
|
}
|
|
104
|
-
const
|
|
105
|
-
|
|
104
|
+
const isWindows = process.platform === "win32";
|
|
105
|
+
/**
|
|
106
|
+
* Interactive shell for PTYs. Windows has no $SHELL: use Git Bash if the user
|
|
107
|
+
* has it, otherwise the console host from $COMSPEC (always set → cmd.exe).
|
|
108
|
+
* POSIX uses the user's login shell, falling back to bash.
|
|
109
|
+
*/
|
|
110
|
+
const SHELL = isWindows
|
|
111
|
+
? process.env.SHELL || process.env.COMSPEC || "powershell.exe"
|
|
112
|
+
: process.env.SHELL || "bash";
|
|
113
|
+
/** `-i` is POSIX-only; cmd.exe / powershell.exe start interactively on their own. */
|
|
114
|
+
const SHELL_ARGS = isWindows ? [] : ["-i"];
|
|
106
115
|
/**
|
|
107
116
|
* Owns one or more PTYs for a client. All output is forwarded as
|
|
108
117
|
* `terminal_output` messages through the provided emit (broadcast to every
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-web-ui",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
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
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|