@liguoshuai/pi-web-chat 1.0.1
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/CHANGELOG.md +88 -0
- package/LICENSE +21 -0
- package/README.md +118 -0
- package/bin/pi-web-chat.js +67 -0
- package/package.json +47 -0
- package/public/app.js +860 -0
- package/public/index.html +68 -0
- package/public/style.css +349 -0
- package/server.js +322 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## [Unreleased]
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- **空内容 pi 气泡**:`message_start` 原本不区分角色,用户消息回显也建了一个空 pi 气泡 — 现在只对 `role:assistant` 开 streaming 块。
|
|
14
|
+
- **重复文本渲染**:pi 在 `message_start` 就带完整 content、随后又用 `text_delta` 发同一文本 — 改用 `text_end.content` 覆盖累加结果,避免出现 `WS_OKWS_OK`。
|
|
15
|
+
- **模型出错时空白回复**:当模型返回 `stopReason=error` 又无内容时(如选了不可用模型),现在显示明确的“生成失败 / 请换模型”提示。
|
|
16
|
+
- **侧边栏漏掉 Web 新建的会话**:`server.js` 传给 pi 的 `--session-dir` 导致新 session 被直接放在 sessions 根目录而非 cwd 子目录中;`listAllSessionFiles` 原仅扫子目录,于是 11 条 Web 创建的会话一个都列不出来 — 现在同时扫根目录下的 `.jsonl`。
|
|
17
|
+
- **新建会话非手动刷新不可见**:根因是上一条 — 侧边栏根本扫不到新文件。现在 `agent_settled` 后列表实时反映新会话,无需刷新页面。
|
|
18
|
+
- **初始化时模型 pill 永远空白**:`init()` 里 `// pull current state once:\n setTimeout(...)` 把 `\n` 当成字面字符写进了单行注释,`get_state` / `get_available_models` 永不发出 — 改为真实换行后两调用均发出。
|
|
19
|
+
- **工具调用参数显示不全**:新增 `toolcall_delta` / `toolcall_end` 处理,用 `toolcall_end` 的最终 `toolCall.arguments` 刷新头部 args 显示。
|
|
20
|
+
- **同一 agent 多轮 agent message 互相污染**:`message_start`(assistant) 重置本块的 text/thinking 累加器,避免上一轮工具调用与下一轮文本串到一起。
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
- 会话切换时右侧完整历史恢复(通过 REST `/api/session` 重建 parent 链)
|
|
24
|
+
- 模型切换器(顶栏 pill,支持所有 pi 配置的模型)
|
|
25
|
+
- Tool-call 折叠块(实时输出、可展开/折叠)
|
|
26
|
+
- Thinking 折叠块
|
|
27
|
+
- 发送按钮双态(发送/停止)+ 红色停止态
|
|
28
|
+
- 新会话后侧边栏在 `agent_settled` 立即刷新(无需手动刷新页面)
|
|
29
|
+
|
|
30
|
+
### Changed
|
|
31
|
+
- 侧边栏会话项去除无意义的 `●` 圆点,改为 tooltip 显示完整 jsonl 路径
|
|
32
|
+
- 发送按钮禁用逻辑:仅在 wsConnected=false 时禁用,而非 streaming 时
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## [0.2.0] - 2026-07-19
|
|
37
|
+
|
|
38
|
+
### Added
|
|
39
|
+
- **新建会话流程修复**:`btnNew` 点击后刷新侧边栏、清理流式状态、生成新 ws 代次
|
|
40
|
+
- **WebSocket 代次机制**:`wsGen` 防止旧 socket 消息污染新上下文
|
|
41
|
+
- **输入框红边提示**:ws 未连时提交会有 350ms 红色 flash
|
|
42
|
+
- **连接状态文本动态更新**:「连接中…」→「已连接」/「已断开」
|
|
43
|
+
- **提交**:`5a8f036` "Fix new-session flow: ws generation guard + sidebar refresh + drop input"
|
|
44
|
+
|
|
45
|
+
### Fixed
|
|
46
|
+
- 新建会话后左侧不出现(缺 `refreshSessions()`)
|
|
47
|
+
- 快速切换会话导致 prompt 被当作 abort(旧 socket stragglers)
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## [0.1.0] - 2026-07-19
|
|
52
|
+
|
|
53
|
+
### Added
|
|
54
|
+
- 项目初始化:`server.js` (Express + ws) + `public/` (HTML/JS/CSS)
|
|
55
|
+
- REST API:
|
|
56
|
+
- `GET /api/sessions?cwd=` — 列出该 cwd 下所有 session(标题、时间、消息数)
|
|
57
|
+
- `GET /api/session?file=` — 重建会话的根→叶对话线
|
|
58
|
+
- WebSocket `/ws?cwd=&session=` — 1:1 桥接 `pi --mode rpc`
|
|
59
|
+
- 前端 UI:
|
|
60
|
+
- 左侧栏:搜索、历史列表、点击切换
|
|
61
|
+
- 右侧:空状态建议、流式对话、Markdown 渲染
|
|
62
|
+
- 底部:textarea + 发送/停止按钮
|
|
63
|
+
- 顶栏:会话名、模型选择 pill
|
|
64
|
+
- 流式渲染:文本打字光标、thinking 块、工具折叠块
|
|
65
|
+
- 会话历史持久化:复用 pi 原生 `~/.pi/agent/sessions/*.jsonl`
|
|
66
|
+
- 多模型切换(pi 配置的所有 provider/model)
|
|
67
|
+
- 响应式深色主题(ChatGPT/Gemini 风格)
|
|
68
|
+
|
|
69
|
+
### Technical Debt (已知)
|
|
70
|
+
- 图片上传未接
|
|
71
|
+
- 多 cwd / 项目切换器未做
|
|
72
|
+
- fork / tree / clone 浏览未接
|
|
73
|
+
- 浅/深主题切换未做
|
|
74
|
+
- 双端实时同步未做
|
|
75
|
+
- 鉴权未做
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## Legend
|
|
80
|
+
|
|
81
|
+
| 标记 | 含义 |
|
|
82
|
+
|------|------|
|
|
83
|
+
| **Added** | 新功能 |
|
|
84
|
+
| **Changed** | 现有功能变更 |
|
|
85
|
+
| **Deprecated** | 即将移除 |
|
|
86
|
+
| **Removed** | 已移除 |
|
|
87
|
+
| **Fixed** | Bug 修复 |
|
|
88
|
+
| **Security** | 安全相关修复 |
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 liguoshuai
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# pi-web-chat
|
|
2
|
+
|
|
3
|
+
一个 [pi](https://pi.dev) 编程代理的 Web 界面,风格参考 ChatGPT / Gemini ——
|
|
4
|
+
左侧历史会话侧边栏 + 右侧对话区 + 底部输入框。底层通过 pi 的 **RPC 模式**
|
|
5
|
+
(`pi --mode rpc`) 与 pi 子进程通信,前端走 WebSocket 流式渲染。
|
|
6
|
+
|
|
7
|
+
> 项目主页:https://github.com/liguoshuai-1990/pi-web-chat
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## 📸 功能
|
|
12
|
+
|
|
13
|
+
- 💬 **流式对话** —— 文本逐字流式渲染,带打字光标
|
|
14
|
+
- 🗂️ **会话历史** —— 自动读取 `~/.pi/agent/sessions/` 下所有历史会话,按首条用户消息做标题
|
|
15
|
+
- 🆕 **新建 / 切换会话** —— 通过左侧栏点击或生成中新建
|
|
16
|
+
- ⚙️ **工具调用折叠块** —— bash / read / write / edit 等以可折叠卡片显示,实时输出
|
|
17
|
+
- 💭 **思考过程折叠块** —— 模型 thinking 以独立可折叠卡片显示
|
|
18
|
+
- 🧩 **模型切换** —— 顶栏模型 pill,覆盖所有 pi 已配置的模型
|
|
19
|
+
- ⏹️ **中止生成** —— 生成中点击发送按钮即可中断
|
|
20
|
+
- ⌨️ **Markdown 渲染** —— 代码块、表格、列表、链接、引用、标题
|
|
21
|
+
- 🖥️ **工作目录绑定** —— 每个 ws 连接由 cwd 决定会话范围(默认 `~`)
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## 🚀 快速开始
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
cd pi-web-chat
|
|
29
|
+
npm install
|
|
30
|
+
|
|
31
|
+
# 确保 pi 已安装并已配置好至少一个 provider/model:
|
|
32
|
+
# pi (交互模式 → 运行 /login 选择 provider,或设置 API key)
|
|
33
|
+
npm start
|
|
34
|
+
# → http://localhost:3000
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### 环境变量
|
|
38
|
+
|
|
39
|
+
| 变量 | 默认值 | 说明 |
|
|
40
|
+
| -------------------- | --------------------------------------- | --------------------------- |
|
|
41
|
+
| `PORT` | `3000` | Web 服务监听端口 |
|
|
42
|
+
| `PI_BIN` | 自动探测(`~/.npm-global/bin/pi` 等) | 显式指定 pi 可执行文件路径 |
|
|
43
|
+
| `PI_SESSIONS_DIR` | `~/.pi/agent/sessions` | pi 的 session 存储目录 |
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## 📁 项目结构
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
pi-web-chat/
|
|
51
|
+
├── README.md 本文件
|
|
52
|
+
├── DESIGN.md 设计文档(架构、数据流、决策)
|
|
53
|
+
├── ISSUES.md 历次问题排查与修复
|
|
54
|
+
├── CHANGELOG.md 版本变更日志
|
|
55
|
+
├── package.json
|
|
56
|
+
├── package-lock.json
|
|
57
|
+
├── server.js Express + WebSocket,桥接 pi RPC
|
|
58
|
+
├── .gitignore
|
|
59
|
+
└── public/
|
|
60
|
+
├── index.html 单页 UI
|
|
61
|
+
├── app.js 前端逻辑
|
|
62
|
+
└── style.css ChatGPT/Gemini 风格样式
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## 🏗️ 架构
|
|
68
|
+
|
|
69
|
+
```
|
|
70
|
+
浏览器 ──WebSocket(/ws?cwd=...&session=...)──► Node server.js ──stdin/stdout(JSONL)──► pi --mode rpc
|
|
71
|
+
(一个 ws 连接 spawn 一个 pi 子进程)
|
|
72
|
+
│
|
|
73
|
+
├──REST /api/sessions ──► 直读 JSONL 列历史
|
|
74
|
+
└──REST /api/session?file=... ─► 重建根→叶路径
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## 📝 License
|
|
80
|
+
|
|
81
|
+
MIT
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## 🛠️ Systemd 服务(可选)
|
|
86
|
+
|
|
87
|
+
若希望开机自启、后台常驻、重启自愈,可安装为 user-level systemd 服务(无需 sudo):
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
# 从项目根目录运行
|
|
91
|
+
./scripts/install-service.sh 3000
|
|
92
|
+
|
|
93
|
+
# 或自定义端口(默认 3000):
|
|
94
|
+
./scripts/install-service.sh 8080
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
脚本会:
|
|
98
|
+
1. 在 `~/.config/systemd/user/pi-web-chat.service` 生成 unit
|
|
99
|
+
2. `systemctl --user daemon-reload && systemctl --user enable --now pi-web-chat`
|
|
100
|
+
3. 设置 `Restart=on-failure` 自动重启
|
|
101
|
+
|
|
102
|
+
查看状态 / 日志:
|
|
103
|
+
```bash
|
|
104
|
+
systemctl --user status pi-web-chat
|
|
105
|
+
journalctl --user -u pi-web-chat -f
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
> ⚠️ **Linger**:systemd user 服务默认随登录会话结束。若需 **开机自启 / 登出后继续跑**,需一次性启用:
|
|
109
|
+
> ```bash
|
|
110
|
+
> sudo loginctl enable-linger $USER
|
|
111
|
+
> ```
|
|
112
|
+
|
|
113
|
+
卸载:
|
|
114
|
+
```bash
|
|
115
|
+
systemctl --user disable --now pi-web-chat
|
|
116
|
+
rm ~/.config/systemd/user/pi-web-chat.service
|
|
117
|
+
systemctl --user daemon-reload
|
|
118
|
+
```
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// pi-web-chat CLI entry point
|
|
3
|
+
// Usage: pi-web-chat [--port=3000] [--cwd=/path] [--help]
|
|
4
|
+
|
|
5
|
+
import { fileURLToPath } from "url";
|
|
6
|
+
import { dirname, resolve } from "path";
|
|
7
|
+
import { spawn } from "child_process";
|
|
8
|
+
|
|
9
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
10
|
+
const __dirname = dirname(__filename);
|
|
11
|
+
const ROOT = resolve(__dirname, "..");
|
|
12
|
+
const SERVER = resolve(ROOT, "server.js");
|
|
13
|
+
|
|
14
|
+
function printHelp() {
|
|
15
|
+
console.log(`
|
|
16
|
+
pi-web-chat — Web UI for pi coding agent (RPC mode)
|
|
17
|
+
|
|
18
|
+
Usage:
|
|
19
|
+
pi-web-chat [options]
|
|
20
|
+
|
|
21
|
+
Options:
|
|
22
|
+
-p, --port <number> Port to listen on (default: 3000, env PORT)
|
|
23
|
+
-c, --cwd <path> Working directory for pi sessions (default: $HOME)
|
|
24
|
+
-h, --help Show this help
|
|
25
|
+
|
|
26
|
+
Environment:
|
|
27
|
+
PORT Same as --port
|
|
28
|
+
PI_BIN Path to pi binary (auto-detected if not set)
|
|
29
|
+
PI_SESSIONS_DIR Pi session storage directory (default: ~/.pi/agent/sessions)
|
|
30
|
+
|
|
31
|
+
Examples:
|
|
32
|
+
pi-web-chat
|
|
33
|
+
pi-web-chat --port 8080
|
|
34
|
+
PORT=4000 pi-web-chat
|
|
35
|
+
`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function parseArgs(argv) {
|
|
39
|
+
const opts = { port: process.env.PORT || 3000, cwd: process.env.HOME };
|
|
40
|
+
for (let i = 0; i < argv.length; i++) {
|
|
41
|
+
const a = argv[i];
|
|
42
|
+
if (a === "-h" || a === "--help") { printHelp(); process.exit(0); }
|
|
43
|
+
if (a === "-p" || a === "--port") opts.port = Number(argv[++i]);
|
|
44
|
+
else if (a.startsWith("--port=")) opts.port = Number(a.split("=")[1]);
|
|
45
|
+
else if (a === "-c" || a === "--cwd") opts.cwd = argv[++i];
|
|
46
|
+
else if (a.startsWith("--cwd=")) opts.cwd = a.split("=")[1];
|
|
47
|
+
else { console.error(`Unknown option: ${a}`); printHelp(); process.exit(1); }
|
|
48
|
+
}
|
|
49
|
+
return opts;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const opts = parseArgs(process.argv.slice(2));
|
|
53
|
+
|
|
54
|
+
// Spawn server.js as a child so we can forward signals cleanly.
|
|
55
|
+
const child = spawn("node", [SERVER], {
|
|
56
|
+
cwd: opts.cwd,
|
|
57
|
+
env: { ...process.env, PORT: String(opts.port) },
|
|
58
|
+
stdio: "inherit",
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
child.on("exit", (code) => process.exit(code ?? 0));
|
|
62
|
+
child.on("error", (e) => { console.error(e); process.exit(1); });
|
|
63
|
+
|
|
64
|
+
// Forward signals.
|
|
65
|
+
["SIGINT", "SIGTERM", "SIGHUP"].forEach((sig) => {
|
|
66
|
+
process.on(sig, () => child.kill(sig));
|
|
67
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@liguoshuai/pi-web-chat",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "A ChatGPT/Gemini-style web UI for the pi coding agent, powered by pi's RPC mode.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "server.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"pi-web-chat": "bin/pi-web-chat.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"server.js",
|
|
12
|
+
"bin/",
|
|
13
|
+
"public/",
|
|
14
|
+
"README.md",
|
|
15
|
+
"CHANGELOG.md",
|
|
16
|
+
"LICENSE"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"start": "node server.js",
|
|
20
|
+
"dev": "node --watch server.js",
|
|
21
|
+
"prepare": "chmod +x bin/pi-web-chat.js"
|
|
22
|
+
},
|
|
23
|
+
"keywords": [
|
|
24
|
+
"pi",
|
|
25
|
+
"coding-agent",
|
|
26
|
+
"chat",
|
|
27
|
+
"web-ui",
|
|
28
|
+
"rpc",
|
|
29
|
+
"ai"
|
|
30
|
+
],
|
|
31
|
+
"repository": {
|
|
32
|
+
"type": "git",
|
|
33
|
+
"url": "git+https://github.com/liguoshuai-1990/pi-web-chat.git"
|
|
34
|
+
},
|
|
35
|
+
"bugs": {
|
|
36
|
+
"url": "https://github.com/liguoshuai-1990/pi-web-chat/issues"
|
|
37
|
+
},
|
|
38
|
+
"homepage": "https://github.com/liguoshuai-1990/pi-web-chat#readme",
|
|
39
|
+
"license": "MIT",
|
|
40
|
+
"engines": {
|
|
41
|
+
"node": ">=18"
|
|
42
|
+
},
|
|
43
|
+
"dependencies": {
|
|
44
|
+
"express": "^4.21.2",
|
|
45
|
+
"ws": "^8.18.0"
|
|
46
|
+
}
|
|
47
|
+
}
|