@liguoshuai/pi-web-chat 2.4.0 → 2.5.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/docs/CHANGELOG.md +42 -1
- package/package.json +3 -3
- package/public/app.js +20 -22
package/docs/CHANGELOG.md
CHANGED
|
@@ -7,7 +7,48 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
---
|
|
9
9
|
|
|
10
|
-
## [2.
|
|
10
|
+
## [2.5.1] - 2026-09-01
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- **AI Coding Agent 工作指南与工程规范 (`AGENTS.md` & `CLAUDE.md`)**:
|
|
14
|
+
- 确立三大强制铁律:工作前必拉取远端主干(`git pull origin main`)、任何修改必递增 SemVer 语义化版本号并在 Web 与各客户端正确呈现、任务完成必通过全量测试并提交推送至远端。
|
|
15
|
+
- 制定 Monorepo 全仓版本同步清单(Lockstep)与多端版本呈现规范。
|
|
16
|
+
- **客户端版本号呈现强化**:
|
|
17
|
+
- Android 原生端:启用 `buildConfig = true`,在侧边栏抽屉底部及后端配置对话框中通过 `BuildConfig.VERSION_NAME` 实时展示客户端版本号(`v2.5.1`)。
|
|
18
|
+
- HarmonyOS 鸿蒙原生端:在侧边栏底部展示应用版本号(`v2.5.1`)。
|
|
19
|
+
- Web 端:通过网关 `/api/config` 动态获取并在侧边栏底部 `#appVersion` 区域正确展示当前版本号(`v2.5.1`)。
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
- 全端版本号统一升级至 2.5.1(Root / Protocol / Server / Web / Android / HarmonyOS)。
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## [2.5.0] - 2026-09-01
|
|
27
|
+
|
|
28
|
+
### Added
|
|
29
|
+
- Android 端聊天消息、思考过程与工具执行块均显示时间戳(当天 `HH:mm`,跨天 `MM-dd HH:mm`)。
|
|
30
|
+
- Android 端新增工具执行渲染(`tool_execution_start/update/end`):展示工具名、参数、结果与执行状态(执行中/完成/错误)。
|
|
31
|
+
- 用户消息头部显示 `user`,助手消息头部显示 `pi`。
|
|
32
|
+
|
|
33
|
+
### Changed
|
|
34
|
+
- Android 端会话列表在新建会话、`agent_start`/`agent_settled`、切换会话及服务端返回 `sessionFile` 时即时刷新,新会话更快出现在侧边栏。
|
|
35
|
+
- 全端版本号统一升级至 2.5.0。
|
|
36
|
+
|
|
37
|
+
## [2.4.1] - 2026-09-01
|
|
38
|
+
|
|
39
|
+
### Fixed
|
|
40
|
+
- **彻底解决新建对话中断已有后台任务的问题**:
|
|
41
|
+
- 修复了 `startNewSession` 在已有活动 WebSocket 连接上直接发送 `new_session` RPC 指令导致原有会话子进程被重置与中断的问题。新建会话时安全断开当前连接(原会话保持后台运行),并为新会话连接独立的无状态 Agent。
|
|
42
|
+
- 服务端网关 `NEW_SESSION` 与 `SWITCH_SESSION` 消息处理器增加安全解绑(detach)逻辑,避免跨会话指令误触或覆盖已有活跃 Agent 进程。
|
|
43
|
+
- **精准修复老会话误判为“运行中”的状态异常**:
|
|
44
|
+
- 严格区分进程资源防回收判定(`isBusy`,包含 pending 异步请求)与真实 AI 流式生成状态(`isStreaming`,严格对应 `state === 'streaming'`)。
|
|
45
|
+
- 服务端 `/api/sessions` 接口与 `backfill_end` 状态同步均修正为读取 `isStreaming`,彻底杜绝老会话连接时因 `get_state`/`get_available_models` 临时 pending 导致误判为“运行中”的问题。
|
|
46
|
+
- 优化了事件环形缓存机制:会话生成结束收敛(`agent_settled`)后彻底清空缓冲区,且仅在流式生成期间缓存事件,避免非流式 RPC 响应污染回放缓冲区导致前端误进入流式状态。
|
|
47
|
+
- 前端 `updateState` 及侧边栏会话路径比对增加统一路径规范化匹配(`sameSession`),避免路径形式差异导致草稿会话残留或状态错乱。
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## [2.4.0] - 2026-08-31
|
|
11
52
|
|
|
12
53
|
### Fixed & Improved
|
|
13
54
|
- **后台异步长任务稳定性与持久化机制全面强化 (Background Async Persistence & Lifetime Fix)**:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@liguoshuai/pi-web-chat",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.5.1",
|
|
4
4
|
"description": "A ChatGPT/Gemini-style web UI for the pi coding agent, powered by pi's RPC mode.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "server.js",
|
|
@@ -40,8 +40,8 @@
|
|
|
40
40
|
"node": ">=18"
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
|
-
"@liguoshuai/pi-chat-protocol": "2.
|
|
44
|
-
"@liguoshuai/pi-chat-server": "2.
|
|
43
|
+
"@liguoshuai/pi-chat-protocol": "2.5.1",
|
|
44
|
+
"@liguoshuai/pi-chat-server": "2.5.1",
|
|
45
45
|
"express": "^4.21.2",
|
|
46
46
|
"ws": "^8.18.0"
|
|
47
47
|
},
|
package/public/app.js
CHANGED
|
@@ -547,6 +547,12 @@ function formatMessageTime(ts) {
|
|
|
547
547
|
return `${year}-${month}-${date} ${timeStr}`;
|
|
548
548
|
}
|
|
549
549
|
|
|
550
|
+
function sameSession(a, b) {
|
|
551
|
+
if (!a || !b) return false;
|
|
552
|
+
if (a === b) return true;
|
|
553
|
+
return a.replace(/^[.~]\//, "") === b.replace(/^[.~]\//, "");
|
|
554
|
+
}
|
|
555
|
+
|
|
550
556
|
// ---- Sidebar / sessions ----
|
|
551
557
|
async function refreshSessions() {
|
|
552
558
|
const cwd = state.cwd || "";
|
|
@@ -565,7 +571,7 @@ function renderSidebar(sessions) {
|
|
|
565
571
|
if (!list) return;
|
|
566
572
|
list.innerHTML = "";
|
|
567
573
|
|
|
568
|
-
const hasCurrentInSessions = Boolean(state.currentSessionFile && sessions.some(s => s.file
|
|
574
|
+
const hasCurrentInSessions = Boolean(state.currentSessionFile && sessions.some(s => sameSession(s.file, state.currentSessionFile)));
|
|
569
575
|
|
|
570
576
|
// If current session is a new/draft session not yet in the session list from REST API:
|
|
571
577
|
if (!hasCurrentInSessions && (!state.currentSessionFile || $("#emptyState")?.style.display !== "none" || state.streaming)) {
|
|
@@ -603,7 +609,7 @@ function renderSidebar(sessions) {
|
|
|
603
609
|
}
|
|
604
610
|
|
|
605
611
|
sessions.forEach((s) => {
|
|
606
|
-
if (s.file
|
|
612
|
+
if (sameSession(s.file, state.currentSessionFile) && !hasCurrentInSessions) return;
|
|
607
613
|
const title = s.sessionName || s.firstUser || "新对话";
|
|
608
614
|
const when = s.timestamp ? new Date(s.timestamp).toLocaleString("zh-CN", { month: "numeric", day: "numeric", hour: "2-digit", minute: "2-digit" }) : "";
|
|
609
615
|
|
|
@@ -617,7 +623,7 @@ function renderSidebar(sessions) {
|
|
|
617
623
|
},
|
|
618
624
|
});
|
|
619
625
|
|
|
620
|
-
const isRunning = Boolean(s.isStreaming || (s.file
|
|
626
|
+
const isRunning = Boolean(s.isStreaming || (sameSession(s.file, state.currentSessionFile) && state.streaming));
|
|
621
627
|
const runningBadge = isRunning ? el("span", { class: "session-running-badge", title: "任务正在后台生成中…" }, [
|
|
622
628
|
el("span", { class: "spinner-dot" }),
|
|
623
629
|
el("span", { text: "运行中" }),
|
|
@@ -629,7 +635,7 @@ function renderSidebar(sessions) {
|
|
|
629
635
|
]);
|
|
630
636
|
|
|
631
637
|
const item = el("div", {
|
|
632
|
-
class: "session-item" + (s.file
|
|
638
|
+
class: "session-item" + (sameSession(s.file, state.currentSessionFile) ? " active" : ""),
|
|
633
639
|
dataset: { file: s.file },
|
|
634
640
|
title: s.file, // hover tooltip = raw jsonl path
|
|
635
641
|
onclick: () => loadSession(s.file),
|
|
@@ -690,13 +696,7 @@ function startNewSession() {
|
|
|
690
696
|
// Mobile: close sidebar on new session
|
|
691
697
|
if (window.innerWidth <= 768) closeSidebar();
|
|
692
698
|
|
|
693
|
-
|
|
694
|
-
if (state.ws && state.ws.readyState === 1) {
|
|
695
|
-
sendWs({ type: "new_session" });
|
|
696
|
-
sendWs({ type: "get_state" });
|
|
697
|
-
} else {
|
|
698
|
-
connectWs({ explicitNewSession: true }); // no session -> pi creates a new one
|
|
699
|
-
}
|
|
699
|
+
connectWs({ explicitNewSession: true }); // no session -> pi creates a new one
|
|
700
700
|
// Instantly render optimistic new session at the top of left sidebar
|
|
701
701
|
renderSidebar([]);
|
|
702
702
|
refreshSessions();
|
|
@@ -797,7 +797,7 @@ async function syncSessionHistory(file, force = false) {
|
|
|
797
797
|
}
|
|
798
798
|
|
|
799
799
|
async function loadSession(file) {
|
|
800
|
-
if (file
|
|
800
|
+
if (sameSession(file, state.currentSessionFile)) return;
|
|
801
801
|
const wasStreaming = state.streaming;
|
|
802
802
|
state.currentSessionFile = file;
|
|
803
803
|
state.streaming = false;
|
|
@@ -1804,7 +1804,7 @@ function sendWs(obj) {
|
|
|
1804
1804
|
function handlePiMessage(obj) {
|
|
1805
1805
|
// Automatically bind to the session file as soon as pi allocates it on disk
|
|
1806
1806
|
const sessionFile = obj.data?.sessionFile || obj.sessionFile || obj.data?.sessionPath || obj.sessionPath;
|
|
1807
|
-
if (sessionFile && sessionFile
|
|
1807
|
+
if (sessionFile && !sameSession(sessionFile, state.currentSessionFile)) {
|
|
1808
1808
|
state.currentSessionFile = sessionFile;
|
|
1809
1809
|
try {
|
|
1810
1810
|
const newUrl = window.location.pathname + "?session=" + encodeURIComponent(sessionFile);
|
|
@@ -1820,8 +1820,6 @@ function handlePiMessage(obj) {
|
|
|
1820
1820
|
// that happened in the background while no browser was attached.
|
|
1821
1821
|
if (obj.type === "backfill_start") {
|
|
1822
1822
|
state.isBackfilling = true;
|
|
1823
|
-
state.streaming = true;
|
|
1824
|
-
setComposerAborting(true);
|
|
1825
1823
|
state.streamingItems = [];
|
|
1826
1824
|
state.streamingMsg = null;
|
|
1827
1825
|
state.activeToolCalls.clear();
|
|
@@ -1838,11 +1836,15 @@ function handlePiMessage(obj) {
|
|
|
1838
1836
|
} else {
|
|
1839
1837
|
finalizeStreamingMsg();
|
|
1840
1838
|
state.streaming = false;
|
|
1839
|
+
state.aborting = false;
|
|
1841
1840
|
setComposerAborting(false);
|
|
1842
|
-
|
|
1841
|
+
if (state.currentSessionFile) {
|
|
1842
|
+
syncSessionHistory(state.currentSessionFile, true);
|
|
1843
|
+
}
|
|
1843
1844
|
}
|
|
1844
1845
|
// jump to the latest content once the replay is done
|
|
1845
1846
|
requestAnimationFrame(scrollBottom);
|
|
1847
|
+
refreshSessions();
|
|
1846
1848
|
return;
|
|
1847
1849
|
}
|
|
1848
1850
|
// Responses to commands we issued (get_state etc.) come back with success+data.
|
|
@@ -2149,16 +2151,12 @@ function updateState(d) {
|
|
|
2149
2151
|
setComposerAborting(true);
|
|
2150
2152
|
ensureStreamingMsg();
|
|
2151
2153
|
}
|
|
2152
|
-
} else if (state.streaming) {
|
|
2154
|
+
} else if (state.streaming && !state.isBackfilling) {
|
|
2153
2155
|
finalizeStreamingMsg();
|
|
2154
2156
|
state.streaming = false;
|
|
2155
2157
|
state.aborting = false;
|
|
2156
2158
|
setComposerAborting(false);
|
|
2157
|
-
|
|
2158
|
-
syncSessionHistory(state.currentSessionFile, true);
|
|
2159
|
-
} else {
|
|
2160
|
-
refreshSessions();
|
|
2161
|
-
}
|
|
2159
|
+
refreshSessions();
|
|
2162
2160
|
}
|
|
2163
2161
|
}
|
|
2164
2162
|
}
|