@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.
Files changed (38) hide show
  1. package/dist/bot/preflight.d.ts.map +1 -1
  2. package/dist/bot/preflight.js +26 -0
  3. package/dist/bot/preflight.js.map +1 -1
  4. package/dist/bot/task-create-handler.d.ts +9 -0
  5. package/dist/bot/task-create-handler.d.ts.map +1 -1
  6. package/dist/bot/task-create-handler.js +26 -0
  7. package/dist/bot/task-create-handler.js.map +1 -1
  8. package/dist/node-types/agent-execute.d.ts.map +1 -1
  9. package/dist/node-types/agent-execute.js +26 -9
  10. package/dist/node-types/agent-execute.js.map +1 -1
  11. package/dist/node-types/plan-task.d.ts.map +1 -1
  12. package/dist/node-types/plan-task.js +28 -2
  13. package/dist/node-types/plan-task.js.map +1 -1
  14. package/dist/ui/bot-slot-card.js +10 -0
  15. package/dist/ui/budget-bar.js +5 -3
  16. package/dist/ui/budget-strip.js +156 -0
  17. package/dist/ui/chat-task-result.js +22 -27
  18. package/dist/ui/instance-stream-view.js +36 -0
  19. package/dist/ui/swarm-dashboard.js +1596 -1654
  20. package/dist/ui/task-detail-view.js +973 -485
  21. package/dist/ui/task-editor.js +32 -34
  22. package/dist/ui/task-pool-list.js +11 -3
  23. package/flowweaver.manifest.json +1 -1
  24. package/package.json +3 -2
  25. package/src/bot/preflight.ts +26 -0
  26. package/src/bot/task-create-handler.ts +39 -0
  27. package/src/node-types/agent-execute.ts +27 -10
  28. package/src/node-types/plan-task.ts +25 -2
  29. package/src/ui/bot-slot-card.tsx +23 -0
  30. package/src/ui/budget-bar.tsx +13 -5
  31. package/src/ui/budget-strip.tsx +199 -0
  32. package/src/ui/chat-task-result.tsx +5 -25
  33. package/src/ui/instance-stream-view.tsx +50 -1
  34. package/src/ui/swarm-dashboard.tsx +89 -84
  35. package/src/ui/task-detail-view.tsx +376 -442
  36. package/src/ui/task-editor.tsx +65 -96
  37. package/src/ui/task-pool-list.tsx +3 -12
  38. 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
- switch (status) {
75
- case 'open': return 'Open';
76
- case 'in-progress': return 'Running';
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={statusToIcon(task.status)} size="sm" />
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
- type ConversationBlock = ThinkingBlock | TextBlock | ToolCallBlock | StatusBlock;
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 BudgetBar from './budget-bar';
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
- {/* BudgetBar (below controls) */}
396
+ {/* Budget strip (below controls) */}
399
397
  {hasBudget && (
400
398
  <Section padding="compact" border="bottom" shrink={true}>
401
- <Flex variant="column-stretch-start-nowrap-4">
402
- <BudgetBar
403
- label="Tokens"
404
- used={sessionBudget!.usedTokens}
405
- limit={sessionBudget!.limitTokens}
406
- unit="tokens"
407
- onLimitChange={async (newLimit: number) => {
408
- try {
409
- await callTool('fw_weaver_swarm_config', { sessionBudgetTokens: newLimit });
410
- toast(`Token budget updated to ${newLimit.toLocaleString()}`, { type: 'success' });
411
- fetchSwarmStatus();
412
- } catch (err: unknown) {
413
- toast(err instanceof Error ? err.message : 'Failed to update budget', { type: 'error' });
414
- }
415
- }}
416
- />
417
- <BudgetBar
418
- label="Cost"
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: 'open', title: 'Ready', color: 'var(--color-text-subtle)' },
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
- <Flex variant="column-stretch-start-nowrap-0">
603
- <Flex
604
- variant="row-center-start-nowrap-8"
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
- </Flex>
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
  )}