@liguoshuai/pi-web-chat 2.11.2 → 2.12.2

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,6 +7,67 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ---
9
9
 
10
+ ## [2.12.2] - 2026-09-04
11
+
12
+ ### Fixed
13
+ - **Web 端发送消息后输入框锁死并误显“正在中止…” (`clients/web/public/app.js`)**:`submitPrompt` 发送成功后调用 `setComposerAborting(true)`(现已更名为 `setComposerStreaming`),其内部误将 `state.aborting` 置为 `true`,导致 `updateComposerUI` 进入“中止中”分支:输入框占位符显示“正在中止当前任务…”,发送按钮变成 `⏳` 并置为 `disabled`,UI 锁死直至任务结束。同时 `agent_start` / `backfill_end` / `updateState` 三处进入流式状态的路径也存在同样的误用。现将语义统一为 `setComposerStreaming`:进入流式状态时始终将 `state.aborting` 清理为 `false`,仅刷新 `updateComposerUI` 与 `renderModelPill`;而“中止”仅由用户点击中止时通过 `abortGeneration` 单独置位,彻底解决发送消息后对话框锁死、误显“中止…”的问题。
14
+
15
+ ### Changed
16
+ - 全端版本号统一递增至 2.12.2(Monorepo Lockstep:Root / Protocol / Server / Web / Android / HarmonyOS)。
17
+
18
+ ## [2.12.1] - 2026-09-04
19
+
20
+ ### Improved
21
+ - **耗时时长格式化进位与边界修复 (`packages/protocol`, `clients/web/public/app.js`)**:
22
+ - 重构 `formatDuration` 算法,避免毫秒数在接近整分边界(例如 `59950ms`、`119950ms`)时因浮点数取模四舍五入导致输出 `"0m 60s"` 或 `"1m 60s"` 异常格式,统一进位并补齐单元测试。
23
+ - **会话列表排序时间戳健壮性增强 (`server/src/routes.js`)**:
24
+ - 在 `/api/sessions` 排序中加入严格的 `Number.isFinite` 与 `Date.parse` 解析校验,防止无效或非标准日期字符串导致 `NaN` 比较破坏 JavaScript 引擎排序稳定性。
25
+ - **流式事件回放与请求 ID 边界保护 (`server/src/agent.js`)**:
26
+ - 优化 WebSocket / SSE 缓冲区回放遍历逻辑,精准依据缓冲区环形溢出状态重放事件,过滤潜在空槽位;
27
+ - 改进 `PiAgent.send` 在子进程异常终止时对于数字 `0` 作为请求 ID 的正确保留与响应返回。
28
+ - **Android 客户端协程生命周期防御 (`clients/android/.../ChatRepository.kt`)**:
29
+ - 仓库层使用 `SupervisorJob()` 配合 `Dispatchers.IO`,避免子协程失败导致整个仓库层作用域被级联取消。
30
+
31
+ ### Changed
32
+ - 全端版本号统一递增至 2.12.1(Monorepo Lockstep:Root / Protocol / Server / Web / Android / HarmonyOS)。
33
+
34
+ ## [2.12.0] - 2026-09-04
35
+
36
+ ### Added
37
+ - **Android 构建产物自定义重命名与 Release 加固配置 (`clients/android/app/build.gradle.kts`, `proguard-rules.pro`)**:
38
+ - 构建产物根据版本号和构建类型重命名为语义化规范格式:`pi-chat-v{versionName}-debug.apk` 与 `pi-chat-v{versionName}-release.apk`。
39
+ - 启用 Release 版本的混淆加固与代码资源缩减(`isMinifyEnabled = true`, `isShrinkResources = true`),配置完整的 ProGuard / R8 保护规则(数据模型、Kotlinx Serialization、OkHttp、Compose 与协程)。
40
+ - 配置默认签名回退支持,使 Release APK 可开箱即用构建。
41
+ - **Android 会话删除功能 (`clients/android/.../ApiService.kt`, `ChatRepository.kt`, `ChatViewModel.kt`, `ChatScreen.kt`)**:
42
+ - 新增 `DELETE /api/session?file=...` 网络请求与仓库层方法,并在侧边栏每个会话项右侧提供删除按钮及二次确认弹窗。
43
+
44
+ ### Improved
45
+ - **Android 侧边栏抽屉界面优化与对齐 Web 体验 (`clients/android/.../ChatScreen.kt`)**:
46
+ - 抽屉宽度适配调整为 `280dp`,改善手机端视觉比例;
47
+ - 会话项展示信息全面对齐 Web:新增格式化时间戳(如 `9月4日 15:30`)、消息条数展示(`· N 条`)与流式任务运行中微动徽标(`运行中`);
48
+ - 选中会话样式重构:高亮背景(`BgHover`)+ 左侧强调色指示条(`Accent`),会话行间距与内边距紧凑精致化;
49
+ - 搜索框高度重构为紧凑式 `34dp`,包含搜索图标与一键清空按钮;
50
+ - 侧边栏底部补齐 `pi.dev` 与 `pi-web-chat` 外部链接跳转,统一展示版本信息。
51
+ - **Android 顶栏排版与信息层级重构 (`clients/android/.../ChatScreen.kt`)**:
52
+ - 顶部导航栏高度规整至 `56dp`,居中展示当前对话主标题,并在下方紧凑排列工作目录(CWD)、模型名称(含 `★ 默认` 徽标)和思考推理等级胶囊;
53
+ - 左右两侧合理布局抽屉按钮、分享导出按钮及设置按钮,排版清晰美观不拥挤,彻底解决信息遮挡与丢失问题。
54
+ - **Android 底部输入框体验与样式重构 (`clients/android/.../ChatScreen.kt`)**:
55
+ - 输入框整体采用 Web 端 ChatGPT 风格圆角胶囊卡片(`RoundedCornerShape(22.dp)`),内边距与高度严格贴合文本行高;
56
+ - 采用 `BasicTextField` 配合优雅的占位符提示,支持多行平滑自适应扩展;
57
+ - 发送按钮、指导指令按钮及中止按钮尺寸规整(32dp 圆形/胶囊),并对齐底部安全区。
58
+
59
+ ### Changed
60
+ - 全端版本号统一递增至 2.12.0(Monorepo Lockstep:Root / Protocol / Server / Web / Android / HarmonyOS)。
61
+
62
+ ## [2.11.3] - 2026-09-03
63
+
64
+ ### Fixed
65
+ - **HarmonyOS 端配置持久化与初始加载缺失 (`clients/harmony/...`)**:`PreferencesHelper` 虽然实现了服务端 URL 的存储与获取,但未在 App 启动/初始化流程中实际调用。现已在 `Index.ets` 的 `aboutToAppear()` 中调用 `PreferencesHelper.getServerUrl()` 动态读取已保存的 URL 进行初始化,并在“保存并重连”时调用 `PreferencesHelper.setServerUrl()` 写入持久化存储,解决应用重启后配置丢失问题。
66
+ - **HarmonyOS 端 UI 界面版本号硬编码滞后 (`clients/harmony/.../Index.ets`)**:侧边栏抽屉底部硬编码展示旧版本号 `v2.10.1`,现已更新为当前全仓最新的 `v2.11.3`,与 Monorepo 版本数严格对齐。
67
+
68
+ ### Changed
69
+ - 全端版本号统一递增至 2.11.3(Monorepo Lockstep:Root / Protocol / Server / Web / Android / HarmonyOS)。
70
+
10
71
  ## [2.11.2] - 2026-09-03
11
72
 
12
73
  ### Fixed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@liguoshuai/pi-web-chat",
3
- "version": "2.11.2",
3
+ "version": "2.12.2",
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",
@@ -42,8 +42,8 @@
42
42
  "dependencies": {
43
43
  "express": "^4.21.2",
44
44
  "ws": "^8.18.0",
45
- "@liguoshuai/pi-chat-server": "2.11.2",
46
- "@liguoshuai/pi-chat-protocol": "2.11.2"
45
+ "@liguoshuai/pi-chat-server": "2.12.2",
46
+ "@liguoshuai/pi-chat-protocol": "2.12.2"
47
47
  },
48
48
  "scripts": {
49
49
  "build": "node --check server.js && node --check bin/pi-web-chat.js && node --check public/app.js",
package/public/app.js CHANGED
@@ -560,14 +560,12 @@ function formatMessageTime(ts) {
560
560
  // ---- Duration Formatter ----
561
561
  function formatDuration(ms) {
562
562
  if (ms == null || isNaN(ms) || ms < 0) return "";
563
- if (ms < 1000) {
563
+ const totalSecs = Math.round(ms / 1000);
564
+ if (totalSecs < 60) {
564
565
  return `${(ms / 1000).toFixed(1)}s`;
565
566
  }
566
- if (ms < 60000) {
567
- return `${(ms / 1000).toFixed(1)}s`;
568
- }
569
- const mins = Math.floor(ms / 60000);
570
- const secs = Math.round((ms % 60000) / 1000);
567
+ const mins = Math.floor(totalSecs / 60);
568
+ const secs = totalSecs % 60;
571
569
  return `${mins}m ${secs}s`;
572
570
  }
573
571
 
@@ -741,7 +739,7 @@ function startNewSession() {
741
739
  state.streamingItems = [];
742
740
  state.streamingMsg = null;
743
741
  state.activeToolCalls.clear();
744
- setComposerAborting(false);
742
+ setComposerStreaming(false);
745
743
  try {
746
744
  window.history.replaceState({}, "", window.location.pathname);
747
745
  } catch {}
@@ -861,7 +859,7 @@ async function loadSession(file) {
861
859
  state.streamingItems = [];
862
860
  state.streamingMsg = null;
863
861
  state.activeToolCalls.clear();
864
- setComposerAborting(false);
862
+ setComposerStreaming(false);
865
863
  try {
866
864
  const newUrl = window.location.pathname + "?session=" + encodeURIComponent(file);
867
865
  window.history.replaceState({ session: file }, "", newUrl);
@@ -1981,7 +1979,7 @@ function handlePiMessage(obj) {
1981
1979
  // After replay, sync the composer / streaming state to what the server thinks.
1982
1980
  if (obj.streaming) {
1983
1981
  state.streaming = true;
1984
- setComposerAborting(true);
1982
+ setComposerStreaming(true);
1985
1983
  ensureStreamingMsg();
1986
1984
  refreshStreamingContent();
1987
1985
  // If ring buffer overflowed while client was away, sync full session history in background
@@ -1992,7 +1990,7 @@ function handlePiMessage(obj) {
1992
1990
  finalizeStreamingMsg();
1993
1991
  state.streaming = false;
1994
1992
  state.aborting = false;
1995
- setComposerAborting(false);
1993
+ setComposerStreaming(false);
1996
1994
  if (state.currentSessionFile) {
1997
1995
  syncSessionHistory(state.currentSessionFile, true);
1998
1996
  }
@@ -2040,7 +2038,7 @@ function handlePiMessage(obj) {
2040
2038
  }
2041
2039
  } else if (obj.command === "abort") {
2042
2040
  state.aborting = false;
2043
- setComposerAborting(false);
2041
+ setComposerStreaming(false);
2044
2042
  }
2045
2043
  else if (obj.command === "switch_session" && obj.success) {
2046
2044
  // ask pi for current state so we can get session id, name
@@ -2076,7 +2074,7 @@ function handlePiMessage(obj) {
2076
2074
  case "agent_start":
2077
2075
  state.streaming = true;
2078
2076
  state.aborting = false;
2079
- setComposerAborting(true);
2077
+ setComposerStreaming(true);
2080
2078
  ensureStreamingMsg();
2081
2079
  refreshStreamingContent();
2082
2080
  refreshSessions();
@@ -2088,7 +2086,7 @@ function handlePiMessage(obj) {
2088
2086
  finalizeStreamingMsg();
2089
2087
  state.streaming = false;
2090
2088
  state.aborting = false;
2091
- setComposerAborting(false);
2089
+ setComposerStreaming(false);
2092
2090
  sendWs({ type: "get_state" });
2093
2091
  refreshSessions(); // titles may have changed
2094
2092
  break;
@@ -2278,14 +2276,14 @@ function handlePiMessage(obj) {
2278
2276
  finalizeStreamingMsg();
2279
2277
  state.streaming = false;
2280
2278
  state.aborting = false;
2281
- setComposerAborting(false);
2279
+ setComposerStreaming(false);
2282
2280
  showToast(`⚠️ ${obj.message || obj.code || "发生错误"}`);
2283
2281
  break;
2284
2282
  case "pi_exit":
2285
2283
  finalizeStreamingMsg();
2286
2284
  state.streaming = false;
2287
2285
  state.aborting = false;
2288
- setComposerAborting(false);
2286
+ setComposerStreaming(false);
2289
2287
  $("#connDot").style.color = "var(--danger)";
2290
2288
  break;
2291
2289
  default:
@@ -2374,14 +2372,14 @@ function updateState(d) {
2374
2372
  if (d.isStreaming) {
2375
2373
  if (!state.streaming) {
2376
2374
  state.streaming = true;
2377
- setComposerAborting(true);
2375
+ setComposerStreaming(true);
2378
2376
  ensureStreamingMsg();
2379
2377
  }
2380
2378
  } else if (state.streaming && !state.isBackfilling) {
2381
2379
  finalizeStreamingMsg();
2382
2380
  state.streaming = false;
2383
2381
  state.aborting = false;
2384
- setComposerAborting(false);
2382
+ setComposerStreaming(false);
2385
2383
  refreshSessions();
2386
2384
  }
2387
2385
  }
@@ -2941,11 +2939,12 @@ function updateComposerUI() {
2941
2939
  }
2942
2940
  }
2943
2941
 
2944
- function setComposerAborting(yes) {
2945
- if (yes) {
2946
- state.aborting = true;
2947
- } else {
2948
- state.aborting = false;
2942
+ function setComposerStreaming(yes) {
2943
+ // Entering/exiting the streaming state must never turn on the "aborting"
2944
+ // flag — otherwise the composer would lock and show "正在中止…" while the
2945
+ // model is actually just running. Abort is a separate transient state.
2946
+ state.aborting = false;
2947
+ if (!yes) {
2949
2948
  const inner = $("#composerInner");
2950
2949
  if (inner) inner.classList.remove("aborting");
2951
2950
  const hint = $(".composer-hint");
@@ -3050,7 +3049,7 @@ function submitPrompt() {
3050
3049
 
3051
3050
  state.streaming = true;
3052
3051
  state.aborting = false;
3053
- setComposerAborting(true);
3052
+ setComposerStreaming(true);
3054
3053
  ensureStreamingMsg(now);
3055
3054
  refreshStreamingContent();
3056
3055
 
@@ -3083,7 +3082,7 @@ function submitPrompt() {
3083
3082
  if (!sendWs({ type: "prompt", message: text, images: imagesToSend })) {
3084
3083
  state.streaming = false;
3085
3084
  state.aborting = false;
3086
- setComposerAborting(false);
3085
+ setComposerStreaming(false);
3087
3086
  stopStreamingTimer();
3088
3087
  state.turnStartedAt = null;
3089
3088
  state.streamingMsgDurationEl = null;