@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 +61 -0
- package/package.json +3 -3
- package/public/app.js +23 -24
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.
|
|
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.
|
|
46
|
-
"@liguoshuai/pi-chat-protocol": "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
|
-
|
|
563
|
+
const totalSecs = Math.round(ms / 1000);
|
|
564
|
+
if (totalSecs < 60) {
|
|
564
565
|
return `${(ms / 1000).toFixed(1)}s`;
|
|
565
566
|
}
|
|
566
|
-
|
|
567
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2382
|
+
setComposerStreaming(false);
|
|
2385
2383
|
refreshSessions();
|
|
2386
2384
|
}
|
|
2387
2385
|
}
|
|
@@ -2941,11 +2939,12 @@ function updateComposerUI() {
|
|
|
2941
2939
|
}
|
|
2942
2940
|
}
|
|
2943
2941
|
|
|
2944
|
-
function
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3085
|
+
setComposerStreaming(false);
|
|
3087
3086
|
stopStreamingTimer();
|
|
3088
3087
|
state.turnStartedAt = null;
|
|
3089
3088
|
state.streamingMsgDurationEl = null;
|