@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,406 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Weaver Bot Workspace — center dock execution workspace.
|
|
3
|
+
*
|
|
4
|
+
* Pack-contributed component loaded via botUI.workspace manifest slot.
|
|
5
|
+
* Receives PackWorkspaceContext from platform — uses it for all tool calls,
|
|
6
|
+
* event streaming, and window management. Zero platform imports.
|
|
7
|
+
*
|
|
8
|
+
* Single chronological timeline (oldest → newest, like a conversation):
|
|
9
|
+
* - Completed runs at the top
|
|
10
|
+
* - Live execution streaming in the middle
|
|
11
|
+
* - Queued tasks at the bottom
|
|
12
|
+
* - Genesis cycles interleaved with history
|
|
13
|
+
*
|
|
14
|
+
* Layout:
|
|
15
|
+
* - Session bar (top)
|
|
16
|
+
* - Settings (collapsed)
|
|
17
|
+
* - Scrollable timeline (main area)
|
|
18
|
+
* - Input bar (pinned bottom)
|
|
19
|
+
*/
|
|
20
|
+
const React = require('react');
|
|
21
|
+
const { useRef, useEffect, useState, useCallback, useMemo } = React;
|
|
22
|
+
const {
|
|
23
|
+
Flex, ScrollArea, EmptyState, TaskBlock, toast, usePackWorkspace, useEventStream,
|
|
24
|
+
} = require('@fw/plugin-ui-kit');
|
|
25
|
+
|
|
26
|
+
// Local pack-specific components and utilities (bundled by esbuild)
|
|
27
|
+
import { SessionBar } from './session-bar';
|
|
28
|
+
import { SettingsSection } from './settings-section';
|
|
29
|
+
import { QueueInput } from './queue-input';
|
|
30
|
+
import { GenesisBlock } from './genesis-block';
|
|
31
|
+
import { ApprovalCard } from './approval-card';
|
|
32
|
+
import { useStreamTimeline } from './use-stream-timeline';
|
|
33
|
+
import { traceToTimeline } from './trace-to-timeline';
|
|
34
|
+
import { sendSteerCommand } from './steer-api';
|
|
35
|
+
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
// Types
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
|
|
40
|
+
interface QueuedTask {
|
|
41
|
+
id: string;
|
|
42
|
+
instruction: string;
|
|
43
|
+
status: string;
|
|
44
|
+
priority: number;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
interface GenesisCycleData {
|
|
48
|
+
id: string;
|
|
49
|
+
timestamp: string;
|
|
50
|
+
durationMs: number;
|
|
51
|
+
proposal: Record<string, unknown> | null;
|
|
52
|
+
outcome: string;
|
|
53
|
+
diffSummary: string | null;
|
|
54
|
+
error: string | null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
interface HistoricalRun {
|
|
58
|
+
id: string;
|
|
59
|
+
outcome: string;
|
|
60
|
+
success?: boolean;
|
|
61
|
+
summary?: string;
|
|
62
|
+
startedAt?: string;
|
|
63
|
+
durationMs?: number;
|
|
64
|
+
duration?: number;
|
|
65
|
+
cost?: number;
|
|
66
|
+
costDetail?: Record<string, unknown>;
|
|
67
|
+
workflowFile?: string;
|
|
68
|
+
plan?: Record<string, unknown>;
|
|
69
|
+
trace?: Array<Record<string, unknown>>;
|
|
70
|
+
nodeMeta?: Record<string, Record<string, unknown>>;
|
|
71
|
+
auditTrail?: Array<Record<string, unknown>>;
|
|
72
|
+
params?: Record<string, unknown>;
|
|
73
|
+
[key: string]: unknown;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
interface TimelineItem {
|
|
77
|
+
kind: 'run' | 'genesis';
|
|
78
|
+
timestamp: number;
|
|
79
|
+
run?: HistoricalRun;
|
|
80
|
+
runTimeline?: Array<Record<string, unknown>>;
|
|
81
|
+
genesis?: GenesisCycleData;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
// Component
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
|
|
88
|
+
function BotWorkspace() {
|
|
89
|
+
const ctx = usePackWorkspace();
|
|
90
|
+
const { callTool, windowData, dispatchEvent, onRefresh } = ctx;
|
|
91
|
+
const packId = ctx.packId;
|
|
92
|
+
|
|
93
|
+
const isLive = windowData?.live === true && !!windowData?.runId;
|
|
94
|
+
const highlightRunId = windowData?.runId as string | undefined;
|
|
95
|
+
|
|
96
|
+
// ── Live execution (SSE stream) ──────────────────────────────
|
|
97
|
+
const stream = useEventStream();
|
|
98
|
+
const {
|
|
99
|
+
timeline: liveTimeline,
|
|
100
|
+
phase: livePhase,
|
|
101
|
+
instruction: liveInstruction,
|
|
102
|
+
elapsed,
|
|
103
|
+
cost,
|
|
104
|
+
plan,
|
|
105
|
+
awaitingApproval,
|
|
106
|
+
} = useStreamTimeline(stream.events, stream.isDone);
|
|
107
|
+
|
|
108
|
+
// Start SSE stream for direct live run (from AI chat)
|
|
109
|
+
useEffect(() => {
|
|
110
|
+
if (!isLive || !windowData?.runId) return;
|
|
111
|
+
stream.start(packId, 'fw_weaver_events', windowData.runId as string);
|
|
112
|
+
return () => stream.stop();
|
|
113
|
+
}, [isLive, packId, windowData?.runId]);
|
|
114
|
+
|
|
115
|
+
// Auto-detect active session task
|
|
116
|
+
const [sessionRunId, setSessionRunId] = useState<string | null>(null);
|
|
117
|
+
|
|
118
|
+
useEffect(() => {
|
|
119
|
+
if (isLive) return;
|
|
120
|
+
const poll = async () => {
|
|
121
|
+
try {
|
|
122
|
+
const status = (await callTool('fw_weaver_status')) as Record<string, unknown> | null;
|
|
123
|
+
const runId = (status?.currentRunId as string) ?? null;
|
|
124
|
+
const alive = status?.alive as boolean | null;
|
|
125
|
+
if (runId && alive === false) {
|
|
126
|
+
setSessionRunId(null);
|
|
127
|
+
} else {
|
|
128
|
+
setSessionRunId(runId);
|
|
129
|
+
}
|
|
130
|
+
} catch { /* non-fatal */ }
|
|
131
|
+
};
|
|
132
|
+
poll();
|
|
133
|
+
const interval = setInterval(poll, 3_000);
|
|
134
|
+
return () => clearInterval(interval);
|
|
135
|
+
}, [isLive, callTool]);
|
|
136
|
+
|
|
137
|
+
// Start SSE for session's current task
|
|
138
|
+
useEffect(() => {
|
|
139
|
+
if (!sessionRunId || isLive) return;
|
|
140
|
+
stream.start(packId, 'fw_weaver_events', sessionRunId);
|
|
141
|
+
return () => stream.stop();
|
|
142
|
+
}, [sessionRunId, packId, isLive]);
|
|
143
|
+
|
|
144
|
+
// Auto-scroll
|
|
145
|
+
const scrollRef = useRef<HTMLDivElement>(null);
|
|
146
|
+
useEffect(() => {
|
|
147
|
+
const el = scrollRef.current;
|
|
148
|
+
if (el) el.scrollTop = el.scrollHeight;
|
|
149
|
+
}, [liveTimeline.length, stream.events.length]);
|
|
150
|
+
|
|
151
|
+
// Steer controls
|
|
152
|
+
const [pausing, setPausing] = useState(false);
|
|
153
|
+
const [stopping, setStopping] = useState(false);
|
|
154
|
+
|
|
155
|
+
const handlePause = useCallback(async () => {
|
|
156
|
+
setPausing(true);
|
|
157
|
+
try {
|
|
158
|
+
await sendSteerCommand(callTool, 'pause');
|
|
159
|
+
toast('Pause signal sent', { type: 'info' });
|
|
160
|
+
} catch (err: unknown) {
|
|
161
|
+
toast(err instanceof Error ? err.message : 'Failed to pause', { type: 'error' });
|
|
162
|
+
}
|
|
163
|
+
setPausing(false);
|
|
164
|
+
}, [callTool]);
|
|
165
|
+
|
|
166
|
+
const handleStop = useCallback(async () => {
|
|
167
|
+
setStopping(true);
|
|
168
|
+
try {
|
|
169
|
+
await sendSteerCommand(callTool, 'cancel');
|
|
170
|
+
toast('Stopping — will take effect after current node completes', { type: 'info' });
|
|
171
|
+
} catch (err: unknown) {
|
|
172
|
+
toast(err instanceof Error ? err.message : 'Failed to stop', { type: 'error' });
|
|
173
|
+
setStopping(false);
|
|
174
|
+
}
|
|
175
|
+
}, [callTool]);
|
|
176
|
+
|
|
177
|
+
// ── History + Genesis + Queue ────────────────────────────────
|
|
178
|
+
const [history, setHistory] = useState<HistoricalRun[]>([]);
|
|
179
|
+
const [genesisCycles, setGenesisCycles] = useState<GenesisCycleData[]>([]);
|
|
180
|
+
const [queuedTasks, setQueuedTasks] = useState<QueuedTask[]>([]);
|
|
181
|
+
const [removingIds, setRemovingIds] = useState<Set<string>>(new Set());
|
|
182
|
+
const [expandedRunId, setExpandedRunId] = useState<string | null>(
|
|
183
|
+
highlightRunId && !isLive ? highlightRunId : null,
|
|
184
|
+
);
|
|
185
|
+
const [liveExpanded, setLiveExpanded] = useState(true);
|
|
186
|
+
|
|
187
|
+
const refreshData = useCallback(() => {
|
|
188
|
+
callTool('fw_weaver_history', { limit: 20 }).then((data: unknown) => {
|
|
189
|
+
if (Array.isArray(data)) {
|
|
190
|
+
setHistory(
|
|
191
|
+
data.map((r: Record<string, unknown>) => {
|
|
192
|
+
const costObj = r.cost && typeof r.cost === 'object' ? (r.cost as Record<string, unknown>) : undefined;
|
|
193
|
+
return { ...r, costDetail: costObj, cost: costObj?.totalCost } as unknown as HistoricalRun;
|
|
194
|
+
}),
|
|
195
|
+
);
|
|
196
|
+
} else {
|
|
197
|
+
setHistory([]);
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
callTool('fw_weaver_insights').then((data: unknown) => {
|
|
201
|
+
const d = data as Record<string, unknown> | null;
|
|
202
|
+
if (d?.evolution) {
|
|
203
|
+
const evo = d.evolution as Record<string, unknown>;
|
|
204
|
+
if (Array.isArray(evo.recentCycles)) {
|
|
205
|
+
setGenesisCycles(evo.recentCycles.slice(0, 10) as GenesisCycleData[]);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
callTool('fw_weaver_queue', { action: 'list' }).then((data: unknown) => {
|
|
210
|
+
const visible = (Array.isArray(data) ? (data as QueuedTask[]) : []).filter(
|
|
211
|
+
(t: QueuedTask) => t.status === 'pending' || t.status === 'running',
|
|
212
|
+
);
|
|
213
|
+
setQueuedTasks(visible);
|
|
214
|
+
});
|
|
215
|
+
}, [callTool]);
|
|
216
|
+
|
|
217
|
+
useEffect(() => { refreshData(); }, [refreshData]);
|
|
218
|
+
|
|
219
|
+
// Poll queue every 10s
|
|
220
|
+
useEffect(() => {
|
|
221
|
+
const interval = setInterval(() => {
|
|
222
|
+
callTool('fw_weaver_queue', { action: 'list' }).then((data: unknown) => {
|
|
223
|
+
const visible = (Array.isArray(data) ? (data as QueuedTask[]) : []).filter(
|
|
224
|
+
(t: QueuedTask) => t.status === 'pending' || t.status === 'running',
|
|
225
|
+
);
|
|
226
|
+
setQueuedTasks(visible);
|
|
227
|
+
});
|
|
228
|
+
}, 10_000);
|
|
229
|
+
return () => clearInterval(interval);
|
|
230
|
+
}, [callTool]);
|
|
231
|
+
|
|
232
|
+
// Listen for refresh events
|
|
233
|
+
useEffect(() => {
|
|
234
|
+
return onRefresh(() => refreshData());
|
|
235
|
+
}, [refreshData, onRefresh]);
|
|
236
|
+
|
|
237
|
+
// Refresh when session task changes
|
|
238
|
+
useEffect(() => {
|
|
239
|
+
if (sessionRunId !== null) refreshData();
|
|
240
|
+
}, [sessionRunId, refreshData]);
|
|
241
|
+
|
|
242
|
+
const handleRemoveTask = useCallback(async (id: string) => {
|
|
243
|
+
setRemovingIds((prev: Set<string>) => new Set(prev).add(id));
|
|
244
|
+
try {
|
|
245
|
+
await callTool('fw_weaver_queue', { action: 'remove', id });
|
|
246
|
+
refreshData();
|
|
247
|
+
toast('Task removed', { type: 'warning' });
|
|
248
|
+
} catch (err: unknown) {
|
|
249
|
+
toast(err instanceof Error ? err.message : 'Failed to remove task', { type: 'error' });
|
|
250
|
+
}
|
|
251
|
+
setRemovingIds((prev: Set<string>) => {
|
|
252
|
+
const next = new Set(prev);
|
|
253
|
+
next.delete(id);
|
|
254
|
+
return next;
|
|
255
|
+
});
|
|
256
|
+
}, [callTool, refreshData]);
|
|
257
|
+
|
|
258
|
+
// Build unified timeline
|
|
259
|
+
const timelineItems = useMemo(() => {
|
|
260
|
+
const items: TimelineItem[] = [];
|
|
261
|
+
for (const run of history) {
|
|
262
|
+
items.push({ kind: 'run', timestamp: new Date(run.startedAt ?? 0).getTime(), run, runTimeline: traceToTimeline(run) });
|
|
263
|
+
}
|
|
264
|
+
for (const cycle of genesisCycles) {
|
|
265
|
+
items.push({ kind: 'genesis', timestamp: new Date(cycle.timestamp).getTime(), genesis: cycle });
|
|
266
|
+
}
|
|
267
|
+
items.sort((a: TimelineItem, b: TimelineItem) => a.timestamp - b.timestamp);
|
|
268
|
+
return items;
|
|
269
|
+
}, [history, genesisCycles]);
|
|
270
|
+
|
|
271
|
+
const toggleExpand = useCallback((id: string) => {
|
|
272
|
+
setExpandedRunId((prev: string | null) => (prev === id ? null : id));
|
|
273
|
+
}, []);
|
|
274
|
+
|
|
275
|
+
// ── Render ────────────────────────────────────────────────────
|
|
276
|
+
|
|
277
|
+
const isStreaming = isLive || !!sessionRunId;
|
|
278
|
+
const hasContent = isStreaming || timelineItems.length > 0 || queuedTasks.length > 0;
|
|
279
|
+
|
|
280
|
+
// Helper to extract instruction text from a run
|
|
281
|
+
function extractInstruction(run: HistoricalRun): string {
|
|
282
|
+
let text = '';
|
|
283
|
+
if (run.params) {
|
|
284
|
+
try {
|
|
285
|
+
const taskJson = (run.params as Record<string, unknown>).taskJson as string;
|
|
286
|
+
if (taskJson) {
|
|
287
|
+
const task = JSON.parse(taskJson) as Record<string, unknown>;
|
|
288
|
+
if (task.instruction) text = task.instruction as string;
|
|
289
|
+
}
|
|
290
|
+
} catch { /* not JSON */ }
|
|
291
|
+
}
|
|
292
|
+
if (!text && run.summary) {
|
|
293
|
+
const summary = run.summary;
|
|
294
|
+
if (summary.includes('|')) {
|
|
295
|
+
const parts = summary.split('|').map((p: string) => p.trim());
|
|
296
|
+
const taskPart = parts.find((p: string) => p.startsWith('Task:'));
|
|
297
|
+
const summaryPart = parts.find((p: string) => p.startsWith('Summary:'));
|
|
298
|
+
text = taskPart?.replace('Task:', '').trim() ?? summaryPart?.replace('Summary:', '').trim() ?? parts[0] ?? '';
|
|
299
|
+
} else {
|
|
300
|
+
text = summary;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
if (!text) {
|
|
304
|
+
const wf = run.workflowFile;
|
|
305
|
+
if (wf) {
|
|
306
|
+
const parts = wf.split('/');
|
|
307
|
+
text = parts[parts.length - 1] ?? 'Bot run';
|
|
308
|
+
} else {
|
|
309
|
+
text = 'Bot run';
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
if (text.length > 120) text = text.slice(0, 117) + '...';
|
|
313
|
+
return text;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Approval slot for running TaskBlock
|
|
317
|
+
const approvalSlot = awaitingApproval && plan
|
|
318
|
+
? React.createElement(ApprovalCard, { plan, callTool })
|
|
319
|
+
: null;
|
|
320
|
+
|
|
321
|
+
return React.createElement(Flex, {
|
|
322
|
+
variant: 'column-stretch-start-nowrap-0',
|
|
323
|
+
style: { width: '100%', height: '100%', overflow: 'hidden' },
|
|
324
|
+
},
|
|
325
|
+
// Session bar
|
|
326
|
+
React.createElement(SessionBar, { callTool, dispatchEvent }),
|
|
327
|
+
|
|
328
|
+
// Settings
|
|
329
|
+
React.createElement(SettingsSection, { callTool, dispatchEvent }),
|
|
330
|
+
|
|
331
|
+
// Main scrollable timeline
|
|
332
|
+
React.createElement(Flex, { variant: 'column-stretch-start-nowrap-0', style: { flex: 1, minHeight: 0 } },
|
|
333
|
+
React.createElement(ScrollArea, { ref: scrollRef },
|
|
334
|
+
React.createElement(Flex, { variant: 'column-stretch-start-nowrap-8', style: { padding: '12px 16px' } },
|
|
335
|
+
// Empty state
|
|
336
|
+
!hasContent && React.createElement(EmptyState, {
|
|
337
|
+
icon: 'smartToy', message: 'No bot runs yet',
|
|
338
|
+
description: 'Ask the AI assistant to run a task, or add tasks below.',
|
|
339
|
+
}),
|
|
340
|
+
|
|
341
|
+
// History + Genesis
|
|
342
|
+
...timelineItems.map((item: TimelineItem) => {
|
|
343
|
+
if (item.kind === 'genesis' && item.genesis) {
|
|
344
|
+
return React.createElement(GenesisBlock, {
|
|
345
|
+
key: `genesis-${item.genesis.id}`, cycle: item.genesis, callTool,
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
if (item.kind === 'run' && item.run) {
|
|
349
|
+
const run = item.run;
|
|
350
|
+
const runId = run.id;
|
|
351
|
+
const isExpanded = expandedRunId === runId;
|
|
352
|
+
const isSuccess = run.outcome === 'completed' || run.success === true;
|
|
353
|
+
return React.createElement(TaskBlock, {
|
|
354
|
+
key: `run-${runId}`,
|
|
355
|
+
state: isSuccess ? 'completed' : 'failed',
|
|
356
|
+
instruction: extractInstruction(run),
|
|
357
|
+
timeline: item.runTimeline ?? [],
|
|
358
|
+
cost: typeof run.cost === 'number' ? run.cost : ((run.costDetail?.totalCost as number) ?? null),
|
|
359
|
+
plan: run.plan,
|
|
360
|
+
startedAt: run.startedAt,
|
|
361
|
+
durationMs: run.durationMs ?? run.duration,
|
|
362
|
+
expanded: isExpanded,
|
|
363
|
+
onToggleExpand: () => toggleExpand(runId),
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
return null;
|
|
367
|
+
}),
|
|
368
|
+
|
|
369
|
+
// Live execution
|
|
370
|
+
isStreaming && React.createElement(TaskBlock, {
|
|
371
|
+
state: 'running',
|
|
372
|
+
instruction: liveInstruction ?? (windowData?.instruction as string) ?? 'Running...',
|
|
373
|
+
timeline: liveTimeline,
|
|
374
|
+
phase: livePhase,
|
|
375
|
+
elapsed,
|
|
376
|
+
cost,
|
|
377
|
+
plan,
|
|
378
|
+
error: stream.error,
|
|
379
|
+
approvalSlot,
|
|
380
|
+
onPause: handlePause,
|
|
381
|
+
onStop: handleStop,
|
|
382
|
+
pauseLoading: pausing,
|
|
383
|
+
stopLoading: stopping,
|
|
384
|
+
expanded: liveExpanded,
|
|
385
|
+
onToggleExpand: () => setLiveExpanded((v: boolean) => !v),
|
|
386
|
+
}),
|
|
387
|
+
|
|
388
|
+
// Queued tasks
|
|
389
|
+
...(isStreaming ? queuedTasks.filter((t: QueuedTask) => t.status !== 'running') : queuedTasks)
|
|
390
|
+
.map((task: QueuedTask) =>
|
|
391
|
+
React.createElement(TaskBlock, {
|
|
392
|
+
key: task.id, state: 'pending', instruction: task.instruction,
|
|
393
|
+
onRemove: () => handleRemoveTask(task.id),
|
|
394
|
+
removeLoading: removingIds.has(task.id),
|
|
395
|
+
}),
|
|
396
|
+
),
|
|
397
|
+
),
|
|
398
|
+
),
|
|
399
|
+
),
|
|
400
|
+
|
|
401
|
+
// Input bar
|
|
402
|
+
React.createElement(QueueInput, { callTool, onTaskAdded: refreshData }),
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
module.exports = BotWorkspace;
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GenesisBlock — displays a genesis self-evolution cycle in the timeline.
|
|
3
|
+
* Collapsible: shows summary when collapsed, full proposal + diff when expanded.
|
|
4
|
+
*/
|
|
5
|
+
const React = require('react');
|
|
6
|
+
const { useState, useCallback } = React;
|
|
7
|
+
const { CollapsibleBlock, Flex, Typography, Icon, Badge, Tag, Button, toast, formatTimestamp } = require('@fw/plugin-ui-kit');
|
|
8
|
+
|
|
9
|
+
interface GenesisOperation {
|
|
10
|
+
type: string;
|
|
11
|
+
args?: Record<string, unknown>;
|
|
12
|
+
costUnits: number;
|
|
13
|
+
rationale: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface GenesisProposal {
|
|
17
|
+
operations: GenesisOperation[];
|
|
18
|
+
totalCost: number;
|
|
19
|
+
impactLevel: string;
|
|
20
|
+
summary: string;
|
|
21
|
+
rationale: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface GenesisCycleData {
|
|
25
|
+
id: string;
|
|
26
|
+
timestamp: string;
|
|
27
|
+
durationMs: number;
|
|
28
|
+
proposal: GenesisProposal | null;
|
|
29
|
+
outcome: string;
|
|
30
|
+
diffSummary: string | null;
|
|
31
|
+
error: string | null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface GenesisBlockProps {
|
|
35
|
+
cycle: GenesisCycleData;
|
|
36
|
+
callTool: (tool: string, args?: Record<string, unknown>) => Promise<unknown>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const OUTCOME_BADGE_VARIANTS: Record<string, string> = {
|
|
40
|
+
applied: 'success',
|
|
41
|
+
'rolled-back': 'error',
|
|
42
|
+
rejected: 'warning',
|
|
43
|
+
stabilized: 'success',
|
|
44
|
+
'no-change': 'default',
|
|
45
|
+
error: 'error',
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const OUTCOME_STATUS: Record<string, string> = {
|
|
49
|
+
applied: 'completed',
|
|
50
|
+
stabilized: 'completed',
|
|
51
|
+
'rolled-back': 'error',
|
|
52
|
+
rejected: 'error',
|
|
53
|
+
error: 'error',
|
|
54
|
+
'no-change': 'completed',
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
function GenesisBlock({ cycle, callTool }: GenesisBlockProps) {
|
|
58
|
+
const [expanded, setExpanded] = useState(false);
|
|
59
|
+
const [rollingBack, setRollingBack] = useState(false);
|
|
60
|
+
|
|
61
|
+
const handleRollback = useCallback(async () => {
|
|
62
|
+
setRollingBack(true);
|
|
63
|
+
try {
|
|
64
|
+
await callTool('fw_weaver_genesis_rollback', { cycleId: cycle.id });
|
|
65
|
+
toast('Genesis cycle rolled back', { type: 'success' });
|
|
66
|
+
} catch (err: unknown) {
|
|
67
|
+
toast(err instanceof Error ? err.message : 'Failed to rollback', { type: 'error' });
|
|
68
|
+
}
|
|
69
|
+
setRollingBack(false);
|
|
70
|
+
}, [callTool, cycle.id]);
|
|
71
|
+
|
|
72
|
+
const summary = cycle.proposal?.summary ?? `Genesis cycle ${cycle.id.slice(0, 8)}`;
|
|
73
|
+
const badgeVariant = OUTCOME_BADGE_VARIANTS[cycle.outcome] ?? 'default';
|
|
74
|
+
const status = OUTCOME_STATUS[cycle.outcome] ?? 'pending';
|
|
75
|
+
|
|
76
|
+
return React.createElement(CollapsibleBlock, {
|
|
77
|
+
status,
|
|
78
|
+
icon: React.createElement(Icon, { name: 'autorenew', size: 14, color: 'color-brand-main' }),
|
|
79
|
+
label: React.createElement(Typography, {
|
|
80
|
+
variant: 'caption-thick', color: 'color-text-high',
|
|
81
|
+
style: { overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', minWidth: 0 },
|
|
82
|
+
}, summary),
|
|
83
|
+
headerSuffix: React.createElement(Flex, { variant: 'row-center-start-nowrap-8' },
|
|
84
|
+
React.createElement(Typography, { variant: 'smallCaption-regular', color: 'color-text-subtle' }, formatTimestamp(cycle.timestamp)),
|
|
85
|
+
React.createElement(Badge, { variant: badgeVariant }, cycle.outcome),
|
|
86
|
+
),
|
|
87
|
+
expanded,
|
|
88
|
+
onToggle: () => setExpanded((v: boolean) => !v),
|
|
89
|
+
},
|
|
90
|
+
// Proposal
|
|
91
|
+
cycle.proposal && React.createElement(Flex, {
|
|
92
|
+
variant: 'column-start-start-nowrap-6',
|
|
93
|
+
style: { padding: '10px 12px', borderBottom: '1px solid var(--color-border-default)' },
|
|
94
|
+
},
|
|
95
|
+
React.createElement(Typography, { variant: 'caption-bold', color: 'color-text-subtle' },
|
|
96
|
+
`Proposal · ${cycle.proposal.impactLevel}`),
|
|
97
|
+
React.createElement(Typography, { variant: 'smallCaption-regular', color: 'color-text-medium' },
|
|
98
|
+
cycle.proposal.rationale),
|
|
99
|
+
...cycle.proposal.operations.map((op: GenesisOperation, i: number) =>
|
|
100
|
+
React.createElement(Flex, { key: i, variant: 'row-center-start-nowrap-6', style: { fontSize: '11px' } },
|
|
101
|
+
React.createElement(Tag, { size: 'small', color: 'info' }, op.type),
|
|
102
|
+
React.createElement(Typography, { variant: 'smallCaption-regular', color: 'color-text-medium' }, op.rationale),
|
|
103
|
+
),
|
|
104
|
+
),
|
|
105
|
+
),
|
|
106
|
+
// Diff
|
|
107
|
+
cycle.diffSummary && React.createElement(Flex, {
|
|
108
|
+
variant: 'column-start-start-nowrap-6',
|
|
109
|
+
style: { padding: '10px 12px', borderBottom: '1px solid var(--color-border-default)' },
|
|
110
|
+
},
|
|
111
|
+
React.createElement(Typography, { variant: 'caption-bold', color: 'color-text-subtle' }, 'Changes'),
|
|
112
|
+
React.createElement('pre', {
|
|
113
|
+
style: {
|
|
114
|
+
fontSize: '11px', fontFamily: 'monospace', lineHeight: 1.4, padding: '8px',
|
|
115
|
+
borderRadius: 'var(--border-radius-compact, 4px)', backgroundColor: 'var(--color-surface-raised)',
|
|
116
|
+
color: 'var(--color-text-medium)', whiteSpace: 'pre-wrap', wordBreak: 'break-word',
|
|
117
|
+
maxHeight: '200px', overflow: 'auto', margin: 0, width: '100%',
|
|
118
|
+
},
|
|
119
|
+
}, cycle.diffSummary),
|
|
120
|
+
),
|
|
121
|
+
// Error
|
|
122
|
+
cycle.error && React.createElement('div', {
|
|
123
|
+
style: { padding: '10px 12px', borderBottom: '1px solid var(--color-border-default)' },
|
|
124
|
+
}, React.createElement(Typography, { variant: 'smallCaption-regular', color: 'color-status-negative' }, cycle.error)),
|
|
125
|
+
// Rollback action
|
|
126
|
+
cycle.outcome === 'applied' && React.createElement(Flex, {
|
|
127
|
+
variant: 'row-center-start-nowrap-8', style: { padding: '10px 12px' },
|
|
128
|
+
},
|
|
129
|
+
React.createElement(Button, {
|
|
130
|
+
size: 'xs', variant: 'outlined', color: 'danger',
|
|
131
|
+
onClick: handleRollback, loading: rollingBack, disabled: rollingBack,
|
|
132
|
+
}, 'Rollback'),
|
|
133
|
+
),
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export { GenesisBlock };
|
|
138
|
+
export default GenesisBlock;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QueueInput — pinned input bar for adding tasks to the queue.
|
|
3
|
+
* Rendered at the very bottom of the workspace, outside the scroll area.
|
|
4
|
+
*/
|
|
5
|
+
const React = require('react');
|
|
6
|
+
const { useState, useCallback } = React;
|
|
7
|
+
const { Flex, IconButton, Input, toast } = require('@fw/plugin-ui-kit');
|
|
8
|
+
|
|
9
|
+
interface QueueInputProps {
|
|
10
|
+
callTool: (tool: string, args?: Record<string, unknown>) => Promise<unknown>;
|
|
11
|
+
onTaskAdded?: () => void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function QueueInput({ callTool, onTaskAdded }: QueueInputProps) {
|
|
15
|
+
const [newTask, setNewTask] = useState('');
|
|
16
|
+
const [adding, setAdding] = useState(false);
|
|
17
|
+
|
|
18
|
+
const handleAdd = useCallback(async () => {
|
|
19
|
+
const instruction = newTask.trim();
|
|
20
|
+
if (!instruction) return;
|
|
21
|
+
setAdding(true);
|
|
22
|
+
try {
|
|
23
|
+
await callTool('fw_weaver_queue', { action: 'add', task: instruction });
|
|
24
|
+
setNewTask('');
|
|
25
|
+
onTaskAdded?.();
|
|
26
|
+
toast('Task added to queue', { type: 'success' });
|
|
27
|
+
} catch (err: unknown) {
|
|
28
|
+
toast(err instanceof Error ? err.message : 'Failed to add task', { type: 'error' });
|
|
29
|
+
}
|
|
30
|
+
setAdding(false);
|
|
31
|
+
}, [callTool, newTask, onTaskAdded]);
|
|
32
|
+
|
|
33
|
+
return React.createElement(Flex, {
|
|
34
|
+
variant: 'row-center-start-nowrap-6',
|
|
35
|
+
style: {
|
|
36
|
+
flexShrink: 0, padding: '8px 16px',
|
|
37
|
+
borderTop: '1px solid var(--color-border-default)',
|
|
38
|
+
width: '100%', boxSizing: 'border-box',
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
React.createElement(Input, {
|
|
42
|
+
type: 'text', size: 'medium', placeholder: 'Add a task...',
|
|
43
|
+
value: newTask, onChange: setNewTask, onEnter: handleAdd, disabled: adding,
|
|
44
|
+
defaultBoxStyle: { flex: 1, minWidth: 0 },
|
|
45
|
+
inputBoxStyle: { maxWidth: 'none' },
|
|
46
|
+
}),
|
|
47
|
+
React.createElement(IconButton, {
|
|
48
|
+
icon: 'add', size: 'md', variant: 'fill', color: 'primary',
|
|
49
|
+
'aria-label': 'Add task', onClick: handleAdd, loading: adding,
|
|
50
|
+
disabled: adding || !newTask.trim(),
|
|
51
|
+
}),
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export { QueueInput };
|
|
56
|
+
export default QueueInput;
|