pi-web-ui 0.11.5 → 0.12.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,428 +1,441 @@
1
- # pi-web-ui
2
-
3
- **English** | [简体中文](README.zh-CN.md)
4
-
5
- A web chat interface for the [pi coding agent](https://pi.dev), built directly on
6
- the **pi SDK** ([`@earendil-works/pi-coding-agent`](https://www.npmjs.com/package/@earendil-works/pi-coding-agent)) —
7
- no subprocess, no JSON-RPC shim. The agent runs in the server process and streams
8
- events to the browser over WebSocket.
9
-
10
- Inspired by [Pintra (pi-vsc)](https://github.com/bilalbentoumi/pi-vsc), which does
11
- the same thing inside VS Code by spawning `pi --mode rpc`. This project instead
12
- uses the SDK's `createAgentSessionRuntime` API in-process (the SDK docs recommend
13
- this over RPC for Node.js apps), so you get type safety, direct state access, and
14
- your existing pi auth/config/extensions — nothing extra to install or configure.
15
-
16
- ## Features
17
-
18
- - 🧠 Full agent loop with **thinking** blocks (collapsible) and streaming text
19
- - 🛠 Tool execution cards with **live output streaming**, status (queued → running → done/error), and copyable arguments
20
- - 💬 Session **history persisted per browser** (localStorage clientId + per-client session dirs) — refresh or restart and your chats come back. The conversation panel also lists the pi CLI/TUI sessions for the current folder (tagged `TUI`), so you can resume a terminal conversation from the web UI
21
- - 📂 **Project memory**: the last workspace of each browser is remembered and restored on restart; a "Recent projects" list in the left panel switches workspaces in one click, and each project keeps its own sessions so you can always pick up an old conversation
22
- - ✏️ **Edit & re-ask**: every past question has an edit button — change it and re-ask from that point. The server forks a new branch session (keeping the full history before that question) while the original conversation stays untouched in the session list
23
- - ⚡ **Long chats stay fast**: past 30 messages, older messages collapse into summary rows (role + first-line preview + block counts — no Markdown/thinking/tool output rendered); click to expand the full content. The latest 15 messages always render in full
24
- - ⬇️ **Self-update**: the top-right corner shows the running version; the update panel checks npm for the latest release and can run `npm i -g` in one click (a restart is required to take effect)
25
- - 🔄 Model & thinking-level cycling (same as pi's TUI), new chat, abort/stop
26
- - 📎 Markdown rendering with GFM tables, syntax-highlighted code blocks and copy buttons
27
- - 📁 Workspace-aware: the agent reads/edits/runs code in a configurable directory using **your** `~/.pi/agent` auth, models, skills and extensions
28
- - 🌐 Multiple browser clients each get an isolated session (private session dir per clientId)
29
- - 🖥 Built-in **terminal** (xterm.js + node-pty, no VS Code needed): three panes — a
30
- **command list** on the left (user-defined commands with `${pwd}` support, persisted in
31
- the project's `.pi/commands.json`), the **terminal** in the middle, and a VSCode-style
32
- **tab strip** on the right for multiple concurrent shells. Switch between chat and
33
- terminal views with the toggle in the top bar.
34
-
35
- ## Quick start
36
-
37
- Requires Node.js ≥ 22.19 (the pi SDK requires it; older Node fails with
38
- `Unexpected token 'with'` when loading the SDK) and a configured pi install
39
- (run `pi` once to log in).
40
-
41
- ```bash
42
- npm install
43
- npm run dev # server on :8787, web UI on :5173 (auto-proxied)
44
- # open http://localhost:5173
45
- ```
46
-
47
- Production:
48
-
49
- ```bash
50
- npm run build # compiles server (tsc) + frontend (vite)
51
- npm start # serves everything on http://localhost:8787
52
- ```
53
-
54
- ## npm package (install / start / stop / update / uninstall)
55
-
56
- The package is published on npm as [`pi-web-ui`](https://www.npmjs.com/package/pi-web-ui).
57
-
58
- ### Install
59
-
60
- ```bash
61
- # install globally (recommended)
62
- npm i -g pi-web-ui
63
-
64
- # or run without installing (pulls the latest, starts on :8787)
65
- npx pi-web-ui
66
-
67
- # or install the local checkout (for testing changes before publishing)
68
- npm i -g .
69
- ```
70
-
71
- > **pi-managed npm?** If your `npm` is the pi wrapper that blocks dependency
72
- > install scripts, approve node-pty's native build once after installing:
73
- > `npm approve-scripts node-pty@1.1.0` (standard npm does this automatically).
74
-
75
- ### As a pi package (web UI inside pi)
76
-
77
- `pi-web-ui` is also published as a **pi package** (`pi-package` on npm) so it
78
- can be installed and used from within a pi session:
79
-
80
- ```bash
81
- pi install npm:pi-web-ui
82
- ```
83
-
84
- Once installed, a `/webui` command becomes available inside pi, launching the
85
- local web UI against your current working directory:
86
-
87
- ```
88
- /webui # start + open browser (current dir)
89
- /webui --port 9000 # start on a specific port
90
- /webui --no-browser # start without opening the browser
91
- /webui stop # stop the running instance
92
- /webui status # show URL / status
93
- ```
94
-
95
- > **Note — `pi install` is NOT a global CLI install.**
96
- >
97
- > `pi install npm:pi-web-ui` only loads the package into pi's extension tree
98
- > (`~/.pi/agent/npm/node_modules/`) and registers its extension for pi sessions.
99
- > It does **not** put a `pi-web-ui` executable on your shell `PATH`, so you
100
- > cannot run the `pi-web-ui` terminal command from that install. For the CLI you
101
- > still need the global npm install above (`npm i -g pi-web-ui`), which is what
102
- > `which pi-web-ui` resolves to. `pi install` ≠ `npm i -g`: one is for pi
103
- > extensions, the other for a system-wide command. Both can coexist (the global
104
- > 0.x CLI for terminal use, the pi package for the `/webui` in-pi entry).
105
-
106
- ### Start
107
-
108
- ```bash
109
- pi-web-ui # foreground, http://localhost:8787
110
- PORT=9000 PI_WEB_CWD=/path/to/project pi-web-ui # custom port / workspace
111
- ```
112
-
113
- To run it in the background or auto-start on boot, use a system service —
114
- see [Deploy & auto-start on boot](#deploy--auto-start-on-boot) (systemd /
115
- launchd / Docker).
116
-
117
- The `pi-web-ui` command serves the built frontend and the WebSocket API from
118
- wherever the package is installed — no repo checkout needed. It uses **your**
119
- `~/.pi/agent` config (auth/models/skills) and stores per-client sessions under
120
- `<PI_WEB_CWD>/.pi-web`.
121
-
122
- ### Stop
123
-
124
- - **Foreground**: press `Ctrl+C` in the terminal running it.
125
- - **systemd**: `sudo systemctl stop pi-web-ui`
126
- - **launchd**: `launchctl bootout gui/$(id -u)/com.xingshuyin.pi-web-ui`
127
- - **Windows (scheduled task)**: `pi-web-ui server stop` (or
128
- `schtasks /End /TN pi-web-ui`; stops the running instance, auto-start stays
129
- until `server uninstall`)
130
- - **Docker**: `docker compose stop` (stop + remove the container: `docker compose down`)
131
-
132
- (Background processes should be managed by a system service, not `nohup` —
133
- service stop commands above also stop and disable auto-start.)
134
-
135
- ### Verify / version
136
-
137
- ```bash
138
- pi-web-ui --version # CLI version
139
- npm ls -g pi-web-ui # installed? which version?
140
- which pi-web-ui # executable location
141
- ```
142
-
143
- ### Update
144
-
145
- ```bash
146
- npm i -g pi-web-ui@latest # upgrade to the latest published version
147
- # restart the server afterwards for the new version to take effect
148
- ```
149
-
150
- ### Uninstall
151
-
152
- ```bash
153
- npm uninstall -g pi-web-ui
154
- ```
155
-
156
- Uninstalling does **not** delete your chats: session data lives in
157
- `<PI_WEB_CWD>/.pi-web` (or `PI_WEB_DATA_DIR`) and survives uninstall/upgrade.
158
-
159
- ### Manage as a system service (auto-start)
160
-
161
- Install the server as a system service that starts on boot, with a custom
162
- port and workspace:
163
-
164
- ```bash
165
- pi-web-ui server install --port 9000 --cwd /path/to/project # install + start
166
- pi-web-ui server status # running? auto-start?
167
- pi-web-ui server restart # restart (also applies config changes)
168
- pi-web-ui server stop # stop + disable auto-start
169
- pi-web-ui server start # start again
170
- pi-web-ui server uninstall # remove the service entirely
171
- ```
172
-
173
- - **macOS** → launchd agent (no sudo): writes and loads
174
- `~/Library/LaunchAgents/com.xingshuyin.pi-web-ui.plist`, restarts on crash
175
- (`KeepAlive`), logs to `/tmp/pi-web-ui.log` / `/tmp/pi-web-ui.err`.
176
- - **Linux** → systemd unit (auto-sudo): writes
177
- `/etc/systemd/system/pi-web-ui.service` and runs `systemctl enable --now`,
178
- logs via `journalctl -u pi-web-ui -f`.
179
- - **Windows** → Task Scheduler: creates a user task that starts at logon
180
- (same as a launchd agent; usually no admin needed, but on some machines
181
- `schtasks /Create` requires an elevated PowerShell — if `install` fails
182
- with `ERROR: Access is denied`, rerun it from an admin shell). It runs a
183
- PowerShell launcher generated at
184
- `%APPDATA%\pi-web-ui\pi-web-ui.ps1` via `powershell.exe -WindowStyle Hidden` — no black console window stays open, so there's nothing to
185
- accidentally close/kill. The launcher sets env, cd's to the workspace,
186
- launches node, appends logs to `%USERPROFILE%\pi-web-ui.log`. The task XML
187
- is saved next to it; restarts on failure. **Always pass `--cwd`
188
- explicitly** — the task inherits the installing shell's directory, and an
189
- admin shell defaults to `C:\WINDOWS\system32`, which the non-elevated
190
- task cannot write to (EPERM at startup). See
191
- [Windows — Task Scheduler](#windows--task-scheduler) for details.
192
- - Options: `--port` (default 8787 or `$PORT`), `--cwd` (default `$PI_WEB_CWD`
193
- or the current directory), `--data-dir` (sessions), `--name` (custom service
194
- name; on macOS the label is `com.xingshuyin.pi-web-ui`, custom names become
195
- `com.<name>.server`). `--print` previews the generated unit/plist/task files
196
- without applying it.
197
- - Rerunning `install` with new options regenerates the config and restarts the
198
- service — that's how you change the port/cwd of an installed service.
199
-
200
- ## Configuration
201
-
202
- | Env var | Default | Description |
203
- | -------------------------- | ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
204
- | `PORT` | `8787` | HTTP/WebSocket port |
205
- | `PI_WEB_CWD` | server's cwd | The workspace directory the agent operates in (read/edit/bash/write) |
206
- | `PI_WEB_DATA_DIR` | `<cwd>/.pi-web` | Where per-client session dirs are stored |
207
- | `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) |
208
- | `PI_CODING_AGENT_DIR` | `~/.pi/agent` | pi config dir (auth.json, models.json, skills, extensions) |
209
-
210
- Example — point the agent at a project:
211
-
212
- ```bash
213
- PI_WEB_CWD=/path/to/your/project npm run dev
214
- ```
215
-
216
- ## Architecture
217
-
218
- ```text
219
- Browser (React + Vite)
220
- │ WebSocket JSON — snapshot-driven protocol (server/protocol.ts)
221
-
222
- server/index.ts express static + ws endpoint
223
-
224
- server/agent-service.ts per-client ClientSession:
225
- │ createAgentSessionRuntime({ sessionManager: SessionManager.continueRecent(cwd, sessionDir) })
226
- │ session.subscribe(events) → throttled full-state snapshots + live tool deltas
227
-
228
- @earendil-works/pi-coding-agent (SDK, in-process)
229
- │ ModelRuntime (auth from ~/.pi/agent) · tools · extensions · skills
230
-
231
- your LLM provider
232
- ```
233
-
234
- Key design points:
235
-
236
- - **Snapshot-driven UI.** The server is the source of truth: after every SDK
237
- event it schedules a throttled (60 ms) full-state snapshot, and the browser
238
- renders purely from snapshots. Reconnects just re-request `get_state`. Large
239
- payloads (tool output, text) are capped during serialization (`server/serialize.ts`).
240
- - **Live assistant streaming.** The in-progress message (SDK
241
- `agent.state.streamingMessage`) is serialized into every snapshot, so thinking
242
- blocks and answer text appear in the browser as they are generated — with a
243
- blinking cursor — instead of only after the turn finishes. The partial message
244
- gets a stable `stream-<ts>` id so it stays mounted (open thinking/tool blocks
245
- keep their state) across snapshots.
246
- - **Size-aware attachments.** Clicking + on a file queues it as an attachment
247
- (shown as chips above the input). On send, the server attaches each file as an
248
- independent custom message (SDK `sendCustomMessage` + `nextTurn` asides) — the
249
- user message stays clean, and each file renders as its own collapsible card:
250
- small text files (≤ `PI_WEB_INLINE_FILE_MAX`, default 12KB) are inlined so the
251
- model sees them immediately; larger files are passed as a `<file path=...>`
252
- reference and the model reads them on demand with its `read` tool, so attaching
253
- a 5 MB file costs only a few tokens until the model actually looks at it.
254
- Images are always attached as image content.
255
- - **File preview with line selection.** Click a file name (or its 👁 button) in
256
- the right panel to open a preview modal with line numbers. Click / drag /
257
- Shift+click to select a line range, then “添加到对话” to queue it as a `lines`
258
- attachment the server inlines only the selected range
259
- (`<file path=... lines="2-3">`), so you can point the agent at exactly the
260
- code you mean without dumping the whole file. Preview reads are capped at
261
- 512 KB and binary files are detected and refused.
262
- - **Live tool output.** `bash_execution_update` / `tool_execution_update` events
263
- are forwarded as lightweight `tool_delta` messages so terminal output streams
264
- in real time; the final output arrives in the toolResult message on the next
265
- snapshot, which supersedes the delta buffer.
266
- - **Isolated sessions.** Each browser client gets `sessions/<clientId>/` under
267
- the data dir, resumed on reconnect via `SessionManager.continueRecent`.
268
- - **Everything you already have.** No separate auth step the SDK reads
269
- `~/.pi/agent/auth.json` and loads your global extensions/skills automatically.
270
-
271
- ## Terminal
272
-
273
- Toggle the terminal view from the top bar (对话/终端). It replaces the chat layout
274
- with three panes:
275
-
276
- - **Left 命令 (commands)**: click a command to open a terminal tab in its directory and
277
- run it. Add/edit/delete commands in the panel; they are saved to
278
- `<project>/.pi/commands.json` (committed to the repo, shared with teammates):
279
-
280
- ```json
281
- {
282
- "commands": [
283
- { "name": "dev", "command": "npm run dev", "cwd": "${pwd}" },
284
- { "name": "test", "command": "npm test", "cwd": "${pwd}/server" },
285
- { "name": "build", "command": "npm run build", "cwd": "~/other-project" }
286
- ]
287
- }
288
- ```
289
-
290
- `${pwd}` resolves to the agent's current working directory (the one shown in the chat
291
- view's file panel, changeable via set_cwd); `~` and relative paths also work. The `+`
292
- button at the top of the command panel creates a new entry.
293
- - **Middle — the terminal**: each tab is a real PTY (your `$SHELL` on
294
- macOS/Linux; PowerShell or cmd.exe — `$COMSPEC` — on Windows); output
295
- streams live and you can type, Ctrl+C, resize, etc. exactly like a desktop
296
- terminal. Git Bash users on Windows get their `$SHELL` automatically.
297
- - **Right 终端 (tabs)**: VSCode-style vertical tab strip. `+` opens a plain shell in the
298
- current directory. Closing a tab kills its process.
299
-
300
- Notes:
301
-
302
- - Running commands keep running while you switch back to the chat view.
303
- - Terminals are killed when the last browser tab for a client disconnects (no orphaned
304
- dev servers), so a dropped connection resets the terminal view.
305
-
306
- ## Protocol
307
-
308
- See `server/protocol.ts` for the full wire format. Client server: `hello`,
309
- `prompt`, `abort`, `new_chat`, `cycle_model`, `cycle_thinking`, `get_state`,
310
- `list_sessions`, `switch_session`, `list_files`, `list_models`, `set_model`,
311
- `set_thinking`, `set_cwd`, `complete_path`, `dialog_response`,
312
- `terminal_create`, `terminal_input`, `terminal_resize`, `terminal_kill`,
313
- `run_command`, `list_commands`, `save_commands`.
314
- Server → client: `ready`, `snapshot` (full `UiState`), `tool_delta`, `notice`,
315
- `terminal_output`, `terminal_exit`, `commands`.
316
-
317
- ## Scripts
318
-
319
- | Script | What it does |
320
- | ---------------------------------- | ------------------------------------------------------- |
321
- | `npm run dev` | server (tsx watch) + Vite dev server with WS proxy |
322
- | `npm run build` | type-check + build frontend and server |
323
- | `npm start` | run the production server (serves`web/dist`) |
324
- | `npm run typecheck` | `tsc --noEmit` for both server and web |
325
- | `node terminal-smoke-test.mjs` | WS-level terminal/commands protocol test (build first) |
326
- | `node terminal-browser-test.mjs` | headless-browser E2E of the terminal view (build first) |
327
-
328
- ## Deploy & auto-start on boot
329
-
330
- Quickest path: `pi-web-ui server install --port 8787 --cwd /path` — installs
331
- and starts the service on boot (see [Manage as a system service](#manage-as-a-system-service-auto-start)).
332
- The manual alternatives below are kept for reference / non-standard setups.
333
-
334
- ### Docker (one command)
335
-
336
- ```bash
337
- docker compose up -d # builds, starts on :8787, auto-restarts on boot
338
- docker compose stop # stop (keeps the container)
339
- docker compose down # stop and remove the container
340
- ```
341
-
342
- `restart: unless-stopped` in `docker-compose.yml` brings the server back up
343
- whenever the Docker daemon starts (boot, crashes, reboots). Mount a volume for
344
- `/app/.pi-web` (sessions persist) and, optionally, your `~/.pi/agent` config
345
- and a workspace see the comments in `docker-compose.yml`.
346
-
347
- ### Linux systemd
348
-
349
- ```bash
350
- sudo npm i -g pi-web-ui
351
- sudo cp deploy/pi-web-ui.service /etc/systemd/system/
352
- # edit User/WorkingDirectory/Environment in the unit first
353
- sudo systemctl daemon-reload
354
- sudo systemctl enable --now pi-web-ui # starts now + on every boot
355
- sudo systemctl stop pi-web-ui # stop
356
- sudo systemctl disable pi-web-ui # stop auto-start on boot
357
- journalctl -u pi-web-ui -f # logs
358
- ```
359
-
360
- ### macOSlaunchd
361
-
362
- ```bash
363
- npm i -g pi-web-ui
364
- cp deploy/com.xingshuyin.pi-web-ui.plist ~/Library/LaunchAgents/
365
- # edit ProgramArguments / WorkingDirectory / PI_WEB_CWD (which pi-web-ui)
366
- launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.xingshuyin.pi-web-ui.plist
367
- launchctl bootout gui/$(id -u)/com.xingshuyin.pi-web-ui # stop + remove auto-start
368
- # logs: /tmp/pi-web-ui.log, /tmp/pi-web-ui.err
369
- ```
370
-
371
- ### Windows — Task Scheduler
372
-
373
- Easiest path (generates everything, no manual XML editing):
374
-
375
- ```bat
376
- npm i -g pi-web-ui
377
- pi-web-ui server install --port 8787 --cwd C:\path\to\project
378
- pi-web-ui server status
379
- pi-web-ui server restart
380
- pi-web-ui server stop :: stop the running instance (auto-start stays)
381
- pi-web-ui server uninstall :: remove the task entirely
382
- ```
383
-
384
- What it does: writes `%APPDATA%\pi-web-ui\pi-web-ui.ps1` (a PowerShell launcher
385
- that sets `PORT`/`PI_WEB_CWD`, cd's to the workspace, launches node and
386
- appends output to `%USERPROFILE%\pi-web-ui.log`) plus the Task Scheduler XML,
387
- then registers a **logon** task (`schtasks /Create /XML` — the task runs when
388
- you log in, same as a launchd agent; usually no admin needed, but see the
389
- troubleshooting note below if you get access denied). The task invokes
390
- `powershell.exe -WindowStyle Hidden`, so the server runs with **no black
391
- console window** — there is nothing to accidentally close or kill. Preview
392
- both generated files without installing: `pi-web-ui server install --print`.
393
-
394
- Manual alternative with `deploy/pi-web-ui-task.xml`: edit the paths, save the
395
- file as **UTF-16 LE** (schtasks requires it), then
396
- `schtasks /Create /TN "pi-web-ui" /XML pi-web-ui-task.xml /F` and
397
- `schtasks /Run /TN "pi-web-ui"`.
398
-
399
- > **Windows troubleshooting**
400
- >
401
- > - **`install` fails with `ERROR: Access is denied` (错误: 拒绝访问)** on
402
- > some machines Task Scheduler refuses to let a non-elevated token create
403
- > tasks (deleting your own task with `schtasks /Delete` still works, which
404
- > is why `server uninstall` succeeds). Fix: run
405
- > `pi-web-ui server install` from an **elevated (admin) PowerShell**.
406
- > - **Always pass `--cwd` explicitly, and point it at a user-writable
407
- > directory.** The task inherits the installing shell's current directory
408
- > as its working directory. Installing from an elevated shell without
409
- > `--cwd` registers the task with `C:\WINDOWS\system32`, and the server
410
- > then fails at startup with
411
- > `EPERM: operation not permitted, mkdir 'C:\WINDOWS\system32\.pi-web\sessions\...'`
412
- > because the logon task runs with a least-privilege token that cannot
413
- > write under `system32`. Use e.g. `--cwd C:\Users\<you>` (sessions then
414
- > go to `C:\Users\<you>\.pi-web`).
415
- > - **Fix an already-broken task** (task created with the wrong directory):
416
- > `pi-web-ui server uninstall`, then
417
- > `pi-web-ui server install --cwd C:\Users\<you>` from an elevated shell.
418
- > Rerunning `install` with new options also regenerates the task in place.
419
-
420
- > **Boot-start without login?** A logon task needs an interactive session, just
421
- > like a launchd agent. For headless/always-on Windows use Docker (see above).
422
-
423
- The three templates use `KeepAlive` / `Restart=on-failure` / `RestartOnFailure`
424
- so the server survives crashes, and start at login/boot automatically.
425
-
426
- ## License
427
-
428
- MIT
1
+ # pi-web-ui
2
+
3
+ **English** | [简体中文](README.zh-CN.md)
4
+
5
+ A web chat interface for the [pi coding agent](https://pi.dev), built directly on
6
+ the **pi SDK** ([`@earendil-works/pi-coding-agent`](https://www.npmjs.com/package/@earendil-works/pi-coding-agent)) —
7
+ no subprocess, no JSON-RPC shim. The agent runs in the server process and streams
8
+ events to the browser over WebSocket.
9
+
10
+ Inspired by [Pintra (pi-vsc)](https://github.com/bilalbentoumi/pi-vsc), which does
11
+ the same thing inside VS Code by spawning `pi --mode rpc`. This project instead
12
+ uses the SDK's `createAgentSessionRuntime` API in-process (the SDK docs recommend
13
+ this over RPC for Node.js apps), so you get type safety, direct state access, and
14
+ your existing pi auth/config/extensions — nothing extra to install or configure.
15
+
16
+ ## Features
17
+
18
+ - 🧠 Full agent loop with **thinking** blocks (collapsible) and streaming text
19
+ - 🛠 Tool execution cards with **live output streaming**, status (queued → running → done/error), and copyable arguments
20
+ - 💬 Session **history persisted per browser** (localStorage clientId + per-client session dirs) — refresh or restart and your chats come back. The conversation panel also lists the pi CLI/TUI sessions for the current folder (tagged `TUI`), so you can resume a terminal conversation from the web UI
21
+ - 📂 **Project memory**: the last workspace of each browser is remembered and restored on restart; a "Recent projects" list in the left panel switches workspaces in one click, and each project keeps its own sessions so you can always pick up an old conversation
22
+ - ✏️ **Edit & re-ask**: every past question has an edit button — change it and re-ask from that point. The server forks a new branch session (keeping the full history before that question) while the original conversation stays untouched in the session list
23
+ - ⚡ **Long chats stay fast**: past 30 messages, older messages collapse into summary rows (role + first-line preview + block counts — no Markdown/thinking/tool output rendered); click to expand the full content. The latest 15 messages always render in full
24
+ - ⬇️ **Self-update**: the top-right corner shows the running version; the update panel checks npm for the latest release and can run `npm i -g` in one click (a restart is required to take effect)
25
+ - 🔄 Model & thinking-level cycling (same as pi's TUI), new chat, abort/stop
26
+ - 📎 Markdown rendering with GFM tables, syntax-highlighted code blocks and copy buttons
27
+ - 📁 Workspace-aware: the agent reads/edits/runs code in a configurable directory using **your** `~/.pi/agent` auth, models, skills and extensions
28
+ - 🌐 Multiple browser clients each get an isolated session (private session dir per clientId)
29
+ - 🖥 Built-in **terminal** (xterm.js + node-pty, no VS Code needed): three panes — a
30
+ **command list** on the left (user-defined commands with `${pwd}` support, persisted in
31
+ the project's `.pi/commands.json`), the **terminal** in the middle, and a VSCode-style
32
+ **tab strip** on the right for multiple concurrent shells. Switch between chat and
33
+ terminal views with the toggle in the top bar.
34
+
35
+ ## Quick start
36
+
37
+ Requires Node.js ≥ 22.19 (the pi SDK requires it; older Node fails with
38
+ `Unexpected token 'with'` when loading the SDK) and a configured pi install
39
+ (run `pi` once to log in).
40
+
41
+ ```bash
42
+ npm install
43
+ npm run dev # server on :8787, web UI on :5173 (auto-proxied)
44
+ # open http://localhost:5173
45
+ ```
46
+
47
+ Production:
48
+
49
+ ```bash
50
+ npm run build # compiles server (tsc) + frontend (vite)
51
+ npm start # serves everything on http://localhost:8787
52
+ ```
53
+
54
+ ## npm package (install / start / stop / update / uninstall)
55
+
56
+ The package is published on npm as [`pi-web-ui`](https://www.npmjs.com/package/pi-web-ui).
57
+
58
+ ### Install
59
+
60
+ ```bash
61
+ # install globally (recommended)
62
+ npm i -g pi-web-ui
63
+
64
+ # or run without installing (pulls the latest, starts on :8787)
65
+ npx pi-web-ui
66
+
67
+ # or install the local checkout (for testing changes before publishing)
68
+ npm i -g .
69
+ ```
70
+
71
+ > **pi-managed npm?** If your `npm` is the pi wrapper that blocks dependency
72
+ > install scripts, approve node-pty's native build once after installing:
73
+ > `npm approve-scripts node-pty@1.1.0` (standard npm does this automatically).
74
+
75
+ ### As a pi package (web UI inside pi)
76
+
77
+ `pi-web-ui` is also published as a **pi package** (`pi-package` on npm) so it
78
+ can be installed and used from within a pi session:
79
+
80
+ ```bash
81
+ pi install npm:pi-web-ui
82
+ ```
83
+
84
+ Once installed, a `/webui` command becomes available inside pi, launching the
85
+ local web UI against your current working directory:
86
+
87
+ ```
88
+ /webui # start + open browser (current dir)
89
+ /webui --port 9000 # start on a specific port
90
+ /webui --no-browser # start without opening the browser
91
+ /webui stop # stop the running instance
92
+ /webui status # show URL / status
93
+ ```
94
+
95
+ > **Note — `pi install` is NOT a global CLI install.**
96
+ >
97
+ > `pi install npm:pi-web-ui` only loads the package into pi's extension tree
98
+ > (`~/.pi/agent/npm/node_modules/`) and registers its extension for pi sessions.
99
+ > It does **not** put a `pi-web-ui` executable on your shell `PATH`, so you
100
+ > cannot run the `pi-web-ui` terminal command from that install. For the CLI you
101
+ > still need the global npm install above (`npm i -g pi-web-ui`), which is what
102
+ > `which pi-web-ui` resolves to. `pi install` ≠ `npm i -g`: one is for pi
103
+ > extensions, the other for a system-wide command. Both can coexist (the global
104
+ > 0.x CLI for terminal use, the pi package for the `/webui` in-pi entry).
105
+
106
+ ### Start
107
+
108
+ ```bash
109
+ pi-web-ui # foreground, http://localhost:8787
110
+ PORT=9000 PI_WEB_CWD=/path/to/project pi-web-ui # custom port / workspace
111
+ ```
112
+
113
+ To run it in the background or auto-start on boot, use a system service —
114
+ see [Deploy &amp; auto-start on boot](#deploy--auto-start-on-boot) (systemd /
115
+ launchd / Docker).
116
+
117
+ The `pi-web-ui` command serves the built frontend and the WebSocket API from
118
+ wherever the package is installed — no repo checkout needed. It uses **your**
119
+ `~/.pi/agent` config (auth/models/skills) and stores per-client sessions under
120
+ `<PI_WEB_CWD>/.pi-web`.
121
+
122
+ ### Stop
123
+
124
+ - **Foreground**: press `Ctrl+C` in the terminal running it.
125
+ - **systemd**: `sudo systemctl stop pi-web-ui`
126
+ - **launchd**: `launchctl bootout gui/$(id -u)/com.xingshuyin.pi-web-ui`
127
+ - **Windows (scheduled task)**: `pi-web-ui server stop` (or
128
+ `schtasks /End /TN pi-web-ui`; stops the running instance, auto-start stays
129
+ until `server uninstall`)
130
+ - **Docker**: `docker compose stop` (stop + remove the container: `docker compose down`)
131
+
132
+ (Background processes should be managed by a system service, not `nohup` —
133
+ service stop commands above also stop and disable auto-start.)
134
+
135
+ ### Verify / version
136
+
137
+ ```bash
138
+ pi-web-ui --version # CLI version
139
+ npm ls -g pi-web-ui # installed? which version?
140
+ which pi-web-ui # executable location
141
+ ```
142
+
143
+ ### Update
144
+
145
+ ```bash
146
+ npm i -g pi-web-ui@latest # upgrade to the latest published version
147
+ # restart the server afterwards for the new version to take effect
148
+ ```
149
+
150
+ ### Uninstall
151
+
152
+ ```bash
153
+ npm uninstall -g pi-web-ui
154
+ ```
155
+
156
+ Uninstalling does **not** delete your chats: session data lives in
157
+ `<PI_WEB_CWD>/.pi-web` (or `PI_WEB_DATA_DIR`) and survives uninstall/upgrade.
158
+
159
+ ### Manage as a system service (auto-start)
160
+
161
+ Install the server as a system service that starts on boot, with a custom
162
+ port and workspace:
163
+
164
+ ```bash
165
+ pi-web-ui server install --port 9000 --cwd /path/to/project # install + start
166
+ pi-web-ui server status # running? auto-start?
167
+ pi-web-ui server restart # restart (also applies config changes)
168
+ pi-web-ui server stop # stop + disable auto-start
169
+ pi-web-ui server start # start again
170
+ pi-web-ui server uninstall # remove the service entirely
171
+ ```
172
+
173
+ - **macOS** → launchd agent (no sudo): writes and loads
174
+ `~/Library/LaunchAgents/com.xingshuyin.pi-web-ui.plist`, restarts on crash
175
+ (`KeepAlive`), logs to `/tmp/pi-web-ui.log` / `/tmp/pi-web-ui.err`.
176
+ - **Linux** → systemd unit (auto-sudo): writes
177
+ `/etc/systemd/system/pi-web-ui.service` and runs `systemctl enable --now`,
178
+ logs via `journalctl -u pi-web-ui -f`.
179
+ - **Windows** → Task Scheduler: creates a user task that starts at logon
180
+ (same as a launchd agent; usually no admin needed, but on some machines
181
+ `schtasks /Create` requires an elevated PowerShell — if `install` fails
182
+ with `ERROR: Access is denied`, rerun it from an admin shell). It runs a
183
+ PowerShell launcher generated at
184
+ `%APPDATA%\pi-web-ui\pi-web-ui.ps1` via `powershell.exe -WindowStyle Hidden` — no black console window stays open, so there's nothing to
185
+ accidentally close/kill. The launcher sets env, cd's to the workspace,
186
+ launches node, appends logs to `%USERPROFILE%\pi-web-ui.log`. The task XML
187
+ is saved next to it; restarts on failure. **Always pass `--cwd`
188
+ explicitly** — the task inherits the installing shell's directory, and an
189
+ admin shell defaults to `C:\WINDOWS\system32`, which the non-elevated
190
+ task cannot write to (EPERM at startup). See
191
+ [Windows — Task Scheduler](#windows--task-scheduler) for details.
192
+ - Options: `--port` (default 8787 or `$PORT`), `--cwd` (default `$PI_WEB_CWD`
193
+ or the current directory), `--data-dir` (sessions), `--name` (custom service
194
+ name; on macOS the label is `com.xingshuyin.pi-web-ui`, custom names become
195
+ `com.<name>.server`). `--print` previews the generated unit/plist/task files
196
+ without applying it.
197
+ - Rerunning `install` with new options regenerates the config and restarts the
198
+ service — that's how you change the port/cwd of an installed service.
199
+
200
+ ## Configuration
201
+
202
+ | Env var | Default | Description |
203
+ | -------------------------- | ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
204
+ | `PORT` | `8787` | HTTP/WebSocket port |
205
+ | `PI_WEB_CWD` | server's cwd | The workspace directory the agent operates in (read/edit/bash/write) |
206
+ | `PI_WEB_DATA_DIR` | `<cwd>/.pi-web` | Where per-client session dirs are stored |
207
+ | `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) |
208
+ | `PI_CODING_AGENT_DIR` | `~/.pi/agent` | pi config dir (auth.json, models.json, skills, extensions) |
209
+
210
+ Example — point the agent at a project:
211
+
212
+ ```bash
213
+ PI_WEB_CWD=/path/to/your/project npm run dev
214
+ ```
215
+
216
+ ## Architecture
217
+
218
+ ```text
219
+ Browser (React + Vite)
220
+ │ WebSocket JSON — snapshot-driven protocol (server/protocol.ts)
221
+
222
+ server/index.ts express static + ws endpoint
223
+
224
+ server/agent-service.ts per-client ClientSession:
225
+ │ createAgentSessionRuntime({ sessionManager: SessionManager.continueRecent(cwd, sessionDir) })
226
+ │ session.subscribe(events) → throttled full-state snapshots + live tool deltas
227
+
228
+ @earendil-works/pi-coding-agent (SDK, in-process)
229
+ │ ModelRuntime (auth from ~/.pi/agent) · tools · extensions · skills
230
+
231
+ your LLM provider
232
+ ```
233
+
234
+ Key design points:
235
+
236
+ - **Snapshot-driven UI.** The server is the source of truth: after every SDK
237
+ event it schedules a throttled (60 ms) full-state snapshot, and the browser
238
+ renders purely from snapshots. Reconnects just re-request `get_state`. Large
239
+ payloads (tool output, text) are capped during serialization (`server/serialize.ts`).
240
+ - **Live assistant streaming.** The in-progress message (SDK
241
+ `agent.state.streamingMessage`) is serialized into every snapshot, so thinking
242
+ blocks and answer text appear in the browser as they are generated — with a
243
+ blinking cursor — instead of only after the turn finishes. The partial message
244
+ gets a stable `stream-<ts>` id so it stays mounted (open thinking/tool blocks
245
+ keep their state) across snapshots.
246
+ - **Size-aware attachments.** Clicking + on a file queues it as an attachment
247
+ (shown as chips above the input). On send, the server attaches each file as an
248
+ independent custom message (SDK `sendCustomMessage` + `nextTurn` asides) — the
249
+ user message stays clean, and each file renders as its own collapsible card:
250
+ small text files (≤ `PI_WEB_INLINE_FILE_MAX`, default 12KB) are inlined so the
251
+ model sees them immediately; larger files are passed as a `<file path=...>`
252
+ reference and the model reads them on demand with its `read` tool, so attaching
253
+ a 5 MB file costs only a few tokens until the model actually looks at it.
254
+ Images are always attached as image content.
255
+ - **Image Q&A.** Besides attaching workspace images from the right panel, you
256
+ can **paste a screenshot (Ctrl+V), drag an image onto the input bar, or use
257
+ the 🖼 upload button** the browser downscales it to ≤1568px and encodes it,
258
+ and it travels with the message (`prompt.attachments[].imageData`, base64),
259
+ no workspace path needed. Attaching an image to a non-vision model shows a
260
+ warning (the image would be ignored).
261
+ - **File chat.** Any local file (text or binary) can be dropped onto the input
262
+ bar or picked via the 📎 button — the browser sends the bytes as base64
263
+ (`prompt.attachments[].fileData`), the server persists them under
264
+ `~/.pi-web/uploads/<clientId>/` and attaches: small text files are inlined so
265
+ the model sees them immediately; large/binary files become absolute-path
266
+ references the model reads on demand (its read tool accepts absolute paths).
267
+ Cap: 20MB.
268
+ - **File preview with line selection.** Click a file name (or its 👁 button) in
269
+ the right panel to open a preview modal with line numbers. Click / drag /
270
+ Shift+click to select a line range, then “添加到对话” to queue it as a `lines`
271
+ attachment — the server inlines only the selected range
272
+ (`<file path=... lines="2-3">`), so you can point the agent at exactly the
273
+ code you mean without dumping the whole file. Preview reads are capped at
274
+ 512 KB and binary files are detected and refused.
275
+ - **Live tool output.** `bash_execution_update` / `tool_execution_update` events
276
+ are forwarded as lightweight `tool_delta` messages so terminal output streams
277
+ in real time; the final output arrives in the toolResult message on the next
278
+ snapshot, which supersedes the delta buffer.
279
+ - **Isolated sessions.** Each browser client gets `sessions/<clientId>/` under
280
+ the data dir, resumed on reconnect via `SessionManager.continueRecent`.
281
+ - **Everything you already have.** No separate auth step — the SDK reads
282
+ `~/.pi/agent/auth.json` and loads your global extensions/skills automatically.
283
+
284
+ ## Terminal
285
+
286
+ Toggle the terminal view from the top bar (对话/终端). It replaces the chat layout
287
+ with three panes:
288
+
289
+ - **Left — 命令 (commands)**: click a command to open a terminal tab in its directory and
290
+ run it. Add/edit/delete commands in the panel; they are saved to
291
+ `<project>/.pi/commands.json` (committed to the repo, shared with teammates):
292
+
293
+ ```json
294
+ {
295
+ "commands": [
296
+ { "name": "dev", "command": "npm run dev", "cwd": "${pwd}" },
297
+ { "name": "test", "command": "npm test", "cwd": "${pwd}/server" },
298
+ { "name": "build", "command": "npm run build", "cwd": "~/other-project" }
299
+ ]
300
+ }
301
+ ```
302
+
303
+ `${pwd}` resolves to the agent's current working directory (the one shown in the chat
304
+ view's file panel, changeable via set_cwd); `~` and relative paths also work. The `+`
305
+ button at the top of the command panel creates a new entry.
306
+ - **Middle — the terminal**: each tab is a real PTY (your `$SHELL` on
307
+ macOS/Linux; PowerShell or cmd.exe — `$COMSPEC` — on Windows); output
308
+ streams live and you can type, Ctrl+C, resize, etc. exactly like a desktop
309
+ terminal. Git Bash users on Windows get their `$SHELL` automatically.
310
+ - **Right 终端 (tabs)**: VSCode-style vertical tab strip. `+` opens a plain shell in the
311
+ current directory. Closing a tab kills its process.
312
+
313
+ Notes:
314
+
315
+ - Running commands keep running while you switch back to the chat view.
316
+ - Terminals are killed when the last browser tab for a client disconnects (no orphaned
317
+ dev servers), so a dropped connection resets the terminal view.
318
+
319
+ ## Protocol
320
+
321
+ See `server/protocol.ts` for the full wire format. Client server: `hello`,
322
+ `prompt`, `abort`, `new_chat`, `cycle_model`, `cycle_thinking`, `get_state`,
323
+ `list_sessions`, `switch_session`, `list_files`, `list_models`, `set_model`,
324
+ `set_thinking`, `set_cwd`, `complete_path`, `dialog_response`,
325
+ `terminal_create`, `terminal_input`, `terminal_resize`, `terminal_kill`,
326
+ `run_command`, `list_commands`, `save_commands`.
327
+ Server → client: `ready`, `snapshot` (full `UiState`), `tool_delta`, `notice`,
328
+ `terminal_output`, `terminal_exit`, `commands`.
329
+
330
+ ## Scripts
331
+
332
+ | Script | What it does |
333
+ | ---------------------------------- | ------------------------------------------------------- |
334
+ | `npm run dev` | server (tsx watch) + Vite dev server with WS proxy |
335
+ | `npm run build` | type-check + build frontend and server |
336
+ | `npm start` | run the production server (serves`web/dist`) |
337
+ | `npm run typecheck` | `tsc --noEmit` for both server and web |
338
+ | `node terminal-smoke-test.mjs` | WS-level terminal/commands protocol test (build first) |
339
+ | `node terminal-browser-test.mjs` | headless-browser E2E of the terminal view (build first) |
340
+
341
+ ## Deploy & auto-start on boot
342
+
343
+ Quickest path: `pi-web-ui server install --port 8787 --cwd /path` installs
344
+ and starts the service on boot (see [Manage as a system service](#manage-as-a-system-service-auto-start)).
345
+ The manual alternatives below are kept for reference / non-standard setups.
346
+
347
+ ### Docker (one command)
348
+
349
+ ```bash
350
+ docker compose up -d # builds, starts on :8787, auto-restarts on boot
351
+ docker compose stop # stop (keeps the container)
352
+ docker compose down # stop and remove the container
353
+ ```
354
+
355
+ `restart: unless-stopped` in `docker-compose.yml` brings the server back up
356
+ whenever the Docker daemon starts (boot, crashes, reboots). Mount a volume for
357
+ `/app/.pi-web` (sessions persist) and, optionally, your `~/.pi/agent` config
358
+ and a workspace — see the comments in `docker-compose.yml`.
359
+
360
+ ### Linuxsystemd
361
+
362
+ ```bash
363
+ sudo npm i -g pi-web-ui
364
+ sudo cp deploy/pi-web-ui.service /etc/systemd/system/
365
+ # edit User/WorkingDirectory/Environment in the unit first
366
+ sudo systemctl daemon-reload
367
+ sudo systemctl enable --now pi-web-ui # starts now + on every boot
368
+ sudo systemctl stop pi-web-ui # stop
369
+ sudo systemctl disable pi-web-ui # stop auto-start on boot
370
+ journalctl -u pi-web-ui -f # logs
371
+ ```
372
+
373
+ ### macOS launchd
374
+
375
+ ```bash
376
+ npm i -g pi-web-ui
377
+ cp deploy/com.xingshuyin.pi-web-ui.plist ~/Library/LaunchAgents/
378
+ # edit ProgramArguments / WorkingDirectory / PI_WEB_CWD (which pi-web-ui)
379
+ launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.xingshuyin.pi-web-ui.plist
380
+ launchctl bootout gui/$(id -u)/com.xingshuyin.pi-web-ui # stop + remove auto-start
381
+ # logs: /tmp/pi-web-ui.log, /tmp/pi-web-ui.err
382
+ ```
383
+
384
+ ### Windows Task Scheduler
385
+
386
+ Easiest path (generates everything, no manual XML editing):
387
+
388
+ ```bat
389
+ npm i -g pi-web-ui
390
+ pi-web-ui server install --port 8787 --cwd C:\path\to\project
391
+ pi-web-ui server status
392
+ pi-web-ui server restart
393
+ pi-web-ui server stop :: stop the running instance (auto-start stays)
394
+ pi-web-ui server uninstall :: remove the task entirely
395
+ ```
396
+
397
+ What it does: writes `%APPDATA%\pi-web-ui\pi-web-ui.ps1` (a PowerShell launcher
398
+ that sets `PORT`/`PI_WEB_CWD`, cd's to the workspace, launches node and
399
+ appends output to `%USERPROFILE%\pi-web-ui.log`) plus the Task Scheduler XML,
400
+ then registers a **logon** task (`schtasks /Create /XML` — the task runs when
401
+ you log in, same as a launchd agent; usually no admin needed, but see the
402
+ troubleshooting note below if you get access denied). The task invokes
403
+ `powershell.exe -WindowStyle Hidden`, so the server runs with **no black
404
+ console window** — there is nothing to accidentally close or kill. Preview
405
+ both generated files without installing: `pi-web-ui server install --print`.
406
+
407
+ Manual alternative with `deploy/pi-web-ui-task.xml`: edit the paths, save the
408
+ file as **UTF-16 LE** (schtasks requires it), then
409
+ `schtasks /Create /TN "pi-web-ui" /XML pi-web-ui-task.xml /F` and
410
+ `schtasks /Run /TN "pi-web-ui"`.
411
+
412
+ > **Windows troubleshooting**
413
+ >
414
+ > - **`install` fails with `ERROR: Access is denied` (错误: 拒绝访问)** — on
415
+ > some machines Task Scheduler refuses to let a non-elevated token create
416
+ > tasks (deleting your own task with `schtasks /Delete` still works, which
417
+ > is why `server uninstall` succeeds). Fix: run
418
+ > `pi-web-ui server install` from an **elevated (admin) PowerShell**.
419
+ > - **Always pass `--cwd` explicitly, and point it at a user-writable
420
+ > directory.** The task inherits the installing shell's current directory
421
+ > as its working directory. Installing from an elevated shell without
422
+ > `--cwd` registers the task with `C:\WINDOWS\system32`, and the server
423
+ > then fails at startup with
424
+ > `EPERM: operation not permitted, mkdir 'C:\WINDOWS\system32\.pi-web\sessions\...'`
425
+ > because the logon task runs with a least-privilege token that cannot
426
+ > write under `system32`. Use e.g. `--cwd C:\Users\<you>` (sessions then
427
+ > go to `C:\Users\<you>\.pi-web`).
428
+ > - **Fix an already-broken task** (task created with the wrong directory):
429
+ > `pi-web-ui server uninstall`, then
430
+ > `pi-web-ui server install --cwd C:\Users\<you>` from an elevated shell.
431
+ > Rerunning `install` with new options also regenerates the task in place.
432
+
433
+ > **Boot-start without login?** A logon task needs an interactive session, just
434
+ > like a launchd agent. For headless/always-on Windows use Docker (see above).
435
+
436
+ The three templates use `KeepAlive` / `Restart=on-failure` / `RestartOnFailure`
437
+ so the server survives crashes, and start at login/boot automatically.
438
+
439
+ ## License
440
+
441
+ MIT