feat: add rich run timeline backend

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
David Kaya
2026-03-24 00:04:47 +01:00
co-authored by Copilot
parent cf8c82b779
commit b742941559
16 changed files with 1072 additions and 26 deletions
+1
View File
@@ -16,6 +16,7 @@ function createSession(
updatedAt: '2026-03-23T00:00:00.000Z',
status,
messages,
runs: [],
};
}
+43
View File
@@ -2,6 +2,7 @@ import { describe, expect, test } from 'bun:test';
import { applySessionEventWorkspace } from '@renderer/lib/sessionWorkspace';
import type { SessionEventRecord } from '@shared/domain/event';
import type { SessionRunRecord } from '@shared/domain/runTimeline';
import { createWorkspaceSettings } from '@shared/domain/tooling';
import type { WorkspaceState } from '@shared/domain/workspace';
@@ -21,6 +22,7 @@ describe('session workspace helpers', () => {
updatedAt: '2026-03-23T00:00:00.000Z',
status: 'idle',
messages: [],
runs: [],
},
],
selectedSessionId: 'session-1',
@@ -185,6 +187,47 @@ describe('session workspace helpers', () => {
});
});
test('applies live run snapshots without waiting for a workspace refresh', () => {
const run: SessionRunRecord = {
id: 'run-1',
requestId: 'turn-1',
projectId: 'project-1',
projectPath: 'C:\\workspace\\alpha',
workspaceKind: 'project',
patternId: 'pattern-1',
patternName: 'Sequential Review',
patternMode: 'sequential',
triggerMessageId: 'msg-user-1',
startedAt: '2026-03-23T00:00:01.000Z',
status: 'running',
agents: [
{
agentId: 'agent-1',
agentName: 'Writer',
model: 'gpt-5.4',
},
],
events: [
{
id: 'run-event-1',
kind: 'run-started',
occurredAt: '2026-03-23T00:00:01.000Z',
status: 'completed',
messageId: 'msg-user-1',
},
],
};
const updated = applySessionEventWorkspace(createWorkspace(), {
sessionId: 'session-1',
kind: 'run-updated',
occurredAt: '2026-03-23T00:00:01.000Z',
run,
} satisfies SessionEventRecord);
expect(updated?.sessions[0].runs).toEqual([run]);
});
test('ignores events for unknown sessions', () => {
const workspace = createWorkspace();
expect(
+171
View File
@@ -0,0 +1,171 @@
import { describe, expect, test } from 'bun:test';
import type { PatternDefinition } from '@shared/domain/pattern';
import {
appendRunActivityEvent,
completeSessionRunRecord,
createSessionRunRecord,
normalizeSessionRunRecords,
upsertRunMessageEvent,
} from '@shared/domain/runTimeline';
import type { ProjectRecord } from '@shared/domain/project';
function createPattern(): PatternDefinition {
return {
id: 'pattern-sequential',
name: 'Sequential Trio Review',
description: 'Sequential handoff review flow.',
mode: 'sequential',
availability: 'available',
maxIterations: 1,
agents: [
{
id: 'agent-writer',
name: 'Writer',
description: 'Writes the draft.',
instructions: 'Write.',
model: 'gpt-5.4',
},
{
id: 'agent-reviewer',
name: 'Reviewer',
description: 'Reviews the draft.',
instructions: 'Review.',
model: 'claude-sonnet-4.5',
},
],
createdAt: '2026-03-23T00:00:00.000Z',
updatedAt: '2026-03-23T00:00:00.000Z',
};
}
function createProject(): ProjectRecord {
return {
id: 'project-1',
name: 'alpha',
path: 'C:\\workspace\\alpha',
addedAt: '2026-03-23T00:00:00.000Z',
};
}
describe('run timeline helpers', () => {
test('creates a persistent run record with agent lanes and a trigger event', () => {
const run = createSessionRunRecord({
requestId: 'turn-1',
project: createProject(),
workspaceKind: 'project',
pattern: createPattern(),
triggerMessageId: 'msg-user-1',
startedAt: '2026-03-23T00:00:01.000Z',
});
expect(run).toMatchObject({
requestId: 'turn-1',
projectId: 'project-1',
projectPath: 'C:\\workspace\\alpha',
patternId: 'pattern-sequential',
patternName: 'Sequential Trio Review',
patternMode: 'sequential',
triggerMessageId: 'msg-user-1',
status: 'running',
});
expect(run.agents).toEqual([
{
agentId: 'agent-writer',
agentName: 'Writer',
model: 'gpt-5.4',
reasoningEffort: undefined,
},
{
agentId: 'agent-reviewer',
agentName: 'Reviewer',
model: 'claude-sonnet-4.5',
reasoningEffort: undefined,
},
]);
expect(run.events[0]).toMatchObject({
kind: 'run-started',
status: 'completed',
messageId: 'msg-user-1',
});
});
test('records grouped message steps and settles the run on completion', () => {
const baseRun = createSessionRunRecord({
requestId: 'turn-1',
project: createProject(),
workspaceKind: 'project',
pattern: createPattern(),
triggerMessageId: 'msg-user-1',
startedAt: '2026-03-23T00:00:01.000Z',
});
const streamingRun = upsertRunMessageEvent(baseRun, {
messageId: 'msg-assistant-1',
authorName: 'Writer',
content: 'Draft',
occurredAt: '2026-03-23T00:00:02.000Z',
status: 'running',
});
const completedRun = completeSessionRunRecord(
upsertRunMessageEvent(streamingRun, {
messageId: 'msg-assistant-1',
authorName: 'Writer',
content: 'Draft polished into the final answer.',
occurredAt: '2026-03-23T00:00:03.000Z',
status: 'completed',
}),
'2026-03-23T00:00:04.000Z',
);
const messageEvent = completedRun.events.find((event) => event.kind === 'message');
expect(messageEvent).toMatchObject({
messageId: 'msg-assistant-1',
agentId: 'agent-writer',
agentName: 'Writer',
content: 'Draft polished into the final answer.',
status: 'completed',
updatedAt: '2026-03-23T00:00:03.000Z',
});
expect(completedRun.status).toBe('completed');
expect(completedRun.completedAt).toBe('2026-03-23T00:00:04.000Z');
expect(completedRun.events.at(-1)).toMatchObject({
kind: 'run-completed',
status: 'completed',
});
});
test('captures handoffs with explicit source and target agents', () => {
const run = appendRunActivityEvent(
createSessionRunRecord({
requestId: 'turn-1',
project: createProject(),
workspaceKind: 'project',
pattern: createPattern(),
triggerMessageId: 'msg-user-1',
startedAt: '2026-03-23T00:00:01.000Z',
}),
{
activityType: 'handoff',
occurredAt: '2026-03-23T00:00:02.000Z',
sourceAgentId: 'agent-writer',
agentId: 'agent-reviewer',
},
);
expect(run.events.at(-1)).toMatchObject({
kind: 'handoff',
agentId: 'agent-writer',
agentName: 'Writer',
sourceAgentId: 'agent-writer',
sourceAgentName: 'Writer',
targetAgentId: 'agent-reviewer',
targetAgentName: 'Reviewer',
});
});
test('normalizes missing run collections to an empty array', () => {
expect(normalizeSessionRunRecords(undefined)).toEqual([]);
});
});
+1
View File
@@ -51,6 +51,7 @@ function createSession(overrides?: Partial<SessionRecord>): SessionRecord {
updatedAt: '2026-03-23T00:00:00.000Z',
status: 'idle',
messages: [],
runs: [],
...overrides,
};
}
+2
View File
@@ -59,6 +59,7 @@ function createSession(overrides?: Partial<SessionRecord>): SessionRecord {
createdAt: '2026-03-23T00:00:00.000Z',
},
],
runs: [],
...overrides,
};
}
@@ -148,6 +149,7 @@ describe('session library helpers', () => {
updatedAt: '2026-03-23T00:07:00.000Z',
});
expect(session.messages[0]?.pending).toBe(false);
expect(session.runs).toEqual([]);
});
test('searches across session title, messages, projects, and patterns', () => {