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 () {
'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 RENDER_DEBOUNCE = 100;
const COMPOSER_SUGGESTION_DEBOUNCE = 120;
@@ -3066,7 +3066,16 @@
case 'system_message':
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;
case 'codex_app_user_input_request':
@@ -3167,7 +3176,10 @@
break;
}
if (pendingNewSessionRequest) pendingNewSessionRequest = null;
appendError(msg.message);
appendError(msg.message, {
transient: msg.transient,
autoDismissMs: msg.autoDismissMs,
});
clearSessionLoading();
if (!isGenerating && currentSessionId) {
setCurrentSessionRunningState(!!getSessionMeta(currentSessionId)?.isRunning);
@@ -3372,6 +3384,54 @@
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 = {}) {
const div = document.createElement('div');
const isCrossConversation = role === 'user' && !!meta.crossConversation;
@@ -3384,13 +3444,20 @@
}
if (role === 'system') {
const tone = String(meta.tone || 'neutral').trim() || 'neutral';
const bubble = document.createElement('div');
bubble.className = 'msg-bubble';
bubble.dataset.tone = tone;
const text = document.createElement('span');
text.className = 'system-message-text';
text.textContent = content;
bubble.appendChild(text);
const transient = !!meta.transient;
if (transient) {
div.classList.add('transient');
}
const closeBtn = document.createElement('button');
closeBtn.type = 'button';
closeBtn.className = 'system-message-close';
@@ -3404,6 +3471,9 @@
});
bubble.appendChild(closeBtn);
div.appendChild(bubble);
if (transient) {
scheduleTransientMessageRemoval(div, meta.autoDismissMs || 6000);
}
return div;
}
@@ -3489,6 +3559,9 @@
hydrateAttachmentPreviews(bubble, attachments);
div.appendChild(avatar);
div.appendChild(bubble);
if (role === 'user' && meta.codexAppSteerStatus) {
setCodexAppSteerStatusElement(div, meta.codexAppSteerStatus, meta.codexAppSteerMessage);
}
if (role === 'user') {
registerUserMessage(resolvedMessageId, div, content);
}
@@ -4575,19 +4648,19 @@
overlay.addEventListener('click', (e) => { if (e.target === overlay) close(); });
}
function appendSystemMessage(message) {
function appendSystemMessage(message, options = {}) {
const welcome = messagesDiv.querySelector('.welcome-msg');
if (welcome) welcome.remove();
messagesDiv.appendChild(createMsgElement('system', message));
messagesDiv.appendChild(createMsgElement('system', message, [], options));
scrollToBottom();
}
function appendError(message) {
const div = document.createElement('div');
div.className = 'msg system';
div.innerHTML = `<div class="msg-bubble" style="border-color:var(--danger);color:var(--danger)">⚠ ${escapeHtml(message)}</div>`;
messagesDiv.appendChild(div);
scrollToBottom();
function appendError(message, options = {}) {
appendSystemMessage(`${message}`, {
tone: 'danger',
transient: options.transient !== false,
autoDismissMs: options.autoDismissMs || 7000,
});
}
function scrollToBottom() {
@@ -5260,12 +5333,12 @@
return;
}
const messageId = createLocalId('user');
const element = createMsgElement('user', text, [], { messageId });
const element = createMsgElement('user', text, [], { messageId, codexAppSteerStatus: 'pending' });
messagesDiv.appendChild(element);
registerUserMessage(messageId, element, text);
updateUserOutlinePanel();
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 = '';
autoResize();
return;

View File

@@ -14,7 +14,7 @@
document.documentElement.dataset.dividerTime = dividerTime;
})();
</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">
</head>
<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/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>
</html>

View File

@@ -1901,6 +1901,64 @@ body.session-loading-active {
border-bottom-right-radius: 4px;
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 {
position: absolute;
top: 8px;
@@ -2082,6 +2140,23 @@ body.session-loading-active {
text-align: center;
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 {
display: block;
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' }));
await nextMessage(messages, ws, (msg) => msg.type === 'session_list' && msg.sessions.some((s) => s.id === codexAppSession.sessionId && s.isRunning));
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 || ''));
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);
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');

View File

@@ -4583,6 +4583,12 @@ function normalizeCodexAppUserInputAnswers(rawAnswers = {}) {
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 = {}) {
if (!routed?.entry?.ws) {
return Promise.resolve({ answers: {} });
@@ -5046,8 +5052,23 @@ function handleCodexAppSteerMessage(ws, msg, options = {}) {
const sessionId = sanitizeId(msg?.sessionId || '');
const entry = activeCodexAppTurns.get(sessionId);
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) => {
sendSteerStatus('failed', '插入失败');
wsSend(ws, { type: 'error', code, message });
return { ok: false, code, message };
};
@@ -5099,18 +5120,32 @@ function handleCodexAppSteerMessage(ws, msg, options = {}) {
wsSend(ws, { type: 'session_message', sessionId, message: persistedUserMessage });
}
sendSteerStatus('pending', '引导中...');
const input = codexAppInputFromMessage(runtimeTextValue, []);
const expectedTurnId = entry.turnId;
codexAppClient.request('turn/steer', {
threadId: entry.threadId,
expectedTurnId,
input,
clientUserMessageId: crypto.randomUUID(),
}, 60000).catch((err) => {
clientUserMessageId: clientMessageId || crypto.randomUUID(),
}, 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, {
type: 'error',
sessionId,
code: 'codexapp_steer_failed',
transient: true,
autoDismissMs: 7000,
message: formatRuntimeError('codex', err?.message || err, { exitCode: null, signal: null }),
});
});