修复会话切换兼容并重新打包发布
This commit is contained in:
614
public/app.js
614
public/app.js
@@ -99,6 +99,8 @@
|
||||
const SESSION_LOAD_OVERLAY_TIMEOUT_MS = 12_000;
|
||||
const SESSION_LOAD_REQUEST_TIMEOUT_MS = 45_000;
|
||||
const SESSION_RESUME_FALLBACK_MS = 1_500;
|
||||
const WS_CLIENT_HEARTBEAT_INTERVAL_MS = 15_000;
|
||||
const WS_CLIENT_HEARTBEAT_TIMEOUT_MS = 10_000;
|
||||
|
||||
function normalizeFrontendAssetVersion(value) {
|
||||
const version = String(value || '').trim().toLowerCase();
|
||||
@@ -207,13 +209,21 @@
|
||||
// --- State ---
|
||||
let ws = null;
|
||||
let wsAuthenticated = false;
|
||||
// 只有服务端明确声明支持时才启用应用层心跳;旧版服务端会把它当成未知消息。
|
||||
let wsSupportsClientHeartbeat = false;
|
||||
let wsHeartbeatTimer = null;
|
||||
let wsHeartbeatPending = null;
|
||||
let wsHeartbeatSeq = 0;
|
||||
let authToken = localStorage.getItem('cc-web-token');
|
||||
let currentSessionId = null;
|
||||
let sessions = [];
|
||||
let sessionCache = new Map();
|
||||
// 未完整加载的历史只作为分页合并缓冲,绝不直接用于切换展示。
|
||||
let sessionHistoryBuffers = new Map();
|
||||
let isGenerating = false;
|
||||
let reconnectAttempts = 0;
|
||||
let reconnectTimer = null;
|
||||
let lastWsDisconnectNoticeAt = 0;
|
||||
let isPageUnloading = false;
|
||||
let pendingText = '';
|
||||
let renderTimer = null;
|
||||
@@ -248,6 +258,19 @@
|
||||
loading: false,
|
||||
error: '',
|
||||
};
|
||||
let outlineHistoryRequestSeq = 0;
|
||||
let activeOutlineHistoryRequest = null;
|
||||
let activeOutlineTargetRequest = null;
|
||||
let currentOutlineHistoryState = {
|
||||
sessionId: null,
|
||||
cursor: 0,
|
||||
total: 0,
|
||||
available: false,
|
||||
loading: false,
|
||||
complete: false,
|
||||
error: '',
|
||||
entries: new Map(),
|
||||
};
|
||||
let sessionLoadOverlayTimer = null;
|
||||
let sessionLoadRequestTimer = null;
|
||||
let sessionResumeFallbackTimer = null;
|
||||
@@ -1916,6 +1939,79 @@
|
||||
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
|
||||
}
|
||||
|
||||
function createOutlineHistoryState(sessionId = null) {
|
||||
return {
|
||||
sessionId,
|
||||
cursor: 0,
|
||||
total: 0,
|
||||
available: false,
|
||||
loading: false,
|
||||
complete: false,
|
||||
error: '',
|
||||
entries: new Map(),
|
||||
};
|
||||
}
|
||||
|
||||
function resetOutlineHistoryState(sessionId = null) {
|
||||
activeOutlineHistoryRequest = null;
|
||||
activeOutlineTargetRequest = null;
|
||||
currentOutlineHistoryState = createOutlineHistoryState(sessionId);
|
||||
}
|
||||
|
||||
function buildOutlineHistoryEntry(message, messageIndex) {
|
||||
if (message?.role !== 'user') return null;
|
||||
const normalizedIndex = Number(messageIndex);
|
||||
if (!Number.isFinite(normalizedIndex) || normalizedIndex < 0) return null;
|
||||
const content = String(message.content || '');
|
||||
const messageId = String(message.id || message.messageId || '').trim();
|
||||
if (!messageId && !content.trim()) return null;
|
||||
return {
|
||||
id: messageId || `history-message-${normalizedIndex}`,
|
||||
targetMessageId: '',
|
||||
label: shortMessagePreview(content, 64),
|
||||
timestamp: String(message.timestamp || message.createdAt || ''),
|
||||
messageIndex: Math.trunc(normalizedIndex),
|
||||
};
|
||||
}
|
||||
|
||||
function mergeOutlineHistoryMessages(messages, baseIndex = 0) {
|
||||
if (!Array.isArray(messages) || currentOutlineHistoryState.sessionId === null) return;
|
||||
const normalizedBase = Number.isFinite(Number(baseIndex)) ? Number(baseIndex) : 0;
|
||||
messages.forEach((message, index) => {
|
||||
const entry = buildOutlineHistoryEntry(message, normalizedBase + index);
|
||||
if (!entry) return;
|
||||
const previous = currentOutlineHistoryState.entries.get(entry.messageIndex);
|
||||
currentOutlineHistoryState.entries.set(entry.messageIndex, { ...previous, ...entry });
|
||||
});
|
||||
}
|
||||
|
||||
function syncOutlineHistoryIndex(snapshot = {}) {
|
||||
const sessionId = String(snapshot?.sessionId || snapshot?.id || '').trim();
|
||||
if (!sessionId) return;
|
||||
if (currentOutlineHistoryState.sessionId !== sessionId) resetOutlineHistoryState(sessionId);
|
||||
const baseIndex = Number.isFinite(Number(snapshot.historyBaseIndex))
|
||||
? Number(snapshot.historyBaseIndex)
|
||||
: Math.max(0, Number(snapshot.historyTotal || 0) - (snapshot.messages || []).length);
|
||||
mergeOutlineHistoryMessages(snapshot.messages || [], baseIndex);
|
||||
const incomingCursor = Number.isFinite(Number(snapshot.historyCursor))
|
||||
? Math.max(0, Number(snapshot.historyCursor))
|
||||
: currentOutlineHistoryState.cursor;
|
||||
const incomingTotal = Number.isFinite(Number(snapshot.historyTotal))
|
||||
? Math.max(0, Number(snapshot.historyTotal))
|
||||
: currentOutlineHistoryState.total;
|
||||
const complete = currentOutlineHistoryState.complete || incomingCursor === 0;
|
||||
currentOutlineHistoryState = {
|
||||
...currentOutlineHistoryState,
|
||||
cursor: complete ? 0 : incomingCursor,
|
||||
total: Math.max(currentOutlineHistoryState.total, incomingTotal),
|
||||
available: snapshot.historyAvailable !== undefined
|
||||
? snapshot.historyAvailable !== false
|
||||
: currentOutlineHistoryState.available,
|
||||
complete,
|
||||
error: '',
|
||||
};
|
||||
}
|
||||
|
||||
function buildUserOutlineTimelineItems(messageItems, titleHistory = []) {
|
||||
const timeline = [];
|
||||
const timelineMessages = [];
|
||||
@@ -1992,33 +2088,60 @@
|
||||
}
|
||||
|
||||
function buildUserOutlineItems() {
|
||||
const seen = new Set();
|
||||
return Array.from(messagesDiv.querySelectorAll('.msg.user[data-message-id]')).map((element) => {
|
||||
const hasMessageIndex = (value) => value !== null && value !== '' && Number.isFinite(Number(value));
|
||||
const entries = new Map();
|
||||
currentOutlineHistoryState.entries.forEach((entry, messageIndex) => {
|
||||
entries.set(`index:${messageIndex}`, { ...entry, messageIndex: Number(messageIndex) });
|
||||
});
|
||||
messagesDiv.querySelectorAll('.msg.user[data-message-id]').forEach((element) => {
|
||||
const id = String(element.dataset.messageId || '').trim();
|
||||
if (!id || seen.has(id)) return null;
|
||||
seen.add(id);
|
||||
if (!id) return;
|
||||
const indexed = userMessageIndex.get(id);
|
||||
const content = indexed?.content || element.querySelector('.msg-text')?.textContent || '';
|
||||
const rawMessageIndex = String(element.dataset.messageIndex || '').trim();
|
||||
const messageIndex = rawMessageIndex ? Number(rawMessageIndex) : Number.NaN;
|
||||
return {
|
||||
const entry = {
|
||||
id,
|
||||
targetMessageId: element.id || '',
|
||||
label: shortMessagePreview(content, 64),
|
||||
timestamp: indexed?.timestamp || '',
|
||||
messageIndex: Number.isFinite(messageIndex) ? messageIndex : null,
|
||||
};
|
||||
}).filter((entry) => entry && entry.targetMessageId);
|
||||
const key = Number.isFinite(entry.messageIndex) ? `index:${entry.messageIndex}` : `id:${entry.id}`;
|
||||
entries.set(key, { ...entries.get(key), ...entry });
|
||||
});
|
||||
return Array.from(entries.values())
|
||||
.filter((entry) => entry && (entry.id || hasMessageIndex(entry.messageIndex)))
|
||||
.sort((left, right) => {
|
||||
const leftIndex = Number(left.messageIndex);
|
||||
const rightIndex = Number(right.messageIndex);
|
||||
const hasLeftIndex = hasMessageIndex(left.messageIndex);
|
||||
const hasRightIndex = hasMessageIndex(right.messageIndex);
|
||||
if (hasLeftIndex && hasRightIndex && leftIndex !== rightIndex) {
|
||||
return leftIndex - rightIndex;
|
||||
}
|
||||
if (hasLeftIndex !== hasRightIndex) return hasLeftIndex ? -1 : 1;
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
|
||||
function updateUserOutlinePanel() {
|
||||
if (!userOutlinePanel || !userOutlineBtn) return;
|
||||
const items = buildUserOutlineTimelineItems(buildUserOutlineItems(), currentOutlineTitleHistory);
|
||||
const historyState = currentOutlineHistoryState;
|
||||
const historyStatus = historyState.loading
|
||||
? '<div class="user-outline-loading" role="status">正在加载完整消息列表…</div>'
|
||||
: historyState.error
|
||||
? '<button type="button" class="user-outline-loading user-outline-retry" data-outline-retry>加载旧消息失败,点击重试</button>'
|
||||
: historyState.cursor > 0 && !historyState.available
|
||||
? '<div class="user-outline-loading" role="status">更早消息的原始记录不可用</div>'
|
||||
: '';
|
||||
if (items.length === 0) {
|
||||
userOutlinePanel.innerHTML = '<div class="user-outline-empty">暂无用户消息</div>';
|
||||
userOutlineBtn.disabled = true;
|
||||
userOutlinePanel.innerHTML = historyStatus || '<div class="user-outline-empty">暂无用户消息</div>';
|
||||
userOutlineBtn.disabled = !historyState.loading
|
||||
&& !(historyState.cursor > 0 && historyState.available);
|
||||
} else {
|
||||
userOutlinePanel.innerHTML = items.map((item) => {
|
||||
userOutlinePanel.innerHTML = `${items.map((item) => {
|
||||
if (item.type === 'date') {
|
||||
return `<div class="user-outline-date" role="separator" aria-label="${escapeHtml(item.label)}"><span>${escapeHtml(item.label)}</span></div>`;
|
||||
}
|
||||
@@ -2031,15 +2154,17 @@
|
||||
`;
|
||||
}
|
||||
if (item.type === 'message') {
|
||||
const messageIndex = Number(item.messageIndex);
|
||||
const messageIndexAttr = Number.isFinite(messageIndex) ? ` data-message-index="${messageIndex}"` : '';
|
||||
return `
|
||||
<button type="button" class="user-outline-item" data-target="${escapeHtml(item.targetMessageId)}">
|
||||
<button type="button" class="user-outline-item" data-target="${escapeHtml(item.targetMessageId || '')}"${messageIndexAttr}>
|
||||
<span class="user-outline-index">${item.messageNumber}</span>
|
||||
<span class="user-outline-text">${escapeHtml(item.label)}</span>
|
||||
</button>
|
||||
`;
|
||||
}
|
||||
return '';
|
||||
}).join('');
|
||||
}).join('')}${historyStatus}`;
|
||||
userOutlineBtn.disabled = false;
|
||||
}
|
||||
}
|
||||
@@ -2057,11 +2182,45 @@
|
||||
closeCcwebPromptOutlinePanel();
|
||||
userOutlinePanel.hidden = false;
|
||||
userOutlineBtn.setAttribute('aria-expanded', 'true');
|
||||
requestOutlineHistoryPage();
|
||||
} else {
|
||||
closeUserOutlinePanel();
|
||||
}
|
||||
}
|
||||
|
||||
function requestOutlineMessageTarget(messageIndex) {
|
||||
const normalizedIndex = Number(messageIndex);
|
||||
if (!currentSessionId || !Number.isFinite(normalizedIndex) || normalizedIndex < 0) return;
|
||||
const target = messagesDiv.querySelector(
|
||||
`[data-session-message="true"][data-message-index="${Math.trunc(normalizedIndex)}"]`,
|
||||
);
|
||||
if (target) {
|
||||
target.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
return;
|
||||
}
|
||||
if (activeOutlineTargetRequest || !ws || ws.readyState !== 1 || !wsAuthenticated) return;
|
||||
pendingAdvancedSearchJump = {
|
||||
sessionId: currentSessionId,
|
||||
messageIndex: Math.trunc(normalizedIndex),
|
||||
query: '',
|
||||
};
|
||||
outlineHistoryRequestSeq += 1;
|
||||
const requestId = `outline-target-${Date.now()}-${outlineHistoryRequestSeq}`;
|
||||
activeOutlineTargetRequest = {
|
||||
sessionId: currentSessionId,
|
||||
requestId,
|
||||
messageIndex: Math.trunc(normalizedIndex),
|
||||
};
|
||||
closeUserOutlinePanel();
|
||||
send({
|
||||
type: 'load_history_page',
|
||||
sessionId: currentSessionId,
|
||||
before: Math.trunc(normalizedIndex) + 1,
|
||||
requestId,
|
||||
purpose: 'outline-target',
|
||||
});
|
||||
}
|
||||
|
||||
function scrollToMessage(anchorId) {
|
||||
if (!anchorId) return;
|
||||
const target = document.getElementById(anchorId);
|
||||
@@ -2403,7 +2562,7 @@
|
||||
return base + (snapshot.messages || []).reduce((sum, message) => sum + estimateSessionMessageWeight(message), 0);
|
||||
}
|
||||
|
||||
function normalizeSessionSnapshot(payload, options = {}) {
|
||||
function normalizeSessionSnapshot(payload) {
|
||||
const sessionId = payload.sessionId || payload.id || '';
|
||||
const messages = cloneMessages(payload.messages || []);
|
||||
const historyTotal = Number.isFinite(Number(payload.historyTotal))
|
||||
@@ -2412,6 +2571,16 @@
|
||||
const historyBaseIndex = Number.isFinite(Number(payload.historyBaseIndex))
|
||||
? Math.max(0, Number(payload.historyBaseIndex))
|
||||
: Math.max(0, historyTotal - messages.length);
|
||||
const historyBuffered = Number.isFinite(Number(payload.historyBuffered))
|
||||
? Math.max(0, Number(payload.historyBuffered))
|
||||
: messages.length;
|
||||
const historyCursor = Number.isFinite(Number(payload.historyCursor))
|
||||
? Math.max(0, Number(payload.historyCursor))
|
||||
: Math.max(0, historyBaseIndex);
|
||||
const historyTruncated = payload.historyTruncated === true
|
||||
|| historyCursor > 0
|
||||
|| historyBaseIndex > 0;
|
||||
const historyPending = !!payload.historyPending;
|
||||
return {
|
||||
sessionId,
|
||||
id: sessionId,
|
||||
@@ -2449,20 +2618,39 @@
|
||||
|| payload.taskTracking?.enabled === true,
|
||||
historyTotal,
|
||||
historyBaseIndex,
|
||||
historyBuffered: Number.isFinite(Number(payload.historyBuffered))
|
||||
? Math.max(0, Number(payload.historyBuffered))
|
||||
: messages.length,
|
||||
historyCursor: Number.isFinite(Number(payload.historyCursor))
|
||||
? Math.max(0, Number(payload.historyCursor))
|
||||
: Math.max(0, historyBaseIndex),
|
||||
historyTruncated: !!payload.historyTruncated,
|
||||
historyBuffered,
|
||||
historyCursor,
|
||||
historyTruncated,
|
||||
historyAvailable: payload.historyAvailable !== false,
|
||||
historySource: payload.historySource || 'snapshot',
|
||||
historyPending: !!payload.historyPending,
|
||||
complete: options.complete !== undefined ? !!options.complete : !payload.historyPending,
|
||||
historyPending,
|
||||
complete: !historyPending
|
||||
&& !historyTruncated
|
||||
&& historyCursor === 0
|
||||
&& historyBaseIndex === 0
|
||||
&& historyBuffered >= historyTotal,
|
||||
};
|
||||
}
|
||||
|
||||
function isCompleteSessionSnapshot(snapshot) {
|
||||
if (!snapshot || snapshot.complete !== true || snapshot.historyPending || snapshot.historyTruncated) return false;
|
||||
const historyTotal = Number.isFinite(Number(snapshot.historyTotal))
|
||||
? Math.max(0, Number(snapshot.historyTotal))
|
||||
: Array.isArray(snapshot.messages) ? snapshot.messages.length : 0;
|
||||
const historyBuffered = Number.isFinite(Number(snapshot.historyBuffered))
|
||||
? Math.max(0, Number(snapshot.historyBuffered))
|
||||
: Array.isArray(snapshot.messages) ? snapshot.messages.length : 0;
|
||||
const historyCursor = Number.isFinite(Number(snapshot.historyCursor))
|
||||
? Math.max(0, Number(snapshot.historyCursor))
|
||||
: 0;
|
||||
const historyBaseIndex = Number.isFinite(Number(snapshot.historyBaseIndex))
|
||||
? Math.max(0, Number(snapshot.historyBaseIndex))
|
||||
: 0;
|
||||
return historyCursor === 0
|
||||
&& historyBaseIndex === 0
|
||||
&& historyBuffered >= historyTotal;
|
||||
}
|
||||
|
||||
function touchSessionCache(sessionId) {
|
||||
const entry = sessionCache.get(sessionId);
|
||||
if (entry) entry.lastUsed = Date.now();
|
||||
@@ -2471,6 +2659,43 @@
|
||||
function invalidateSessionCache(sessionId) {
|
||||
if (!sessionId) return;
|
||||
sessionCache.delete(sessionId);
|
||||
sessionHistoryBuffers.delete(sessionId);
|
||||
}
|
||||
|
||||
function bufferSessionSnapshot(snapshot) {
|
||||
if (!snapshot?.sessionId || isCompleteSessionSnapshot(snapshot)) {
|
||||
if (snapshot?.sessionId) sessionHistoryBuffers.delete(snapshot.sessionId);
|
||||
return;
|
||||
}
|
||||
const bufferedSnapshot = deepClone(snapshot);
|
||||
const weight = estimateSessionSnapshotWeight(bufferedSnapshot);
|
||||
if (weight > SESSION_CACHE_MAX_WEIGHT) {
|
||||
sessionHistoryBuffers.delete(bufferedSnapshot.sessionId);
|
||||
return;
|
||||
}
|
||||
sessionHistoryBuffers.set(bufferedSnapshot.sessionId, {
|
||||
snapshot: bufferedSnapshot,
|
||||
weight,
|
||||
lastUsed: Date.now(),
|
||||
});
|
||||
while (sessionHistoryBuffers.size > SESSION_CACHE_LIMIT) {
|
||||
let oldestId = null;
|
||||
let oldestTs = Infinity;
|
||||
for (const [sessionId, entry] of sessionHistoryBuffers) {
|
||||
if ((entry.lastUsed || 0) < oldestTs) {
|
||||
oldestTs = entry.lastUsed || 0;
|
||||
oldestId = sessionId;
|
||||
}
|
||||
}
|
||||
if (!oldestId) break;
|
||||
sessionHistoryBuffers.delete(oldestId);
|
||||
}
|
||||
}
|
||||
|
||||
function getSessionHistoryBuffer(sessionId) {
|
||||
const entry = sessionHistoryBuffers.get(sessionId);
|
||||
if (entry) entry.lastUsed = Date.now();
|
||||
return entry?.snapshot || null;
|
||||
}
|
||||
|
||||
function pruneSessionCache() {
|
||||
@@ -2492,7 +2717,7 @@
|
||||
}
|
||||
|
||||
function cacheSessionSnapshot(snapshot) {
|
||||
if (!snapshot?.sessionId || !snapshot.complete) return;
|
||||
if (!snapshot?.sessionId || !isCompleteSessionSnapshot(snapshot)) return;
|
||||
const cachedSnapshot = deepClone(snapshot);
|
||||
const weight = estimateSessionSnapshotWeight(cachedSnapshot);
|
||||
if (weight > SESSION_CACHE_MAX_WEIGHT) {
|
||||
@@ -2507,6 +2732,7 @@
|
||||
weight,
|
||||
lastUsed: Date.now(),
|
||||
});
|
||||
sessionHistoryBuffers.delete(cachedSnapshot.sessionId);
|
||||
pruneSessionCache();
|
||||
}
|
||||
|
||||
@@ -2532,6 +2758,9 @@
|
||||
const meta = getSessionMeta(sessionId);
|
||||
entry.meta = meta ? deepClone(meta) : null;
|
||||
}
|
||||
for (const sessionId of sessionHistoryBuffers.keys()) {
|
||||
if (!knownIds.has(sessionId)) sessionHistoryBuffers.delete(sessionId);
|
||||
}
|
||||
}
|
||||
|
||||
function mergeSessionListSnapshot(snapshot) {
|
||||
@@ -2588,7 +2817,10 @@
|
||||
function getSessionCacheDisposition(sessionId) {
|
||||
const entry = sessionCache.get(sessionId);
|
||||
const meta = getSessionMeta(sessionId);
|
||||
if (!entry?.snapshot?.complete || !meta) return 'miss';
|
||||
if (!entry?.snapshot || !isCompleteSessionSnapshot(entry.snapshot) || !meta) {
|
||||
if (entry && !isCompleteSessionSnapshot(entry.snapshot)) sessionCache.delete(sessionId);
|
||||
return 'miss';
|
||||
}
|
||||
if (entry.version === (meta.updated || null) && !meta.hasUnread && !meta.isRunning && !meta.waitingOnChildren) {
|
||||
return 'strong';
|
||||
}
|
||||
@@ -6299,6 +6531,7 @@
|
||||
closeFileBrowser();
|
||||
currentSessionId = null;
|
||||
resetHistoryLoadState(null);
|
||||
resetOutlineHistoryState(null);
|
||||
syncTaskTrackingControl(null);
|
||||
loadedHistorySessionId = null;
|
||||
currentSessionMessageCount = 0;
|
||||
@@ -6326,7 +6559,9 @@
|
||||
updateReloadMcpButtonUI();
|
||||
// 真正替换聊天 DOM 时,使之前 renderMessages 的异步批次失效。
|
||||
renderEpoch++;
|
||||
messagesDiv.innerHTML = buildWelcomeMarkup(currentCwd);
|
||||
messagesDiv.innerHTML = '';
|
||||
if (historyLoadMore) messagesDiv.appendChild(historyLoadMore);
|
||||
messagesDiv.insertAdjacentHTML('beforeend', buildWelcomeMarkup(currentCwd));
|
||||
setStatsDisplay(null);
|
||||
renderPendingAttachments();
|
||||
renderPendingNotes({ scroll: false });
|
||||
@@ -6358,9 +6593,13 @@
|
||||
activeToolCalls.clear();
|
||||
activeTodoCallTargets.clear();
|
||||
}
|
||||
if (currentSessionId !== snapshot.sessionId) resetHistoryLoadState(snapshot.sessionId);
|
||||
if (currentSessionId !== snapshot.sessionId) {
|
||||
resetHistoryLoadState(snapshot.sessionId);
|
||||
resetOutlineHistoryState(snapshot.sessionId);
|
||||
}
|
||||
currentSessionId = snapshot.sessionId;
|
||||
syncHistoryLoadControl(snapshot);
|
||||
syncOutlineHistoryIndex(snapshot);
|
||||
syncTaskTrackingControl(snapshot);
|
||||
loadedHistorySessionId = snapshot.sessionId;
|
||||
currentOutlineTitleHistory = normalizeOutlineTitleHistory(snapshot.titleHistory);
|
||||
@@ -6651,27 +6890,96 @@
|
||||
});
|
||||
}
|
||||
|
||||
function requestOutlineHistoryPage() {
|
||||
const state = currentOutlineHistoryState;
|
||||
if (!state.sessionId || state.sessionId !== currentSessionId || state.cursor <= 0 || !state.available) return;
|
||||
if (activeOutlineHistoryRequest || !ws || ws.readyState !== 1 || !wsAuthenticated) return;
|
||||
outlineHistoryRequestSeq += 1;
|
||||
const requestId = `outline-history-${Date.now()}-${outlineHistoryRequestSeq}`;
|
||||
activeOutlineHistoryRequest = {
|
||||
sessionId: state.sessionId,
|
||||
requestId,
|
||||
before: state.cursor,
|
||||
};
|
||||
currentOutlineHistoryState = { ...state, loading: true, error: '' };
|
||||
updateUserOutlinePanel();
|
||||
send({
|
||||
type: 'load_history_page',
|
||||
sessionId: state.sessionId,
|
||||
before: state.cursor,
|
||||
requestId,
|
||||
purpose: 'outline',
|
||||
});
|
||||
}
|
||||
|
||||
function mergeHistoryChunkIntoCachedSnapshot(sessionId, messages, options = {}) {
|
||||
if (!sessionId || !Array.isArray(messages) || messages.length === 0) return;
|
||||
updateCachedSession(sessionId, (snapshot) => {
|
||||
const existingMessages = Array.isArray(snapshot.messages) ? snapshot.messages : [];
|
||||
const existingBase = Number.isFinite(Number(snapshot.historyBaseIndex))
|
||||
? Number(snapshot.historyBaseIndex)
|
||||
: Math.max(0, Number(snapshot.historyTotal || existingMessages.length) - existingMessages.length);
|
||||
const incomingBase = Number.isFinite(Number(options.baseIndex)) ? Number(options.baseIndex) : 0;
|
||||
const indexed = new Map();
|
||||
existingMessages.forEach((message, index) => indexed.set(existingBase + index, message));
|
||||
messages.forEach((message, index) => indexed.set(incomingBase + index, message));
|
||||
const indexes = Array.from(indexed.keys()).sort((a, b) => a - b);
|
||||
snapshot.messages = indexes.map((index) => indexed.get(index));
|
||||
snapshot.historyBaseIndex = indexes.length > 0 ? indexes[0] : incomingBase;
|
||||
snapshot.historyBuffered = snapshot.messages.length;
|
||||
if (Number.isFinite(Number(options.total))) snapshot.historyTotal = Math.max(0, Number(options.total));
|
||||
if (Number.isFinite(Number(options.cursor))) snapshot.historyCursor = Math.max(0, Number(options.cursor));
|
||||
snapshot.historyTruncated = snapshot.historyCursor > 0;
|
||||
const cachedSnapshot = sessionCache.get(sessionId)?.snapshot || null;
|
||||
const bufferedSnapshot = getSessionHistoryBuffer(sessionId);
|
||||
const snapshot = cachedSnapshot || bufferedSnapshot;
|
||||
if (!snapshot) return;
|
||||
const existingMessages = Array.isArray(snapshot.messages) ? snapshot.messages : [];
|
||||
const existingBase = Number.isFinite(Number(snapshot.historyBaseIndex))
|
||||
? Number(snapshot.historyBaseIndex)
|
||||
: Math.max(0, Number(snapshot.historyTotal || existingMessages.length) - existingMessages.length);
|
||||
const incomingBase = Number.isFinite(Number(options.baseIndex)) ? Number(options.baseIndex) : 0;
|
||||
const indexed = new Map();
|
||||
existingMessages.forEach((message, index) => indexed.set(existingBase + index, message));
|
||||
messages.forEach((message, index) => indexed.set(incomingBase + index, message));
|
||||
const indexes = Array.from(indexed.keys()).sort((a, b) => a - b);
|
||||
snapshot.messages = indexes.map((index) => indexed.get(index));
|
||||
snapshot.historyBaseIndex = indexes.length > 0 ? indexes[0] : incomingBase;
|
||||
snapshot.historyBuffered = snapshot.messages.length;
|
||||
if (Number.isFinite(Number(options.total))) snapshot.historyTotal = Math.max(0, Number(options.total));
|
||||
if (Number.isFinite(Number(options.cursor))) snapshot.historyCursor = Math.max(0, Number(options.cursor));
|
||||
snapshot.historyTruncated = snapshot.historyCursor > 0 || snapshot.historyBaseIndex > 0;
|
||||
snapshot.historyPending = false;
|
||||
snapshot.complete = snapshot.historyCursor === 0
|
||||
&& !snapshot.historyTruncated
|
||||
&& snapshot.historyBuffered >= snapshot.historyTotal;
|
||||
if (isCompleteSessionSnapshot(snapshot)) {
|
||||
cacheSessionSnapshot(snapshot);
|
||||
} else {
|
||||
if (cachedSnapshot) sessionCache.delete(sessionId);
|
||||
bufferSessionSnapshot(snapshot);
|
||||
}
|
||||
}
|
||||
|
||||
function mergeHistoryChunkIntoSessionLoadSnapshot(snapshot, messages, options = {}) {
|
||||
if (!snapshot || !Array.isArray(messages) || messages.length === 0) return;
|
||||
const existingMessages = Array.isArray(snapshot.messages) ? snapshot.messages : [];
|
||||
const existingBase = Number.isFinite(Number(snapshot.historyBaseIndex))
|
||||
? Number(snapshot.historyBaseIndex)
|
||||
: Math.max(0, Number(snapshot.historyTotal || existingMessages.length) - existingMessages.length);
|
||||
const incomingBase = Number.isFinite(Number(options.baseIndex)) ? Number(options.baseIndex) : 0;
|
||||
const indexed = new Map();
|
||||
existingMessages.forEach((message, index) => indexed.set(existingBase + index, message));
|
||||
messages.forEach((message, index) => indexed.set(incomingBase + index, message));
|
||||
const indexes = Array.from(indexed.keys()).sort((a, b) => a - b);
|
||||
snapshot.messages = indexes.map((index) => indexed.get(index));
|
||||
snapshot.historyBaseIndex = indexes.length > 0 ? indexes[0] : incomingBase;
|
||||
snapshot.historyBuffered = snapshot.messages.length;
|
||||
if (Number.isFinite(Number(options.total))) {
|
||||
snapshot.historyTotal = Math.max(0, Number(options.total));
|
||||
}
|
||||
if (Number.isFinite(Number(options.cursor))) {
|
||||
snapshot.historyCursor = Math.max(0, Number(options.cursor));
|
||||
} else if (options.lastChunk) {
|
||||
snapshot.historyCursor = Math.max(0, snapshot.historyBaseIndex);
|
||||
}
|
||||
if (options.lastChunk) {
|
||||
snapshot.historyPending = false;
|
||||
snapshot.complete = snapshot.historyCursor === 0;
|
||||
});
|
||||
snapshot.historyTruncated = snapshot.historyCursor > 0 || snapshot.historyBaseIndex > 0;
|
||||
if (!Number.isFinite(Number(snapshot.historyTotal))) {
|
||||
snapshot.historyTotal = snapshot.messages.length;
|
||||
}
|
||||
snapshot.complete = snapshot.historyCursor === 0
|
||||
&& !snapshot.historyTruncated
|
||||
&& snapshot.historyBuffered >= snapshot.historyTotal;
|
||||
} else {
|
||||
snapshot.historyPending = true;
|
||||
snapshot.complete = false;
|
||||
}
|
||||
}
|
||||
|
||||
function createSessionSwitchRequestId(sessionId) {
|
||||
@@ -6716,7 +7024,6 @@
|
||||
return;
|
||||
}
|
||||
if (activeSessionLoad?.sessionId === sessionId && activeSessionLoad.snapshot) {
|
||||
activeSessionLoad.snapshot.complete = true;
|
||||
cacheSessionSnapshot(activeSessionLoad.snapshot);
|
||||
}
|
||||
finishSessionSwitch(sessionId, requestId);
|
||||
@@ -7356,6 +7663,41 @@
|
||||
reconnectTimer = null;
|
||||
}
|
||||
|
||||
function clearWsHeartbeat() {
|
||||
if (wsHeartbeatTimer) {
|
||||
clearInterval(wsHeartbeatTimer);
|
||||
wsHeartbeatTimer = null;
|
||||
}
|
||||
wsHeartbeatPending = null;
|
||||
}
|
||||
|
||||
function sendWsHeartbeat(socket) {
|
||||
if (!wsSupportsClientHeartbeat || ws !== socket || socket.readyState !== 1 || !wsAuthenticated) return;
|
||||
const now = Date.now();
|
||||
if (wsHeartbeatPending) {
|
||||
if (now - wsHeartbeatPending.sentAt <= WS_CLIENT_HEARTBEAT_TIMEOUT_MS) return;
|
||||
wsHeartbeatPending = null;
|
||||
try { socket.close(4000, 'heartbeat timeout'); } catch {}
|
||||
return;
|
||||
}
|
||||
wsHeartbeatSeq += 1;
|
||||
const requestId = `client-heartbeat-${Date.now()}-${wsHeartbeatSeq}`;
|
||||
wsHeartbeatPending = { requestId, sentAt: now };
|
||||
try {
|
||||
socket.send(JSON.stringify({ type: 'client_heartbeat', requestId }));
|
||||
} catch {
|
||||
wsHeartbeatPending = null;
|
||||
try { socket.close(4000, 'heartbeat send failed'); } catch {}
|
||||
}
|
||||
}
|
||||
|
||||
function startWsHeartbeat(socket) {
|
||||
clearWsHeartbeat();
|
||||
if (!wsSupportsClientHeartbeat) return;
|
||||
sendWsHeartbeat(socket);
|
||||
wsHeartbeatTimer = setInterval(() => sendWsHeartbeat(socket), WS_CLIENT_HEARTBEAT_INTERVAL_MS);
|
||||
}
|
||||
|
||||
function connect() {
|
||||
if (!canConnectWs()) return;
|
||||
if (ws && ws.readyState <= 1) return;
|
||||
@@ -7364,6 +7706,7 @@
|
||||
const socket = new WebSocket(WS_URL);
|
||||
ws = socket;
|
||||
wsAuthenticated = false;
|
||||
wsSupportsClientHeartbeat = false;
|
||||
|
||||
socket.onopen = () => {
|
||||
if (ws !== socket) return;
|
||||
@@ -7390,6 +7733,27 @@
|
||||
if (ws !== socket) return;
|
||||
ws = null;
|
||||
wsAuthenticated = false;
|
||||
wsSupportsClientHeartbeat = false;
|
||||
clearWsHeartbeat();
|
||||
if (activeOutlineHistoryRequest) {
|
||||
activeOutlineHistoryRequest = null;
|
||||
currentOutlineHistoryState = {
|
||||
...currentOutlineHistoryState,
|
||||
loading: false,
|
||||
error: '连接中断,连接恢复后可重试',
|
||||
};
|
||||
if (userOutlinePanel && !userOutlinePanel.hidden) updateUserOutlinePanel();
|
||||
}
|
||||
if (activeOutlineTargetRequest) {
|
||||
activeOutlineTargetRequest = null;
|
||||
pendingAdvancedSearchJump = null;
|
||||
if (!isPageUnloading) {
|
||||
appendError('连接中断,定位消息加载失败;连接恢复后请重新点击该条目。', {
|
||||
transient: true,
|
||||
autoDismissMs: 7000,
|
||||
});
|
||||
}
|
||||
}
|
||||
if (activeHistoryPageRequest) {
|
||||
activeHistoryPageRequest = null;
|
||||
currentHistoryState = {
|
||||
@@ -7408,6 +7772,11 @@
|
||||
recoverCurrent: activeSessionLoad.recoverCurrent === true,
|
||||
targetMessageIndex: activeSessionLoad.targetMessageIndex,
|
||||
};
|
||||
const now = Date.now();
|
||||
if (now - lastWsDisconnectNoticeAt >= 5000) {
|
||||
lastWsDisconnectNoticeAt = now;
|
||||
showToast('连接中断,正在重连并自动重试会话切换…');
|
||||
}
|
||||
} else if (currentSessionId && (isGenerating || currentSessionRunning) && !isPageUnloading) {
|
||||
pendingSessionResumeRequest = {
|
||||
sessionId: currentSessionId,
|
||||
@@ -7490,6 +7859,12 @@
|
||||
function handleServerMessage(msg) {
|
||||
if (handleTaskBoardProtocolMessage(msg)) return;
|
||||
switch (msg.type) {
|
||||
case 'client_heartbeat_ack':
|
||||
if (wsHeartbeatPending?.requestId === String(msg.requestId || '')) {
|
||||
wsHeartbeatPending = null;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'auth_result':
|
||||
if (msg.success) {
|
||||
if (shouldReloadForFrontendAssetVersion(msg.frontendAssetVersion)) {
|
||||
@@ -7499,6 +7874,8 @@
|
||||
const shouldLoadInitialSession = !initialSessionListHandled && !currentSessionId;
|
||||
authToken = msg.token;
|
||||
wsAuthenticated = true;
|
||||
wsSupportsClientHeartbeat = msg.features?.clientHeartbeat === true;
|
||||
startWsHeartbeat(ws);
|
||||
localStorage.setItem('cc-web-token', msg.token);
|
||||
document.dispatchEvent(new CustomEvent('cc-web-auth-restored'));
|
||||
loginOverlay.hidden = true;
|
||||
@@ -7525,6 +7902,8 @@
|
||||
pendingInitialSessionLoad = shouldLoadInitialSession;
|
||||
}
|
||||
} else {
|
||||
clearWsHeartbeat();
|
||||
wsSupportsClientHeartbeat = false;
|
||||
pendingSessionSwitchRequest = null;
|
||||
pendingSessionResumeRequest = null;
|
||||
clearSessionResumeFallbackTimer();
|
||||
@@ -7621,6 +8000,11 @@
|
||||
break;
|
||||
}
|
||||
mergeSessionListSnapshot(snapshot);
|
||||
if (isCompleteSessionSnapshot(snapshot)) {
|
||||
sessionHistoryBuffers.delete(snapshot.sessionId);
|
||||
} else {
|
||||
bufferSessionSnapshot(snapshot);
|
||||
}
|
||||
if (matchesActiveLoad) {
|
||||
activeSessionLoad.snapshot = snapshot;
|
||||
}
|
||||
@@ -7659,13 +8043,77 @@
|
||||
const matchesManualHistoryLoad = !!(typeof activeHistoryPageRequest !== 'undefined' && activeHistoryPageRequest
|
||||
&& activeHistoryPageRequest.sessionId === msg.sessionId
|
||||
&& activeHistoryPageRequest.requestId === historyRequestId);
|
||||
const matchesOutlineHistoryLoad = !!(activeOutlineHistoryRequest
|
||||
&& activeOutlineHistoryRequest.sessionId === msg.sessionId
|
||||
&& activeOutlineHistoryRequest.requestId === historyRequestId);
|
||||
const matchesOutlineTargetLoad = !!(activeOutlineTargetRequest
|
||||
&& activeOutlineTargetRequest.sessionId === msg.sessionId
|
||||
&& activeOutlineTargetRequest.requestId === historyRequestId);
|
||||
const allowsLegacyHistory = !historyRequestId
|
||||
&& msg.sessionId === currentSessionId
|
||||
&& loadedHistorySessionId === msg.sessionId;
|
||||
if (matchesOutlineTargetLoad) {
|
||||
if (msg.sessionId === currentSessionId && loadedHistorySessionId === msg.sessionId) {
|
||||
const baseIndex = Number.isFinite(Number(msg.historyBaseIndex))
|
||||
? Number(msg.historyBaseIndex)
|
||||
: 0;
|
||||
prependHistoryMessages(msg.messages || [], {
|
||||
preserveScroll: false,
|
||||
baseIndex,
|
||||
});
|
||||
mergeOutlineHistoryMessages(msg.messages || [], baseIndex);
|
||||
mergeHistoryChunkIntoCachedSnapshot(msg.sessionId, msg.messages || [], {
|
||||
baseIndex,
|
||||
total: msg.historyTotal,
|
||||
});
|
||||
activeOutlineTargetRequest = null;
|
||||
scheduleAdvancedSearchJump();
|
||||
if (userOutlinePanel && !userOutlinePanel.hidden) updateUserOutlinePanel();
|
||||
} else {
|
||||
activeOutlineTargetRequest = null;
|
||||
pendingAdvancedSearchJump = null;
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (matchesOutlineHistoryLoad) {
|
||||
if (msg.sessionId === currentSessionId && currentOutlineHistoryState.sessionId === msg.sessionId) {
|
||||
const nextCursor = Number.isFinite(Number(msg.historyCursor))
|
||||
? Math.max(0, Number(msg.historyCursor))
|
||||
: Math.max(0, activeOutlineHistoryRequest.before - (msg.messages || []).length);
|
||||
mergeOutlineHistoryMessages(
|
||||
msg.messages || [],
|
||||
Number.isFinite(Number(msg.historyBaseIndex)) ? Number(msg.historyBaseIndex) : 0,
|
||||
);
|
||||
activeOutlineHistoryRequest = null;
|
||||
currentOutlineHistoryState = {
|
||||
...currentOutlineHistoryState,
|
||||
cursor: nextCursor,
|
||||
total: Number.isFinite(Number(msg.historyTotal))
|
||||
? Math.max(currentOutlineHistoryState.total, Number(msg.historyTotal))
|
||||
: currentOutlineHistoryState.total,
|
||||
available: msg.historyAvailable !== false,
|
||||
loading: false,
|
||||
complete: nextCursor === 0,
|
||||
error: '',
|
||||
};
|
||||
if (userOutlinePanel && !userOutlinePanel.hidden) updateUserOutlinePanel();
|
||||
if (nextCursor > 0) requestOutlineHistoryPage();
|
||||
} else {
|
||||
activeOutlineHistoryRequest = null;
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (activeSessionLoad?.recoverCurrent && matchesActiveHistoryLoad) {
|
||||
if (activeSessionLoad.snapshot) {
|
||||
activeSessionLoad.snapshot.messages = cloneMessages(msg.messages || [])
|
||||
.concat(activeSessionLoad.snapshot.messages);
|
||||
mergeHistoryChunkIntoSessionLoadSnapshot(activeSessionLoad.snapshot, msg.messages || [], {
|
||||
baseIndex: Number.isFinite(Number(msg.historyBaseIndex)) ? Number(msg.historyBaseIndex) : 0,
|
||||
cursor: msg.historyCursor,
|
||||
total: msg.historyTotal,
|
||||
lastChunk: !msg.remaining,
|
||||
});
|
||||
if (!isCompleteSessionSnapshot(activeSessionLoad.snapshot)) {
|
||||
bufferSessionSnapshot(activeSessionLoad.snapshot);
|
||||
}
|
||||
}
|
||||
if (!msg.remaining) finalizeLoadedSession(msg.sessionId, historyRequestId || undefined);
|
||||
break;
|
||||
@@ -7675,13 +8123,25 @@
|
||||
&& loadedHistorySessionId === msg.sessionId) {
|
||||
const blocking = isBlockingSessionLoad(msg.sessionId);
|
||||
if (activeSessionLoad?.sessionId === msg.sessionId && activeSessionLoad.snapshot) {
|
||||
activeSessionLoad.snapshot.messages = cloneMessages(msg.messages || []).concat(activeSessionLoad.snapshot.messages);
|
||||
mergeHistoryChunkIntoSessionLoadSnapshot(activeSessionLoad.snapshot, msg.messages || [], {
|
||||
baseIndex: Number.isFinite(Number(msg.historyBaseIndex)) ? Number(msg.historyBaseIndex) : 0,
|
||||
cursor: msg.historyCursor,
|
||||
total: msg.historyTotal,
|
||||
lastChunk: !msg.remaining,
|
||||
});
|
||||
if (!isCompleteSessionSnapshot(activeSessionLoad.snapshot)) {
|
||||
bufferSessionSnapshot(activeSessionLoad.snapshot);
|
||||
}
|
||||
}
|
||||
prependHistoryMessages(msg.messages || [], {
|
||||
preserveScroll: !blocking,
|
||||
skipScrollbar: blocking,
|
||||
baseIndex: Number.isFinite(Number(msg.historyBaseIndex)) ? Number(msg.historyBaseIndex) : 0,
|
||||
});
|
||||
mergeOutlineHistoryMessages(
|
||||
msg.messages || [],
|
||||
Number.isFinite(Number(msg.historyBaseIndex)) ? Number(msg.historyBaseIndex) : 0,
|
||||
);
|
||||
scheduleAdvancedSearchJump();
|
||||
if (matchesManualHistoryLoad) {
|
||||
const nextCursor = Number.isFinite(Number(msg.historyCursor))
|
||||
@@ -8004,7 +8464,40 @@
|
||||
break;
|
||||
|
||||
case 'error':
|
||||
// 兼容尚未重启的旧服务端:它会把新客户端心跳当成未知消息。
|
||||
// 该错误不代表业务请求失败,不能污染聊天区或触发切换失败提示。
|
||||
if (/^Unknown type:\s*client_heartbeat\s*$/i.test(String(msg.message || ''))) {
|
||||
wsSupportsClientHeartbeat = false;
|
||||
wsHeartbeatPending = null;
|
||||
clearWsHeartbeat();
|
||||
break;
|
||||
}
|
||||
const errorRequestId = String(msg.requestId || '');
|
||||
const matchesOutlineTargetError = !!(activeOutlineTargetRequest
|
||||
&& (!msg.sessionId || msg.sessionId === activeOutlineTargetRequest.sessionId)
|
||||
&& errorRequestId === activeOutlineTargetRequest.requestId);
|
||||
if (matchesOutlineTargetError) {
|
||||
activeOutlineTargetRequest = null;
|
||||
pendingAdvancedSearchJump = null;
|
||||
appendError(msg.message || '加载定位消息失败,请重试', {
|
||||
transient: true,
|
||||
autoDismissMs: 7000,
|
||||
});
|
||||
break;
|
||||
}
|
||||
const matchesOutlineHistoryError = !!(activeOutlineHistoryRequest
|
||||
&& (!msg.sessionId || msg.sessionId === activeOutlineHistoryRequest.sessionId)
|
||||
&& errorRequestId === activeOutlineHistoryRequest.requestId);
|
||||
if (matchesOutlineHistoryError) {
|
||||
activeOutlineHistoryRequest = null;
|
||||
currentOutlineHistoryState = {
|
||||
...currentOutlineHistoryState,
|
||||
loading: false,
|
||||
error: msg.message || '加载定位历史失败,请重试',
|
||||
};
|
||||
if (userOutlinePanel && !userOutlinePanel.hidden) updateUserOutlinePanel();
|
||||
break;
|
||||
}
|
||||
const matchesHistoryPageError = !!(typeof activeHistoryPageRequest !== 'undefined' && activeHistoryPageRequest
|
||||
&& (!msg.sessionId || msg.sessionId === activeHistoryPageRequest.sessionId)
|
||||
&& errorRequestId === activeHistoryPageRequest.requestId);
|
||||
@@ -10214,9 +10707,10 @@
|
||||
collabAgentIdsByToolUseId = new Map();
|
||||
closedCollabAgentIdsByToolUseId = new Map();
|
||||
messagesDiv.innerHTML = '';
|
||||
if (historyLoadMore) messagesDiv.appendChild(historyLoadMore);
|
||||
clearUserMessageIndex();
|
||||
if (messages.length === 0) {
|
||||
messagesDiv.innerHTML = buildWelcomeMarkup(currentCwd);
|
||||
messagesDiv.insertAdjacentHTML('beforeend', buildWelcomeMarkup(currentCwd));
|
||||
updateUserOutlinePanel();
|
||||
renderPendingNotes({ scroll: false });
|
||||
scrollToBottom();
|
||||
@@ -10265,7 +10759,7 @@
|
||||
const prevScrollTop = messagesDiv.scrollTop;
|
||||
const frag = document.createDocumentFragment();
|
||||
for (let i = start; i < end; i++) frag.appendChild(buildMsgElement(messages[i], baseIndex + i));
|
||||
messagesDiv.insertBefore(frag, messagesDiv.firstChild);
|
||||
messagesDiv.insertBefore(frag, historyLoadMore?.nextSibling || null);
|
||||
updateUserOutlinePanel();
|
||||
// Compensate scrollTop so visible area stays unchanged
|
||||
messagesDiv.scrollTop = prevScrollTop + (messagesDiv.scrollHeight - prevHeight);
|
||||
@@ -10296,14 +10790,14 @@
|
||||
const frag = document.createDocumentFragment();
|
||||
uniqueMessages.forEach(({ message, index }) => frag.appendChild(buildMsgElement(message, baseIndex + index)));
|
||||
if (!preserveScroll) {
|
||||
messagesDiv.insertBefore(frag, messagesDiv.firstChild);
|
||||
messagesDiv.insertBefore(frag, historyLoadMore?.nextSibling || null);
|
||||
updateUserOutlinePanel();
|
||||
if (!skipScrollbar) updateScrollbar();
|
||||
return;
|
||||
}
|
||||
const prevHeight = messagesDiv.scrollHeight;
|
||||
const prevScrollTop = messagesDiv.scrollTop;
|
||||
messagesDiv.insertBefore(frag, messagesDiv.firstChild);
|
||||
messagesDiv.insertBefore(frag, historyLoadMore?.nextSibling || null);
|
||||
updateUserOutlinePanel();
|
||||
messagesDiv.scrollTop = prevScrollTop + (messagesDiv.scrollHeight - prevHeight);
|
||||
if (!skipScrollbar) updateScrollbar();
|
||||
@@ -11315,8 +11809,14 @@
|
||||
}
|
||||
|
||||
function highlightActiveSession() {
|
||||
let activeAssigned = false;
|
||||
document.querySelectorAll('.session-item').forEach((el) => {
|
||||
el.classList.toggle('active', el.dataset.id === currentSessionId);
|
||||
const isTarget = el.dataset.id === currentSessionId;
|
||||
const isActive = isTarget && !activeAssigned;
|
||||
if (isActive) activeAssigned = true;
|
||||
el.classList.toggle('active', isActive);
|
||||
if (isActive) el.setAttribute('aria-current', 'true');
|
||||
else el.removeAttribute('aria-current');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -12036,11 +12536,19 @@
|
||||
toggleUserOutlinePanel();
|
||||
});
|
||||
userOutlinePanel.addEventListener('click', (e) => {
|
||||
const retry = e.target instanceof HTMLElement ? e.target.closest('[data-outline-retry]') : null;
|
||||
if (retry) {
|
||||
e.preventDefault();
|
||||
requestOutlineHistoryPage();
|
||||
return;
|
||||
}
|
||||
const target = e.target instanceof HTMLElement ? e.target.closest('.user-outline-item') : null;
|
||||
if (!target) return;
|
||||
const anchorId = target.getAttribute('data-target') || '';
|
||||
const messageIndex = Number(target.getAttribute('data-message-index'));
|
||||
closeUserOutlinePanel();
|
||||
scrollToMessage(anchorId);
|
||||
if (anchorId) scrollToMessage(anchorId);
|
||||
else if (Number.isFinite(messageIndex)) requestOutlineMessageTarget(messageIndex);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user