@co0ontty/wand 1.22.0 → 1.24.0
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/dist/cli.js +111 -2
- package/dist/pidfile.d.ts +38 -0
- package/dist/pidfile.js +117 -0
- package/dist/tui/attach.d.ts +18 -0
- package/dist/tui/attach.js +306 -0
- package/dist/tui/commands.d.ts +60 -0
- package/dist/tui/commands.js +505 -0
- package/dist/tui/index.js +171 -3
- package/dist/tui/ipc-client.d.ts +27 -0
- package/dist/tui/ipc-client.js +153 -0
- package/dist/tui/ipc-protocol.d.ts +50 -0
- package/dist/tui/ipc-protocol.js +7 -0
- package/dist/tui/ipc-server.d.ts +17 -0
- package/dist/tui/ipc-server.js +100 -0
- package/dist/tui/layout.d.ts +44 -0
- package/dist/tui/layout.js +365 -11
- package/dist/tui/service-panel.d.ts +19 -0
- package/dist/tui/service-panel.js +108 -0
- package/dist/tui/snapshot.d.ts +26 -0
- package/dist/tui/snapshot.js +58 -0
- package/dist/web-ui/content/scripts.js +253 -125
- package/dist/web-ui/content/styles.css +253 -141
- package/package.json +1 -1
|
@@ -59,7 +59,6 @@
|
|
|
59
59
|
(function() {
|
|
60
60
|
var configPath = "${escapeHtml(configPath)}";
|
|
61
61
|
var CHAT_EXPAND_STATE_STORAGE_KEY = "wand-chat-expand-state-v1";
|
|
62
|
-
var CHAT_AUTO_FOLLOW_STORAGE_KEY = "wand-chat-auto-follow";
|
|
63
62
|
|
|
64
63
|
var state = {
|
|
65
64
|
selectedId: (function() {
|
|
@@ -183,16 +182,13 @@
|
|
|
183
182
|
quickCommitGenerating: false,
|
|
184
183
|
quickCommitError: "",
|
|
185
184
|
quickCommitForm: { autoMessage: false, customMessage: "", makeTag: false, tag: "", push: false },
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
})(),
|
|
194
|
-
showChatJumpToBottom: false,
|
|
195
|
-
chatScrollThreshold: 200,
|
|
185
|
+
// Telegram 风格的"贴底"状态:true = 用户当前贴在底部,新消息会自然出现;
|
|
186
|
+
// false = 用户向上滚了,未读会累积到气泡里,不会自动滚他们的视图。
|
|
187
|
+
chatStickToBottom: true,
|
|
188
|
+
chatUnreadCount: 0,
|
|
189
|
+
// state.currentMessages 中第一条未读消息的 index,-1 表示没有未读。
|
|
190
|
+
chatUnreadStartIndex: -1,
|
|
191
|
+
chatScrollThreshold: 120,
|
|
196
192
|
chatIsProgrammaticScroll: false,
|
|
197
193
|
chatScrollElement: null,
|
|
198
194
|
chatScrollHandler: null,
|
|
@@ -406,14 +402,6 @@
|
|
|
406
402
|
}
|
|
407
403
|
}
|
|
408
404
|
|
|
409
|
-
function persistChatAutoFollow() {
|
|
410
|
-
try {
|
|
411
|
-
localStorage.setItem(CHAT_AUTO_FOLLOW_STORAGE_KEY, state.chatAutoFollow ? "true" : "false");
|
|
412
|
-
} catch (e) {
|
|
413
|
-
// Ignore localStorage errors
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
|
|
417
405
|
function getChatScrollElement() {
|
|
418
406
|
var chatOutput = document.getElementById("chat-output");
|
|
419
407
|
if (!chatOutput) {
|
|
@@ -429,36 +417,106 @@
|
|
|
429
417
|
return null;
|
|
430
418
|
}
|
|
431
419
|
|
|
420
|
+
// column-reverse: scrollTop=0 是视觉底部,越往上看 scrollTop 绝对值越大。
|
|
421
|
+
// 部分浏览器历史上在 column-reverse 里给负 scrollTop,所以用绝对值更稳。
|
|
432
422
|
function isChatNearBottom(chatMsgs) {
|
|
433
423
|
var el = chatMsgs || getChatScrollElement();
|
|
434
424
|
if (!el) return true;
|
|
435
|
-
return el.scrollTop < state.chatScrollThreshold;
|
|
425
|
+
return Math.abs(el.scrollTop) < state.chatScrollThreshold;
|
|
436
426
|
}
|
|
437
427
|
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
button.setAttribute("title", enabled ? "追踪底部:开启(点击暂停)" : "追踪底部:已暂停(点击开启)");
|
|
445
|
-
button.setAttribute("aria-label", enabled ? "追踪底部:开启" : "追踪底部:已暂停");
|
|
446
|
-
button.innerHTML = enabled
|
|
447
|
-
? '<svg viewBox="0 0 16 16" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3.5 2.5l4.5 4.5 4.5-4.5"/><path d="M3.5 8.5l4.5 4.5 4.5-4.5"/></svg>'
|
|
448
|
-
: '<svg viewBox="0 0 16 16" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5.5 3v10"/><path d="M10.5 3v10"/></svg>';
|
|
428
|
+
// 没有手动 toggle 了——是否贴底完全由用户的滚动行为决定。
|
|
429
|
+
// 这个函数只用来在某些场景(点未读气泡)下显式把状态扳回 true。
|
|
430
|
+
function setChatStickToBottom(enabled) {
|
|
431
|
+
state.chatStickToBottom = !!enabled;
|
|
432
|
+
if (state.chatStickToBottom) clearChatUnread({ removeDivider: true });
|
|
433
|
+
updateChatUnreadBubble();
|
|
449
434
|
}
|
|
450
435
|
|
|
451
|
-
function
|
|
452
|
-
|
|
436
|
+
function clearChatUnread(options) {
|
|
437
|
+
options = options || {};
|
|
438
|
+
var hadUnread = state.chatUnreadCount > 0 || state.chatUnreadStartIndex >= 0;
|
|
439
|
+
state.chatUnreadCount = 0;
|
|
440
|
+
state.chatUnreadStartIndex = -1;
|
|
441
|
+
if (options.removeDivider !== false) {
|
|
442
|
+
var chatMsgs = getChatScrollElement();
|
|
443
|
+
if (chatMsgs) {
|
|
444
|
+
var divider = chatMsgs.querySelector(".chat-unread-divider");
|
|
445
|
+
if (divider && divider.parentNode) divider.parentNode.removeChild(divider);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
if (hadUnread) updateChatUnreadBubble();
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// 在 chatMessages 容器里把"未读分割线"放到正确位置——visually 在
|
|
452
|
+
// 最后一条已读和第一条未读中间。column-reverse 下 DOM[0] 是最新(视觉底部),
|
|
453
|
+
// 所以分割线在 DOM 里应该插到"第一条已读消息"之前。
|
|
454
|
+
function refreshChatUnreadDivider(chatMessages) {
|
|
455
|
+
if (!chatMessages) chatMessages = getChatScrollElement();
|
|
456
|
+
if (!chatMessages) return;
|
|
457
|
+
var existing = chatMessages.querySelector(".chat-unread-divider");
|
|
458
|
+
if (state.chatUnreadStartIndex < 0 || state.chatUnreadCount <= 0) {
|
|
459
|
+
if (existing && existing.parentNode) existing.parentNode.removeChild(existing);
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
var startIdx = state.chatUnreadStartIndex;
|
|
463
|
+
// 找到 DOM 里第一条 originalIndex < startIdx 的消息——它紧邻分割线下方(DOM 顺序),
|
|
464
|
+
// 也就是视觉上紧贴在分割线"上方"(column-reverse)。
|
|
465
|
+
var nodes = chatMessages.querySelectorAll(".chat-message");
|
|
466
|
+
var boundary = null;
|
|
467
|
+
for (var i = 0; i < nodes.length; i++) {
|
|
468
|
+
var idxAttr = nodes[i].getAttribute("data-msg-index");
|
|
469
|
+
if (idxAttr === null) continue;
|
|
470
|
+
var idx = parseInt(idxAttr, 10);
|
|
471
|
+
if (!isNaN(idx) && idx < startIdx) { boundary = nodes[i]; break; }
|
|
472
|
+
}
|
|
473
|
+
// 没找到 boundary:未读消息覆盖了整个可见窗口——把分割线挂到末尾即可。
|
|
474
|
+
var label = state.chatUnreadCount + " 条新消息";
|
|
475
|
+
if (!existing) {
|
|
476
|
+
existing = document.createElement("div");
|
|
477
|
+
existing.className = "chat-unread-divider";
|
|
478
|
+
existing.setAttribute("role", "separator");
|
|
479
|
+
existing.innerHTML = '<span class="chat-unread-divider-line"></span>'
|
|
480
|
+
+ '<span class="chat-unread-divider-label"></span>'
|
|
481
|
+
+ '<span class="chat-unread-divider-line"></span>';
|
|
482
|
+
}
|
|
483
|
+
existing.querySelector(".chat-unread-divider-label").textContent = label;
|
|
484
|
+
if (boundary) {
|
|
485
|
+
if (existing.nextSibling !== boundary || existing.parentNode !== chatMessages) {
|
|
486
|
+
chatMessages.insertBefore(existing, boundary);
|
|
487
|
+
}
|
|
488
|
+
} else {
|
|
489
|
+
if (existing.parentNode !== chatMessages || existing.nextSibling !== null) {
|
|
490
|
+
chatMessages.appendChild(existing);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
function updateChatUnreadBubble() {
|
|
496
|
+
var bubble = document.getElementById("chat-unread-bubble");
|
|
497
|
+
if (!bubble) return;
|
|
453
498
|
var selectedSession = state.sessions.find(function(s) { return s.id === state.selectedId; });
|
|
454
|
-
var
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
499
|
+
var notAtBottom = !isChatNearBottom();
|
|
500
|
+
// 显示条件:有选中会话 + 在 chat 视图 + 用户已经滚开了底部。
|
|
501
|
+
// 不强制要求有未读——用户主动滚上去时也给一个"回到底部"的入口。
|
|
502
|
+
var shouldShow = !!selectedSession && state.currentView === "chat" && notAtBottom;
|
|
503
|
+
bubble.classList.toggle("visible", shouldShow);
|
|
504
|
+
bubble.classList.toggle("has-unread", state.chatUnreadCount > 0);
|
|
505
|
+
var countEl = bubble.querySelector(".chat-unread-bubble-count");
|
|
506
|
+
if (countEl) {
|
|
507
|
+
if (state.chatUnreadCount > 0) {
|
|
508
|
+
countEl.textContent = state.chatUnreadCount > 99 ? "99+" : String(state.chatUnreadCount);
|
|
509
|
+
countEl.classList.add("visible");
|
|
510
|
+
} else {
|
|
511
|
+
countEl.textContent = "";
|
|
512
|
+
countEl.classList.remove("visible");
|
|
513
|
+
}
|
|
461
514
|
}
|
|
515
|
+
var label = state.chatUnreadCount > 0
|
|
516
|
+
? (state.chatUnreadCount + " 条新消息,点击查看")
|
|
517
|
+
: "回到最新消息";
|
|
518
|
+
bubble.setAttribute("aria-label", label);
|
|
519
|
+
bubble.setAttribute("title", label);
|
|
462
520
|
var chatContainer = document.getElementById("chat-output");
|
|
463
521
|
if (chatContainer) chatContainer.classList.toggle("has-jump-btn", shouldShow);
|
|
464
522
|
}
|
|
@@ -467,38 +525,26 @@
|
|
|
467
525
|
var chatMsgs = getChatScrollElement();
|
|
468
526
|
if (!chatMsgs || !chatMsgs.isConnected) return;
|
|
469
527
|
state.chatIsProgrammaticScroll = true;
|
|
528
|
+
var done = function() {
|
|
529
|
+
state.chatIsProgrammaticScroll = false;
|
|
530
|
+
state.chatStickToBottom = true;
|
|
531
|
+
clearChatUnread({ removeDivider: true });
|
|
532
|
+
updateChatUnreadBubble();
|
|
533
|
+
};
|
|
470
534
|
if (smooth && typeof chatMsgs.scrollTo === "function") {
|
|
471
535
|
chatMsgs.scrollTo({ top: 0, behavior: "smooth" });
|
|
472
|
-
setTimeout(
|
|
473
|
-
state.chatIsProgrammaticScroll = false;
|
|
474
|
-
updateChatJumpToBottomButton();
|
|
475
|
-
}, 220);
|
|
536
|
+
setTimeout(done, 260);
|
|
476
537
|
return;
|
|
477
538
|
}
|
|
478
539
|
chatMsgs.scrollTop = 0;
|
|
479
|
-
requestAnimationFrame(
|
|
480
|
-
state.chatIsProgrammaticScroll = false;
|
|
481
|
-
updateChatJumpToBottomButton();
|
|
482
|
-
});
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
function setChatAutoFollow(enabled, options) {
|
|
486
|
-
options = options || {};
|
|
487
|
-
state.chatAutoFollow = !!enabled;
|
|
488
|
-
persistChatAutoFollow();
|
|
489
|
-
updateChatFollowToggleButton();
|
|
490
|
-
if (state.chatAutoFollow && options.scrollNow !== false) {
|
|
491
|
-
scrollChatToBottom(!!options.smooth);
|
|
492
|
-
} else {
|
|
493
|
-
updateChatJumpToBottomButton();
|
|
494
|
-
}
|
|
540
|
+
requestAnimationFrame(done);
|
|
495
541
|
}
|
|
496
542
|
|
|
497
543
|
function bindChatScrollListener() {
|
|
498
544
|
var chatMsgs = getChatScrollElement();
|
|
499
545
|
if (!chatMsgs || !chatMsgs.isConnected) return;
|
|
500
546
|
if (state.chatScrollElement === chatMsgs && state.chatScrollHandler) {
|
|
501
|
-
|
|
547
|
+
updateChatUnreadBubble();
|
|
502
548
|
return;
|
|
503
549
|
}
|
|
504
550
|
if (state.chatScrollElement && state.chatScrollHandler) {
|
|
@@ -507,22 +553,24 @@
|
|
|
507
553
|
state.chatScrollElement = chatMsgs;
|
|
508
554
|
state.chatScrollHandler = function() {
|
|
509
555
|
if (!chatMsgs.isConnected) return;
|
|
556
|
+
// 程序触发的滚动(点了气泡)不算"用户翻页"——别把状态弄乱。
|
|
510
557
|
if (state.chatIsProgrammaticScroll) {
|
|
511
|
-
|
|
558
|
+
updateChatUnreadBubble();
|
|
512
559
|
return;
|
|
513
560
|
}
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
561
|
+
var atBottom = isChatNearBottom(chatMsgs);
|
|
562
|
+
if (atBottom) {
|
|
563
|
+
// 用户自己滚到底了——清未读、贴回底部、撤下气泡。
|
|
564
|
+
state.chatStickToBottom = true;
|
|
565
|
+
clearChatUnread({ removeDivider: true });
|
|
566
|
+
} else {
|
|
567
|
+
// 用户主动往上翻——脱离贴底状态。新消息只会累积到气泡,不滚视图。
|
|
568
|
+
state.chatStickToBottom = false;
|
|
521
569
|
}
|
|
522
|
-
|
|
570
|
+
updateChatUnreadBubble();
|
|
523
571
|
};
|
|
524
572
|
chatMsgs.addEventListener("scroll", state.chatScrollHandler, { passive: true });
|
|
525
|
-
|
|
573
|
+
updateChatUnreadBubble();
|
|
526
574
|
}
|
|
527
575
|
|
|
528
576
|
/** Load older messages by expanding the visible window */
|
|
@@ -865,8 +913,11 @@
|
|
|
865
913
|
}
|
|
866
914
|
state.chatScrollElement = null;
|
|
867
915
|
state.chatScrollHandler = null;
|
|
868
|
-
state.showChatJumpToBottom = false;
|
|
869
916
|
state.chatIsProgrammaticScroll = false;
|
|
917
|
+
// 切会话时未读状态归零、贴底重置——避免上一个会话残留的"未读气泡"。
|
|
918
|
+
state.chatStickToBottom = true;
|
|
919
|
+
state.chatUnreadCount = 0;
|
|
920
|
+
state.chatUnreadStartIndex = -1;
|
|
870
921
|
}
|
|
871
922
|
|
|
872
923
|
function getEffectiveCwd() {
|
|
@@ -1388,10 +1439,10 @@
|
|
|
1388
1439
|
'<button id="terminal-jump-bottom" class="terminal-jump-bottom' + (state.showTerminalJumpToBottom ? ' visible' : '') + '" type="button" title="回到底部" aria-label="回到底部"><svg viewBox="0 0 16 16" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M8 3.5v9M3.5 8l4.5 4.5L12.5 8"/></svg></button>' +
|
|
1389
1440
|
'</div>' +
|
|
1390
1441
|
'<div id="chat-output" class="chat-container hidden">' +
|
|
1391
|
-
'<
|
|
1392
|
-
'<
|
|
1393
|
-
|
|
1394
|
-
'
|
|
1442
|
+
'<button id="chat-unread-bubble" class="chat-unread-bubble" type="button" title="回到最新消息" aria-label="回到最新消息">' +
|
|
1443
|
+
'<span class="chat-unread-bubble-icon"><svg viewBox="0 0 16 16" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M8 3.5v9M3.5 8l4.5 4.5L12.5 8"/></svg></span>' +
|
|
1444
|
+
'<span class="chat-unread-bubble-count" aria-hidden="true"></span>' +
|
|
1445
|
+
'</button>' +
|
|
1395
1446
|
'</div>' +
|
|
1396
1447
|
'<div id="blank-chat" class="blank-chat' + (state.selectedId ? " hidden" : "") + '">' +
|
|
1397
1448
|
'<div class="blank-chat-inner">' +
|
|
@@ -1423,15 +1474,22 @@
|
|
|
1423
1474
|
'<div class="composer-top-row">' +
|
|
1424
1475
|
'<div id="todo-progress" class="todo-progress hidden">' +
|
|
1425
1476
|
'<div class="todo-progress-header" id="todo-progress-toggle">' +
|
|
1426
|
-
'<
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1477
|
+
'<div class="todo-progress-left">' +
|
|
1478
|
+
'<span class="todo-progress-ring" id="todo-progress-ring" aria-hidden="true" style="--progress:0">' +
|
|
1479
|
+
'<svg width="16" height="16" viewBox="0 0 36 36">' +
|
|
1480
|
+
'<circle class="todo-ring-track" cx="18" cy="18" r="15.5" fill="none" stroke-width="4"/>' +
|
|
1481
|
+
'<circle class="todo-ring-fill" cx="18" cy="18" r="15.5" fill="none" stroke-width="4" stroke-linecap="round"/>' +
|
|
1482
|
+
'</svg>' +
|
|
1483
|
+
'</span>' +
|
|
1484
|
+
'<span class="todo-progress-counter" id="todo-progress-counter"></span>' +
|
|
1485
|
+
'<span class="todo-progress-task" id="todo-progress-task"></span>' +
|
|
1486
|
+
'</div>' +
|
|
1487
|
+
'<svg class="todo-progress-chevron" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="6 15 12 9 18 15"/></svg>' +
|
|
1433
1488
|
'</div>' +
|
|
1434
1489
|
'</div>' +
|
|
1490
|
+
'<div class="todo-progress-body hidden" id="todo-progress-body">' +
|
|
1491
|
+
'<ul class="todo-progress-list" id="todo-progress-list"></ul>' +
|
|
1492
|
+
'</div>' +
|
|
1435
1493
|
'</div>' +
|
|
1436
1494
|
'<div class="input-composer">' +
|
|
1437
1495
|
'<button id="prompt-optimize-btn" class="prompt-optimize-btn" type="button" title="提示词优化(AI)" aria-label="提示词优化">' +
|
|
@@ -5080,17 +5138,10 @@
|
|
|
5080
5138
|
if (jumpBottomBtn) jumpBottomBtn.addEventListener("click", function() {
|
|
5081
5139
|
maybeScrollTerminalToBottom("force");
|
|
5082
5140
|
});
|
|
5083
|
-
|
|
5084
|
-
|
|
5085
|
-
|
|
5086
|
-
|
|
5087
|
-
} else {
|
|
5088
|
-
setChatAutoFollow(true, { scrollNow: true, smooth: false });
|
|
5089
|
-
}
|
|
5090
|
-
});
|
|
5091
|
-
var chatJumpBottomBtn = document.getElementById("chat-jump-bottom");
|
|
5092
|
-
if (chatJumpBottomBtn) chatJumpBottomBtn.addEventListener("click", function() {
|
|
5093
|
-
setChatAutoFollow(true, { scrollNow: true, smooth: true });
|
|
5141
|
+
// 未读气泡:点一下就贴回最新消息,顺手清掉未读分割线和计数。
|
|
5142
|
+
var chatUnreadBubble = document.getElementById("chat-unread-bubble");
|
|
5143
|
+
if (chatUnreadBubble) chatUnreadBubble.addEventListener("click", function() {
|
|
5144
|
+
scrollChatToBottom(true);
|
|
5094
5145
|
});
|
|
5095
5146
|
var fileRefresh = document.getElementById("file-explorer-refresh");
|
|
5096
5147
|
if (fileRefresh) fileRefresh.addEventListener("click", function() { refreshFileExplorer(); });
|
|
@@ -7118,8 +7169,7 @@
|
|
|
7118
7169
|
ensureChatMessagesContainer(chatContainer);
|
|
7119
7170
|
}
|
|
7120
7171
|
bindChatScrollListener();
|
|
7121
|
-
|
|
7122
|
-
updateChatJumpToBottomButton();
|
|
7172
|
+
updateChatUnreadBubble();
|
|
7123
7173
|
updateInteractiveControls();
|
|
7124
7174
|
}
|
|
7125
7175
|
|
|
@@ -7588,6 +7638,14 @@
|
|
|
7588
7638
|
// Reset todo progress bar
|
|
7589
7639
|
var todoEl = document.getElementById("todo-progress");
|
|
7590
7640
|
if (todoEl) todoEl.classList.add("hidden");
|
|
7641
|
+
// 同时清掉上一会话残留的 "回复中 N.Ns" 状态条以及它的计时器/glow。
|
|
7642
|
+
// 不清就会出现:切到新建的空会话,底部仍显示前一会话的 todolist + 回复中。
|
|
7643
|
+
var staleStatusBar = document.querySelector(".structured-status-bar");
|
|
7644
|
+
if (staleStatusBar) staleStatusBar.remove();
|
|
7645
|
+
if (_statusBarTimerId) { clearInterval(_statusBarTimerId); _statusBarTimerId = null; }
|
|
7646
|
+
_statusBarStartTime = 0;
|
|
7647
|
+
var staleComposer = document.querySelector(".input-composer");
|
|
7648
|
+
if (staleComposer) staleComposer.classList.remove("in-flight");
|
|
7591
7649
|
var session = foundSession;
|
|
7592
7650
|
state.preferredCommand = getPreferredTool();
|
|
7593
7651
|
state.chatMode = getSafeModeForTool("claude", session && session.mode ? session.mode : state.chatMode);
|
|
@@ -13768,8 +13826,7 @@
|
|
|
13768
13826
|
if (!chatMessages) return null;
|
|
13769
13827
|
chatMessages.innerHTML = html;
|
|
13770
13828
|
bindChatScrollListener();
|
|
13771
|
-
|
|
13772
|
-
updateChatJumpToBottomButton();
|
|
13829
|
+
updateChatUnreadBubble();
|
|
13773
13830
|
return chatMessages;
|
|
13774
13831
|
}
|
|
13775
13832
|
|
|
@@ -13795,13 +13852,18 @@
|
|
|
13795
13852
|
state.lastRenderedEmpty = "empty";
|
|
13796
13853
|
state.lastRenderedMsgCount = 0;
|
|
13797
13854
|
}
|
|
13855
|
+
// 空会话进入空状态前,把上一会话残留的状态条 / todo 进度条清掉。
|
|
13856
|
+
// 这里是 selectSession 之外的兜底:WS init 等异步路径也会落到这条空分支。
|
|
13857
|
+
renderStructuredStatusBar(null, selectedSession);
|
|
13858
|
+
updateTodoProgress([]);
|
|
13798
13859
|
return;
|
|
13799
13860
|
}
|
|
13800
13861
|
|
|
13801
13862
|
// Lazy loading: only render the most recent chatRenderedCount messages.
|
|
13802
|
-
//
|
|
13863
|
+
// 新消息进来时永远展开渲染窗口,避免用户正在看的旧消息被挤进"加载更早"里——
|
|
13864
|
+
// Telegram 风格下我们不主动挪用户的视线,最稳妥的办法就是别让他看的那条消失。
|
|
13803
13865
|
var totalMsgCount = allMessages.length;
|
|
13804
|
-
if (totalMsgCount > state.chatRenderedCount
|
|
13866
|
+
if (totalMsgCount > state.chatRenderedCount) {
|
|
13805
13867
|
state.chatRenderedCount = totalMsgCount;
|
|
13806
13868
|
}
|
|
13807
13869
|
var visibleOffset = Math.max(0, totalMsgCount - state.chatRenderedCount);
|
|
@@ -13857,6 +13919,13 @@
|
|
|
13857
13919
|
var chatMessages = ensureChatMessagesContainer(chatOutput);
|
|
13858
13920
|
if (!chatMessages) return;
|
|
13859
13921
|
|
|
13922
|
+
// 在动 DOM 之前先看用户是不是贴在底部——这决定后面我们要不要让视图
|
|
13923
|
+
// "继续粘在底部"。column-reverse 下 scrollTop 接近 0 = 视觉底部。
|
|
13924
|
+
// 同时把 state.chatStickToBottom 同步到当前真实状态,避免长时间不滚动后
|
|
13925
|
+
// 的状态漂移(比如新会话 init 的瞬间)。
|
|
13926
|
+
var renderWasAtBottom = isChatNearBottom(chatMessages);
|
|
13927
|
+
if (renderWasAtBottom) state.chatStickToBottom = true;
|
|
13928
|
+
|
|
13860
13929
|
var existingCount = chatMessages.querySelectorAll(".chat-message").length;
|
|
13861
13930
|
// Full render when: forced, no existing messages, or message count decreased/changed
|
|
13862
13931
|
var needsFullRender = forceRender || existingCount === 0 || msgCount !== existingCount;
|
|
@@ -13906,16 +13975,29 @@
|
|
|
13906
13975
|
}
|
|
13907
13976
|
|
|
13908
13977
|
chatMessages.innerHTML = html;
|
|
13978
|
+
// 给每条消息打 data-msg-index(用 state.currentMessages 的全局索引),
|
|
13979
|
+
// 后面 refreshChatUnreadDivider 用它找未读分割线的位置。
|
|
13980
|
+
(function() {
|
|
13981
|
+
var msgEls = chatMessages.querySelectorAll(".chat-message:not(.system-info)");
|
|
13982
|
+
// column-reverse: DOM[0] = 最新(最高 originalIndex)
|
|
13983
|
+
var totalVisible = msgEls.length;
|
|
13984
|
+
for (var idx = 0; idx < totalVisible; idx++) {
|
|
13985
|
+
msgEls[idx].setAttribute("data-msg-index", String(visibleOffset + totalVisible - 1 - idx));
|
|
13986
|
+
}
|
|
13987
|
+
})();
|
|
13909
13988
|
// 会话切换 / 首次渲染后,浏览器会把旧的 scrollTop 钳制到新内容
|
|
13910
13989
|
// 的最大值——column-reverse 下这意味着视觉上跳到最上面(最旧消息),
|
|
13911
13990
|
// 也就是用户反馈的"退出再回来时被重定向到最上面"。这里在
|
|
13912
13991
|
// prevMsgCount === 0(resetChatRenderCache 后或刚从空状态进入)
|
|
13913
13992
|
// 强制 scrollTop=0(视觉底部 = 最新消息),避免错位。
|
|
13914
|
-
// 注意:仅在"换会话/初次渲染"时强制重置;同一会话内的全量重渲染
|
|
13915
|
-
// (prevMsgCount > 0,比如 forceFullRender 或消息数减少)继续走
|
|
13916
|
-
// smartScrollToBottom,尊重用户的 chatAutoFollow 选择。
|
|
13917
13993
|
if (prevMsgCount === 0) {
|
|
13918
13994
|
chatMessages.scrollTop = 0;
|
|
13995
|
+
state.chatStickToBottom = true;
|
|
13996
|
+
clearChatUnread({ removeDivider: true });
|
|
13997
|
+
} else if (renderWasAtBottom) {
|
|
13998
|
+
// 同一会话内的全量重渲染:用户原本贴底就保持贴底,浏览器在 innerHTML
|
|
13999
|
+
// 重置后可能把 scrollTop 钳到一个奇怪的值,这里显式拉回 0。
|
|
14000
|
+
chatMessages.scrollTop = 0;
|
|
13919
14001
|
}
|
|
13920
14002
|
attachAllCopyHandlers(chatMessages);
|
|
13921
14003
|
bindChatScrollListener();
|
|
@@ -13940,9 +14022,13 @@
|
|
|
13940
14022
|
}
|
|
13941
14023
|
}
|
|
13942
14024
|
}
|
|
13943
|
-
//
|
|
14025
|
+
// 不主动 smartScrollToBottom——同一会话的全量重渲染要么是
|
|
14026
|
+
// streaming fallback(页面位置应保持),要么是 msgCount 减少(极少见,
|
|
14027
|
+
// 走 prevMsgCount===0 那条分支已经处理)。让浏览器自带的 scroll
|
|
14028
|
+
// anchoring 接手,避免在用户阅读时把视图拽走。
|
|
13944
14029
|
requestAnimationFrame(function() {
|
|
13945
|
-
|
|
14030
|
+
refreshChatUnreadDivider(chatMessages);
|
|
14031
|
+
updateChatUnreadBubble();
|
|
13946
14032
|
observeLoadMoreSentinel();
|
|
13947
14033
|
});
|
|
13948
14034
|
}
|
|
@@ -14017,6 +14103,11 @@
|
|
|
14017
14103
|
newMessages.reverse();
|
|
14018
14104
|
var fragment = document.createDocumentFragment();
|
|
14019
14105
|
var insertedEls = [];
|
|
14106
|
+
// 记录每条新消息的 originalIndex,方便后面打标签 / 计算未读起点。
|
|
14107
|
+
var insertedOrigIdx = [];
|
|
14108
|
+
// 第一条新消息(数组里 index 最小的,时间上最早的那条)对应的全局索引——
|
|
14109
|
+
// 用作未读起点。
|
|
14110
|
+
var firstNewOrigIdx = visibleOffset + existingCount;
|
|
14020
14111
|
for (var i = 0; i < newMessages.length; i++) {
|
|
14021
14112
|
var div = document.createElement("div");
|
|
14022
14113
|
var nmOrigIdx = visibleOffset + existingCount + (newMessages.length - 1 - i);
|
|
@@ -14024,7 +14115,9 @@
|
|
|
14024
14115
|
var el = div.firstElementChild;
|
|
14025
14116
|
if (el) {
|
|
14026
14117
|
el.classList.add("animate-in");
|
|
14118
|
+
el.setAttribute("data-msg-index", String(nmOrigIdx));
|
|
14027
14119
|
insertedEls.push(el);
|
|
14120
|
+
insertedOrigIdx.push(nmOrigIdx);
|
|
14028
14121
|
fragment.appendChild(el);
|
|
14029
14122
|
}
|
|
14030
14123
|
}
|
|
@@ -14034,10 +14127,31 @@
|
|
|
14034
14127
|
applyPersistedExpandState(chatMessages);
|
|
14035
14128
|
// Collapse all existing cards; new cards (with animate-in) stay expanded
|
|
14036
14129
|
collapseOldToolCards(chatMessages, insertedEls);
|
|
14037
|
-
//
|
|
14038
|
-
|
|
14039
|
-
|
|
14040
|
-
|
|
14130
|
+
// Telegram 行为:
|
|
14131
|
+
// - 用户原本就贴在底部 → 维持贴底(column-reverse 通常会自动留在底部,
|
|
14132
|
+
// 但浏览器的 scroll anchoring 在某些边界场景会把 scrollTop 调成非 0;
|
|
14133
|
+
// 这里显式拉回 0 做兜底,不用动画,不会让用户感觉"被甩")。
|
|
14134
|
+
// - 用户已经滚上去 → 一根毛都不动他的视图,只把未读累到气泡里。
|
|
14135
|
+
if (renderWasAtBottom) {
|
|
14136
|
+
requestAnimationFrame(function() {
|
|
14137
|
+
if (chatMessages.isConnected && Math.abs(chatMessages.scrollTop) > 1) {
|
|
14138
|
+
state.chatIsProgrammaticScroll = true;
|
|
14139
|
+
chatMessages.scrollTop = 0;
|
|
14140
|
+
requestAnimationFrame(function() { state.chatIsProgrammaticScroll = false; });
|
|
14141
|
+
}
|
|
14142
|
+
// 视为已读 —— 用户当前就在底部看着,这些新消息直接进入"已读"。
|
|
14143
|
+
clearChatUnread({ removeDivider: true });
|
|
14144
|
+
updateChatUnreadBubble();
|
|
14145
|
+
});
|
|
14146
|
+
} else {
|
|
14147
|
+
// 累计未读。如果之前没有未读,就用这一批的最早一条做分割线起点。
|
|
14148
|
+
if (state.chatUnreadStartIndex < 0) {
|
|
14149
|
+
state.chatUnreadStartIndex = firstNewOrigIdx;
|
|
14150
|
+
}
|
|
14151
|
+
state.chatUnreadCount += insertedEls.length;
|
|
14152
|
+
refreshChatUnreadDivider(chatMessages);
|
|
14153
|
+
updateChatUnreadBubble();
|
|
14154
|
+
}
|
|
14041
14155
|
} else if (msgCount === existingCount && outputHash !== prevHash) {
|
|
14042
14156
|
// Same message count but content changed (streaming update).
|
|
14043
14157
|
// Optimization: only re-render the newest N messages (column-reverse: first children)
|
|
@@ -14072,8 +14186,18 @@
|
|
|
14072
14186
|
if (replacedAny) {
|
|
14073
14187
|
bindChatScrollListener();
|
|
14074
14188
|
applyPersistedExpandState(chatMessages);
|
|
14189
|
+
// Streaming 更新只是改最新一条的内容,不改条数。column-reverse 下
|
|
14190
|
+
// 浏览器的 scroll anchoring 会自动保持视觉位置;用户贴底时新内容
|
|
14191
|
+
// 自然出现在底部,用户上滚时视图也不受打扰——不需要再 smartScroll。
|
|
14075
14192
|
requestAnimationFrame(function() {
|
|
14076
|
-
|
|
14193
|
+
// 兜底:用户贴底时如果浏览器把 scrollTop 调成非零,拉回来。
|
|
14194
|
+
if (renderWasAtBottom && chatMessages.isConnected && Math.abs(chatMessages.scrollTop) > 1) {
|
|
14195
|
+
state.chatIsProgrammaticScroll = true;
|
|
14196
|
+
chatMessages.scrollTop = 0;
|
|
14197
|
+
requestAnimationFrame(function() { state.chatIsProgrammaticScroll = false; });
|
|
14198
|
+
}
|
|
14199
|
+
refreshChatUnreadDivider(chatMessages);
|
|
14200
|
+
updateChatUnreadBubble();
|
|
14077
14201
|
});
|
|
14078
14202
|
var newestMsgEl = chatMessages.querySelector(".chat-message");
|
|
14079
14203
|
var allCards = chatMessages.querySelectorAll(".tool-use-card, .inline-diff[data-expand-key]");
|
|
@@ -14102,19 +14226,11 @@
|
|
|
14102
14226
|
updateTodoProgress(allMessages);
|
|
14103
14227
|
}
|
|
14104
14228
|
|
|
14105
|
-
//
|
|
14106
|
-
//
|
|
14107
|
-
|
|
14108
|
-
|
|
14109
|
-
|
|
14110
|
-
return;
|
|
14111
|
-
}
|
|
14112
|
-
var chatMsgs = (container && container.classList && container.classList.contains("chat-messages"))
|
|
14113
|
-
? container
|
|
14114
|
-
: getChatScrollElement();
|
|
14115
|
-
if (!chatMsgs || !chatMsgs.isConnected) return;
|
|
14116
|
-
scrollChatToBottom(false);
|
|
14117
|
-
}
|
|
14229
|
+
// 注:旧版的 smartScrollToBottom / chatAutoFollow / chat-follow-toggle 都已经
|
|
14230
|
+
// 拆掉,改成 Telegram 风格:贴底状态完全由用户的滚动行为驱动,未读靠
|
|
14231
|
+
// chat-unread-bubble 气泡提示,不再主动滚动用户的视图。
|
|
14232
|
+
// 相关入口:scrollChatToBottom(用户点气泡时强制贴底)、
|
|
14233
|
+
// refreshChatUnreadDivider(分割线渲染)、updateChatUnreadBubble(气泡 UI)。
|
|
14118
14234
|
|
|
14119
14235
|
// --- Todo progress bar ---
|
|
14120
14236
|
var todoExpanded = false;
|
|
@@ -14132,10 +14248,10 @@
|
|
|
14132
14248
|
if (prog && body) {
|
|
14133
14249
|
if (todoExpanded) {
|
|
14134
14250
|
prog.classList.add("expanded");
|
|
14135
|
-
body.classList.
|
|
14251
|
+
body.classList.add("expanded");
|
|
14136
14252
|
} else {
|
|
14137
14253
|
prog.classList.remove("expanded");
|
|
14138
|
-
body.classList.
|
|
14254
|
+
body.classList.remove("expanded");
|
|
14139
14255
|
}
|
|
14140
14256
|
}
|
|
14141
14257
|
});
|
|
@@ -14157,14 +14273,17 @@
|
|
|
14157
14273
|
}
|
|
14158
14274
|
|
|
14159
14275
|
var container = document.getElementById("todo-progress");
|
|
14276
|
+
var bodyEl = document.getElementById("todo-progress-body");
|
|
14160
14277
|
if (!container) return;
|
|
14161
14278
|
|
|
14162
14279
|
if (!todos || todos.length === 0) {
|
|
14163
14280
|
container.classList.add("hidden");
|
|
14281
|
+
if (bodyEl) bodyEl.classList.add("hidden");
|
|
14164
14282
|
return;
|
|
14165
14283
|
}
|
|
14166
14284
|
|
|
14167
14285
|
container.classList.remove("hidden");
|
|
14286
|
+
if (bodyEl) bodyEl.classList.remove("hidden");
|
|
14168
14287
|
|
|
14169
14288
|
var completed = 0;
|
|
14170
14289
|
var inProgress = 0;
|
|
@@ -14186,6 +14305,7 @@
|
|
|
14186
14305
|
if (allDone) {
|
|
14187
14306
|
// Hide todo when all tasks are completed
|
|
14188
14307
|
container.classList.add("hidden");
|
|
14308
|
+
if (bodyEl) bodyEl.classList.add("hidden");
|
|
14189
14309
|
return;
|
|
14190
14310
|
} else {
|
|
14191
14311
|
container.classList.remove("all-done");
|
|
@@ -14197,6 +14317,14 @@
|
|
|
14197
14317
|
var task = document.getElementById("todo-progress-task");
|
|
14198
14318
|
if (task) task.textContent = activeTask;
|
|
14199
14319
|
|
|
14320
|
+
// Drive the circular progress ring with the honest "completed / total" fraction
|
|
14321
|
+
// (counter text shows the 1-indexed current step, ring shows actual done ratio).
|
|
14322
|
+
var ring = document.getElementById("todo-progress-ring");
|
|
14323
|
+
if (ring) {
|
|
14324
|
+
var ratio = todos.length > 0 ? completed / todos.length : 0;
|
|
14325
|
+
ring.style.setProperty("--progress", ratio.toFixed(3));
|
|
14326
|
+
}
|
|
14327
|
+
|
|
14200
14328
|
// Render expanded list
|
|
14201
14329
|
var list = document.getElementById("todo-progress-list");
|
|
14202
14330
|
if (list) {
|