@synergenius/flow-weaver-pack-weaver 0.9.53 → 0.9.55
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ui/approval-card.js +134 -0
- package/dist/ui/bot-workspace.js +1087 -0
- package/dist/ui/genesis-block.js +155 -0
- package/dist/ui/queue-input.js +82 -0
- package/dist/ui/session-bar.js +174 -0
- package/dist/ui/settings-section.js +104 -0
- package/dist/ui/steer-api.d.ts +7 -0
- package/dist/ui/steer-api.d.ts.map +1 -0
- package/dist/ui/steer-api.js +11 -0
- package/dist/ui/steer-api.js.map +1 -0
- package/dist/ui/trace-to-timeline.d.ts +85 -0
- package/dist/ui/trace-to-timeline.d.ts.map +1 -0
- package/dist/ui/trace-to-timeline.js +115 -0
- package/dist/ui/trace-to-timeline.js.map +1 -0
- package/dist/ui/use-stream-timeline.d.ts +48 -0
- package/dist/ui/use-stream-timeline.d.ts.map +1 -0
- package/dist/ui/use-stream-timeline.js +193 -0
- package/dist/ui/use-stream-timeline.js.map +1 -0
- package/flowweaver.manifest.json +3 -1
- package/package.json +1 -1
- package/src/ui/approval-card.tsx +118 -0
- package/src/ui/bot-workspace.tsx +406 -0
- package/src/ui/genesis-block.tsx +138 -0
- package/src/ui/queue-input.tsx +56 -0
- package/src/ui/session-bar.tsx +157 -0
- package/src/ui/settings-section.tsx +97 -0
- package/src/ui/steer-api.ts +17 -0
- package/src/ui/trace-to-timeline.ts +204 -0
- package/src/ui/use-stream-timeline.ts +240 -0
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SessionBar — shows at top of workspace when an autonomous session is active.
|
|
3
|
+
* Displays session stats and provides start/pause/stop controls.
|
|
4
|
+
*/
|
|
5
|
+
const React = require('react');
|
|
6
|
+
const { useState, useEffect, useCallback, useRef } = React;
|
|
7
|
+
const { Flex, Typography, Icon, Button, Badge, toast, formatDuration, formatCost } = require('@fw/plugin-ui-kit');
|
|
8
|
+
|
|
9
|
+
interface SessionState {
|
|
10
|
+
sessionId?: string;
|
|
11
|
+
status: string;
|
|
12
|
+
currentTask?: string | null;
|
|
13
|
+
completedTasks?: number;
|
|
14
|
+
totalCost?: number;
|
|
15
|
+
startedAt?: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface SessionBarProps {
|
|
19
|
+
callTool: (tool: string, args?: Record<string, unknown>) => Promise<unknown>;
|
|
20
|
+
dispatchEvent: (name: string, detail?: unknown) => void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function SessionBar({ callTool, dispatchEvent }: SessionBarProps) {
|
|
24
|
+
const [session, setSession] = useState<SessionState | null>(null);
|
|
25
|
+
const [elapsed, setElapsed] = useState(0);
|
|
26
|
+
const [starting, setStarting] = useState(false);
|
|
27
|
+
const [stopping, setStopping] = useState(false);
|
|
28
|
+
const [pausing, setPausing] = useState(false);
|
|
29
|
+
|
|
30
|
+
const prevActiveRef = useRef(false);
|
|
31
|
+
|
|
32
|
+
const pollStatus = useCallback(async () => {
|
|
33
|
+
try {
|
|
34
|
+
const result = (await callTool('fw_weaver_session', { action: 'status' })) as SessionState;
|
|
35
|
+
const isActive = result.status !== 'no active session' && result.status !== 'idle';
|
|
36
|
+
setSession(isActive ? result : null);
|
|
37
|
+
|
|
38
|
+
if (prevActiveRef.current && !isActive) {
|
|
39
|
+
dispatchEvent('fw:refresh-bot-workspace');
|
|
40
|
+
}
|
|
41
|
+
prevActiveRef.current = isActive;
|
|
42
|
+
} catch {
|
|
43
|
+
/* non-fatal */
|
|
44
|
+
}
|
|
45
|
+
}, [callTool, dispatchEvent]);
|
|
46
|
+
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
pollStatus();
|
|
49
|
+
const interval = setInterval(pollStatus, 5_000);
|
|
50
|
+
return () => clearInterval(interval);
|
|
51
|
+
}, [pollStatus]);
|
|
52
|
+
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
if (!session?.startedAt || session.status === 'idle') return;
|
|
55
|
+
const tick = () => setElapsed(Date.now() - (session.startedAt ?? Date.now()));
|
|
56
|
+
tick();
|
|
57
|
+
const interval = setInterval(tick, 1000);
|
|
58
|
+
return () => clearInterval(interval);
|
|
59
|
+
}, [session?.startedAt, session?.status]);
|
|
60
|
+
|
|
61
|
+
const [startError, setStartError] = useState<string | null>(null);
|
|
62
|
+
|
|
63
|
+
const handleStart = useCallback(async () => {
|
|
64
|
+
setStarting(true);
|
|
65
|
+
setStartError(null);
|
|
66
|
+
try {
|
|
67
|
+
const result = await callTool('fw_weaver_session', { action: 'start' });
|
|
68
|
+
const data = result as Record<string, unknown> | null;
|
|
69
|
+
if (data?.error) {
|
|
70
|
+
setStartError(data.error as string);
|
|
71
|
+
toast(data.error as string, { type: 'error' });
|
|
72
|
+
setStarting(false);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
toast('Session started', { type: 'success' });
|
|
76
|
+
dispatchEvent('fw:refresh-bot-workspace');
|
|
77
|
+
await pollStatus();
|
|
78
|
+
} catch (err: unknown) {
|
|
79
|
+
const msg = err instanceof Error ? err.message : 'Failed to start session';
|
|
80
|
+
setStartError(msg);
|
|
81
|
+
toast(msg, { type: 'error' });
|
|
82
|
+
}
|
|
83
|
+
setStarting(false);
|
|
84
|
+
}, [callTool, pollStatus, dispatchEvent]);
|
|
85
|
+
|
|
86
|
+
const handleStop = useCallback(async () => {
|
|
87
|
+
setStopping(true);
|
|
88
|
+
try {
|
|
89
|
+
await callTool('fw_weaver_session', { action: 'stop' });
|
|
90
|
+
toast('Session ended', { type: 'info' });
|
|
91
|
+
await pollStatus();
|
|
92
|
+
} catch (err: unknown) {
|
|
93
|
+
toast(err instanceof Error ? err.message : 'Failed to stop session', { type: 'error' });
|
|
94
|
+
}
|
|
95
|
+
setStopping(false);
|
|
96
|
+
}, [callTool, pollStatus]);
|
|
97
|
+
|
|
98
|
+
const handlePause = useCallback(async () => {
|
|
99
|
+
setPausing(true);
|
|
100
|
+
try {
|
|
101
|
+
await callTool('fw_weaver_steer', { command: 'pause' });
|
|
102
|
+
toast('Session paused', { type: 'info' });
|
|
103
|
+
} catch (err: unknown) {
|
|
104
|
+
toast(err instanceof Error ? err.message : 'Failed to pause session', { type: 'error' });
|
|
105
|
+
}
|
|
106
|
+
setPausing(false);
|
|
107
|
+
}, [callTool]);
|
|
108
|
+
|
|
109
|
+
const isActive = session && session.status !== 'idle' && session.status !== 'no active session';
|
|
110
|
+
|
|
111
|
+
if (!isActive) {
|
|
112
|
+
return React.createElement(Flex, {
|
|
113
|
+
variant: 'row-center-space-between-nowrap-10',
|
|
114
|
+
style: { flexShrink: 0, padding: '8px 16px', backgroundColor: 'transparent', borderBottom: '1px solid var(--color-border-default)' },
|
|
115
|
+
},
|
|
116
|
+
React.createElement(Icon, { name: 'smartToy', size: 14, color: 'color-text-subtle' }),
|
|
117
|
+
React.createElement(Flex, { variant: 'row-center-start-nowrap-12', style: { flex: 1, fontSize: '12px' } },
|
|
118
|
+
React.createElement(Typography, {
|
|
119
|
+
variant: 'caption-regular',
|
|
120
|
+
color: startError ? 'color-status-negative' : 'color-text-subtle',
|
|
121
|
+
}, startError ?? 'No active session'),
|
|
122
|
+
),
|
|
123
|
+
React.createElement(Flex, { variant: 'row-center-start-nowrap-6', style: { flexShrink: 0 } },
|
|
124
|
+
React.createElement(Button, {
|
|
125
|
+
size: 'sm', variant: 'clear', onClick: handleStart, loading: starting, disabled: starting,
|
|
126
|
+
}, 'Start Session'),
|
|
127
|
+
),
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return React.createElement(Flex, {
|
|
132
|
+
variant: 'row-center-space-between-nowrap-10',
|
|
133
|
+
style: { flexShrink: 0, padding: '8px 16px', backgroundColor: 'var(--color-brand-main-alpha-10)', borderBottom: '1px solid var(--color-brand-main)' },
|
|
134
|
+
},
|
|
135
|
+
React.createElement(Badge, { variant: 'success' }, 'Session'),
|
|
136
|
+
React.createElement(Flex, {
|
|
137
|
+
variant: 'row-center-start-nowrap-12',
|
|
138
|
+
style: { flex: 1, fontSize: '12px', color: 'var(--color-text-medium)' },
|
|
139
|
+
},
|
|
140
|
+
session.completedTasks != null && React.createElement('span', null, `${session.completedTasks} tasks done`),
|
|
141
|
+
session.totalCost != null && session.totalCost > 0 && React.createElement('span', null, formatCost(session.totalCost)),
|
|
142
|
+
session.startedAt && React.createElement('span', null, `${formatDuration(elapsed)} elapsed`),
|
|
143
|
+
session.currentTask && React.createElement('span', { style: { opacity: 0.7 } }, `· ${session.currentTask}`),
|
|
144
|
+
),
|
|
145
|
+
React.createElement(Flex, { variant: 'row-center-start-nowrap-6', style: { flexShrink: 0 } },
|
|
146
|
+
React.createElement(Button, {
|
|
147
|
+
size: 'sm', variant: 'clear', onClick: handlePause, loading: pausing, disabled: pausing || stopping,
|
|
148
|
+
}, 'Pause'),
|
|
149
|
+
React.createElement(Button, {
|
|
150
|
+
size: 'sm', variant: 'clear', color: 'danger', onClick: handleStop, loading: stopping, disabled: stopping || pausing,
|
|
151
|
+
}, 'End'),
|
|
152
|
+
),
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export { SessionBar };
|
|
157
|
+
export default SessionBar;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SettingsSection — inline collapsible settings, rendered inside workspace.
|
|
3
|
+
* Starts collapsed; fetches provider/insight data on mount.
|
|
4
|
+
*/
|
|
5
|
+
const React = require('react');
|
|
6
|
+
const { useState, useEffect, useCallback } = React;
|
|
7
|
+
const { Flex, Typography, Button, CollapsibleSection, KeyValueRow, toast } = require('@fw/plugin-ui-kit');
|
|
8
|
+
|
|
9
|
+
interface SettingsSectionProps {
|
|
10
|
+
callTool: (tool: string, args?: Record<string, unknown>) => Promise<unknown>;
|
|
11
|
+
dispatchEvent: (name: string, detail?: unknown) => void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface ProviderInfo {
|
|
15
|
+
name: string;
|
|
16
|
+
source: string;
|
|
17
|
+
envVarsSet?: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface InsightsData {
|
|
21
|
+
trust?: { score: number; phase: number };
|
|
22
|
+
cost?: { last7Days: number; trend: string };
|
|
23
|
+
health?: { overall: number };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function SettingsSection({ callTool, dispatchEvent }: SettingsSectionProps) {
|
|
27
|
+
const [providers, setProviders] = useState<ProviderInfo[]>([]);
|
|
28
|
+
const [insights, setInsights] = useState<InsightsData | null>(null);
|
|
29
|
+
|
|
30
|
+
const handleClear = useCallback(() => {
|
|
31
|
+
if (!confirm('Clear all bot run history? This action cannot be undone.')) return;
|
|
32
|
+
callTool('fw_weaver_history', { clear: true }).then(() => {
|
|
33
|
+
dispatchEvent('fw:refresh-bot-workspace');
|
|
34
|
+
toast('Run history cleared', { type: 'success' });
|
|
35
|
+
}).catch(() => {
|
|
36
|
+
toast('Failed to clear history', { type: 'error' });
|
|
37
|
+
});
|
|
38
|
+
}, [callTool, dispatchEvent]);
|
|
39
|
+
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
Promise.all([
|
|
42
|
+
callTool('fw_weaver_providers') as Promise<ProviderInfo[] | null>,
|
|
43
|
+
callTool('fw_weaver_insights') as Promise<InsightsData | null>,
|
|
44
|
+
]).then(([prov, ins]) => {
|
|
45
|
+
if (prov) setProviders(Array.isArray(prov) ? prov : []);
|
|
46
|
+
if (ins) setInsights(ins);
|
|
47
|
+
});
|
|
48
|
+
}, [callTool]);
|
|
49
|
+
|
|
50
|
+
const activeProvider = providers.find((p) => p.envVarsSet) ?? providers[0];
|
|
51
|
+
const trustScore = insights?.trust?.score;
|
|
52
|
+
const trustDisplay =
|
|
53
|
+
trustScore != null
|
|
54
|
+
? (trustScore <= 1 ? (trustScore * 100).toFixed(0) : Math.round(trustScore)) + '%'
|
|
55
|
+
: '\u2014';
|
|
56
|
+
|
|
57
|
+
return React.createElement(CollapsibleSection, {
|
|
58
|
+
title: 'Settings', variant: 'list', defaultExpanded: false,
|
|
59
|
+
},
|
|
60
|
+
React.createElement(Flex, { variant: 'column-start-start-nowrap-4', style: { width: '100%' } },
|
|
61
|
+
activeProvider && React.createElement(KeyValueRow, {
|
|
62
|
+
keyName: 'Provider',
|
|
63
|
+
value: `${activeProvider.name} (${activeProvider.source})`,
|
|
64
|
+
size: 'small',
|
|
65
|
+
}),
|
|
66
|
+
insights?.trust && React.createElement(KeyValueRow, {
|
|
67
|
+
keyName: 'Trust',
|
|
68
|
+
value: `Phase ${insights.trust.phase} · ${trustDisplay}`,
|
|
69
|
+
size: 'small',
|
|
70
|
+
}),
|
|
71
|
+
insights?.health && React.createElement(KeyValueRow, {
|
|
72
|
+
keyName: 'Health',
|
|
73
|
+
value: `${insights.health.overall}/100`,
|
|
74
|
+
size: 'small',
|
|
75
|
+
}),
|
|
76
|
+
insights?.cost && React.createElement(KeyValueRow, {
|
|
77
|
+
keyName: 'Cost (7d)',
|
|
78
|
+
value: `$${insights.cost.last7Days?.toFixed(2) ?? '0.00'} · ${insights.cost.trend ?? 'stable'}`,
|
|
79
|
+
size: 'small',
|
|
80
|
+
}),
|
|
81
|
+
React.createElement(Flex, {
|
|
82
|
+
variant: 'row-center-space-between-nowrap-8',
|
|
83
|
+
style: { width: '100%', marginTop: '4px' },
|
|
84
|
+
},
|
|
85
|
+
React.createElement(Button, {
|
|
86
|
+
size: 'xs', variant: 'outlined', color: 'danger', onClick: handleClear,
|
|
87
|
+
}, 'Clear Run History'),
|
|
88
|
+
React.createElement(Typography, {
|
|
89
|
+
variant: 'smallCaption-regular', color: 'color-text-subtle',
|
|
90
|
+
}, 'Edit settings in .weaver.json'),
|
|
91
|
+
),
|
|
92
|
+
),
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export { SettingsSection };
|
|
97
|
+
export default SettingsSection;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Steer API for the workspace — sends steering commands via the pack tool API.
|
|
3
|
+
* Uses the workspace context's callTool instead of a hardcoded pack-tool URL.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export type SteerCommand = 'pause' | 'resume' | 'cancel' | 'redirect';
|
|
7
|
+
|
|
8
|
+
export async function sendSteerCommand(
|
|
9
|
+
callTool: (tool: string, args?: Record<string, unknown>) => Promise<unknown>,
|
|
10
|
+
command: SteerCommand,
|
|
11
|
+
payload?: string,
|
|
12
|
+
): Promise<void> {
|
|
13
|
+
await callTool('fw_weaver_steer', {
|
|
14
|
+
command,
|
|
15
|
+
...(payload ? { payload } : {}),
|
|
16
|
+
});
|
|
17
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
// TimelineEntry is provided by @fw/plugin-ui-kit at runtime in the sandbox.
|
|
2
|
+
// Type-only import — erased at compile time, no runtime dependency.
|
|
3
|
+
interface TimelineEntry {
|
|
4
|
+
id: string;
|
|
5
|
+
timestamp: Date;
|
|
6
|
+
type: 'task-started' | 'node-started' | 'node-completed' | 'node-failed' | 'agent-request' | 'user-action' | 'task-completed' | 'task-failed';
|
|
7
|
+
nodeId?: string;
|
|
8
|
+
label: string;
|
|
9
|
+
detail?: string;
|
|
10
|
+
outputs?: Array<{ portLabel: string; value: unknown }>;
|
|
11
|
+
duration?: number;
|
|
12
|
+
color?: string;
|
|
13
|
+
icon?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// -- Historical trace helper types --
|
|
17
|
+
|
|
18
|
+
export interface HistoricalTraceEvent {
|
|
19
|
+
type: 'node-start' | 'node-complete' | 'node-error';
|
|
20
|
+
nodeId: string;
|
|
21
|
+
nodeType?: string;
|
|
22
|
+
timestamp: number;
|
|
23
|
+
durationMs?: number;
|
|
24
|
+
error?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface HistoricalStepLog {
|
|
28
|
+
step: string;
|
|
29
|
+
status: 'ok' | 'blocked' | 'error';
|
|
30
|
+
detail?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface HistoricalPlan {
|
|
34
|
+
summary: string;
|
|
35
|
+
steps: Array<{
|
|
36
|
+
id: string;
|
|
37
|
+
operation: string;
|
|
38
|
+
description: string;
|
|
39
|
+
args?: Record<string, unknown>;
|
|
40
|
+
}>;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface HistoricalAuditEvent {
|
|
44
|
+
type: string;
|
|
45
|
+
timestamp: string;
|
|
46
|
+
runId: string;
|
|
47
|
+
data?: Record<string, unknown>;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface HistoricalCostSummary {
|
|
51
|
+
totalInputTokens: number;
|
|
52
|
+
totalOutputTokens: number;
|
|
53
|
+
totalCost: number;
|
|
54
|
+
model: string;
|
|
55
|
+
provider: string;
|
|
56
|
+
entries: Array<{ step: string; model: string; estimatedCost: number }>;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface HistoricalRun {
|
|
60
|
+
id: string;
|
|
61
|
+
botId?: string;
|
|
62
|
+
botName?: string;
|
|
63
|
+
workflowFile?: string;
|
|
64
|
+
instruction?: string;
|
|
65
|
+
outcome: string; // 'completed' | 'failed' | 'error' | 'skipped'
|
|
66
|
+
success?: boolean;
|
|
67
|
+
durationMs?: number;
|
|
68
|
+
duration?: number; // fallback
|
|
69
|
+
summary?: string;
|
|
70
|
+
outputs?: Record<string, unknown>;
|
|
71
|
+
startedAt?: string;
|
|
72
|
+
finishedAt?: string;
|
|
73
|
+
provider?: string;
|
|
74
|
+
cost?: number;
|
|
75
|
+
// Execution trace data
|
|
76
|
+
trace?: HistoricalTraceEvent[];
|
|
77
|
+
nodeMeta?: Record<string, { label?: string; color?: string; icon?: string }>;
|
|
78
|
+
stepLog?: HistoricalStepLog[];
|
|
79
|
+
plan?: HistoricalPlan;
|
|
80
|
+
costDetail?: HistoricalCostSummary;
|
|
81
|
+
auditTrail?: HistoricalAuditEvent[];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// -- Trace-to-timeline conversion --
|
|
85
|
+
|
|
86
|
+
export function traceToTimeline(run: HistoricalRun): TimelineEntry[] {
|
|
87
|
+
const entries: TimelineEntry[] = [];
|
|
88
|
+
let idCounter = 0;
|
|
89
|
+
|
|
90
|
+
const meta = run.nodeMeta ?? {};
|
|
91
|
+
|
|
92
|
+
// Prefer node-level trace events (richer, with timing); fall back to stepLog
|
|
93
|
+
if (run.trace && run.trace.length > 0) {
|
|
94
|
+
const nodeStarts = new Map<string, HistoricalTraceEvent>();
|
|
95
|
+
|
|
96
|
+
for (const event of run.trace) {
|
|
97
|
+
if (event.type === 'node-start') {
|
|
98
|
+
nodeStarts.set(event.nodeId, event);
|
|
99
|
+
} else if (event.type === 'node-complete' || event.type === 'node-error') {
|
|
100
|
+
const start = nodeStarts.get(event.nodeId);
|
|
101
|
+
const duration =
|
|
102
|
+
event.durationMs ?? (start ? event.timestamp - start.timestamp : undefined);
|
|
103
|
+
const nm = meta[event.nodeId] ?? (event.nodeType ? meta[event.nodeType] : undefined);
|
|
104
|
+
entries.push({
|
|
105
|
+
id: `trace-${idCounter++}`,
|
|
106
|
+
timestamp: new Date(event.timestamp),
|
|
107
|
+
type: event.type === 'node-complete' ? 'node-completed' : 'node-failed',
|
|
108
|
+
nodeId: event.nodeId,
|
|
109
|
+
label: nm?.label ?? event.nodeType ?? event.nodeId,
|
|
110
|
+
detail: event.error,
|
|
111
|
+
duration,
|
|
112
|
+
color: nm?.color,
|
|
113
|
+
icon: nm?.icon,
|
|
114
|
+
});
|
|
115
|
+
nodeStarts.delete(event.nodeId);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Any nodes still in nodeStarts are still running (shouldn't happen for historical)
|
|
120
|
+
for (const [nodeId, event] of nodeStarts) {
|
|
121
|
+
const nm = meta[nodeId] ?? (event.nodeType ? meta[event.nodeType] : undefined);
|
|
122
|
+
entries.push({
|
|
123
|
+
id: `trace-${idCounter++}`,
|
|
124
|
+
timestamp: new Date(event.timestamp),
|
|
125
|
+
type: 'node-started',
|
|
126
|
+
nodeId,
|
|
127
|
+
label: nm?.label ?? event.nodeType ?? nodeId,
|
|
128
|
+
color: nm?.color,
|
|
129
|
+
icon: nm?.icon,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
} else if (run.stepLog && run.stepLog.length > 0) {
|
|
133
|
+
// Fallback: use stepLog (coarser, from plan execution steps)
|
|
134
|
+
for (const step of run.stepLog) {
|
|
135
|
+
const type =
|
|
136
|
+
step.status === 'ok'
|
|
137
|
+
? 'node-completed'
|
|
138
|
+
: step.status === 'error'
|
|
139
|
+
? 'node-failed'
|
|
140
|
+
: 'node-completed'; // 'blocked' treated as completed with detail
|
|
141
|
+
entries.push({
|
|
142
|
+
id: `step-${idCounter++}`,
|
|
143
|
+
timestamp: new Date(run.startedAt ?? Date.now()),
|
|
144
|
+
type: type as TimelineEntry['type'],
|
|
145
|
+
label: step.step,
|
|
146
|
+
detail: step.detail,
|
|
147
|
+
icon: step.status === 'ok' ? 'success' : step.status === 'error' ? 'error' : 'warning',
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Append a task-level result entry for failed/completed runs
|
|
153
|
+
if (run.outcome === 'failed' || run.outcome === 'error') {
|
|
154
|
+
// Extract error details from audit trail
|
|
155
|
+
let errorDetail = '';
|
|
156
|
+
if (run.auditTrail) {
|
|
157
|
+
for (const a of run.auditTrail) {
|
|
158
|
+
if (a.type === 'step-complete' && a.data) {
|
|
159
|
+
const errors = a.data.errors as string[] | undefined;
|
|
160
|
+
if (errors && errors.length > 0) {
|
|
161
|
+
errorDetail = errors
|
|
162
|
+
.map((e: string) => (e.length > 120 ? e.slice(0, 117) + '...' : e))
|
|
163
|
+
.join('\n');
|
|
164
|
+
break;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
// Fallback to summary
|
|
170
|
+
if (!errorDetail && run.summary) {
|
|
171
|
+
const parts = run.summary.split('|').map((p: string) => p.trim());
|
|
172
|
+
const outcomePart = parts.find((p: string) => p.startsWith('Outcome:'));
|
|
173
|
+
const summaryPart = parts.find((p: string) => p.startsWith('Summary:'));
|
|
174
|
+
errorDetail = summaryPart?.replace('Summary:', '').trim() ?? outcomePart ?? run.summary;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
entries.push({
|
|
178
|
+
id: `result-${idCounter++}`,
|
|
179
|
+
timestamp: new Date(run.finishedAt ?? run.startedAt ?? Date.now()),
|
|
180
|
+
type: 'task-failed',
|
|
181
|
+
label: 'Task failed',
|
|
182
|
+
detail: errorDetail || 'Unknown failure',
|
|
183
|
+
icon: 'error',
|
|
184
|
+
});
|
|
185
|
+
} else if (run.outcome === 'completed' && run.success) {
|
|
186
|
+
entries.push({
|
|
187
|
+
id: `result-${idCounter++}`,
|
|
188
|
+
timestamp: new Date(run.finishedAt ?? run.startedAt ?? Date.now()),
|
|
189
|
+
type: 'task-completed',
|
|
190
|
+
label: 'Task completed',
|
|
191
|
+
detail: run.summary?.includes('|')
|
|
192
|
+
? run.summary
|
|
193
|
+
.split('|')
|
|
194
|
+
.find((p: string) => p.trim().startsWith('Summary:'))
|
|
195
|
+
?.replace('Summary:', '')
|
|
196
|
+
.trim()
|
|
197
|
+
: undefined,
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Sort by timestamp
|
|
202
|
+
entries.sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
|
|
203
|
+
return entries;
|
|
204
|
+
}
|