@synergenius/flow-weaver-pack-weaver 0.9.201 → 0.9.203
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/bot/preflight.d.ts.map +1 -1
- package/dist/bot/preflight.js +26 -0
- package/dist/bot/preflight.js.map +1 -1
- package/dist/bot/task-create-handler.d.ts +9 -0
- package/dist/bot/task-create-handler.d.ts.map +1 -1
- package/dist/bot/task-create-handler.js +26 -0
- package/dist/bot/task-create-handler.js.map +1 -1
- package/dist/node-types/agent-execute.d.ts.map +1 -1
- package/dist/node-types/agent-execute.js +26 -9
- package/dist/node-types/agent-execute.js.map +1 -1
- package/dist/node-types/plan-task.d.ts.map +1 -1
- package/dist/node-types/plan-task.js +28 -2
- package/dist/node-types/plan-task.js.map +1 -1
- package/dist/ui/bot-slot-card.js +10 -0
- package/dist/ui/budget-bar.js +5 -3
- package/dist/ui/budget-strip.js +156 -0
- package/dist/ui/chat-task-result.js +22 -27
- package/dist/ui/instance-stream-view.js +36 -0
- package/dist/ui/swarm-dashboard.js +1596 -1654
- package/dist/ui/task-detail-view.js +973 -485
- package/dist/ui/task-editor.js +32 -34
- package/dist/ui/task-pool-list.js +11 -3
- package/flowweaver.manifest.json +1 -1
- package/package.json +3 -2
- package/src/bot/preflight.ts +26 -0
- package/src/bot/task-create-handler.ts +39 -0
- package/src/node-types/agent-execute.ts +27 -10
- package/src/node-types/plan-task.ts +25 -2
- package/src/ui/bot-slot-card.tsx +23 -0
- package/src/ui/budget-bar.tsx +13 -5
- package/src/ui/budget-strip.tsx +199 -0
- package/src/ui/chat-task-result.tsx +5 -25
- package/src/ui/instance-stream-view.tsx +50 -1
- package/src/ui/swarm-dashboard.tsx +89 -84
- package/src/ui/task-detail-view.tsx +376 -442
- package/src/ui/task-editor.tsx +65 -96
- package/src/ui/task-pool-list.tsx +3 -12
- package/src/ui/task-status.ts +60 -0
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BudgetStrip — compact single-line budget display with sparkline.
|
|
3
|
+
*
|
|
4
|
+
* Shows: cost, mini sparkline bars (per-task spend), token count, task tally.
|
|
5
|
+
* Click pencil icon → popover to edit token/cost limits.
|
|
6
|
+
*
|
|
7
|
+
* Replaces the old BudgetBar (two stacked progress bars) with a denser,
|
|
8
|
+
* more informative single-strip view.
|
|
9
|
+
*/
|
|
10
|
+
import React, { useState, useCallback, useRef } from 'react';
|
|
11
|
+
import { Flex, Typography, Button, Input, StatusChip } from '@fw/plugin-ui-kit';
|
|
12
|
+
import { styled } from '@fw/plugin-theme';
|
|
13
|
+
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// Types
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
interface BudgetStripProps {
|
|
19
|
+
/** Current cost spent */
|
|
20
|
+
costUsed: number;
|
|
21
|
+
/** Cost budget limit */
|
|
22
|
+
costLimit: number;
|
|
23
|
+
/** Source of cost data */
|
|
24
|
+
costSource?: 'cli' | 'estimated';
|
|
25
|
+
/** Current tokens used */
|
|
26
|
+
tokensUsed: number;
|
|
27
|
+
/** Token budget limit */
|
|
28
|
+
tokenLimit: number;
|
|
29
|
+
/** Task counts */
|
|
30
|
+
tasksDone?: number;
|
|
31
|
+
tasksFailed?: number;
|
|
32
|
+
tasksOpen?: number;
|
|
33
|
+
/** Called when limits are changed. If not provided, limits are read-only. */
|
|
34
|
+
onLimitsChange?: (costLimit: number, tokenLimit: number) => void;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
// Styled
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
|
|
41
|
+
const Strip = styled.div({
|
|
42
|
+
display: 'flex',
|
|
43
|
+
alignItems: 'center',
|
|
44
|
+
gap: '$gap-cluster',
|
|
45
|
+
padding: '$gap-list-tight $gap-card-content',
|
|
46
|
+
borderRadius: '$border-radius-container',
|
|
47
|
+
backgroundColor: '$color-surface-float',
|
|
48
|
+
border: '1px solid $color-border-subtle',
|
|
49
|
+
minHeight: '34px',
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
const PopoverOverlay = styled.div({
|
|
54
|
+
position: 'fixed',
|
|
55
|
+
inset: 0,
|
|
56
|
+
zIndex: 999,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const PopoverCard = styled.div({
|
|
60
|
+
position: 'absolute',
|
|
61
|
+
zIndex: 1000,
|
|
62
|
+
backgroundColor: '$color-surface-main',
|
|
63
|
+
border: '1px solid $color-border-default',
|
|
64
|
+
borderRadius: '$border-radius-popover',
|
|
65
|
+
padding: '$gap-card-content',
|
|
66
|
+
boxShadow: '0 4px 16px rgba(0,0,0,0.4)',
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
// Helpers
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
|
|
74
|
+
function formatCost(n: number): string {
|
|
75
|
+
if (n >= 1) return `$${n.toFixed(2)}`;
|
|
76
|
+
if (n >= 0.01) return `$${n.toFixed(2)}`;
|
|
77
|
+
if (n > 0) return `$${n.toFixed(4)}`;
|
|
78
|
+
return '$0';
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
// Component
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
|
|
86
|
+
function BudgetStrip({
|
|
87
|
+
costUsed, costLimit, costSource,
|
|
88
|
+
tokenLimit,
|
|
89
|
+
tasksDone = 0, tasksFailed = 0, tasksOpen = 0,
|
|
90
|
+
onLimitsChange,
|
|
91
|
+
}: BudgetStripProps) {
|
|
92
|
+
const [popoverOpen, setPopoverOpen] = useState(false);
|
|
93
|
+
const [editCost, setEditCost] = useState('');
|
|
94
|
+
const [editTokens, setEditTokens] = useState('');
|
|
95
|
+
const editRef = useRef<HTMLDivElement>(null);
|
|
96
|
+
|
|
97
|
+
const handleOpenPopover = useCallback(() => {
|
|
98
|
+
setEditCost(String(costLimit));
|
|
99
|
+
setEditTokens(String(tokenLimit));
|
|
100
|
+
setPopoverOpen(true);
|
|
101
|
+
}, [costLimit, tokenLimit]);
|
|
102
|
+
|
|
103
|
+
const handleSave = useCallback(() => {
|
|
104
|
+
const c = parseFloat(editCost);
|
|
105
|
+
const t = parseFloat(editTokens);
|
|
106
|
+
if (c > 0 && t > 0 && onLimitsChange) {
|
|
107
|
+
onLimitsChange(c, t);
|
|
108
|
+
}
|
|
109
|
+
setPopoverOpen(false);
|
|
110
|
+
}, [editCost, editTokens, onLimitsChange]);
|
|
111
|
+
|
|
112
|
+
const sourceTag = costSource === 'cli' ? 'actual' : costSource === 'estimated' ? 'est.' : null;
|
|
113
|
+
|
|
114
|
+
const tallyParts: Array<{ count: number; label: string; chipStatus: 'success' | 'error' | 'info'; icon: string }> = [];
|
|
115
|
+
if (tasksDone > 0) tallyParts.push({ count: tasksDone, label: 'done', chipStatus: 'success', icon: 'check' });
|
|
116
|
+
if (tasksFailed > 0) tallyParts.push({ count: tasksFailed, label: 'failed', chipStatus: 'error', icon: 'close' });
|
|
117
|
+
if (tasksOpen > 0) tallyParts.push({ count: tasksOpen, label: 'open', chipStatus: 'info', icon: 'schedule' });
|
|
118
|
+
|
|
119
|
+
return (
|
|
120
|
+
<Strip>
|
|
121
|
+
{/* LEFT: Task status */}
|
|
122
|
+
<Flex variant="row-center-start-nowrap-4" style={{ flexShrink: 0 }}>
|
|
123
|
+
{tallyParts.map((p, i) => (
|
|
124
|
+
<StatusChip key={i} status={p.chipStatus} icon={p.icon}>{p.count} {p.label}</StatusChip>
|
|
125
|
+
))}
|
|
126
|
+
</Flex>
|
|
127
|
+
|
|
128
|
+
{/* CENTER: Budget */}
|
|
129
|
+
<Flex variant="row-center-center-nowrap-4" style={{ flex: 1, minWidth: 0 }}>
|
|
130
|
+
<Typography variant="smallCaption-thick" color="color-text-high">
|
|
131
|
+
{costLimit > 0 ? `${Math.round((costUsed / costLimit) * 100)}%` : '0%'}
|
|
132
|
+
</Typography>
|
|
133
|
+
<Typography variant="smallCaption-regular" color="color-border-default">|</Typography>
|
|
134
|
+
<Typography variant="smallCaption-regular" color="color-text-subtle">
|
|
135
|
+
{formatCost(costUsed)} / {formatCost(costLimit)}
|
|
136
|
+
</Typography>
|
|
137
|
+
{sourceTag && (
|
|
138
|
+
<Typography variant="smallCaption-regular" color={costSource === 'cli' ? 'color-status-positive' : 'color-text-subtle'}>
|
|
139
|
+
({sourceTag})
|
|
140
|
+
</Typography>
|
|
141
|
+
)}
|
|
142
|
+
</Flex>
|
|
143
|
+
|
|
144
|
+
{/* RIGHT: Edit budget */}
|
|
145
|
+
{onLimitsChange && (
|
|
146
|
+
<div ref={editRef} style={{ position: 'relative', flexShrink: 0 }}>
|
|
147
|
+
<Button size="xs" variant="outlined" onClick={handleOpenPopover}>
|
|
148
|
+
Edit Budget
|
|
149
|
+
</Button>
|
|
150
|
+
|
|
151
|
+
{popoverOpen && (
|
|
152
|
+
<>
|
|
153
|
+
<PopoverOverlay onClick={() => setPopoverOpen(false)} />
|
|
154
|
+
<PopoverCard style={{ top: '100%', right: 0, marginTop: '4px' }}>
|
|
155
|
+
<Flex variant="column-stretch-start-nowrap-6">
|
|
156
|
+
<Typography variant="smallCaption-thick" color="color-text-high">
|
|
157
|
+
Budget Limits
|
|
158
|
+
</Typography>
|
|
159
|
+
<Flex variant="row-center-space-between-nowrap-4" style={{ whiteSpace: 'nowrap' }}>
|
|
160
|
+
<Typography variant="smallCaption-regular" color="color-text-subtle" style={{ flexShrink: 0 }}>Cost ($)</Typography>
|
|
161
|
+
<Input
|
|
162
|
+
type="number"
|
|
163
|
+
size="extraSmall"
|
|
164
|
+
value={editCost}
|
|
165
|
+
onChange={(v: string) => setEditCost(v)}
|
|
166
|
+
autoFocus={true}
|
|
167
|
+
/>
|
|
168
|
+
</Flex>
|
|
169
|
+
<Flex variant="row-center-space-between-nowrap-4" style={{ whiteSpace: 'nowrap' }}>
|
|
170
|
+
<Typography variant="smallCaption-regular" color="color-text-subtle" style={{ flexShrink: 0 }}>Tokens</Typography>
|
|
171
|
+
<Input
|
|
172
|
+
type="number"
|
|
173
|
+
size="extraSmall"
|
|
174
|
+
value={editTokens}
|
|
175
|
+
onChange={(v: string) => setEditTokens(v)}
|
|
176
|
+
onKeyDown={(e: { key: string }) => { if (e.key === 'Enter') handleSave(); }}
|
|
177
|
+
/>
|
|
178
|
+
</Flex>
|
|
179
|
+
<Flex variant="row-center-end-nowrap-4">
|
|
180
|
+
<Button size="xs" variant="clear" onClick={() => setPopoverOpen(false)}>
|
|
181
|
+
Cancel
|
|
182
|
+
</Button>
|
|
183
|
+
<Button size="xs" variant="fill" color="primary" onClick={handleSave}>
|
|
184
|
+
Save
|
|
185
|
+
</Button>
|
|
186
|
+
</Flex>
|
|
187
|
+
</Flex>
|
|
188
|
+
</PopoverCard>
|
|
189
|
+
</>
|
|
190
|
+
)}
|
|
191
|
+
</div>
|
|
192
|
+
)}
|
|
193
|
+
</Strip>
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export { BudgetStrip };
|
|
198
|
+
export type { BudgetStripProps };
|
|
199
|
+
export default BudgetStrip;
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import React, { useState, useEffect, useCallback, useRef } from 'react';
|
|
11
11
|
import { Flex, Typography, StatusIcon, Button } from '@fw/plugin-ui-kit';
|
|
12
|
+
import { TASK_STATUS_ICON, TASK_STATUS_ICON_OVERRIDE, TASK_STATUS_LABEL } from './task-status';
|
|
12
13
|
|
|
13
14
|
// ---------------------------------------------------------------------------
|
|
14
15
|
// Types
|
|
@@ -53,31 +54,10 @@ function parseResult(result: unknown): { task: TaskData | null; subtasks: Subtas
|
|
|
53
54
|
}
|
|
54
55
|
}
|
|
55
56
|
|
|
56
|
-
type StatusKind = 'running' | 'completed' | 'failed' | 'pending';
|
|
57
|
-
|
|
58
|
-
function statusToIcon(status: TaskData['status']): StatusKind {
|
|
59
|
-
switch (status) {
|
|
60
|
-
case 'open':
|
|
61
|
-
return 'pending';
|
|
62
|
-
case 'in-progress':
|
|
63
|
-
return 'running';
|
|
64
|
-
case 'done':
|
|
65
|
-
return 'completed';
|
|
66
|
-
case 'cancelled':
|
|
67
|
-
return 'failed';
|
|
68
|
-
default:
|
|
69
|
-
return 'pending';
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
57
|
function statusLabel(status: TaskData['status']): string {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
case 'done': return 'Done';
|
|
78
|
-
case 'cancelled': return 'Cancelled';
|
|
79
|
-
default: return status;
|
|
80
|
-
}
|
|
58
|
+
// 'in-progress' maps to 'Running' in chat context (differs from TASK_STATUS_LABEL's 'In Progress')
|
|
59
|
+
if (status === 'in-progress') return 'Running';
|
|
60
|
+
return TASK_STATUS_LABEL[status] ?? status;
|
|
81
61
|
}
|
|
82
62
|
|
|
83
63
|
function isTerminal(status: TaskData['status']): boolean {
|
|
@@ -174,7 +154,7 @@ function ChatTaskResult(props: ChatTaskResultProps | null) {
|
|
|
174
154
|
}}
|
|
175
155
|
>
|
|
176
156
|
{/* Status icon */}
|
|
177
|
-
<StatusIcon status={
|
|
157
|
+
<StatusIcon status={TASK_STATUS_ICON[task.status] || 'pending'} icon={TASK_STATUS_ICON_OVERRIDE[task.status]} size="sm" />
|
|
178
158
|
|
|
179
159
|
{/* Title + meta column */}
|
|
180
160
|
<Flex
|
|
@@ -79,7 +79,13 @@ interface StatusBlock {
|
|
|
79
79
|
detail?: string;
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
|
|
82
|
+
interface WarningBlock {
|
|
83
|
+
kind: 'warning';
|
|
84
|
+
label: string;
|
|
85
|
+
detail?: string;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
type ConversationBlock = ThinkingBlock | TextBlock | ToolCallBlock | StatusBlock | WarningBlock;
|
|
83
89
|
|
|
84
90
|
// ---------------------------------------------------------------------------
|
|
85
91
|
// Build conversation blocks from events
|
|
@@ -194,6 +200,30 @@ function buildConversation(events: StreamEvent[]): ConversationBlock[] {
|
|
|
194
200
|
lastType = 'bot-failed';
|
|
195
201
|
break;
|
|
196
202
|
|
|
203
|
+
// ── Session/bridge warnings (from CliSession markDead, active turn kill) ──
|
|
204
|
+
|
|
205
|
+
case 'session-warning':
|
|
206
|
+
flushThinking(false);
|
|
207
|
+
flushText(false);
|
|
208
|
+
blocks.push({
|
|
209
|
+
kind: 'warning',
|
|
210
|
+
label: (d.label as string) ?? 'Session warning',
|
|
211
|
+
detail: (d.detail as string) ?? (d.stderr as string) ?? undefined,
|
|
212
|
+
});
|
|
213
|
+
lastType = 'session-warning';
|
|
214
|
+
break;
|
|
215
|
+
|
|
216
|
+
case 'audit:bridge-tool-filtered':
|
|
217
|
+
if ((d.textToolCallDetected as number) > 0) {
|
|
218
|
+
blocks.push({
|
|
219
|
+
kind: 'warning',
|
|
220
|
+
label: `MCP tools not connected — ${d.textToolCallDetected} tool calls output as text`,
|
|
221
|
+
detail: 'The CLI session does not have MCP tools registered. Tool calls were output as text instead of structured tool_use.',
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
lastType = 'audit:bridge-tool-filtered';
|
|
225
|
+
break;
|
|
226
|
+
|
|
197
227
|
// ── Swarm/workflow-level events (emitted by runner + swarm-controller) ──
|
|
198
228
|
|
|
199
229
|
case 'task-claimed':
|
|
@@ -371,6 +401,7 @@ function useConversationTypewriter(blocks: ConversationBlock[]) {
|
|
|
371
401
|
return Math.min(display.length, TYPEWRITER.maxToolResultLen);
|
|
372
402
|
}
|
|
373
403
|
case 'status': return (block.detail ?? '').length;
|
|
404
|
+
case 'warning': return (block.detail ?? '').length;
|
|
374
405
|
default: return 0;
|
|
375
406
|
}
|
|
376
407
|
}, []);
|
|
@@ -786,6 +817,24 @@ function InstanceStreamView({
|
|
|
786
817
|
</CollapsibleBlock>
|
|
787
818
|
);
|
|
788
819
|
}
|
|
820
|
+
case 'warning':
|
|
821
|
+
return (
|
|
822
|
+
<CollapsibleBlock
|
|
823
|
+
status="error"
|
|
824
|
+
label={block.label}
|
|
825
|
+
expanded={!!block.detail}
|
|
826
|
+
canExpand={!!block.detail}
|
|
827
|
+
onToggle={() => {}}
|
|
828
|
+
>
|
|
829
|
+
{block.detail && (
|
|
830
|
+
<Section padding="compact">
|
|
831
|
+
<Typography variant="body-regular" color="color-danger-main">
|
|
832
|
+
{block.detail}
|
|
833
|
+
</Typography>
|
|
834
|
+
</Section>
|
|
835
|
+
)}
|
|
836
|
+
</CollapsibleBlock>
|
|
837
|
+
);
|
|
789
838
|
default:
|
|
790
839
|
return null;
|
|
791
840
|
}
|
|
@@ -26,8 +26,7 @@ import {
|
|
|
26
26
|
|
|
27
27
|
// Local pack-specific components (bundled by esbuild)
|
|
28
28
|
import SwarmControls from './swarm-controls';
|
|
29
|
-
import
|
|
30
|
-
import BotSlotCard from './bot-slot-card';
|
|
29
|
+
import BudgetStrip from './budget-strip';
|
|
31
30
|
import TaskPoolList from './task-pool-list.js';
|
|
32
31
|
import TaskDetailView from './task-detail-view';
|
|
33
32
|
import TaskEditor from './task-editor';
|
|
@@ -37,6 +36,7 @@ import DecisionLog from './decision-log';
|
|
|
37
36
|
import CapabilityEditor from './capability-editor';
|
|
38
37
|
import InstanceStreamView from './instance-stream-view';
|
|
39
38
|
import { ICON_CATALOG, BOT_COLORS } from './bot-constants';
|
|
39
|
+
import { TaskStatus, TASK_STATUS_BOARD_LABEL, TASK_STATUS_BOARD_COLOR } from './task-status';
|
|
40
40
|
|
|
41
41
|
// ---------------------------------------------------------------------------
|
|
42
42
|
// Types
|
|
@@ -79,8 +79,6 @@ interface SwarmStatus {
|
|
|
79
79
|
packVersion?: string;
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
type TaskStatus = 'open' | 'in-progress' | 'done' | 'cancelled';
|
|
83
|
-
|
|
84
82
|
interface PoolTask {
|
|
85
83
|
id: string;
|
|
86
84
|
title: string;
|
|
@@ -395,41 +393,27 @@ function SwarmDashboard() {
|
|
|
395
393
|
onRefresh={refreshAll}
|
|
396
394
|
/>
|
|
397
395
|
|
|
398
|
-
{/*
|
|
396
|
+
{/* Budget strip (below controls) */}
|
|
399
397
|
{hasBudget && (
|
|
400
398
|
<Section padding="compact" border="bottom" shrink={true}>
|
|
401
|
-
<
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
used={sessionBudget!.usedCost}
|
|
420
|
-
limit={sessionBudget!.limitCost}
|
|
421
|
-
unit="USD"
|
|
422
|
-
onLimitChange={async (newLimit: number) => {
|
|
423
|
-
try {
|
|
424
|
-
await callTool('fw_weaver_swarm_config', { sessionBudgetCost: newLimit });
|
|
425
|
-
toast(`Cost budget updated to $${newLimit.toFixed(2)}`, { type: 'success' });
|
|
426
|
-
fetchSwarmStatus();
|
|
427
|
-
} catch (err: unknown) {
|
|
428
|
-
toast(err instanceof Error ? err.message : 'Failed to update budget', { type: 'error' });
|
|
429
|
-
}
|
|
430
|
-
}}
|
|
431
|
-
/>
|
|
432
|
-
</Flex>
|
|
399
|
+
<BudgetStrip
|
|
400
|
+
costUsed={sessionBudget!.usedCost}
|
|
401
|
+
costLimit={sessionBudget!.limitCost}
|
|
402
|
+
tokensUsed={sessionBudget!.usedTokens}
|
|
403
|
+
tokenLimit={sessionBudget!.limitTokens}
|
|
404
|
+
tasksDone={tasks.filter((t: { status: string }) => t.status === 'done').length}
|
|
405
|
+
tasksFailed={tasks.filter((t: { status: string }) => t.status === 'failed' || t.status === 'cancelled').length}
|
|
406
|
+
tasksOpen={tasks.filter((t: { status: string }) => t.status !== 'done' && t.status !== 'failed' && t.status !== 'cancelled').length}
|
|
407
|
+
onLimitsChange={async (costLimit: number, tokenLimit: number) => {
|
|
408
|
+
try {
|
|
409
|
+
await callTool('fw_weaver_swarm_config', { sessionBudgetCost: costLimit, sessionBudgetTokens: tokenLimit });
|
|
410
|
+
toast('Budget updated', { type: 'success' });
|
|
411
|
+
fetchSwarmStatus();
|
|
412
|
+
} catch (err: unknown) {
|
|
413
|
+
toast(err instanceof Error ? err.message : 'Failed to update budget', { type: 'error' });
|
|
414
|
+
}
|
|
415
|
+
}}
|
|
416
|
+
/>
|
|
433
417
|
</Section>
|
|
434
418
|
)}
|
|
435
419
|
|
|
@@ -541,12 +525,10 @@ function SwarmDashboard() {
|
|
|
541
525
|
const allTasks = tasks as Array<Record<string, unknown>>;
|
|
542
526
|
const doneIds = new Set(allTasks.filter(t => t.status === 'done' || t.status === 'cancelled').map(t => t.id as string));
|
|
543
527
|
|
|
528
|
+
const statusColumns: TaskStatus[] = ['open', 'in-progress', 'done', 'cancelled'];
|
|
544
529
|
const kanbanColumns = [
|
|
545
530
|
{ id: 'waiting', title: 'Waiting', color: 'var(--color-status-caution)' },
|
|
546
|
-
{ id:
|
|
547
|
-
{ id: 'in-progress', title: 'In Progress', color: 'var(--color-brand-main)' },
|
|
548
|
-
{ id: 'done', title: 'Done', color: 'var(--color-status-positive)' },
|
|
549
|
-
{ id: 'cancelled', title: 'Cancelled', color: 'var(--color-status-negative)' },
|
|
531
|
+
...statusColumns.map(s => ({ id: s, title: TASK_STATUS_BOARD_LABEL[s], color: `var(--${TASK_STATUS_BOARD_COLOR[s]})` })),
|
|
550
532
|
];
|
|
551
533
|
|
|
552
534
|
const kanbanCards = allTasks
|
|
@@ -599,51 +581,76 @@ function SwarmDashboard() {
|
|
|
599
581
|
>
|
|
600
582
|
{/* When swarm is running: show swarm instances */}
|
|
601
583
|
{hasSwarmInstances && (
|
|
602
|
-
<
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
style={{ padding: '8px 16px', borderBottom: '1px solid var(--color-border-default)' }}
|
|
606
|
-
>
|
|
607
|
-
<Typography variant="smallCaption-regular" color="color-text-subtle" style={{ width: '120px', flexShrink: 0 }}>Worker</Typography>
|
|
608
|
-
<Typography variant="smallCaption-regular" color="color-text-subtle" style={{ width: '110px', flexShrink: 0 }}>Bot</Typography>
|
|
609
|
-
<Typography variant="smallCaption-regular" color="color-text-subtle" style={{ width: '70px', flexShrink: 0 }}>Status</Typography>
|
|
610
|
-
<Typography variant="smallCaption-regular" color="color-text-subtle" style={{ flex: 1, minWidth: 0 }}>Task</Typography>
|
|
611
|
-
<Typography variant="smallCaption-regular" color="color-text-subtle" style={{ width: '50px', flexShrink: 0, textAlign: 'right' }}>Tokens</Typography>
|
|
612
|
-
<Typography variant="smallCaption-regular" color="color-text-subtle" style={{ width: '50px', flexShrink: 0, textAlign: 'right' }}>Cost</Typography>
|
|
613
|
-
<Typography variant="smallCaption-regular" color="color-text-subtle" style={{ width: '50px', flexShrink: 0 }}>{''}</Typography>
|
|
614
|
-
</Flex>
|
|
615
|
-
{/* Instance rows */}
|
|
616
|
-
{swarmInstanceEntries.map((inst: InstanceInfo) => {
|
|
617
|
-
// Look up profile -> bot for this instance
|
|
584
|
+
<Table
|
|
585
|
+
size="compact"
|
|
586
|
+
data={swarmInstanceEntries.map((inst: InstanceInfo) => {
|
|
618
587
|
const profile = profiles.find((p: Record<string, unknown>) => p.id === inst.profileId);
|
|
619
588
|
const botId = profile?.botId as string | undefined;
|
|
620
589
|
const bot = registeredBots.find((b) => b.id === botId);
|
|
621
|
-
return (
|
|
622
|
-
<BotSlotCard
|
|
623
|
-
key={inst.instanceId}
|
|
624
|
-
bot={{
|
|
625
|
-
botId: inst.instanceId,
|
|
626
|
-
botName: inst.profileId ? `${inst.instanceId} (${inst.profileId})` : inst.instanceId,
|
|
627
|
-
status: inst.status,
|
|
628
|
-
currentTaskId: inst.currentTaskId,
|
|
629
|
-
currentRunId: inst.currentRunId,
|
|
630
|
-
startedAt: inst.startedAt,
|
|
631
|
-
tokensUsed: inst.tokensUsed,
|
|
632
|
-
cost: inst.cost,
|
|
633
|
-
}}
|
|
634
|
-
profileName={(profile?.name as string) || inst.profileId}
|
|
635
|
-
botDisplayName={bot?.name}
|
|
636
|
-
botIcon={bot?.icon}
|
|
637
|
-
botColor={bot?.color}
|
|
638
|
-
currentTaskTitle={resolveTaskTitle(inst.currentTaskId, tasks)}
|
|
639
|
-
onView={() => handleInstanceClick(inst)}
|
|
640
|
-
onPause={(id: string) => handleSteerBot(id, 'pause')}
|
|
641
|
-
onResume={(id: string) => handleSteerBot(id, 'resume')}
|
|
642
|
-
onStop={(id: string) => handleSteerBot(id, 'cancel')}
|
|
643
|
-
/>
|
|
644
|
-
);
|
|
590
|
+
return { ...inst, _profileName: (profile?.name as string) || inst.profileId, _botName: bot?.name, _botIcon: bot?.icon, _botColor: bot?.color };
|
|
645
591
|
})}
|
|
646
|
-
|
|
592
|
+
getRowKey={(row: InstanceInfo) => row.instanceId}
|
|
593
|
+
onRowClick={(row: InstanceInfo) => row.status === 'executing' && handleInstanceClick(row)}
|
|
594
|
+
columns={[
|
|
595
|
+
{
|
|
596
|
+
key: 'worker', header: 'Worker', width: '25%',
|
|
597
|
+
render: (_v: unknown, row: Record<string, unknown>) => (
|
|
598
|
+
<Flex variant="row-center-start-nowrap-0" style={{ gap: 'var(--gap-button-icon)' }}>
|
|
599
|
+
<Icon name={(row._botIcon as string) || 'smartToy'} size={12} color={(row._botColor as string) || 'color-text-subtle'} />
|
|
600
|
+
<span>{row._profileName as string}</span>
|
|
601
|
+
</Flex>
|
|
602
|
+
),
|
|
603
|
+
},
|
|
604
|
+
{
|
|
605
|
+
key: 'status', header: 'Status', width: '15%',
|
|
606
|
+
render: (_v: unknown, row: Record<string, unknown>) => {
|
|
607
|
+
const s = row.status as string;
|
|
608
|
+
return (
|
|
609
|
+
<Flex variant="row-center-start-nowrap-0" style={{ gap: 'var(--gap-button-icon)' }}>
|
|
610
|
+
<StatusIcon status={s === 'executing' ? 'running' : s === 'paused' ? 'warning' : 'pending'} size="xs" />
|
|
611
|
+
<span>{s === 'executing' ? 'Running' : s === 'paused' ? 'Paused' : s === 'stopped' ? 'Stopped' : 'Idle'}</span>
|
|
612
|
+
</Flex>
|
|
613
|
+
);
|
|
614
|
+
},
|
|
615
|
+
},
|
|
616
|
+
{
|
|
617
|
+
key: 'task', header: 'Task',
|
|
618
|
+
render: (_v: unknown, row: Record<string, unknown>) => (
|
|
619
|
+
<span>{row.status === 'executing' ? resolveTaskTitle(row.currentTaskId as string, tasks) || (row.currentTaskId as string) || '-' : '-'}</span>
|
|
620
|
+
),
|
|
621
|
+
},
|
|
622
|
+
{
|
|
623
|
+
key: 'tokens', header: 'Tokens', width: '80px', align: 'right' as const,
|
|
624
|
+
render: (_v: unknown, row: Record<string, unknown>) => {
|
|
625
|
+
const t = row.tokensUsed as number;
|
|
626
|
+
return <span>{t >= 1000 ? `${(t / 1000).toFixed(1)}k` : t}</span>;
|
|
627
|
+
},
|
|
628
|
+
},
|
|
629
|
+
{
|
|
630
|
+
key: 'cost', header: 'Cost', width: '80px', align: 'right' as const,
|
|
631
|
+
render: (_v: unknown, row: Record<string, unknown>) => {
|
|
632
|
+
const c = row.cost as number;
|
|
633
|
+
return <span>{c < 0.01 && c > 0 ? '<$0.01' : `$${c.toFixed(2)}`}</span>;
|
|
634
|
+
},
|
|
635
|
+
},
|
|
636
|
+
{
|
|
637
|
+
key: 'actions', header: '', width: '60px', align: 'right' as const,
|
|
638
|
+
render: (_v: unknown, row: Record<string, unknown>) => (
|
|
639
|
+
<Flex variant="row-center-end-nowrap-1" onClick={(e: { stopPropagation: () => void }) => e.stopPropagation()}>
|
|
640
|
+
{row.status === 'executing' && (
|
|
641
|
+
<IconButton icon="pause" size="xs" variant="clear" onClick={() => handleSteerBot(row.instanceId as string, 'pause')} title="Pause" />
|
|
642
|
+
)}
|
|
643
|
+
{row.status === 'paused' && (
|
|
644
|
+
<IconButton icon="playArrow" size="xs" variant="clear" onClick={() => handleSteerBot(row.instanceId as string, 'resume')} title="Resume" />
|
|
645
|
+
)}
|
|
646
|
+
{(row.status === 'executing' || row.status === 'paused') && (
|
|
647
|
+
<IconButton icon="stop" size="xs" variant="clear" color="danger" onClick={() => handleSteerBot(row.instanceId as string, 'cancel')} title="Stop" />
|
|
648
|
+
)}
|
|
649
|
+
</Flex>
|
|
650
|
+
),
|
|
651
|
+
},
|
|
652
|
+
]}
|
|
653
|
+
/>
|
|
647
654
|
)}
|
|
648
655
|
|
|
649
656
|
{/* When swarm is NOT running: show registered bots */}
|
|
@@ -969,7 +976,6 @@ function SwarmDashboard() {
|
|
|
969
976
|
<Flex variant="column-stretch-start-nowrap-4">
|
|
970
977
|
<SectionTitle size="xs">Recent Runs</SectionTitle>
|
|
971
978
|
<Table
|
|
972
|
-
size="compact"
|
|
973
979
|
getRowKey={(row: Record<string, unknown>) => (row.id as string) ?? String(Math.random())}
|
|
974
980
|
onRowClick={(row: Record<string, unknown>) => {
|
|
975
981
|
if (row.taskId) handleTaskClick(row.taskId as string);
|
|
@@ -1002,7 +1008,6 @@ function SwarmDashboard() {
|
|
|
1002
1008
|
}},
|
|
1003
1009
|
]}
|
|
1004
1010
|
data={runHistory as Array<Record<string, unknown>>}
|
|
1005
|
-
maxHeight="200px"
|
|
1006
1011
|
/>
|
|
1007
1012
|
</Flex>
|
|
1008
1013
|
)}
|