feat: improve conversation timeline and agent plans

This commit is contained in:
shiyue
2026-07-29 16:50:12 +08:00
parent 4d97446a6f
commit 33c9783079
16 changed files with 663 additions and 59 deletions

View File

@@ -1472,14 +1472,20 @@
const title = String(event.title || '').replace(/\s+/g, ' ').trim().slice(0, 120);
const changedAtMs = Date.parse(String(event.changedAt || ''));
const messageIndex = Number(event.messageIndex);
const anchorMessageIndex = event.anchorMessageIndex;
const source = String(event.source || '').trim().toLowerCase();
if (!title || !Number.isFinite(changedAtMs) || !Number.isFinite(messageIndex) || messageIndex < 0 || source !== 'llm') return;
normalized.push({
const normalizedMessageIndex = Math.trunc(messageIndex);
const normalizedEvent = {
title,
changedAt: new Date(changedAtMs).toISOString(),
messageIndex: Math.trunc(messageIndex),
messageIndex: normalizedMessageIndex,
source: 'llm',
});
};
if (Number.isInteger(anchorMessageIndex) && anchorMessageIndex >= 0 && anchorMessageIndex < normalizedMessageIndex) {
normalizedEvent.anchorMessageIndex = anchorMessageIndex;
}
normalized.push(normalizedEvent);
});
return normalized.slice(-100);
}
@@ -1493,10 +1499,11 @@
function buildUserOutlineTimelineItems(messageItems, titleHistory = []) {
const timeline = [];
const timelineMessages = [];
(Array.isArray(messageItems) ? messageItems : []).forEach((message, order) => {
const messageIndex = Number(message.messageIndex);
const timestampMs = Date.parse(String(message.timestamp || ''));
timeline.push({
const timelineMessage = {
...message,
type: 'message',
messageNumber: order + 1,
@@ -1504,17 +1511,31 @@
sortPosition: Number.isFinite(messageIndex) && messageIndex >= 0 ? messageIndex * 2 : null,
sortTime: Number.isFinite(timestampMs) ? timestampMs : null,
sortOrder: order,
});
};
timelineMessages.push(timelineMessage);
timeline.push(timelineMessage);
});
normalizeOutlineTitleHistory(titleHistory).forEach((event, order) => {
const timestampMs = Date.parse(event.changedAt);
const exactAnchor = Number.isInteger(event.anchorMessageIndex)
? timelineMessages.find((message) => message.messageIndex === event.anchorMessageIndex) || null
: null;
const precedingAnchor = timelineMessages.reduce((latest, message) => {
if (!Number.isFinite(message.messageIndex) || message.messageIndex >= event.messageIndex) return latest;
if (!latest || message.messageIndex > latest.messageIndex) return message;
return latest;
}, null);
const anchor = exactAnchor || precedingAnchor;
const anchorMessageIndex = Number.isFinite(anchor?.messageIndex) ? anchor.messageIndex : null;
timeline.push({
type: 'title',
label: event.title,
timestamp: event.changedAt,
timestamp: anchor?.timestamp || event.changedAt,
changedAt: event.changedAt,
messageIndex: event.messageIndex,
...(anchorMessageIndex !== null ? { anchorMessageIndex } : {}),
source: event.source,
sortPosition: event.messageIndex * 2 - 1,
sortPosition: anchorMessageIndex !== null ? anchorMessageIndex * 2 - 1 : event.messageIndex * 2 - 1,
sortTime: Number.isFinite(timestampMs) ? timestampMs : null,
sortOrder: order,
});
@@ -7475,6 +7496,45 @@
];
}
function markMergedCollabAgentElement(element, tools, mergedTool = null) {
if (!element) return element;
const map = tools instanceof Map ? tools : new Map();
if (!(tools instanceof Map)) {
const list = Array.isArray(tools) ? tools : [];
list.forEach((tool, index) => {
if (toolKind(tool) !== 'collab_agent_tool_call') return;
const key = String(tool?.id || `collab-${index + 1}`);
map.set(key, tool);
});
}
const displayTool = mergedTool || mergeCollabAgentTools(Array.from(map.values()));
element.dataset.collabMerged = 'true';
if (displayTool?.id) element.dataset.toolUseId = String(displayTool.id);
element.dataset.childIds = displayTool ? getCollabAgentIdsFromTool(displayTool).join(',') : '';
element.__collabTools = map;
return element;
}
function collabAgentToolElementMatches(element, toolUseId) {
if (!element || !toolUseId) return false;
const targetId = String(toolUseId);
if (String(element.dataset?.toolUseId || '') === targetId) return true;
const map = element.__collabTools;
if (!(map instanceof Map)) return false;
if (map.has(targetId)) return true;
return Array.from(map.keys()).some((key) => String(key) === targetId);
}
function findCollabAgentToolElement(toolUseId, preferredElement = null) {
if (preferredElement?.isConnected && collabAgentToolElementMatches(preferredElement, toolUseId)) {
return preferredElement;
}
return findLatestToolCallElement(messagesDiv, (candidate) => (
candidate.dataset?.collabMerged === 'true'
&& collabAgentToolElementMatches(candidate, toolUseId)
));
}
function collabStateTone(statusText) {
const normalized = String(statusText || '').toLowerCase();
if (!normalized) return 'pending';
@@ -7722,10 +7782,14 @@
const toolMount = bubble.querySelector(':scope > .cross-conversation-reply-body') || bubble;
const FOLD_AT = 3;
let grouped = false;
const savedCollabTools = m.toolCalls.filter((tc) => toolKind(tc) === 'collab_agent_tool_call');
const renderToolCalls = renderToolCallsWithMergedCollab(m.toolCalls);
for (const tc of renderToolCalls) {
if (isEmptyReasoningTool(tc)) continue;
const details = createToolCallElement(tc.id || `saved-${Math.random().toString(36).slice(2)}`, tc, true);
if (toolKind(tc) === 'collab_agent_tool_call') {
markMergedCollabAgentElement(details, savedCollabTools, tc);
}
// 散落的 .tool-call 达到 FOLD_AT 个时,移入唯一 .tool-group
const loose = Array.from(toolMount.children).filter(isGroupableToolCall);
@@ -8151,18 +8215,14 @@
if (!merged) return existing;
if (existing) {
existing.__collabTools = map;
existing.dataset.toolUseId = merged.id || existing.dataset.toolUseId || '';
existing.dataset.childIds = getCollabAgentIdsFromTool(merged).join(',');
markMergedCollabAgentElement(existing, map, merged);
existing.replaceChildren(buildToolContentElement(merged));
removeDuplicateCollabAgentNodes(toolsDiv);
return existing;
}
const el = createToolCallElement(merged.id, merged, !!merged.done);
el.dataset.collabMerged = 'true';
el.dataset.childIds = getCollabAgentIdsFromTool(merged).join(',');
el.__collabTools = map;
markMergedCollabAgentElement(el, map, merged);
toolsDiv.appendChild(el);
removeDuplicateCollabAgentNodes(toolsDiv);
return el;
@@ -8270,14 +8330,16 @@
function updateToolCall(toolUseId, result, done = true) {
const tool = activeToolCalls.get(toolUseId) || null;
const toolUseIdText = toolUseId ? String(toolUseId) : '';
const scope = getLatestAssistantToolScope();
if (toolKind(tool) === 'collab_agent_tool_call') {
const existingCollabElement = findCollabAgentToolElement(toolUseId, tool?.domElement);
const scope = existingCollabElement?.parentElement || getLatestAssistantToolScope();
const toolsDiv = scope?.querySelector?.('.msg-tools') || scope?.querySelector?.('.msg-bubble') || scope;
const nextTool = { ...tool, result, done };
const el = upsertCollabAgentToolCall(toolsDiv, toolUseId, nextTool, done);
if (el) rememberToolCallTarget(toolUseId, nextTool, el);
return;
}
const scope = getLatestAssistantToolScope();
let el = tool?.domElement && tool.domElement.isConnected ? tool.domElement : null;
if (!el) {
@@ -8363,6 +8425,7 @@
});
if (!isCurrentSessionUpdate) return;
const previousTool = activeToolCalls.get(toolUseId) || null;
activeToolCalls.set(toolUseId, {
id: toolUseId,
name: tool.name,
@@ -8371,6 +8434,7 @@
kind: tool.kind || null,
meta: tool.meta || null,
done: !!tool.done,
domElement: previousTool?.domElement || null,
});
updateToolCall(toolUseId, tool.result, !!tool.done);
}