@liguoshuai/pi-web-chat 1.8.4 → 1.8.5
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/ARCHITECTURE.md +1 -1
- package/docs/CHANGELOG.md +13 -0
- package/package.json +1 -1
- package/public/app.js +76 -20
- package/public/style.css +7 -2
package/docs/ARCHITECTURE.md
CHANGED
package/docs/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
---
|
|
9
9
|
|
|
10
|
+
## [1.8.5] - 2026-08-23
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- **中止/终止生成按钮逻辑修复 (Abort Button Logic & UX Fix)**:
|
|
14
|
+
- 修复生成过程中点击终止按钮(`■`)时,因输入框为空触发提前返回(`if (!text) return`)导致无法发出 `abort` 信号的问题。
|
|
15
|
+
- 抽离独立的 `abortGeneration()` 统一处理中断生成流程,确保点击停止按钮无条件触发任务终止。
|
|
16
|
+
- 修复输入框有草稿时点击停止按钮误触发 `steer`(插入指令)的问题,明确区分停止按钮与插入指令按钮职责。
|
|
17
|
+
- 新增中止中即时视觉反馈(按钮变为等待状态 `⏳`,输入框边缘高亮变红,提示文字变更为“中止当前任务中…”),并在底层 `agent_settled` 或 `abort` 响应后安全恢复。
|
|
18
|
+
- 支持在生成过程中按 `Escape` 键快速中止当前任务(模态框或菜单打开时除外)。
|
|
19
|
+
- 在新建会话及切换会话时,若当前任务正在运行中,自动先行中断当前流式任务以避免后台状态错乱。
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
10
23
|
## [1.8.4] - 2026-07-26
|
|
11
24
|
|
|
12
25
|
### Added
|
package/package.json
CHANGED
package/public/app.js
CHANGED
|
@@ -42,6 +42,7 @@ const state = {
|
|
|
42
42
|
thinkingLevel: "medium",
|
|
43
43
|
sessionId: null,
|
|
44
44
|
isBackfilling: false,
|
|
45
|
+
aborting: false,
|
|
45
46
|
};
|
|
46
47
|
|
|
47
48
|
let toastTimer = null;
|
|
@@ -576,8 +577,12 @@ async function syncSessionHistory(file, force = false) {
|
|
|
576
577
|
}
|
|
577
578
|
|
|
578
579
|
async function loadSession(file) {
|
|
580
|
+
if (state.streaming) {
|
|
581
|
+
abortGeneration();
|
|
582
|
+
}
|
|
579
583
|
state.currentSessionFile = file;
|
|
580
584
|
state.streaming = false;
|
|
585
|
+
state.aborting = false;
|
|
581
586
|
state.streamingItems = [];
|
|
582
587
|
state.streamingMsg = null;
|
|
583
588
|
state.activeToolCalls.clear();
|
|
@@ -1295,6 +1300,9 @@ function handlePiMessage(obj) {
|
|
|
1295
1300
|
renderThinkingPill();
|
|
1296
1301
|
updateEmptyStateModelInfo();
|
|
1297
1302
|
}
|
|
1303
|
+
} else if (obj.command === "abort") {
|
|
1304
|
+
state.aborting = false;
|
|
1305
|
+
setComposerAborting(false);
|
|
1298
1306
|
}
|
|
1299
1307
|
else if (obj.command === "switch_session" && obj.success) {
|
|
1300
1308
|
// ask pi for current state so we can get session id, name
|
|
@@ -1329,6 +1337,7 @@ function handlePiMessage(obj) {
|
|
|
1329
1337
|
break;
|
|
1330
1338
|
case "agent_start":
|
|
1331
1339
|
state.streaming = true;
|
|
1340
|
+
state.aborting = false;
|
|
1332
1341
|
setComposerAborting(true);
|
|
1333
1342
|
ensureStreamingMsg();
|
|
1334
1343
|
refreshStreamingContent();
|
|
@@ -1340,6 +1349,7 @@ function handlePiMessage(obj) {
|
|
|
1340
1349
|
case "agent_settled":
|
|
1341
1350
|
finalizeStreamingMsg();
|
|
1342
1351
|
state.streaming = false;
|
|
1352
|
+
state.aborting = false;
|
|
1343
1353
|
setComposerAborting(false);
|
|
1344
1354
|
sendWs({ type: "get_state" });
|
|
1345
1355
|
refreshSessions(); // titles may have changed
|
|
@@ -1465,6 +1475,7 @@ function handlePiMessage(obj) {
|
|
|
1465
1475
|
case "pi_exit":
|
|
1466
1476
|
finalizeStreamingMsg();
|
|
1467
1477
|
state.streaming = false;
|
|
1478
|
+
state.aborting = false;
|
|
1468
1479
|
setComposerAborting(false);
|
|
1469
1480
|
$("#connDot").style.color = "var(--danger)";
|
|
1470
1481
|
break;
|
|
@@ -1560,6 +1571,7 @@ function updateState(d) {
|
|
|
1560
1571
|
} else if (state.streaming) {
|
|
1561
1572
|
finalizeStreamingMsg();
|
|
1562
1573
|
state.streaming = false;
|
|
1574
|
+
state.aborting = false;
|
|
1563
1575
|
setComposerAborting(false);
|
|
1564
1576
|
if (state.currentSessionFile) {
|
|
1565
1577
|
syncSessionHistory(state.currentSessionFile, true);
|
|
@@ -2064,8 +2076,24 @@ function updateComposerUI() {
|
|
|
2064
2076
|
const text = ta ? ta.value.trim() : "";
|
|
2065
2077
|
const sendBtn = $("#sendBtn");
|
|
2066
2078
|
const steerBtn = $("#steerBtn");
|
|
2079
|
+
const inner = $("#composerInner");
|
|
2067
2080
|
|
|
2068
|
-
if (
|
|
2081
|
+
if (inner) {
|
|
2082
|
+
inner.classList.toggle("aborting", !!state.aborting);
|
|
2083
|
+
}
|
|
2084
|
+
|
|
2085
|
+
if (state.aborting) {
|
|
2086
|
+
if (sendBtn) {
|
|
2087
|
+
sendBtn.classList.add("stop");
|
|
2088
|
+
sendBtn.disabled = true;
|
|
2089
|
+
sendBtn.textContent = "⏳";
|
|
2090
|
+
sendBtn.title = "中止中…";
|
|
2091
|
+
}
|
|
2092
|
+
if (steerBtn) {
|
|
2093
|
+
steerBtn.style.display = "none";
|
|
2094
|
+
}
|
|
2095
|
+
if (ta) ta.placeholder = "正在中止当前任务…";
|
|
2096
|
+
} else if (state.streaming) {
|
|
2069
2097
|
if (sendBtn) {
|
|
2070
2098
|
sendBtn.classList.add("stop");
|
|
2071
2099
|
sendBtn.disabled = !state.wsConnected;
|
|
@@ -2096,10 +2124,34 @@ function updateComposerUI() {
|
|
|
2096
2124
|
}
|
|
2097
2125
|
|
|
2098
2126
|
function setComposerAborting(yes) {
|
|
2127
|
+
if (!yes) {
|
|
2128
|
+
state.aborting = false;
|
|
2129
|
+
const inner = $("#composerInner");
|
|
2130
|
+
if (inner) inner.classList.remove("aborting");
|
|
2131
|
+
const hint = $(".composer-hint");
|
|
2132
|
+
if (hint && hint.textContent.includes("中止")) {
|
|
2133
|
+
hint.textContent = "pi 会执行命令与读写你的文件 —— 请注意操作内容。";
|
|
2134
|
+
}
|
|
2135
|
+
}
|
|
2099
2136
|
updateComposerUI();
|
|
2100
2137
|
renderModelPill();
|
|
2101
2138
|
}
|
|
2102
2139
|
|
|
2140
|
+
function abortGeneration() {
|
|
2141
|
+
if (!state.streaming && !state.aborting) return;
|
|
2142
|
+
if (!state.wsConnected) {
|
|
2143
|
+
const hint = $(".composer-hint");
|
|
2144
|
+
if (hint) hint.textContent = "操作失败:WebSocket 未连接。正在尝试重连…";
|
|
2145
|
+
scheduleReconnect(0);
|
|
2146
|
+
return;
|
|
2147
|
+
}
|
|
2148
|
+
state.aborting = true;
|
|
2149
|
+
updateComposerUI();
|
|
2150
|
+
const hint = $(".composer-hint");
|
|
2151
|
+
if (hint) hint.textContent = "中止当前任务中…";
|
|
2152
|
+
sendWs({ type: "abort" });
|
|
2153
|
+
}
|
|
2154
|
+
|
|
2103
2155
|
function submitSteer() {
|
|
2104
2156
|
const ta = $("#composer");
|
|
2105
2157
|
const text = ta.value.trim();
|
|
@@ -2126,8 +2178,19 @@ function submitSteer() {
|
|
|
2126
2178
|
}
|
|
2127
2179
|
|
|
2128
2180
|
function submitPrompt() {
|
|
2181
|
+
if (state.streaming) {
|
|
2182
|
+
const ta = $("#composer");
|
|
2183
|
+
const text = ta ? ta.value.trim() : "";
|
|
2184
|
+
if (text) {
|
|
2185
|
+
submitSteer();
|
|
2186
|
+
} else {
|
|
2187
|
+
abortGeneration();
|
|
2188
|
+
}
|
|
2189
|
+
return;
|
|
2190
|
+
}
|
|
2191
|
+
|
|
2129
2192
|
const ta = $("#composer");
|
|
2130
|
-
const text = ta.value.trim();
|
|
2193
|
+
const text = ta ? ta.value.trim() : "";
|
|
2131
2194
|
const hint = $(".composer-hint");
|
|
2132
2195
|
if (!text) return;
|
|
2133
2196
|
if (!state.wsConnected) {
|
|
@@ -2137,16 +2200,7 @@ function submitPrompt() {
|
|
|
2137
2200
|
if (box) { box.style.boxShadow = "0 0 0 2px var(--danger)"; setTimeout(() => { box.style.boxShadow = ""; }, 350); }
|
|
2138
2201
|
return;
|
|
2139
2202
|
}
|
|
2140
|
-
|
|
2141
|
-
if (text) {
|
|
2142
|
-
submitSteer();
|
|
2143
|
-
return;
|
|
2144
|
-
}
|
|
2145
|
-
if (hint) hint.textContent = "中止当前生成中…";
|
|
2146
|
-
sendWs({ type: "abort" });
|
|
2147
|
-
return;
|
|
2148
|
-
}
|
|
2149
|
-
|
|
2203
|
+
|
|
2150
2204
|
if (hint) hint.textContent = "pi 会执行命令与读写你的文件 —— 请注意操作内容。"; // restore default
|
|
2151
2205
|
// Render the user's message locally for instant feedback.
|
|
2152
2206
|
appendMessageNode("user", { text });
|
|
@@ -2154,6 +2208,7 @@ function submitPrompt() {
|
|
|
2154
2208
|
autoResize();
|
|
2155
2209
|
|
|
2156
2210
|
state.streaming = true;
|
|
2211
|
+
state.aborting = false;
|
|
2157
2212
|
setComposerAborting(true);
|
|
2158
2213
|
ensureStreamingMsg();
|
|
2159
2214
|
refreshStreamingContent();
|
|
@@ -2183,7 +2238,7 @@ async function init() {
|
|
|
2183
2238
|
$("#btnNew").addEventListener("click", () => {
|
|
2184
2239
|
if (state.streaming) {
|
|
2185
2240
|
if (!confirm("正在生成中,新建会话会终止当前操作,确定吗?")) return;
|
|
2186
|
-
|
|
2241
|
+
abortGeneration();
|
|
2187
2242
|
}
|
|
2188
2243
|
clearChat();
|
|
2189
2244
|
showEmptyState(true);
|
|
@@ -2200,12 +2255,7 @@ async function init() {
|
|
|
2200
2255
|
|
|
2201
2256
|
$("#sendBtn").addEventListener("click", () => {
|
|
2202
2257
|
if (state.streaming) {
|
|
2203
|
-
|
|
2204
|
-
if (ta && ta.value.trim()) {
|
|
2205
|
-
submitSteer();
|
|
2206
|
-
} else {
|
|
2207
|
-
submitPrompt();
|
|
2208
|
-
}
|
|
2258
|
+
abortGeneration();
|
|
2209
2259
|
} else {
|
|
2210
2260
|
submitPrompt();
|
|
2211
2261
|
}
|
|
@@ -2259,11 +2309,17 @@ async function init() {
|
|
|
2259
2309
|
});
|
|
2260
2310
|
}
|
|
2261
2311
|
|
|
2262
|
-
// Keyboard shortcut: Ctrl+M / Cmd+M to toggle model selector
|
|
2312
|
+
// Keyboard shortcut: Ctrl+M / Cmd+M to toggle model selector, Escape to abort if streaming
|
|
2263
2313
|
window.addEventListener("keydown", (e) => {
|
|
2264
2314
|
if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "m") {
|
|
2265
2315
|
e.preventDefault();
|
|
2266
2316
|
toggleModelMenu();
|
|
2317
|
+
} else if (e.key === "Escape" && state.streaming) {
|
|
2318
|
+
const isMenuOpen = $("#modelMenu")?.classList.contains("open") || $("#thinkingMenu")?.classList.contains("open");
|
|
2319
|
+
const isModalOpen = $("#cwdModal")?.style.display === "flex";
|
|
2320
|
+
if (!isMenuOpen && !isModalOpen) {
|
|
2321
|
+
abortGeneration();
|
|
2322
|
+
}
|
|
2267
2323
|
}
|
|
2268
2324
|
});
|
|
2269
2325
|
|
package/public/style.css
CHANGED
|
@@ -708,7 +708,10 @@ body {
|
|
|
708
708
|
transition: border-color .15s;
|
|
709
709
|
}
|
|
710
710
|
.composer-inner:focus-within { border-color: var(--text-dim); }
|
|
711
|
-
.composer-inner.aborting {
|
|
711
|
+
.composer-inner.aborting {
|
|
712
|
+
border-color: var(--danger);
|
|
713
|
+
box-shadow: 0 0 0 1px var(--danger);
|
|
714
|
+
}
|
|
712
715
|
.composer textarea {
|
|
713
716
|
flex: 1; background: transparent; border: none; outline: none; resize: none;
|
|
714
717
|
color: var(--text); font-family: inherit; font-size: 15px; line-height: 1.5;
|
|
@@ -717,10 +720,12 @@ body {
|
|
|
717
720
|
.composer .send-btn {
|
|
718
721
|
width: 36px; height: 32px; border-radius: 18px; border: none; cursor: pointer;
|
|
719
722
|
background: var(--accent); color: #fff; display: flex; align-items: center; justify-content: center;
|
|
720
|
-
flex-shrink: 0; transition: background .15s, opacity .15s;
|
|
723
|
+
flex-shrink: 0; transition: background .15s, opacity .15s, transform .1s;
|
|
721
724
|
}
|
|
722
725
|
.composer .send-btn:disabled { background: #3a3a3a; color: #6f6f6f; cursor: not-allowed; }
|
|
723
726
|
.composer .send-btn.stop { background: var(--danger); }
|
|
727
|
+
.composer .send-btn.stop:hover:not(:disabled) { background: #f87171; }
|
|
728
|
+
.composer .send-btn.stop:active:not(:disabled) { transform: scale(0.95); }
|
|
724
729
|
.composer .steer-btn {
|
|
725
730
|
display: inline-flex;
|
|
726
731
|
align-items: center;
|