@liguoshuai/pi-web-chat 2.11.2 → 2.12.10

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/README.md CHANGED
@@ -9,7 +9,7 @@
9
9
  (`pi --mode rpc`) 与 pi 子进程通信,前端走 WebSocket 流式渲染。
10
10
 
11
11
  > 📦 **NPM 软件包**:[`@liguoshuai/pi-web-chat`](https://www.npmjs.com/package/@liguoshuai/pi-web-chat)
12
- > 🔗 **项目主页**:https://github.com/liguoshuai-1990/pi-web-chat
12
+ > 🔗 **项目主页**:https://github.com/liguoshuai-1990/pi-chat
13
13
 
14
14
  ---
15
15
 
@@ -132,8 +132,8 @@ npx @liguoshuai/pi-web-chat --port 8080 --cwd /path/to/your/project
132
132
  #### 方式 C:通过 Git 源码克隆与启动(适合二次开发)
133
133
 
134
134
  ```bash
135
- git clone https://github.com/liguoshuai-1990/pi-web-chat.git
136
- cd pi-web-chat
135
+ git clone https://github.com/liguoshuai-1990/pi-chat.git
136
+ cd pi-chat
137
137
  npm install
138
138
  npm start
139
139
  ```
@@ -181,11 +181,14 @@ Web 服务启动后,在浏览器中访问:
181
181
  | `PORT` | `3000` | Web 服务监听端口 |
182
182
  | `PI_BIN` | 自动探测(`~/.npm-global/bin/pi`、`/usr/local/bin/pi` 或 `PATH`) | 显式指定 pi 可执行文件绝对路径 |
183
183
  | `PI_SESSIONS_DIR` | `~/.pi/agent/sessions` | pi 的 session 存储目录 |
184
- | `IDLE_TIMEOUT_MS` | `300000` (5分钟) | 真正空闲(无连接+非流式)后的进程回收超时(0 为禁用回收) |
185
- | `MAX_AGENT_LIFETIME_MS` | `1800000` (30分钟) | 单个 Agent 进程后台生存硬上限(0 为无上限) |
186
- | `EVENT_BUFFER_SIZE` | `2000` | 离线环形 Buffer 允许缓存的最大事件条数 |
184
+ | `IDLE_TIMEOUT_MS` | `300000` (5分钟) | 真正空闲(无连接+非流式)后的进程回收超时(`0` 为禁用回收) |
185
+ | `MAX_AGENT_LIFETIME_MS` | `0` (无上限) | 单个 Agent 进程后台生存硬上限(`0` 为禁用) |
186
+ | `EVENT_BUFFER_SIZE` | `5000` | 离线环形 Buffer 允许缓存的最大事件条数 |
187
187
  | `MAX_CONCURRENT_AGENTS` | `0` (无限制) | 进程池最大并发 Agent 进程数量 |
188
188
  | `IDLE_DROP_HEAP` | `false` | 进入空闲时是否给 V8 引擎 GC 提示(需 `--expose-gc`) |
189
+ | `HOST` | `0.0.0.0` | 服务监听地址 |
190
+ | `AUTH_TOKEN` / `PI_AUTH_TOKEN` | 空 | 全局访问令牌,客户端需以 Bearer Token 鉴权(空为不鉴权) |
191
+ | `ALLOWED_ORIGINS` | 空 | 允许的 CORS/WebSocket 源白名单(逗号分隔,空为仅允许 localhost/同源) |
189
192
 
190
193
  ---
191
194
 
@@ -1,6 +1,6 @@
1
1
  # 架构设计文档
2
2
 
3
- > pi-web-chat 的技术架构、数据流、关键设计决策与扩展点说明(对应 v2.3.0 版本)。
3
+ > pi-web-chat 的技术架构、数据流、关键设计决策与扩展点说明(对应 v2.12.7 版本)。
4
4
 
5
5
  ---
6
6
 
@@ -147,11 +147,14 @@ Browser server.js pi RPC 子进程
147
147
  | `PORT` | `3000` | HTTP 与 WebSocket 监听端口 |
148
148
  | `PI_BIN` | 自动探测 | pi 可执行文件绝对路径 |
149
149
  | `PI_SESSIONS_DIR` | `~/.pi/agent/sessions` | 会话 JSONL 文件存取目录 |
150
- | `IDLE_TIMEOUT_MS` | `300000` (5分钟) | 真正空闲(无连接+非流式)后的回收超时(0 为禁用回收) |
151
- | `MAX_AGENT_LIFETIME_MS` | `1800000` (30分钟) | 单个 Agent 进程后台生存硬上限(0 为无上限) |
152
- | `EVENT_BUFFER_SIZE` | `2000` | 离线环形 Buffer 允许缓存的最大事件条数 |
150
+ | `IDLE_TIMEOUT_MS` | `300000` (5分钟) | 真正空闲(无连接+非流式)后的回收超时(`0` 为禁用回收) |
151
+ | `MAX_AGENT_LIFETIME_MS` | `0` (无上限) | 单个 Agent 进程后台生存硬上限(`0` 为禁用) |
152
+ | `EVENT_BUFFER_SIZE` | `5000` | 离线环形 Buffer 允许缓存的最大事件条数 |
153
153
  | `MAX_CONCURRENT_AGENTS` | `0` (无限制) | 进程池最大并发 Agent 进程数量 |
154
154
  | `IDLE_DROP_HEAP` | `false` | 进入空闲时是否给 V8 引擎 GC 提示(需 `--expose-gc`) |
155
+ | `HOST` | `0.0.0.0` | 服务监听地址 |
156
+ | `AUTH_TOKEN` / `PI_AUTH_TOKEN` | 空 | 全局访问令牌,客户端需以 Bearer Token 鉴权(空为不鉴权) |
157
+ | `ALLOWED_ORIGINS` | 空 | 允许的 CORS/WebSocket 源白名单(逗号分隔,空为仅允许 localhost/同源) |
155
158
 
156
159
  ---
157
160
 
package/docs/CHANGELOG.md CHANGED
@@ -5,8 +5,161 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+
9
+ ## [2.12.10] - 2026-09-04
10
+
11
+ ### Fixed
12
+ - **修复 GitHub Actions CI 的 Android 构建失败 (`clients/android/app/build.gradle.kts`)**:Kotlin DSL 脚本中 `java.util.Properties()` 因 `java` 标识符被 Java 插件扩展遮蔽而解析失败,导致 `./gradlew assembleDebug` 编译报错 `Unresolved reference: util`。改为脚本顶部显式 `import java.util.Properties` 并引用 `Properties()`,恢复 Android Debug APK 产物构建。
13
+
14
+ ### Changed
15
+ - 全端版本号统一递增至 2.12.10(Monorepo Lockstep:Root / Protocol / Server / Web / Android / HarmonyOS)。
16
+
17
+ ---
18
+ ---
19
+
20
+ ## [2.12.9] - 2026-09-04
21
+
22
+ ### Fixed
23
+ - **用户旅途文档修正与补充 (`docs/USER_JOURNEY.md`)**:基于 5 个代码探索子代理的完整分析结果,修正并补充文档内容:
24
+ - 修正痛点 #7:会话搜索功能仅 HarmonyOS 缺失(Web 和 Android 已有),原误标为"全端"问题。
25
+ - 新增痛点 #9-#13:HarmonyOS 端无 Token 输入 UI、错误信息不可见、无删除会话、无模型选择 UI;Android/HarmonyOS 端错误提示 UI 薄弱。
26
+ - 功能对齐矩阵补充 7 行:空状态引导芯片、键盘快捷键、消息发送失败重试、离开页面警告、图片 Lightbox、代码块复制按钮、错误提示方式。
27
+ - 三端交互差异对比补充 8 行:空状态引导、键盘快捷键、离开警告、发送失败重试、图片大图查看、明文 HTTP 限制、流式看门狗超时。
28
+ - 改进建议从 8 条扩展至 9 条,新增移动端错误提示增强建议。
29
+ - 文档末尾补充分析方法说明与子代理交叉验证过程。
30
+
31
+ ### Changed
32
+ - 全端版本号统一递增至 2.12.9(Monorepo Lockstep:Root / Protocol / Server / Web / Android / HarmonyOS)。
33
+
34
+ ---
35
+ ---
36
+
37
+ ## [2.12.8] - 2026-09-04
38
+
39
+ ### Added
40
+ - **用户旅途全景分析文档 (`docs/USER_JOURNEY.md`)**:新增面向终端使用者的 Customer Journey Map 文档,深度分析代码库后从用户视角梳理完整用户旅途,覆盖六大阶段(发现获取 → 配置连接 → 核心对话 → 会话管理 → 高级功能 → 异常恢复),包含产品定位与用户画像、系统拓扑与触点、三端功能对齐矩阵、交互模式差异对比、用户痛点与改进建议、协议消息全景附录。
41
+
42
+ ### Changed
43
+ - 全端版本号统一递增至 2.12.8(Monorepo Lockstep:Root / Protocol / Server / Web / Android / HarmonyOS)。
44
+
45
+ ---
46
+ ---
47
+
48
+ ## [2.12.7] - 2026-09-04
49
+
50
+ ### Fixed
51
+ - **全仓文档版本号漂移清理 (`README.md`, `clients/harmony/README.md`, `clients/android/README.md`)**:修复多处残留的旧版本号(`version-2.10.0` 徽章、HarmonyOS / Android 页首标题与工程目录注释中的 `v2.11.3`、`2.10.0`),统一对齐至当前版本 2.12.7。
52
+ - **Web 客户端仓库地址更新 (`clients/web/package.json`, `clients/web/README.md`)**:`repository` / `bugs` / `homepage` 及克隆地址由已废弃的 `pi-web-chat` 仓库更正为 Monorepo 主仓 `pi-chat`。
53
+ - **环境变量文档补齐与默认值修正 (`.env.example`, `server/.env.example`, `clients/web/README.md`, `clients/web/docs/ARCHITECTURE.md`)**:补充缺失的 `PI_AUTH_TOKEN`、`MAX_AGENT_LIFETIME_MS`、`EVENT_BUFFER_SIZE`、`IDLE_DROP_HEAP`、`HOST`、`ALLOWED_ORIGINS` 说明;修正 `MAX_AGENT_LIFETIME_MS`(默认 `0` 禁用)与 `EVENT_BUFFER_SIZE`(默认 `5000`)两处过时默认值。
54
+ - **Android 构建产物命名文档同步 (`README.md`, `clients/android/README.md`, `docs/ARCHITECTURE.md`)**:将过时的 `app-debug.apk` 引用更正为构建脚本实际产出的 `pi-chat-v{version}-debug.apk` 命名。
55
+
56
+ ### Changed
57
+ - 全端版本号统一递增至 2.12.7(Monorepo Lockstep:Root / Protocol / Server / Web / Android / HarmonyOS)。
58
+
8
59
  ---
9
60
 
61
+ ## [2.12.6] - 2026-09-04
62
+
63
+ ### Fixed
64
+ - **服务端代码深度扫描修复(`server/src/*`)**:
65
+ - `auth.js`:Token 校验改用 `crypto.timingSafeEqual` 恒定时间比较,消除计时侧信道攻击风险,并对非字符串输入做安全归一化。
66
+ - `ws.js`:修复 WebSocket 鉴权失败/容量拒绝时错误消息的 `id` 被错误放入 `details` 字段的问题,改为顶层 `id`,保证客户端可按请求 id 正确关联错误响应。
67
+ - `ws.js`:移除 `verifyClient` 中残留的空操作死代码(仅注释无逻辑的判断块)。
68
+ - `agent.js`:修复 `replayBufferedSse` 在非流式状态下回放后未清空事件缓冲区的问题(与 `replayBufferedWs` 行为对齐),避免旧事件向新连接的 SSE 客户端重复回放。
69
+ - `routes.js`:`/api/log-error` 端点新增字段长度截断(每字段上限 2048 字符),防止恶意/异常客户端写入超大内容刷屏服务端日志。
70
+
71
+ ---
72
+
73
+ ## [2.12.5] - 2026-09-04
74
+
75
+ ### Improved
76
+ - **CI NPM 发布支持动态 OTP 验证与 Automation Token 配置 (`.github/workflows/ci.yml`)**:
77
+ - 在 `workflow_dispatch` 触发器中新增可选的 `npm_otp` 参数,允许在缺少 Automation Token 时通过手动输入 2FA 一次性验证码完成 NPM 发布;
78
+ - 完善发布环境的 Token 传递与错误提示。
79
+
80
+ ### Changed
81
+ - 全端版本号统一递增至 2.12.5(Monorepo Lockstep:Root / Protocol / Server / Web / Android / HarmonyOS)。
82
+
83
+ ## [2.12.4] - 2026-09-04
84
+
85
+ ### Added
86
+ - **GitHub Actions CI 自动发布至 NPM Registry (`.github/workflows/ci.yml`)**:
87
+ - 新增 `publish-npm` 任务,集成 GitHub 仓库配置的 `NPM_TOKEN` Secret 与 `actions/setup-node` 注册表认证;
88
+ - 支持在推送版本 Tag(如 `v2.12.4`)或手动触发 `workflow_dispatch`(勾选 `publish_npm: true`)时,自动将全套 Monorepo 包(`@liguoshuai/pi-chat-protocol`、`@liguoshuai/pi-chat-server`、`@liguoshuai/pi-web-chat`)安全发布至 NPM Registry。
89
+
90
+ ### Changed
91
+ - 全端版本号统一递增至 2.12.4(Monorepo Lockstep:Root / Protocol / Server / Web / Android / HarmonyOS)。
92
+
93
+ ## [2.12.3] - 2026-09-04
94
+
95
+ ### Fixed
96
+ - **Android 端 Compose `Modifier.padding` 参数签名编译错误 (`clients/android/app/src/main/java/com/pichat/android/ui/screen/ChatScreen.kt`)**:修复输入栏容器 `Column` 中非法混用 `horizontal` 与 `top` / `bottom` 命名参数导致 Kotlin 编译报错(`None of the following candidates is applicable`)的问题,规范化为四向命名参数(`start`, `top`, `end`, `bottom`)。
97
+ - **GitHub Actions CI 升级 (`.github/workflows/ci.yml`)**:将 `actions/setup-java` 从 v4 升级至官方推荐的 v5。
98
+
99
+ ### Changed
100
+ - 全端版本号统一递增至 2.12.3(Monorepo Lockstep:Root / Protocol / Server / Web / Android / HarmonyOS)。
101
+
102
+ ## [2.12.2] - 2026-09-04
103
+
104
+ ### Fixed
105
+ - **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` 单独置位,彻底解决发送消息后对话框锁死、误显“中止…”的问题。
106
+
107
+ ### Changed
108
+ - 全端版本号统一递增至 2.12.2(Monorepo Lockstep:Root / Protocol / Server / Web / Android / HarmonyOS)。
109
+
110
+ ## [2.12.1] - 2026-09-04
111
+
112
+ ### Improved
113
+ - **耗时时长格式化进位与边界修复 (`packages/protocol`, `clients/web/public/app.js`)**:
114
+ - 重构 `formatDuration` 算法,避免毫秒数在接近整分边界(例如 `59950ms`、`119950ms`)时因浮点数取模四舍五入导致输出 `"0m 60s"` 或 `"1m 60s"` 异常格式,统一进位并补齐单元测试。
115
+ - **会话列表排序时间戳健壮性增强 (`server/src/routes.js`)**:
116
+ - 在 `/api/sessions` 排序中加入严格的 `Number.isFinite` 与 `Date.parse` 解析校验,防止无效或非标准日期字符串导致 `NaN` 比较破坏 JavaScript 引擎排序稳定性。
117
+ - **流式事件回放与请求 ID 边界保护 (`server/src/agent.js`)**:
118
+ - 优化 WebSocket / SSE 缓冲区回放遍历逻辑,精准依据缓冲区环形溢出状态重放事件,过滤潜在空槽位;
119
+ - 改进 `PiAgent.send` 在子进程异常终止时对于数字 `0` 作为请求 ID 的正确保留与响应返回。
120
+ - **Android 客户端协程生命周期防御 (`clients/android/.../ChatRepository.kt`)**:
121
+ - 仓库层使用 `SupervisorJob()` 配合 `Dispatchers.IO`,避免子协程失败导致整个仓库层作用域被级联取消。
122
+
123
+ ### Changed
124
+ - 全端版本号统一递增至 2.12.1(Monorepo Lockstep:Root / Protocol / Server / Web / Android / HarmonyOS)。
125
+
126
+ ## [2.12.0] - 2026-09-04
127
+
128
+ ### Added
129
+ - **Android 构建产物自定义重命名与 Release 加固配置 (`clients/android/app/build.gradle.kts`, `proguard-rules.pro`)**:
130
+ - 构建产物根据版本号和构建类型重命名为语义化规范格式:`pi-chat-v{versionName}-debug.apk` 与 `pi-chat-v{versionName}-release.apk`。
131
+ - 启用 Release 版本的混淆加固与代码资源缩减(`isMinifyEnabled = true`, `isShrinkResources = true`),配置完整的 ProGuard / R8 保护规则(数据模型、Kotlinx Serialization、OkHttp、Compose 与协程)。
132
+ - 配置默认签名回退支持,使 Release APK 可开箱即用构建。
133
+ - **Android 会话删除功能 (`clients/android/.../ApiService.kt`, `ChatRepository.kt`, `ChatViewModel.kt`, `ChatScreen.kt`)**:
134
+ - 新增 `DELETE /api/session?file=...` 网络请求与仓库层方法,并在侧边栏每个会话项右侧提供删除按钮及二次确认弹窗。
135
+
136
+ ### Improved
137
+ - **Android 侧边栏抽屉界面优化与对齐 Web 体验 (`clients/android/.../ChatScreen.kt`)**:
138
+ - 抽屉宽度适配调整为 `280dp`,改善手机端视觉比例;
139
+ - 会话项展示信息全面对齐 Web:新增格式化时间戳(如 `9月4日 15:30`)、消息条数展示(`· N 条`)与流式任务运行中微动徽标(`运行中`);
140
+ - 选中会话样式重构:高亮背景(`BgHover`)+ 左侧强调色指示条(`Accent`),会话行间距与内边距紧凑精致化;
141
+ - 搜索框高度重构为紧凑式 `34dp`,包含搜索图标与一键清空按钮;
142
+ - 侧边栏底部补齐 `pi.dev` 与 `pi-web-chat` 外部链接跳转,统一展示版本信息。
143
+ - **Android 顶栏排版与信息层级重构 (`clients/android/.../ChatScreen.kt`)**:
144
+ - 顶部导航栏高度规整至 `56dp`,居中展示当前对话主标题,并在下方紧凑排列工作目录(CWD)、模型名称(含 `★ 默认` 徽标)和思考推理等级胶囊;
145
+ - 左右两侧合理布局抽屉按钮、分享导出按钮及设置按钮,排版清晰美观不拥挤,彻底解决信息遮挡与丢失问题。
146
+ - **Android 底部输入框体验与样式重构 (`clients/android/.../ChatScreen.kt`)**:
147
+ - 输入框整体采用 Web 端 ChatGPT 风格圆角胶囊卡片(`RoundedCornerShape(22.dp)`),内边距与高度严格贴合文本行高;
148
+ - 采用 `BasicTextField` 配合优雅的占位符提示,支持多行平滑自适应扩展;
149
+ - 发送按钮、指导指令按钮及中止按钮尺寸规整(32dp 圆形/胶囊),并对齐底部安全区。
150
+
151
+ ### Changed
152
+ - 全端版本号统一递增至 2.12.0(Monorepo Lockstep:Root / Protocol / Server / Web / Android / HarmonyOS)。
153
+
154
+ ## [2.11.3] - 2026-09-03
155
+
156
+ ### Fixed
157
+ - **HarmonyOS 端配置持久化与初始加载缺失 (`clients/harmony/...`)**:`PreferencesHelper` 虽然实现了服务端 URL 的存储与获取,但未在 App 启动/初始化流程中实际调用。现已在 `Index.ets` 的 `aboutToAppear()` 中调用 `PreferencesHelper.getServerUrl()` 动态读取已保存的 URL 进行初始化,并在“保存并重连”时调用 `PreferencesHelper.setServerUrl()` 写入持久化存储,解决应用重启后配置丢失问题。
158
+ - **HarmonyOS 端 UI 界面版本号硬编码滞后 (`clients/harmony/.../Index.ets`)**:侧边栏抽屉底部硬编码展示旧版本号 `v2.10.1`,现已更新为当前全仓最新的 `v2.11.3`,与 Monorepo 版本数严格对齐。
159
+
160
+ ### Changed
161
+ - 全端版本号统一递增至 2.11.3(Monorepo Lockstep:Root / Protocol / Server / Web / Android / HarmonyOS)。
162
+
10
163
  ## [2.11.2] - 2026-09-03
11
164
 
12
165
  ### 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.10",
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",
@@ -26,12 +26,12 @@
26
26
  ],
27
27
  "repository": {
28
28
  "type": "git",
29
- "url": "git+https://github.com/liguoshuai-1990/pi-web-chat.git"
29
+ "url": "git+https://github.com/liguoshuai-1990/pi-chat.git"
30
30
  },
31
31
  "bugs": {
32
- "url": "https://github.com/liguoshuai-1990/pi-web-chat/issues"
32
+ "url": "https://github.com/liguoshuai-1990/pi-chat/issues"
33
33
  },
34
- "homepage": "https://github.com/liguoshuai-1990/pi-web-chat#readme",
34
+ "homepage": "https://github.com/liguoshuai-1990/pi-chat#readme",
35
35
  "license": "MIT",
36
36
  "publishConfig": {
37
37
  "access": "public"
@@ -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-protocol": "2.12.10",
46
+ "@liguoshuai/pi-chat-server": "2.12.10"
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;