fix: show codexapp steer insertion status
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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');
|
||||
|
||||
39
server.js
39
server.js
@@ -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 }),
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user