feat: overhaul task board and cross-conversation workflows

This commit is contained in:
shiyue
2026-08-12 09:41:36 +08:00
parent e7d28935ef
commit 86231f0d97
55 changed files with 11628 additions and 280 deletions

View File

@@ -295,6 +295,12 @@
savedSelection: null,
};
let usageDashboardRequestSeq = 0;
const taskBoardViewState = {
open: false,
savedFocus: null,
};
let taskBoardController = null;
let taskTrackingController = null;
const usageEchartInstances = new Set();
let usageChartResizeFrame = 0;
let usageChartRefreshFrame = 0;
@@ -318,6 +324,7 @@
const pendingNotesByTarget = new Map();
const queuedMessagesByTarget = new Map();
const userMessageIndex = new Map();
const codexAppSteerRecords = new Map();
const pendingSlashDraftsByRequestId = new Map();
const expandedOldSessionGroups = new Set();
document.documentElement.dataset.dividerTime = showAgentDividerTime ? 'show' : 'hide';
@@ -388,6 +395,11 @@
const usageDashboardProjectEmpty = $('#usage-dashboard-project-empty');
const usageDashboardSessionRows = $('#usage-dashboard-session-rows');
const usageDashboardSessionEmpty = $('#usage-dashboard-session-empty');
const taskBoardOpen = $('#task-board-open');
const taskBoardPanel = $('#task-board-panel');
const taskBoardClose = $('#task-board-close');
const taskBoardRoot = $('#task-board-root');
const taskTrackingControl = $('#task-tracking-control');
const sessionList = $('#session-list');
const chatHeader = chatMain?.querySelector('.chat-header') || null;
const messagesWrap = chatMain?.querySelector('.messages-wrap') || null;
@@ -1549,6 +1561,7 @@
function clearUserMessageIndex() {
userMessageIndex.clear();
codexAppSteerRecords.clear();
}
function registerUserMessage(messageId, element, content, timestamp = '') {
@@ -1562,6 +1575,73 @@
});
}
function cloneCodexAppSteerAttachments(attachments) {
return Array.isArray(attachments)
? attachments.map((attachment) => ({ ...attachment }))
: [];
}
function registerCodexAppSteerRecord(messageId, options = {}) {
const id = String(messageId || '').trim();
if (!id || !options.element) return null;
const previous = codexAppSteerRecords.get(id) || {};
const record = {
id,
element: options.element,
text: String(options.text || ''),
attachments: cloneCodexAppSteerAttachments(options.attachments),
sessionId: options.sessionId || null,
mode: options.mode || currentMode,
agent: options.agent || currentAgent,
status: options.status || previous.status || 'pending',
message: options.message || previous.message || '',
committed: previous.committed === true,
inFlight: false,
timestamp: options.timestamp || previous.timestamp || '',
};
codexAppSteerRecords.set(id, record);
return record;
}
function clearCodexAppSteerRecord(messageId) {
const id = String(messageId || '').trim();
if (!id) return;
codexAppSteerRecords.delete(id);
}
function commitCodexAppSteerMessage(record) {
if (!record || record.committed) return false;
const element = record.element;
if (!element || record.sessionId !== currentSessionId) return false;
if (element.dataset.sessionMessage === 'true') {
record.committed = true;
return false;
}
const messageIndex = currentSessionMessageCount;
currentSessionMessageCount += 1;
markSessionMessageElement(element, messageIndex);
record.committed = true;
updateUserOutlinePanel();
return true;
}
function registerCodexAppSteerSessionMessage(sessionId, message, element) {
if (!message || message.role !== 'user' || !message.codexAppSteerStatus || !element) return null;
const messageId = String(message.id || message.messageId || element.dataset.messageId || '').trim();
if (!messageId) return null;
return registerCodexAppSteerRecord(messageId, {
element,
text: message.content,
attachments: message.attachments,
sessionId,
mode: message.mode || currentMode,
agent: message.agent || currentAgent,
status: message.codexAppSteerStatus,
message: message.codexAppSteerMessage,
timestamp: message.timestamp,
});
}
function normalizeOutlineTitleHistory(history) {
if (!Array.isArray(history)) return [];
const normalized = [];
@@ -2118,6 +2198,13 @@
waitingReplyCount: Number(payload.waitingReplyCount || 0),
failedReplyCount: Number(payload.failedReplyCount || 0),
pendingReplies: Array.isArray(payload.pendingReplies) ? deepClone(payload.pendingReplies) : [],
taskTracking: payload.taskTracking ? deepClone(payload.taskTracking) : {
enabled: payload.taskTrackingEnabled === true,
statusId: 'unassigned',
version: 0,
},
taskTrackingEnabled: payload.taskTrackingEnabled === true
|| payload.taskTracking?.enabled === true,
historyTotal,
historyBaseIndex,
historyPending: !!payload.historyPending,
@@ -5287,6 +5374,84 @@
});
}
function setTaskBoardUnderlyingInert(inert) {
if (!chatMain) return;
if (inert) chatMain.setAttribute('data-task-board-inert', 'true');
else chatMain.removeAttribute('data-task-board-inert');
if (messagesWrap) messagesWrap.toggleAttribute('inert', inert);
if (inputArea) inputArea.toggleAttribute('inert', inert);
if (chatHeader) chatHeader.toggleAttribute('inert', inert);
}
function closeTaskBoard(options = {}) {
if (!taskBoardPanel || !taskBoardViewState.open) return;
taskBoardViewState.open = false;
taskBoardPanel.hidden = true;
taskBoardPanel.setAttribute('aria-hidden', 'true');
taskBoardOpen?.setAttribute('aria-expanded', 'false');
setTaskBoardUnderlyingInert(false);
requestAnimationFrame(() => {
if (options.restoreFocus !== false) {
const target = taskBoardViewState.savedFocus;
if (target?.isConnected && typeof target.focus === 'function') target.focus();
else taskBoardOpen?.focus();
}
});
}
function openTaskBoard() {
if (!taskBoardPanel || !taskBoardRoot || !window.CcwebTaskBoard) return;
if (usageDashboardState.open) closeUsageDashboard({ restoreFocus: false });
if (advancedSessionSearchState.open) closeAdvancedSessionSearch({ restoreFocus: false });
taskBoardViewState.savedFocus = document.activeElement instanceof HTMLElement ? document.activeElement : null;
taskBoardViewState.open = true;
taskBoardPanel.hidden = false;
taskBoardPanel.setAttribute('aria-hidden', 'false');
taskBoardOpen?.setAttribute('aria-expanded', 'true');
setTaskBoardUnderlyingInert(true);
if (isSidebarDrawerMode()) closeSidebar();
if (!taskBoardController) {
taskBoardController = window.CcwebTaskBoard.mount(taskBoardRoot, {
send,
openSession(sessionId) {
closeTaskBoard({ restoreFocus: false });
openSession(sessionId);
},
onError(error) { showToast(error?.message || '任务看板操作失败'); },
});
} else {
taskBoardController.refresh();
}
requestAnimationFrame(() => taskBoardPanel.querySelector('button, input, select')?.focus());
}
function syncTaskTrackingControl(snapshot = null) {
if (!taskTrackingControl || !window.CcwebTaskBoard) return;
const sessionId = snapshot?.sessionId || currentSessionId || '';
const tracking = snapshot?.taskTracking || {};
if (!sessionId) {
taskTrackingControl.hidden = true;
if (taskTrackingController) taskTrackingController.setState({ sessionId: '', disabled: true });
return;
}
taskTrackingControl.hidden = false;
const state = {
sessionId,
enabled: tracking.enabled === true || snapshot?.taskTrackingEnabled === true,
version: Number.isFinite(Number(tracking.version)) ? Number(tracking.version) : null,
disabled: false,
};
if (!taskTrackingController) {
taskTrackingController = window.CcwebTaskBoard.renderTrackingControl(taskTrackingControl, {
...state,
send,
onError(error) { showToast(error?.message || '保存任务跟踪设置失败'); },
});
} else {
taskTrackingController.setState(state);
}
}
function handleUsageStatisticsResult(msg) {
const requestId = String(msg?.requestId || '');
if (!requestId || requestId !== usageDashboardState.requestId) return;
@@ -5703,6 +5868,7 @@
closeCcwebPromptOutlinePanel();
closeFileBrowser();
currentSessionId = null;
syncTaskTrackingControl(null);
loadedHistorySessionId = null;
currentSessionMessageCount = 0;
currentOutlineTitleHistory = [];
@@ -5762,6 +5928,7 @@
activeTodoCallTargets.clear();
}
currentSessionId = snapshot.sessionId;
syncTaskTrackingControl(snapshot);
loadedHistorySessionId = snapshot.sessionId;
currentOutlineTitleHistory = normalizeOutlineTitleHistory(snapshot.titleHistory);
currentSessionMessageCount = Math.max(
@@ -6130,6 +6297,7 @@
function openSession(sessionId, options = {}) {
if (!sessionId) return;
if (taskBoardViewState.open) closeTaskBoard({ restoreFocus: false });
if (usageDashboardState.open) closeUsageDashboard({ restoreFocus: false });
const meta = getSessionMeta(sessionId);
const cachedAgent = sessionCache.get(sessionId)?.snapshot?.agent;
@@ -6728,7 +6896,46 @@
return true;
}
function handleTaskBoardProtocolMessage(msg) {
const taskMessageTypes = new Set([
'task_board_result',
'task_board_event',
'task_tracking_result',
'task_status_result',
'task_status_definitions_result',
'task_archive_result',
]);
if (!taskMessageTypes.has(msg?.type)) return false;
taskBoardController?.handleMessage(msg);
taskTrackingController?.handleMessage(msg);
const task = msg.task && typeof msg.task === 'object' ? msg.task : null;
const sessionId = String(task?.sessionId || msg.sessionId || '');
if (task?.taskTracking && sessionId) {
const listItem = sessions.find((session) => session.id === sessionId);
if (listItem) {
listItem.taskTracking = deepClone(task.taskTracking);
listItem.taskTrackingEnabled = task.taskTracking.enabled === true;
}
const cached = sessionCache.get(sessionId)?.snapshot;
if (cached) {
cached.taskTracking = deepClone(task.taskTracking);
cached.taskTrackingEnabled = task.taskTracking.enabled === true;
}
if (sessionId === currentSessionId) {
syncTaskTrackingControl({
sessionId,
taskTracking: task.taskTracking,
taskTrackingEnabled: task.taskTracking.enabled === true,
});
}
}
return true;
}
function handleServerMessage(msg) {
if (handleTaskBoardProtocolMessage(msg)) return;
switch (msg.type) {
case 'auth_result':
if (msg.success) {
@@ -6928,9 +7135,10 @@
case 'session_message':
if (msg.sessionId && msg.message) {
const isCrossConversationReply = !!msg.message.crossConversation?.replyToRequestId;
const isProvisionalCodexAppSteer = !!msg.message.codexAppSteerStatus;
updateCachedSession(msg.sessionId, (snapshot) => {
snapshot.messages = Array.isArray(snapshot.messages) ? snapshot.messages : [];
snapshot.messages.push(deepClone(msg.message));
if (!isProvisionalCodexAppSteer) snapshot.messages.push(deepClone(msg.message));
snapshot.updated = msg.message.timestamp || new Date().toISOString();
if (isCrossConversationReply) {
snapshot.readyReplyCount = Math.max(0, Number(snapshot.readyReplyCount || 0) - 1);
@@ -6953,13 +7161,16 @@
}
}
if (msg.sessionId === currentSessionId && msg.message) {
const messageIndex = currentSessionMessageCount;
currentSessionMessageCount += 1;
const isProvisionalCodexAppSteer = !!msg.message.codexAppSteerStatus;
const messageIndex = isProvisionalCodexAppSteer ? null : currentSessionMessageCount;
if (!isProvisionalCodexAppSteer) currentSessionMessageCount += 1;
collectClosedCollabAgentIds([msg.message]).forEach((id) => closedCollabAgentIds.add(id));
const welcome = messagesDiv.querySelector('.welcome-msg');
if (welcome) welcome.remove();
const shouldFollow = !(currentSessionRunning || isGenerating) || isNearBottom();
messagesDiv.appendChild(buildMsgElement(msg.message, messageIndex));
const element = buildMsgElement(msg.message, messageIndex);
messagesDiv.appendChild(element);
registerCodexAppSteerSessionMessage(msg.sessionId, msg.message, element);
followOutputIfNeeded(shouldFollow);
setCurrentSessionRunningState(!!getSessionMeta(currentSessionId)?.isRunning);
renderPendingCcwebPrompts({ scroll: false });
@@ -7231,6 +7442,7 @@
title: request.title,
branchSourceSessionId: request.branchSourceSessionId,
branchMessageIndex: request.branchMessageIndex,
taskTrackingEnabled: request.taskTrackingEnabled === true,
createCwd: true,
requestId: request.requestId,
});
@@ -7244,6 +7456,7 @@
title: request.title,
branchSourceSessionId: request.branchSourceSessionId,
branchMessageIndex: request.branchMessageIndex,
taskTrackingEnabled: request.taskTrackingEnabled === true,
});
},
});
@@ -7490,13 +7703,81 @@
return '引导中...';
}
function retryCodexAppSteerMessage(messageId) {
const id = String(messageId || '').trim();
const record = id ? codexAppSteerRecords.get(id) : null;
if (!record || record.status !== 'failed' || record.inFlight) return false;
if (record.sessionId && currentSessionId && record.sessionId !== currentSessionId) {
appendError('该失败项不属于当前会话,无法重试。');
setCodexAppSteerStatusElement(record.element, 'failed', record.message || '插入失败');
return false;
}
if (!isGenerating || !currentSessionRunning || !isCodexAppAgent(currentAgent)) {
appendError('当前对话已结束,无法在原 turn 中重试插入。');
setCodexAppSteerStatusElement(record.element, 'failed', record.message || '插入失败');
return false;
}
if (!ws || ws.readyState !== 1) {
appendError('WebSocket 已断开,请恢复连接后重试。');
setCodexAppSteerStatusElement(record.element, 'failed', record.message || '插入失败');
return false;
}
record.inFlight = true;
setCodexAppSteerStatusElement(record.element, 'pending', '引导中...');
try {
send({
type: 'message',
text: record.text,
attachments: cloneCodexAppSteerAttachments(record.attachments),
sessionId: record.sessionId,
mode: record.mode,
agent: record.agent,
clientMessageId: id,
});
return true;
} catch (err) {
record.inFlight = false;
record.status = 'failed';
record.message = record.message || '插入失败';
setCodexAppSteerStatusElement(record.element, 'failed', record.message);
appendError(err?.message || '重试发送失败。');
return false;
}
}
function deleteCodexAppSteerMessage(messageId) {
const id = String(messageId || '').trim();
if (!id) return false;
const record = codexAppSteerRecords.get(id);
const indexed = userMessageIndex.get(id);
const element = record?.element || indexed?.element || messagesDiv.querySelector(`[data-message-id="${cssEscape(id)}"]`);
const status = record?.status || element?.querySelector('.codex-steer-status')?.dataset.status;
if (status !== 'failed') return false;
if (record?.inFlight) return false;
if (element) element.remove();
clearCodexAppSteerRecord(id);
userMessageIndex.delete(id);
updateUserOutlinePanel();
updateScrollbar();
return true;
}
function setCodexAppSteerStatusElement(element, status, message) {
if (!element) return false;
const normalized = ['pending', 'inserted', 'failed'].includes(status) ? status : 'pending';
const messageId = String(element.dataset.messageId || '').trim();
const record = messageId ? codexAppSteerRecords.get(messageId) : null;
if (record) {
record.status = normalized;
record.message = message || codexAppSteerStatusLabel(normalized);
if (normalized !== 'pending') record.inFlight = false;
}
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');
element.setAttribute('aria-busy', normalized === 'pending' ? 'true' : 'false');
const bubble = element.querySelector('.msg-bubble');
if (!bubble) return false;
let statusEl = bubble.querySelector('.codex-steer-status');
@@ -7506,7 +7787,45 @@
bubble.appendChild(statusEl);
}
statusEl.dataset.status = normalized;
statusEl.setAttribute('role', 'status');
statusEl.setAttribute('aria-live', 'polite');
statusEl.textContent = message || codexAppSteerStatusLabel(normalized);
let actions = bubble.querySelector('.codex-steer-actions');
if (normalized !== 'failed') {
if (actions) actions.remove();
if (normalized === 'inserted' && record) commitCodexAppSteerMessage(record);
return true;
}
if (!actions) {
actions = document.createElement('div');
actions.className = 'codex-steer-actions';
const retryButton = document.createElement('button');
retryButton.type = 'button';
retryButton.className = 'codex-steer-action codex-steer-retry';
retryButton.textContent = '重试';
retryButton.setAttribute('aria-label', '重试插入此消息');
retryButton.addEventListener('click', (event) => {
event.preventDefault();
event.stopPropagation();
retryCodexAppSteerMessage(messageId);
});
const deleteButton = document.createElement('button');
deleteButton.type = 'button';
deleteButton.className = 'codex-steer-action codex-steer-delete';
deleteButton.textContent = '删除';
deleteButton.setAttribute('aria-label', '删除失败插入项');
deleteButton.addEventListener('click', (event) => {
event.preventDefault();
event.stopPropagation();
deleteCodexAppSteerMessage(messageId);
});
actions.appendChild(retryButton);
actions.appendChild(deleteButton);
bubble.appendChild(actions);
}
actions.querySelectorAll('button').forEach((button) => {
button.disabled = !!record?.inFlight;
});
return true;
}
@@ -10883,12 +11202,16 @@
} else {
messagesDiv.appendChild(element);
}
if (currentSessionId) {
const messageIndex = currentSessionMessageCount;
currentSessionMessageCount += 1;
markSessionMessageElement(element, messageIndex);
}
registerUserMessage(messageId, element, text, timestamp);
registerCodexAppSteerRecord(messageId, {
element,
text,
attachments,
sessionId: currentSessionId,
mode: currentMode,
agent: currentAgent,
timestamp,
});
updateUserOutlinePanel();
if (shouldFollow) {
scrollToBottom();
@@ -11154,6 +11477,11 @@
setUsageDashboardFeatureEnabled(false);
}
if (taskBoardOpen && taskBoardPanel && taskBoardRoot) {
taskBoardOpen.addEventListener('click', openTaskBoard);
taskBoardClose?.addEventListener('click', () => closeTaskBoard());
}
document.addEventListener('keydown', (event) => {
if ((event.ctrlKey || event.metaKey) && event.shiftKey && event.key.toLowerCase() === 'f') {
event.preventDefault();
@@ -11170,6 +11498,11 @@
closeUsageDashboard();
return;
}
if (event.key === 'Escape' && taskBoardViewState.open) {
event.preventDefault();
closeTaskBoard();
return;
}
if (event.key === 'Escape' && advancedSessionSearchState.open) {
event.preventDefault();
closeAdvancedSessionSearch();
@@ -12563,6 +12896,8 @@
const branchMessageIndex = Number.isFinite(Number(options.branchMessageIndex))
? Number(options.branchMessageIndex)
: null;
const taskTrackingEnabled = options.taskTrackingEnabled === true
|| options.taskTracking?.enabled === true;
const requestId = createSessionSwitchRequestId('new');
pendingNewSessionRequest = {
cwd,
@@ -12573,6 +12908,7 @@
title,
branchSourceSessionId,
branchMessageIndex,
taskTrackingEnabled,
requestId,
};
if (cwd) saveRecentCwd(cwd);
@@ -12581,6 +12917,7 @@
if (title) payload.title = title;
if (branchSourceSessionId) payload.branchSourceSessionId = branchSourceSessionId;
if (branchMessageIndex !== null) payload.branchMessageIndex = branchMessageIndex;
if (taskTrackingEnabled) payload.taskTrackingEnabled = true;
send(payload);
}
@@ -12717,6 +13054,7 @@
title: options.title || '',
branchSourceSessionId: options.branchSourceSessionId || '',
branchMessageIndex: options.branchMessageIndex,
taskTrackingEnabled: options.taskTrackingEnabled === true,
});
}