chore: rebuild CentOS7 release package
This commit is contained in:
660
public/app.js
660
public/app.js
@@ -2,11 +2,12 @@
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
const ASSET_VERSION = '20260625-branch-bubble';
|
||||
const ASSET_VERSION = '20260626-ccweb-prompt-compact-ui';
|
||||
const WS_URL = `${location.protocol === 'https:' ? 'wss' : 'ws'}://${location.host}/ws`;
|
||||
const RENDER_DEBOUNCE = 100;
|
||||
const COMPOSER_SUGGESTION_DEBOUNCE = 120;
|
||||
const DIVIDER_TIME_STORAGE_KEY = 'cc-web-show-divider-time';
|
||||
const CCWEB_PROMPT_VIEW_MODE_STORAGE_KEY = 'cc-web-ccweb-prompt-view-mode';
|
||||
const PROJECT_COLLAPSE_STORAGE_KEY = 'cc-web-collapsed-projects';
|
||||
const CROSS_CONVERSATION_REPLY_COLLAPSE_STORAGE_KEY = 'cc-web-collapsed-cross-replies';
|
||||
const CROSS_CONVERSATION_REPLY_COLLAPSE_LIMIT = 500;
|
||||
@@ -210,6 +211,7 @@
|
||||
let queuedMessageSeq = 0;
|
||||
let queuedMessageDrainTimer = null;
|
||||
let isReloadingMcp = false;
|
||||
const mcpStartupToastKeys = new Map();
|
||||
let sessionSearchQuery = '';
|
||||
const collapsedProjectKeys = (() => {
|
||||
try {
|
||||
@@ -263,6 +265,8 @@
|
||||
const chatCwd = $('#chat-cwd');
|
||||
const userOutlineBtn = $('#user-outline-btn');
|
||||
const userOutlinePanel = $('#user-outline-panel');
|
||||
const ccwebPromptOutlineBtn = $('#ccweb-prompt-outline-btn');
|
||||
const ccwebPromptOutlinePanel = $('#ccweb-prompt-outline-panel');
|
||||
const reloadMcpBtn = $('#reload-mcp-btn');
|
||||
const costDisplay = $('#cost-display');
|
||||
const attachmentTray = $('#attachment-tray');
|
||||
@@ -525,6 +529,7 @@
|
||||
pendingNotesTray.innerHTML = '';
|
||||
const notes = getCurrentNotes(false);
|
||||
const queuedMessages = getCurrentQueue(false);
|
||||
renderPendingCcwebPrompts({ scroll: false, updateScrollbar: false });
|
||||
if ((!notes || notes.length === 0) && (!queuedMessages || queuedMessages.length === 0)) {
|
||||
pendingNotesTray.hidden = true;
|
||||
if (options.updateScrollbar !== false) updateScrollbar();
|
||||
@@ -544,6 +549,140 @@
|
||||
if (options.updateScrollbar !== false) updateScrollbar();
|
||||
}
|
||||
|
||||
function collectCurrentPendingCcwebPrompts() {
|
||||
if (!currentSessionId) return [];
|
||||
const prompts = [];
|
||||
const seen = new Set();
|
||||
const entry = sessionCache.get(currentSessionId);
|
||||
const messages = Array.isArray(entry?.snapshot?.messages) ? entry.snapshot.messages : [];
|
||||
messages.forEach((message) => {
|
||||
const prompt = message?.ccwebPrompt;
|
||||
if (!prompt?.id || seen.has(prompt.id) || (prompt.status || 'pending') !== 'pending') return;
|
||||
seen.add(prompt.id);
|
||||
prompts.push(prompt);
|
||||
});
|
||||
messagesDiv?.querySelectorAll?.('.ccweb-prompt-card[data-status="pending"]').forEach((card) => {
|
||||
const promptId = card.dataset.promptId || '';
|
||||
if (!promptId || seen.has(promptId)) return;
|
||||
seen.add(promptId);
|
||||
prompts.push({
|
||||
id: promptId,
|
||||
title: card.querySelector('.ccweb-prompt-title')?.textContent || '需要用户确认',
|
||||
questions: Array.from(card.querySelectorAll('.ccweb-prompt-question')).map((questionEl) => ({
|
||||
id: questionEl.dataset.questionId || '',
|
||||
})),
|
||||
});
|
||||
});
|
||||
return prompts;
|
||||
}
|
||||
|
||||
function scrollToCcwebPrompt(promptId) {
|
||||
if (!promptId || !messagesDiv) return false;
|
||||
const card = messagesDiv.querySelector(`.ccweb-prompt-card[data-prompt-id="${cssEscape(promptId)}"]`);
|
||||
if (!card) {
|
||||
showToast('未找到待提交表单', '可能在未加载的历史消息中');
|
||||
return false;
|
||||
}
|
||||
const target = card.closest('.msg') || card;
|
||||
const containerRect = messagesDiv.getBoundingClientRect();
|
||||
const targetRect = target.getBoundingClientRect();
|
||||
const targetTop = messagesDiv.scrollTop + targetRect.top - containerRect.top - 72;
|
||||
messagesDiv.scrollTo({ top: Math.max(0, targetTop), behavior: 'smooth' });
|
||||
card.classList.remove('ccweb-prompt-focus');
|
||||
requestAnimationFrame(() => {
|
||||
card.classList.add('ccweb-prompt-focus');
|
||||
window.setTimeout(() => card.classList.remove('ccweb-prompt-focus'), 1400);
|
||||
});
|
||||
updateScrollbar();
|
||||
return true;
|
||||
}
|
||||
|
||||
function dismissCcwebPrompt(promptId) {
|
||||
if (!promptId || !currentSessionId) return;
|
||||
send({
|
||||
type: 'ccweb_prompt_user_dismiss',
|
||||
sessionId: currentSessionId,
|
||||
promptId,
|
||||
});
|
||||
}
|
||||
|
||||
function createPendingCcwebPromptElement(prompt) {
|
||||
const item = document.createElement('div');
|
||||
item.className = 'pending-ccweb-prompt';
|
||||
item.dataset.promptId = prompt.id || '';
|
||||
|
||||
const badge = document.createElement('span');
|
||||
badge.className = 'pending-ccweb-prompt-badge';
|
||||
badge.setAttribute('aria-label', '未提交');
|
||||
|
||||
const title = document.createElement('div');
|
||||
title.className = 'pending-ccweb-prompt-title';
|
||||
const questionCount = Array.isArray(prompt.questions) ? prompt.questions.length : 0;
|
||||
title.textContent = `${prompt.title || '需要用户确认'} · ${questionCount || 1} 题`;
|
||||
|
||||
const action = document.createElement('button');
|
||||
action.type = 'button';
|
||||
action.className = 'pending-ccweb-prompt-action';
|
||||
action.textContent = '定位';
|
||||
action.addEventListener('click', () => {
|
||||
closeCcwebPromptOutlinePanel();
|
||||
scrollToCcwebPrompt(prompt.id);
|
||||
});
|
||||
|
||||
const dismiss = document.createElement('button');
|
||||
dismiss.type = 'button';
|
||||
dismiss.className = 'pending-ccweb-prompt-dismiss';
|
||||
dismiss.textContent = '忽略';
|
||||
dismiss.title = '忽略并删除这个未提交表单';
|
||||
dismiss.addEventListener('click', () => {
|
||||
dismiss.disabled = true;
|
||||
dismiss.textContent = '删除中';
|
||||
dismissCcwebPrompt(prompt.id);
|
||||
});
|
||||
|
||||
item.append(badge, title, action, dismiss);
|
||||
return item;
|
||||
}
|
||||
|
||||
function renderPendingCcwebPrompts(options = {}) {
|
||||
if (!ccwebPromptOutlineBtn || !ccwebPromptOutlinePanel) return;
|
||||
const prompts = collectCurrentPendingCcwebPrompts();
|
||||
const anchor = ccwebPromptOutlineBtn.closest('.ccweb-prompt-outline-anchor');
|
||||
if (prompts.length === 0) {
|
||||
if (anchor) anchor.hidden = true;
|
||||
closeCcwebPromptOutlinePanel();
|
||||
delete ccwebPromptOutlineBtn.dataset.count;
|
||||
ccwebPromptOutlinePanel.innerHTML = '';
|
||||
if (options.updateScrollbar !== false) updateScrollbar();
|
||||
return;
|
||||
}
|
||||
if (anchor) anchor.hidden = false;
|
||||
ccwebPromptOutlineBtn.disabled = false;
|
||||
ccwebPromptOutlineBtn.dataset.count = String(prompts.length);
|
||||
ccwebPromptOutlinePanel.replaceChildren(...prompts.map((prompt) => createPendingCcwebPromptElement(prompt)));
|
||||
if (options.updateScrollbar !== false) updateScrollbar();
|
||||
}
|
||||
|
||||
function closeCcwebPromptOutlinePanel() {
|
||||
if (!ccwebPromptOutlinePanel || !ccwebPromptOutlineBtn) return;
|
||||
ccwebPromptOutlinePanel.hidden = true;
|
||||
ccwebPromptOutlineBtn.setAttribute('aria-expanded', 'false');
|
||||
}
|
||||
|
||||
function toggleCcwebPromptOutlinePanel() {
|
||||
if (!ccwebPromptOutlinePanel || !ccwebPromptOutlineBtn) return;
|
||||
if (ccwebPromptOutlinePanel.hidden) {
|
||||
renderPendingCcwebPrompts({ scroll: false, updateScrollbar: false });
|
||||
const anchor = ccwebPromptOutlineBtn.closest('.ccweb-prompt-outline-anchor');
|
||||
if (anchor?.hidden || !ccwebPromptOutlinePanel.children.length) return;
|
||||
closeUserOutlinePanel();
|
||||
ccwebPromptOutlinePanel.hidden = false;
|
||||
ccwebPromptOutlineBtn.setAttribute('aria-expanded', 'true');
|
||||
} else {
|
||||
closeCcwebPromptOutlinePanel();
|
||||
}
|
||||
}
|
||||
|
||||
function findPendingNote(noteId) {
|
||||
const key = getCurrentNoteKey();
|
||||
const notes = getNotesForKey(key, false);
|
||||
@@ -1246,6 +1385,7 @@
|
||||
if (!userOutlinePanel || !userOutlineBtn) return;
|
||||
if (userOutlinePanel.hidden) {
|
||||
updateUserOutlinePanel();
|
||||
closeCcwebPromptOutlinePanel();
|
||||
userOutlinePanel.hidden = false;
|
||||
userOutlineBtn.setAttribute('aria-expanded', 'true');
|
||||
} else {
|
||||
@@ -1843,15 +1983,65 @@
|
||||
reloadMcpBtn.setAttribute('aria-busy', isReloadingMcp ? 'true' : 'false');
|
||||
}
|
||||
|
||||
function normalizeMcpStartupStatusPayload(payload) {
|
||||
if (!payload || typeof payload !== 'object') return null;
|
||||
if (payload.mcpStatus && typeof payload.mcpStatus === 'object') return payload.mcpStatus;
|
||||
if (payload.status && typeof payload.status === 'object') return payload.status;
|
||||
return payload;
|
||||
}
|
||||
|
||||
function mcpStartupStatusToastText(status) {
|
||||
const summary = normalizeMcpStartupStatusPayload(status);
|
||||
if (!summary) return '已请求重载,等待状态';
|
||||
const server = String(summary.server || summary.name || 'ccweb').trim() || 'ccweb';
|
||||
const state = String(summary.status || 'unknown').trim().toLowerCase();
|
||||
const message = String(summary.message || '').trim();
|
||||
if (state === 'ready') return `${server} MCP 已启动`;
|
||||
if (state === 'failed') return `${server} MCP 启动失败${message ? `:${message}` : ''}`;
|
||||
if (state === 'cancelled' || state === 'canceled') return `${server} MCP 启动已取消${message ? `:${message}` : ''}`;
|
||||
if (state === 'starting') return `${server} MCP 正在启动`;
|
||||
if (state === 'pending' || state === 'unknown') return '已请求重载,等待状态';
|
||||
return `${server} MCP 状态:${state}`;
|
||||
}
|
||||
|
||||
function rememberMcpStartupStatus(sessionId, status) {
|
||||
const summary = normalizeMcpStartupStatusPayload(status);
|
||||
if (!sessionId || !summary) return;
|
||||
updateCachedSession(sessionId, (snapshot) => {
|
||||
snapshot.codexAppMcpStartupStatus = deepClone(summary);
|
||||
});
|
||||
}
|
||||
|
||||
function showMcpStartupStatusToast(status, sessionId = currentSessionId, options = {}) {
|
||||
const summary = normalizeMcpStartupStatusPayload(status);
|
||||
const text = mcpStartupStatusToastText(summary);
|
||||
const server = String(summary?.server || summary?.name || 'ccweb').trim() || 'ccweb';
|
||||
const state = String(summary?.status || 'pending').trim().toLowerCase() || 'pending';
|
||||
if (state === 'ready' && !options.notifyReady) return;
|
||||
if ((state === 'starting' || state === 'pending' || state === 'unknown') && !options.notifyPending) return;
|
||||
const stamp = state === 'failed' || state === 'cancelled' || state === 'canceled'
|
||||
? String(summary?.message || text || '')
|
||||
: '';
|
||||
const cacheKey = sessionId || currentSessionId || 'global';
|
||||
const nextKey = `${server}|${state}|${stamp}`;
|
||||
if (mcpStartupToastKeys.get(cacheKey) === nextKey) return;
|
||||
mcpStartupToastKeys.set(cacheKey, nextKey);
|
||||
showToast(text, sessionId);
|
||||
}
|
||||
|
||||
async function reloadCurrentMcpServers() {
|
||||
if (!currentSessionId || !isCodexAppAgent(currentAgent) || isReloadingMcp) return;
|
||||
isReloadingMcp = true;
|
||||
updateReloadMcpButtonUI();
|
||||
try {
|
||||
await fetchAuthJson(`/api/sessions/${encodeURIComponent(currentSessionId)}/reload-mcp`, {
|
||||
const data = await fetchAuthJson(`/api/sessions/${encodeURIComponent(currentSessionId)}/reload-mcp`, {
|
||||
method: 'POST',
|
||||
});
|
||||
showToast('已请求重载 MCP');
|
||||
rememberMcpStartupStatus(currentSessionId, data.mcpStatus);
|
||||
showMcpStartupStatusToast(data.mcpStatus || { status: 'pending' }, currentSessionId, {
|
||||
notifyReady: true,
|
||||
notifyPending: true,
|
||||
});
|
||||
} catch (err) {
|
||||
showToast(err?.message || '重载 MCP 失败');
|
||||
} finally {
|
||||
@@ -2102,6 +2292,432 @@
|
||||
panel.querySelector('input, button')?.focus();
|
||||
}
|
||||
|
||||
function ccwebPromptStatusLabel(status) {
|
||||
if (status === 'submitted') return '已提交';
|
||||
if (status === 'cancelled') return '已取消';
|
||||
return '待回答';
|
||||
}
|
||||
|
||||
function ccwebPromptRecommendedOption(question) {
|
||||
const options = Array.isArray(question?.options) ? question.options : [];
|
||||
return options.find((option) => option?.recommended) || options[0] || null;
|
||||
}
|
||||
|
||||
function getCcwebPromptViewMode() {
|
||||
return localStorage.getItem(CCWEB_PROMPT_VIEW_MODE_STORAGE_KEY) === 'tabs' ? 'tabs' : 'cards';
|
||||
}
|
||||
|
||||
function setCcwebPromptActiveQuestion(card, index) {
|
||||
if (!card) return;
|
||||
const questions = Array.from(card.querySelectorAll('.ccweb-prompt-question'));
|
||||
if (questions.length === 0) return;
|
||||
const activeIndex = Math.min(Math.max(Number(index) || 0, 0), questions.length - 1);
|
||||
card.dataset.activeQuestionIndex = String(activeIndex);
|
||||
questions.forEach((questionEl, questionIndex) => {
|
||||
const isActive = questionIndex === activeIndex;
|
||||
questionEl.classList.toggle('is-active', isActive);
|
||||
questionEl.setAttribute('aria-hidden', isActive ? 'false' : 'true');
|
||||
});
|
||||
card.querySelectorAll('.ccweb-prompt-tab').forEach((tab, tabIndex) => {
|
||||
const isActive = tabIndex === activeIndex;
|
||||
tab.classList.toggle('is-active', isActive);
|
||||
tab.setAttribute('aria-selected', isActive ? 'true' : 'false');
|
||||
tab.tabIndex = isActive ? 0 : -1;
|
||||
});
|
||||
const counter = card.querySelector('.ccweb-prompt-tab-counter');
|
||||
if (counter) counter.textContent = `问题 ${activeIndex + 1} / ${questions.length}`;
|
||||
const prev = card.querySelector('[data-ccweb-prompt-prev]');
|
||||
const next = card.querySelector('[data-ccweb-prompt-next]');
|
||||
if (prev) prev.disabled = activeIndex <= 0;
|
||||
if (next) next.disabled = activeIndex >= questions.length - 1;
|
||||
}
|
||||
|
||||
function setCcwebPromptViewMode(card, mode) {
|
||||
if (!card) return;
|
||||
const normalized = mode === 'tabs' ? 'tabs' : 'cards';
|
||||
card.dataset.viewMode = normalized;
|
||||
card.querySelectorAll('.ccweb-prompt-view-btn').forEach((button) => {
|
||||
const isActive = button.dataset.viewMode === normalized;
|
||||
button.classList.toggle('is-active', isActive);
|
||||
button.setAttribute('aria-pressed', isActive ? 'true' : 'false');
|
||||
});
|
||||
if (normalized === 'tabs') {
|
||||
setCcwebPromptActiveQuestion(card, Number(card.dataset.activeQuestionIndex || 0));
|
||||
} else {
|
||||
card.querySelectorAll('.ccweb-prompt-question').forEach((questionEl) => {
|
||||
questionEl.setAttribute('aria-hidden', 'false');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function createCcwebPromptViewControls(card, questions) {
|
||||
const switcher = document.createElement('div');
|
||||
switcher.className = 'ccweb-prompt-view-switcher';
|
||||
switcher.setAttribute('aria-label', '表单显示方式');
|
||||
[
|
||||
{ mode: 'cards', label: '▦', title: '卡片视图' },
|
||||
{ mode: 'tabs', label: '▤', title: '页签视图' },
|
||||
].forEach((item) => {
|
||||
const button = document.createElement('button');
|
||||
button.type = 'button';
|
||||
button.className = 'ccweb-prompt-view-btn';
|
||||
button.dataset.viewMode = item.mode;
|
||||
button.textContent = item.label;
|
||||
button.title = item.title;
|
||||
button.setAttribute('aria-label', item.title);
|
||||
button.addEventListener('click', () => {
|
||||
localStorage.setItem(CCWEB_PROMPT_VIEW_MODE_STORAGE_KEY, item.mode);
|
||||
setCcwebPromptViewMode(card, item.mode);
|
||||
});
|
||||
switcher.appendChild(button);
|
||||
});
|
||||
return switcher;
|
||||
}
|
||||
|
||||
function createCcwebPromptTabs(card, questions) {
|
||||
const controls = document.createElement('div');
|
||||
controls.className = 'ccweb-prompt-view-controls';
|
||||
|
||||
const tabs = document.createElement('div');
|
||||
tabs.className = 'ccweb-prompt-tabs';
|
||||
tabs.setAttribute('role', 'tablist');
|
||||
questions.forEach((question, index) => {
|
||||
const tab = document.createElement('button');
|
||||
tab.type = 'button';
|
||||
tab.className = 'ccweb-prompt-tab';
|
||||
tab.setAttribute('role', 'tab');
|
||||
tab.textContent = question.title || `问题 ${index + 1}`;
|
||||
tab.addEventListener('click', () => setCcwebPromptActiveQuestion(card, index));
|
||||
tabs.appendChild(tab);
|
||||
});
|
||||
|
||||
controls.append(tabs);
|
||||
return controls;
|
||||
}
|
||||
|
||||
function createCcwebPromptTabNav(card, questions) {
|
||||
const nav = document.createElement('div');
|
||||
nav.className = 'ccweb-prompt-tab-nav';
|
||||
if (!Array.isArray(questions) || questions.length <= 1) return nav;
|
||||
const prev = document.createElement('button');
|
||||
prev.type = 'button';
|
||||
prev.className = 'ccweb-prompt-tab-nav-btn';
|
||||
prev.dataset.ccwebPromptPrev = '1';
|
||||
prev.textContent = '上一个';
|
||||
prev.addEventListener('click', () => setCcwebPromptActiveQuestion(card, Number(card.dataset.activeQuestionIndex || 0) - 1));
|
||||
const counter = document.createElement('span');
|
||||
counter.className = 'ccweb-prompt-tab-counter';
|
||||
const next = document.createElement('button');
|
||||
next.type = 'button';
|
||||
next.className = 'ccweb-prompt-tab-nav-btn';
|
||||
next.dataset.ccwebPromptNext = '1';
|
||||
next.textContent = '下一个';
|
||||
next.addEventListener('click', () => setCcwebPromptActiveQuestion(card, Number(card.dataset.activeQuestionIndex || 0) + 1));
|
||||
nav.append(prev, counter, next);
|
||||
return nav;
|
||||
}
|
||||
|
||||
function setCcwebPromptError(card, message) {
|
||||
const error = card.querySelector('.ccweb-prompt-error');
|
||||
if (!error) return;
|
||||
error.textContent = message || '';
|
||||
error.hidden = !message;
|
||||
}
|
||||
|
||||
function updateCcwebPromptAnswerFromSelection(questionEl, question) {
|
||||
const textarea = questionEl.querySelector('.ccweb-prompt-answer');
|
||||
if (!textarea) return;
|
||||
const selectedIds = Array.from(questionEl.querySelectorAll('.ccweb-prompt-option.is-selected'))
|
||||
.map((button) => button.dataset.optionId || '')
|
||||
.filter(Boolean);
|
||||
const selectedOptions = (question.options || []).filter((option) => selectedIds.includes(option.id));
|
||||
const answerText = selectedOptions.map((option) => option.answerText || option.label || '').filter(Boolean).join('\n');
|
||||
if (answerText) textarea.value = answerText;
|
||||
}
|
||||
|
||||
function selectCcwebPromptOption(questionEl, question, optionId) {
|
||||
const buttons = Array.from(questionEl.querySelectorAll('.ccweb-prompt-option'));
|
||||
if (question.selectionMode === 'multi') {
|
||||
buttons.forEach((button) => {
|
||||
if (button.dataset.optionId === optionId) button.classList.toggle('is-selected');
|
||||
});
|
||||
} else {
|
||||
buttons.forEach((button) => {
|
||||
button.classList.toggle('is-selected', button.dataset.optionId === optionId);
|
||||
});
|
||||
}
|
||||
updateCcwebPromptAnswerFromSelection(questionEl, question);
|
||||
}
|
||||
|
||||
function createCcwebPromptQuestionElement(question, index, prompt) {
|
||||
const questionEl = document.createElement('section');
|
||||
questionEl.className = 'ccweb-prompt-question';
|
||||
questionEl.dataset.questionId = question.id || `question_${index + 1}`;
|
||||
|
||||
const head = document.createElement('div');
|
||||
head.className = 'ccweb-prompt-question-head';
|
||||
const title = document.createElement('div');
|
||||
title.className = 'ccweb-prompt-question-title';
|
||||
title.textContent = question.title || `问题 ${index + 1}`;
|
||||
head.appendChild(title);
|
||||
if (question.required !== false && prompt.status !== 'submitted') {
|
||||
const required = document.createElement('span');
|
||||
required.className = 'ccweb-prompt-required';
|
||||
required.textContent = '必答';
|
||||
head.appendChild(required);
|
||||
}
|
||||
questionEl.appendChild(head);
|
||||
|
||||
if (question.question) {
|
||||
const body = document.createElement('div');
|
||||
body.className = 'ccweb-prompt-question-body';
|
||||
body.textContent = question.question;
|
||||
questionEl.appendChild(body);
|
||||
}
|
||||
|
||||
if (prompt.status === 'submitted') {
|
||||
const answer = prompt.answers?.[question.id] || {};
|
||||
if (Array.isArray(answer.selectedOptionLabels) && answer.selectedOptionLabels.length > 0) {
|
||||
const selected = document.createElement('div');
|
||||
selected.className = 'ccweb-prompt-selected-readonly';
|
||||
selected.textContent = `选择:${answer.selectedOptionLabels.join(',')}`;
|
||||
questionEl.appendChild(selected);
|
||||
}
|
||||
const answerText = document.createElement('div');
|
||||
answerText.className = 'ccweb-prompt-answer-readonly';
|
||||
answerText.textContent = answer.answerText || '(未填写答案)';
|
||||
questionEl.appendChild(answerText);
|
||||
return questionEl;
|
||||
}
|
||||
|
||||
const options = Array.isArray(question.options) ? question.options : [];
|
||||
if (options.length > 0 && question.selectionMode !== 'none') {
|
||||
const optionList = document.createElement('div');
|
||||
optionList.className = 'ccweb-prompt-options';
|
||||
options.forEach((option) => {
|
||||
const button = document.createElement('button');
|
||||
button.type = 'button';
|
||||
button.className = 'ccweb-prompt-option';
|
||||
button.dataset.optionId = option.id || '';
|
||||
const label = document.createElement('span');
|
||||
label.className = 'ccweb-prompt-option-label';
|
||||
label.textContent = option.label || option.id || '选项';
|
||||
button.appendChild(label);
|
||||
if (option.recommended) {
|
||||
const badge = document.createElement('span');
|
||||
badge.className = 'ccweb-prompt-option-badge';
|
||||
badge.textContent = '推荐';
|
||||
button.appendChild(badge);
|
||||
}
|
||||
if (option.description) {
|
||||
const desc = document.createElement('span');
|
||||
desc.className = 'ccweb-prompt-option-desc';
|
||||
desc.textContent = option.description;
|
||||
button.appendChild(desc);
|
||||
}
|
||||
button.addEventListener('click', () => selectCcwebPromptOption(questionEl, question, option.id));
|
||||
optionList.appendChild(button);
|
||||
});
|
||||
questionEl.appendChild(optionList);
|
||||
}
|
||||
|
||||
const answer = document.createElement('textarea');
|
||||
answer.className = 'ccweb-prompt-answer';
|
||||
answer.rows = 4;
|
||||
answer.placeholder = question.answerPlaceholder || '填写你的答案...';
|
||||
answer.value = question.defaultAnswer || '';
|
||||
questionEl.appendChild(answer);
|
||||
|
||||
const recommended = ccwebPromptRecommendedOption(question);
|
||||
if (recommended?.recommended) {
|
||||
selectCcwebPromptOption(questionEl, question, recommended.id);
|
||||
}
|
||||
|
||||
return questionEl;
|
||||
}
|
||||
|
||||
function collectCcwebPromptAnswers(card, prompt) {
|
||||
const answers = {};
|
||||
for (const question of prompt.questions || []) {
|
||||
const escapedId = cssEscape(question.id || '');
|
||||
const questionEl = card.querySelector(`.ccweb-prompt-question[data-question-id="${escapedId}"]`);
|
||||
if (!questionEl) continue;
|
||||
const selectedOptionIds = Array.from(questionEl.querySelectorAll('.ccweb-prompt-option.is-selected'))
|
||||
.map((button) => button.dataset.optionId || '')
|
||||
.filter(Boolean);
|
||||
const answerText = String(questionEl.querySelector('.ccweb-prompt-answer')?.value || '').trim();
|
||||
if (question.required !== false && !answerText) {
|
||||
return { ok: false, message: `请填写「${question.title || question.id}」的答案。` };
|
||||
}
|
||||
answers[question.id] = { selectedOptionIds, answerText };
|
||||
}
|
||||
return { ok: true, answers };
|
||||
}
|
||||
|
||||
function createCcwebPromptElement(prompt, meta = {}) {
|
||||
const card = document.createElement('section');
|
||||
const promptStatus = prompt?.status || 'pending';
|
||||
const questions = Array.isArray(prompt?.questions) ? prompt.questions : [];
|
||||
card.className = 'ccweb-prompt-card';
|
||||
card.dataset.promptId = prompt?.id || '';
|
||||
card.dataset.status = promptStatus;
|
||||
card.dataset.viewMode = 'cards';
|
||||
|
||||
const header = document.createElement('div');
|
||||
header.className = 'ccweb-prompt-header';
|
||||
const titleWrap = document.createElement('div');
|
||||
titleWrap.className = 'ccweb-prompt-title-wrap';
|
||||
const title = document.createElement('div');
|
||||
title.className = 'ccweb-prompt-title';
|
||||
title.textContent = prompt?.title || '需要用户确认';
|
||||
titleWrap.appendChild(title);
|
||||
header.appendChild(titleWrap);
|
||||
const headerActions = document.createElement('div');
|
||||
headerActions.className = 'ccweb-prompt-header-actions';
|
||||
const status = document.createElement('span');
|
||||
status.className = 'ccweb-prompt-status';
|
||||
status.textContent = promptStatus === 'pending' ? '●' : ccwebPromptStatusLabel(prompt?.status || 'pending');
|
||||
status.title = ccwebPromptStatusLabel(prompt?.status || 'pending');
|
||||
status.setAttribute('aria-label', ccwebPromptStatusLabel(prompt?.status || 'pending'));
|
||||
headerActions.appendChild(status);
|
||||
if (promptStatus === 'pending' && questions.length > 1) {
|
||||
headerActions.appendChild(createCcwebPromptViewControls(card, questions));
|
||||
}
|
||||
header.appendChild(headerActions);
|
||||
card.appendChild(header);
|
||||
|
||||
if (prompt?.description) {
|
||||
const desc = document.createElement('div');
|
||||
desc.className = 'ccweb-prompt-desc';
|
||||
desc.textContent = prompt.description;
|
||||
card.appendChild(desc);
|
||||
}
|
||||
|
||||
if (promptStatus === 'pending' && questions.length > 1) {
|
||||
card.appendChild(createCcwebPromptTabs(card, questions));
|
||||
}
|
||||
|
||||
const questionsWrap = document.createElement('div');
|
||||
questionsWrap.className = 'ccweb-prompt-questions';
|
||||
questions.forEach((question, index) => {
|
||||
questionsWrap.appendChild(createCcwebPromptQuestionElement(question, index, prompt));
|
||||
});
|
||||
card.appendChild(questionsWrap);
|
||||
|
||||
const error = document.createElement('div');
|
||||
error.className = 'ccweb-prompt-error';
|
||||
error.hidden = true;
|
||||
card.appendChild(error);
|
||||
|
||||
if ((prompt?.status || 'pending') === 'pending') {
|
||||
const footer = document.createElement('div');
|
||||
footer.className = 'ccweb-prompt-footer';
|
||||
if (questions.length > 1) {
|
||||
footer.appendChild(createCcwebPromptTabNav(card, questions));
|
||||
}
|
||||
const footerActions = document.createElement('div');
|
||||
footerActions.className = 'ccweb-prompt-footer-actions';
|
||||
const fillRecommended = document.createElement('button');
|
||||
fillRecommended.type = 'button';
|
||||
fillRecommended.className = 'ccweb-prompt-secondary';
|
||||
fillRecommended.textContent = '填入推荐';
|
||||
fillRecommended.addEventListener('click', () => {
|
||||
questions.forEach((question) => {
|
||||
const option = ccwebPromptRecommendedOption(question);
|
||||
if (!option) return;
|
||||
const questionEl = card.querySelector(`.ccweb-prompt-question[data-question-id="${cssEscape(question.id || '')}"]`);
|
||||
if (questionEl) selectCcwebPromptOption(questionEl, question, option.id);
|
||||
});
|
||||
});
|
||||
footerActions.appendChild(fillRecommended);
|
||||
|
||||
const submit = document.createElement('button');
|
||||
submit.type = 'button';
|
||||
submit.className = 'ccweb-prompt-submit';
|
||||
submit.textContent = '提交全部';
|
||||
submit.addEventListener('click', () => {
|
||||
const collected = collectCcwebPromptAnswers(card, prompt);
|
||||
if (!collected.ok) {
|
||||
setCcwebPromptError(card, collected.message);
|
||||
return;
|
||||
}
|
||||
setCcwebPromptError(card, '');
|
||||
submit.disabled = true;
|
||||
submit.textContent = '提交中';
|
||||
send({
|
||||
type: 'ccweb_prompt_user_response',
|
||||
sessionId: meta.sessionId || currentSessionId,
|
||||
promptId: prompt.id,
|
||||
answers: collected.answers,
|
||||
});
|
||||
});
|
||||
footerActions.appendChild(submit);
|
||||
footer.appendChild(footerActions);
|
||||
card.appendChild(footer);
|
||||
}
|
||||
|
||||
if (promptStatus === 'pending' && questions.length > 1) {
|
||||
setCcwebPromptActiveQuestion(card, 0);
|
||||
setCcwebPromptViewMode(card, getCcwebPromptViewMode());
|
||||
}
|
||||
|
||||
return card;
|
||||
}
|
||||
|
||||
function updateCcwebPromptMessageInSnapshot(snapshot, prompt) {
|
||||
if (!snapshot || !Array.isArray(snapshot.messages) || !prompt?.id) return;
|
||||
for (const message of snapshot.messages) {
|
||||
if (message?.ccwebPrompt?.id === prompt.id) {
|
||||
message.ccwebPrompt = deepClone(prompt);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function removeCcwebPromptMessageFromSnapshot(snapshot, promptId) {
|
||||
if (!snapshot || !Array.isArray(snapshot.messages) || !promptId) return;
|
||||
snapshot.messages = snapshot.messages.filter((message) => message?.ccwebPrompt?.id !== promptId);
|
||||
}
|
||||
|
||||
function removeCcwebPromptMessageFromDom(promptId) {
|
||||
if (!promptId) return 0;
|
||||
let removed = 0;
|
||||
document.querySelectorAll(`.ccweb-prompt-card[data-prompt-id="${cssEscape(promptId)}"]`).forEach((card) => {
|
||||
const messageEl = card.closest('.msg');
|
||||
if (messageEl?.parentNode) {
|
||||
messageEl.remove();
|
||||
} else {
|
||||
card.remove();
|
||||
}
|
||||
removed += 1;
|
||||
});
|
||||
if (removed > 0) {
|
||||
updateUserOutlinePanel();
|
||||
updateScrollbar();
|
||||
}
|
||||
return removed;
|
||||
}
|
||||
|
||||
function applyCcwebPromptUserUpdate(msg) {
|
||||
if (msg.sessionId && msg.prompt) {
|
||||
updateCachedSession(msg.sessionId, (snapshot) => updateCcwebPromptMessageInSnapshot(snapshot, msg.prompt));
|
||||
}
|
||||
if (msg.sessionId !== currentSessionId || !msg.prompt?.id) return;
|
||||
document.querySelectorAll(`.ccweb-prompt-card[data-prompt-id="${cssEscape(msg.prompt.id)}"]`).forEach((card) => {
|
||||
card.replaceWith(createCcwebPromptElement(msg.prompt, { sessionId: msg.sessionId }));
|
||||
});
|
||||
renderPendingCcwebPrompts({ scroll: false });
|
||||
}
|
||||
|
||||
function applyCcwebPromptUserRemove(msg) {
|
||||
const promptId = msg.promptId || msg.prompt?.id || '';
|
||||
if (msg.sessionId && promptId) {
|
||||
updateCachedSession(msg.sessionId, (snapshot) => removeCcwebPromptMessageFromSnapshot(snapshot, promptId));
|
||||
}
|
||||
if (msg.sessionId !== currentSessionId || !promptId) return;
|
||||
removeCcwebPromptMessageFromDom(promptId);
|
||||
renderPendingCcwebPrompts({ scroll: false });
|
||||
}
|
||||
|
||||
function closeDirectoryPicker() {
|
||||
if (!directoryPickerState) return;
|
||||
const { overlay, escapeHandler } = directoryPickerState;
|
||||
@@ -3399,6 +4015,7 @@
|
||||
function resetChatView(agent) {
|
||||
setCurrentAgent(agent);
|
||||
closeUserOutlinePanel();
|
||||
closeCcwebPromptOutlinePanel();
|
||||
closeFileBrowser();
|
||||
currentSessionId = null;
|
||||
loadedHistorySessionId = null;
|
||||
@@ -3460,6 +4077,7 @@
|
||||
setCurrentSessionRunningState(snapshot.isRunning);
|
||||
setStatsDisplay(snapshot);
|
||||
closeUserOutlinePanel();
|
||||
closeCcwebPromptOutlinePanel();
|
||||
currentCwd = snapshot.cwd || null;
|
||||
updateCwdBadge();
|
||||
if (snapshot.mode && MODE_LABELS[snapshot.mode]) {
|
||||
@@ -3492,6 +4110,7 @@
|
||||
const { preserveCurrent = true, loadLast = true } = options;
|
||||
setCurrentAgent(targetAgent);
|
||||
closeUserOutlinePanel();
|
||||
closeCcwebPromptOutlinePanel();
|
||||
renderSessionList();
|
||||
|
||||
const currentMeta = currentSessionId ? getSessionMeta(currentSessionId) : null;
|
||||
@@ -3651,6 +4270,7 @@
|
||||
send({ type: 'detach_view' });
|
||||
}
|
||||
closeUserOutlinePanel();
|
||||
closeCcwebPromptOutlinePanel();
|
||||
clearSessionLoading();
|
||||
touchSessionCache(sessionId);
|
||||
applySessionSnapshot(snapshot, { immediate: true, suppressUnreadToast: true });
|
||||
@@ -3660,6 +4280,7 @@
|
||||
function openSession(sessionId, options = {}) {
|
||||
if (!sessionId) return;
|
||||
closeUserOutlinePanel();
|
||||
closeCcwebPromptOutlinePanel();
|
||||
if (options.forceSync) {
|
||||
beginSessionSwitch(sessionId, { blocking: options.blocking !== false, force: true, label: options.label });
|
||||
return;
|
||||
@@ -4355,6 +4976,7 @@
|
||||
messagesDiv.appendChild(buildMsgElement(msg.message, messageIndex));
|
||||
followOutputIfNeeded(shouldFollow);
|
||||
setCurrentSessionRunningState(!!getSessionMeta(currentSessionId)?.isRunning);
|
||||
renderPendingCcwebPrompts({ scroll: false });
|
||||
}
|
||||
renderSessionList();
|
||||
break;
|
||||
@@ -4499,6 +5121,19 @@
|
||||
applyCcwebMcpChildAgentUpdate(msg);
|
||||
break;
|
||||
|
||||
case 'ccweb_prompt_user_update':
|
||||
applyCcwebPromptUserUpdate(msg);
|
||||
break;
|
||||
|
||||
case 'ccweb_prompt_user_remove':
|
||||
applyCcwebPromptUserRemove(msg);
|
||||
break;
|
||||
|
||||
case 'mcp_startup_status':
|
||||
rememberMcpStartupStatus(msg.sessionId, msg.mcpStatus || msg.status);
|
||||
showMcpStartupStatusToast(msg.mcpStatus || msg.status, msg.sessionId);
|
||||
break;
|
||||
|
||||
case 'mode_changed':
|
||||
if (msg.mode && MODE_LABELS[msg.mode]) {
|
||||
currentMode = msg.mode;
|
||||
@@ -5870,6 +6505,10 @@
|
||||
|
||||
function buildMsgElement(m, messageIndex = null) {
|
||||
const el = createMsgElement(m.role, m.content, m.attachments || [], m);
|
||||
if (m.ccwebPrompt) {
|
||||
const bubble = el.querySelector('.msg-bubble');
|
||||
if (bubble) bubble.appendChild(createCcwebPromptElement(m.ccwebPrompt, { sessionId: currentSessionId }));
|
||||
}
|
||||
if (m.role === 'assistant' && m.toolCalls && m.toolCalls.length > 0) {
|
||||
const bubble = el.querySelector('.msg-bubble');
|
||||
const toolMount = bubble.querySelector(':scope > .cross-conversation-reply-body') || bubble;
|
||||
@@ -7489,6 +8128,16 @@
|
||||
});
|
||||
}
|
||||
|
||||
if (ccwebPromptOutlineBtn && ccwebPromptOutlinePanel) {
|
||||
ccwebPromptOutlineBtn.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
toggleCcwebPromptOutlinePanel();
|
||||
});
|
||||
ccwebPromptOutlinePanel.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
});
|
||||
}
|
||||
|
||||
if (reloadMcpBtn) {
|
||||
reloadMcpBtn.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
@@ -7551,6 +8200,11 @@
|
||||
e.target !== userOutlineBtn) {
|
||||
closeUserOutlinePanel();
|
||||
}
|
||||
if (ccwebPromptOutlinePanel && !ccwebPromptOutlinePanel.hidden &&
|
||||
!ccwebPromptOutlinePanel.contains(e.target) &&
|
||||
e.target !== ccwebPromptOutlineBtn) {
|
||||
closeCcwebPromptOutlinePanel();
|
||||
}
|
||||
});
|
||||
sendBtn.addEventListener('click', sendMessage);
|
||||
if (queueSendBtn) {
|
||||
|
||||
Reference in New Issue
Block a user