修复完整会话历史与长气泡交互并更新 CentOS 7 发布包

提交当前工作区全部源码、回归脚本、任务记录及重新构建的 Bun baseline 发布包。发布单文件 MCP 初始化及前端资源一致性验证通过。
This commit is contained in:
shiyue
2026-10-10 23:40:16 +08:00
parent 747771a048
commit 69b6db0f5b
71 changed files with 9536 additions and 503 deletions

View File

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

View File

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