@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 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.3.0] - 2026-08-31
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.4.0",
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.4.0",
44
- "@liguoshuai/pi-chat-server": "2.4.0",
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 === state.currentSessionFile));
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 === state.currentSessionFile && !hasCurrentInSessions) return;
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 === state.currentSessionFile && state.streaming));
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 === state.currentSessionFile ? " active" : ""),
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
- // If WebSocket is already live, request new session directly without reconnect overhead
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 === state.currentSessionFile) return;
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 !== state.currentSessionFile) {
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
- refreshSessions();
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
- if (state.currentSessionFile) {
2158
- syncSessionHistory(state.currentSessionFile, true);
2159
- } else {
2160
- refreshSessions();
2161
- }
2159
+ refreshSessions();
2162
2160
  }
2163
2161
  }
2164
2162
  }