pi-web-ui 0.44.1 → 0.45.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 +109 -39
- package/README.zh-CN.md +21 -0
- package/dist/server/agent-service.js +39 -1
- package/dist/server/client-state.js +2 -0
- package/dist/server/index.js +6 -0
- package/dist/server/protocol-version.js +1 -1
- package/dist/server/settings-service.js +105 -11
- package/electron/main.mjs +427 -0
- package/package.json +19 -5
- package/themes/md-preview.css +7328 -7253
- package/themes/white.css +7389 -7314
- package/web/dist/assets/{TerminalPanel-D29yC6uX.js → TerminalPanel-C2R8nfig.js} +1 -1
- package/web/dist/assets/{index-BByVm30o.css → index-CODfCAWK.css} +1 -1
- package/web/dist/assets/index-CeePtC8A.js +19 -0
- package/web/dist/index.html +2 -2
- package/web/dist/assets/index-DcIUI-e4.js +0 -19
package/README.md
CHANGED
|
@@ -1,20 +1,33 @@
|
|
|
1
|
-
|
|
1
|
+
<div align="center">
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
# 💬 pi-web-ui
|
|
4
4
|
|
|
5
|
-
[
|
|
6
|
-
[](https://nodejs.org/)
|
|
7
|
-
[](LICENSE)
|
|
5
|
+
**English** | [Chinese (Simplified)](https://github.com/xing-shuyin/pi-web-ui/blob/main/README.zh-CN.md)
|
|
8
6
|
|
|
9
|
-
|
|
10
|
-
> and run your workspace from one place.
|
|
7
|
+
*The polished browser cockpit for the [pi coding agent](https://pi.dev).*
|
|
11
8
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
9
|
+
<p>
|
|
10
|
+
<a href="https://www.npmjs.com/package/pi-web-ui"><img src="https://img.shields.io/npm/v/pi-web-ui?color=cb3837&logo=npm&label=pi-web-ui" alt="npm version"></a>
|
|
11
|
+
<a href="https://nodejs.org/"><img src="https://img.shields.io/node/v/pi-web-ui?logo=node.js&logoColor=white" alt="Node.js"></a>
|
|
12
|
+
<a href="LICENSE"><img src="https://img.shields.io/github/license/xing-shuyin/pi-web-ui" alt="License"></a>
|
|
13
|
+
<a href="https://www.npmjs.com/package/pi-web-ui"><img src="https://img.shields.io/npm/dm/pi-web-ui?label=downloads" alt="npm downloads"></a>
|
|
14
|
+
<a href="https://github.com/xing-shuyin/pi-web-ui/actions/workflows/ci.yml"><img src="https://img.shields.io/github/actions/workflow/status/xing-shuyin/pi-web-ui/ci.yml?branch=main&label=CI" alt="CI status"></a>
|
|
15
|
+
<a href="https://github.com/xing-shuyin/pi-web-ui/stargazers"><img src="https://img.shields.io/github/stars/xing-shuyin/pi-web-ui?style=social" alt="GitHub stars"></a>
|
|
16
|
+
<a href="https://github.com/xing-shuyin/pi-web-ui/fork"><img src="https://img.shields.io/badge/PRs-welcome-brightgreen?style=flat" alt="PRs welcome"></a>
|
|
17
|
+
</p>
|
|
18
|
+
|
|
19
|
+
Stream conversations, inspect tool calls, manage files, and run your workspace — all from one place.
|
|
20
|
+
|
|
21
|
+

|
|
22
|
+
|
|
23
|
+
</div>
|
|
24
|
+
|
|
25
|
+
A web chat interface for the [pi coding agent](https://pi.dev). The agent runs
|
|
26
|
+
**in-process** via the pi SDK and streams events to the browser over WebSocket:
|
|
27
|
+
thinking blocks, tool calls, file trees, a built-in terminal, model management,
|
|
28
|
+
theme switching, and a full settings panel — tuned for daily development.
|
|
29
|
+
|
|
30
|
+
> **Requirements** — Node.js ≥ 22.19 and a configured pi install.
|
|
18
31
|
|
|
19
32
|
## More from the author
|
|
20
33
|
|
|
@@ -22,34 +35,54 @@ Requires Node.js ≥ 22.19 and a configured pi install.
|
|
|
22
35
|
>
|
|
23
36
|
> [**dsh-ui-tools**](https://github.com/xing-shuyin/dsh-ui-tools) is the author's companion project for building and extending UI tools in the DSH ecosystem.
|
|
24
37
|
|
|
38
|
+
## ✨ Highlights
|
|
39
|
+
|
|
40
|
+
| 💬 **Chat that works like you do** | 🖼️ **Files & images** | 🧩 **Extensible by design** | 🔒 **Private by default** |
|
|
41
|
+
| --- | --- | --- | --- |
|
|
42
|
+
| Streaming replies, steer & follow-up queueing, slash commands, multiple conversations per project, edit-&-re-ask. | Attach files, paste images, ask about pictures (vision bridge), preview anything with GBK fallback. | Drop-in UI **plugins** (extra top-bar tabs + agent tools) and standalone **themes** — no rebuild, no restart. | Loopback-only, credential-safe: provider keys & headers never reach the browser. |
|
|
43
|
+
|
|
44
|
+
## 📚 Table of Contents
|
|
45
|
+
|
|
46
|
+
- 🚀 [Features](#features)
|
|
47
|
+
- 🖼️ [Screenshots](#screenshots)
|
|
48
|
+
- 📦 [Install](#install)
|
|
49
|
+
- ⚡ [Quick start](#quick-start)
|
|
50
|
+
- 🖥️ [System service](#system-service)
|
|
51
|
+
- 🧩 [Plugins](#plugins)
|
|
52
|
+
- 🎨 [Themes](#themes)
|
|
53
|
+
- 🔒 [Security](#security)
|
|
54
|
+
- 🌐 [Reverse proxy (nginx)](#reverse-proxy-nginx)
|
|
55
|
+
- 🤝 [Contribute](#contribute)
|
|
56
|
+
- 📄 [License](#license)
|
|
57
|
+
|
|
25
58
|
## Features
|
|
26
59
|
|
|
27
|
-
|
|
60
|
+
### 💬 Chat
|
|
28
61
|
|
|
29
|
-
- Streaming agent chat over WebSocket — the pi SDK runs in-process; events are pushed as snapshots (60 ms throttled) and the browser renders them.
|
|
62
|
+
- **Streaming agent chat over WebSocket** — the pi SDK runs in-process; events are pushed as snapshots (60 ms throttled) and the browser renders them.
|
|
30
63
|
- Thinking blocks, tool-call cards and bash outputs with live status (running → finished · waiting for the model · duration).
|
|
31
|
-
-
|
|
64
|
+
- **Steer (follow-up queueing)** — send a follow-up while the agent is replying; it is queued and injected as soon as the current turn's tool calls settle (the "Interrupt" equivalent of the pi CLI).
|
|
32
65
|
- **Slash commands** — `/` opens a command picker (built-in / extension / template / skill); built-ins include `/new /model /compact /cwd /thinking /resume`, plus `/help` (command list) and `/copy` (copy last reply).
|
|
33
66
|
- **Multiple conversations per project** — each conversation gets its own agent runtime and keeps running in the background after you switch away; the "Running conversations" list shows stream progress and lets you switch back.
|
|
34
67
|
- **Edit & re-ask** — fork any past question into a new branch and re-prompt; the original conversation stays untouched.
|
|
35
68
|
- Long threads auto-collapse messages older than 30 into lazy summary rows (click to expand).
|
|
36
69
|
- Question navigation — a floating rail plus per-question tags to jump between questions.
|
|
37
70
|
|
|
38
|
-
|
|
71
|
+
### 🖼️ Files, images & attachments
|
|
39
72
|
|
|
40
73
|
- Three attachment modes: `inline` (≤12 KB), `reference` (path only), `lines` (selected ranges) — over-limit ones degrade automatically.
|
|
41
74
|
- Paste / drag-drop / upload images — resized client-side and sent as image content when the model supports vision (warning otherwise).
|
|
42
75
|
- **Vision bridge** — when the current model is text-only, images are transcribed into text evidence by an auto-discovered vision model (cached per batch; model & on/off configurable in Settings).
|
|
43
76
|
- Attach arbitrary files without a workspace path — stored in a global uploads dir, inlined when small, referenced by absolute path otherwise.
|
|
44
77
|
- File preview — line numbers, click/drag/Shift selection (add to chat as `lines`), GBK fallback decoding, binary hex view, media preview over HTTP with Range support, and a download button.
|
|
45
|
-
- Live file tree — the server watches the listed directory (fs.watch) and re-lists on change; oversized directories show a truncation warning.
|
|
78
|
+
- Live file tree — the server watches the listed directory (`fs.watch`) and re-lists on change; oversized directories show a truncation warning.
|
|
46
79
|
|
|
47
|
-
|
|
80
|
+
### 🖥️ Terminal & Git
|
|
48
81
|
|
|
49
82
|
- Built-in terminal (xterm.js + node-pty) with per-client PTY management; Windows auto-selects Git Bash (busybox fallback).
|
|
50
83
|
- **Source control (Git) panel** — status / branch / diff / untracked files via a hidden query terminal; commit, switch branch, push and pull run in the visible terminal and auto-switch to the terminal view.
|
|
51
84
|
|
|
52
|
-
|
|
85
|
+
### 🎛️ Models & settings
|
|
53
86
|
|
|
54
87
|
- Theme switching — pick a theme in the top bar; each theme is a full standalone stylesheet (default dark + a bundled light). See [Themes](#themes) for how to add your own or contribute one.
|
|
55
88
|
- Model management — edit `models.json` in the UI and set per-provider API keys (keys/headers never leave the server).
|
|
@@ -57,19 +90,19 @@ Requires Node.js ≥ 22.19 and a configured pi install.
|
|
|
57
90
|
- First-run setup wizard.
|
|
58
91
|
- Settings panel — system prompt (append or replace), toggle skills/extensions on/off with immediate effect, save/apply/delete settings presets, and vision-bridge model & switch.
|
|
59
92
|
|
|
60
|
-
|
|
93
|
+
### 🎯 Goal mode
|
|
61
94
|
|
|
62
95
|
- Goal bar — set a target with a review model, max rounds and a lock switch.
|
|
63
|
-
- Goal wizard (
|
|
96
|
+
- Goal wizard (**AI Refine**) — turns a raw request into a concrete goal through a guided questionnaire.
|
|
64
97
|
- Automatic review loop — after each turn an independent review session checks the goal against the final text and `git diff HEAD`; on fail the feedback is injected as steer until it passes (or the round cap is hit).
|
|
65
98
|
|
|
66
|
-
|
|
99
|
+
### ⚙️ Background tasks
|
|
67
100
|
|
|
68
101
|
- Background-task panel — servers launched by the agent are detected via port snapshots and listed (port/pid/name); stop one or kill all.
|
|
69
102
|
- Tool watchdog — a tool call running over 20 minutes is aborted automatically.
|
|
70
103
|
- **Stop bash command only** — abort a running bash tool without killing the conversation.
|
|
71
104
|
|
|
72
|
-
|
|
105
|
+
### 🛡️ Safety & operations
|
|
73
106
|
|
|
74
107
|
- Loopback-only by default; set `PI_WEB_HOST=0.0.0.0` for LAN / containers.
|
|
75
108
|
- WebSocket Origin/Host same-authority check — cross-origin pages are rejected (403); `PI_WEB_ALLOW_ORIGINS` whitelist for reverse proxies.
|
|
@@ -77,20 +110,25 @@ Requires Node.js ≥ 22.19 and a configured pi install.
|
|
|
77
110
|
- Credentials stay server-side — provider headers are never sent to the browser.
|
|
78
111
|
- Sound alerts, Chinese/English UI, and a recent-projects list (click to switch workspace).
|
|
79
112
|
|
|
80
|
-
|
|
113
|
+
### 🚢 Deploy & update
|
|
81
114
|
|
|
82
115
|
- Foreground, global npm install, Docker (docker-compose), macOS launchd, Linux systemd, Windows Task Scheduler, and a desktop shortcut (`server shortcut`).
|
|
83
116
|
- In-app self-update — checks the npm registry, installs and auto-restarts the service.
|
|
84
117
|
|
|
85
|
-
## Screenshots
|
|
86
|
-
|
|
87
|
-

|
|
88
118
|
|
|
89
|
-
|
|
119
|
+
## Screenshots
|
|
90
120
|
|
|
91
|
-
|
|
121
|
+
<table>
|
|
122
|
+
<tr>
|
|
123
|
+
<td align="center"><img src="https://raw.githubusercontent.com/xing-shuyin/pi-web-ui/main/assets/shot1.png" alt="Settings panel"><br><sub>Settings panel</sub></td>
|
|
124
|
+
<td align="center"><img src="https://raw.githubusercontent.com/xing-shuyin/pi-web-ui/main/assets/shot2.jpeg" alt="Built-in terminal"><br><sub>Built-in terminal</sub></td>
|
|
125
|
+
</tr>
|
|
126
|
+
<tr>
|
|
127
|
+
<td align="center"><img src="https://raw.githubusercontent.com/xing-shuyin/pi-web-ui/main/assets/shot3.jpeg" alt="Chat interface"><br><sub>Chat interface</sub></td>
|
|
128
|
+
<td align="center"><img src="https://raw.githubusercontent.com/xing-shuyin/pi-web-ui/main/assets/shot4.jpeg" alt="Git source control panel"><br><sub>Git source control panel</sub></td>
|
|
129
|
+
</tr>
|
|
130
|
+
</table>
|
|
92
131
|
|
|
93
|
-

|
|
94
132
|
|
|
95
133
|
## Install
|
|
96
134
|
|
|
@@ -109,26 +147,29 @@ silences the warning):
|
|
|
109
147
|
npm i -g --allow-scripts=node-pty,@google/genai,protobufjs pi-web-ui@latest
|
|
110
148
|
```
|
|
111
149
|
|
|
112
|
-
|
|
150
|
+
|
|
151
|
+
## Quick start
|
|
152
|
+
|
|
153
|
+
**Start**
|
|
113
154
|
|
|
114
155
|
```bash
|
|
115
156
|
pi-web-ui # foreground, http://localhost:8787
|
|
116
157
|
PORT=9000 PI_WEB_CWD=/path/to/project pi-web-ui # custom port / workspace
|
|
117
158
|
```
|
|
118
159
|
|
|
119
|
-
|
|
160
|
+
**Stop**
|
|
120
161
|
|
|
121
162
|
- **Foreground**: press `Ctrl+C` in the terminal running it.
|
|
122
163
|
- **As a service**: `pi-web-ui server stop` (stops the instance; auto-start stays until `server uninstall`).
|
|
123
164
|
|
|
124
|
-
|
|
165
|
+
**Update**
|
|
125
166
|
|
|
126
167
|
```bash
|
|
127
168
|
npm i -g pi-web-ui@latest # upgrade to the latest published version
|
|
128
169
|
pi-web-ui server restart # restart the service to apply it (foreground: restart manually)
|
|
129
170
|
```
|
|
130
171
|
|
|
131
|
-
|
|
172
|
+
**Uninstall**
|
|
132
173
|
|
|
133
174
|
```bash
|
|
134
175
|
npm uninstall -g pi-web-ui
|
|
@@ -137,7 +178,8 @@ npm uninstall -g pi-web-ui
|
|
|
137
178
|
Uninstalling does **not** delete your chats — session data lives in
|
|
138
179
|
`<cwd>/.pi-web` (or `PI_WEB_DATA_DIR`) and survives uninstall/upgrade.
|
|
139
180
|
|
|
140
|
-
|
|
181
|
+
|
|
182
|
+
## System service
|
|
141
183
|
|
|
142
184
|
```bash
|
|
143
185
|
pi-web-ui server install --port 9000 --cwd /path/to/project # install + start
|
|
@@ -164,7 +206,8 @@ Options: `--port` (default 8787), `--cwd` (workspace), `--data-dir` (sessions),
|
|
|
164
206
|
regenerates the config and restarts the service — that's how you change its
|
|
165
207
|
port/cwd.
|
|
166
208
|
|
|
167
|
-
|
|
209
|
+
|
|
210
|
+
## Plugins
|
|
168
211
|
|
|
169
212
|
Plugins are optional UI components (extra top-bar tabs backed by their own
|
|
170
213
|
client view, optionally with a server-side entry and agent tools). They live in
|
|
@@ -257,6 +300,7 @@ pi-web-ui uninstall <id> # remove a plugin
|
|
|
257
300
|
disappears. Plugin configuration written inside the plugin dir is removed
|
|
258
301
|
too — back up `<dataDir>/plugins/<id>/config.json` first if you need it.
|
|
259
302
|
|
|
303
|
+
|
|
260
304
|
## Themes
|
|
261
305
|
|
|
262
306
|
Each theme is a **complete standalone stylesheet** — a full copy of the bundled dark `web/src/styles.css` with a different palette (no CSS-variable extraction, no base file to include). Picking a theme swaps the whole file, so any theme works with every build.
|
|
@@ -299,6 +343,7 @@ Want your theme shipped to everyone? Open a pull request at [github.com/xing-shu
|
|
|
299
343
|
|
|
300
344
|
Rules for merged themes: the file must be a single self-contained CSS file, be a full standalone theme (no imports of the base `styles.css`), set the `--term-*` variables for a readable terminal, and override `.hljs` syntax colors for readable code.
|
|
301
345
|
|
|
346
|
+
|
|
302
347
|
## Security
|
|
303
348
|
|
|
304
349
|
- **Loopback-only by default** — the server binds `127.0.0.1` and is not
|
|
@@ -316,6 +361,7 @@ Rules for merged themes: the file must be a single self-contained CSS file, be a
|
|
|
316
361
|
`Authorization` / API keys) are never sent to the browser; the model
|
|
317
362
|
management UI edits everything else and the server preserves the headers.
|
|
318
363
|
|
|
364
|
+
|
|
319
365
|
## Reverse proxy (nginx)
|
|
320
366
|
|
|
321
367
|
Serve pi-web-ui behind nginx on the same host (it binds loopback only, so a
|
|
@@ -382,7 +428,31 @@ Key points:
|
|
|
382
428
|
|
|
383
429
|
Full working example (with an frp tunnel): `deploy/nginx-subpath.conf`.
|
|
384
430
|
|
|
385
|
-
## License
|
|
386
431
|
|
|
387
|
-
|
|
432
|
+
## Contribute
|
|
433
|
+
|
|
434
|
+
pi-web-ui is a small open-source project — **your contributions are what make it grow**. Code, plugins, themes, docs, translations, ideas: everything is welcome, and every merged PR ships to all users with the next `npm publish`. ❤️
|
|
435
|
+
|
|
436
|
+
| Way to contribute | How to get started |
|
|
437
|
+
| --- | --- |
|
|
438
|
+
| 🧩 **Write a plugin** | Build your own UI tab + agent tools. Copy `dev/plugins/demo-mailbox` as the minimal template (it doubles as the test fixture), develop locally, then either open a PR to ship it in the [catalog](#plugin-catalog) or [publish it standalone](https://github.com/xing-shuyin/pi-web-ui/tree/main/dev/plugins). |
|
|
439
|
+
| 🎨 **Contribute a theme** | Copy `themes/light.css` as a self-contained template, tweak the `:root` palette + `--term-*` + `.hljs`, verify with `npm run dev`, then open a PR — full walkthrough in [Contributing a theme](#contributing-a-theme-to-the-repository-github). |
|
|
440
|
+
| 💻 **Fix a bug / add a feature** | Look for [open issues](https://github.com/xing-shuyin/pi-web-ui/issues) or propose something new. Fork → branch → PR. Keep the code conventions in `AGENTS.md` (tabs, i18n keys in both languages, protocol changes in `server/protocol.ts`). |
|
|
441
|
+
| 📖 **Docs & translations** | Improve the READMEs, write plugin docs, fix typos, or help translate the UI / docs into more languages. |
|
|
442
|
+
| 💡 **Ideas & feedback** | Open an [issue](https://github.com/xing-shuyin/pi-web-ui/issues) or start a [discussion](https://github.com/xing-shuyin/pi-web-ui/discussions) — feature requests, bug reports, UI polish ideas, deployment experience reports. |
|
|
443
|
+
|
|
444
|
+
**Before opening a PR**, a quick sanity pass keeps reviewers happy:
|
|
445
|
+
|
|
446
|
+
- `npm run check:protocol` + `npm test` — protocol sync and unit tests.
|
|
447
|
+
- `npm run typecheck` — no type errors.
|
|
448
|
+
- `npm run build` — both frontend and backend compile.
|
|
449
|
+
- For protocol changes: add branches in both `server/index.ts` and `web/src/use-chat.ts` (see the "Protocol single source" note in `AGENTS.md`).
|
|
450
|
+
|
|
451
|
+
> Enjoying pi-web-ui? Give the repo a ⭐ — it helps others find it. And if you
|
|
452
|
+
> built something cool on top (plugin, theme, deployment recipe), tell us — we
|
|
453
|
+
> love showcasing community work.
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
## License
|
|
388
457
|
|
|
458
|
+
[MIT](LICENSE)
|
package/README.zh-CN.md
CHANGED
|
@@ -354,6 +354,27 @@ server {
|
|
|
354
354
|
|
|
355
355
|
带 frp 内网穿透的完整可运行示例:`deploy/nginx-subpath.conf`。
|
|
356
356
|
|
|
357
|
+
## 参与贡献
|
|
358
|
+
|
|
359
|
+
pi-web-ui 是一个小型开源项目 —— **你的贡献就是它成长的力量**。代码、插件、主题、文档、翻译、想法,统统欢迎;每一个合并的 PR 都会随下一次 `npm publish` 送达所有用户。❤️
|
|
360
|
+
|
|
361
|
+
| 贡献方式 | 如何开始 |
|
|
362
|
+
| --- | --- |
|
|
363
|
+
| 🧩 **写插件** | 打造你自己的界面 tab + AI 工具。以 `dev/plugins/demo-mailbox` 为最小模板(它兼作测试夹具),本地开发后既可开 PR 收录进[插件目录](#插件目录),也可独立发布。 |
|
|
364
|
+
| 🎨 **贡献主题** | 以 `themes/light.css` 为自包含模板,调整 `:root` 配色 + `--term-*` + `.hljs`,用 `npm run dev` 验证后开 PR —— 完整步骤见[向仓库贡献主题](#向仓库贡献主题github)。 |
|
|
365
|
+
| 💻 **修 bug / 加功能** | 在 [Issues](https://github.com/xing-shuyin/pi-web-ui/issues) 里挑一个,或提出新想法。Fork → 分支 → PR。代码约定见 `AGENTS.md`(Tab 缩进、i18n 双语 key、协议改动只动 `server/protocol.ts`)。 |
|
|
366
|
+
| 📖 **文档与翻译** | 完善 README、补插件文档、改错别字,或帮忙把界面/文档翻译成更多语言。 |
|
|
367
|
+
| 💡 **想法与反馈** | 在 [Issues](https://github.com/xing-shuyin/pi-web-ui/issues) 或 [Discussions](https://github.com/xing-shuyin/pi-web-ui/discussions) 里开帖 —— 功能建议、bug 报告、界面优化点子、部署经验分享都欢迎。 |
|
|
368
|
+
|
|
369
|
+
**开 PR 前**,快速自检能让维护者更省心:
|
|
370
|
+
|
|
371
|
+
- `npm run check:protocol` + `npm test` —— 协议同步与单元测试。
|
|
372
|
+
- `npm run typecheck` —— 无类型错误。
|
|
373
|
+
- `npm run build` —— 前后端都能编译。
|
|
374
|
+
- 涉及协议改动:`server/index.ts` 与 `web/src/use-chat.ts` 两端 dispatch 都要加分支(详见 `AGENTS.md`「协议单源」)。
|
|
375
|
+
|
|
376
|
+
> 喜欢 pi-web-ui?给仓库点个 ⭐,帮助更多人发现它。如果你在上面做了很酷的东西(插件、主题、部署方案),记得告诉我们 —— 我们乐于展示社区作品。
|
|
377
|
+
|
|
357
378
|
## License
|
|
358
379
|
|
|
359
380
|
MIT
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* snapshots. The frontend is snapshot-driven (server is the source of truth),
|
|
11
11
|
* so reconnects just re-request a snapshot.
|
|
12
12
|
*/
|
|
13
|
-
import { spawn } from "node:child_process";
|
|
13
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
14
14
|
import { existsSync, readFileSync, rmSync, statSync, writeFileSync, mkdirSync, watch, } from "node:fs";
|
|
15
15
|
import { basename, dirname, join, relative, resolve, sep } from "node:path";
|
|
16
16
|
import { fileURLToPath } from "node:url";
|
|
@@ -550,6 +550,8 @@ export class ClientSession {
|
|
|
550
550
|
flushSnapshot: () => this.flushSnapshot(),
|
|
551
551
|
isDisposed: () => this.disposed,
|
|
552
552
|
getSession: () => this.session,
|
|
553
|
+
cwd: () => this.cwd,
|
|
554
|
+
agentDir: () => this.agentDir,
|
|
553
555
|
isStreaming: () => this.session.isStreaming,
|
|
554
556
|
reloadSession: async () => {
|
|
555
557
|
await this.session.reload();
|
|
@@ -1251,6 +1253,7 @@ export class ClientSession {
|
|
|
1251
1253
|
tools: state.tools.map((t) => t.name),
|
|
1252
1254
|
version: ++this.version,
|
|
1253
1255
|
piConfigured: this.isPiConfigured(),
|
|
1256
|
+
piAgentInstalled: this.isPiCliInstalled(),
|
|
1254
1257
|
stats,
|
|
1255
1258
|
};
|
|
1256
1259
|
}
|
|
@@ -1336,6 +1339,38 @@ export class ClientSession {
|
|
|
1336
1339
|
this.piCheckCache = { at: now, configured };
|
|
1337
1340
|
return configured;
|
|
1338
1341
|
}
|
|
1342
|
+
/**
|
|
1343
|
+
* Whether the pi CLI binary is installed and runnable (`pi --version`
|
|
1344
|
+
* probe). Cached machine-wide (same binary for every client) for 10s —
|
|
1345
|
+
* the check is only rerun after install or when the cache expires.
|
|
1346
|
+
*/
|
|
1347
|
+
static piCliProbe = null;
|
|
1348
|
+
static PI_CLI_PROBE_TTL_MS = 10_000;
|
|
1349
|
+
isPiCliInstalled() {
|
|
1350
|
+
const now = Date.now();
|
|
1351
|
+
const cached = ClientSession.piCliProbe;
|
|
1352
|
+
if (cached && now - cached.at < ClientSession.PI_CLI_PROBE_TTL_MS)
|
|
1353
|
+
return cached.installed;
|
|
1354
|
+
let installed = false;
|
|
1355
|
+
try {
|
|
1356
|
+
const res = spawnSync("pi", ["--version"], {
|
|
1357
|
+
timeout: 5000,
|
|
1358
|
+
stdio: "ignore",
|
|
1359
|
+
// Windows: `pi` resolves to a pi.cmd shim — spawnSync can only
|
|
1360
|
+
// exec those through a shell (else ENOENT).
|
|
1361
|
+
shell: process.platform === "win32",
|
|
1362
|
+
});
|
|
1363
|
+
installed = !res.error && res.status === 0;
|
|
1364
|
+
}
|
|
1365
|
+
catch {
|
|
1366
|
+
installed = false;
|
|
1367
|
+
}
|
|
1368
|
+
ClientSession.piCliProbe = { at: now, installed };
|
|
1369
|
+
return installed;
|
|
1370
|
+
}
|
|
1371
|
+
static invalidatePiCliProbe() {
|
|
1372
|
+
ClientSession.piCliProbe = null;
|
|
1373
|
+
}
|
|
1339
1374
|
/**
|
|
1340
1375
|
* Run a command async, collecting stdout+stderr; kills on timeout.
|
|
1341
1376
|
* Never throws / never crashes the server: spawn errors (ENOENT etc.)
|
|
@@ -1483,6 +1518,9 @@ export class ClientSession {
|
|
|
1483
1518
|
text: `pi agent 安装失败:${err.message}`,
|
|
1484
1519
|
});
|
|
1485
1520
|
}
|
|
1521
|
+
// The CLI may just have landed on PATH (or the install may have failed) —
|
|
1522
|
+
// drop the probe cache so the next snapshot re-checks.
|
|
1523
|
+
ClientSession.invalidatePiCliProbe();
|
|
1486
1524
|
this.flushSnapshot();
|
|
1487
1525
|
}
|
|
1488
1526
|
/** Send a snapshot immediately (cancels any pending throttled one).
|
|
@@ -178,6 +178,7 @@ export class ClientStateStore {
|
|
|
178
178
|
terminalBash: s?.settings?.terminalBash ?? false,
|
|
179
179
|
terminalBashIdleMs: s?.settings?.terminalBashIdleMs ?? 15_000,
|
|
180
180
|
thinkingWrap: s?.settings?.thinkingWrap ?? false,
|
|
181
|
+
toolsWrap: s?.settings?.toolsWrap ?? true,
|
|
181
182
|
visionBridgeEnabled: s?.settings?.visionBridgeEnabled ?? true,
|
|
182
183
|
visionBridgeModel: s?.settings?.visionBridgeModel ?? null,
|
|
183
184
|
visionBridgePromptMode: s?.settings?.visionBridgePromptMode === "replace" ? "replace" : "append",
|
|
@@ -201,6 +202,7 @@ export class ClientStateStore {
|
|
|
201
202
|
terminalBash: settings.terminalBash ?? cur.terminalBash ?? false,
|
|
202
203
|
terminalBashIdleMs: settings.terminalBashIdleMs ?? cur.terminalBashIdleMs ?? 15_000,
|
|
203
204
|
thinkingWrap: settings.thinkingWrap ?? cur.thinkingWrap ?? false,
|
|
205
|
+
toolsWrap: settings.toolsWrap ?? cur.toolsWrap ?? true,
|
|
204
206
|
visionBridgeEnabled: settings.visionBridgeEnabled ?? cur.visionBridgeEnabled ?? true,
|
|
205
207
|
visionBridgeModel: settings.visionBridgeModel ?? cur.visionBridgeModel ?? null,
|
|
206
208
|
visionBridgePromptMode: settings.visionBridgePromptMode ??
|
package/dist/server/index.js
CHANGED
|
@@ -179,6 +179,11 @@ const here = dirname(fileURLToPath(import.meta.url)); // <pkg>/dist/server or <p
|
|
|
179
179
|
// Resolve the package root robustly: dev runs from <repo>/server (tsx), prod
|
|
180
180
|
// from <pkg>/dist/server — the ancestor that actually has package.json wins.
|
|
181
181
|
function resolvePkgRoot() {
|
|
182
|
+
// PI_WEB_PKG_ROOT: Electron 桌面版打包后,server 子进程从 extraResources 目录
|
|
183
|
+
//(process.resourcesPath)加载 web/dist 和 themes。通过这个 env var 告诉
|
|
184
|
+
// server 去哪里找 pkgRoot,避免 resolvePkgRoot 的候选路径找不到 package.json。
|
|
185
|
+
if (process.env.PI_WEB_PKG_ROOT)
|
|
186
|
+
return process.env.PI_WEB_PKG_ROOT;
|
|
182
187
|
const candidates = [
|
|
183
188
|
resolve(here, ".."),
|
|
184
189
|
resolve(here, "..", ".."),
|
|
@@ -717,6 +722,7 @@ wss.on("connection", (ws) => {
|
|
|
717
722
|
terminalBash: msg.terminalBash,
|
|
718
723
|
terminalBashIdleMs: msg.terminalBashIdleMs,
|
|
719
724
|
thinkingWrap: msg.thinkingWrap,
|
|
725
|
+
toolsWrap: msg.toolsWrap,
|
|
720
726
|
visionBridgeEnabled: msg.visionBridgeEnabled,
|
|
721
727
|
visionBridgeModel: msg.visionBridgeModel,
|
|
722
728
|
visionBridgePromptMode: msg.visionBridgePromptMode,
|
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
* 预设存取 + 何时需要 reload」,真正动 runtime 的 session.reload() 走宿主回调
|
|
7
7
|
* (reloadSession 里还会刷新斜杠命令目录)。
|
|
8
8
|
*/
|
|
9
|
-
import {
|
|
9
|
+
import { existsSync, readdirSync } from "node:fs";
|
|
10
|
+
import { basename, dirname, join } from "node:path";
|
|
10
11
|
import { extensionKey } from "./client-state.js";
|
|
11
12
|
import { findVisionModels, SYSTEM_PROMPT } from "./vision-bridge.js";
|
|
12
13
|
export class SettingsService {
|
|
@@ -39,20 +40,88 @@ export class SettingsService {
|
|
|
39
40
|
this.pendingReload = false;
|
|
40
41
|
return v;
|
|
41
42
|
}
|
|
43
|
+
/** 判断某个 skill 名是否仍存在于磁盘任何来源(agent 区 / 项目 .pi / 祖先
|
|
44
|
+
* .agents/skills / npm 包内 skills)。被禁用且文件已删除的名字不应再
|
|
45
|
+
* 出现在设置面板,也不应留在持久化记录里。 */
|
|
46
|
+
skillStillOnDisk(name) {
|
|
47
|
+
const cwd = this.host.cwd();
|
|
48
|
+
const agentDir = this.host.agentDir();
|
|
49
|
+
const check = (base) => existsSync(join(base, name)) || existsSync(join(base, `${name}.md`));
|
|
50
|
+
// ① 用户区 <agentDir>/skills ② 项目 .pi/skills
|
|
51
|
+
if (check(join(agentDir, "skills")))
|
|
52
|
+
return true;
|
|
53
|
+
if (check(join(cwd, ".pi", "skills")))
|
|
54
|
+
return true;
|
|
55
|
+
// ③ 祖先链 .agents/skills(SDK collectAncestorAgentsSkillDirs 语义,最多上溯 6 层)
|
|
56
|
+
let dir = cwd;
|
|
57
|
+
for (let i = 0; i < 6 && dir !== dirname(dir); i++, dir = dirname(dir)) {
|
|
58
|
+
if (check(join(dir, ".agents", "skills")))
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
// ④ npm 包内 skills(agent 级 + 项目级,含 @scope 两级子包)
|
|
62
|
+
for (const npmRoot of [
|
|
63
|
+
join(agentDir, "npm", "node_modules"),
|
|
64
|
+
join(cwd, ".pi", "npm", "node_modules"),
|
|
65
|
+
]) {
|
|
66
|
+
try {
|
|
67
|
+
for (const entry of readdirSync(npmRoot, { withFileTypes: true })) {
|
|
68
|
+
if (!entry.isDirectory())
|
|
69
|
+
continue;
|
|
70
|
+
if (!entry.name.startsWith("@")) {
|
|
71
|
+
if (check(join(npmRoot, entry.name, "skills")))
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
for (const sub of readdirSync(join(npmRoot, entry.name), { withFileTypes: true })) {
|
|
76
|
+
if (sub.isDirectory() && check(join(npmRoot, entry.name, sub.name, "skills"))) {
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
// npm 目录不存在/不可读 → 不是来源
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
42
89
|
push() {
|
|
43
90
|
const disabledSkills = new Set(this.settings.disabledSkills);
|
|
44
91
|
const reviewDisabledSkills = new Set(this.settings.reviewDisabledSkills);
|
|
45
92
|
const disabledExts = new Set(this.settings.disabledExtensions);
|
|
93
|
+
let loadedSkillNames = null;
|
|
46
94
|
try {
|
|
47
|
-
|
|
48
|
-
|
|
95
|
+
const loadedSkills = this.host.getSession().resourceLoader.getSkills().skills;
|
|
96
|
+
const loadedExts = this.host.getSession().resourceLoader.getExtensions().extensions;
|
|
97
|
+
loadedSkillNames = new Set(loadedSkills.map((s) => s.name));
|
|
98
|
+
// Prune entries that no longer exist on disk AND aren't disabled
|
|
99
|
+
// (e.g. a skill/extension file was deleted). Disabled entries are
|
|
100
|
+
// kept so they can be re-enabled even when filtered out of the loader.
|
|
101
|
+
const keepSkills = new Set([
|
|
102
|
+
...loadedSkills.map((s) => s.name),
|
|
103
|
+
...this.settings.disabledSkills,
|
|
104
|
+
]);
|
|
105
|
+
const keepExts = new Set([
|
|
106
|
+
...loadedExts.map((e) => extensionKey(e)),
|
|
107
|
+
...this.settings.disabledExtensions,
|
|
108
|
+
]);
|
|
109
|
+
for (const name of [...this.knownSkills.keys()]) {
|
|
110
|
+
if (!keepSkills.has(name))
|
|
111
|
+
this.knownSkills.delete(name);
|
|
112
|
+
}
|
|
113
|
+
for (const id of [...this.knownExtensions.keys()]) {
|
|
114
|
+
if (!keepExts.has(id))
|
|
115
|
+
this.knownExtensions.delete(id);
|
|
116
|
+
}
|
|
117
|
+
for (const s of loadedSkills) {
|
|
49
118
|
this.knownSkills.set(s.name, {
|
|
50
119
|
name: s.name,
|
|
51
120
|
description: s.description,
|
|
52
121
|
enabled: true,
|
|
53
122
|
});
|
|
54
123
|
}
|
|
55
|
-
for (const e of
|
|
124
|
+
for (const e of loadedExts) {
|
|
56
125
|
const id = extensionKey(e);
|
|
57
126
|
const p = e.sourceInfo?.path ?? e.path;
|
|
58
127
|
this.knownExtensions.set(id, {
|
|
@@ -68,13 +137,33 @@ export class SettingsService {
|
|
|
68
137
|
catch {
|
|
69
138
|
// Session not ready yet — keep whatever we already know.
|
|
70
139
|
}
|
|
71
|
-
//
|
|
72
|
-
//
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
140
|
+
// 清理“源文件已删除”的禁用残留记录:磁盘上已不存在的技能名从
|
|
141
|
+
// disabledSkills / reviewDisabledSkills 持久化记录中移除——否则每次
|
|
142
|
+
// 推送都会把已删除的 skill 以灰条形式永恒地补回面板(“关闭过的
|
|
143
|
+
// skill 被一直记录”)。session 未就绪时保守跳过。
|
|
144
|
+
if (loadedSkillNames !== null) {
|
|
145
|
+
const stale = [
|
|
146
|
+
...new Set([...this.settings.disabledSkills, ...this.settings.reviewDisabledSkills]),
|
|
147
|
+
].filter((name) => !loadedSkillNames.has(name) && !this.skillStillOnDisk(name));
|
|
148
|
+
if (stale.length > 0) {
|
|
149
|
+
this.settings.disabledSkills = this.settings.disabledSkills.filter((n) => !stale.includes(n));
|
|
150
|
+
this.settings.reviewDisabledSkills = this.settings.reviewDisabledSkills.filter((n) => !stale.includes(n));
|
|
151
|
+
this.host.stateStore.saveSettings(this.host.clientId, {
|
|
152
|
+
disabledSkills: this.settings.disabledSkills,
|
|
153
|
+
reviewDisabledSkills: this.settings.reviewDisabledSkills,
|
|
154
|
+
});
|
|
76
155
|
}
|
|
77
156
|
}
|
|
157
|
+
// Disabled entries that still exist on disk are re-added (with the
|
|
158
|
+
// last-known description) so they can be re-enabled; entries whose
|
|
159
|
+
// source file was deleted are dropped instead of being resurrected.
|
|
160
|
+
for (const name of this.settings.disabledSkills) {
|
|
161
|
+
if (this.knownSkills.has(name))
|
|
162
|
+
continue;
|
|
163
|
+
if (!this.skillStillOnDisk(name))
|
|
164
|
+
continue;
|
|
165
|
+
this.knownSkills.set(name, { name, description: "", enabled: false });
|
|
166
|
+
}
|
|
78
167
|
for (const id of this.settings.disabledExtensions) {
|
|
79
168
|
if (!this.knownExtensions.has(id)) {
|
|
80
169
|
this.knownExtensions.set(id, {
|
|
@@ -103,6 +192,7 @@ export class SettingsService {
|
|
|
103
192
|
terminalBash: this.settings.terminalBash,
|
|
104
193
|
terminalBashIdleMs: this.settings.terminalBashIdleMs,
|
|
105
194
|
thinkingWrap: this.settings.thinkingWrap,
|
|
195
|
+
toolsWrap: this.settings.toolsWrap,
|
|
106
196
|
visionBridgeEnabled: this.settings.visionBridgeEnabled,
|
|
107
197
|
visionBridgeModel: this.settings.visionBridgeModel,
|
|
108
198
|
visionBridgePromptMode: this.settings.visionBridgePromptMode,
|
|
@@ -174,6 +264,9 @@ export class SettingsService {
|
|
|
174
264
|
if (partial.thinkingWrap !== undefined) {
|
|
175
265
|
this.settings.thinkingWrap = partial.thinkingWrap;
|
|
176
266
|
}
|
|
267
|
+
if (partial.toolsWrap !== undefined) {
|
|
268
|
+
this.settings.toolsWrap = partial.toolsWrap;
|
|
269
|
+
}
|
|
177
270
|
if (partial.visionBridgeEnabled !== undefined) {
|
|
178
271
|
this.settings.visionBridgeEnabled = partial.visionBridgeEnabled;
|
|
179
272
|
}
|
|
@@ -245,10 +338,11 @@ export class SettingsService {
|
|
|
245
338
|
reviewDisabledSkills: [
|
|
246
339
|
...(p.reviewDisabledSkills ?? this.settings.reviewDisabledSkills),
|
|
247
340
|
],
|
|
248
|
-
// Presets don't capture vision-bridge prefs — keep the current ones.
|
|
249
|
-
visionBridgeEnabled: this.settings.visionBridgeEnabled,
|
|
250
341
|
// 纯 UI 偏好不进预设——保留当前值。
|
|
251
342
|
thinkingWrap: this.settings.thinkingWrap,
|
|
343
|
+
toolsWrap: this.settings.toolsWrap,
|
|
344
|
+
// Presets don't capture vision-bridge prefs — keep the current ones.
|
|
345
|
+
visionBridgeEnabled: this.settings.visionBridgeEnabled,
|
|
252
346
|
visionBridgeModel: this.settings.visionBridgeModel,
|
|
253
347
|
visionBridgePromptMode: this.settings.visionBridgePromptMode,
|
|
254
348
|
visionBridgePrompt: this.settings.visionBridgePrompt,
|