feat: enhance codex app and cross-conversation messaging

This commit is contained in:
shiyue
2026-06-13 22:13:30 +08:00
parent 04e15c9c89
commit 4a1c988990
10 changed files with 3740 additions and 179 deletions

View File

@@ -2,8 +2,10 @@
(function () {
'use strict';
const ASSET_VERSION = '20260613-codexapp-tools2';
const WS_URL = `${location.protocol === 'https:' ? 'wss' : 'ws'}://${location.host}/ws`;
const RENDER_DEBOUNCE = 100;
const COMPOSER_SUGGESTION_DEBOUNCE = 120;
const SLASH_COMMANDS = [
{ cmd: '/clear', desc: '清除当前会话' },
@@ -24,6 +26,7 @@
const AGENT_LABELS = {
claude: 'Claude',
codex: 'Codex',
codexapp: 'Codex App',
};
const DEFAULT_AGENT = 'claude';
@@ -99,6 +102,10 @@
let loadedHistorySessionId = null;
let activeSessionLoad = null;
let sidebarSwipe = null;
let activeComposerToken = null;
let composerSuggestionTimer = null;
let composerRequestSeq = 0;
let latestComposerRequestId = '';
let pendingAttachments = [];
let uploadingAttachments = [];
let attachmentPreviewModal = null;
@@ -108,6 +115,7 @@
let currentSessionRunning = false;
let fileBrowserState = null;
let directoryPickerState = null;
let codexAppUserInputModal = null;
let pendingNewSessionRequest = null;
let skipDeleteConfirm = localStorage.getItem('cc-web-skip-delete-confirm') === '1';
let pendingInitialSessionLoad = false;
@@ -143,6 +151,7 @@
const chatCwd = $('#chat-cwd');
const costDisplay = $('#cost-display');
const attachmentTray = $('#attachment-tray');
const pendingNotesTray = $('#pending-notes-tray');
const imageUploadInput = $('#image-upload-input');
const attachBtn = $('#attach-btn');
const messagesDiv = $('#messages');
@@ -172,6 +181,15 @@
return AGENT_LABELS[agent] ? agent : DEFAULT_AGENT;
}
function isCodexLikeAgent(agent) {
const normalized = normalizeAgent(agent);
return normalized === 'codex' || normalized === 'codexapp';
}
function isCodexAppAgent(agent) {
return normalizeAgent(agent) === 'codexapp';
}
function getDraftNoteKey(agent = currentAgent) {
return `draft:${normalizeAgent(agent)}`;
}
@@ -218,9 +236,10 @@
}
if (inputWrapper) inputWrapper.classList.toggle('note-mode-active', active);
if (sendBtn) {
const allowRuntimeInsert = isGenerating && isCodexAppAgent(currentAgent) && !active;
sendBtn.classList.toggle('note-send', active);
sendBtn.title = active ? '记录笔记' : '发送';
sendBtn.hidden = isGenerating ? !active : false;
sendBtn.title = active ? '记录笔记' : (allowRuntimeInsert ? '插入' : '发送');
sendBtn.hidden = isGenerating ? (!active && !allowRuntimeInsert) : false;
}
if (msgInput) {
msgInput.placeholder = active ? '记录笔记,稍后从气泡发送…' : defaultMsgInputPlaceholder;
@@ -240,15 +259,15 @@
function createPendingNoteElement(note) {
const div = document.createElement('div');
div.className = 'msg note';
div.className = 'pending-note';
div.dataset.noteId = note.id;
const avatar = document.createElement('div');
avatar.className = 'msg-avatar note-avatar';
avatar.className = 'note-avatar';
avatar.textContent = 'N';
const bubble = document.createElement('div');
bubble.className = 'msg-bubble note-bubble';
bubble.className = 'note-bubble';
const meta = document.createElement('div');
meta.className = 'note-meta';
@@ -274,24 +293,23 @@
}
function renderPendingNotes(options = {}) {
messagesDiv.querySelectorAll('.msg.note').forEach((node) => node.remove());
if (!pendingNotesTray) return;
pendingNotesTray.innerHTML = '';
const notes = getCurrentNotes(false);
if (!notes || notes.length === 0) {
updateScrollbar();
pendingNotesTray.hidden = true;
if (options.updateScrollbar !== false) updateScrollbar();
return;
}
const welcome = messagesDiv.querySelector('.welcome-msg');
if (welcome) welcome.remove();
const frag = document.createDocumentFragment();
notes.forEach((note) => frag.appendChild(createPendingNoteElement(note)));
messagesDiv.appendChild(frag);
if (options.scroll !== false) {
scrollToBottom();
} else {
updateScrollbar();
pendingNotesTray.appendChild(frag);
pendingNotesTray.hidden = false;
if (options.scrollIntoView !== false && options.scroll !== false) {
pendingNotesTray.scrollTop = pendingNotesTray.scrollHeight;
}
if (options.updateScrollbar !== false) updateScrollbar();
}
function findPendingNote(noteId) {
@@ -336,7 +354,7 @@
function beginEditPendingNote(noteId) {
const found = findPendingNote(noteId);
if (!found) return;
const noteEl = messagesDiv.querySelector(`.msg.note[data-note-id="${noteId}"]`);
const noteEl = pendingNotesTray?.querySelector(`.pending-note[data-note-id="${noteId}"]`);
const bubble = noteEl?.querySelector('.note-bubble');
if (!bubble) return;
@@ -650,6 +668,15 @@
return sessions.find((s) => s.id === sessionId) || null;
}
function compareSessionUpdatedDesc(a, b) {
return new Date(b?.updated || 0) - new Date(a?.updated || 0);
}
function compareSessionPinnedDesc(a, b) {
const pinnedDiff = new Date(b?.pinnedAt || 0) - new Date(a?.pinnedAt || 0);
return pinnedDiff || compareSessionUpdatedDesc(a, b);
}
function shortSessionId(sessionId) {
const value = String(sessionId || '');
return value ? value.slice(0, 8) : '';
@@ -730,6 +757,7 @@
mode: payload.mode || 'yolo',
model: payload.model || '',
agent: normalizeAgent(payload.agent),
pinnedAt: payload.pinnedAt || null,
hasUnread: !!payload.hasUnread,
cwd: payload.cwd || null,
totalCost: typeof payload.totalCost === 'number' ? payload.totalCost : 0,
@@ -832,6 +860,7 @@
snapshot.agent = normalizeAgent(meta.agent || snapshot.agent);
snapshot.hasUnread = !!meta.hasUnread;
snapshot.updated = meta.updated || snapshot.updated;
snapshot.pinnedAt = meta.pinnedAt || null;
snapshot.isRunning = !!meta.isRunning;
}
return snapshot;
@@ -898,6 +927,154 @@
return data || {};
}
function closeCodexAppUserInputModal(sendCancel = false) {
if (!codexAppUserInputModal) return;
const { overlay, escapeHandler, requestId, sessionId } = codexAppUserInputModal;
if (escapeHandler) document.removeEventListener('keydown', escapeHandler);
if (overlay && overlay.parentNode) overlay.parentNode.removeChild(overlay);
codexAppUserInputModal = null;
if (sendCancel && requestId) {
send({
type: 'codex_app_user_input_response',
sessionId,
requestId,
answers: {},
});
}
}
function cssEscape(value) {
if (window.CSS && typeof window.CSS.escape === 'function') return window.CSS.escape(String(value || ''));
return String(value || '').replace(/["\\]/g, '\\$&');
}
function collectCodexAppUserInputAnswers(panel, questions) {
const answers = {};
for (const question of questions) {
const id = String(question?.id || '').trim();
if (!id) continue;
const escapedId = cssEscape(id);
const checked = panel.querySelector(`input[name="codex-ui-${escapedId}"]:checked`);
const values = [];
if (checked) {
if (checked.value === '__other__') {
const input = panel.querySelector(`[data-codex-ui-other="${escapedId}"]`);
const text = String(input?.value || '').trim();
if (text) values.push(text);
} else {
values.push(checked.value);
}
} else {
const input = panel.querySelector(`[data-codex-ui-other="${escapedId}"]`);
const text = String(input?.value || '').trim();
if (text) values.push(text);
}
answers[id] = { answers: values };
}
return answers;
}
function renderCodexAppQuestion(question, index) {
const id = String(question?.id || `q${index}`);
const options = Array.isArray(question?.options) ? question.options : [];
const hasOther = !!question?.isOther || options.length === 0;
const inputType = question?.isSecret ? 'password' : 'text';
const optionHtml = options.map((option, optionIndex) => {
const value = String(option?.label || `选项 ${optionIndex + 1}`);
return `
<label class="codex-user-input-option">
<input type="radio" name="codex-ui-${escapeHtml(id)}" value="${escapeHtml(value)}"${optionIndex === 0 && !hasOther ? ' checked' : ''}>
<span class="codex-user-input-option-copy">
<span class="codex-user-input-option-label">${escapeHtml(value)}</span>
${option?.description ? `<span class="codex-user-input-option-desc">${escapeHtml(option.description)}</span>` : ''}
</span>
</label>
`;
}).join('');
const otherHtml = hasOther ? `
<label class="codex-user-input-option codex-user-input-other-option">
${options.length > 0 ? `<input type="radio" name="codex-ui-${escapeHtml(id)}" value="__other__">` : ''}
<span class="codex-user-input-option-copy">
<span class="codex-user-input-option-label">${options.length > 0 ? '其他' : '回答'}</span>
<input class="codex-user-input-text" type="${inputType}" data-codex-ui-other="${escapeHtml(id)}" autocomplete="off">
</span>
</label>
` : '';
return `
<section class="codex-user-input-question">
<div class="codex-user-input-kicker">${escapeHtml(question?.header || `问题 ${index + 1}`)}</div>
<div class="codex-user-input-prompt">${escapeHtml(question?.question || '请选择一个答案。')}</div>
<div class="codex-user-input-options">
${optionHtml}
${otherHtml}
</div>
</section>
`;
}
function showCodexAppUserInputModal(msg) {
closeCodexAppUserInputModal(true);
const questions = Array.isArray(msg.questions) ? msg.questions : [];
const overlay = document.createElement('div');
overlay.className = 'modal-overlay codex-user-input-overlay';
overlay.innerHTML = `
<div class="modal-panel codex-user-input-panel">
<div class="modal-header">
<span class="modal-title">Codex App 需要输入</span>
<button class="modal-close-btn" type="button" data-codex-ui-cancel>✕</button>
</div>
<div class="modal-body codex-user-input-body">
${questions.length > 0
? questions.map((question, index) => renderCodexAppQuestion(question, index)).join('')
: '<div class="modal-empty">Codex App 没有提供可回答的问题。</div>'}
</div>
<div class="modal-footer">
<button class="modal-btn-secondary" type="button" data-codex-ui-cancel>取消</button>
<button class="modal-btn-primary" type="button" data-codex-ui-submit>提交</button>
</div>
</div>
`;
document.body.appendChild(overlay);
const panel = overlay.querySelector('.codex-user-input-panel');
const escapeHandler = (e) => {
if (e.key === 'Escape') closeCodexAppUserInputModal(true);
};
document.addEventListener('keydown', escapeHandler);
codexAppUserInputModal = {
overlay,
requestId: msg.requestId || '',
sessionId: msg.sessionId || '',
escapeHandler,
};
overlay.querySelectorAll('[data-codex-ui-cancel]').forEach((button) => {
button.addEventListener('click', () => closeCodexAppUserInputModal(true));
});
overlay.addEventListener('click', (e) => {
if (e.target === overlay) closeCodexAppUserInputModal(true);
});
overlay.querySelector('[data-codex-ui-submit]')?.addEventListener('click', () => {
send({
type: 'codex_app_user_input_response',
sessionId: msg.sessionId,
requestId: msg.requestId,
answers: collectCodexAppUserInputAnswers(panel, questions),
});
closeCodexAppUserInputModal(false);
});
panel.querySelectorAll('.codex-user-input-text').forEach((input) => {
input.addEventListener('focus', () => {
const radio = input.closest('.codex-user-input-option')?.querySelector('input[type="radio"]');
if (radio) radio.checked = true;
});
});
panel.querySelector('input, button')?.focus();
}
function closeDirectoryPicker() {
if (!directoryPickerState) return;
const { overlay, escapeHandler } = directoryPickerState;
@@ -1824,26 +2001,66 @@
group.cwd = getSessionEffectiveCwd(session) || group.cwd;
}
}
for (const group of groups) {
group.sessions.sort(compareSessionUpdatedDesc);
}
ungroupedSessions.sort(compareSessionUpdatedDesc);
return {
groups: groups.sort((a, b) => new Date(b.latestUpdated || 0) - new Date(a.latestUpdated || 0)),
ungroupedSessions,
};
}
function splitPinnedSessions(sessionItems) {
const pinnedSessions = [];
const regularSessions = [];
for (const session of sessionItems) {
if (session.pinnedAt) {
pinnedSessions.push(session);
} else {
regularSessions.push(session);
}
}
pinnedSessions.sort(compareSessionPinnedDesc);
regularSessions.sort(compareSessionUpdatedDesc);
return { pinnedSessions, regularSessions };
}
function applySessionPinnedState(sessionId, pinnedAt) {
sessions = sessions.map((session) => (
session.id === sessionId ? { ...session, pinnedAt: pinnedAt || null } : session
));
updateCachedSession(sessionId, (snapshot) => {
snapshot.pinnedAt = pinnedAt || null;
});
renderSessionList();
}
function toggleSessionPinned(session) {
if (!session?.id) return;
const nextPinned = !session.pinnedAt;
const pinnedAt = nextPinned ? new Date().toISOString() : null;
applySessionPinnedState(session.id, pinnedAt);
send({ type: 'set_session_pinned', sessionId: session.id, pinned: nextPinned });
}
function createSessionListItem(session) {
const item = document.createElement('div');
item.className = `session-item${session.id === currentSessionId ? ' active' : ''}`;
const isPinned = !!session.pinnedAt;
item.className = `session-item${session.id === currentSessionId ? ' active' : ''}${isPinned ? ' pinned' : ''}`;
item.dataset.id = session.id;
const sessionCwd = getSessionEffectiveCwd(session);
if (sessionCwd) item.title = sessionCwd;
item.innerHTML = `
<div class="session-item-main">
<span class="session-item-title">${escapeHtml(session.title || 'Untitled')}</span>
${isPinned ? '<span class="session-item-pin-badge" title="已置顶">顶</span>' : ''}
${session.isRunning ? '<span class="session-item-status">运行中</span>' : ''}
</div>
${session.hasUnread ? '<span class="session-unread-dot"></span>' : ''}
<span class="session-item-time">${timeAgo(session.updated)}</span>
<div class="session-item-actions">
<button class="session-item-btn pin${isPinned ? ' active' : ''}" title="${isPinned ? '取消置顶' : '置顶'}" aria-label="${isPinned ? '取消置顶' : '置顶'}">${isPinned ? '✓' : '⇧'}</button>
<button class="session-item-btn copy-id" title="复制 ID">ID</button>
<button class="session-item-btn edit" title="重命名">✎</button>
<button class="session-item-btn delete" title="删除">×</button>
@@ -1857,6 +2074,11 @@
copyTextToClipboard(session.id, '会话 ID 已复制');
return;
}
if (target.classList.contains('pin')) {
e.stopPropagation();
toggleSessionPinned(session);
return;
}
if (target.classList.contains('delete')) {
e.stopPropagation();
const doDelete = () => {
@@ -1928,7 +2150,13 @@
});
}
if (importSessionBtn) {
importSessionBtn.textContent = currentAgent === 'codex' ? '导入本地 Codex 会话' : '导入本地 Claude 会话';
if (isCodexAppAgent(currentAgent)) {
importSessionBtn.textContent = 'Codex App 暂不支持导入';
importSessionBtn.disabled = true;
} else {
importSessionBtn.textContent = currentAgent === 'codex' ? '导入本地 Codex 会话' : '导入本地 Claude 会话';
importSessionBtn.disabled = false;
}
}
}
@@ -1961,7 +2189,7 @@
clearSessionLoading();
setCurrentSessionRunningState(false);
currentCwd = null;
currentModel = currentAgent === 'claude' ? 'opus' : '';
currentModel = isCodexLikeAgent(currentAgent) ? '' : 'opus';
isGenerating = false;
generatingSessionId = null;
pendingText = '';
@@ -2152,7 +2380,7 @@
}
function setStatsDisplay(msg) {
if (currentAgent === 'codex' && msg && msg.totalUsage) {
if (isCodexLikeAgent(currentAgent) && msg && msg.totalUsage) {
const usage = msg.totalUsage;
if ((usage.inputTokens || 0) > 0 || (usage.outputTokens || 0) > 0) {
const cacheText = usage.cachedInputTokens ? ` · cache ${usage.cachedInputTokens}` : '';
@@ -2204,7 +2432,7 @@
DEFAULT_CODEX_MODEL_OPTIONS.forEach((opt) => addBaseOption(opt.value, opt.label, opt.desc));
addBaseOption(currentModel, currentModel, '当前会话模型');
sessions
.filter((s) => normalizeAgent(s.agent) === 'codex' && s.id === currentSessionId)
.filter((s) => isCodexLikeAgent(s.agent) && s.id === currentSessionId)
.forEach((s) => addBaseOption(s.model, s.model, '当前会话已保存模型'));
return options;
@@ -2409,6 +2637,7 @@
cwd: snapshot.cwd || session.cwd || '',
projectName: snapshot.cwd ? getPathLeaf(snapshot.cwd) : session.projectName || '',
title: snapshot.title || session.title,
pinnedAt: snapshot.pinnedAt || session.pinnedAt || null,
}
: session
));
@@ -2471,6 +2700,10 @@
renderSessionList();
break;
case 'session_pinned':
applySessionPinnedState(msg.sessionId, msg.pinnedAt || null);
break;
case 'text_delta':
if (!ensureGeneratingForEvent(msg)) break;
pendingText += msg.text;
@@ -2488,6 +2721,7 @@
case 'tool_start':
if (!ensureGeneratingForEvent(msg)) break;
if (isEmptyReasoningTool({ name: msg.name, input: msg.input, kind: msg.kind || null, meta: msg.meta || null, done: false })) break;
activeToolCalls.set(msg.toolUseId, { name: msg.name, input: msg.input, kind: msg.kind || null, meta: msg.meta || null, done: false });
appendToolCall(msg.toolUseId, msg.name, msg.input, false, msg.kind || null, msg.meta || null);
break;
@@ -2500,9 +2734,10 @@
input: msg.input,
kind: msg.kind || null,
meta: msg.meta || null,
result: msg.result,
done: false,
});
appendToolCall(msg.toolUseId, msg.name, msg.input, false, msg.kind || null, msg.meta || null);
appendToolCall(msg.toolUseId, msg.name, msg.input, false, msg.kind || null, msg.meta || null, msg.result);
}
activeToolCalls.get(msg.toolUseId).done = false;
if (msg.name) activeToolCalls.get(msg.toolUseId).name = msg.name;
@@ -2515,6 +2750,17 @@
case 'tool_end':
if (!isCurrentSessionEvent(msg)) break;
if (!activeToolCalls.has(msg.toolUseId) && !isEmptyReasoningTool({ name: msg.name, input: msg.input, result: msg.result, kind: msg.kind || null, meta: msg.meta || null, done: true })) {
activeToolCalls.set(msg.toolUseId, {
name: msg.name,
input: msg.input,
kind: msg.kind || null,
meta: msg.meta || null,
result: msg.result,
done: true,
});
appendToolCall(msg.toolUseId, msg.name, msg.input, true, msg.kind || null, msg.meta || null, msg.result);
}
if (activeToolCalls.has(msg.toolUseId)) {
activeToolCalls.get(msg.toolUseId).done = true;
if (msg.kind) activeToolCalls.get(msg.toolUseId).kind = msg.kind;
@@ -2558,6 +2804,13 @@
appendSystemMessage(msg.message);
break;
case 'codex_app_user_input_request':
if (msg.sessionId && msg.sessionId !== currentSessionId) {
showToast('Codex App 需要输入', msg.sessionId);
}
showCodexAppUserInputModal(msg);
break;
case 'mode_changed':
if (msg.mode && MODE_LABELS[msg.mode]) {
currentMode = msg.mode;
@@ -2714,6 +2967,10 @@
if (typeof _onCwdSuggestions === 'function') _onCwdSuggestions(msg);
break;
case 'composer_suggestions':
handleComposerSuggestions(msg);
break;
case 'update_info':
if (typeof window._ccOnUpdateInfo === 'function') window._ccOnUpdateInfo(msg);
break;
@@ -2853,7 +3110,8 @@
function createMsgElement(role, content, attachments = [], meta = {}) {
const div = document.createElement('div');
const isCrossConversation = role === 'user' && !!meta.crossConversation;
div.className = `msg ${role}${role === 'assistant' ? ' agent-' + currentAgent : ''}${isCrossConversation ? ' cross-conversation' : ''}`;
const isCrossConversationReply = isCrossConversation && !!(meta.crossConversation.reply || meta.crossConversation.replyToRequestId);
div.className = `msg ${role}${role === 'assistant' ? ' agent-' + currentAgent : ''}${isCrossConversation ? ' cross-conversation' : ''}${isCrossConversationReply ? ' cross-conversation-reply' : ''}`;
if (role === 'system') {
const bubble = document.createElement('div');
@@ -2866,11 +3124,11 @@
const avatar = document.createElement('div');
avatar.className = 'msg-avatar';
if (isCrossConversation) {
avatar.textContent = '↗';
avatar.textContent = isCrossConversationReply ? '↩' : '↗';
} else if (role === 'user') {
avatar.textContent = 'U';
} else if (currentAgent === 'codex') {
avatar.innerHTML = `<img src="/codex.png" width="24" height="24" style="display:block;" alt="Codex">`;
} else if (isCodexLikeAgent(currentAgent)) {
avatar.innerHTML = `<img src="/codex.png" width="24" height="24" style="display:block;" alt="${isCodexAppAgent(currentAgent) ? 'Codex App' : 'Codex'}">`;
} else {
avatar.innerHTML = `<img src="/claude.png" width="24" height="24" style="display:block;" alt="Claude">`;
}
@@ -2888,7 +3146,9 @@
const label = document.createElement('span');
label.className = 'cross-conversation-label';
label.textContent = `来自「${sourceTitle}」的对话`;
label.textContent = isCrossConversationReply
? `来自「${sourceTitle}」的回复`
: `来自「${sourceTitle}」的对话`;
sourceMeta.appendChild(label);
if (sourceId) {
@@ -3090,6 +3350,26 @@
}
}
function reasoningPartText(parts) {
if (!Array.isArray(parts)) return '';
return parts.map((part) => {
if (typeof part === 'string') return part;
if (typeof part?.text === 'string') return part.text;
return '';
}).filter(Boolean).join('\n');
}
function isEmptyReasoningTool(tool) {
if (toolKind(tool) !== 'reasoning') return false;
const resultText = stringifyToolValue(tool?.result).trim();
const input = tool?.input || {};
const inputText = [
reasoningPartText(input.content),
reasoningPartText(input.summary),
].filter(Boolean).join('\n').trim();
return !resultText && !inputText;
}
function toolStateLabel(tool, done) {
if (!done) return 'Running';
if (toolKind(tool) === 'command_execution' && typeof tool?.meta?.exitCode === 'number') {
@@ -3183,6 +3463,7 @@
const FOLD_AT = 3;
let grouped = false;
for (const tc of m.toolCalls) {
if (isEmptyReasoningTool(tc)) continue;
const details = createToolCallElement(tc.id || `saved-${Math.random().toString(36).slice(2)}`, tc, true);
// 散落的 .tool-call 达到 FOLD_AT 个时,移入唯一 .tool-group
@@ -3548,7 +3829,7 @@
const kind = toolKind(tool);
if (tool.name === 'AskUserQuestion') {
details.open = true;
} else if (agent !== 'codex' && !done && kind === 'command_execution') {
} else if (!isCodexLikeAgent(agent) && !done && kind === 'command_execution') {
details.open = true;
}
@@ -3559,7 +3840,7 @@
return details;
}
function appendToolCall(toolUseId, name, input, done, kind = null, meta = null) {
function appendToolCall(toolUseId, name, input, done, kind = null, meta = null, result = undefined) {
const streamEl = document.getElementById('streaming-msg');
if (!streamEl) return;
const bubble = streamEl.querySelector('.msg-bubble');
@@ -3568,6 +3849,8 @@
if (!toolsDiv) { toolsDiv = bubble; }
const tool = { id: toolUseId, name, input, kind, meta, done };
if (result !== undefined) tool.result = result;
if (isEmptyReasoningTool(tool)) return;
// 如果是 todo_list检查是否已存在相同 id 的 todo_list
if (kind === 'todo_list' && input?.id) {
@@ -3672,6 +3955,11 @@
};
nextTool.done = done;
if (result !== undefined) nextTool.result = result;
if (isEmptyReasoningTool(nextTool)) {
activeToolCalls.delete(toolUseId);
el.remove();
return;
}
rememberToolCallTarget(toolUseId, nextTool, el);
const summary = el.querySelector('summary');
if (summary) applyToolSummary(summary, nextTool, done);
@@ -3690,6 +3978,9 @@
if (normalized === 'codex') {
return '删除本会话将同步删去本地 Codex rollout 历史与线程记录,不可恢复。确认删除?';
}
if (normalized === 'codexapp') {
return '删除本会话只会删除 cc-web 中的 Codex App 会话记录,不会清理本地 Codex App 线程历史。确认删除?';
}
return '删除本会话将同步删去本地 Claude 中的会话历史,不可恢复。确认删除?';
}
@@ -3868,7 +4159,26 @@
return;
}
const { groups: projectGroups, ungroupedSessions } = groupSessionsByProject(visibleSessions);
const { pinnedSessions, regularSessions } = splitPinnedSessions(visibleSessions);
if (pinnedSessions.length > 0) {
const pinnedGroupEl = document.createElement('section');
pinnedGroupEl.className = 'session-project-group session-pinned-group';
const pinnedHeader = document.createElement('div');
pinnedHeader.className = 'session-project-header session-pinned-header';
pinnedHeader.innerHTML = `
<span class="session-project-name">置顶</span>
<span class="session-project-header-actions">
<span class="session-project-count">${pinnedSessions.length}</span>
</span>
`;
pinnedGroupEl.appendChild(pinnedHeader);
for (const session of pinnedSessions) {
pinnedGroupEl.appendChild(createSessionListItem(session));
}
sessionList.appendChild(pinnedGroupEl);
}
const { groups: projectGroups, ungroupedSessions } = groupSessionsByProject(regularSessions);
for (const group of projectGroups) {
const groupEl = document.createElement('section');
groupEl.className = 'session-project-group';
@@ -4093,52 +4403,137 @@
}
}
// --- Slash Command Menu ---
function showCmdMenu(filter) {
const filtered = SLASH_COMMANDS.filter(c =>
c.cmd.startsWith(filter) || c.desc.includes(filter.slice(1))
);
// Exact match first (fixes /mode vs /model ambiguity)
filtered.sort((a, b) => (b.cmd === filter ? 1 : 0) - (a.cmd === filter ? 1 : 0));
if (filtered.length === 0) {
// --- Composer Modifier Menu ---
function getLocalSlashSuggestions(query) {
const normalized = String(query || '').replace(/^\//, '').toLowerCase();
const filtered = SLASH_COMMANDS
.filter((item) => {
const cmd = item.cmd.toLowerCase();
const desc = item.desc.toLowerCase();
return !normalized || cmd.includes(normalized) || desc.includes(normalized);
})
.sort((a, b) => (b.cmd === `/${normalized}` ? 1 : 0) - (a.cmd === `/${normalized}` ? 1 : 0));
return filtered.map((item) => ({
kind: 'command',
name: item.cmd,
label: item.cmd,
description: item.desc,
insertion: `${item.cmd} `,
appendSpace: false,
}));
}
function findActiveComposerToken() {
if (!msgInput) return null;
const value = msgInput.value || '';
const cursor = typeof msgInput.selectionStart === 'number' ? msgInput.selectionStart : value.length;
const before = value.slice(0, cursor);
if (!before.includes('\n') && value.startsWith('/') && cursor > 0) {
const nextWhitespace = value.search(/\s/);
const end = nextWhitespace >= 0 ? Math.min(cursor, nextWhitespace) : cursor;
if (cursor <= end || nextWhitespace < 0) {
return { trigger: '/', query: value.slice(1, cursor), start: 0, end: cursor };
}
}
const lineStart = Math.max(before.lastIndexOf('\n'), before.lastIndexOf('\r')) + 1;
const line = before.slice(lineStart);
const match = line.match(/(^|\s)([@$])([^\s]*)$/);
if (!match) return null;
const prefixLength = match[1] ? match[1].length : 0;
const start = lineStart + match.index + prefixLength;
return {
trigger: match[2],
query: match[3] || '',
start,
end: cursor,
};
}
function showCmdMenu(token, items) {
const safeItems = Array.isArray(items) ? items : [];
if (!token || safeItems.length === 0) {
hideCmdMenu();
return;
}
activeComposerToken = token;
cmdMenuIndex = 0;
cmdMenu.innerHTML = filtered.map((c, i) =>
`<div class="cmd-item${i === 0 ? ' active' : ''}" data-cmd="${c.cmd}">
<span class="cmd-item-cmd">${c.cmd}</span>
<span class="cmd-item-desc">${c.desc}</span>
</div>`
).join('');
cmdMenu.innerHTML = safeItems.map((item, i) => {
const kindLabel = item.kind === 'skill'
? 'Skill'
: item.kind === 'prompt'
? 'Prompt'
: item.kind === 'file'
? (item.itemType === 'directory' ? 'Dir' : 'File')
: 'Cmd';
return `<div class="cmd-item${i === 0 ? ' active' : ''}" data-index="${i}">
<span class="cmd-item-kind">${kindLabel}</span>
<span class="cmd-item-main">
<span class="cmd-item-cmd">${escapeHtml(item.label || item.name || item.insertion || '')}</span>
<span class="cmd-item-desc">${escapeHtml(item.description || item.title || '')}</span>
</span>
</div>`;
}).join('');
cmdMenu._items = safeItems;
cmdMenu.hidden = false;
// Click handlers
cmdMenu.querySelectorAll('.cmd-item').forEach(el => {
cmdMenu.querySelectorAll('.cmd-item').forEach((el) => {
el.addEventListener('click', () => {
const cmd = el.dataset.cmd;
if (cmd === '/model') {
hideCmdMenu();
msgInput.value = '';
showModelPicker();
return;
}
if (cmd === '/mode') {
hideCmdMenu();
msgInput.value = '';
showModePicker();
return;
}
msgInput.value = cmd + ' ';
hideCmdMenu();
msgInput.focus();
const index = Number.parseInt(el.dataset.index || '-1', 10);
selectComposerItemByIndex(index);
});
});
}
function requestComposerSuggestions() {
const token = findActiveComposerToken();
if (!token || noteMode) {
hideCmdMenu();
return;
}
if (token.trigger === '/') {
showCmdMenu(token, getLocalSlashSuggestions(token.query));
return;
}
clearTimeout(composerSuggestionTimer);
composerSuggestionTimer = setTimeout(() => {
const liveToken = findActiveComposerToken();
if (!liveToken || liveToken.trigger !== token.trigger || liveToken.start !== token.start) {
hideCmdMenu();
return;
}
const requestId = `composer-${Date.now()}-${++composerRequestSeq}`;
latestComposerRequestId = requestId;
activeComposerToken = liveToken;
send({
type: 'composer_suggestions',
requestId,
trigger: liveToken.trigger,
query: liveToken.query,
sessionId: currentSessionId,
agent: currentAgent,
});
}, COMPOSER_SUGGESTION_DEBOUNCE);
}
function handleComposerSuggestions(msg) {
if (!msg || msg.requestId !== latestComposerRequestId) return;
const token = findActiveComposerToken();
if (!token || token.trigger !== msg.trigger) {
hideCmdMenu();
return;
}
showCmdMenu(token, msg.items || []);
}
function hideCmdMenu() {
cmdMenu.hidden = true;
cmdMenuIndex = -1;
cmdMenu._items = [];
activeComposerToken = null;
}
function navigateCmdMenu(direction) {
@@ -4147,12 +4542,16 @@
items[cmdMenuIndex]?.classList.remove('active');
cmdMenuIndex = (cmdMenuIndex + direction + items.length) % items.length;
items[cmdMenuIndex]?.classList.add('active');
items[cmdMenuIndex]?.scrollIntoView({ block: 'nearest' });
}
function selectCmdMenuItem() {
const items = cmdMenu.querySelectorAll('.cmd-item');
if (cmdMenuIndex >= 0 && items[cmdMenuIndex]) {
const cmd = items[cmdMenuIndex].dataset.cmd;
function selectComposerItemByIndex(index) {
const items = Array.isArray(cmdMenu._items) ? cmdMenu._items : [];
const item = items[index];
if (!item) return;
if (item.kind === 'command') {
const cmd = item.name || item.label || '';
if (cmd === '/model') {
hideCmdMenu();
msgInput.value = '';
@@ -4165,10 +4564,30 @@
showModePicker();
return;
}
msgInput.value = cmd + ' ';
msgInput.value = item.insertion || `${cmd} `;
hideCmdMenu();
msgInput.focus();
autoResize();
return;
}
const token = activeComposerToken || findActiveComposerToken();
if (!token) return;
const value = msgInput.value || '';
const insertion = String(item.insertion || item.label || item.name || '');
const appendSpace = item.appendSpace !== false;
const suffix = appendSpace ? ' ' : '';
msgInput.value = value.slice(0, token.start) + insertion + suffix + value.slice(token.end);
const nextCursor = token.start + insertion.length + suffix.length;
msgInput.setSelectionRange(nextCursor, nextCursor);
hideCmdMenu();
msgInput.focus();
autoResize();
if (!appendSpace) requestComposerSuggestions();
}
function selectCmdMenuItem() {
if (cmdMenuIndex >= 0) selectComposerItemByIndex(cmdMenuIndex);
}
// --- Option Picker (generic) ---
@@ -4232,10 +4651,10 @@
}
function showModelPicker() {
if (currentAgent === 'codex') {
if (isCodexLikeAgent(currentAgent)) {
const current = _splitCodexThinkingModel(currentModel || '');
const baseOptions = getCodexBaseModelOptions();
showOptionPicker('选择 Codex 模型', baseOptions, current.base || '', (baseValue) => {
showOptionPicker(`选择 ${isCodexAppAgent(currentAgent) ? 'Codex App' : 'Codex'} 模型`, baseOptions, current.base || '', (baseValue) => {
const base = String(baseValue || '').trim();
const thinkingOptions = [
{ value: '', label: '无 (默认)', desc: '不附加 (low/medium/high/xhigh) 后缀' },
@@ -4296,10 +4715,29 @@
return;
}
if ((!text && pendingAttachments.length === 0) || isGenerating || isBlockingSessionLoad()) return;
const runtimeInsert = isGenerating && isCodexAppAgent(currentAgent);
if ((!text && pendingAttachments.length === 0) || isBlockingSessionLoad()) return;
if (isGenerating && !runtimeInsert) return;
hideCmdMenu();
hideOptionPicker();
if (runtimeInsert) {
if (pendingAttachments.length > 0) {
appendError('Codex App 运行中插入暂不支持图片附件,请先移除图片。');
return;
}
if (text.startsWith('/')) {
appendError('Codex App 运行中暂不支持 slash 指令插入。');
return;
}
messagesDiv.appendChild(createMsgElement('user', text, []));
scrollToBottom();
send({ type: 'message', text, sessionId: currentSessionId, mode: currentMode, agent: currentAgent });
msgInput.value = '';
autoResize();
return;
}
// Slash commands: don't show as user bubble
if (text.startsWith('/')) {
if (pendingAttachments.length > 0) {
@@ -4397,7 +4835,9 @@
});
importSessionBtn.addEventListener('click', () => {
newChatDropdown.hidden = true;
if (currentAgent === 'codex') {
if (isCodexAppAgent(currentAgent)) {
appendError('Codex App 模式暂不支持导入本地会话。');
} else if (currentAgent === 'codex') {
showImportCodexSessionModal();
} else {
showImportSessionModal();
@@ -4461,13 +4901,7 @@
msgInput.addEventListener('input', () => {
autoResize();
const val = msgInput.value;
// Show slash command menu
if (!noteMode && val.startsWith('/') && !val.includes('\n')) {
showCmdMenu(val);
} else {
hideCmdMenu();
}
requestComposerSuggestions();
});
msgInput.addEventListener('keydown', (e) => {
@@ -5116,7 +5550,7 @@
}
function showSettingsPanel() {
if (currentAgent === 'codex') {
if (isCodexLikeAgent(currentAgent)) {
showCodexSettingsPanel();
return;
}
@@ -6085,7 +6519,7 @@
// Register Service Worker for mobile push notifications
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js').catch(() => {});
navigator.serviceWorker.register(`/sw.js?v=${ASSET_VERSION}`).catch(() => {});
}
// Restore remembered password

View File

@@ -12,7 +12,7 @@
document.documentElement.dataset.theme = theme;
})();
</script>
<link rel="stylesheet" href="style.css">
<link rel="stylesheet" href="style.css?v=20260613-codexapp-tools2">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/atom-one-dark.min.css">
</head>
<body>
@@ -65,6 +65,7 @@
<div id="chat-agent-menu" class="chat-agent-menu" hidden>
<button type="button" class="chat-agent-option active" data-agent="claude">Claude</button>
<button type="button" class="chat-agent-option" data-agent="codex">Codex</button>
<button type="button" class="chat-agent-option" data-agent="codexapp">Codex App</button>
</div>
<span id="chat-runtime-state" class="chat-runtime-state" hidden>运行中</span>
<button id="chat-cwd" class="chat-cwd" type="button" hidden></button>
@@ -94,6 +95,7 @@
<div class="input-area">
<div id="attachment-tray" class="attachment-tray" hidden></div>
<div id="pending-notes-tray" class="pending-notes-tray" hidden></div>
<div class="input-wrapper">
<input id="image-upload-input" type="file" accept="image/png,image/jpeg,image/webp,image/gif" multiple hidden>
<button id="attach-btn" class="attach-btn" title="上传图片" type="button">
@@ -137,6 +139,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"></script>
<script src="app.js?v=20260613-codexapp-tools2"></script>
</body>
</html>

View File

@@ -661,6 +661,10 @@ body.session-loading-active {
.session-project-group {
margin: 0 0 12px;
}
.session-pinned-group {
padding-bottom: 8px;
border-bottom: 1px solid rgba(221, 208, 192, 0.72);
}
.session-project-header {
position: sticky;
top: 0;
@@ -729,6 +733,9 @@ body.session-loading-active {
outline: 2px solid rgba(192, 85, 58, 0.22);
outline-offset: 2px;
}
.session-pinned-header {
color: var(--accent);
}
.session-item {
display: flex;
align-items: center;
@@ -741,6 +748,16 @@ body.session-loading-active {
}
.session-item:hover { background: var(--bg-tertiary); }
.session-item.active { background: var(--accent-light); }
.session-item.pinned::before {
content: '';
position: absolute;
left: 4px;
top: 9px;
bottom: 9px;
width: 3px;
border-radius: 999px;
background: var(--accent);
}
.session-item-main {
flex: 1;
min-width: 0;
@@ -757,6 +774,16 @@ body.session-loading-active {
color: var(--text-primary);
}
.session-item.active .session-item-title { color: var(--accent); font-weight: 500; }
.session-item-pin-badge {
flex-shrink: 0;
padding: 1px 5px;
border-radius: 999px;
background: rgba(192, 85, 58, 0.11);
color: var(--accent);
font-size: 10px;
font-weight: 800;
line-height: 1.4;
}
.session-item-status {
display: inline-flex;
align-items: center;
@@ -810,6 +837,18 @@ body.session-loading-active {
font-weight: 800;
letter-spacing: 0.02em;
}
.session-item-btn.pin {
min-width: 24px;
font-weight: 800;
}
.session-item-btn.pin.active {
color: var(--accent);
background: rgba(192, 85, 58, 0.1);
}
.session-item-btn.pin:hover {
color: var(--accent);
background: rgba(192, 85, 58, 0.12);
}
.session-item-btn.delete:hover { color: var(--danger); background: var(--accent-light); }
/* Inline edit in sidebar */
.session-item-edit-input {
@@ -1028,6 +1067,8 @@ body.session-loading-active {
overflow-x: hidden;
padding: 16px;
padding-right: 20px;
padding-bottom: 24px;
scroll-padding-bottom: 24px;
display: flex;
flex-direction: column;
gap: 12px;
@@ -1298,6 +1339,9 @@ body.session-loading-active {
background: var(--info);
color: #fff;
}
.msg.user.cross-conversation-reply .msg-avatar {
background: var(--success);
}
.msg.user.cross-conversation .msg-bubble {
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.76), transparent),
@@ -1306,6 +1350,22 @@ body.session-loading-active {
color: var(--text-primary);
box-shadow: 0 10px 22px rgba(45, 31, 20, 0.05);
}
.msg.user.cross-conversation-reply .msg-bubble {
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.78), transparent),
rgba(93, 138, 84, 0.12);
border-color: rgba(93, 138, 84, 0.28);
}
.msg.user.cross-conversation-reply .cross-conversation-meta,
.msg.user.cross-conversation-reply .cross-conversation-id-btn {
color: var(--success);
}
.msg.user.cross-conversation-reply .cross-conversation-id-btn {
border-color: rgba(93, 138, 84, 0.28);
}
.msg.user.cross-conversation-reply .cross-conversation-id-btn:hover {
background: rgba(93, 138, 84, 0.14);
}
.cross-conversation-meta {
display: flex;
align-items: center;
@@ -1340,24 +1400,6 @@ body.session-loading-active {
border-bottom-left-radius: 4px;
color: var(--text-primary);
}
.msg.note {
align-self: flex-end;
flex-direction: row-reverse;
max-width: min(680px, 85%);
}
.msg.note .note-avatar {
background: var(--note-accent);
color: #fff;
}
.msg.note .note-bubble {
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.72), transparent),
var(--note-bg);
border: 1px solid var(--note-border);
border-bottom-right-radius: 4px;
color: var(--text-primary);
box-shadow: 0 10px 22px rgba(45, 31, 20, 0.05);
}
.note-meta {
margin-bottom: 6px;
color: var(--note-accent);
@@ -1452,6 +1494,11 @@ body.session-loading-active {
/* Markdown content */
.msg-bubble p { margin: 0 0 8px 0; }
.msg-bubble p:last-child { margin-bottom: 0; }
.msg-bubble hr {
border: 0;
border-top: 1px solid rgba(122, 104, 82, 0.22);
margin: 10px 0 11px;
}
.msg-bubble ul, .msg-bubble ol { margin: 4px 0 8px 20px; }
.msg-bubble li { margin-bottom: 2px; }
.msg-bubble h1, .msg-bubble h2, .msg-bubble h3, .msg-bubble h4 {
@@ -1585,7 +1632,7 @@ body.session-loading-active {
color: var(--text-secondary);
background: var(--bg-secondary);
display: flex;
align-items: anchor-center;
align-items: center;
gap: 8px;
user-select: none;
list-style: none;
@@ -1891,7 +1938,7 @@ body.session-loading-active {
30% { transform: translateY(-7px); }
}
/* === Slash Command Menu === */
/* === Composer Modifier Menu === */
.cmd-menu {
position: absolute;
bottom: 80px;
@@ -1904,27 +1951,64 @@ body.session-loading-active {
padding: 6px;
min-width: 240px;
max-width: 320px;
max-height: min(52vh, 360px);
overflow-y: auto;
overscroll-behavior: contain;
z-index: 50;
}
.cmd-menu::-webkit-scrollbar { width: 8px; }
.cmd-menu::-webkit-scrollbar-track { background: transparent; }
.cmd-menu::-webkit-scrollbar-thumb {
background: rgba(95, 74, 58, 0.22);
border-radius: 999px;
}
.cmd-menu::-webkit-scrollbar-thumb:hover {
background: rgba(95, 74, 58, 0.34);
}
.cmd-item {
display: flex;
align-items: center;
align-items: flex-start;
gap: 10px;
padding: 10px 14px;
border-radius: 8px;
cursor: pointer;
transition: background 0.12s;
min-width: 0;
}
.cmd-item:hover, .cmd-item.active { background: var(--accent-light); }
.cmd-item-kind {
flex: 0 0 auto;
min-width: 42px;
padding: 2px 6px;
border-radius: 6px;
background: rgba(192, 85, 58, 0.1);
color: var(--accent);
font-size: 10px;
font-weight: 800;
line-height: 1.4;
text-align: center;
text-transform: uppercase;
}
.cmd-item-main {
display: flex;
flex-direction: column;
gap: 2px;
min-width: 0;
}
.cmd-item-cmd {
font-weight: 600;
font-size: 14px;
color: var(--accent);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.cmd-item-desc {
font-size: 13px;
color: var(--text-secondary);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* === Input Area === */
@@ -2001,9 +2085,63 @@ body.session-loading-active {
font-size: 12px;
line-height: 1.55;
}
.pending-notes-tray {
display: flex;
flex-direction: column;
gap: 8px;
width: 100%;
max-width: 800px;
max-height: min(32vh, 220px);
margin: 0 auto 10px;
padding: 2px 3px;
overflow-y: auto;
scrollbar-width: thin;
scrollbar-color: var(--note-border) transparent;
}
.pending-notes-tray[hidden] {
display: none;
}
.pending-notes-tray::-webkit-scrollbar {
width: 6px;
}
.pending-notes-tray::-webkit-scrollbar-thumb {
background: var(--note-border);
border-radius: 999px;
}
.pending-note {
display: grid;
grid-template-columns: 28px minmax(0, 1fr);
align-items: start;
gap: 8px;
animation: fadeIn 0.2s ease;
}
.pending-note .note-avatar {
width: 28px;
height: 28px;
border-radius: 9px;
display: flex;
align-items: center;
justify-content: center;
background: var(--note-accent);
color: #fff;
font-size: 12px;
font-weight: 800;
flex-shrink: 0;
}
.pending-note .note-bubble {
min-width: 0;
padding: 10px 12px;
border-radius: 12px;
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.72), transparent),
var(--note-bg);
border: 1px solid var(--note-border);
color: var(--text-primary);
box-shadow: 0 8px 18px rgba(45, 31, 20, 0.05);
}
.input-wrapper {
display: flex;
align-items: anchor-center;
align-items: center;
gap: 8px;
background: #fff;
border: 1px solid var(--border-color);
@@ -2011,6 +2149,7 @@ body.session-loading-active {
padding: 8px 12px;
max-width: 800px;
margin: 0 auto;
width: 100%;
transition: border-color 0.2s, box-shadow 0.2s;
}
.input-wrapper.drag-active {
@@ -2087,6 +2226,7 @@ body.session-loading-active {
}
#msg-input {
flex: 1;
min-width: 0;
background: transparent;
border: none;
color: var(--text-primary);
@@ -2204,7 +2344,11 @@ body.session-loading-active {
.menu-btn { display: block; }
.msg { max-width: 95%; }
.input-area { padding: 8px 10px; padding-bottom: max(10px, var(--safe-bottom)); }
.messages { padding: 12px 8px; gap: 10px; }
.messages {
padding: 12px 8px 24px;
scroll-padding-bottom: 24px;
gap: 10px;
}
.session-item-actions { display: flex; }
.cmd-menu { left: 10px; right: 10px; transform: none; min-width: auto; bottom: 72px; }
.option-picker { left: 10px; right: 10px; transform: none; min-width: auto; bottom: 72px; }
@@ -2252,7 +2396,9 @@ body.session-loading-active {
.attach-btn { width: 34px; height: 34px; border-radius: 10px; }
.note-mode-btn { width: 34px; height: 34px; border-radius: 10px; }
.send-btn, .abort-btn { width: 34px; height: 34px; }
.msg.note { max-width: 92%; }
.pending-notes-tray { max-height: 34vh; margin-bottom: 8px; }
.pending-note { grid-template-columns: 26px minmax(0, 1fr); gap: 7px; }
.pending-note .note-avatar { width: 26px; height: 26px; border-radius: 8px; font-size: 11px; }
.note-actions { gap: 5px; }
.note-action { min-height: 28px; padding: 0 8px; }
.note-edit-input { min-width: 0; }
@@ -3037,6 +3183,81 @@ html[data-theme='coolvibe'] .settings-back:hover {
line-height: 1.5;
word-break: break-all;
}
.codex-user-input-panel {
max-width: 520px;
}
.codex-user-input-body {
display: flex;
flex-direction: column;
gap: 16px;
}
.codex-user-input-question {
display: flex;
flex-direction: column;
gap: 10px;
}
.codex-user-input-kicker {
font-size: 12px;
font-weight: 700;
color: var(--accent);
}
.codex-user-input-prompt {
color: var(--text-primary);
font-size: 15px;
line-height: 1.5;
}
.codex-user-input-options {
display: flex;
flex-direction: column;
gap: 8px;
}
.codex-user-input-option {
display: flex;
align-items: flex-start;
gap: 10px;
padding: 10px 12px;
border: 1px solid var(--border-color);
border-radius: 8px;
background: var(--bg-primary);
cursor: pointer;
}
.codex-user-input-option:hover {
border-color: rgba(192, 85, 58, 0.36);
}
.codex-user-input-option input[type='radio'] {
margin-top: 3px;
}
.codex-user-input-option-copy {
min-width: 0;
flex: 1;
display: flex;
flex-direction: column;
gap: 4px;
}
.codex-user-input-option-label {
color: var(--text-primary);
font-weight: 600;
font-size: 14px;
}
.codex-user-input-option-desc {
color: var(--text-muted);
font-size: 12px;
line-height: 1.4;
}
.codex-user-input-text {
width: 100%;
margin-top: 4px;
padding: 8px 10px;
background: #fff;
border: 1px solid var(--border-color);
border-radius: 8px;
color: var(--text-primary);
font: inherit;
outline: none;
}
.codex-user-input-text:focus {
border-color: var(--accent);
}
.modal-quick-picks {
display: flex;
flex-wrap: wrap;