修复完整会话历史与长气泡交互并更新 CentOS 7 发布包
提交当前工作区全部源码、回归脚本、任务记录及重新构建的 Bun baseline 发布包。发布单文件 MCP 初始化及前端资源一致性验证通过。
This commit is contained in:
346
public/app.js
346
public/app.js
@@ -20,11 +20,17 @@
|
||||
const CROSS_CONVERSATION_REPLY_COLLAPSE_LIMIT = 500;
|
||||
const ASSISTANT_LAST_SECTION_BUTTON_CLASS = 'msg-last-section-btn';
|
||||
const ASSISTANT_BRANCH_BUTTON_CLASS = 'msg-branch-btn';
|
||||
const ASSISTANT_COPY_REPLY_BUTTON_CLASS = 'msg-copy-reply-btn';
|
||||
const ASSISTANT_REPLY_COLLAPSE_CHARS = 6000;
|
||||
const ASSISTANT_REPLY_COLLAPSE_BLOCKS = 24;
|
||||
const assistantReplyExpansion = new Map();
|
||||
const ASSISTANT_LAST_SECTION_FOCUS_CLASS = 'msg-last-section-focus';
|
||||
const ASSISTANT_LAST_SECTION_SCROLL_OFFSET = 72;
|
||||
const ASSISTANT_LAST_SECTION_SKIP_SELECTOR = [
|
||||
`.${ASSISTANT_LAST_SECTION_BUTTON_CLASS}`,
|
||||
`.${ASSISTANT_BRANCH_BUTTON_CLASS}`,
|
||||
`.${ASSISTANT_COPY_REPLY_BUTTON_CLASS}`,
|
||||
'.msg-replies-toggle',
|
||||
'.msg-action-row',
|
||||
'.msg-tools',
|
||||
'.tool-call',
|
||||
@@ -256,7 +262,7 @@
|
||||
cursor: 0,
|
||||
remaining: 0,
|
||||
total: 0,
|
||||
available: false,
|
||||
available: true,
|
||||
loading: false,
|
||||
error: '',
|
||||
};
|
||||
@@ -267,7 +273,7 @@
|
||||
sessionId: null,
|
||||
cursor: 0,
|
||||
total: 0,
|
||||
available: false,
|
||||
available: true,
|
||||
loading: false,
|
||||
complete: false,
|
||||
error: '',
|
||||
@@ -1955,7 +1961,7 @@
|
||||
codexAppSteerRecords.delete(id);
|
||||
}
|
||||
|
||||
function commitCodexAppSteerMessage(record) {
|
||||
function commitCodexAppSteerMessage(record, requestedIndex = null) {
|
||||
if (!record || record.committed) return false;
|
||||
const element = record.element;
|
||||
if (!element || record.sessionId !== currentSessionId) return false;
|
||||
@@ -1963,9 +1969,16 @@
|
||||
record.committed = true;
|
||||
return false;
|
||||
}
|
||||
const messageIndex = currentSessionMessageCount;
|
||||
currentSessionMessageCount += 1;
|
||||
markSessionMessageElement(element, messageIndex);
|
||||
const messageIndex = Number.isInteger(requestedIndex) && requestedIndex >= 0
|
||||
? requestedIndex : currentSessionMessageCount;
|
||||
currentSessionMessageCount = Math.max(currentSessionMessageCount, messageIndex + 1);
|
||||
markSessionMessageElement(element, messageIndex, {
|
||||
id: record.id,
|
||||
role: 'user',
|
||||
content: record.text,
|
||||
attachments: cloneCodexAppSteerAttachments(record.attachments),
|
||||
timestamp: record.timestamp,
|
||||
});
|
||||
record.committed = true;
|
||||
updateUserOutlinePanel();
|
||||
return true;
|
||||
@@ -1988,6 +2001,41 @@
|
||||
});
|
||||
}
|
||||
|
||||
function completeCodexAppSteerMessage(record, message = null, messageIndex = null) {
|
||||
if (!record?.element) return false;
|
||||
const element = record.element;
|
||||
commitCodexAppSteerMessage(record, messageIndex);
|
||||
if (Number.isInteger(messageIndex) && messageIndex >= 0) {
|
||||
markSessionMessageElement(element, messageIndex);
|
||||
currentSessionMessageCount = Math.max(currentSessionMessageCount, messageIndex + 1);
|
||||
}
|
||||
if (message) {
|
||||
markSessionMessageElement(element, Number(element.dataset.messageIndex), message);
|
||||
}
|
||||
const confirmedMessage = message || element.__sessionMessage;
|
||||
if (confirmedMessage) {
|
||||
mergeHistoryChunkIntoCachedSnapshot(record.sessionId, [confirmedMessage], {
|
||||
baseIndex: Number(element.dataset.messageIndex), total: currentSessionMessageCount,
|
||||
});
|
||||
}
|
||||
element.classList.remove('codex-steer-message', 'codex-steer-pending', 'codex-steer-inserted', 'codex-steer-failed');
|
||||
element.removeAttribute('aria-busy');
|
||||
element.querySelector('.codex-steer-status')?.remove();
|
||||
element.querySelector('.codex-steer-actions')?.remove();
|
||||
clearCodexAppSteerRecord(record.id);
|
||||
return true;
|
||||
}
|
||||
|
||||
function reconcileCodexAppSteerSessionMessage(sessionId, message, messageIndex = null) {
|
||||
if (message?.role !== 'user') return false;
|
||||
const id = String(message.id || message.clientMessageId || '').trim();
|
||||
const record = codexAppSteerRecords.get(id);
|
||||
if (!record || record.sessionId !== sessionId) return false;
|
||||
// 新轮回显与原失败卡片共用 ID,直接确认原卡片,避免再追加一个用户气泡。
|
||||
if (!message.codexAppSteerStatus) return completeCodexAppSteerMessage(record, message, messageIndex);
|
||||
return setCodexAppSteerStatusElement(record.element, message.codexAppSteerStatus, message.codexAppSteerMessage);
|
||||
}
|
||||
|
||||
function normalizeOutlineTitleHistory(history) {
|
||||
if (!Array.isArray(history)) return [];
|
||||
const normalized = [];
|
||||
@@ -2026,7 +2074,7 @@
|
||||
sessionId,
|
||||
cursor: 0,
|
||||
total: 0,
|
||||
available: false,
|
||||
available: true,
|
||||
loading: false,
|
||||
complete: false,
|
||||
error: '',
|
||||
@@ -2215,13 +2263,12 @@
|
||||
? '<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>'
|
||||
: historyState.sessionId && !historyState.available
|
||||
? '<div class="user-outline-loading" role="status">更早的记录尚待核实,已保存的消息可正常查看。</div>'
|
||||
: '';
|
||||
if (items.length === 0) {
|
||||
userOutlinePanel.innerHTML = historyStatus || '<div class="user-outline-empty">暂无用户消息</div>';
|
||||
userOutlineBtn.disabled = !historyState.loading
|
||||
&& !(historyState.cursor > 0 && historyState.available);
|
||||
userOutlineBtn.disabled = historyState.cursor <= 0 && !historyStatus;
|
||||
} else {
|
||||
userOutlinePanel.innerHTML = `${items.map((item) => {
|
||||
if (item.type === 'date') {
|
||||
@@ -2419,6 +2466,11 @@
|
||||
}
|
||||
|
||||
function scrollAssistantBubbleToLastSection(bubble) {
|
||||
const messageEl = bubble?.closest('.msg.assistant');
|
||||
if (messageEl?.classList.contains('assistant-single-collapsed')) {
|
||||
// 单条长回复的末段可能在裁剪区域,先展开再计算定位坐标。
|
||||
bubble.querySelector('.msg-replies-toggle')?.click();
|
||||
}
|
||||
const target = getAssistantLastSectionTarget(bubble);
|
||||
if (!target) return false;
|
||||
const rect = getRangeRectFromTextPosition(target.node, target.offset) || target.scope.getBoundingClientRect();
|
||||
@@ -2477,11 +2529,176 @@
|
||||
if (!bubble) return;
|
||||
let button = bubble.querySelector(`:scope .${ASSISTANT_LAST_SECTION_BUTTON_CLASS}`);
|
||||
const hasTarget = !!getAssistantLastSectionTarget(bubble);
|
||||
if (!button && !hasTarget) return;
|
||||
if (!button) button = createAssistantLastSectionButton();
|
||||
button.hidden = !hasTarget;
|
||||
button.disabled = !hasTarget;
|
||||
getMessageActionRow(bubble)?.appendChild(button);
|
||||
if (button || hasTarget) {
|
||||
if (!button) button = createAssistantLastSectionButton();
|
||||
button.hidden = !hasTarget;
|
||||
button.disabled = !hasTarget;
|
||||
getMessageActionRow(bubble)?.appendChild(button);
|
||||
}
|
||||
syncAssistantReplyControls(messageEl);
|
||||
}
|
||||
|
||||
function getAssistantReplySections(content) {
|
||||
const source = typeof content === 'string' ? content : Array.isArray(content)
|
||||
? content.filter((block) => block?.type === 'text').map((block) => block.text || '').join('\n\n')
|
||||
: '';
|
||||
if (!source) return [];
|
||||
const sections = [];
|
||||
let start = 0;
|
||||
let offset = 0;
|
||||
let fence = null;
|
||||
// 原生分界独占一行;围栏内的同名 HTML 属于用户可见代码,不能作为回复边界。
|
||||
for (const line of source.split('\n')) {
|
||||
const marker = line.match(/^\s{0,3}(`{3,}|~{3,})/);
|
||||
if (marker) {
|
||||
if (!fence) fence = marker[1];
|
||||
else if (marker[1][0] === fence[0] && marker[1].length >= fence.length) fence = null;
|
||||
} else if (!fence && /^\s*<div\s+class=["']agent-message-divider["'][^>]*>[\s\S]*<\/div>\s*$/.test(line)) {
|
||||
const section = source.slice(start, offset).replace(/^(?:[ \t]*\r?\n)+|(?:\r?\n[ \t]*)+$/g, '');
|
||||
if (section.trim()) sections.push(section);
|
||||
start = offset + line.length + 1;
|
||||
} else if (!fence && /^\s*<div\s+class=["']agent-message-divider(?:["']|$)/.test(line)
|
||||
&& offset + line.length >= source.length) {
|
||||
// 分界 HTML 尚在流式传输时,复制继续取上一条完整回复。
|
||||
const section = source.slice(start, offset).replace(/^(?:[ \t]*\r?\n)+|(?:\r?\n[ \t]*)+$/g, '');
|
||||
if (section.trim()) sections.push(section);
|
||||
return sections;
|
||||
}
|
||||
offset += line.length + 1;
|
||||
}
|
||||
const last = source.slice(start).replace(/^(?:[ \t]*\r?\n)+|(?:\r?\n[ \t]*)+$/g, '');
|
||||
if (last.trim()) sections.push(last);
|
||||
return sections;
|
||||
}
|
||||
|
||||
function getAssistantReplySource(messageEl) {
|
||||
return messageEl?.__assistantContent ?? messageEl?.__sessionMessage?.content ?? '';
|
||||
}
|
||||
|
||||
function getAssistantLastReplyText(messageEl) {
|
||||
const sections = getAssistantReplySections(getAssistantReplySource(messageEl));
|
||||
return sections[sections.length - 1] || '';
|
||||
}
|
||||
|
||||
function shouldCollapseAssistantReplies(sections, blockCount) {
|
||||
return sections.length > 0 && (sections.join('\n\n').length > ASSISTANT_REPLY_COLLAPSE_CHARS
|
||||
|| blockCount >= ASSISTANT_REPLY_COLLAPSE_BLOCKS);
|
||||
}
|
||||
|
||||
function createAssistantReplyCopyButton() {
|
||||
const button = document.createElement('button');
|
||||
button.type = 'button';
|
||||
button.className = ASSISTANT_COPY_REPLY_BUTTON_CLASS;
|
||||
button.title = '复制最后一段回复';
|
||||
button.setAttribute('aria-label', '复制最后一段回复');
|
||||
button.innerHTML = `<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="11" height="11" rx="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v1"></path></svg>`;
|
||||
button.addEventListener('click', (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
const messageEl = button.closest('.msg.assistant');
|
||||
copyTextToClipboard(getAssistantLastReplyText(messageEl), '最后一段回复已复制');
|
||||
});
|
||||
return button;
|
||||
}
|
||||
|
||||
function markAssistantEarlierReplies(bubble, divider) {
|
||||
const earlier = [];
|
||||
bubble.querySelectorAll('.assistant-earlier-reply').forEach((element) => element.classList.remove('assistant-earlier-reply'));
|
||||
const visit = (container) => {
|
||||
for (const child of container.children) {
|
||||
if (child === divider) break;
|
||||
if (child.classList.contains('agent-message-divider')) {
|
||||
child.classList.add('assistant-earlier-reply');
|
||||
earlier.push(child);
|
||||
continue;
|
||||
}
|
||||
if (child.matches(ASSISTANT_LAST_SECTION_SKIP_SELECTOR)) continue;
|
||||
if (child.contains(divider)) visit(child);
|
||||
else if (child.compareDocumentPosition(divider) & Node.DOCUMENT_POSITION_FOLLOWING) {
|
||||
child.classList.add('assistant-earlier-reply');
|
||||
earlier.push(child);
|
||||
}
|
||||
}
|
||||
};
|
||||
visit(bubble);
|
||||
return earlier;
|
||||
}
|
||||
|
||||
function getAssistantSingleReplyContainer(bubble) {
|
||||
const existing = bubble.querySelector(':scope > .msg-text, :scope > .assistant-reply-content, :scope > .cross-conversation-reply-body');
|
||||
if (existing) {
|
||||
existing.classList.add('assistant-reply-content');
|
||||
return existing;
|
||||
}
|
||||
const content = Array.from(bubble.children).filter((element) => !element.matches(ASSISTANT_LAST_SECTION_SKIP_SELECTOR));
|
||||
if (content.length === 0) return null;
|
||||
const container = document.createElement('div');
|
||||
container.className = 'assistant-reply-content';
|
||||
bubble.insertBefore(container, content[0]);
|
||||
content.forEach((element) => container.appendChild(element));
|
||||
return container;
|
||||
}
|
||||
|
||||
function syncAssistantReplyControls(messageEl) {
|
||||
const bubble = messageEl.querySelector(':scope > .msg-bubble');
|
||||
if (!bubble) return;
|
||||
const sections = getAssistantReplySections(getAssistantReplySource(messageEl));
|
||||
const hasText = sections.length > 0;
|
||||
let copy = bubble.querySelector(`.${ASSISTANT_COPY_REPLY_BUTTON_CLASS}`);
|
||||
if (!copy && hasText) copy = createAssistantReplyCopyButton();
|
||||
if (copy) {
|
||||
copy.hidden = !hasText;
|
||||
copy.disabled = !hasText;
|
||||
getMessageActionRow(bubble)?.appendChild(copy);
|
||||
}
|
||||
|
||||
const dividers = Array.from(bubble.querySelectorAll('.agent-message-divider'))
|
||||
.filter((element) => !element.closest('pre, code'));
|
||||
const divider = dividers[dividers.length - 1];
|
||||
const canCollapse = shouldCollapseAssistantReplies(sections, collectAssistantTextScopes(bubble).length);
|
||||
const multipleReplies = sections.length > 1 && !!divider;
|
||||
let toggle = bubble.querySelector('.msg-replies-toggle');
|
||||
if (!canCollapse) {
|
||||
messageEl.classList.remove('assistant-history-collapsed');
|
||||
messageEl.classList.remove('assistant-single-collapsed');
|
||||
toggle?.remove();
|
||||
bubble.querySelectorAll('.assistant-earlier-reply').forEach((element) => element.classList.remove('assistant-earlier-reply'));
|
||||
return;
|
||||
}
|
||||
if (multipleReplies) {
|
||||
markAssistantEarlierReplies(bubble, divider);
|
||||
} else {
|
||||
if (!getAssistantSingleReplyContainer(bubble)) return;
|
||||
}
|
||||
if (!toggle) {
|
||||
toggle = document.createElement('button');
|
||||
toggle.type = 'button';
|
||||
toggle.className = 'msg-replies-toggle';
|
||||
toggle.addEventListener('click', (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
const expanded = messageEl.classList.contains('assistant-history-collapsed')
|
||||
|| messageEl.classList.contains('assistant-single-collapsed');
|
||||
messageEl.__assistantRepliesExpanded = expanded;
|
||||
const key = `${currentSessionId || ''}:${messageEl.dataset.messageKey || messageEl.dataset.messageId || ''}`;
|
||||
assistantReplyExpansion.set(key, expanded);
|
||||
if (assistantReplyExpansion.size > 500) assistantReplyExpansion.delete(assistantReplyExpansion.keys().next().value);
|
||||
syncAssistantReplyControls(messageEl);
|
||||
updateScrollbar();
|
||||
});
|
||||
bubble.insertBefore(toggle, bubble.firstChild);
|
||||
}
|
||||
if (!bubble.id) bubble.id = createLocalId('assistant-reply');
|
||||
const key = `${currentSessionId || ''}:${messageEl.dataset.messageKey || messageEl.dataset.messageId || ''}`;
|
||||
const expanded = messageEl.__assistantRepliesExpanded ?? assistantReplyExpansion.get(key) ?? false;
|
||||
messageEl.classList.toggle('assistant-history-collapsed', multipleReplies && !expanded);
|
||||
messageEl.classList.toggle('assistant-single-collapsed', !multipleReplies && !expanded);
|
||||
toggle.setAttribute('aria-controls', bubble.id);
|
||||
toggle.setAttribute('aria-expanded', expanded ? 'true' : 'false');
|
||||
toggle.textContent = multipleReplies
|
||||
? expanded ? '收起较早回复' : `展开较早回复(${sections.length - 1} 段)`
|
||||
: expanded ? '收起长回复' : '展开完整回复';
|
||||
toggle.setAttribute('aria-label', toggle.textContent);
|
||||
}
|
||||
|
||||
function createAssistantBranchButton(messageIndex) {
|
||||
@@ -7081,7 +7298,7 @@
|
||||
cursor: 0,
|
||||
remaining: 0,
|
||||
total: 0,
|
||||
available: false,
|
||||
available: true,
|
||||
loading: false,
|
||||
error: '',
|
||||
};
|
||||
@@ -7092,28 +7309,35 @@
|
||||
if (!historyLoadMore) return;
|
||||
const state = currentHistoryState;
|
||||
historyLoadMore.innerHTML = '';
|
||||
historyLoadMore.hidden = !state.sessionId || state.cursor <= 0;
|
||||
historyLoadMore.hidden = !state.sessionId || (state.cursor <= 0 && state.available);
|
||||
if (historyLoadMore.hidden) return;
|
||||
|
||||
const copy = document.createElement('span');
|
||||
copy.className = 'history-load-more-copy';
|
||||
if (!state.available) {
|
||||
copy.textContent = `还有 ${state.cursor} 条更早消息,但原始记录不可用,无法恢复。`;
|
||||
// 来源状态只提示更早记录待核实,已保存消息仍按游标分页。
|
||||
const sourceNotice = state.available
|
||||
? ''
|
||||
: '更早的记录尚待核实,已保存的消息可正常查看。';
|
||||
if (sourceNotice) {
|
||||
historyLoadMore.classList.add('is-unavailable');
|
||||
historyLoadMore.appendChild(copy);
|
||||
return;
|
||||
} else {
|
||||
historyLoadMore.classList.remove('is-unavailable');
|
||||
}
|
||||
historyLoadMore.classList.remove('is-unavailable');
|
||||
copy.textContent = state.error
|
||||
? state.error
|
||||
: `还有 ${state.cursor} 条更早消息`;
|
||||
copy.textContent = [
|
||||
state.error || (state.cursor > 0
|
||||
? `还有 ${state.cursor} 条已保存的更早消息`
|
||||
: '已显示全部已保存消息。'),
|
||||
sourceNotice,
|
||||
].filter(Boolean).join(' ');
|
||||
historyLoadMore.appendChild(copy);
|
||||
if (state.cursor <= 0) return;
|
||||
const button = document.createElement('button');
|
||||
button.type = 'button';
|
||||
button.className = 'history-load-more-button';
|
||||
button.textContent = state.loading ? '正在加载…' : (state.error ? '重试' : '查看更早消息');
|
||||
button.disabled = state.loading;
|
||||
button.addEventListener('click', requestOlderHistory);
|
||||
historyLoadMore.append(copy, button);
|
||||
historyLoadMore.appendChild(button);
|
||||
}
|
||||
|
||||
function syncHistoryLoadControl(payload = null) {
|
||||
@@ -7143,7 +7367,7 @@
|
||||
|
||||
function requestOlderHistory() {
|
||||
const state = currentHistoryState;
|
||||
if (!state.sessionId || state.sessionId !== currentSessionId || state.cursor <= 0 || !state.available) return;
|
||||
if (!state.sessionId || state.sessionId !== currentSessionId || state.cursor <= 0) return;
|
||||
if (activeHistoryPageRequest || !ws || ws.readyState !== 1 || !wsAuthenticated) return;
|
||||
historyPageRequestSeq += 1;
|
||||
const requestId = `history-page-${Date.now()}-${historyPageRequestSeq}`;
|
||||
@@ -7164,7 +7388,7 @@
|
||||
|
||||
function requestOutlineHistoryPage() {
|
||||
const state = currentOutlineHistoryState;
|
||||
if (!state.sessionId || state.sessionId !== currentSessionId || state.cursor <= 0 || !state.available) return;
|
||||
if (!state.sessionId || state.sessionId !== currentSessionId || state.cursor <= 0) return;
|
||||
if (activeOutlineHistoryRequest || !ws || ws.readyState !== 1 || !wsAuthenticated) return;
|
||||
outlineHistoryRequestSeq += 1;
|
||||
const requestId = `outline-history-${Date.now()}-${outlineHistoryRequestSeq}`;
|
||||
@@ -7552,7 +7776,7 @@
|
||||
<div class="code-block-actions">
|
||||
<button class="code-preview-btn mermaid-toggle-btn" type="button" onclick="ccToggleMermaid(this, event)" aria-label="切换 Mermaid 图形与代码">代码</button>
|
||||
<button class="code-preview-btn mermaid-copy-graph-btn" type="button" onclick="ccCopyMermaidDiagram(this, event)" aria-label="复制 Mermaid 图形">复制图</button>
|
||||
<button class="code-preview-btn mermaid-download-btn" type="button" onclick="ccDownloadMermaidDiagram(this, event)" aria-label="下载 Mermaid 图形">下载图</button>
|
||||
<button class="code-preview-btn mermaid-download-btn" type="button" onclick="ccDownloadMermaidDiagram(this, event)" title="下载 PNG 图片(2 倍)" aria-label="下载 Mermaid PNG 图片(2 倍)">下载图</button>
|
||||
<button class="code-copy-btn" type="button" onclick="ccCopyCode(this, event)" aria-label="复制 Mermaid 代码">Copy</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -7787,34 +8011,31 @@
|
||||
return serializeMermaidSvg(wrapper);
|
||||
}
|
||||
|
||||
function svgTextToPngBlob(svgText) {
|
||||
function svgTextToPngBlob(svgText, scale = 1) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const svgBlob = new Blob([svgText], { type: 'image/svg+xml;charset=utf-8' });
|
||||
const url = URL.createObjectURL(svgBlob);
|
||||
// 使用 Data URL,避免 SVG Blob 中的 HTML 标签触发 Canvas 安全限制。
|
||||
const url = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgText)}`;
|
||||
const image = new Image();
|
||||
image.onload = () => {
|
||||
try {
|
||||
const canvas = document.createElement('canvas');
|
||||
const width = Math.max(1, image.naturalWidth || image.width || 1200);
|
||||
const height = Math.max(1, image.naturalHeight || image.height || 800);
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
canvas.width = width * scale;
|
||||
canvas.height = height * scale;
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.fillStyle = '#ffffff';
|
||||
ctx.fillRect(0, 0, width, height);
|
||||
ctx.drawImage(image, 0, 0, width, height);
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
ctx.drawImage(image, 0, 0, canvas.width, canvas.height);
|
||||
canvas.toBlob((blob) => {
|
||||
URL.revokeObjectURL(url);
|
||||
if (blob) resolve(blob);
|
||||
else reject(new Error('PNG 导出失败'));
|
||||
}, 'image/png');
|
||||
} catch (error) {
|
||||
URL.revokeObjectURL(url);
|
||||
reject(error);
|
||||
}
|
||||
};
|
||||
image.onerror = () => {
|
||||
URL.revokeObjectURL(url);
|
||||
reject(new Error('SVG 图形读取失败'));
|
||||
};
|
||||
image.src = url;
|
||||
@@ -7893,8 +8114,8 @@
|
||||
const svgText = await getMermaidSvgText(wrapper);
|
||||
if (!svgText) throw new Error('暂无可下载图形');
|
||||
const cid = wrapper.dataset.cid || Date.now();
|
||||
const blob = new Blob([svgText], { type: 'image/svg+xml;charset=utf-8' });
|
||||
downloadBlob(blob, `mermaid-${cid}.svg`);
|
||||
const blob = await svgTextToPngBlob(svgText, 2);
|
||||
downloadBlob(blob, `mermaid-${cid}.png`);
|
||||
showToast('图形已下载');
|
||||
setTemporaryButtonLabel(btn, '已下载', '下载图');
|
||||
} catch (error) {
|
||||
@@ -8461,11 +8682,19 @@
|
||||
}
|
||||
}
|
||||
if (msg.sessionId === currentSessionId && msg.message) {
|
||||
if (reconcileCodexAppSteerSessionMessage(msg.sessionId, msg.message, msg.messageIndex)) {
|
||||
renderSessionList();
|
||||
break;
|
||||
}
|
||||
const isProvisionalCodexAppSteer = !!msg.message.codexAppSteerStatus;
|
||||
const existingElement = !isProvisionalCodexAppSteer
|
||||
? findRenderedSessionMessageByKey(getStableMessageKey(msg.message))
|
||||
: null;
|
||||
if (existingElement) {
|
||||
if (Number.isInteger(msg.messageIndex) && msg.messageIndex >= 0) {
|
||||
markSessionMessageElement(existingElement, msg.messageIndex);
|
||||
currentSessionMessageCount = Math.max(currentSessionMessageCount, msg.messageIndex + 1);
|
||||
}
|
||||
const fingerprint = getMessageRenderFingerprint(msg.message);
|
||||
if (existingElement.dataset.messageFingerprint !== fingerprint) {
|
||||
const messageIndex = Number(existingElement.dataset.messageIndex);
|
||||
@@ -8478,8 +8707,9 @@
|
||||
renderSessionList();
|
||||
break;
|
||||
}
|
||||
const messageIndex = isProvisionalCodexAppSteer ? null : currentSessionMessageCount;
|
||||
if (!isProvisionalCodexAppSteer) currentSessionMessageCount += 1;
|
||||
const messageIndex = isProvisionalCodexAppSteer ? null
|
||||
: Number.isInteger(msg.messageIndex) && msg.messageIndex >= 0 ? msg.messageIndex : currentSessionMessageCount;
|
||||
if (!isProvisionalCodexAppSteer) currentSessionMessageCount = Math.max(currentSessionMessageCount, messageIndex + 1);
|
||||
collectClosedCollabAgentIds([msg.message]).forEach((id) => closedCollabAgentIds.add(id));
|
||||
const welcome = messagesDiv.querySelector('.welcome-msg');
|
||||
if (welcome) welcome.remove();
|
||||
@@ -8636,7 +8866,7 @@
|
||||
|
||||
case 'codex_app_steer_status':
|
||||
if (!isCurrentSessionEvent(msg)) break;
|
||||
updateCodexAppSteerMessage(msg.clientMessageId, msg.status, msg.message);
|
||||
updateCodexAppSteerMessage(msg.clientMessageId, msg.status, msg.message, msg.messageIndex);
|
||||
break;
|
||||
|
||||
case 'codex_app_user_input_request':
|
||||
@@ -8861,6 +9091,9 @@
|
||||
break;
|
||||
}
|
||||
if (pendingNewSessionRequest) pendingNewSessionRequest = null;
|
||||
if (msg.clientMessageId) {
|
||||
updateCodexAppSteerMessage(msg.clientMessageId, 'failed', msg.message || '发送失败');
|
||||
}
|
||||
applyPendingSlashDraftResponse(msg);
|
||||
appendError(msg.message, {
|
||||
transient: msg.transient,
|
||||
@@ -9114,6 +9347,9 @@
|
||||
if (!streamEl) return;
|
||||
const bubble = streamEl.querySelector('.msg-bubble');
|
||||
if (!bubble) return;
|
||||
streamEl.__assistantContent = window.pendingContentBlocks?.length > 0
|
||||
? window.pendingContentBlocks
|
||||
: pendingText;
|
||||
const shouldFollow = isNearBottom();
|
||||
|
||||
if (window.pendingContentBlocks && window.pendingContentBlocks.length > 0) {
|
||||
@@ -9154,8 +9390,8 @@
|
||||
setCodexAppSteerStatusElement(record.element, 'failed', record.message || '插入失败');
|
||||
return false;
|
||||
}
|
||||
if (!isGenerating || !currentSessionRunning || !isCodexAppAgent(currentAgent)) {
|
||||
appendError('当前对话已结束,无法在原 turn 中重试插入。');
|
||||
if (!isCodexAppAgent(currentAgent)) {
|
||||
appendError('当前会话不是 Codex App 会话,无法重试。');
|
||||
setCodexAppSteerStatusElement(record.element, 'failed', record.message || '插入失败');
|
||||
return false;
|
||||
}
|
||||
@@ -9164,9 +9400,9 @@
|
||||
setCodexAppSteerStatusElement(record.element, 'failed', record.message || '插入失败');
|
||||
return false;
|
||||
}
|
||||
|
||||
const startsNewTurn = !isGenerating || !currentSessionRunning;
|
||||
record.inFlight = true;
|
||||
setCodexAppSteerStatusElement(record.element, 'pending', '引导中...');
|
||||
setCodexAppSteerStatusElement(record.element, 'pending', startsNewTurn ? '重新发送中...' : '引导中...');
|
||||
try {
|
||||
send({
|
||||
type: 'message',
|
||||
@@ -9176,7 +9412,9 @@
|
||||
mode: record.mode,
|
||||
agent: record.agent,
|
||||
clientMessageId: id,
|
||||
codexAppSteer: true,
|
||||
});
|
||||
if (startsNewTurn && !isGenerating) startGenerating();
|
||||
return true;
|
||||
} catch (err) {
|
||||
record.inFlight = false;
|
||||
@@ -9245,7 +9483,7 @@
|
||||
retryButton.type = 'button';
|
||||
retryButton.className = 'codex-steer-action codex-steer-retry';
|
||||
retryButton.textContent = '重试';
|
||||
retryButton.setAttribute('aria-label', '重试插入此消息');
|
||||
retryButton.setAttribute('aria-label', '重新发送此消息');
|
||||
retryButton.addEventListener('click', (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
@@ -9271,9 +9509,12 @@
|
||||
return true;
|
||||
}
|
||||
|
||||
function updateCodexAppSteerMessage(clientMessageId, status, message) {
|
||||
function updateCodexAppSteerMessage(clientMessageId, status, message, messageIndex = null) {
|
||||
const id = String(clientMessageId || '').trim();
|
||||
if (!id) return false;
|
||||
const record = codexAppSteerRecords.get(id);
|
||||
if (!record) return false;
|
||||
if (status === 'sent') return completeCodexAppSteerMessage(record, null, messageIndex);
|
||||
const indexed = userMessageIndex.get(id);
|
||||
const element = indexed?.element || messagesDiv.querySelector(`[data-message-id="${cssEscape(id)}"]`);
|
||||
return setCodexAppSteerStatusElement(element, status, message);
|
||||
@@ -9419,6 +9660,7 @@
|
||||
|
||||
function createMsgElement(role, content, attachments = [], meta = {}) {
|
||||
const div = document.createElement('div');
|
||||
if (role === 'assistant') div.__assistantContent = content;
|
||||
const isCrossConversation = !!meta.crossConversation;
|
||||
const isCrossConversationReply = isCrossConversation && !!(meta.crossConversation.reply || meta.crossConversation.replyToRequestId);
|
||||
const canCollapseCrossConversationReply = role === 'assistant' && isCrossConversationReply;
|
||||
@@ -12809,7 +13051,7 @@
|
||||
} else {
|
||||
updateScrollbar();
|
||||
}
|
||||
send({ type: 'message', text, attachments, sessionId: currentSessionId, mode: currentMode, agent: currentAgent, clientMessageId: messageId });
|
||||
send({ type: 'message', text, attachments, sessionId: currentSessionId, mode: currentMode, agent: currentAgent, clientMessageId: messageId, codexAppSteer: true });
|
||||
msgInput.value = '';
|
||||
pendingAttachments = [];
|
||||
renderPendingAttachments();
|
||||
|
||||
@@ -3496,6 +3496,7 @@ html[data-theme='wasteland'] .advanced-search-panel {
|
||||
}
|
||||
.msg-action-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 6px;
|
||||
@@ -3505,7 +3506,8 @@ html[data-theme='wasteland'] .advanced-search-panel {
|
||||
display: none;
|
||||
}
|
||||
.msg-last-section-btn,
|
||||
.msg-branch-btn {
|
||||
.msg-branch-btn,
|
||||
.msg-copy-reply-btn {
|
||||
appearance: none;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
@@ -3523,16 +3525,19 @@ html[data-theme='wasteland'] .advanced-search-panel {
|
||||
transition: opacity 0.15s ease, background 0.15s ease, border-color 0.15s ease, color 0.15s ease, transform 0.15s ease;
|
||||
}
|
||||
.msg-last-section-btn[hidden],
|
||||
.msg-branch-btn[hidden] {
|
||||
.msg-branch-btn[hidden],
|
||||
.msg-copy-reply-btn[hidden] {
|
||||
display: none;
|
||||
}
|
||||
.msg-last-section-btn svg,
|
||||
.msg-branch-btn svg {
|
||||
.msg-branch-btn svg,
|
||||
.msg-copy-reply-btn svg {
|
||||
display: block;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.msg-last-section-btn:hover,
|
||||
.msg-branch-btn:hover {
|
||||
.msg-branch-btn:hover,
|
||||
.msg-copy-reply-btn:hover {
|
||||
opacity: 1;
|
||||
background: var(--bg-tertiary);
|
||||
border-color: var(--accent);
|
||||
@@ -3540,7 +3545,8 @@ html[data-theme='wasteland'] .advanced-search-panel {
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
.msg-last-section-btn:focus-visible,
|
||||
.msg-branch-btn:focus-visible {
|
||||
.msg-branch-btn:focus-visible,
|
||||
.msg-copy-reply-btn:focus-visible {
|
||||
opacity: 1;
|
||||
outline: 2px solid rgba(91, 126, 161, 0.28);
|
||||
outline-offset: 2px;
|
||||
@@ -3548,6 +3554,48 @@ html[data-theme='wasteland'] .advanced-search-panel {
|
||||
.msg-last-section-focus {
|
||||
animation: lastSectionFocus 1.1s ease;
|
||||
}
|
||||
.msg.assistant.assistant-history-collapsed .assistant-earlier-reply {
|
||||
display: none !important;
|
||||
}
|
||||
.msg.assistant.assistant-single-collapsed .assistant-reply-content {
|
||||
max-height: 420px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.msg-replies-toggle {
|
||||
appearance: none;
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
margin: 0 0 10px;
|
||||
padding: 5px 9px;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
background: var(--bg-secondary);
|
||||
color: var(--text-secondary);
|
||||
font: inherit;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
}
|
||||
.msg-replies-toggle:hover {
|
||||
color: var(--accent);
|
||||
border-color: var(--accent);
|
||||
}
|
||||
.msg-replies-toggle:focus-visible {
|
||||
outline: 2px solid var(--accent);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
.msg-last-section-btn,
|
||||
.msg-branch-btn,
|
||||
.msg-copy-reply-btn {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
}
|
||||
.msg-replies-toggle {
|
||||
min-height: 44px;
|
||||
}
|
||||
}
|
||||
@keyframes lastSectionFocus {
|
||||
0% { background: rgba(91, 126, 161, 0.18); }
|
||||
100% { background: transparent; }
|
||||
|
||||
Reference in New Issue
Block a user