@rexvane/piweb 0.3.4 → 0.3.6

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
@@ -14,109 +14,84 @@
14
14
 
15
15
  ---
16
16
 
17
- **PiWeb** brings a sleek, feature-rich browser interface to the [pi coding agent](https://github.com/earendil-works/pi) with zero modifications to the core Pi engine. Built with Next.js and Tailwind CSS.
17
+ **PiWeb** brings a sleek, feature-rich browser interface to the [pi coding agent](https://github.com/earendil-works/pi) with zero modifications to the core Pi engine. Install with one command, and all your existing pi configuration, sessions, models, skills, and plugins work out of the box.
18
18
 
19
- ## Key Features
19
+ ## Features
20
20
 
21
- - **Real-Time Streaming**: Full SSE snapshots with ordered deltas, automatic replay via `Last-Event-ID`, real-time thinking process and inline tool execution cards.
22
- - **Composer & Input**: Image attachments (paste, pick, drag-and-drop), 2-level model selector, model cycling forward/backward, queue management (steering / follow-up), and `/` slash commands.
23
- - **Decoupled Workspaces & Sessions**: Group sessions by workspace folders, native system folder picker on Windows, rename/delete workspaces with centered confirmation modals, preserve sessions under "Ungrouped", and maintain empty workspaces independently.
24
- - **Session Exploration**: Session tree branch visualization & navigation, user message draft recovery, export session logs to JSONL / HTML, and trajectory views.
25
- - **Comprehensive Settings**:
26
- - **General**: Tool presets (Read Only / Workspace Write / Full Access), language (zh/en), appearance, enter key behaviors, and auto-compact.
27
- - **Models**: Manage 30+ built-in providers, custom providers in `models.json`, and OAuth logins (Claude, Codex, Copilot, etc.).
28
- - **Plugins & Skills**: View and manage extensions and skills directly via Pi's built-in package manager.
29
- - **Security & Sandboxing**: Optional HTTP Basic Auth via `PI_WEB_PASSWORD`, origin validation on write actions, and strict path boundary checks.
21
+ ### Real-Time Streaming Chat
30
22
 
31
- ## Quick Start
23
+ Full SSE snapshots with ordered deltas and automatic replay via `Last-Event-ID`. The thinking process and inline tool execution cards stream live.
32
24
 
33
- **Prerequisites**: Node.js ≥ 22.19.0 (Node.js 24 recommended) and Git for repository and growth-history features.
25
+ <p align="center"><img src="assets/showcase.png" alt="Streaming chat with tool cards" width="840" /></p>
34
26
 
35
- ### Install from npm (recommended)
27
+ ### Workspaces & Sessions
36
28
 
37
- ```bash
38
- npm install -g @rexvane/piweb
39
- piweb
40
- ```
29
+ Sessions grouped by project folder, with a native folder picker (Windows / macOS / Linux), rename and archive. Session tree branches, draft recovery, and JSONL / HTML export.
41
30
 
42
- A global install prepares the production bundle once (during install, or on the first run if the script was skipped) and then starts in production mode. If that build fails, `piweb` reports it instead of falling back to a development server that cannot work without dev dependencies; retry with `npm rebuild -g @rexvane/piweb`. The package registers only the `piweb` command, so it never conflicts with the official `pi` CLI (`npm i -g @earendil-works/pi-coding-agent`) if you have both.
31
+ ### Project Growth Tree
43
32
 
44
- ### Inside this repository
33
+ Every step the agent takes on your codebase is snapshotted: navigate the timeline round by round, see added/modified/deleted files, and open line-level diffs for any file at any step — even if the workspace has no git history of its own.
45
34
 
46
- ```bash
47
- npm install
48
- npm run dev # development server with hot reload
49
- ```
35
+ ### Context Meter
50
36
 
51
- Or run a production build locally:
37
+ Click the ring next to the composer to see the context window broken down into 13 segments — System Prompt, System/Custom Tools, Memory (AGENTS.md), Skills, messages by type, Compacted Data, Auto-Compact Buffer, and Free Space.
52
38
 
53
- ```bash
54
- npm install
55
- npm run build # or: npm run build:release (staged, does not touch a running build)
56
- npm start # serves on http://127.0.0.1:30141 (auto-increments if busy)
57
- ```
39
+ ### Model & Provider Management
58
40
 
59
- ### CLI Options (`bin/piweb.js`)
41
+ 30+ built-in providers, OAuth logins (Claude, Codex, Copilot, etc.), custom providers written to `models.json`, one-click model discovery, and a placeholder key auto-filled for local gateways like Ollama.
60
42
 
61
- ```
62
- -p, --port <port> Listen port (default 30141, env PORT; auto-increments if in use)
63
- -H, --hostname <host> Bind address (default 127.0.0.1, env PI_WEB_HOSTNAME)
64
- --dev Start in development mode (`next dev`; env PI_WEB_DEV=1; also used when no production build exists)
65
- --no-open Do not automatically open browser (env PI_WEB_NO_OPEN=1)
66
- -h, --help Show help
67
- ```
43
+ ### Plugins & Skills
44
+
45
+ Install, update, and uninstall pi packages directly from the UI. Enable/disable or delete skills; every change applies to running sessions immediately.
68
46
 
69
- Environment variables: `PI_WEB_PASSWORD` enables a browser login session and HTTP Basic Auth for API clients (user `pi`). It is **required** for any non-loopback production bind address. Development mode is loopback-only, even with a password; if no production build exists, an external bind fails rather than falling back to an exposed development server. Use HTTPS or a trusted VPN for remote access. `PI_WEB_EDITOR` overrides the editor used by "open in editor" (default `code`). Without a password, PiWeb only accepts requests whose `Host` is loopback.
47
+ ### Security
70
48
 
71
- `GET /api/health` exposes only a fixed service identifier for credential-free startup probes. Runtime versions are available from the authenticated `/api/version` endpoint. Tool presets limit the tools offered to the agent; they are not an operating-system sandbox, and installed extensions run with the server process's permissions.
49
+ Optional login via `PI_WEB_PASSWORD` (required for non-loopback binds), origin validation on write actions, strict path boundary checks, and tool presets (Read Only / Workspace Write / Full Access).
72
50
 
73
- Model catalog discovery blocks loopback, private, and reserved network addresses by default. If you intentionally use a local model gateway, set `PI_WEB_ALLOW_PRIVATE_MODEL_DISCOVERY=1` before starting PiWeb. This relaxes the discovery endpoint only; use it only on a trusted PiWeb instance.
51
+ ## Usage
74
52
 
75
- ### Development
53
+ ### Install
76
54
 
77
55
  ```bash
78
- npm run dev # Run Next.js in development mode
79
- npm run typecheck # TypeScript check
80
- npm test # Service, protocol and component regressions
81
- npm run check # Full check (types + tests + build)
82
- npm run build:release # Validate and stage a production build without replacing the running build
56
+ npm install -g @rexvane/piweb
57
+ piweb
83
58
  ```
84
59
 
85
- ## Status and known limitations (2026-09-14)
86
-
87
- The audit follow-up is merged into this branch and has been through an isolated release plus a local production deployment. It covers the production login build, upload integrity through the Next.js proxy, per-session extension isolation and tool-policy reloads, lossless model/settings writes, composer and pending-extension-UI lifecycle, Git/Growth/diff correctness, and isolated release preparation.
60
+ The package registers only the `piweb` command, so it never conflicts with the official `pi` CLI. Requires Node.js ≥ 22.19.0 (24 recommended); Git is needed for growth-history features.
88
61
 
89
- The latest local validation passed TypeScript checking, **355 tests across 52 files** (one additional test skipped), `npm run build`, and `git diff --check`. The skipped test requires Windows symlink privileges or Developer Mode.
62
+ ### Update
90
63
 
91
- Known limitations: saving the model configuration normalizes it to plain JSON (comments are not preserved, data is); updates replace dependencies in place during a maintenance window rather than being zero-downtime; a custom tool allowlist lives only for the session lifetime; third-party extension module globals are not isolated.
64
+ ```bash
65
+ npm update -g @rexvane/piweb
66
+ ```
92
67
 
93
- ## Production builds and updates
68
+ Or install a specific version: `npm install -g @rexvane/piweb@0.3.4`.
94
69
 
95
- Use `npm run build:release` when preparing a build while PiWeb is running. It validates types and tests, builds into a fresh `.next-releases/<id>` directory, and only then publishes the build selection for the next start. It does not restart the server or replace the output used by an existing process. `npm start` uses the last successfully prepared release; after a failed or interrupted release attempt it reports the problem instead of silently serving a stale build. Correct the error and run `npm run build:release` again to recover.
70
+ ### Uninstall
96
71
 
97
- The in-app updater uses the same validation path. Source files and npm dependencies are still updated in the installation directory, so perform updates during a maintenance window with no running agent turns. This is not a fully isolated zero-downtime deployment system. Restart PiWeb manually after a successful update; running processes do not automatically switch to the new build.
72
+ ```bash
73
+ npm uninstall -g @rexvane/piweb
74
+ ```
98
75
 
99
- CI runs the full checks on Linux and Windows with Node.js 22.19.0 and 24. Ordinary `npm run build` still uses `.next`, so do not run it against an installation whose active production process is using that directory.
76
+ This removes PiWeb only — your pi data lives in `~/.pi/agent/` and is left untouched.
100
77
 
101
- ## Architecture
78
+ ### Run
102
79
 
80
+ ```bash
81
+ piweb # http://127.0.0.1:30141 (auto-increments if busy)
82
+ piweb -p 30143 --no-open # custom port, don't open the browser
83
+ piweb -H 0.0.0.0 # LAN access (requires PI_WEB_PASSWORD)
103
84
  ```
104
- Browser (React 19 + Tailwind, src/components)
105
- │ REST commands (POST /api/agent/[id]) + SSE streams (GET /api/agent/[id]/events)
106
- ▼
107
- Next.js Server (src/app/api, src/lib)
108
- ├─ agent-manager AgentSession pool: lazy startup, event translation, idle eviction
109
- ├─ session-reader Direct read ~/.pi/agent/sessions/**.jsonl (cold render without running agent)
110
- ├─ trajectory Trajectory ledger + server-side timing (TTFT / decode)
111
- ├─ skills/prompts Skills & Prompt template discovery + frontmatter toggles
112
- ├─ models-service Model catalogue, auth, OAuth bridge & models.json
113
- ├─ security-service Tool presets & project trust
114
- ├─ plugins-service Pi package management (DefaultPackageManager) & extension registry
115
- ├─ workspace-store Persistent workspaces registry & native folder picker
116
- ▼
117
- @earendil-works/pi-coding-agent (Official Pi SDK, zero engine modifications)
118
- ```
85
+
86
+ | Option | Description |
87
+ | --- | --- |
88
+ | `-p, --port <port>` | Listen port (default 30141, env `PORT`) |
89
+ | `-H, --hostname <host>` | Bind address (default 127.0.0.1, env `PI_WEB_HOSTNAME`) |
90
+ | `--dev` | Development mode |
91
+ | `--no-open` | Do not auto-open the browser |
92
+
93
+ Set `PI_WEB_PASSWORD` to enable the login page and HTTP Basic Auth (user `pi`). Remote access over plain HTTP is not recommended; use HTTPS or a trusted VPN.
119
94
 
120
95
  ## License
121
96
 
122
- [MIT License](LICENSE)
97
+ [MIT](LICENSE)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rexvane/piweb",
3
- "version": "0.3.4",
3
+ "version": "0.3.6",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "description": "Modern web UI for the pi coding agent, driven by the official pi SDK without modifying pi",
@@ -9,6 +9,7 @@
9
9
  * Repository checkouts always have dev dependencies and keep `npm run dev`.
10
10
  */
11
11
  import fs from "node:fs";
12
+ import os from "node:os";
12
13
  import path from "node:path";
13
14
  import { createRequire } from "node:module";
14
15
  import { spawnSync } from "node:child_process";
@@ -65,12 +66,63 @@ export function resolveNextBin(root) {
65
66
  * neither compiled nor mapped through the `@/*` path alias. Build in a staging
66
67
  * directory outside `node_modules` (same drive, so the dependency links resolve)
67
68
  * and copy the finished output back into the package.
69
+ *
70
+ * The staging directory must NOT sit inside any `node_modules`: for a global
71
+ * install like ~/.local/lib/node_modules/@rexvane/piweb, the grandparent IS
72
+ * node_modules, and Next would exclude the staged sources exactly the same way.
73
+ * Walk up until we are outside every node_modules segment; give up and use the
74
+ * system temp dir (cross-device copy is still correct, just slower).
68
75
  */
76
+ function stagingParent(root) {
77
+ // Split on BOTH separators: a win32 Node can see POSIX paths (and vice versa)
78
+ // when the package is installed through a compatibility layer or the test
79
+ // suite simulates foreign layouts.
80
+ const segments = path.dirname(root).split(/[\\/]/).filter(Boolean);
81
+ // Drop trailing path components while the path still contains a node_modules segment.
82
+ while (segments.length > 1 && segments.includes("node_modules")) {
83
+ segments.pop();
84
+ }
85
+ const candidate = segments.length ? path.resolve(`/${segments.join(path.sep)}`) : path.sep;
86
+ try {
87
+ fs.mkdirSync(candidate, { recursive: true });
88
+ fs.accessSync(candidate, fs.constants.W_OK);
89
+ return candidate;
90
+ } catch {
91
+ return os.tmpdir();
92
+ }
93
+ }
94
+
95
+ /** Staging directory must live outside EVERY node_modules on the way up (Next excludes those paths). */
96
+ export function assertStagingOutsideNodeModules(root) {
97
+ const parent = stagingParent(root);
98
+ if (parent.split(/[\\/]/).includes("node_modules")) {
99
+ throw new Error(`staging parent is inside node_modules: ${parent}`);
100
+ }
101
+ return parent;
102
+ }
103
+
104
+ function linkOrCopyDependencies(root, staging, log) {
105
+ const target = path.join(root, "node_modules");
106
+ const linkPath = path.join(staging, "node_modules");
107
+ // Windows: junction 不需要符号链接权限;跨卷时 junction 也会失败(试回退)。
108
+ // POSIX(macOS/Linux): 目录符号链接即可,"junction" 类型会被 libuv 拒绝
109
+ // (0.3.5 的 Mac 现场:symlinkSync(..., 'junction') 抛 EINVAL)。
110
+ const type = process.platform === "win32" ? "junction" : "dir";
111
+ try {
112
+ fs.symlinkSync(target, linkPath, type);
113
+ return;
114
+ } catch {
115
+ /* fall through to the copy */
116
+ }
117
+ // 兜底:符号链接不可用(如无权限)时整份复制依赖。慢但保证构建可解析。
118
+ log("[piweb] Symlink unavailable; copying dependencies into the staging build (slower)...");
119
+ fs.cpSync(target, linkPath, { recursive: true, verbatimSymlinks: true });
120
+ }
121
+
69
122
  function buildInStaging(root, nextBin, env, log, warn) {
70
- const parent = path.dirname(path.dirname(root));
71
123
  let staging;
72
124
  try {
73
- staging = fs.mkdtempSync(path.join(parent, ".piweb-build-"));
125
+ staging = fs.mkdtempSync(path.join(stagingParent(root), ".piweb-build-"));
74
126
  } catch {
75
127
  staging = fs.mkdtempSync(path.join(os.tmpdir(), "piweb-build-"));
76
128
  }
@@ -83,12 +135,7 @@ function buildInStaging(root, nextBin, env, log, warn) {
83
135
  const from = path.join(root, file);
84
136
  if (fs.existsSync(from)) fs.copyFileSync(from, path.join(staging, file));
85
137
  }
86
- try {
87
- fs.symlinkSync(path.join(root, "node_modules"), path.join(staging, "node_modules"), "junction");
88
- } catch (error) {
89
- warn(`[piweb] Could not link dependencies into the staging build: ${error.message}`);
90
- return false;
91
- }
138
+ linkOrCopyDependencies(root, staging, log);
92
139
  log(`[piweb] Building in ${staging} ...`);
93
140
  const result = spawnSync(process.execPath, [nextBin, "build", "--webpack"], { cwd: staging, stdio: "inherit", env });
94
141
  const produced = path.join(staging, ".next", "BUILD_ID");
@@ -98,6 +145,8 @@ function buildInStaging(root, nextBin, env, log, warn) {
98
145
  fs.cpSync(path.join(staging, ".next"), target, { recursive: true });
99
146
  return fs.existsSync(path.join(target, "BUILD_ID"));
100
147
  } finally {
148
+ // 复制兜底时 staging 里是整份 node_modules,必须删掉;rmSync 跟随链接删除
149
+ // 的是链接本身(Node 14+ 目录符号链接不穿透),真实依赖不受影响。
101
150
  fs.rmSync(staging, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
102
151
  }
103
152
  }
@@ -81,7 +81,6 @@ export function AppShell() {
81
81
  setGroupBy,
82
82
  setOrderBy,
83
83
  addWorkspaceByPicker,
84
- addWorkspaceByPath,
85
84
  removeWorkspace,
86
85
  refreshModels,
87
86
  sendCommand,
@@ -702,7 +701,6 @@ export function AppShell() {
702
701
  providerNames={providerNames}
703
702
  authByProvider={authByProvider}
704
703
  addWorkspaceByPicker={addWorkspaceByPicker}
705
- addWorkspaceByPath={addWorkspaceByPath}
706
704
  heroModel={heroModel}
707
705
  onSelectHeroModel={(provider, id) => {
708
706
  setHeroModel({ provider, id });
@@ -1021,7 +1019,6 @@ function Hero({
1021
1019
  providerNames,
1022
1020
  authByProvider,
1023
1021
  addWorkspaceByPicker,
1024
- addWorkspaceByPath,
1025
1022
  heroModel,
1026
1023
  onSelectHeroModel,
1027
1024
  defaultModel,
@@ -1048,7 +1045,6 @@ function Hero({
1048
1045
  providerNames: Record<string, string>;
1049
1046
  authByProvider: Record<string, boolean>;
1050
1047
  addWorkspaceByPicker: () => Promise<string | null>;
1051
- addWorkspaceByPath: (dir: string) => Promise<string | null>;
1052
1048
  heroModel: { provider: string; id: string } | null;
1053
1049
  onSelectHeroModel: (provider: string, id: string) => void;
1054
1050
  defaultModel?: ModelChoice;
@@ -1058,8 +1054,6 @@ function Hero({
1058
1054
  }) {
1059
1055
  const { t } = useI18n();
1060
1056
  const [wsMenu, setWsMenu] = useState(false);
1061
- const [wsPathDraft, setWsPathDraft] = useState("");
1062
- const [wsPathError, setWsPathError] = useState<string | null>(null);
1063
1057
  const menuRef = useRef<HTMLDivElement>(null);
1064
1058
 
1065
1059
  useEffect(() => {
@@ -1081,19 +1075,6 @@ function Hero({
1081
1075
  }
1082
1076
  };
1083
1077
 
1084
- // 手动输入路径添加(原生选择器不可用 / 无桌面环境兜底)
1085
- const addByPath = async () => {
1086
- const p = await addWorkspaceByPath(wsPathDraft);
1087
- if (p) {
1088
- setWsPathDraft("");
1089
- setWsPathError(null);
1090
- setCwd(p);
1091
- setWsMenu(false);
1092
- } else {
1093
- setWsPathError(t.workspacePathInvalid);
1094
- }
1095
- };
1096
-
1097
1078
  const currentLabel = cwd ? (getWorkspaceName ? getWorkspaceName(cwd) : basename(cwd)) : t.startWith;
1098
1079
 
1099
1080
  return (
@@ -1155,24 +1136,6 @@ function Hero({
1155
1136
  <IconProjectAddOutline16 size={14} style={{ flex: "none", color: "var(--dsw-label-tertiary)" }} />
1156
1137
  {t.addWorkspace}
1157
1138
  </button>
1158
- <div className="flex items-center gap-1.5 px-2 py-1.5" style={{ fontSize: 12.5 }}>
1159
- <input
1160
- className="min-w-0 flex-1 rounded-lg px-2 py-1"
1161
- style={{ border: "0.5px solid var(--dsw-border-l3)", background: "var(--dsw-input-bg, transparent)", color: "inherit" }}
1162
- placeholder={t.workspacePathPlaceholder}
1163
- value={wsPathDraft}
1164
- onChange={(e) => { setWsPathDraft(e.target.value); setWsPathError(null); }}
1165
- onKeyDown={(e) => {
1166
- if (e.key === "Enter") { e.preventDefault(); void addByPath(); }
1167
- }}
1168
- />
1169
- <button type="button" className="pw-chip" style={{ flex: "none" }} disabled={!wsPathDraft.trim()} onClick={() => void addByPath()}>
1170
- {t.workspacePathAdd}
1171
- </button>
1172
- </div>
1173
- {wsPathError && (
1174
- <div className="px-3.5 pb-1.5" style={{ fontSize: 11, color: "var(--dsw-danger)" }}>{wsPathError}</div>
1175
- )}
1176
1139
  </div>
1177
1140
  )}
1178
1141
  </div>
@@ -496,27 +496,6 @@ export function usePiWeb() {
496
496
  }
497
497
  }, [refreshWorkspaces]);
498
498
 
499
- /** 手动输入路径添加工作区(原生选择器不可用 / 无桌面环境时的兜底) */
500
- const addWorkspaceByPath = useCallback(async (dir: string): Promise<string | null> => {
501
- const trimmed = dir.trim().replace(/^["']|["']$/g, "");
502
- if (!trimmed) return null;
503
- try {
504
- const r = await fetch("/api/workspaces", {
505
- method: "POST",
506
- headers: { "Content-Type": "application/json" },
507
- body: JSON.stringify({ action: "add", path: trimmed }),
508
- });
509
- const j = await r.json();
510
- if (!r.ok || !j.success) throw new Error(j.error || `request failed (${r.status})`);
511
- setAddedWorkspaces(j.data?.workspaces ?? []);
512
- await refreshWorkspaces();
513
- return trimmed;
514
- } catch (error) {
515
- const message = error instanceof Error ? error.message : "failed to add workspace";
516
- setState((current) => ({ ...current, error: message }));
517
- return null;
518
- }
519
- }, [refreshWorkspaces]);
520
499
 
521
500
  const removeWorkspace = useCallback(
522
501
  async (dir: string) => {
@@ -906,7 +885,6 @@ export function usePiWeb() {
906
885
  setGroupBy,
907
886
  setOrderBy,
908
887
  addWorkspaceByPicker,
909
- addWorkspaceByPath,
910
888
  removeWorkspace,
911
889
  refreshModels,
912
890
  sendCommand,
package/src/i18n.tsx CHANGED
@@ -145,9 +145,6 @@ const zh = {
145
145
  workspaces: "工作区",
146
146
  searchSessions: "搜索会话...",
147
147
  addWorkspace: "添加工作区",
148
- workspacePathPlaceholder: "输入路径添加…",
149
- workspacePathInvalid: "路径无效或目录不存在",
150
- workspacePathAdd: "添加",
151
148
  groupBy: "分组方式",
152
149
  sortBy: "排序方式",
153
150
  byWorkspace: "按工作区",
@@ -678,9 +675,6 @@ const en: Dict = {
678
675
  workspaces: "Workspaces",
679
676
  searchSessions: "Search sessions...",
680
677
  addWorkspace: "Add workspace",
681
- workspacePathPlaceholder: "Type a path to add…",
682
- workspacePathInvalid: "Invalid path or directory does not exist",
683
- workspacePathAdd: "Add",
684
678
  groupBy: "Group by",
685
679
  sortBy: "Sort by",
686
680
  byWorkspace: "Workspace",