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 CHANGED
@@ -1,20 +1,33 @@
1
- # pi-web-ui
1
+ <div align="center">
2
2
 
3
- **English** | [简体中文](https://github.com/xing-shuyin/pi-web-ui/blob/main/README.zh-CN.md)
3
+ # 💬 pi-web-ui
4
4
 
5
- [![npm version](https://img.shields.io/npm/v/pi-web-ui?color=cb3837&logo=npm)](https://www.npmjs.com/package/pi-web-ui)
6
- [![Node.js](https://img.shields.io/node/v/pi-web-ui?logo=node.js&logoColor=white)](https://nodejs.org/)
7
- [![License](https://img.shields.io/github/license/xing-shuyin/pi-web-ui)](LICENSE)
5
+ **English** | [Chinese (Simplified)](https://github.com/xing-shuyin/pi-web-ui/blob/main/README.zh-CN.md)
8
6
 
9
- > A polished browser interface for pi: stream conversations, inspect tools, manage files,
10
- > and run your workspace from one place.
7
+ *The polished browser cockpit for the [pi coding agent](https://pi.dev).*
11
8
 
12
- A web chat interface for the [pi coding agent](https://pi.dev) — the agent runs
13
- in-process via the pi SDK and streams events to the browser over WebSocket. Chat
14
- with thinking blocks and tool calls, attach files, ask about images, use a
15
- built-in terminal, manage models, tweak the system prompt, toggle skills and
16
- extensions on/off, and save/apply settings presets — all from a settings panel.
17
- Requires Node.js ≥ 22.19 and a configured pi install.
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
+ ![Git source control panel](https://raw.githubusercontent.com/xing-shuyin/pi-web-ui/main/assets/shot4.jpeg)
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
- **Chat**
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
- - **补充 (steer)** — 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).
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
- **Files, images & attachments**
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
- **Terminal & Git**
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
- **Models & settings**
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
- **Goal mode**
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 ("AI 提炼") — turns a raw request into a concrete goal through a guided questionnaire.
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
- **Background tasks**
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
- **Safety & operations**
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
- **Deploy & update**
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
- ![Settings panel](https://raw.githubusercontent.com/xing-shuyin/pi-web-ui/main/assets/shot1.png)
88
118
 
89
- ![Built-in terminal](https://raw.githubusercontent.com/xing-shuyin/pi-web-ui/main/assets/shot2.jpeg)
119
+ ## Screenshots
90
120
 
91
- ![Chat interface](https://raw.githubusercontent.com/xing-shuyin/pi-web-ui/main/assets/shot3.jpeg)
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
- ![Git source control panel](https://raw.githubusercontent.com/xing-shuyin/pi-web-ui/main/assets/shot4.jpeg)
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
- ## Start
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
- ## Stop
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
- ## Update
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
- ## Uninstall
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
- ## Run as a system service (auto-start on boot)
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
- ## Plugins (UI extensions)
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
- MIT
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 ??
@@ -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,
@@ -8,4 +8,4 @@
8
8
  * its own copy in web/src/protocol-version.ts; scripts/check-protocol-sync.mjs
9
9
  * verifies the two never drift.
10
10
  */
11
- export const PROTOCOL_VERSION = 9;
11
+ export const PROTOCOL_VERSION = 10;
@@ -6,7 +6,8 @@
6
6
  * 预设存取 + 何时需要 reload」,真正动 runtime 的 session.reload() 走宿主回调
7
7
  * (reloadSession 里还会刷新斜杠命令目录)。
8
8
  */
9
- import { basename } from "node:path";
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
- // Refresh the cache with the CURRENTLY loaded set (post-filter).
48
- for (const s of this.host.getSession().resourceLoader.getSkills().skills) {
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 this.host.getSession().resourceLoader.getExtensions().extensions) {
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
- // Disabled entries are filtered out of the loader — keep them in the
72
- // panel (with the last-known description) so they can be re-enabled.
73
- for (const name of this.settings.disabledSkills) {
74
- if (!this.knownSkills.has(name)) {
75
- this.knownSkills.set(name, { name, description: "", enabled: false });
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,