fix: 消除会话列表解析阻塞并按需加载工具详情
This commit is contained in:
380
public/app.js
380
public/app.js
@@ -300,6 +300,7 @@
|
||||
let codexAppElicitationModal = null;
|
||||
let pendingNewSessionRequest = null;
|
||||
let sessionLoadAbortController = null;
|
||||
const toolDetailControllers = new Set();
|
||||
let currentSessionViewRequestId = '';
|
||||
let currentSessionHistoryRevision = null;
|
||||
let currentSessionSubscriptionRequestId = '';
|
||||
@@ -4361,6 +4362,58 @@
|
||||
return !!extension && FILE_BROWSER_IMAGE_EXTENSIONS.has(extension.toLowerCase());
|
||||
}
|
||||
|
||||
async function getFileBrowserImagePreview(sessionId, filePath, signal) {
|
||||
const { blob, mimeType } = await fetchAuthImage(
|
||||
`/api/fs/image?sessionId=${encodeURIComponent(sessionId)}&path=${encodeURIComponent(filePath)}`,
|
||||
signal
|
||||
);
|
||||
if (signal?.aborted) throw new DOMException('图片读取已取消', 'AbortError');
|
||||
const objectUrl = mimeType !== 'image/svg+xml';
|
||||
const url = objectUrl
|
||||
? URL.createObjectURL(blob)
|
||||
: await new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
const abort = () => reader.abort();
|
||||
reader.onload = () => resolve(reader.result);
|
||||
reader.onerror = () => reject(reader.error || new Error('图片读取失败'));
|
||||
reader.onabort = () => reject(new DOMException('图片读取已取消', 'AbortError'));
|
||||
reader.onloadend = () => signal?.removeEventListener('abort', abort);
|
||||
signal?.addEventListener('abort', abort, { once: true });
|
||||
reader.readAsDataURL(blob);
|
||||
});
|
||||
return { url, objectUrl, blob, mimeType };
|
||||
}
|
||||
|
||||
function getFileBrowserImageGallery(state) {
|
||||
return state.directoryEntries
|
||||
.filter((entry) => entry.kind === 'file' && (
|
||||
entry.imagePreviewableHint === true
|
||||
|| (entry.imagePreviewableHint == null && isFileBrowserImagePath(entry.path))
|
||||
))
|
||||
.map((entry) => ({
|
||||
filename: entry.name || getPathLeaf(entry.path),
|
||||
size: entry.size,
|
||||
filePath: normalizeBrowserPath(entry.path),
|
||||
sessionId: state.sessionId,
|
||||
}));
|
||||
}
|
||||
|
||||
function openFileBrowserImageGallery() {
|
||||
const state = fileBrowserState;
|
||||
if (!state || state.previewImageEl.hidden || !state.selectedFilePath) return;
|
||||
const gallery = getFileBrowserImageGallery(state);
|
||||
let index = gallery.findIndex((item) => item.filePath === state.selectedFilePath);
|
||||
if (index < 0) {
|
||||
gallery.push({
|
||||
filename: getPathLeaf(state.selectedFilePath),
|
||||
filePath: state.selectedFilePath,
|
||||
sessionId: state.sessionId,
|
||||
});
|
||||
index = gallery.length - 1;
|
||||
}
|
||||
openAttachmentPreviewModal(gallery[index], { gallery, index, browserState: state });
|
||||
}
|
||||
|
||||
function clearFileBrowserImagePreview(state) {
|
||||
state.previewImageNode.onload = null;
|
||||
state.previewImageNode.onerror = null;
|
||||
@@ -4381,6 +4434,7 @@
|
||||
function closeFileBrowser() {
|
||||
if (!fileBrowserState) return;
|
||||
const state = fileBrowserState;
|
||||
if (attachmentPreviewModal?.browserState === state) closeAttachmentPreviewModal();
|
||||
cancelFileBrowserPreview(state);
|
||||
const { overlay, escapeHandler } = state;
|
||||
if (escapeHandler) document.removeEventListener('keydown', escapeHandler);
|
||||
@@ -4444,6 +4498,7 @@
|
||||
if (!fileBrowserState) return;
|
||||
const state = fileBrowserState;
|
||||
const safeEntries = Array.isArray(entries) ? entries : [];
|
||||
state.directoryEntries = safeEntries;
|
||||
|
||||
if (safeEntries.length === 0) {
|
||||
state.listEl.innerHTML = '<div class="modal-empty">这个目录里还没有可显示的文件</div>';
|
||||
@@ -4493,6 +4548,8 @@
|
||||
async function loadFileBrowserDirectory(targetPath, options = {}) {
|
||||
if (!fileBrowserState) return;
|
||||
const state = fileBrowserState;
|
||||
if (attachmentPreviewModal?.browserState === state) closeAttachmentPreviewModal();
|
||||
state.directoryEntries = [];
|
||||
const normalizedPath = normalizeBrowserPath(targetPath);
|
||||
const previousPath = state.currentPath;
|
||||
const requestId = ++state.directoryRequestId;
|
||||
@@ -4542,6 +4599,7 @@
|
||||
async function openFileBrowserFile(targetPath, options = {}) {
|
||||
if (!fileBrowserState) return;
|
||||
const state = fileBrowserState;
|
||||
if (attachmentPreviewModal?.browserState === state) closeAttachmentPreviewModal();
|
||||
const normalizedPath = normalizeBrowserPath(targetPath);
|
||||
const imagePreviewable = options.imagePreviewable === true
|
||||
|| (options.imagePreviewable == null && isFileBrowserImagePath(normalizedPath));
|
||||
@@ -4556,24 +4614,14 @@
|
||||
|
||||
try {
|
||||
if (imagePreviewable) {
|
||||
const { blob, mimeType } = await fetchAuthImage(
|
||||
`/api/fs/image?sessionId=${encodeURIComponent(state.sessionId)}&path=${encodeURIComponent(normalizedPath)}`,
|
||||
controller.signal
|
||||
const { url: imageUrl, objectUrl, blob, mimeType } = await getFileBrowserImagePreview(
|
||||
state.sessionId, normalizedPath, controller.signal
|
||||
);
|
||||
if (state !== fileBrowserState || requestId !== state.previewRequestId) return;
|
||||
const imageUrl = mimeType === 'image/svg+xml'
|
||||
? await new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => resolve(reader.result);
|
||||
reader.onerror = () => reject(reader.error || new Error('图片读取失败'));
|
||||
reader.readAsDataURL(blob);
|
||||
})
|
||||
: URL.createObjectURL(blob);
|
||||
if (state !== fileBrowserState || requestId !== state.previewRequestId) {
|
||||
if (mimeType !== 'image/svg+xml') URL.revokeObjectURL(imageUrl);
|
||||
if (objectUrl) URL.revokeObjectURL(imageUrl);
|
||||
return;
|
||||
}
|
||||
if (mimeType !== 'image/svg+xml') state.previewImageUrl = imageUrl;
|
||||
if (objectUrl) state.previewImageUrl = imageUrl;
|
||||
const name = normalizedPath.split('/').pop() || '图片预览';
|
||||
state.previewImageNode.alt = name;
|
||||
state.previewImageNode.onload = () => {
|
||||
@@ -4668,9 +4716,9 @@
|
||||
</div>
|
||||
<div class="file-browser-preview-empty" data-browser-preview-empty>选择一个文件查看内容</div>
|
||||
<pre class="file-browser-preview-content" data-browser-preview-content hidden></pre>
|
||||
<div class="file-browser-preview-image" data-browser-preview-image hidden>
|
||||
<button class="file-browser-preview-image" type="button" data-browser-preview-image title="全屏预览图片" aria-label="全屏预览图片" hidden>
|
||||
<img data-browser-preview-image-node alt="">
|
||||
</div>
|
||||
</button>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
@@ -4683,6 +4731,7 @@
|
||||
sessionId: currentSessionId,
|
||||
rootPath: currentCwd,
|
||||
currentPath: '',
|
||||
directoryEntries: [],
|
||||
selectedFilePath: '',
|
||||
directoryRequestId: 0,
|
||||
previewRequestId: 0,
|
||||
@@ -4707,7 +4756,7 @@
|
||||
fileBrowserState = state;
|
||||
|
||||
state.escapeHandler = (e) => {
|
||||
if (e.key === 'Escape') closeFileBrowser();
|
||||
if (e.key === 'Escape' && !e.defaultPrevented && !attachmentPreviewModal) closeFileBrowser();
|
||||
};
|
||||
document.addEventListener('keydown', state.escapeHandler);
|
||||
|
||||
@@ -4724,6 +4773,7 @@
|
||||
state.mobileBackBtn.addEventListener('click', () => {
|
||||
setFileBrowserPreviewMode(false);
|
||||
});
|
||||
state.previewImageEl.addEventListener('click', openFileBrowserImageGallery);
|
||||
|
||||
updateFileBrowserPathBar();
|
||||
renderFileBrowserPreviewEmpty('文件预览', '选择一个文件查看内容');
|
||||
@@ -4908,15 +4958,89 @@
|
||||
return promise;
|
||||
}
|
||||
|
||||
function closeAttachmentPreviewModal() {
|
||||
if (!attachmentPreviewModal) return;
|
||||
const { overlay, escapeHandler } = attachmentPreviewModal;
|
||||
if (escapeHandler) document.removeEventListener('keydown', escapeHandler);
|
||||
if (overlay && overlay.parentNode) overlay.parentNode.removeChild(overlay);
|
||||
attachmentPreviewModal = null;
|
||||
function clearAttachmentPreviewImage(state) {
|
||||
state.requestId += 1;
|
||||
state.abortController?.abort();
|
||||
state.abortController = null;
|
||||
state.imgEl.onload = null;
|
||||
state.imgEl.onerror = null;
|
||||
state.imgEl.removeAttribute('src');
|
||||
state.imgEl.hidden = true;
|
||||
if (state.imageObjectUrl) URL.revokeObjectURL(state.imageObjectUrl);
|
||||
state.imageObjectUrl = '';
|
||||
return state.requestId;
|
||||
}
|
||||
|
||||
async function openAttachmentPreviewModal(attachment) {
|
||||
function closeAttachmentPreviewModal() {
|
||||
if (!attachmentPreviewModal) return;
|
||||
const state = attachmentPreviewModal;
|
||||
attachmentPreviewModal = null;
|
||||
clearAttachmentPreviewImage(state);
|
||||
document.removeEventListener('keydown', state.escapeHandler, true);
|
||||
state.overlay.remove();
|
||||
if (state.returnFocus?.isConnected) state.returnFocus.focus({ preventScroll: true });
|
||||
}
|
||||
|
||||
async function loadAttachmentPreviewImage(state, index) {
|
||||
const attachment = state.gallery ? state.gallery[index] : state.attachment;
|
||||
if (!attachment || state !== attachmentPreviewModal) return;
|
||||
const requestId = clearAttachmentPreviewImage(state);
|
||||
const controller = new AbortController();
|
||||
state.abortController = controller;
|
||||
state.index = index;
|
||||
const isCurrent = () => state === attachmentPreviewModal && requestId === state.requestId;
|
||||
const pageText = state.gallery ? `${index + 1} / ${state.gallery.length}` : '';
|
||||
state.nameEl.textContent = attachment.filename || 'image';
|
||||
state.nameEl.title = attachment.filename || 'image';
|
||||
state.imgEl.alt = attachment.filename || 'image';
|
||||
state.descEl.textContent = [pageText, formatFileSize(attachment.size || 0)].filter(Boolean).join(' · ');
|
||||
state.placeholderEl.textContent = '正在加载图片…';
|
||||
state.placeholderEl.hidden = false;
|
||||
state.stageEl.classList.remove('is-ready', 'is-error');
|
||||
state.stageEl.classList.add('is-loading');
|
||||
if (state.previousBtn) state.previousBtn.disabled = index <= 0;
|
||||
if (state.nextBtn) state.nextBtn.disabled = index >= state.gallery.length - 1;
|
||||
try {
|
||||
const preview = state.gallery
|
||||
? await getFileBrowserImagePreview(attachment.sessionId, attachment.filePath, controller.signal)
|
||||
: { url: await getAttachmentPreviewUrl(attachment), objectUrl: false };
|
||||
if (!isCurrent()) {
|
||||
if (preview.objectUrl) URL.revokeObjectURL(preview.url);
|
||||
return;
|
||||
}
|
||||
if (preview.objectUrl) state.imageObjectUrl = preview.url;
|
||||
if (preview.blob) state.descEl.textContent = `${pageText} · ${formatFileSize(preview.blob.size)} · ${preview.mimeType}`;
|
||||
state.imgEl.onload = () => {
|
||||
if (!isCurrent()) return;
|
||||
state.imgEl.hidden = false;
|
||||
state.placeholderEl.hidden = true;
|
||||
state.stageEl.classList.remove('is-loading');
|
||||
state.stageEl.classList.add('is-ready');
|
||||
};
|
||||
state.imgEl.onerror = () => {
|
||||
if (!isCurrent()) return;
|
||||
state.placeholderEl.textContent = '图片加载失败';
|
||||
state.stageEl.classList.remove('is-loading');
|
||||
state.stageEl.classList.add('is-error');
|
||||
};
|
||||
state.imgEl.src = preview.url;
|
||||
} catch (err) {
|
||||
if (!isCurrent()) return;
|
||||
state.placeholderEl.textContent = err.message || '图片预览失败';
|
||||
state.stageEl.classList.remove('is-loading');
|
||||
state.stageEl.classList.add('is-error');
|
||||
} finally {
|
||||
if (state.abortController === controller) state.abortController = null;
|
||||
}
|
||||
}
|
||||
|
||||
function stepAttachmentPreviewImage(state, direction) {
|
||||
if (!state.gallery || state !== attachmentPreviewModal) return;
|
||||
const index = state.index + direction;
|
||||
if (index >= 0 && index < state.gallery.length) loadAttachmentPreviewImage(state, index);
|
||||
}
|
||||
|
||||
async function openAttachmentPreviewModal(attachment, options = {}) {
|
||||
if (!attachment || attachment.storageState === 'expired') {
|
||||
showToast('图片已过期,无法预览');
|
||||
return;
|
||||
@@ -4924,10 +5048,11 @@
|
||||
|
||||
closeAttachmentPreviewModal();
|
||||
|
||||
const gallery = Array.isArray(options.gallery) && options.gallery.length ? options.gallery : null;
|
||||
const overlay = document.createElement('div');
|
||||
overlay.className = 'modal-overlay attachment-preview-overlay';
|
||||
overlay.className = `modal-overlay attachment-preview-overlay${gallery ? ' is-fullscreen' : ''}`;
|
||||
overlay.innerHTML = `
|
||||
<div class="modal-panel modal-panel-wide attachment-preview-panel">
|
||||
<div class="modal-panel modal-panel-wide attachment-preview-panel" role="dialog" aria-modal="true" aria-label="图片预览">
|
||||
<div class="modal-header">
|
||||
<span class="modal-title">图片预览</span>
|
||||
<button class="modal-close-btn" type="button" aria-label="关闭">✕</button>
|
||||
@@ -4935,61 +5060,79 @@
|
||||
<div class="attachment-preview-body">
|
||||
<div class="attachment-preview-stage is-loading">
|
||||
<div class="attachment-preview-placeholder">正在加载图片…</div>
|
||||
<img class="attachment-preview-image" alt="${escapeHtml(attachment.filename || 'image')}" hidden>
|
||||
<img class="attachment-preview-image" alt="" hidden>
|
||||
${gallery ? `
|
||||
<button class="attachment-preview-nav attachment-preview-previous" type="button" data-preview-previous aria-label="上一张" title="上一张"><span aria-hidden="true">←</span></button>
|
||||
<button class="attachment-preview-nav attachment-preview-next" type="button" data-preview-next aria-label="下一张" title="下一张"><span aria-hidden="true">→</span></button>
|
||||
` : ''}
|
||||
</div>
|
||||
<div class="attachment-preview-meta">
|
||||
<div class="attachment-preview-name">${escapeHtml(attachment.filename || 'image')}</div>
|
||||
<div class="attachment-preview-desc">${escapeHtml(formatFileSize(attachment.size || 0))} · 点击空白处关闭</div>
|
||||
<div class="attachment-preview-name"></div>
|
||||
<div class="attachment-preview-desc" aria-live="polite"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const returnFocus = document.activeElement;
|
||||
document.body.appendChild(overlay);
|
||||
const stageEl = overlay.querySelector('.attachment-preview-stage');
|
||||
const imgEl = overlay.querySelector('.attachment-preview-image');
|
||||
const placeholderEl = overlay.querySelector('.attachment-preview-placeholder');
|
||||
const closeBtn = overlay.querySelector('.modal-close-btn');
|
||||
|
||||
const finishClose = () => closeAttachmentPreviewModal();
|
||||
attachmentPreviewModal = {
|
||||
const state = {
|
||||
overlay,
|
||||
attachment,
|
||||
gallery,
|
||||
index: gallery ? Math.max(0, Math.min(gallery.length - 1, Number(options.index) || 0)) : 0,
|
||||
browserState: gallery ? options.browserState : null,
|
||||
returnFocus,
|
||||
stageEl: overlay.querySelector('.attachment-preview-stage'),
|
||||
imgEl: overlay.querySelector('.attachment-preview-image'),
|
||||
placeholderEl: overlay.querySelector('.attachment-preview-placeholder'),
|
||||
nameEl: overlay.querySelector('.attachment-preview-name'),
|
||||
descEl: overlay.querySelector('.attachment-preview-desc'),
|
||||
previousBtn: overlay.querySelector('[data-preview-previous]'),
|
||||
nextBtn: overlay.querySelector('[data-preview-next]'),
|
||||
requestId: 0,
|
||||
abortController: null,
|
||||
imageObjectUrl: '',
|
||||
escapeHandler: null,
|
||||
};
|
||||
|
||||
attachmentPreviewModal.escapeHandler = (e) => {
|
||||
if (e.key === 'Escape') finishClose();
|
||||
attachmentPreviewModal = state;
|
||||
state.escapeHandler = (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
e.preventDefault();
|
||||
e.stopImmediatePropagation();
|
||||
finishClose();
|
||||
} else if (gallery && (e.key === 'ArrowLeft' || e.key === 'ArrowRight')) {
|
||||
e.preventDefault();
|
||||
e.stopImmediatePropagation();
|
||||
stepAttachmentPreviewImage(state, e.key === 'ArrowLeft' ? -1 : 1);
|
||||
} else if (e.key === 'Tab') {
|
||||
const controls = [closeBtn, state.previousBtn, state.nextBtn].filter((button) => button && !button.disabled);
|
||||
const first = controls[0];
|
||||
const last = controls[controls.length - 1];
|
||||
if (!controls.includes(document.activeElement)) {
|
||||
e.preventDefault();
|
||||
first.focus();
|
||||
} else if (e.shiftKey && document.activeElement === first) {
|
||||
e.preventDefault();
|
||||
last.focus();
|
||||
} else if (!e.shiftKey && document.activeElement === last) {
|
||||
e.preventDefault();
|
||||
first.focus();
|
||||
}
|
||||
}
|
||||
};
|
||||
document.addEventListener('keydown', attachmentPreviewModal.escapeHandler);
|
||||
|
||||
document.addEventListener('keydown', state.escapeHandler, true);
|
||||
closeBtn.addEventListener('click', finishClose);
|
||||
state.previousBtn?.addEventListener('click', () => stepAttachmentPreviewImage(state, -1));
|
||||
state.nextBtn?.addEventListener('click', () => stepAttachmentPreviewImage(state, 1));
|
||||
overlay.addEventListener('click', (e) => {
|
||||
if (e.target === overlay) finishClose();
|
||||
});
|
||||
|
||||
try {
|
||||
const url = await getAttachmentPreviewUrl(attachment);
|
||||
if (!attachmentPreviewModal || attachmentPreviewModal.overlay !== overlay) return;
|
||||
imgEl.onload = () => {
|
||||
if (!attachmentPreviewModal || attachmentPreviewModal.overlay !== overlay) return;
|
||||
imgEl.hidden = false;
|
||||
placeholderEl.hidden = true;
|
||||
stageEl.classList.remove('is-loading');
|
||||
stageEl.classList.add('is-ready');
|
||||
};
|
||||
imgEl.onerror = () => {
|
||||
if (!attachmentPreviewModal || attachmentPreviewModal.overlay !== overlay) return;
|
||||
placeholderEl.textContent = '图片加载失败';
|
||||
stageEl.classList.remove('is-loading');
|
||||
stageEl.classList.add('is-error');
|
||||
};
|
||||
imgEl.src = url;
|
||||
} catch (err) {
|
||||
if (!attachmentPreviewModal || attachmentPreviewModal.overlay !== overlay) return;
|
||||
placeholderEl.textContent = err.message || '图片预览失败';
|
||||
stageEl.classList.remove('is-loading');
|
||||
stageEl.classList.add('is-error');
|
||||
}
|
||||
closeBtn.focus({ preventScroll: true });
|
||||
await loadAttachmentPreviewImage(state, state.index);
|
||||
}
|
||||
|
||||
function hydrateAttachmentPreviews(root, attachments = []) {
|
||||
@@ -7000,6 +7143,7 @@
|
||||
}
|
||||
|
||||
function resetChatView(agent) {
|
||||
abortToolDetailRequests();
|
||||
clearPendingMissingSessionConfirmation();
|
||||
currentSessionViewRequestId = '';
|
||||
currentSessionHistoryRevision = null;
|
||||
@@ -7232,6 +7376,7 @@
|
||||
}
|
||||
|
||||
function setSessionLoading(sessionId, options = {}) {
|
||||
if (sessionId) abortToolDetailRequests();
|
||||
sessionLoadAbortController?.abort();
|
||||
sessionLoadAbortController = null;
|
||||
clearSessionLoadOverlayTimer();
|
||||
@@ -7647,6 +7792,7 @@
|
||||
}
|
||||
|
||||
function showCachedSession(sessionId) {
|
||||
abortToolDetailRequests();
|
||||
const snapshot = buildCachedSessionSnapshot(sessionId);
|
||||
if (!snapshot) return false;
|
||||
if (!isPrimaryUiAgent(snapshot.agent)) return false;
|
||||
@@ -8971,6 +9117,7 @@
|
||||
const resumeToolCalls = renderToolCallsWithMergedCollab(msg.toolCalls);
|
||||
for (const tc of resumeToolCalls) {
|
||||
activeToolCalls.set(tc.id, {
|
||||
...tc,
|
||||
name: tc.name,
|
||||
input: tc.input,
|
||||
result: tc.result,
|
||||
@@ -8978,10 +9125,7 @@
|
||||
meta: tc.meta || null,
|
||||
done: tc.done,
|
||||
});
|
||||
appendToolCall(tc.id, tc.name, tc.input, tc.done, tc.kind || null, tc.meta || null);
|
||||
if (tc.result !== undefined && tc.result !== null) {
|
||||
updateToolCall(tc.id, tc.result, !!tc.done);
|
||||
}
|
||||
appendToolCall(tc.id, tc.name, tc.input, tc.done, tc.kind || null, tc.meta || null, tc.result, tc.detailRef);
|
||||
}
|
||||
}
|
||||
break;
|
||||
@@ -10227,6 +10371,7 @@
|
||||
|
||||
function isEmptyReasoningTool(tool) {
|
||||
if (toolKind(tool) !== 'reasoning') return false;
|
||||
if (tool.detailRef) return false;
|
||||
const resultText = stringifyToolValue(tool?.result).trim();
|
||||
const input = tool?.input || {};
|
||||
const inputText = [
|
||||
@@ -11640,8 +11785,27 @@
|
||||
}
|
||||
|
||||
function parseCcwebDisplayImageResult(result) {
|
||||
if (result && typeof result === 'object') return result;
|
||||
try { return JSON.parse(String(result || '')); } catch { return null; }
|
||||
const envelope = parseMaybeJsonObject(result);
|
||||
if (!envelope || Array.isArray(envelope)) return null;
|
||||
const textBlocks = Array.isArray(envelope.content)
|
||||
? envelope.content.filter((block) => block?.type === 'text' && typeof block.text === 'string')
|
||||
: [];
|
||||
// 历史和实时结果均可能保留 MCP 包装;只解包标准的一层,避免循环或异常嵌套。
|
||||
const candidates = [
|
||||
envelope,
|
||||
parseMaybeJsonObject(envelope.structuredContent),
|
||||
...textBlocks.map((block) => parseMaybeJsonObject(block.text)),
|
||||
];
|
||||
const payload = candidates.find((candidate) => candidate && !Array.isArray(candidate)
|
||||
&& typeof candidate.ok === 'boolean') || null;
|
||||
if (envelope.isError === true) {
|
||||
return {
|
||||
ok: false,
|
||||
message: payload?.message || envelope.message
|
||||
|| textBlocks.map((block) => block.text).join('\n').trim() || '图片渲染失败',
|
||||
};
|
||||
}
|
||||
return payload;
|
||||
}
|
||||
|
||||
function createCcwebDisplayImageToolContent(tool = {}) {
|
||||
@@ -11663,6 +11827,73 @@
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
function abortToolDetailRequests() {
|
||||
for (const controller of toolDetailControllers) controller.abort();
|
||||
toolDetailControllers.clear();
|
||||
}
|
||||
|
||||
async function renderExpandedToolContent(details) {
|
||||
if (!details.open || !details.__tool) return;
|
||||
const tool = details.__tool;
|
||||
if (details.__renderedTool === tool || details.__detailController) return;
|
||||
const replaceContent = (content) => {
|
||||
Array.from(details.children).filter(child => child.tagName !== 'SUMMARY').forEach(child => child.remove());
|
||||
details.appendChild(content);
|
||||
};
|
||||
if (!tool.detailRef) {
|
||||
replaceContent(buildToolContentElement(tool));
|
||||
details.__renderedTool = tool;
|
||||
return;
|
||||
}
|
||||
const controller = new AbortController();
|
||||
details.__detailController = controller;
|
||||
toolDetailControllers.add(controller);
|
||||
const viewRequestId = currentSessionViewRequestId;
|
||||
const sessionId = currentSessionId;
|
||||
const placeholder = document.createElement('div');
|
||||
placeholder.className = 'tool-call-content';
|
||||
placeholder.textContent = '正在读取工具详情…';
|
||||
replaceContent(placeholder);
|
||||
const timer = setTimeout(() => controller.abort(), SESSION_LOAD_REQUEST_TIMEOUT_MS);
|
||||
try {
|
||||
const requestId = createSessionSwitchRequestId(sessionId);
|
||||
const parameters = new URLSearchParams({ requestId, toolDetail: JSON.stringify(tool.detailRef) });
|
||||
const response = await fetch(`/api/sessions/${encodeURIComponent(sessionId)}?${parameters}`, {
|
||||
headers: { Authorization: `Bearer ${authToken}` }, cache: 'no-store', signal: controller.signal,
|
||||
});
|
||||
const payload = await response.json();
|
||||
if (!response.ok) throw new Error(payload.message || '工具详情读取失败');
|
||||
if (payload.sessionId !== sessionId || payload.requestId !== requestId || payload.type !== 'session_tool_detail') {
|
||||
throw new Error('工具详情响应与请求不匹配');
|
||||
}
|
||||
if (!details.isConnected || currentSessionId !== sessionId || currentSessionViewRequestId !== viewRequestId
|
||||
|| details.__tool !== tool) return;
|
||||
details.__tool = { ...payload.tool, done: tool.done };
|
||||
details.__renderedTool = details.__tool;
|
||||
replaceContent(buildToolContentElement(details.__tool));
|
||||
} catch (error) {
|
||||
if (details.__tool !== tool || !details.isConnected || currentSessionId !== sessionId
|
||||
|| currentSessionViewRequestId !== viewRequestId) return;
|
||||
placeholder.textContent = error.name === 'AbortError' ? '工具详情读取超时' : error.message;
|
||||
const retry = document.createElement('button');
|
||||
retry.textContent = '重试';
|
||||
retry.addEventListener('click', () => renderExpandedToolContent(details));
|
||||
placeholder.appendChild(retry);
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
toolDetailControllers.delete(controller);
|
||||
if (details.__detailController === controller) details.__detailController = null;
|
||||
if (details.__tool !== tool && details.__renderedTool !== details.__tool) renderExpandedToolContent(details);
|
||||
}
|
||||
}
|
||||
|
||||
function setToolContentState(details, tool) {
|
||||
details.__detailController?.abort();
|
||||
details.__tool = tool;
|
||||
if (details.open) renderExpandedToolContent(details);
|
||||
else Array.from(details.children).filter(child => child.tagName !== 'SUMMARY').forEach(child => child.remove());
|
||||
}
|
||||
|
||||
function createToolCallElement(toolUseId, tool, done) {
|
||||
const kind = toolKind(tool);
|
||||
if (isCcwebDisplayImageTool(tool)) {
|
||||
@@ -11710,7 +11941,11 @@
|
||||
const summary = document.createElement('summary');
|
||||
applyToolSummary(summary, tool, done);
|
||||
details.appendChild(summary);
|
||||
details.appendChild(buildToolContentElement({ ...tool, done }));
|
||||
details.addEventListener('toggle', () => {
|
||||
if (details.open) renderExpandedToolContent(details);
|
||||
else details.__detailController?.abort();
|
||||
});
|
||||
setToolContentState(details, { ...tool, done });
|
||||
return details;
|
||||
}
|
||||
|
||||
@@ -11756,7 +11991,7 @@
|
||||
});
|
||||
}
|
||||
|
||||
function appendToolCall(toolUseId, name, input, done, kind = null, meta = null, result = undefined) {
|
||||
function appendToolCall(toolUseId, name, input, done, kind = null, meta = null, result = undefined, detailRef = undefined) {
|
||||
const streamEl = document.getElementById('streaming-msg');
|
||||
if (!streamEl) return;
|
||||
const bubble = streamEl.querySelector('.msg-bubble');
|
||||
@@ -11766,6 +12001,7 @@
|
||||
if (!toolsDiv) { toolsDiv = bubble; }
|
||||
|
||||
const tool = { id: toolUseId, name, input, kind, meta, done };
|
||||
if (detailRef) tool.detailRef = detailRef;
|
||||
if (result !== undefined) tool.result = result;
|
||||
if (isEmptyReasoningTool(tool)) return;
|
||||
if (toolKind(tool) === 'collab_agent_tool_call') {
|
||||
@@ -11900,6 +12136,10 @@
|
||||
const summary = el.querySelector('summary');
|
||||
if (summary) applyToolSummary(summary, nextTool, done);
|
||||
if (nextTool.name === 'AskUserQuestion') return;
|
||||
if (el.tagName === 'DETAILS') {
|
||||
setToolContentState(el, { ...nextTool });
|
||||
return;
|
||||
}
|
||||
const nextContent = buildToolContentElement(nextTool);
|
||||
const content = Array.from(el.children).find((child) => child.tagName !== 'SUMMARY') || null;
|
||||
if (content) {
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
document.documentElement.dataset.dividerTime = dividerTime;
|
||||
})();
|
||||
</script>
|
||||
<link rel="stylesheet" href="style.css?v=20260913-history-control-position">
|
||||
<link rel="stylesheet" href="style.css?v=20261017-file-browser-image-gallery">
|
||||
<link rel="stylesheet" href="task-board.css?v=__CC_WEB_FRONTEND_ASSET_VERSION__">
|
||||
<link rel="stylesheet" href="gitea-workflow.css?v=__CC_WEB_GITEA_WORKFLOW_ASSET_VERSION__">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/atom-one-dark.min.css">
|
||||
|
||||
@@ -5960,6 +5960,10 @@ html[data-theme='coolvibe'] .settings-back:hover {
|
||||
object-fit: contain;
|
||||
background: #111;
|
||||
}
|
||||
.attachment-preview-image[hidden],
|
||||
.attachment-preview-placeholder[hidden] {
|
||||
display: none;
|
||||
}
|
||||
.attachment-preview-meta {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -5974,6 +5978,80 @@ html[data-theme='coolvibe'] .settings-back:hover {
|
||||
color: var(--text-secondary);
|
||||
font-size: 12px;
|
||||
}
|
||||
.attachment-preview-overlay.is-fullscreen {
|
||||
padding: 0;
|
||||
background: #111;
|
||||
z-index: 510;
|
||||
}
|
||||
.attachment-preview-overlay.is-fullscreen .attachment-preview-panel {
|
||||
width: 100%;
|
||||
max-width: none;
|
||||
height: 100vh;
|
||||
height: 100dvh;
|
||||
max-height: 100vh;
|
||||
max-height: 100dvh;
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
}
|
||||
.attachment-preview-overlay.is-fullscreen .modal-header {
|
||||
padding-top: max(12px, env(safe-area-inset-top));
|
||||
}
|
||||
.attachment-preview-overlay.is-fullscreen .attachment-preview-body {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
gap: 10px;
|
||||
padding: 12px max(12px, env(safe-area-inset-right)) max(12px, env(safe-area-inset-bottom)) max(12px, env(safe-area-inset-left));
|
||||
}
|
||||
.attachment-preview-overlay.is-fullscreen .attachment-preview-stage {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
max-height: none;
|
||||
border-radius: 8px;
|
||||
}
|
||||
.attachment-preview-overlay.is-fullscreen .attachment-preview-image {
|
||||
height: 100%;
|
||||
max-height: none;
|
||||
}
|
||||
.attachment-preview-overlay.is-fullscreen .attachment-preview-meta {
|
||||
flex-shrink: 0;
|
||||
min-width: 0;
|
||||
}
|
||||
.attachment-preview-overlay.is-fullscreen .attachment-preview-name {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.attachment-preview-nav {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
z-index: 1;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
padding: 0;
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
border-radius: 8px;
|
||||
color: #fff;
|
||||
background: rgba(0, 0, 0, 0.65);
|
||||
font-size: 24px;
|
||||
cursor: pointer;
|
||||
touch-action: manipulation;
|
||||
}
|
||||
.attachment-preview-previous { left: 12px; }
|
||||
.attachment-preview-next { right: 12px; }
|
||||
.attachment-preview-nav:hover:not(:disabled) {
|
||||
background: rgba(0, 0, 0, 0.9);
|
||||
}
|
||||
.attachment-preview-nav:disabled {
|
||||
opacity: 0.25;
|
||||
cursor: default;
|
||||
}
|
||||
.attachment-preview-nav:focus-visible {
|
||||
outline: 2px solid #fff;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
.modal-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
@@ -7436,7 +7514,15 @@ textarea.codex-elicitation-input {
|
||||
justify-content: center;
|
||||
overflow: auto;
|
||||
padding: 14px;
|
||||
width: 100%;
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
background: var(--bg-primary);
|
||||
cursor: zoom-in;
|
||||
}
|
||||
.file-browser-preview-image:focus-visible {
|
||||
outline: 2px solid var(--text-secondary);
|
||||
outline-offset: -3px;
|
||||
}
|
||||
.file-browser-preview-image[hidden] {
|
||||
display: none;
|
||||
|
||||
Reference in New Issue
Block a user