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.
This commit is contained in:
tommy0103
2026-07-14 18:11:51 +08:00
parent 2cad3b554d
commit b58c34d3af
18 changed files with 1968 additions and 913 deletions
+58
View File
@@ -0,0 +1,58 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { assembleSessionMessages } from '../app/src/shared/session-detail-assembly.mjs';
test('session assembly preserves thinking and attaches tool result and subagent evidence', () => {
const messages = [
{ uuid: 'thinking-1', type: 'assistant', content_type: 'thinking', text: 'reasoning' },
{ uuid: 'answer-1', type: 'assistant', content_type: 'text', text: 'answer' },
{ uuid: 'tool-1', type: 'assistant', content_type: 'tool_use', text: '' },
{ uuid: 'result-1', type: 'user', content_type: 'tool_result', text: '' },
];
const assembled = assembleSessionMessages({
messages,
toolCalls: [{ id: 'call-1', message_uuid: 'tool-1', name: 'Agent', input_json: '{"description":"inspect"}' }],
toolResults: [{ tool_use_id: 'call-1', message_uuid: 'result-1', content: 'done', is_error: 0 }],
subagents: [{ agent_id: 'agent-1', parent_tool_use_id: 'call-1', agent_type: 'reviewer', description: 'inspect' }],
workflows: [],
});
assert.equal(assembled.length, 1);
assert.equal(assembled[0].uuid, 'answer-1');
assert.equal(assembled[0]._thinking, 'reasoning');
assert.deepEqual(assembled[0].tool_calls[0].result.content, 'done');
assert.equal(assembled[0].tool_calls[0].subagent.agent_id, 'agent-1');
});
test('session assembly keeps Skill evidence standalone and embeds matching workflow agents', () => {
const assembled = assembleSessionMessages({
messages: [
{ uuid: 'skill-1', type: 'assistant', content_type: 'tool_use', text: '' },
{ uuid: 'skill-md', type: 'user', content_type: 'text', is_meta: 1, text: 'Base directory for this skill\n# Skill' },
{ uuid: 'workflow-1', type: 'assistant', content_type: 'tool_use', text: '' },
],
toolCalls: [
{ id: 'call-skill', message_uuid: 'skill-1', name: 'Skill', input_json: '{"skill":"obelisk"}' },
{ id: 'call-workflow', message_uuid: 'workflow-1', name: 'Workflow', input_json: '{}' },
],
toolResults: [{ tool_use_id: 'call-workflow', content: 'run-1 complete', is_error: 0 }],
subagents: [],
workflows: [{
run_id: 'run-1',
workflow_name: 'review',
status: 'complete',
agents: [{ agent_id: 'agent-1', phase: 'review', label: 'Reviewer', state: 'complete' }],
}],
});
assert.equal(assembled[0]._skillMd, 'Base directory for this skill\n# Skill');
assert.equal(assembled[1].tool_calls[0].workflow.run_id, 'run-1');
assert.deepEqual(assembled[1].tool_calls[0].workflow.agents, [{
agent_id: 'agent-1',
phase: 'review',
label: 'Reviewer',
state: 'complete',
tokens: undefined,
duration_ms: undefined,
}]);
});
+84
View File
@@ -0,0 +1,84 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import {
applySessionPatch,
createSessionPatch,
createSessionPatchCursor,
} from '../app/src/shared/session-patch.mjs';
function snapshot(overrides = {}) {
return {
messages: [
{ uuid: 'message-1', timestamp: '2026-07-14T00:00:01Z', text: 'one' },
{ uuid: 'message-2', timestamp: '2026-07-14T00:00:02Z', text: 'two' },
],
toolCalls: [{ id: 'call-1', message_uuid: 'message-1', name: 'exec', input_json: '"return 1"' }],
toolResults: [{ tool_use_id: 'call-1', message_uuid: 'message-1', content: 'running', is_error: 0 }],
subagents: [],
workflows: [{ run_id: 'workflow-1', status: 'running', agents: [{ agent_id: 'agent-1', state: 'running' }] }],
summaries: [],
...overrides,
};
}
test('session patch returns only appended and updated rows, then reconstructs the new snapshot', () => {
const current = snapshot();
const cursor = createSessionPatchCursor(current);
const next = snapshot({
messages: [...current.messages, { uuid: 'message-3', timestamp: '2026-07-14T00:00:03Z', text: 'three' }],
toolResults: [{ ...current.toolResults[0], content: 'complete' }],
});
const patch = createSessionPatch(next, cursor);
assert.deepEqual(patch.changes.messages.map(row => row.uuid), ['message-3']);
assert.deepEqual(patch.changes.toolResults.map(row => row.tool_use_id), ['call-1']);
assert.deepEqual(patch.changes.toolCalls, []);
assert.deepEqual(patch.removed.messages, []);
assert.equal(patch.positions.messages['message-3'], 2);
assert.deepEqual(applySessionPatch(current, cursor, patch), {
snapshot: next,
cursor: createSessionPatchCursor(next),
});
});
test('session patch reports removals and nested workflow updates', () => {
const current = snapshot();
const cursor = createSessionPatchCursor(current);
const next = snapshot({
messages: [current.messages[1]],
workflows: [{ run_id: 'workflow-1', status: 'complete', agents: [{ agent_id: 'agent-1', state: 'complete' }] }],
});
const patch = createSessionPatch(next, cursor);
assert.deepEqual(patch.removed.messages, ['message-1']);
assert.deepEqual(patch.changes.workflows, next.workflows);
assert.deepEqual(applySessionPatch(current, cursor, patch).snapshot, next);
});
test('session patch repositions existing rows when their content is unchanged', () => {
const current = snapshot();
const cursor = createSessionPatchCursor(current);
const next = snapshot({
messages: [current.messages[1], current.messages[0]],
});
const patch = createSessionPatch(next, cursor);
assert.deepEqual(applySessionPatch(current, cursor, patch).snapshot, next);
});
test('session patch cursor is compact and never carries transcript content', () => {
const largeText = 'private transcript content '.repeat(1000);
const current = snapshot({
messages: [{ uuid: 'message-large', timestamp: '2026-07-14T00:00:00Z', text: largeText }],
toolResults: [{ tool_use_id: 'call-large', message_uuid: 'message-large', content: largeText, is_error: 0 }],
});
const cursor = createSessionPatchCursor(current);
const serializedCursor = JSON.stringify(cursor);
assert.equal(serializedCursor.includes('private transcript content'), false);
assert.ok(serializedCursor.length < JSON.stringify(current).length / 20);
});
+100
View File
@@ -2,6 +2,14 @@ import { test } from 'node:test';
import assert from 'node:assert/strict';
import { createSessionLiveReloadCoordinator } from '../app/src/renderer/src/session-live-reload.mjs';
import { state } from '../app/src/renderer/src/store.js';
import {
getCachedSessionDetail,
loadSessionDetail,
loadSessionDetailPatch,
} from '../app/src/renderer/src/data.js';
import { assembleSessionMessages } from '../app/src/shared/session-detail-assembly.mjs';
import { createSessionPatch } from '../app/src/shared/session-patch.mjs';
test('live snapshots coalesce while scrolling and commit once after scroll end', async () => {
let scrolling = true;
@@ -87,3 +95,95 @@ test('scrolling that starts during IPC defers the loaded snapshot commit', async
assert.equal(loads, 1, 'the already-loaded snapshot is reused');
assert.deepEqual(commits, ['loaded-before-scroll-ended']);
});
test('a skipped live patch does not advance the visible patch baseline', async t => {
const sessionId = 'coalesced-patch-session';
const previousSessions = state.sessions;
t.after(() => {
state.sessions = previousSessions;
delete globalThis.window;
});
let rows = [
{ uuid: 'message-1', type: 'user', timestamp: '2026-07-14T00:00:01Z', text: 'one' },
];
let patchCalls = 0;
let releaseFirstPatch;
let firstPatchStarted;
const firstPatchGate = new Promise(resolve => { releaseFirstPatch = resolve; });
const firstPatchReady = new Promise(resolve => { firstPatchStarted = resolve; });
globalThis.window = {
obelisk: {
getSessionMessages: async () => rows,
getSessionToolCalls: async () => [],
getSessionToolResults: async () => [],
getSessionSubagents: async () => [],
getSessionWorkflows: async () => [],
getSessionSummaries: async () => [],
getSessionPatch: async (_id, cursor) => {
const snapshotAtCall = { messages: assembleSessionMessages({
messages: rows,
toolCalls: [],
toolResults: [],
subagents: [],
workflows: [],
}), workflows: [] };
patchCalls++;
if (patchCalls === 1) {
firstPatchStarted();
await firstPatchGate;
}
return createSessionPatch(snapshotAtCall, cursor);
},
},
};
state.sessions = [{ id: sessionId, messages: [] }];
await loadSessionDetail(sessionId);
const commits = [];
const coordinator = createSessionLiveReloadCoordinator({
isScrolling: () => false,
load: () => loadSessionDetailPatch(sessionId),
commit: async latest => {
commits.push({
messages: latest.messages.map(message => message.uuid),
changedIds: latest.messagePatch.changedIds,
});
latest.acceptMessagePatch?.();
},
});
rows = [...rows, { uuid: 'message-2', type: 'assistant', timestamp: '2026-07-14T00:00:02Z', text: 'two' }];
const first = coordinator.request();
await firstPatchReady;
rows = [...rows, { uuid: 'message-3', type: 'assistant', timestamp: '2026-07-14T00:00:03Z', text: 'three' }];
const second = coordinator.request();
releaseFirstPatch();
await Promise.all([first, second]);
assert.deepEqual(commits, [{
messages: ['message-1', 'message-2', 'message-3'],
changedIds: ['message-2', 'message-3'],
}]);
assert.deepEqual(
getCachedSessionDetail(sessionId).messages.map(message => message.uuid),
['message-1', 'message-2', 'message-3'],
'accepted patches become the reusable session-detail snapshot',
);
assert.deepEqual(
state.sessions.find(session => session.id === sessionId).messages,
[],
'the stale full-snapshot copy is invalidated after patch acceptance',
);
const evictionSessionIds = ['eviction-session-1', 'eviction-session-2', 'eviction-session-3'];
state.sessions.push(...evictionSessionIds.map(id => ({ id, messages: [] })));
for (const id of evictionSessionIds) await loadSessionDetail(id);
assert.equal(getCachedSessionDetail(sessionId), null, 'the oldest accepted snapshot is evicted by the bounded cache');
assert.deepEqual(
state.sessions.find(session => session.id === sessionId).messages,
[],
'an evicted session cannot fall back to stale initial messages and must reload',
);
});
+10
View File
@@ -4,6 +4,7 @@ import assert from 'node:assert/strict';
import {
createSessionLiveState,
consumeSessionDirty,
markSessionDirty,
noteSessionUpdated,
} from '../app/src/renderer/src/session-live.mjs';
@@ -25,3 +26,12 @@ test('session live state reloads the visible session without leaving it dirty',
assert.deepEqual(action, { reload: true, sessionId: 'session-1' });
assert.equal(consumeSessionDirty(live, 'session-1'), false);
});
test('a rejected visible commit can put the session back into the dirty set', () => {
const live = createSessionLiveState();
noteSessionUpdated(live, 'session-1', 'session-1');
markSessionDirty('session-1', live);
assert.equal(consumeSessionDirty(live, 'session-1'), true);
});
+37 -5
View File
@@ -6,6 +6,14 @@ const sessionDetail = readFileSync(
new URL('../app/src/renderer/src/views/SessionDetail.vue', import.meta.url),
'utf8',
);
const timelineRow = readFileSync(
new URL('../app/src/renderer/src/components/SessionTimelineRow.vue', import.meta.url),
'utf8',
);
const timelinePresentation = readFileSync(
new URL('../app/src/renderer/src/session-timeline-presentation.mjs', import.meta.url),
'utf8',
);
const viewportModule = readFileSync(
new URL('../app/src/renderer/src/session-timeline-viewport.mjs', import.meta.url),
'utf8',
@@ -20,8 +28,11 @@ test('SessionDetail renders a measured virtual window instead of the complete ti
assert.match(sessionDetail, /v-for="virtualRow in virtualRows"/);
assert.match(sessionDetail, /:data-index="virtualRow\.index"/);
assert.match(sessionDetail, /:ref="measureElement"/);
assert.match(sessionDetail, /<SessionTimelineRow/);
assert.match(timelineRow, /buildSessionTimelinePresentation/);
assert.doesNotMatch(sessionDetail, /renderMarkdown|renderPrettyTool/);
assert.doesNotMatch(sessionDetail, /querySelectorAll/);
assert.doesNotMatch(sessionDetail, /v-memo/);
assert.doesNotMatch(sessionDetail + timelineRow, /v-memo/);
assert.doesNotMatch(sessionDetail, /session-view-state/);
assert.doesNotMatch(sessionDetail, /outerHTML/);
assert.doesNotMatch(sessionDetail, /closest\(['"]\.msg/);
@@ -44,14 +55,35 @@ test('timeline viewport owns dynamic measurement, overscan, anchoring, and tail-
test('timeline count and disclosure classes come from renderer state rather than DOM state', () => {
assert.match(sessionDetail, /const totalMsgs = computed\(\(\) => timelineItems\.value\.length\)/);
assert.match(sessionDetail, /disclosures\.isOpen/);
assert.match(sessionDetail, /disclosures\.isRaw/);
assert.doesNotMatch(sessionDetail, /function toggleDisclosure[\s\S]{0,200}classList/);
assert.doesNotMatch(sessionDetail, /function toggleRaw[\s\S]{0,200}classList/);
assert.match(timelineRow, /disclosures\.isOpen/);
assert.match(timelineRow, /disclosures\.isRaw/);
assert.doesNotMatch(timelineRow, /function toggleDisclosure[\s\S]{0,200}classList/);
assert.doesNotMatch(timelineRow, /function toggleRaw[\s\S]{0,200}classList/);
assert.doesNotMatch(sessionDetail, /createSessionDisclosureRegistry/);
});
test('timeline row memoizes derived HTML behind stable content dependencies', () => {
assert.match(timelineRow, /const presentation = computed/);
assert.match(timelineRow, /query: props\.query/);
assert.match(timelineRow, /expandedText: expandedText\.value/);
assert.match(timelinePresentation, /toolPrettyHtml/);
assert.match(timelinePresentation, /toolResultHtml/);
assert.match(timelinePresentation, /renderMarkdown/);
});
test('cold startup does not enable append-follow before a real session snapshot exists', () => {
assert.match(sessionDetail, /if \(!latest\) return/);
assert.match(sessionDetail, /timelineViewport\.completeInitialSnapshot\(\)/);
});
test('live patch state advances only after the visible snapshot commit is accepted', () => {
const loadLiveSnapshot = sessionDetail.match(/async function loadLiveSnapshot\(\) \{([\s\S]*?)\n\}/)?.[1] || '';
const commitLiveSnapshot = sessionDetail.match(/async function commitLiveSnapshot\(snapshot\) \{([\s\S]*?)\n\}/)?.[1] || '';
assert.doesNotMatch(loadLiveSnapshot, /clearSessionDirty|acceptMessagePatch/);
assert.match(
commitLiveSnapshot,
/await commitSessionSnapshot\(snapshot\.latest\);[\s\S]*acceptMessagePatch[\s\S]*clearSessionDirty/,
);
assert.match(commitLiveSnapshot, /markSessionDirty\(snapshot\.sessionId\)/);
});