diff --git a/config/cross-conversation-replies.json b/config/cross-conversation-replies.json
index 6e823b1..210cacd 100644
--- a/config/cross-conversation-replies.json
+++ b/config/cross-conversation-replies.json
@@ -1,5 +1,5 @@
{
"version": 1,
- "updatedAt": "2026-06-30T15:42:26.421Z",
+ "updatedAt": "2026-07-02T06:02:53.126Z",
"replies": []
}
\ No newline at end of file
diff --git a/dist-exe/cc-web-bun-linux-x64-baseline.tar.gz b/dist-exe/cc-web-bun-linux-x64-baseline.tar.gz
index 0e2c3e1..557e9d8 100644
Binary files a/dist-exe/cc-web-bun-linux-x64-baseline.tar.gz and b/dist-exe/cc-web-bun-linux-x64-baseline.tar.gz differ
diff --git a/lib/agent-runtime.js b/lib/agent-runtime.js
index f955b7f..16d1c23 100644
--- a/lib/agent-runtime.js
+++ b/lib/agent-runtime.js
@@ -23,6 +23,32 @@ function createAgentRuntime(deps) {
getRuntimeSessionId,
} = deps;
+ function readRuntimePositiveIntEnv(name, fallback, options = {}) {
+ const raw = Number.parseInt(String(processEnv?.[name] || ''), 10);
+ const min = Number.isFinite(options.min) ? options.min : 1;
+ const max = Number.isFinite(options.max) ? options.max : Number.MAX_SAFE_INTEGER;
+ if (!Number.isFinite(raw) || raw <= 0) return fallback;
+ return Math.max(min, Math.min(max, raw));
+ }
+
+ const RUNTIME_FULL_TEXT_MAX_CHARS = readRuntimePositiveIntEnv(
+ 'CC_WEB_RUNTIME_FULL_TEXT_MAX_CHARS',
+ 256 * 1024,
+ { min: 4096 },
+ );
+ const RUNTIME_TRUNCATED_HEAD = '[cc-web: 前文过长,已保留尾部以保护服务稳定性]\n';
+
+ function keepTail(value, maxLen) {
+ const text = String(value || '');
+ if (!Number.isFinite(maxLen) || maxLen <= 0 || text.length <= maxLen) return text;
+ const keep = Math.max(0, maxLen - RUNTIME_TRUNCATED_HEAD.length);
+ return `${RUNTIME_TRUNCATED_HEAD}${text.slice(-keep)}`;
+ }
+
+ function appendCappedText(current, addition, maxLen = RUNTIME_FULL_TEXT_MAX_CHARS) {
+ return keepTail(`${String(current || '')}${String(addition || '')}`, maxLen);
+ }
+
function tomlString(value) {
return JSON.stringify(String(value || ''));
}
@@ -330,7 +356,7 @@ function createAgentRuntime(deps) {
? (/\n\s*$/.test(currentText) ? `\n${createAgentMessageDivider()}\n\n` : `\n\n${createAgentMessageDivider()}\n\n`)
: '';
const chunk = separator + nextText;
- entry.fullText += chunk;
+ entry.fullText = appendCappedText(entry.fullText || '', chunk);
return chunk;
}
@@ -383,7 +409,7 @@ function createAgentRuntime(deps) {
for (const block of content) {
if (block.type === 'text' && block.text) {
- entry.fullText += block.text;
+ entry.fullText = appendCappedText(entry.fullText || '', block.text);
sendRuntime(entry, sessionId, { type: 'text_delta', text: block.text });
} else if (block.type === 'tool_use') {
const toolInput = sanitizeToolInput(block.name, block.input);
diff --git a/public/app.js b/public/app.js
index 24859aa..abfd051 100644
--- a/public/app.js
+++ b/public/app.js
@@ -2,7 +2,7 @@
(function () {
'use strict';
- const ASSET_VERSION = '20260629-ccweb-prompt-dark-theme';
+ const ASSET_VERSION = '20260702-visible-no-rerender';
const WS_URL = `${location.protocol === 'https:' ? 'wss' : 'ws'}://${location.host}/ws`;
const RENDER_DEBOUNCE = 100;
const COMPOSER_SUGGESTION_DEBOUNCE = 120;
@@ -76,6 +76,8 @@
const OLD_SESSION_COLLAPSE_DAYS = 7;
const OLD_SESSION_COLLAPSE_MS = OLD_SESSION_COLLAPSE_DAYS * 24 * 60 * 60 * 1000;
const SESSION_LOAD_OVERLAY_TIMEOUT_MS = 12_000;
+ const SESSION_LOAD_REQUEST_TIMEOUT_MS = 45_000;
+ const SESSION_RESUME_FALLBACK_MS = 1_500;
const MODEL_OPTIONS = [
{ value: 'opus', label: 'Opus', desc: '最强大,1M 上下文' },
@@ -183,6 +185,8 @@
let loadedHistorySessionId = null;
let activeSessionLoad = null;
let sessionLoadOverlayTimer = null;
+ let sessionLoadRequestTimer = null;
+ let sessionResumeFallbackTimer = null;
let sidebarSwipe = null;
let activeComposerToken = null;
let composerSuggestionTimer = null;
@@ -202,6 +206,7 @@
let codexAppApprovalModal = null;
let pendingNewSessionRequest = null;
let pendingSessionSwitchRequest = null;
+ let pendingSessionResumeRequest = null;
let sessionSwitchRequestSeq = 0;
let skipDeleteConfirm = localStorage.getItem('cc-web-skip-delete-confirm') === '1';
let pendingInitialSessionLoad = false;
@@ -4126,7 +4131,11 @@
if (fileBrowserState && (fileBrowserState.sessionId !== snapshot.sessionId || (snapshot.cwd && fileBrowserState.rootPath !== snapshot.cwd))) {
closeFileBrowser();
}
- const preserveStreaming = !!(options.preserveStreaming && isGenerating && snapshot.sessionId === currentSessionId && snapshot.isRunning);
+ const hasStreamingElement = !!document.getElementById('streaming-msg');
+ const preserveStreaming = !!(options.preserveStreaming &&
+ snapshot.sessionId === currentSessionId &&
+ snapshot.isRunning &&
+ (isGenerating || currentSessionRunning || hasStreamingElement));
if (isGenerating && !preserveStreaming) {
isGenerating = false;
generatingSessionId = null;
@@ -4220,6 +4229,59 @@
sessionLoadOverlayTimer = null;
}
+ function clearSessionLoadRequestTimer() {
+ if (!sessionLoadRequestTimer) return;
+ clearTimeout(sessionLoadRequestTimer);
+ sessionLoadRequestTimer = null;
+ }
+
+ function clearSessionResumeFallbackTimer() {
+ if (!sessionResumeFallbackTimer) return;
+ clearTimeout(sessionResumeFallbackTimer);
+ sessionResumeFallbackTimer = null;
+ }
+
+ function clearPendingSessionSwitchRequest(sessionId, requestId) {
+ if (!pendingSessionSwitchRequest) return;
+ if (sessionId && pendingSessionSwitchRequest.sessionId !== sessionId) return;
+ if (requestId && pendingSessionSwitchRequest.requestId !== requestId) return;
+ pendingSessionSwitchRequest = null;
+ }
+
+ function clearPendingSessionResumeRequest(sessionId, requestId) {
+ if (!pendingSessionResumeRequest) return;
+ if (sessionId && pendingSessionResumeRequest.sessionId !== sessionId) return;
+ if (requestId && pendingSessionResumeRequest.requestId !== requestId) return;
+ pendingSessionResumeRequest = null;
+ clearSessionResumeFallbackTimer();
+ }
+
+ function notifySessionLoadTimeout(sessionId) {
+ const meta = sessionId ? getSessionMeta(sessionId) : null;
+ const title = meta?.title ? `“${meta.title}”` : '所选会话';
+ appendError(`${title} 加载超时,已取消本次切换。若服务刚重启或网络恢复后,可重新点击该会话。`, {
+ transient: true,
+ autoDismissMs: 9000,
+ preserveScroll: false,
+ });
+ }
+
+ function scheduleSessionLoadRequestTimeout(sessionId, requestId) {
+ clearSessionLoadRequestTimer();
+ if (!sessionId || !requestId) return;
+ sessionLoadRequestTimer = setTimeout(() => {
+ sessionLoadRequestTimer = null;
+ if (!activeSessionLoad ||
+ activeSessionLoad.sessionId !== sessionId ||
+ activeSessionLoad.requestId !== requestId) {
+ return;
+ }
+ clearPendingSessionSwitchRequest(sessionId, requestId);
+ clearSessionLoading(sessionId);
+ notifySessionLoadTimeout(sessionId);
+ }, SESSION_LOAD_REQUEST_TIMEOUT_MS);
+ }
+
function releaseSessionLoadingOverlay({ keepActiveLoad = true, allowRetry = false } = {}) {
clearSessionLoadOverlayTimer();
document.body.classList.remove('session-loading-active');
@@ -4236,10 +4298,19 @@
function setSessionLoading(sessionId, options = {}) {
clearSessionLoadOverlayTimer();
+ clearSessionLoadRequestTimer();
const loading = !!sessionId;
const blocking = options.blocking !== false;
const requestId = loading ? (options.requestId || createSessionSwitchRequestId(sessionId)) : '';
- activeSessionLoad = loading ? { sessionId, blocking, snapshot: null, requestId, overlayReleased: false } : null;
+ activeSessionLoad = loading ? {
+ sessionId,
+ blocking,
+ snapshot: null,
+ requestId,
+ overlayReleased: false,
+ recoverCurrent: options.recoverCurrent === true,
+ } : null;
+ if (loading) scheduleSessionLoadRequestTimeout(sessionId, requestId);
const showOverlay = !!(loading && blocking);
if (showOverlay) {
document.body.classList.add('session-loading-active');
@@ -4315,6 +4386,7 @@
blocking: options.blocking !== false,
label: options.label || '',
requestId: options.requestId || activeSessionLoad?.requestId || createSessionSwitchRequestId(sessionId),
+ recoverCurrent: options.recoverCurrent === true,
};
if (ws && ws.readyState === 1 && wsAuthenticated) {
flushPendingSessionSwitch();
@@ -4323,9 +4395,42 @@
if (!ws || ws.readyState > 1) connect();
}
+ function requestSessionResume(sessionId, options = {}) {
+ if (!sessionId) return;
+ pendingSessionResumeRequest = {
+ sessionId,
+ requestId: options.requestId || createSessionSwitchRequestId(sessionId),
+ };
+ if (ws && ws.readyState === 1 && wsAuthenticated) {
+ flushPendingSessionResume();
+ return;
+ }
+ if (!ws || ws.readyState > 1) connect();
+ }
+
+ function scheduleSessionResumeFallback(sessionId, requestId) {
+ clearSessionResumeFallbackTimer();
+ if (!sessionId || !requestId) return;
+ sessionResumeFallbackTimer = setTimeout(() => {
+ sessionResumeFallbackTimer = null;
+ if (!pendingSessionResumeRequest ||
+ pendingSessionResumeRequest.sessionId !== sessionId ||
+ pendingSessionResumeRequest.requestId !== requestId) {
+ return;
+ }
+ pendingSessionResumeRequest = null;
+ requestSessionLoad(sessionId, {
+ blocking: false,
+ label: '正在恢复运行输出…',
+ requestId: createSessionSwitchRequestId(sessionId),
+ recoverCurrent: true,
+ });
+ }, SESSION_RESUME_FALLBACK_MS);
+ }
+
function flushPendingSessionSwitch() {
- if (!pendingSessionSwitchRequest) return;
- if (!ws || ws.readyState !== 1 || !wsAuthenticated) return;
+ if (!pendingSessionSwitchRequest) return false;
+ if (!ws || ws.readyState !== 1 || !wsAuthenticated) return false;
const request = pendingSessionSwitchRequest;
pendingSessionSwitchRequest = null;
if (!activeSessionLoad) {
@@ -4333,9 +4438,20 @@
blocking: request.blocking,
label: request.label || undefined,
requestId: request.requestId,
+ recoverCurrent: request.recoverCurrent === true,
});
}
ws.send(JSON.stringify({ type: 'load_session', sessionId: request.sessionId, requestId: request.requestId }));
+ return true;
+ }
+
+ function flushPendingSessionResume() {
+ if (!pendingSessionResumeRequest) return false;
+ if (!ws || ws.readyState !== 1 || !wsAuthenticated) return false;
+ const request = pendingSessionResumeRequest;
+ ws.send(JSON.stringify({ type: 'resume_session', sessionId: request.sessionId, requestId: request.requestId }));
+ scheduleSessionResumeFallback(request.sessionId, request.requestId);
+ return true;
}
function showCachedSession(sessionId) {
@@ -4883,9 +4999,15 @@
if (activeSessionLoad?.sessionId && !isPageUnloading) {
pendingSessionSwitchRequest = {
sessionId: activeSessionLoad.sessionId,
- blocking: activeSessionLoad.blocking,
+ blocking: false,
label: sessionLoadingLabel?.textContent || '',
requestId: activeSessionLoad.requestId || createSessionSwitchRequestId(activeSessionLoad.sessionId),
+ recoverCurrent: activeSessionLoad.recoverCurrent === true,
+ };
+ } else if (currentSessionId && (isGenerating || currentSessionRunning) && !isPageUnloading) {
+ pendingSessionResumeRequest = {
+ sessionId: currentSessionId,
+ requestId: createSessionSwitchRequestId(currentSessionId),
};
}
clearSessionLoading();
@@ -4934,7 +5056,18 @@
document.dispatchEvent(new CustomEvent('cc-web-auth-restored'));
loginOverlay.hidden = true;
app.hidden = false;
- flushPendingSessionSwitch();
+ const flushedSessionSwitch = flushPendingSessionSwitch();
+ const flushedSessionResume = flushedSessionSwitch ? false : flushPendingSessionResume();
+ if (!flushedSessionSwitch &&
+ !flushedSessionResume &&
+ !pendingSessionSwitchRequest &&
+ !pendingSessionResumeRequest &&
+ currentSessionId &&
+ (isGenerating || currentSessionRunning)) {
+ requestSessionResume(currentSessionId, {
+ requestId: createSessionSwitchRequestId(currentSessionId),
+ });
+ }
send({ type: 'get_codex_config' });
// Check if must change password
if (msg.mustChangePassword) {
@@ -4944,6 +5077,8 @@
}
} else {
pendingSessionSwitchRequest = null;
+ pendingSessionResumeRequest = null;
+ clearSessionResumeFallbackTimer();
clearSessionLoading();
authToken = null;
wsAuthenticated = false;
@@ -5007,7 +5142,7 @@
const canSwitchToSessionInfo = matchesActiveLoad
|| matchesPendingNewSession
|| msg.sessionId === currentSessionId
- || (!currentSessionId && !activeLoad && !pendingNewSession)
+ || (!messageRequestId && !currentSessionId && !activeLoad && !pendingNewSession)
|| (!messageRequestId && !activeLoad && !pendingNewSession);
mergeSessionListSnapshot(snapshot);
if (matchesActiveLoad) {
@@ -5041,6 +5176,10 @@
break;
case 'session_history_chunk':
+ if (activeSessionLoad?.recoverCurrent && activeSessionLoad.sessionId === msg.sessionId) {
+ if (!msg.remaining) finalizeLoadedSession(msg.sessionId);
+ break;
+ }
if (msg.sessionId === currentSessionId && loadedHistorySessionId === msg.sessionId) {
const blocking = isBlockingSessionLoad(msg.sessionId);
if (activeSessionLoad?.sessionId === msg.sessionId && activeSessionLoad.snapshot) {
@@ -5274,10 +5413,11 @@
case 'resume_generating':
if (!isCurrentSessionEvent(msg)) break;
+ clearPendingSessionResumeRequest(msg.sessionId || currentSessionId, msg.requestId);
// Server has an active process for this session — resume streaming
setCurrentSessionRunningState(true);
if (!isGenerating || !document.getElementById('streaming-msg')) {
- startGenerating(msg.sessionId || currentSessionId);
+ startGenerating(msg.sessionId || currentSessionId, { follow: isNearBottom() });
} else {
updateGenerationControls();
toolGroupCount = 0;
@@ -5312,8 +5452,24 @@
}
break;
- case 'error':
+ case 'resume_session_result':
if (!isCurrentSessionEvent(msg)) break;
+ clearPendingSessionResumeRequest(msg.sessionId || currentSessionId, msg.requestId);
+ setCurrentSessionRunningState(!!msg.isRunning);
+ if (!msg.isRunning && currentSessionId && msg.sessionId === currentSessionId) {
+ updateGenerationControls();
+ }
+ break;
+
+ case 'error':
+ const errorRequestId = String(msg.requestId || '');
+ const matchesActiveLoadError = !!(activeSessionLoad &&
+ (!msg.sessionId || msg.sessionId === activeSessionLoad.sessionId) &&
+ (!errorRequestId || errorRequestId === activeSessionLoad.requestId));
+ if (!matchesActiveLoadError && !isCurrentSessionEvent(msg)) break;
+ if (matchesActiveLoadError) {
+ clearPendingSessionSwitchRequest(activeSessionLoad.sessionId, activeSessionLoad.requestId);
+ }
if (msg.code === 'new_session_cwd_missing' && pendingNewSessionRequest?.cwd) {
const request = pendingNewSessionRequest;
pendingNewSessionRequest = null;
@@ -5401,8 +5557,12 @@
showToast(`「${msg.title}」任务完成`, msg.sessionId);
showBrowserNotification(msg.title);
if (msg.sessionId === currentSessionId) {
- // Reload current session to show completed response
- openSession(msg.sessionId, { forceSync: true, blocking: false });
+ // 当前用户如果正在向上翻历史,不要自动整屏重绘并把滚动条拽回底部。
+ if (isNearBottom()) {
+ openSession(msg.sessionId, { forceSync: true, blocking: false });
+ } else {
+ send({ type: 'list_sessions' });
+ }
} else {
send({ type: 'list_sessions' });
}
@@ -5435,9 +5595,10 @@
}
// --- Generating State ---
- function startGenerating(sessionId = currentSessionId) {
+ function startGenerating(sessionId = currentSessionId, options = {}) {
const targetSessionId = sessionId || currentSessionId || null;
if (targetSessionId && currentSessionId && targetSessionId !== currentSessionId) return false;
+ const shouldFollow = options.follow !== false;
isGenerating = true;
generatingSessionId = targetSessionId;
setCurrentSessionRunningState(true);
@@ -5469,7 +5630,11 @@
bubble.appendChild(toolsDiv);
syncAssistantLastSectionButton(msgEl);
messagesDiv.appendChild(msgEl);
- scrollToBottom();
+ if (shouldFollow) {
+ scrollToBottom();
+ } else {
+ updateScrollbar();
+ }
return true;
}
@@ -10210,20 +10375,21 @@
rememberPw.checked = true;
}
- // Visibility change: re-sync state when user returns to tab (critical for mobile)
+ // 页签切回只做轻量状态同步:不要因为 visible 事件整屏重载当前会话,
+ // 否则 renderMessages() 会重建气泡并把滚动条拉回底部。
document.addEventListener('visibilitychange', () => {
if (document.visibilityState !== 'visible') return;
if (!ws || ws.readyState > 1) {
// WS is dead, force reconnect
reconnectAttempts = 0;
connect();
- } else if (ws.readyState === 1 && currentSessionId) {
- // Preserve active streaming UI when returning to foreground.
- if (isGenerating || currentSessionRunning) {
- send({ type: 'load_session', sessionId: currentSessionId });
- } else {
- beginSessionSwitch(currentSessionId, { blocking: false, force: true });
+ } else if (ws.readyState === 1 && wsAuthenticated) {
+ if (currentSessionId && !activeSessionLoad && (isGenerating || currentSessionRunning)) {
+ requestSessionResume(currentSessionId, {
+ requestId: createSessionSwitchRequestId(currentSessionId),
+ });
}
+ send({ type: 'list_sessions' });
}
});
diff --git a/public/index.html b/public/index.html
index 8cb545a..ee8d86a 100644
--- a/public/index.html
+++ b/public/index.html
@@ -173,6 +173,6 @@
-
+