fix: show codexapp steer insertion status

This commit is contained in:
shiyue
2026-06-15 13:48:40 +08:00
parent ed3238fa49
commit 0849666a6e
5 changed files with 229 additions and 18 deletions

View File

@@ -2,7 +2,7 @@
(function () { (function () {
'use strict'; 'use strict';
const ASSET_VERSION = '20260615-reload-mcp'; const ASSET_VERSION = '20260615-codexapp-steer-status';
const WS_URL = `${location.protocol === 'https:' ? 'wss' : 'ws'}://${location.host}/ws`; const WS_URL = `${location.protocol === 'https:' ? 'wss' : 'ws'}://${location.host}/ws`;
const RENDER_DEBOUNCE = 100; const RENDER_DEBOUNCE = 100;
const COMPOSER_SUGGESTION_DEBOUNCE = 120; const COMPOSER_SUGGESTION_DEBOUNCE = 120;
@@ -3066,7 +3066,16 @@
case 'system_message': case 'system_message':
if (!isCurrentSessionEvent(msg)) break; if (!isCurrentSessionEvent(msg)) break;
appendSystemMessage(msg.message); appendSystemMessage(msg.message, {
tone: msg.tone,
transient: msg.transient,
autoDismissMs: msg.autoDismissMs,
});
break;
case 'codex_app_steer_status':
if (!isCurrentSessionEvent(msg)) break;
updateCodexAppSteerMessage(msg.clientMessageId, msg.status, msg.message);
break; break;
case 'codex_app_user_input_request': case 'codex_app_user_input_request':
@@ -3167,7 +3176,10 @@
break; break;
} }
if (pendingNewSessionRequest) pendingNewSessionRequest = null; if (pendingNewSessionRequest) pendingNewSessionRequest = null;
appendError(msg.message); appendError(msg.message, {
transient: msg.transient,
autoDismissMs: msg.autoDismissMs,
});
clearSessionLoading(); clearSessionLoading();
if (!isGenerating && currentSessionId) { if (!isGenerating && currentSessionId) {
setCurrentSessionRunningState(!!getSessionMeta(currentSessionId)?.isRunning); setCurrentSessionRunningState(!!getSessionMeta(currentSessionId)?.isRunning);
@@ -3372,6 +3384,54 @@
catch { return escapeHtml(text); } catch { return escapeHtml(text); }
} }
function codexAppSteerStatusLabel(status) {
if (status === 'inserted') return '已插入';
if (status === 'failed') return '插入失败';
return '引导中...';
}
function setCodexAppSteerStatusElement(element, status, message) {
if (!element) return false;
const normalized = ['pending', 'inserted', 'failed'].includes(status) ? status : 'pending';
element.classList.add('codex-steer-message');
element.classList.toggle('codex-steer-pending', normalized === 'pending');
element.classList.toggle('codex-steer-inserted', normalized === 'inserted');
element.classList.toggle('codex-steer-failed', normalized === 'failed');
const bubble = element.querySelector('.msg-bubble');
if (!bubble) return false;
let statusEl = bubble.querySelector('.codex-steer-status');
if (!statusEl) {
statusEl = document.createElement('div');
statusEl.className = 'codex-steer-status';
bubble.appendChild(statusEl);
}
statusEl.dataset.status = normalized;
statusEl.textContent = message || codexAppSteerStatusLabel(normalized);
return true;
}
function updateCodexAppSteerMessage(clientMessageId, status, message) {
const id = String(clientMessageId || '').trim();
if (!id) return false;
const indexed = userMessageIndex.get(id);
const element = indexed?.element || messagesDiv.querySelector(`[data-message-id="${cssEscape(id)}"]`);
return setCodexAppSteerStatusElement(element, status, message);
}
function scheduleTransientMessageRemoval(element, timeoutMs) {
const ttl = Number(timeoutMs);
if (!element || !Number.isFinite(ttl) || ttl <= 0) return;
window.setTimeout(() => {
if (!element || !element.isConnected) return;
element.classList.add('is-dismissing');
window.setTimeout(() => {
if (!element || !element.isConnected) return;
element.remove();
updateScrollbar();
}, 220);
}, ttl);
}
function createMsgElement(role, content, attachments = [], meta = {}) { function createMsgElement(role, content, attachments = [], meta = {}) {
const div = document.createElement('div'); const div = document.createElement('div');
const isCrossConversation = role === 'user' && !!meta.crossConversation; const isCrossConversation = role === 'user' && !!meta.crossConversation;
@@ -3384,13 +3444,20 @@
} }
if (role === 'system') { if (role === 'system') {
const tone = String(meta.tone || 'neutral').trim() || 'neutral';
const bubble = document.createElement('div'); const bubble = document.createElement('div');
bubble.className = 'msg-bubble'; bubble.className = 'msg-bubble';
bubble.dataset.tone = tone;
const text = document.createElement('span'); const text = document.createElement('span');
text.className = 'system-message-text'; text.className = 'system-message-text';
text.textContent = content; text.textContent = content;
bubble.appendChild(text); bubble.appendChild(text);
const transient = !!meta.transient;
if (transient) {
div.classList.add('transient');
}
const closeBtn = document.createElement('button'); const closeBtn = document.createElement('button');
closeBtn.type = 'button'; closeBtn.type = 'button';
closeBtn.className = 'system-message-close'; closeBtn.className = 'system-message-close';
@@ -3404,6 +3471,9 @@
}); });
bubble.appendChild(closeBtn); bubble.appendChild(closeBtn);
div.appendChild(bubble); div.appendChild(bubble);
if (transient) {
scheduleTransientMessageRemoval(div, meta.autoDismissMs || 6000);
}
return div; return div;
} }
@@ -3489,6 +3559,9 @@
hydrateAttachmentPreviews(bubble, attachments); hydrateAttachmentPreviews(bubble, attachments);
div.appendChild(avatar); div.appendChild(avatar);
div.appendChild(bubble); div.appendChild(bubble);
if (role === 'user' && meta.codexAppSteerStatus) {
setCodexAppSteerStatusElement(div, meta.codexAppSteerStatus, meta.codexAppSteerMessage);
}
if (role === 'user') { if (role === 'user') {
registerUserMessage(resolvedMessageId, div, content); registerUserMessage(resolvedMessageId, div, content);
} }
@@ -4575,19 +4648,19 @@
overlay.addEventListener('click', (e) => { if (e.target === overlay) close(); }); overlay.addEventListener('click', (e) => { if (e.target === overlay) close(); });
} }
function appendSystemMessage(message) { function appendSystemMessage(message, options = {}) {
const welcome = messagesDiv.querySelector('.welcome-msg'); const welcome = messagesDiv.querySelector('.welcome-msg');
if (welcome) welcome.remove(); if (welcome) welcome.remove();
messagesDiv.appendChild(createMsgElement('system', message)); messagesDiv.appendChild(createMsgElement('system', message, [], options));
scrollToBottom(); scrollToBottom();
} }
function appendError(message) { function appendError(message, options = {}) {
const div = document.createElement('div'); appendSystemMessage(`${message}`, {
div.className = 'msg system'; tone: 'danger',
div.innerHTML = `<div class="msg-bubble" style="border-color:var(--danger);color:var(--danger)">⚠ ${escapeHtml(message)}</div>`; transient: options.transient !== false,
messagesDiv.appendChild(div); autoDismissMs: options.autoDismissMs || 7000,
scrollToBottom(); });
} }
function scrollToBottom() { function scrollToBottom() {
@@ -5260,12 +5333,12 @@
return; return;
} }
const messageId = createLocalId('user'); const messageId = createLocalId('user');
const element = createMsgElement('user', text, [], { messageId }); const element = createMsgElement('user', text, [], { messageId, codexAppSteerStatus: 'pending' });
messagesDiv.appendChild(element); messagesDiv.appendChild(element);
registerUserMessage(messageId, element, text); registerUserMessage(messageId, element, text);
updateUserOutlinePanel(); updateUserOutlinePanel();
scrollToBottom(); scrollToBottom();
send({ type: 'message', text, sessionId: currentSessionId, mode: currentMode, agent: currentAgent }); send({ type: 'message', text, sessionId: currentSessionId, mode: currentMode, agent: currentAgent, clientMessageId: messageId });
msgInput.value = ''; msgInput.value = '';
autoResize(); autoResize();
return; return;

View File

@@ -14,7 +14,7 @@
document.documentElement.dataset.dividerTime = dividerTime; document.documentElement.dataset.dividerTime = dividerTime;
})(); })();
</script> </script>
<link rel="stylesheet" href="style.css?v=20260615-reload-mcp"> <link rel="stylesheet" href="style.css?v=20260615-codexapp-steer-status">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/atom-one-dark.min.css"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/atom-one-dark.min.css">
</head> </head>
<body> <body>
@@ -150,6 +150,6 @@
<script src="https://cdnjs.cloudflare.com/ajax/libs/marked/12.0.1/marked.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/marked/12.0.1/marked.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
<script src="app.js?v=20260615-reload-mcp"></script> <script src="app.js?v=20260615-codexapp-steer-status"></script>
</body> </body>
</html> </html>

View File

@@ -1901,6 +1901,64 @@ body.session-loading-active {
border-bottom-right-radius: 4px; border-bottom-right-radius: 4px;
padding-right: 42px; padding-right: 42px;
} }
.msg.user.codex-steer-message .msg-bubble {
padding-bottom: 10px;
}
.msg.user.codex-steer-pending .msg-avatar,
.msg.user.codex-steer-inserted .msg-avatar,
.msg.user.codex-steer-failed .msg-avatar {
background: var(--bg-tertiary);
border: 1px solid var(--border-color);
color: var(--text-muted);
}
.msg.user.codex-steer-pending .msg-bubble {
background: var(--bg-tertiary);
border: 1px dashed var(--border-color);
color: var(--text-secondary);
}
.msg.user.codex-steer-inserted .msg-bubble {
background: var(--bg-tertiary);
border: 1px solid var(--border-color);
color: var(--text-secondary);
opacity: 0.94;
}
.msg.user.codex-steer-failed .msg-bubble {
background: rgba(220, 53, 69, 0.1);
border: 1px solid rgba(220, 53, 69, 0.32);
color: var(--danger);
}
.codex-steer-status {
display: flex;
align-items: center;
gap: 6px;
margin-top: 6px;
font-size: 11px;
font-weight: 700;
line-height: 1.2;
opacity: 0.88;
}
.codex-steer-status::before {
content: '';
width: 6px;
height: 6px;
border-radius: 999px;
background: currentColor;
opacity: 0.72;
}
.codex-steer-status[data-status='pending'] {
color: var(--text-muted);
}
.codex-steer-status[data-status='inserted'] {
color: var(--success);
}
.codex-steer-status[data-status='failed'] {
color: var(--danger);
}
.msg.user.codex-steer-pending .msg-copy-btn {
background: rgba(255, 255, 255, 0.08);
border-color: var(--border-color);
color: var(--text-muted);
}
.msg-copy-btn { .msg-copy-btn {
position: absolute; position: absolute;
top: 8px; top: 8px;
@@ -2082,6 +2140,23 @@ body.session-loading-active {
text-align: center; text-align: center;
white-space: pre-line; white-space: pre-line;
} }
.msg.system.transient .msg-bubble {
border-style: solid;
box-shadow: 0 6px 18px rgba(27, 39, 51, 0.08);
}
.msg.system .msg-bubble[data-tone='danger'] {
border-color: rgba(220, 53, 69, 0.34);
color: var(--danger);
}
.msg.system .msg-bubble[data-tone='info'] {
border-color: rgba(58, 134, 255, 0.26);
color: var(--text-secondary);
}
.msg.system.is-dismissing {
opacity: 0;
transform: translateY(-4px);
transition: opacity 0.22s ease, transform 0.22s ease;
}
.msg.system .system-message-text { .msg.system .system-message-text {
display: block; display: block;
padding-right: 26px; padding-right: 26px;

View File

@@ -887,9 +887,37 @@ async function main() {
ws.send(JSON.stringify({ type: 'message', text: 'slow codexapp prompt', sessionId: codexAppSession.sessionId, mode: 'yolo', agent: 'codexapp' })); ws.send(JSON.stringify({ type: 'message', text: 'slow codexapp prompt', sessionId: codexAppSession.sessionId, mode: 'yolo', agent: 'codexapp' }));
await nextMessage(messages, ws, (msg) => msg.type === 'session_list' && msg.sessions.some((s) => s.id === codexAppSession.sessionId && s.isRunning)); await nextMessage(messages, ws, (msg) => msg.type === 'session_list' && msg.sessions.some((s) => s.id === codexAppSession.sessionId && s.isRunning));
await sleep(150); await sleep(150);
ws.send(JSON.stringify({ type: 'message', text: 'runtime steer insert', sessionId: codexAppSession.sessionId, mode: 'yolo', agent: 'codexapp' })); ws.send(JSON.stringify({
type: 'message',
text: 'runtime steer insert',
sessionId: codexAppSession.sessionId,
mode: 'yolo',
agent: 'codexapp',
clientMessageId: 'regression-steer-message',
}));
const steerPending = await nextMessage(messages, ws, (msg) =>
msg.type === 'codex_app_steer_status' &&
msg.sessionId === codexAppSession.sessionId &&
msg.clientMessageId === 'regression-steer-message' &&
msg.status === 'pending'
);
assert(/引导中/.test(steerPending.message || ''), 'Codex App steer should expose pending status');
const steerDelta = await nextMessage(messages, ws, (msg) => msg.type === 'text_delta' && msg.sessionId === codexAppSession.sessionId && /steer accepted: runtime steer insert/.test(msg.text || '')); const steerDelta = await nextMessage(messages, ws, (msg) => msg.type === 'text_delta' && msg.sessionId === codexAppSession.sessionId && /steer accepted: runtime steer insert/.test(msg.text || ''));
assert(/runtime steer insert/.test(steerDelta.text || ''), 'Codex App running message should use turn/steer'); assert(/runtime steer insert/.test(steerDelta.text || ''), 'Codex App running message should use turn/steer');
const steerInserted = await nextMessage(messages, ws, (msg) =>
msg.type === 'codex_app_steer_status' &&
msg.sessionId === codexAppSession.sessionId &&
msg.clientMessageId === 'regression-steer-message' &&
msg.status === 'inserted'
);
assert(/已插入/.test(steerInserted.message || ''), 'Codex App steer should expose inserted status');
const steerSystemMessage = await nextMessage(messages, ws, (msg) =>
msg.type === 'system_message' &&
msg.sessionId === codexAppSession.sessionId &&
/已引导对话: runtime steer insert/.test(msg.message || '')
);
assert(steerSystemMessage.transient === true, 'Codex App steer marker should be transient');
assert(/已引导对话: runtime steer insert/.test(steerSystemMessage.message || ''), 'Codex App steer should show guided conversation marker with preview');
await nextMessage(messages, ws, (msg) => msg.type === 'done' && msg.sessionId === codexAppSession.sessionId); await nextMessage(messages, ws, (msg) => msg.type === 'done' && msg.sessionId === codexAppSession.sessionId);
storedCodexApp = JSON.parse(fs.readFileSync(path.join(sessionsDir, `${codexAppSession.sessionId}.json`), 'utf8')); storedCodexApp = JSON.parse(fs.readFileSync(path.join(sessionsDir, `${codexAppSession.sessionId}.json`), 'utf8'));
assert(storedCodexApp.codexAppThreadId === codexAppThreadId, 'Codex App follow-up should resume the same app-server thread'); assert(storedCodexApp.codexAppThreadId === codexAppThreadId, 'Codex App follow-up should resume the same app-server thread');

View File

@@ -4583,6 +4583,12 @@ function normalizeCodexAppUserInputAnswers(rawAnswers = {}) {
return { answers }; return { answers };
} }
function previewInlineText(text, maxLength = 36) {
const normalized = String(text || '').replace(/\s+/g, ' ').trim();
if (!normalized) return '空内容';
return normalized.length > maxLength ? `${normalized.slice(0, maxLength - 1)}...` : normalized;
}
function requestCodexAppUserInput(routed, params = {}) { function requestCodexAppUserInput(routed, params = {}) {
if (!routed?.entry?.ws) { if (!routed?.entry?.ws) {
return Promise.resolve({ answers: {} }); return Promise.resolve({ answers: {} });
@@ -5046,8 +5052,23 @@ function handleCodexAppSteerMessage(ws, msg, options = {}) {
const sessionId = sanitizeId(msg?.sessionId || ''); const sessionId = sanitizeId(msg?.sessionId || '');
const entry = activeCodexAppTurns.get(sessionId); const entry = activeCodexAppTurns.get(sessionId);
if (!entry) return null; if (!entry) return null;
const clientMessageId = String(msg?.clientMessageId || '').trim();
const sendSteerStatus = (status, message) => {
const targetWs = entry.ws || ws;
if (!targetWs) return;
const payload = {
type: 'codex_app_steer_status',
sessionId,
status,
message,
};
if (clientMessageId) payload.clientMessageId = clientMessageId;
wsSend(targetWs, payload);
};
const fail = (code, message) => { const fail = (code, message) => {
sendSteerStatus('failed', '插入失败');
wsSend(ws, { type: 'error', code, message }); wsSend(ws, { type: 'error', code, message });
return { ok: false, code, message }; return { ok: false, code, message };
}; };
@@ -5099,18 +5120,32 @@ function handleCodexAppSteerMessage(ws, msg, options = {}) {
wsSend(ws, { type: 'session_message', sessionId, message: persistedUserMessage }); wsSend(ws, { type: 'session_message', sessionId, message: persistedUserMessage });
} }
sendSteerStatus('pending', '引导中...');
const input = codexAppInputFromMessage(runtimeTextValue, []); const input = codexAppInputFromMessage(runtimeTextValue, []);
const expectedTurnId = entry.turnId; const expectedTurnId = entry.turnId;
codexAppClient.request('turn/steer', { codexAppClient.request('turn/steer', {
threadId: entry.threadId, threadId: entry.threadId,
expectedTurnId, expectedTurnId,
input, input,
clientUserMessageId: crypto.randomUUID(), clientUserMessageId: clientMessageId || crypto.randomUUID(),
}, 60000).catch((err) => { }, 60000).then(() => {
sendSteerStatus('inserted', '已插入');
wsSend(entry.ws || ws, {
type: 'system_message',
sessionId,
tone: 'info',
transient: true,
autoDismissMs: 5000,
message: `已引导对话: ${previewInlineText(textValue)}`,
});
}).catch((err) => {
sendSteerStatus('failed', '插入失败');
wsSend(entry.ws || ws, { wsSend(entry.ws || ws, {
type: 'error', type: 'error',
sessionId, sessionId,
code: 'codexapp_steer_failed', code: 'codexapp_steer_failed',
transient: true,
autoDismissMs: 7000,
message: formatRuntimeError('codex', err?.message || err, { exitCode: null, signal: null }), message: formatRuntimeError('codex', err?.message || err, { exitCode: null, signal: null }),
}); });
}); });