fix: 消除会话列表解析阻塞并按需加载工具详情

This commit is contained in:
shiyue
2026-10-19 10:58:22 +08:00
parent fb2c3360c9
commit 99cc807f4b
35 changed files with 1468 additions and 214 deletions

View File

@@ -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) {

View File

@@ -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">

View File

@@ -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;