Files
obelisk/app/src/shared/session-detail-assembly.mjs
T
tommy0103 b58c34d3af perf(app): stabilize live session timeline updates
Virtualize SessionDetail rows behind stable presentation boundaries and apply typed incremental patches only after visible commits. Preserve reader state across live updates, handle coalesced and reordered patches, and verify the 120Hz append path with Electron tracing.
2026-07-14 18:11:51 +08:00

174 lines
6.6 KiB
JavaScript

// @ts-check
/** @typedef {import('./session-detail-types.ts').AssembledMessage} AssembledMessage */
/** @typedef {import('./session-detail-types.ts').AssembledToolCall} AssembledToolCall */
/** @typedef {import('./session-detail-types.ts').SessionDetailAssemblyInput} SessionDetailAssemblyInput */
/** @typedef {import('./session-detail-types.ts').SessionSubagentRow} SessionSubagentRow */
/** @typedef {import('./session-detail-types.ts').SessionToolResultRow} SessionToolResultRow */
/**
* @param {SessionDetailAssemblyInput} input
* @returns {AssembledMessage[]}
*/
export function assembleSessionMessages({ messages, toolCalls, toolResults, subagents, workflows }) {
const resultsByCallId = /** @type {Map<string, SessionToolResultRow>} */ (new Map());
for (const result of toolResults || []) resultsByCallId.set(result.tool_use_id, result);
const subagentsByCallId = /** @type {Map<string, SessionSubagentRow>} */ (new Map());
for (const subagent of subagents || []) {
if (subagent.parent_tool_use_id) subagentsByCallId.set(subagent.parent_tool_use_id, subagent);
}
const callsByMessageUuid = /** @type {Map<string, AssembledToolCall[]>} */ (new Map());
for (const toolCall of toolCalls || []) {
const call = /** @type {AssembledToolCall} */ ({
id: toolCall.id,
name: toolCall.name,
input_json: toolCall.input_json,
result: resultsByCallId.get(toolCall.id) || null,
});
const subagent = subagentsByCallId.get(toolCall.id);
if (subagent) {
call.subagent = {
agent_id: subagent.agent_id,
agent_type: subagent.agent_type,
description: subagent.description,
};
}
const messageUuid = toolCall.message_uuid;
const calls = callsByMessageUuid.get(messageUuid) || [];
calls.push(call);
callsByMessageUuid.set(messageUuid, calls);
}
for (const workflow of workflows || []) {
for (const calls of callsByMessageUuid.values()) {
for (const call of calls) {
if (call.name !== 'Workflow' || call.workflow) continue;
const resultText = call.result?.content || '';
if (!resultText.includes(workflow.run_id) && !resultText.includes(workflow.workflow_name || '___none___')) continue;
call.workflow = {
run_id: workflow.run_id,
workflow_name: workflow.workflow_name,
status: workflow.status,
duration_ms: workflow.duration_ms,
total_tokens: workflow.total_tokens,
agent_count: workflow.agent_count,
agents: (workflow.agents || []).map(agent => ({
agent_id: agent.agent_id,
phase: agent.phase,
label: agent.label,
state: agent.state,
tokens: agent.tokens,
duration_ms: agent.duration_ms,
})),
};
}
}
}
const metaPattern = /^\s*<(task-notification|command-name|local-command|system-reminder)/;
const rawAssembled = (messages || []).map(message => {
const assembled = /** @type {AssembledMessage} */ ({
uuid: message.uuid,
type: message.type || message.role,
timestamp: message.timestamp,
text: message.text,
content_type: message.content_type || null,
is_meta: message.is_meta || (message.text && metaPattern.test(message.text) ? 1 : 0),
});
const calls = callsByMessageUuid.get(message.uuid);
if (calls?.length) assembled.tool_calls = calls;
return assembled;
});
const assembledMessages = /** @type {AssembledMessage[]} */ ([]);
for (let index = 0; index < rawAssembled.length; index++) {
const message = rawAssembled[index];
if (message.content_type === 'tool_result') continue;
if (message.type === 'assistant' && message.content_type === 'thinking') {
const thinkingParts = [message.text || ''];
let nextIndex = index + 1;
while (
nextIndex < rawAssembled.length
&& rawAssembled[nextIndex].type === 'assistant'
&& rawAssembled[nextIndex].content_type === 'thinking'
) {
thinkingParts.push(rawAssembled[nextIndex].text || '');
nextIndex++;
}
if (
nextIndex < rawAssembled.length
&& rawAssembled[nextIndex].type === 'assistant'
&& rawAssembled[nextIndex].content_type !== 'thinking'
) {
rawAssembled[nextIndex]._thinking = thinkingParts.join('\n\n');
index = nextIndex - 1;
continue;
}
assembledMessages.push({ ...message, text: thinkingParts.join('\n\n'), content_type: 'thinking' });
index = nextIndex - 1;
continue;
}
if (message.type === 'assistant' && message.content_type === 'tool_use') {
const merged = /** @type {AssembledMessage} */ ({
...message,
tool_calls: [...(message.tool_calls || [])],
});
const mergedCalls = merged.tool_calls || [];
if (message._thinking) merged._thinking = message._thinking;
const skillOnly = mergedCalls.length === 1 && mergedCalls[0].name === 'Skill';
let nextIndex = index + 1;
while (nextIndex < rawAssembled.length) {
const next = rawAssembled[nextIndex];
if (next.content_type === 'tool_result') {
nextIndex++;
continue;
}
if (next.is_meta && next.text && next.text.includes('Base directory for this skill')) {
merged._skillMd = next.text;
nextIndex++;
continue;
}
if (!skillOnly && next.type === 'assistant' && next.content_type === 'tool_use') {
if (next.tool_calls) mergedCalls.push(...next.tool_calls);
if (next.text && !merged.text) merged.text = next.text;
nextIndex++;
continue;
}
break;
}
assembledMessages.push(merged);
index = nextIndex - 1;
continue;
}
const output = /** @type {AssembledMessage} */ ({ ...message });
if (message._thinking) output._thinking = message._thinking;
if (message.type === 'assistant' && message.content_type !== 'tool_use' && message.content_type !== 'thinking') {
if (!output.tool_calls) output.tool_calls = [];
let nextIndex = index + 1;
while (nextIndex < rawAssembled.length) {
const next = rawAssembled[nextIndex];
if (next.content_type === 'tool_result') {
nextIndex++;
continue;
}
if (next.type === 'assistant' && next.content_type === 'tool_use') {
if (next.tool_calls) output.tool_calls.push(...next.tool_calls);
nextIndex++;
continue;
}
break;
}
if (!output.tool_calls.length) delete output.tool_calls;
index = nextIndex - 1;
}
assembledMessages.push(output);
}
return assembledMessages;
}