pi-web-ui 0.26.3 → 0.27.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
@@ -51,6 +51,7 @@ Requires Node.js ≥ 22.19 and a configured pi install.
51
51
 
52
52
  **Models & settings**
53
53
 
54
+ - 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.
54
55
  - Model management — edit `models.json` in the UI and set per-provider API keys (keys/headers never leave the server).
55
56
  - Thinking level per model (only the levels the model actually supports are shown).
56
57
  - First-run setup wizard.
@@ -83,7 +84,13 @@ Requires Node.js ≥ 22.19 and a configured pi install.
83
84
 
84
85
  ## Screenshots
85
86
 
86
- ![pi-web-ui main interface](https://cdn.jsdelivr.net/gh/xing-shuyin/pi-web-ui@main/assets/shot.jpeg)
87
+ ![Settings panel](https://raw.githubusercontent.com/xing-shuyin/pi-web-ui/main/assets/shot1.png)
88
+
89
+ ![Built-in terminal](https://raw.githubusercontent.com/xing-shuyin/pi-web-ui/main/assets/shot2.jpeg)
90
+
91
+ ![Chat interface](https://raw.githubusercontent.com/xing-shuyin/pi-web-ui/main/assets/shot3.jpeg)
92
+
93
+ ![Git source control panel](https://raw.githubusercontent.com/xing-shuyin/pi-web-ui/main/assets/shot4.jpeg)
87
94
 
88
95
  ## Install
89
96
 
@@ -157,6 +164,48 @@ Options: `--port` (default 8787), `--cwd` (workspace), `--data-dir` (sessions),
157
164
  regenerates the config and restarts the service — that's how you change its
158
165
  port/cwd.
159
166
 
167
+ ## Themes
168
+
169
+ 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.
170
+
171
+ Built-in themes ship in the npm package (`themes/`, e.g. the bundled light theme). The theme picker lives in the top bar (🌞 icon); the current choice is stored per browser in `localStorage`.
172
+
173
+ ### Using a theme
174
+
175
+ Just pick it in the top bar — built-in and user themes are merged in the same menu. User themes win over built-ins on the same id.
176
+
177
+ ### Providing a theme locally (no GitHub needed)
178
+
179
+ Any CSS file dropped into your **data-dir themes folder** shows up in the theme menu automatically — no restart, no rebuild:
180
+
181
+ 1. Find your data dir (default `~/.pi-web`, override with `PI_WEB_DATA_DIR`).
182
+ 2. Create `<dataDir>/themes/` and drop your stylesheet in: e.g. `~/.pi-web/themes/my-theme.css`.
183
+ 3. Reload the page and pick it in the top bar. The **file name (without `.css`)** is the theme id shown in the menu.
184
+
185
+ ```
186
+ ~/.pi-web/
187
+ └── themes/
188
+ └── my-theme.css # appears in the menu as "my-theme"
189
+ ```
190
+
191
+ Easiest way to write one: copy `themes/light.css` (or the bundled dark `web/src/styles.css` from the source repo) and change the `:root` colors plus any hardcoded values — the file must be **self-contained**. Notes:
192
+
193
+ - The **xterm terminal canvas keeps its dark palette** in every theme (`TERM_THEME` in `TermXterm.tsx`); keep the terminal's container background matching it so the canvas blends.
194
+ - Syntax-highlight colors (`highlight.js`'s `github-dark.css` is bundled) must be overridden in your theme file or code will be unreadable — see the `.hljs` overrides at the bottom of `themes/light.css` for the pattern.
195
+ - Theme ids must match `^[A-Za-z0-9_-]+$` (no dots/slashes — path-traversal guard on the server).
196
+
197
+ ### Contributing a theme to the repository (GitHub)
198
+
199
+ Want your theme shipped to everyone? Open a pull request at [github.com/xing-shuyin/pi-web-ui](https://github.com/xing-shuyin/pi-web-ui):
200
+
201
+ 1. Fork the repo and clone it.
202
+ 2. Create your theme as `themes/<id>.css` — a **self-contained** stylesheet. Copy `themes/light.css` as the starting template (it's the generator output for a full standalone theme).
203
+ 3. Verify locally: run `npm run dev`, then use the top bar theme picker — your theme must be listed and render correctly (chat cards, code blocks, tool-call cards, git/terminal panels).
204
+ 4. If you only changed colors in `styles.css` and want the bundled light theme updated too, regenerate it with `node make-light-theme.mjs`.
205
+ 5. Commit (`git add themes/<id>.css`) and open the PR. The `themes/` folder is already in the npm package `files` whitelist, so once merged and released, `npm i -g pi-web-ui` will ship your theme to everyone.
206
+
207
+ 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`), keep the xterm area dark, and override `.hljs` syntax colors for readable code.
208
+
160
209
  ## Security
161
210
 
162
211
  - **Loopback-only by default** — the server binds `127.0.0.1` and is not
package/README.zh-CN.md CHANGED
@@ -51,6 +51,7 @@
51
51
  **模型与设置**
52
52
 
53
53
  - 模型管理 —— UI 里编辑 models.json、按 provider 设置 API key(密钥/headers 永不下发浏览器)。
54
+ - 主题切换 —— 顶栏选择主题;每个主题是完整独立的样式表(默认深色 + 内置亮色)。如何添加自定义主题或向仓库贡献主题,见 [主题](#主题)。
54
55
  - 思考强度(thinking level)按模型切换(只显示该模型实际支持的档位)。
55
56
  - 首次配置引导(PiSetupModal)。
56
57
  - 设置面板 —— 系统提示词(追加或整体替换)、技能/插件一键开关(即时生效)、设置预设保存/应用/删除、视觉桥模型与开关。
@@ -82,7 +83,13 @@
82
83
 
83
84
  ## 界面截图
84
85
 
85
- ![pi-web-ui 主界面](https://cdn.jsdelivr.net/gh/xing-shuyin/pi-web-ui@main/assets/shot.jpeg)
86
+ ![设置面板](https://raw.githubusercontent.com/xing-shuyin/pi-web-ui/main/assets/shot1.png)
87
+
88
+ ![内置终端](https://raw.githubusercontent.com/xing-shuyin/pi-web-ui/main/assets/shot2.jpeg)
89
+
90
+ ![对话界面](https://raw.githubusercontent.com/xing-shuyin/pi-web-ui/main/assets/shot3.jpeg)
91
+
92
+ ![Git 源代码管理面板](https://raw.githubusercontent.com/xing-shuyin/pi-web-ui/main/assets/shot4.jpeg)
86
93
 
87
94
  ## 安装
88
95
 
@@ -152,6 +159,48 @@ pi-web-ui server unquiesce # 解除排空,恢复接收新工
152
159
  `--name`(自定义服务名)。重复执行 `server install` 并传入新选项即可重新生成配置
153
160
  并重启服务 —— 这就是修改已装服务端口/工作目录的方式。
154
161
 
162
+ ## 主题
163
+
164
+ 每个主题是**一份完整独立的样式表** —— 即内置深色 `web/src/styles.css` 的整份副本,只是配色不同(不做 CSS 变量抽取、不需要引入基础文件)。切换主题就是整文件替换,因此任何主题都能在所有版本上工作。
165
+
166
+ 内置主题随 npm 包分发(`themes/`,例如自带的亮色主题)。主题选择器在顶栏(🌞 图标),当前选择按浏览器存在 `localStorage`。
167
+
168
+ ### 使用主题
169
+
170
+ 在顶栏直接选择即可 —— 内置主题和用户主题合并显示在同一个菜单里;同名 id 时用户主题优先。
171
+
172
+ ### 本地添加主题(无需 GitHub)
173
+
174
+ 把任意 CSS 文件丢进**数据目录的 themes 文件夹**就会自动出现在主题菜单里 —— 不用重启、不用重新构建:
175
+
176
+ 1. 找到数据目录(默认 `~/.pi-web`,可用 `PI_WEB_DATA_DIR` 覆盖)。
177
+ 2. 创建 `<dataDir>/themes/` 并放入你的样式表,例如 `~/.pi-web/themes/my-theme.css`。
178
+ 3. 刷新页面,在顶栏选择它。**文件名(去掉 `.css`)** 就是菜单里显示的主题 id。
179
+
180
+ ```
181
+ ~/.pi-web/
182
+ └── themes/
183
+ └── my-theme.css # 菜单里显示为 "my-theme"
184
+ ```
185
+
186
+ 最容易的写法:复制 `themes/light.css`(或源码仓库里内置的深色 `web/src/styles.css`),改 `:root` 颜色和必要的硬编码值即可 —— 文件必须**自包含**。注意:
187
+
188
+ - **xterm 终端画布在所有主题下都保持深色**(`TermXterm.tsx` 的 `TERM_THEME`);终端容器背景要与其保持一致,画布才能自然融合。
189
+ - 代码高亮色(打包自带 `highlight.js` 的 `github-dark.css`)必须在你的主题文件里覆盖,否则代码会看不清 —— 参照 `themes/light.css` 末尾的 `.hljs` 覆盖写法。
190
+ - 主题 id 必须匹配 `^[A-Za-z0-9_-]+$`(不能有点和斜杠 —— 服务端有路径穿越防护)。
191
+
192
+ ### 向仓库贡献主题(GitHub)
193
+
194
+ 想让你的主题随包分发给所有人?在 [github.com/xing-shuyin/pi-web-ui](https://github.com/xing-shuyin/pi-web-ui) 开一个 Pull Request:
195
+
196
+ 1. Fork 并 clone 仓库。
197
+ 2. 创建 `themes/<id>.css` —— 一份**自包含**的样式表。以 `themes/light.css` 为模板(它是生成器产出的完整独立主题)。
198
+ 3. 本地验证:运行 `npm run dev`,用顶栏主题选择器确认你的主题能被列出、渲染正确(对话卡片、代码块、工具调用卡片、Git/终端面板)。
199
+ 4. 如果你只改了 `styles.css` 里的颜色、想让内置亮色主题同步更新,用 `node make-light-theme.mjs` 重新生成。
200
+ 5. 提交(`git add themes/<id>.css`)并开 PR。`themes/` 已在 npm 包 `files` 白名单里,合并发布后 `npm i -g pi-web-ui` 即可把你的主题带给所有人。
201
+
202
+ 合并主题的规则:必须是单一自包含 CSS 文件、是完整独立主题(不得 import 基础 `styles.css`)、保持 xterm 区域深色、覆盖 `.hljs` 语法高亮色以保证代码可读。
203
+
155
204
  ## 安全
156
205
 
157
206
  - **默认只绑 loopback** —— 服务器只监听 `127.0.0.1`,不暴露到网络;需要局域网访问或
@@ -31,6 +31,7 @@ import { VERSION, getAgentDir } from "@earendil-works/pi-coding-agent";
31
31
  import { AgentService, previewKind, workspacePath, QuiesceRejectedError, } from "./agent-service.js";
32
32
  import { startControlServer } from "./control-socket.js";
33
33
  import { ensureWindowsBash, windowsBashDir } from "./ensure-bash.js";
34
+ import { listThemes, resolveThemeFile } from "./themes.js";
34
35
  const PORT = Number(process.env.PORT ?? 8787);
35
36
  const CWD = resolve(process.env.PI_WEB_CWD ?? process.cwd());
36
37
  const DATA_DIR = resolve(process.env.PI_WEB_DATA_DIR ?? join(homedir(), ".pi-web"));
@@ -122,7 +123,41 @@ app.get("/api/file", async (req, res) => {
122
123
  // module so it works when installed as a package (global/npx/Docker), not just
123
124
  // from the repo root. In dev, Vite serves the UI on :5173 and proxies /ws.
124
125
  const here = dirname(fileURLToPath(import.meta.url)); // <pkg>/dist/server or <pkg>/server
125
- const pkgRoot = resolve(here, "..", "..");
126
+ // Resolve the package root robustly: dev runs from <repo>/server (tsx), prod
127
+ // from <pkg>/dist/server — the ancestor that actually has package.json wins.
128
+ function resolvePkgRoot() {
129
+ const candidates = [
130
+ resolve(here, ".."),
131
+ resolve(here, "..", ".."),
132
+ resolve(here, "..", "..", ".."),
133
+ ];
134
+ for (const c of candidates) {
135
+ if (existsSync(join(c, "package.json")))
136
+ return c;
137
+ }
138
+ return candidates[0];
139
+ }
140
+ const pkgRoot = resolvePkgRoot();
141
+ // Theme CSS files: complete standalone stylesheets. Builtin themes ship in
142
+ // <pkg>/themes (npm files whitelist); user themes can be dropped into
143
+ // <dataDir>/themes and are served alongside (user wins on id collision).
144
+ const BUILTIN_THEMES_DIR = join(pkgRoot, "themes");
145
+ const USER_THEMES_DIR = join(DATA_DIR, "themes");
146
+ app.get("/api/themes", (_req, res) => {
147
+ res.json({ themes: listThemes(BUILTIN_THEMES_DIR, USER_THEMES_DIR) });
148
+ });
149
+ // Serve a theme's full CSS file so the frontend can swap the whole stylesheet.
150
+ // Registered before the SPA catch-all below (otherwise it'd return index.html).
151
+ app.get("/themes/:id.css", (req, res) => {
152
+ const file = resolveThemeFile(BUILTIN_THEMES_DIR, USER_THEMES_DIR, req.params.id);
153
+ if (!file) {
154
+ res.status(404).end("theme not found");
155
+ return;
156
+ }
157
+ res.setHeader("Content-Type", "text/css; charset=utf-8");
158
+ res.setHeader("Cache-Control", "no-cache");
159
+ res.sendFile(file);
160
+ });
126
161
  /** Set in the env of the replacement child spawned by a self-update restart. */
127
162
  const RESTART_CHILD_ENV = "PI_WEB_RESTART_CHILD";
128
163
  const webDist = join(pkgRoot, "web", "dist");
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Theme management: complete standalone CSS files that replace the whole UI
3
+ * stylesheet. Each theme is a full copy of web/src/styles.css with a different
4
+ * palette — no variable extraction, the browser just swaps the entire file.
5
+ *
6
+ * Theme sources (merged, user wins over builtin on id collision):
7
+ * - builtin: <pkgRoot>/themes/*.css (ships with the npm package)
8
+ * - user : <dataDir>/themes/*.css (drop a css file here to add a theme)
9
+ */
10
+ import { existsSync, readdirSync, statSync } from "node:fs";
11
+ import { join } from "node:path";
12
+ /** Only simple file ids — no path traversal. */
13
+ const ID_RE = /^[A-Za-z0-9_-]+$/;
14
+ export function listThemes(builtinDir, userDir) {
15
+ const scan = (dir, builtin) => {
16
+ if (!existsSync(dir))
17
+ return [];
18
+ return readdirSync(dir)
19
+ .filter((f) => f.endsWith(".css"))
20
+ .filter((f) => ID_RE.test(f.slice(0, -4)))
21
+ .sort()
22
+ .map((f) => ({ id: f.slice(0, -4), name: f.slice(0, -4), builtin }));
23
+ };
24
+ const builtin = scan(builtinDir, true);
25
+ const user = scan(userDir, false);
26
+ const seen = new Set();
27
+ return [...builtin, ...user]
28
+ .filter((t) => (seen.has(t.id) ? false : (seen.add(t.id), true)))
29
+ .sort((a, b) => a.name.localeCompare(b.name));
30
+ }
31
+ /** Resolve a theme id to its css file path (user dir first). */
32
+ export function resolveThemeFile(builtinDir, userDir, id) {
33
+ if (!ID_RE.test(id))
34
+ return null;
35
+ const userPath = join(userDir, `${id}.css`);
36
+ if (existsSync(userPath) && statSync(userPath).isFile())
37
+ return userPath;
38
+ const builtinPath = join(builtinDir, `${id}.css`);
39
+ if (existsSync(builtinPath) && statSync(builtinPath).isFile()) {
40
+ return builtinPath;
41
+ }
42
+ return null;
43
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-web-ui",
3
- "version": "0.26.3",
3
+ "version": "0.27.0",
4
4
  "description": "Web chat interface for the pi coding agent, powered by the pi SDK (@earendil-works/pi-coding-agent) — one-command run, Docker/systemd/launchd deployable",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -13,6 +13,7 @@
13
13
  "dist/",
14
14
  "web/dist/",
15
15
  "web/public/",
16
+ "themes/",
16
17
  "deploy/",
17
18
  "extensions/",
18
19
  "README.md",