@synergenius/flow-weaver-pack-weaver 0.9.200 → 0.9.202

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 (181) hide show
  1. package/dist/ai-chat-provider.js +5 -5
  2. package/dist/ai-chat-provider.js.map +1 -1
  3. package/dist/bot/acceptance-merge.d.ts +21 -0
  4. package/dist/bot/acceptance-merge.d.ts.map +1 -0
  5. package/dist/bot/acceptance-merge.js +46 -0
  6. package/dist/bot/acceptance-merge.js.map +1 -0
  7. package/dist/bot/ai-client.d.ts +14 -2
  8. package/dist/bot/ai-client.d.ts.map +1 -1
  9. package/dist/bot/ai-client.js +71 -24
  10. package/dist/bot/ai-client.js.map +1 -1
  11. package/dist/bot/assistant-tools.js +3 -3
  12. package/dist/bot/assistant-tools.js.map +1 -1
  13. package/dist/bot/audit-logger.d.ts.map +1 -1
  14. package/dist/bot/audit-logger.js +34 -14
  15. package/dist/bot/audit-logger.js.map +1 -1
  16. package/dist/bot/audit-trail.d.ts +67 -0
  17. package/dist/bot/audit-trail.d.ts.map +1 -0
  18. package/dist/bot/audit-trail.js +153 -0
  19. package/dist/bot/audit-trail.js.map +1 -0
  20. package/dist/bot/behavior-defaults.d.ts +1 -1
  21. package/dist/bot/behavior-defaults.d.ts.map +1 -1
  22. package/dist/bot/behavior-defaults.js +7 -3
  23. package/dist/bot/behavior-defaults.js.map +1 -1
  24. package/dist/bot/capability-registry.d.ts +9 -0
  25. package/dist/bot/capability-registry.d.ts.map +1 -1
  26. package/dist/bot/capability-registry.js +81 -27
  27. package/dist/bot/capability-registry.js.map +1 -1
  28. package/dist/bot/capability-types.d.ts +10 -0
  29. package/dist/bot/capability-types.d.ts.map +1 -1
  30. package/dist/bot/cli-provider.d.ts.map +1 -1
  31. package/dist/bot/cli-provider.js +8 -7
  32. package/dist/bot/cli-provider.js.map +1 -1
  33. package/dist/bot/preflight.d.ts +48 -0
  34. package/dist/bot/preflight.d.ts.map +1 -0
  35. package/dist/bot/preflight.js +273 -0
  36. package/dist/bot/preflight.js.map +1 -0
  37. package/dist/bot/provider-shim.d.ts +74 -0
  38. package/dist/bot/provider-shim.d.ts.map +1 -0
  39. package/dist/bot/provider-shim.js +176 -0
  40. package/dist/bot/provider-shim.js.map +1 -0
  41. package/dist/bot/runner.d.ts +2 -0
  42. package/dist/bot/runner.d.ts.map +1 -1
  43. package/dist/bot/runner.js +60 -17
  44. package/dist/bot/runner.js.map +1 -1
  45. package/dist/bot/step-executor.d.ts.map +1 -1
  46. package/dist/bot/step-executor.js +72 -115
  47. package/dist/bot/step-executor.js.map +1 -1
  48. package/dist/bot/swarm-controller.d.ts +2 -0
  49. package/dist/bot/swarm-controller.d.ts.map +1 -1
  50. package/dist/bot/swarm-controller.js +92 -20
  51. package/dist/bot/swarm-controller.js.map +1 -1
  52. package/dist/bot/task-create-handler.d.ts +37 -0
  53. package/dist/bot/task-create-handler.d.ts.map +1 -0
  54. package/dist/bot/task-create-handler.js +124 -0
  55. package/dist/bot/task-create-handler.js.map +1 -0
  56. package/dist/bot/task-store.d.ts +1 -0
  57. package/dist/bot/task-store.d.ts.map +1 -1
  58. package/dist/bot/task-store.js +61 -0
  59. package/dist/bot/task-store.js.map +1 -1
  60. package/dist/bot/types.d.ts +1 -1
  61. package/dist/bot/types.d.ts.map +1 -1
  62. package/dist/bot/weaver-tools.d.ts.map +1 -1
  63. package/dist/bot/weaver-tools.js +7 -39
  64. package/dist/bot/weaver-tools.js.map +1 -1
  65. package/dist/node-types/agent-execute.d.ts +25 -8
  66. package/dist/node-types/agent-execute.d.ts.map +1 -1
  67. package/dist/node-types/agent-execute.js +89 -23
  68. package/dist/node-types/agent-execute.js.map +1 -1
  69. package/dist/node-types/bot-report.d.ts.map +1 -1
  70. package/dist/node-types/bot-report.js +24 -3
  71. package/dist/node-types/bot-report.js.map +1 -1
  72. package/dist/node-types/plan-task.d.ts +8 -17
  73. package/dist/node-types/plan-task.d.ts.map +1 -1
  74. package/dist/node-types/plan-task.js +217 -256
  75. package/dist/node-types/plan-task.js.map +1 -1
  76. package/dist/node-types/review-result.js +8 -6
  77. package/dist/node-types/review-result.js.map +1 -1
  78. package/dist/palindrome.d.ts +9 -0
  79. package/dist/palindrome.d.ts.map +1 -0
  80. package/dist/palindrome.js +14 -0
  81. package/dist/palindrome.js.map +1 -0
  82. package/dist/ui/approval-card.js +91 -82
  83. package/dist/ui/bot-activity.js +73 -56
  84. package/dist/ui/bot-config.js +48 -31
  85. package/dist/ui/bot-dashboard.js +52 -36
  86. package/dist/ui/bot-panel.js +230 -228
  87. package/dist/ui/bot-slot-card.js +100 -90
  88. package/dist/ui/bot-status.js +37 -15
  89. package/dist/ui/budget-bar.js +57 -31
  90. package/dist/ui/capability-editor.js +447 -378
  91. package/dist/ui/chat-task-result.js +78 -71
  92. package/dist/ui/decision-log.js +68 -81
  93. package/dist/ui/genesis-block.js +86 -95
  94. package/dist/ui/instance-stream-view.js +722 -0
  95. package/dist/ui/profile-card.js +96 -221
  96. package/dist/ui/profile-editor.js +532 -575
  97. package/dist/ui/settings-section.js +41 -45
  98. package/dist/ui/swarm-controls.js +212 -135
  99. package/dist/ui/swarm-dashboard.js +3992 -2715
  100. package/dist/ui/task-detail-view.js +415 -521
  101. package/dist/ui/task-editor.js +339 -390
  102. package/dist/ui/task-pool-list.js +60 -55
  103. package/dist/workflows/src/palindrome.d.ts +11 -0
  104. package/dist/workflows/src/palindrome.d.ts.map +1 -0
  105. package/dist/workflows/src/palindrome.js +16 -0
  106. package/dist/workflows/src/palindrome.js.map +1 -0
  107. package/dist/workflows/tests/palindrome.test.d.ts +2 -0
  108. package/dist/workflows/tests/palindrome.test.d.ts.map +1 -0
  109. package/dist/workflows/tests/palindrome.test.js +41 -0
  110. package/dist/workflows/tests/palindrome.test.js.map +1 -0
  111. package/dist/workflows/weaver-bot-batch.js +1 -1
  112. package/dist/workflows/weaver-bot-batch.js.map +1 -1
  113. package/dist/workflows/weaver-bot.js +1 -1
  114. package/dist/workflows/weaver-bot.js.map +1 -1
  115. package/flowweaver.manifest.json +1 -1
  116. package/package.json +8 -2
  117. package/src/ai-chat-provider.ts +5 -5
  118. package/src/bot/acceptance-merge.ts +62 -0
  119. package/src/bot/ai-client.ts +77 -21
  120. package/src/bot/assistant-tools.ts +3 -3
  121. package/src/bot/audit-logger.ts +42 -14
  122. package/src/bot/audit-trail.ts +211 -0
  123. package/src/bot/behavior-defaults.ts +7 -2
  124. package/src/bot/capability-registry.ts +84 -28
  125. package/src/bot/capability-types.ts +11 -0
  126. package/src/bot/cli-provider.ts +8 -7
  127. package/src/bot/preflight.ts +311 -0
  128. package/src/bot/provider-shim.ts +218 -0
  129. package/src/bot/runner.ts +68 -20
  130. package/src/bot/step-executor.ts +69 -127
  131. package/src/bot/swarm-controller.ts +94 -20
  132. package/src/bot/task-create-handler.ts +164 -0
  133. package/src/bot/task-store.ts +76 -0
  134. package/src/bot/types.ts +4 -1
  135. package/src/bot/weaver-tools.ts +7 -45
  136. package/src/node-types/agent-execute.ts +102 -16
  137. package/src/node-types/bot-report.ts +24 -3
  138. package/src/node-types/plan-task.ts +238 -280
  139. package/src/node-types/review-result.ts +8 -6
  140. package/src/palindrome.ts +14 -0
  141. package/src/ui/approval-card.tsx +78 -62
  142. package/src/ui/bot-activity.tsx +12 -10
  143. package/src/ui/bot-config.tsx +12 -10
  144. package/src/ui/bot-dashboard.tsx +13 -11
  145. package/src/ui/bot-panel.tsx +189 -171
  146. package/src/ui/bot-slot-card.tsx +125 -70
  147. package/src/ui/bot-status.tsx +4 -4
  148. package/src/ui/budget-bar.tsx +86 -25
  149. package/src/ui/capability-editor.tsx +392 -257
  150. package/src/ui/chat-task-result.tsx +81 -78
  151. package/src/ui/decision-log.tsx +76 -73
  152. package/src/ui/genesis-block.tsx +91 -61
  153. package/src/ui/instance-stream-view.tsx +861 -0
  154. package/src/ui/profile-card.tsx +195 -168
  155. package/src/ui/profile-editor.tsx +453 -370
  156. package/src/ui/settings-section.tsx +46 -39
  157. package/src/ui/swarm-controls.tsx +252 -123
  158. package/src/ui/swarm-dashboard.tsx +999 -466
  159. package/src/ui/task-detail-view.tsx +485 -428
  160. package/src/ui/task-editor.tsx +329 -271
  161. package/src/ui/task-pool-list.tsx +68 -62
  162. package/src/workflows/src/palindrome.ts +16 -0
  163. package/src/workflows/tests/palindrome.test.ts +49 -0
  164. package/src/workflows/weaver-bot-batch.ts +1 -1
  165. package/src/workflows/weaver-bot.ts +1 -1
  166. package/dist/ui/bot-constants.d.ts +0 -14
  167. package/dist/ui/bot-constants.d.ts.map +0 -1
  168. package/dist/ui/bot-constants.js +0 -189
  169. package/dist/ui/bot-constants.js.map +0 -1
  170. package/dist/ui/steer-api.d.ts +0 -7
  171. package/dist/ui/steer-api.d.ts.map +0 -1
  172. package/dist/ui/steer-api.js +0 -11
  173. package/dist/ui/steer-api.js.map +0 -1
  174. package/dist/ui/trace-to-timeline.d.ts +0 -91
  175. package/dist/ui/trace-to-timeline.d.ts.map +0 -1
  176. package/dist/ui/trace-to-timeline.js +0 -116
  177. package/dist/ui/trace-to-timeline.js.map +0 -1
  178. package/dist/ui/use-stream-timeline.d.ts +0 -50
  179. package/dist/ui/use-stream-timeline.d.ts.map +0 -1
  180. package/dist/ui/use-stream-timeline.js +0 -245
  181. package/dist/ui/use-stream-timeline.js.map +0 -1
@@ -5,35 +5,37 @@
5
5
  * Composes: SwarmControls, BudgetBar, BotSlotCard, TaskPoolList,
6
6
  * TaskEditor, and TaskDetailView into a unified swarm dashboard.
7
7
  *
8
- * Two-panel navigation:
8
+ * Three-panel navigation:
9
9
  * - Default: full swarm dashboard (controls + budget + bots + task pool + create form)
10
10
  * - On task click: TaskDetailView (with back button returning to dashboard)
11
+ * - On instance click: InstanceStreamView (live AI stream for a bot instance)
11
12
  *
12
13
  * Data loading:
13
14
  * - Polls fw_weaver_swarm_status every 3s for swarm state
14
15
  * - Polls fw_weaver_task_list every 5s for task pool
15
16
  * - Both called immediately on mount
16
17
  *
17
- * Pattern: CommonJS require for platform deps, ESM import for local components,
18
- * React.createElement throughout, module.exports at end.
18
+ * Pattern: ESM imports, JSX rendering.
19
19
  */
20
- const React = require('react');
21
- const { useState, useEffect, useCallback, useRef } = React;
22
- const {
20
+ import React, { useState, useEffect, useCallback, useRef } from 'react';
21
+ import {
23
22
  Flex, ScrollArea, EmptyState, Typography, StatusIcon, Icon, Chip, Card, Tabs, SectionTitle, Button,
24
- Input, IconButton, IconPicker, ColorPicker, toast, usePackWorkspace,
25
- } = require('@fw/plugin-ui-kit');
23
+ Input, Slider, IconButton, IconPicker, ColorPicker, Section, Table, KeyValueRow, Field,
24
+ SparklineChart, BarChart, MetricCard, KanbanBoard, TaskCard, toast, usePackWorkspace,
25
+ } from '@fw/plugin-ui-kit';
26
26
 
27
27
  // Local pack-specific components (bundled by esbuild)
28
28
  import SwarmControls from './swarm-controls';
29
29
  import BudgetBar from './budget-bar';
30
30
  import BotSlotCard from './bot-slot-card';
31
- import TaskPoolList from './task-pool-list';
31
+ import TaskPoolList from './task-pool-list.js';
32
32
  import TaskDetailView from './task-detail-view';
33
33
  import TaskEditor from './task-editor';
34
34
  import ProfileCard from './profile-card';
35
35
  import ProfileEditor from './profile-editor';
36
36
  import DecisionLog from './decision-log';
37
+ import CapabilityEditor from './capability-editor';
38
+ import InstanceStreamView from './instance-stream-view';
37
39
  import { ICON_CATALOG, BOT_COLORS } from './bot-constants';
38
40
 
39
41
  // ---------------------------------------------------------------------------
@@ -118,12 +120,20 @@ function SwarmDashboard() {
118
120
 
119
121
  // --- Navigation state ---
120
122
  const [selectedTaskId, setSelectedTaskId] = useState<string | null>(null);
123
+ const [selectedInstance, setSelectedInstance] = useState<InstanceInfo | null>(null);
121
124
  const [activeTab, setActiveTab] = useState('tasks');
122
125
  const [editingBotId, setEditingBotId] = useState<string | null>(null);
123
126
  const [profileEditorMode, setProfileEditorMode] = useState<'create' | 'edit' | null>(null);
124
127
  const [editingProfileId, setEditingProfileId] = useState<string | null>(null);
128
+ const [taskView, setTaskView] = useState<'list' | 'board'>('list');
125
129
  const [taskEditorMode, setTaskEditorMode] = useState<'create' | 'edit' | null>(null);
126
130
  const [editingTaskId, setEditingTaskId] = useState<string | null>(null);
131
+ const [hintText, setHintText] = useState('');
132
+ const [hintSending, setHintSending] = useState(false);
133
+ const [costData, setCostData] = useState<Record<string, unknown> | null>(null);
134
+ const [localConcurrency, setLocalConcurrency] = useState<number | null>(null);
135
+ const [runHistory, setRunHistory] = useState<Array<Record<string, unknown>>>([]);
136
+ const concurrencyTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
127
137
 
128
138
  // --- Data state ---
129
139
  const [swarmStatus, setSwarmStatus] = useState<SwarmStatus | null>(null);
@@ -214,10 +224,28 @@ function SwarmDashboard() {
214
224
  } catch { /* non-fatal */ }
215
225
  }, [callTool]);
216
226
 
227
+ const fetchCosts = useCallback(async () => {
228
+ try {
229
+ const raw = await callTool('fw_weaver_costs', {});
230
+ if (!mountedRef.current) return;
231
+ const data = typeof raw === 'string' ? JSON.parse(raw) : raw;
232
+ if (data && typeof data === 'object') setCostData(data as Record<string, unknown>);
233
+ } catch { /* non-fatal */ }
234
+ }, [callTool]);
235
+
236
+ const fetchRunHistory = useCallback(async () => {
237
+ try {
238
+ const raw = await callTool('fw_weaver_history', { limit: 30 });
239
+ if (!mountedRef.current) return;
240
+ const data = typeof raw === 'string' ? JSON.parse(raw) : raw;
241
+ if (Array.isArray(data)) setRunHistory(data);
242
+ } catch { /* non-fatal */ }
243
+ }, [callTool]);
244
+
217
245
  const refreshAll = useCallback(async () => {
218
- await Promise.all([fetchSwarmStatus(), fetchTaskList(), fetchBots(), fetchConfig(), fetchProfiles(), fetchOrchestratorStatus()]);
246
+ await Promise.all([fetchSwarmStatus(), fetchTaskList(), fetchBots(), fetchConfig(), fetchProfiles(), fetchOrchestratorStatus(), fetchCosts(), fetchRunHistory()]);
219
247
  if (mountedRef.current) setLoading(false);
220
- }, [fetchSwarmStatus, fetchTaskList, fetchBots, fetchConfig, fetchProfiles, fetchOrchestratorStatus]);
248
+ }, [fetchSwarmStatus, fetchTaskList, fetchBots, fetchConfig, fetchProfiles, fetchOrchestratorStatus, fetchCosts, fetchRunHistory]);
221
249
 
222
250
  // Initial fetch on mount
223
251
  useEffect(() => {
@@ -286,6 +314,15 @@ function SwarmDashboard() {
286
314
  refreshAll();
287
315
  }, [refreshAll]);
288
316
 
317
+ const handleInstanceClick = useCallback((inst: InstanceInfo) => {
318
+ setSelectedInstance(inst);
319
+ }, []);
320
+
321
+ const handleInstanceBack = useCallback(() => {
322
+ setSelectedInstance(null);
323
+ refreshAll();
324
+ }, [refreshAll]);
325
+
289
326
  // --- Render: TaskEditor from detail view ---
290
327
 
291
328
  const handleEditFromDetail = useCallback((tid: string) => {
@@ -294,23 +331,47 @@ function SwarmDashboard() {
294
331
  }, []);
295
332
 
296
333
  if (selectedTaskId && taskEditorMode === 'edit' && editingTaskId) {
297
- return React.createElement(TaskEditor, {
298
- mode: 'edit',
299
- taskId: editingTaskId,
300
- onSave: () => { setTaskEditorMode(null); setEditingTaskId(null); },
301
- onCancel: () => { setTaskEditorMode(null); setEditingTaskId(null); },
302
- onDelete: () => { setTaskEditorMode(null); setEditingTaskId(null); setSelectedTaskId(null); fetchTaskList(); },
303
- });
334
+ return (
335
+ <TaskEditor
336
+ mode="edit"
337
+ taskId={editingTaskId}
338
+ onSave={() => { setTaskEditorMode(null); setEditingTaskId(null); }}
339
+ onCancel={() => { setTaskEditorMode(null); setEditingTaskId(null); }}
340
+ onDelete={() => { setTaskEditorMode(null); setEditingTaskId(null); setSelectedTaskId(null); fetchTaskList(); }}
341
+ />
342
+ );
343
+ }
344
+
345
+ // --- Render: InstanceStreamView (when instance is selected) ---
346
+
347
+ if (selectedInstance) {
348
+ const instProfile = profiles.find((p: Record<string, unknown>) => p.id === selectedInstance.profileId);
349
+ const instBotId = instProfile?.botId as string | undefined;
350
+ const instBot = registeredBots.find((b) => b.id === instBotId);
351
+ return (
352
+ <InstanceStreamView
353
+ instanceId={selectedInstance.instanceId}
354
+ profileId={selectedInstance.profileId}
355
+ profileName={(instProfile?.name as string) || selectedInstance.profileId}
356
+ profileIcon={instBot?.icon}
357
+ profileColor={instBot?.color}
358
+ currentRunId={selectedInstance.currentRunId}
359
+ currentTaskId={selectedInstance.currentTaskId}
360
+ onBack={handleInstanceBack}
361
+ />
362
+ );
304
363
  }
305
364
 
306
365
  // --- Render: TaskDetailView (when task is selected) ---
307
366
 
308
367
  if (selectedTaskId) {
309
- return React.createElement(TaskDetailView, {
310
- taskId: selectedTaskId,
311
- onBack: handleBack,
312
- onEdit: handleEditFromDetail,
313
- });
368
+ return (
369
+ <TaskDetailView
370
+ taskId={selectedTaskId}
371
+ onBack={handleBack}
372
+ onEdit={handleEditFromDetail}
373
+ />
374
+ );
314
375
  }
315
376
 
316
377
  // --- Render: Swarm Dashboard (default view) ---
@@ -323,458 +384,930 @@ function SwarmDashboard() {
323
384
  const hasBudget = !!(sessionBudget && (sessionBudget.limitTokens > 0 || sessionBudget.limitCost > 0));
324
385
  const hasContent = hasSwarmInstances || hasRegisteredBots || tasks.length > 0 || swarmStatus !== null;
325
386
 
326
- return React.createElement(Flex, {
327
- variant: 'column-stretch-start-nowrap-0',
328
- style: { width: '100%', height: '100%', overflow: 'hidden' },
329
- },
330
- // ── SwarmControls (top bar) ──────────────────────────────────
331
- React.createElement(SwarmControls, {
332
- swarmStatus,
333
- onRefresh: refreshAll,
334
- }),
335
-
336
- // ── BudgetBar (below controls) ──────────────────────────────
337
- hasBudget && React.createElement(Flex, {
338
- variant: 'column-stretch-start-nowrap-4',
339
- style: { padding: '8px 16px', flexShrink: 0, borderBottom: '1px solid var(--color-border-default)' },
340
- },
341
- React.createElement(BudgetBar, {
342
- label: 'Tokens',
343
- used: sessionBudget!.usedTokens,
344
- limit: sessionBudget!.limitTokens,
345
- unit: 'tokens',
346
- }),
347
- React.createElement(BudgetBar, {
348
- label: 'Cost',
349
- used: sessionBudget!.usedCost,
350
- limit: sessionBudget!.limitCost,
351
- unit: 'USD',
352
- }),
353
- ),
354
-
355
- // ── Bot slot cards (horizontal scrollable row) ──────────────
356
- // ── Tabs ──────────────────────────────────────────────────────
357
- React.createElement(Tabs, {
358
- tabs: [
359
- { id: 'tasks', title: `Tasks (${tasks.length})` },
360
- { id: 'bots', title: hasSwarmInstances ? `Workers (${swarmInstanceEntries.length})` : `Bots (${registeredBots.length})` },
361
- { id: 'profiles', title: `Profiles (${profiles.length})` },
362
- { id: 'config', title: 'Config' },
363
- ],
364
- activeTabId: activeTab,
365
- onSelectTab: (id: string) => setActiveTab(id),
366
- size: 'sm',
367
- }),
368
-
369
- // ── Tab content ──────────────────────────────────────────────
370
- React.createElement(Flex, {
371
- variant: 'column-stretch-start-nowrap-0',
372
- style: { flex: 1, minHeight: 0, overflow: 'auto' },
373
- },
374
-
375
- // Tasks tab — when TaskEditor is open, render it INSTEAD of the list
376
- activeTab === 'tasks' && taskEditorMode != null && React.createElement(TaskEditor, {
377
- mode: taskEditorMode,
378
- taskId: editingTaskId ?? undefined,
379
- onSave: () => { setTaskEditorMode(null); setEditingTaskId(null); fetchTaskList(); },
380
- onCancel: () => { setTaskEditorMode(null); setEditingTaskId(null); },
381
- onDelete: taskEditorMode === 'edit'
382
- ? () => { setTaskEditorMode(null); setEditingTaskId(null); fetchTaskList(); }
383
- : undefined,
384
- }),
385
-
386
- // Tasks tab — default list view
387
- activeTab === 'tasks' && taskEditorMode == null && React.createElement(Flex, {
388
- variant: 'column-stretch-start-nowrap-0',
389
- style: { flex: 1, minHeight: 0 },
390
- },
391
- !hasContent && !loading && React.createElement(EmptyState, {
392
- icon: 'smartToy',
393
- message: 'No tasks yet',
394
- description: 'Create tasks below or ask the AI assistant to create them for you.',
395
- }),
396
-
397
- hasContent && React.createElement(TaskPoolList, {
398
- tasks,
399
- onTaskClick: handleTaskClick,
400
- }),
401
-
402
- // Task clear buttons (only shown when tasks exist)
403
- tasks.length > 0 && React.createElement(Flex, {
404
- variant: 'row-center-start-nowrap-8',
405
- style: { padding: '4px 16px', flexShrink: 0 },
406
- },
407
- React.createElement(Button, {
408
- size: 'xs', variant: 'outlined', color: 'secondary',
409
- leftIcon: 'remove',
410
- onClick: async () => {
411
- const ok = await ctx.confirm('Clear all completed, failed, and cancelled tasks?', {
412
- title: 'Clear Completed Tasks',
413
- confirmLabel: 'Clear',
414
- state: 'warning',
415
- });
416
- if (!ok) return;
417
- try {
418
- const raw = await callTool('fw_weaver_tasks_clear', { filter: 'completed' });
419
- const data = parseToolResult(raw) as { cleared?: number } | null;
420
- toast(`Cleared ${data?.cleared ?? 0} tasks`, { type: 'success' });
421
- fetchTaskList();
422
- } catch (err: unknown) {
423
- toast(err instanceof Error ? err.message : 'Failed to clear tasks', { type: 'error' });
424
- }
425
- },
426
- }, 'Clear Completed'),
427
- React.createElement(Button, {
428
- size: 'xs', variant: 'outlined', color: 'danger',
429
- leftIcon: 'deleteForever',
430
- onClick: async () => {
431
- const ok = await ctx.confirm('This will stop the swarm and remove ALL tasks. This cannot be undone.', {
432
- title: 'Clear All Tasks',
433
- confirmLabel: 'Clear All',
434
- state: 'danger',
435
- });
436
- if (!ok) return;
437
- try {
438
- const raw = await callTool('fw_weaver_tasks_clear', { filter: 'all' });
439
- const data = parseToolResult(raw) as { cleared?: number } | null;
440
- toast(`Cleared ${data?.cleared ?? 0} tasks`, { type: 'success' });
441
- refreshAll();
442
- } catch (err: unknown) {
443
- toast(err instanceof Error ? err.message : 'Failed to clear tasks', { type: 'error' });
444
- }
445
- },
446
- }, 'Clear All'),
447
- ),
448
-
449
- // New Task button (bottom bar)
450
- React.createElement(Flex, {
451
- variant: 'column-stretch-start-nowrap-0',
452
- style: { flexShrink: 0, borderTop: '1px solid var(--color-border-default)', padding: '8px 16px' },
453
- },
454
- React.createElement(Button, {
455
- size: 'xs', variant: 'clear', color: 'primary',
456
- leftIcon: 'add',
457
- onClick: () => setTaskEditorMode('create'),
458
- }, 'New Task'),
459
- ),
460
- ),
461
-
462
- // Bots tab
463
- activeTab === 'bots' && React.createElement(Flex, {
464
- variant: 'column-stretch-start-nowrap-0',
465
- style: { flex: 1, minHeight: 0 },
466
- },
467
- // When swarm is running: show swarm instances
468
- hasSwarmInstances && React.createElement(Flex, {
469
- variant: 'column-stretch-start-nowrap-0',
470
- },
471
- React.createElement(Flex, {
472
- variant: 'row-center-start-nowrap-8',
473
- style: { padding: '8px 16px', borderBottom: '1px solid var(--color-border-default)' },
474
- },
475
- React.createElement(Typography, {
476
- variant: 'smallCaption-regular', color: 'color-text-subtle',
477
- style: { width: '120px', flexShrink: 0 },
478
- }, 'Worker'),
479
- React.createElement(Typography, {
480
- variant: 'smallCaption-regular', color: 'color-text-subtle',
481
- style: { width: '110px', flexShrink: 0 },
482
- }, 'Bot'),
483
- React.createElement(Typography, {
484
- variant: 'smallCaption-regular', color: 'color-text-subtle',
485
- style: { width: '70px', flexShrink: 0 },
486
- }, 'Status'),
487
- React.createElement(Typography, {
488
- variant: 'smallCaption-regular', color: 'color-text-subtle',
489
- style: { flex: 1, minWidth: 0 },
490
- }, 'Task'),
491
- React.createElement(Typography, {
492
- variant: 'smallCaption-regular', color: 'color-text-subtle',
493
- style: { width: '50px', flexShrink: 0, textAlign: 'right' },
494
- }, 'Tokens'),
495
- React.createElement(Typography, {
496
- variant: 'smallCaption-regular', color: 'color-text-subtle',
497
- style: { width: '50px', flexShrink: 0, textAlign: 'right' },
498
- }, 'Cost'),
499
- React.createElement(Typography, {
500
- variant: 'smallCaption-regular', color: 'color-text-subtle',
501
- style: { width: '50px', flexShrink: 0 },
502
- }, ''),
503
- ),
504
- // Instance rows
505
- ...swarmInstanceEntries.map((inst: InstanceInfo) => {
506
- // Look up profile → bot for this instance
507
- const profile = profiles.find((p: Record<string, unknown>) => p.id === inst.profileId);
508
- const botId = profile?.botId as string | undefined;
509
- const bot = registeredBots.find((b) => b.id === botId);
510
- return React.createElement(BotSlotCard, {
511
- key: inst.instanceId,
512
- bot: {
513
- botId: inst.instanceId,
514
- botName: inst.profileId ? `${inst.instanceId} (${inst.profileId})` : inst.instanceId,
515
- status: inst.status,
516
- currentTaskId: inst.currentTaskId,
517
- currentRunId: inst.currentRunId,
518
- startedAt: inst.startedAt,
519
- tokensUsed: inst.tokensUsed,
520
- cost: inst.cost,
521
- },
522
- profileName: (profile?.name as string) || inst.profileId,
523
- botDisplayName: bot?.name,
524
- botIcon: bot?.icon,
525
- botColor: bot?.color,
526
- currentTaskTitle: resolveTaskTitle(inst.currentTaskId, tasks),
527
- onPause: (id: string) => handleSteerBot(id, 'pause'),
528
- onResume: (id: string) => handleSteerBot(id, 'resume'),
529
- onStop: (id: string) => handleSteerBot(id, 'cancel'),
530
- });
531
- }),
532
- ),
533
-
534
- // When swarm is NOT running: show registered bots
535
- !hasSwarmInstances && hasRegisteredBots && React.createElement(Flex, {
536
- variant: 'column-stretch-start-nowrap-0',
537
- style: { padding: '8px 16px' },
538
- },
539
- ...registeredBots.map((bot) => {
540
- const isEditing = editingBotId === bot.id;
541
- return React.createElement(Flex, {
542
- key: bot.id,
543
- variant: 'column-stretch-start-nowrap-0',
544
- style: { borderBottom: '1px solid var(--color-border-default)' },
545
- },
546
- // Bot row (clickable to toggle edit)
547
- React.createElement(Flex, {
548
- variant: 'row-center-start-nowrap-8',
549
- style: { padding: '6px 0', cursor: 'pointer' },
550
- onClick: () => setEditingBotId(isEditing ? null : bot.id),
551
- },
552
- React.createElement(Icon, {
553
- name: bot.icon || 'smartToy', size: 16,
554
- color: bot.color || 'color-text-medium',
555
- }),
556
- React.createElement(Flex, { variant: 'column-start-start-nowrap-1', style: { flex: 1, minWidth: 0 } },
557
- React.createElement(Typography, { variant: 'smallCaption-regular', color: 'color-text-high' }, bot.name),
558
- bot.description && React.createElement(Typography, { variant: 'smallCaption-regular', color: 'color-text-subtle' }, bot.description),
559
- ),
560
- React.createElement(Icon, { name: isEditing ? 'expandLess' : 'expandMore', size: 14, color: 'color-text-subtle' }),
561
- ),
562
-
563
- // Edit section (icon + color pickers)
564
- isEditing && React.createElement(Flex, {
565
- variant: 'column-stretch-start-nowrap-12',
566
- style: { padding: '8px 0 12px 24px' },
567
- },
568
- React.createElement(IconPicker, {
569
- catalog: ICON_CATALOG,
570
- value: bot.icon || 'smartToy',
571
- onChange: (icon: string) => handleUpdateBot(bot.id, { icon }),
572
- accentColor: bot.color || undefined,
573
- defaultExpanded: true,
574
- }),
575
- React.createElement(ColorPicker, {
576
- colors: BOT_COLORS,
577
- value: bot.color || '',
578
- onChange: (color: string) => handleUpdateBot(bot.id, { color }),
579
- defaultExpanded: true,
580
- }),
581
- ),
582
- );
583
- }),
584
- ),
585
-
586
- // No bots at all
587
- !hasSwarmInstances && !hasRegisteredBots && React.createElement(EmptyState, {
588
- icon: 'smartToy',
589
- message: 'No bots registered',
590
- description: 'Register bots to start the swarm.',
591
- }),
592
- ),
593
-
594
- // Profiles tab — when ProfileEditor is open, render it INSTEAD of the list
595
- activeTab === 'profiles' && profileEditorMode != null && React.createElement(ProfileEditor, {
596
- mode: profileEditorMode,
597
- profileId: editingProfileId ?? undefined,
598
- bots: registeredBots.map((b: { id: string; name: string; icon?: string; color?: string }) => ({ id: b.id, name: b.name, icon: b.icon, color: b.color })),
599
- onSave: () => { setProfileEditorMode(null); setEditingProfileId(null); fetchProfiles(); },
600
- onCancel: () => { setProfileEditorMode(null); setEditingProfileId(null); },
601
- onDelete: profileEditorMode === 'edit'
602
- ? () => { setProfileEditorMode(null); setEditingProfileId(null); fetchProfiles(); }
603
- : undefined,
604
- }),
605
-
606
- // Profiles tab — default list view
607
- activeTab === 'profiles' && profileEditorMode == null && React.createElement(Flex, {
608
- variant: 'column-stretch-start-nowrap-0',
609
- style: { flex: 1, minHeight: 0 },
610
- },
611
- // ── Profile cards (scrollable) ──
612
- React.createElement(Flex, {
613
- variant: 'column-stretch-start-nowrap-8',
614
- style: { flex: 1, minHeight: 0, overflow: 'auto', padding: '12px 16px' },
615
- },
616
- profiles.length > 0
617
- ? React.createElement(Flex, { variant: 'column-stretch-start-nowrap-8' },
618
- ...profiles.map((p: Record<string, unknown>) => {
619
- const activeCount = swarmInstanceEntries.filter(
620
- (inst: InstanceInfo) => inst.profileId === p.id && inst.status === 'executing',
621
- ).length;
622
- return React.createElement(ProfileCard, {
623
- key: p.id as string,
624
- profile: p as unknown,
625
- activeInstances: activeCount,
626
- onEdit: (id: string) => { setEditingProfileId(id); setProfileEditorMode('edit'); },
627
- onDelete: async (id: string) => {
628
- const ok = await ctx.confirm('Are you sure you want to delete this profile?', {
629
- title: 'Delete Profile',
630
- confirmLabel: 'Delete',
631
- state: 'danger',
632
- });
633
- if (!ok) return;
634
- try {
635
- await callTool('fw_weaver_profile_delete', { id });
636
- await fetchProfiles();
637
- toast('Profile deleted', { type: 'success' });
638
- } catch (err: unknown) {
639
- toast(err instanceof Error ? err.message : 'Failed to delete profile', { type: 'error' });
640
- }
641
- },
642
- });
643
- }),
644
- )
645
- : React.createElement(EmptyState, {
646
- icon: 'person',
647
- message: 'No profiles',
648
- description: 'Profiles define how bots behave. Start the swarm to create defaults, or create one below.',
649
- }),
650
-
651
- // ── Routing / Decision Log (inside scrollable area) ──
652
- React.createElement(DecisionLog, {
653
- decisions: orchestratorDecisions as unknown[],
654
- }),
655
- ),
656
-
657
- // ── New Profile button (bottom bar) ──
658
- React.createElement(Flex, {
659
- variant: 'column-stretch-start-nowrap-0',
660
- style: { flexShrink: 0, borderTop: '1px solid var(--color-border-default)', padding: '8px 16px' },
661
- },
662
- React.createElement(Button, {
663
- size: 'xs', variant: 'clear', color: 'primary',
664
- leftIcon: 'add',
665
- onClick: () => setProfileEditorMode('create'),
666
- }, 'New Profile'),
667
- ),
668
-
669
- ),
670
-
671
- // Config tab
672
- activeTab === 'config' && React.createElement(Flex, {
673
- variant: 'column-stretch-start-nowrap-12',
674
- style: { padding: '12px 16px' },
675
- },
676
- // ── Provider ──
677
- React.createElement(Flex, { variant: 'row-center-space-between-nowrap-8' },
678
- React.createElement(Typography, { variant: 'smallCaption-regular', color: 'color-text-subtle' }, 'Provider'),
679
- React.createElement(Flex, { variant: 'row-center-start-nowrap-4' },
680
- React.createElement(Typography, { variant: 'smallCaption-regular', color: 'color-text-high' },
681
- providers.find((p) => p.envVarsSet)?.name ?? 'None detected',
682
- ),
683
- providers.find((p) => p.envVarsSet) && React.createElement(Chip, { label: 'active', size: 'small', color: 'color-status-info' }),
684
- ),
685
- ),
686
-
687
- // Trust
688
- insights?.trust && React.createElement(Flex, { variant: 'row-center-space-between-nowrap-8' },
689
- React.createElement(Typography, { variant: 'smallCaption-regular', color: 'color-text-subtle' }, 'Trust'),
690
- React.createElement(Typography, { variant: 'smallCaption-regular', color: 'color-text-high' },
691
- `Phase ${(insights.trust as Record<string, unknown>).phase} · ${(insights.trust as Record<string, unknown>).score}/100`,
692
- ),
693
- ),
694
-
695
- // Health
696
- insights?.health && React.createElement(Flex, { variant: 'row-center-space-between-nowrap-8' },
697
- React.createElement(Typography, { variant: 'smallCaption-regular', color: 'color-text-subtle' }, 'Health'),
698
- React.createElement(Typography, { variant: 'smallCaption-regular', color: 'color-text-high' },
699
- `${(insights.health as Record<string, unknown>).overall}/100`,
700
- ),
701
- ),
702
-
703
- // Available providers
704
- providers.length > 0 && React.createElement(Flex, { variant: 'column-stretch-start-nowrap-4' },
705
- React.createElement(Typography, { variant: 'smallCaption-regular', color: 'color-text-subtle' }, 'Available Providers'),
706
- React.createElement(Flex, { variant: 'row-center-start-wrap-4' },
707
- ...providers.map((p) =>
708
- React.createElement(Chip, {
709
- key: p.name,
710
- label: p.name,
711
- size: 'small',
712
- color: p.envVarsSet ? 'color-status-positive' : 'color-brand-alt',
713
- }),
714
- ),
715
- ),
716
- ),
717
-
718
- // ── Reset section ──
719
- React.createElement(Flex, { variant: 'column-stretch-start-nowrap-8', style: { marginTop: 16, paddingTop: 12, borderTop: '1px solid var(--color-border-default)' } },
720
- React.createElement(SectionTitle, { size: 'xs' }, 'Reset'),
721
- React.createElement(Flex, { variant: 'row-center-start-nowrap-8' },
722
- React.createElement(Button, {
723
- size: 'xs', variant: 'outlined', color: 'secondary',
724
- leftIcon: 'undo',
725
- onClick: async () => {
726
- const ok = await ctx.confirm('Reset Weaver configuration to defaults? Your .weaver.json will be deleted.', {
727
- title: 'Reset Config',
728
- confirmLabel: 'Reset',
729
- state: 'warning',
730
- });
731
- if (!ok) return;
387
+ return (
388
+ <Flex
389
+ variant="column-stretch-start-nowrap-0"
390
+ style={{ width: '100%', height: '100%', overflow: 'hidden' }}
391
+ >
392
+ {/* SwarmControls (top bar) */}
393
+ <SwarmControls
394
+ swarmStatus={swarmStatus}
395
+ onRefresh={refreshAll}
396
+ />
397
+
398
+ {/* BudgetBar (below controls) */}
399
+ {hasBudget && (
400
+ <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) => {
732
408
  try {
733
- await callTool('fw_weaver_config_reset', {});
734
- toast('Config reset to defaults', { type: 'success' });
735
- refreshAll();
409
+ await callTool('fw_weaver_swarm_config', { sessionBudgetTokens: newLimit });
410
+ toast(`Token budget updated to ${newLimit.toLocaleString()}`, { type: 'success' });
411
+ fetchSwarmStatus();
736
412
  } catch (err: unknown) {
737
- toast(err instanceof Error ? err.message : 'Failed to reset config', { type: 'error' });
413
+ toast(err instanceof Error ? err.message : 'Failed to update budget', { type: 'error' });
738
414
  }
739
- },
740
- }, 'Reset Config'),
741
- React.createElement(Button, {
742
- size: 'xs', variant: 'outlined', color: 'danger',
743
- leftIcon: 'warning',
744
- onClick: async () => {
745
- const ok = await ctx.confirm(
746
- 'This will stop the swarm, delete ALL tasks, profiles, run history, and configuration. This cannot be undone.',
747
- {
748
- title: 'Reset Everything',
749
- confirmLabel: 'Reset Everything',
750
- state: 'danger',
751
- },
752
- );
753
- if (!ok) return;
415
+ }}
416
+ />
417
+ <BudgetBar
418
+ label="Cost"
419
+ used={sessionBudget!.usedCost}
420
+ limit={sessionBudget!.limitCost}
421
+ unit="USD"
422
+ onLimitChange={async (newLimit: number) => {
754
423
  try {
755
- const raw = await callTool('fw_weaver_reset_all', {});
756
- const data = parseToolResult(raw) as { tasksCleared?: number; profilesCleared?: number } | null;
757
- toast(`Full reset complete — ${data?.tasksCleared ?? 0} tasks, ${data?.profilesCleared ?? 0} profiles cleared`, { type: 'success' });
758
- refreshAll();
424
+ await callTool('fw_weaver_swarm_config', { sessionBudgetCost: newLimit });
425
+ toast(`Cost budget updated to $${newLimit.toFixed(2)}`, { type: 'success' });
426
+ fetchSwarmStatus();
759
427
  } catch (err: unknown) {
760
- toast(err instanceof Error ? err.message : 'Failed to reset', { type: 'error' });
428
+ toast(err instanceof Error ? err.message : 'Failed to update budget', { type: 'error' });
761
429
  }
762
- },
763
- }, 'Reset Everything'),
764
- ),
765
- ),
766
-
767
- // ── Pack version ──
768
- swarmStatus?.packVersion && React.createElement(Typography, {
769
- variant: 'smallCaption-regular', color: 'color-text-low',
770
- style: { textAlign: 'right', paddingTop: 8 },
771
- }, `Weaver v${swarmStatus.packVersion}`),
772
- ),
773
- ),
430
+ }}
431
+ />
432
+ </Flex>
433
+ </Section>
434
+ )}
435
+
436
+ {/* Tabs */}
437
+ <Tabs
438
+ tabs={[
439
+ { id: 'tasks', title: `Tasks (${tasks.length})` },
440
+ { id: 'control', title: 'Control' },
441
+ { id: 'bots', title: hasSwarmInstances ? `Workers (${swarmInstanceEntries.length})` : `Bots (${registeredBots.length})` },
442
+ { id: 'profiles', title: `Profiles (${profiles.length})` },
443
+ { id: 'capabilities', title: 'Capabilities' },
444
+ { id: 'costs', title: 'Costs' },
445
+ { id: 'config', title: 'Config' },
446
+ ]}
447
+ activeTabId={activeTab}
448
+ onSelectTab={(id: string) => setActiveTab(id)}
449
+ size="sm"
450
+ />
451
+
452
+ {/* Tab content */}
453
+ <Flex
454
+ variant="column-stretch-start-nowrap-0"
455
+ style={{ flex: 1, minHeight: 0, overflow: 'auto' }}
456
+ >
457
+
458
+ {/* Tasks tab — when TaskEditor is open, render it INSTEAD of the list */}
459
+ {activeTab === 'tasks' && taskEditorMode != null && (
460
+ <TaskEditor
461
+ mode={taskEditorMode}
462
+ taskId={editingTaskId ?? undefined}
463
+ onSave={() => { setTaskEditorMode(null); setEditingTaskId(null); fetchTaskList(); }}
464
+ onCancel={() => { setTaskEditorMode(null); setEditingTaskId(null); }}
465
+ onDelete={taskEditorMode === 'edit'
466
+ ? () => { setTaskEditorMode(null); setEditingTaskId(null); fetchTaskList(); }
467
+ : undefined}
468
+ />
469
+ )}
470
+
471
+ {/* Tasks tab — default view (list or board) */}
472
+ {activeTab === 'tasks' && taskEditorMode == null && (
473
+ <Flex
474
+ variant="column-stretch-start-nowrap-0"
475
+ style={{ flex: 1, minHeight: 0 }}
476
+ >
477
+ {/* View toggle + actions bar */}
478
+ <Section padding="compact" border="bottom" shrink>
479
+ <Flex variant="row-center-space-between-nowrap-8">
480
+ <Flex variant="row-center-start-nowrap-4">
481
+ <IconButton
482
+ icon="list"
483
+ size="xs"
484
+ variant={taskView === 'list' ? 'outlined' : 'clear'}
485
+ onClick={() => setTaskView('list')}
486
+ title="List view"
487
+ />
488
+ <IconButton
489
+ icon="dashboard"
490
+ size="xs"
491
+ variant={taskView === 'board' ? 'outlined' : 'clear'}
492
+ onClick={() => setTaskView('board')}
493
+ title="Board view"
494
+ />
495
+ </Flex>
496
+ <Flex variant="row-center-start-nowrap-4">
497
+ {tasks.length > 0 && (
498
+ <>
499
+ <Button size="xs" variant="clear" color="secondary" leftIcon="remove"
500
+ onClick={async () => {
501
+ const ok = await ctx.confirm('Clear all completed, failed, and cancelled tasks?', {
502
+ title: 'Clear Completed Tasks', confirmLabel: 'Clear', state: 'warning',
503
+ });
504
+ if (!ok) return;
505
+ try {
506
+ const raw = await callTool('fw_weaver_tasks_clear', { filter: 'completed' });
507
+ const data = parseToolResult(raw) as { cleared?: number } | null;
508
+ toast(`Cleared ${data?.cleared ?? 0} tasks`, { type: 'success' });
509
+ fetchTaskList();
510
+ } catch (err: unknown) {
511
+ toast(err instanceof Error ? err.message : 'Failed to clear tasks', { type: 'error' });
512
+ }
513
+ }}
514
+ >Clear Done</Button>
515
+ </>
516
+ )}
517
+ <Button size="xs" variant="clear" color="primary" leftIcon="add"
518
+ onClick={() => setTaskEditorMode('create')}
519
+ >New Task</Button>
520
+ </Flex>
521
+ </Flex>
522
+ </Section>
523
+
524
+ {/* Empty state */}
525
+ {!hasContent && !loading && (
526
+ <EmptyState
527
+ icon="smartToy"
528
+ message="No tasks yet"
529
+ description="Create tasks below or ask the AI assistant to create them for you."
530
+ />
531
+ )}
532
+
533
+ {/* List view */}
534
+ {hasContent && taskView === 'list' && (
535
+ <TaskPoolList tasks={tasks} onTaskClick={handleTaskClick} />
536
+ )}
537
+
538
+ {/* Board view */}
539
+ {hasContent && taskView === 'board' && (() => {
540
+ // Split open tasks into ready vs blocked
541
+ const allTasks = tasks as Array<Record<string, unknown>>;
542
+ const doneIds = new Set(allTasks.filter(t => t.status === 'done' || t.status === 'cancelled').map(t => t.id as string));
543
+
544
+ const kanbanColumns = [
545
+ { 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)' },
550
+ ];
551
+
552
+ const kanbanCards = allTasks
553
+ .filter(t => !t.isParent) // Only leaf tasks on the board
554
+ .map(t => {
555
+ const deps = (t.dependsOn as string[]) ?? [];
556
+ const unresolvedDeps = deps.filter(d => !doneIds.has(d));
557
+ const subtasks = allTasks.filter(st => st.parentId === t.id);
558
+ const subtasksDone = subtasks.filter(st => st.status === 'done').length;
559
+ const profileName = profiles.find((p: Record<string, unknown>) => p.id === t.assignedProfile)?.name as string | undefined;
560
+
561
+ // Open tasks with unresolved deps go to Waiting column
562
+ const columnId = t.status === 'open' && unresolvedDeps.length > 0
563
+ ? 'waiting'
564
+ : t.status as string;
565
+
566
+ return {
567
+ id: t.id as string,
568
+ columnId,
569
+ content: (
570
+ <TaskCard
571
+ title={t.title as string}
572
+ priority={t.priority as number}
573
+ profile={profileName ?? (t.assignedProfile as string) ?? undefined}
574
+ complexity={t.complexity as string ?? undefined}
575
+ subtaskProgress={subtasks.length > 0 ? { done: subtasksDone, total: subtasks.length } : undefined}
576
+ cost={t.costUsed as number ?? undefined}
577
+ blockedBy={unresolvedDeps.length > 0 ? unresolvedDeps : undefined}
578
+ isActive={!!(t.activeRunId)}
579
+ onClick={() => handleTaskClick(t.id as string)}
580
+ />
581
+ ),
582
+ };
583
+ });
774
584
 
585
+ return (
586
+ <Flex variant="column-stretch-start-nowrap-0" style={{ flex: 1, minHeight: 0, padding: '8px' }}>
587
+ <KanbanBoard columns={kanbanColumns} cards={kanbanCards} emptyLabel="No tasks" />
588
+ </Flex>
589
+ );
590
+ })()}
591
+ </Flex>
592
+ )}
593
+
594
+ {/* Bots tab */}
595
+ {activeTab === 'bots' && (
596
+ <Flex
597
+ variant="column-stretch-start-nowrap-0"
598
+ style={{ flex: 1, minHeight: 0 }}
599
+ >
600
+ {/* When swarm is running: show swarm instances */}
601
+ {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
618
+ const profile = profiles.find((p: Record<string, unknown>) => p.id === inst.profileId);
619
+ const botId = profile?.botId as string | undefined;
620
+ 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
+ );
645
+ })}
646
+ </Flex>
647
+ )}
648
+
649
+ {/* When swarm is NOT running: show registered bots */}
650
+ {!hasSwarmInstances && hasRegisteredBots && (
651
+ <Flex
652
+ variant="column-stretch-start-nowrap-0"
653
+ style={{ padding: '8px 16px' }}
654
+ >
655
+ {registeredBots.map((bot) => {
656
+ const isEditing = editingBotId === bot.id;
657
+ return (
658
+ <Flex
659
+ key={bot.id}
660
+ variant="column-stretch-start-nowrap-0"
661
+ style={{ borderBottom: '1px solid var(--color-border-default)' }}
662
+ >
663
+ {/* Bot row (clickable to toggle edit) */}
664
+ <Flex
665
+ variant="row-center-start-nowrap-8"
666
+ style={{ padding: '6px 0', cursor: 'pointer' }}
667
+ onClick={() => setEditingBotId(isEditing ? null : bot.id)}
668
+ >
669
+ <Icon
670
+ name={bot.icon || 'smartToy'}
671
+ size={16}
672
+ color={bot.color || 'color-text-medium'}
673
+ />
674
+ <Flex variant="column-start-start-nowrap-1" style={{ flex: 1, minWidth: 0 }}>
675
+ <Typography variant="smallCaption-regular" color="color-text-high">{bot.name}</Typography>
676
+ {bot.description && (
677
+ <Typography variant="smallCaption-regular" color="color-text-subtle">{bot.description}</Typography>
678
+ )}
679
+ </Flex>
680
+ <Icon name={isEditing ? 'expandLess' : 'expandMore'} size={14} color="color-text-subtle" />
681
+ </Flex>
682
+
683
+ {/* Edit section (icon + color pickers) */}
684
+ {isEditing && (
685
+ <Flex
686
+ variant="column-stretch-start-nowrap-12"
687
+ style={{ padding: '8px 0 12px 24px' }}
688
+ >
689
+ <IconPicker
690
+ catalog={ICON_CATALOG}
691
+ value={bot.icon || 'smartToy'}
692
+ onChange={(icon: string) => handleUpdateBot(bot.id, { icon })}
693
+ accentColor={bot.color || undefined}
694
+ defaultExpanded={true}
695
+ />
696
+ <ColorPicker
697
+ colors={BOT_COLORS}
698
+ value={bot.color || ''}
699
+ onChange={(color: string) => handleUpdateBot(bot.id, { color })}
700
+ defaultExpanded={true}
701
+ />
702
+
703
+ {/* Bot management actions */}
704
+ <Flex variant="row-center-start-nowrap-8">
705
+ <Button
706
+ size="xs"
707
+ variant="outlined"
708
+ color="secondary"
709
+ leftIcon="verified"
710
+ onClick={async () => {
711
+ try {
712
+ const raw = await callTool('fw_weaver_validate_bot', { id: bot.id });
713
+ const data = typeof raw === 'string' ? JSON.parse(raw) : raw;
714
+ if (data?.valid) {
715
+ toast(`${bot.name} is valid`, { type: 'success' });
716
+ } else {
717
+ toast(`Validation issues: ${JSON.stringify(data?.issues ?? data)}`, { type: 'warning' });
718
+ }
719
+ } catch (err: unknown) {
720
+ toast(err instanceof Error ? err.message : 'Validation failed', { type: 'error' });
721
+ }
722
+ }}
723
+ >Validate</Button>
724
+ <Button
725
+ size="xs"
726
+ variant="outlined"
727
+ color="danger"
728
+ leftIcon="deleteForever"
729
+ onClick={async () => {
730
+ const ok = await ctx.confirm(`Unregister bot "${bot.name}"? The workflow file will not be deleted.`, {
731
+ title: 'Unregister Bot',
732
+ confirmLabel: 'Unregister',
733
+ state: 'danger',
734
+ });
735
+ if (!ok) return;
736
+ try {
737
+ await callTool('fw_weaver_unregister_bot', { id: bot.id });
738
+ toast(`Bot "${bot.name}" unregistered`, { type: 'info' });
739
+ fetchBots();
740
+ } catch (err: unknown) {
741
+ toast(err instanceof Error ? err.message : 'Failed to unregister', { type: 'error' });
742
+ }
743
+ }}
744
+ >Unregister</Button>
745
+ </Flex>
746
+ </Flex>
747
+ )}
748
+ </Flex>
749
+ );
750
+ })}
751
+ </Flex>
752
+ )}
753
+
754
+ {/* No bots at all */}
755
+ {!hasSwarmInstances && !hasRegisteredBots && (
756
+ <EmptyState
757
+ icon="smartToy"
758
+ message="No bots registered"
759
+ description="Register bots to start the swarm."
760
+ />
761
+ )}
762
+ </Flex>
763
+ )}
764
+
765
+ {/* Profiles tab — when ProfileEditor is open, render it INSTEAD of the list */}
766
+ {activeTab === 'profiles' && profileEditorMode != null && (
767
+ <ProfileEditor
768
+ mode={profileEditorMode}
769
+ profileId={editingProfileId ?? undefined}
770
+ bots={registeredBots.map((b: { id: string; name: string; icon?: string; color?: string }) => ({ id: b.id, name: b.name, icon: b.icon, color: b.color }))}
771
+ onSave={() => { setProfileEditorMode(null); setEditingProfileId(null); fetchProfiles(); }}
772
+ onCancel={() => { setProfileEditorMode(null); setEditingProfileId(null); }}
773
+ onDelete={profileEditorMode === 'edit'
774
+ ? () => { setProfileEditorMode(null); setEditingProfileId(null); fetchProfiles(); }
775
+ : undefined}
776
+ />
777
+ )}
778
+
779
+ {/* Profiles tab — default list view */}
780
+ {activeTab === 'profiles' && profileEditorMode == null && (
781
+ <Flex
782
+ variant="column-stretch-start-nowrap-0"
783
+ style={{ flex: 1, minHeight: 0 }}
784
+ >
785
+ {/* Profile cards (scrollable) */}
786
+ <Flex
787
+ variant="column-stretch-start-nowrap-8"
788
+ style={{ flex: 1, minHeight: 0, overflow: 'auto', padding: '12px 16px' }}
789
+ >
790
+ {profiles.length > 0
791
+ ? (
792
+ <Flex variant="column-stretch-start-nowrap-8">
793
+ {profiles.map((p: Record<string, unknown>) => {
794
+ const activeCount = swarmInstanceEntries.filter(
795
+ (inst: InstanceInfo) => inst.profileId === p.id && inst.status === 'executing',
796
+ ).length;
797
+ return (
798
+ <ProfileCard
799
+ key={p.id as string}
800
+ profile={p as unknown}
801
+ activeInstances={activeCount}
802
+ onEdit={(id: string) => { setEditingProfileId(id); setProfileEditorMode('edit'); }}
803
+ onDelete={async (id: string) => {
804
+ const ok = await ctx.confirm('Are you sure you want to delete this profile?', {
805
+ title: 'Delete Profile',
806
+ confirmLabel: 'Delete',
807
+ state: 'danger',
808
+ });
809
+ if (!ok) return;
810
+ try {
811
+ await callTool('fw_weaver_profile_delete', { id });
812
+ await fetchProfiles();
813
+ toast('Profile deleted', { type: 'success' });
814
+ } catch (err: unknown) {
815
+ toast(err instanceof Error ? err.message : 'Failed to delete profile', { type: 'error' });
816
+ }
817
+ }}
818
+ />
819
+ );
820
+ })}
821
+ </Flex>
822
+ )
823
+ : (
824
+ <EmptyState
825
+ icon="person"
826
+ message="No profiles"
827
+ description="Profiles define how bots behave. Start the swarm to create defaults, or create one below."
828
+ />
829
+ )}
830
+ </Flex>
831
+
832
+ {/* New Profile button (bottom bar) */}
833
+ <Flex
834
+ variant="column-stretch-start-nowrap-0"
835
+ style={{ flexShrink: 0, borderTop: '1px solid var(--color-border-default)', padding: '8px 16px' }}
836
+ >
837
+ <Button
838
+ size="xs"
839
+ variant="clear"
840
+ color="primary"
841
+ leftIcon="add"
842
+ onClick={() => setProfileEditorMode('create')}
843
+ >New Profile</Button>
844
+ </Flex>
845
+ </Flex>
846
+ )}
847
+
848
+ {/* Capabilities tab */}
849
+ {activeTab === 'capabilities' && (
850
+ <CapabilityEditor
851
+ onSave={async (caps) => {
852
+ toast(`${caps.length} capabilities saved`, { type: 'success' });
853
+ }}
854
+ />
855
+ )}
856
+
857
+ {/* Costs tab */}
858
+ {activeTab === 'costs' && (() => {
859
+ // Build sparkline data from run history (cost over time, chronological)
860
+ const costSparkline = [...runHistory]
861
+ .reverse()
862
+ .filter((r) => typeof r.startedAt === 'string')
863
+ .map((r, i) => ({
864
+ x: i,
865
+ y: ((r.cost as Record<string, unknown>)?.totalCost as number) ?? (r.cost as number) ?? 0,
866
+ }));
867
+
868
+ // Build token sparkline
869
+ const tokenSparkline = [...runHistory]
870
+ .reverse()
871
+ .filter((r) => typeof r.startedAt === 'string')
872
+ .map((r, i) => ({
873
+ x: i,
874
+ y: ((r.cost as Record<string, unknown>)?.totalInputTokens as number) ??
875
+ ((r.cost as Record<string, unknown>)?.totalOutputTokens as number) ?? 0,
876
+ }));
877
+
878
+ // Build model breakdown for bar chart from costData
879
+ const costInsights = insights?.cost as Record<string, unknown> | undefined;
880
+ const byModel = costData?.byModel as Record<string, Record<string, unknown>> | undefined;
881
+ const modelBars = byModel
882
+ ? Object.entries(byModel).map(([model, data]) => ({
883
+ label: model.replace('claude-', '').replace(/-\d.*$/, ''),
884
+ value: (data.cost as number) ?? 0,
885
+ }))
886
+ : [];
887
+
888
+ const trend = (costInsights?.trend as string) === 'increasing' ? 'up' as const
889
+ : (costInsights?.trend as string) === 'decreasing' ? 'down' as const
890
+ : 'stable' as const;
891
+
892
+ return (
893
+ <Section padding="default">
894
+ <Flex variant="column-stretch-start-nowrap-16">
895
+
896
+ {/* Metric cards row */}
897
+ <Flex variant="row-stretch-start-nowrap-8">
898
+ <MetricCard
899
+ value={`$${(swarmStatus?.totalCost ?? 0).toFixed(2)}`}
900
+ label="Session Cost"
901
+ trend={trend}
902
+ trendLabel={(costInsights?.trend as string) ?? undefined}
903
+ sparklineData={costSparkline.length >= 2 ? costSparkline : undefined}
904
+ />
905
+ <MetricCard
906
+ value={(swarmStatus?.totalTokensUsed ?? 0).toLocaleString()}
907
+ label="Tokens Used"
908
+ sparklineData={tokenSparkline.length >= 2 ? tokenSparkline : undefined}
909
+ sparklineColor="var(--color-status-info)"
910
+ />
911
+ <MetricCard
912
+ value={String(swarmStatus?.tasksCompleted ?? 0)}
913
+ label="Tasks Completed"
914
+ />
915
+ <MetricCard
916
+ value={`$${((costInsights?.last7Days as number) ?? 0).toFixed(2)}`}
917
+ label="7-Day Cost"
918
+ trendLabel={(costInsights?.costPerSuccessfulRun as number)
919
+ ? `$${(costInsights.costPerSuccessfulRun as number).toFixed(3)}/run`
920
+ : undefined}
921
+ />
922
+ </Flex>
923
+
924
+ {/* Cost over time chart */}
925
+ {costSparkline.length >= 2 && (
926
+ <Flex variant="column-stretch-start-nowrap-4">
927
+ <SectionTitle size="xs">Cost Over Time</SectionTitle>
928
+ <SparklineChart
929
+ data={costSparkline}
930
+ height={80}
931
+ responsive
932
+ showArea
933
+ showDots
934
+ showYLabels
935
+ formatY={(v) => `$${v.toFixed(3)}`}
936
+ strokeWidth={2}
937
+ />
938
+ </Flex>
939
+ )}
940
+
941
+ {/* Cost by model bar chart */}
942
+ {modelBars.length > 0 && (
943
+ <Flex variant="column-stretch-start-nowrap-4">
944
+ <SectionTitle size="xs">Cost by Model</SectionTitle>
945
+ <BarChart
946
+ data={modelBars}
947
+ formatValue={(v) => `$${v.toFixed(3)}`}
948
+ />
949
+ </Flex>
950
+ )}
951
+
952
+ {/* High cost workflows */}
953
+ {(costInsights?.highCostWorkflows as Array<Record<string, unknown>> | undefined)?.length ? (
954
+ <Flex variant="column-stretch-start-nowrap-4">
955
+ <SectionTitle size="xs">High-Cost Workflows</SectionTitle>
956
+ <BarChart
957
+ data={(costInsights!.highCostWorkflows as Array<Record<string, unknown>>).map((w) => ({
958
+ label: String(w.workflow ?? '').replace(/\.ts$/, ''),
959
+ value: (w.avgCost as number) ?? 0,
960
+ color: 'var(--color-status-caution)',
961
+ }))}
962
+ formatValue={(v) => `$${v.toFixed(3)}`}
963
+ />
964
+ </Flex>
965
+ ) : null}
966
+
967
+ {/* Detailed breakdown table */}
968
+ {runHistory.length > 0 && (
969
+ <Flex variant="column-stretch-start-nowrap-4">
970
+ <SectionTitle size="xs">Recent Runs</SectionTitle>
971
+ <Table
972
+ size="compact"
973
+ getRowKey={(row: Record<string, unknown>) => (row.id as string) ?? String(Math.random())}
974
+ onRowClick={(row: Record<string, unknown>) => {
975
+ if (row.taskId) handleTaskClick(row.taskId as string);
976
+ }}
977
+ columns={[
978
+ { key: 'outcome', header: '', width: '20px', render: (_: unknown, row: Record<string, unknown>) => (
979
+ <StatusIcon status={row.success ? 'completed' : 'failed'} size="xs" />
980
+ )},
981
+ { key: 'taskId', header: 'Task', render: (_: unknown, row: Record<string, unknown>) => (
982
+ <Typography variant="smallCaption-regular" color="color-text-high" truncate>
983
+ {(row.taskId as string) ?? '-'}
984
+ </Typography>
985
+ )},
986
+ { key: 'model', header: 'Model', width: '80px', render: (_: unknown, row: Record<string, unknown>) => {
987
+ const costObj = row.cost as Record<string, unknown> | undefined;
988
+ return (
989
+ <Typography variant="smallCaption-regular" color="color-text-medium" mono>
990
+ {((costObj?.model as string) ?? '').replace('claude-', '') || '-'}
991
+ </Typography>
992
+ );
993
+ }},
994
+ { key: 'cost', header: 'Cost', width: '65px', align: 'right' as const, render: (_: unknown, row: Record<string, unknown>) => {
995
+ const costObj = row.cost as Record<string, unknown> | undefined;
996
+ const val = (costObj?.totalCost as number) ?? 0;
997
+ return (
998
+ <Typography variant="smallCaption-regular" color="color-text-high" mono>
999
+ {`$${val.toFixed(3)}`}
1000
+ </Typography>
1001
+ );
1002
+ }},
1003
+ ]}
1004
+ data={runHistory as Array<Record<string, unknown>>}
1005
+ maxHeight="200px"
1006
+ />
1007
+ </Flex>
1008
+ )}
1009
+
1010
+ {/* Empty state */}
1011
+ {!swarmStatus && !costData && runHistory.length === 0 && (
1012
+ <EmptyState icon="payments" message="No cost data yet" />
1013
+ )}
1014
+ </Flex>
1015
+ </Section>
1016
+ );
1017
+ })()}
1018
+
1019
+ {/* Control tab — active swarm management */}
1020
+ {activeTab === 'control' && (
1021
+ <Section padding="default">
1022
+ <Flex variant="column-stretch-start-nowrap-16">
1023
+
1024
+ {/* Orchestrator Guidance */}
1025
+ <Flex variant="column-stretch-start-nowrap-8">
1026
+ <SectionTitle size="xs">Orchestrator Guidance</SectionTitle>
1027
+ <Typography variant="smallCaption-regular" color="color-text-subtle">
1028
+ Influence how the orchestrator routes tasks to profiles. Hints persist across dispatch cycles.
1029
+ </Typography>
1030
+ <Input
1031
+ type="text"
1032
+ value={hintText}
1033
+ onChange={(value: string) => setHintText(value)}
1034
+ onKeyDown={(e: { key: string }) => {
1035
+ if (e.key === 'Enter' && hintText.trim()) {
1036
+ setHintSending(true);
1037
+ callTool('fw_weaver_orchestrator_hint', { hint: hintText.trim() })
1038
+ .then(() => { toast('Hint sent to orchestrator', { type: 'success' }); setHintText(''); })
1039
+ .catch((err: Error) => toast(err.message, { type: 'error' }))
1040
+ .finally(() => setHintSending(false));
1041
+ }
1042
+ }}
1043
+ placeholder='e.g. "Prioritize security tasks" or "Use architect for auth changes"'
1044
+ disabled={hintSending}
1045
+ inputBoxStyle={{ maxWidth: 'none' }}
1046
+ />
1047
+ <Flex variant="row-center-end-nowrap-0">
1048
+ <Button
1049
+ size="xs"
1050
+ variant="outlined"
1051
+ color="primary"
1052
+ onClick={() => {
1053
+ if (!hintText.trim()) return;
1054
+ setHintSending(true);
1055
+ callTool('fw_weaver_orchestrator_hint', { hint: hintText.trim() })
1056
+ .then(() => { toast('Hint sent to orchestrator', { type: 'success' }); setHintText(''); })
1057
+ .catch((err: Error) => toast(err.message, { type: 'error' }))
1058
+ .finally(() => setHintSending(false));
1059
+ }}
1060
+ loading={hintSending}
1061
+ disabled={!hintText.trim() || hintSending}
1062
+ >Send</Button>
1063
+ </Flex>
1064
+ </Flex>
1065
+
1066
+ {/* Concurrency & Budgets */}
1067
+ <Flex variant="column-stretch-start-nowrap-8">
1068
+ <SectionTitle size="xs">Concurrency &amp; Budgets</SectionTitle>
1069
+ <Flex variant="row-center-start-nowrap-12">
1070
+ <Typography variant="smallCaption-regular" color="color-text-subtle">Max Concurrent</Typography>
1071
+ <Slider
1072
+ value={localConcurrency ?? swarmStatus?.maxConcurrent ?? 5}
1073
+ min={1}
1074
+ max={20}
1075
+ step={1}
1076
+ onChange={(val: number) => {
1077
+ setLocalConcurrency(val);
1078
+ if (concurrencyTimerRef.current) clearTimeout(concurrencyTimerRef.current);
1079
+ concurrencyTimerRef.current = setTimeout(async () => {
1080
+ try {
1081
+ await callTool('fw_weaver_swarm_config', { maxConcurrent: val });
1082
+ fetchSwarmStatus();
1083
+ } catch { /* non-fatal */ }
1084
+ setLocalConcurrency(null);
1085
+ }, 500);
1086
+ }}
1087
+ />
1088
+ <Typography variant="caption-thick" color="color-text-high">
1089
+ {localConcurrency ?? swarmStatus?.maxConcurrent ?? 5}
1090
+ </Typography>
1091
+ </Flex>
1092
+ <Flex variant="row-center-start-nowrap-16">
1093
+ {sessionBudget && (
1094
+ <Field label="Token Limit" labelWidth={110}>
1095
+ <Input
1096
+ type="number"
1097
+ value={String(sessionBudget.limitTokens)}
1098
+ size="small"
1099
+ onChange={() => {}}
1100
+ onBlur={async (e: { target: { value: string } }) => {
1101
+ const val = parseInt(e.target.value, 10);
1102
+ if (!val || val < 1) return;
1103
+ try {
1104
+ await callTool('fw_weaver_swarm_config', { sessionBudgetTokens: val });
1105
+ toast(`Token limit: ${val.toLocaleString()}`, { type: 'success' });
1106
+ fetchSwarmStatus();
1107
+ } catch (err: unknown) {
1108
+ toast(err instanceof Error ? err.message : 'Failed', { type: 'error' });
1109
+ }
1110
+ }}
1111
+ />
1112
+ </Field>
1113
+ )}
1114
+ {sessionBudget && (
1115
+ <Field label="Cost Limit ($)" labelWidth={110}>
1116
+ <Input
1117
+ type="number"
1118
+ value={String(sessionBudget.limitCost)}
1119
+ size="small"
1120
+ onChange={() => {}}
1121
+ onBlur={async (e: { target: { value: string } }) => {
1122
+ const val = parseFloat(e.target.value);
1123
+ if (!val || val <= 0) return;
1124
+ try {
1125
+ await callTool('fw_weaver_swarm_config', { sessionBudgetCost: val });
1126
+ toast(`Cost limit: $${val.toFixed(2)}`, { type: 'success' });
1127
+ fetchSwarmStatus();
1128
+ } catch (err: unknown) {
1129
+ toast(err instanceof Error ? err.message : 'Failed', { type: 'error' });
1130
+ }
1131
+ }}
1132
+ />
1133
+ </Field>
1134
+ )}
1135
+ </Flex>
1136
+ {swarmStatus && (
1137
+ <Flex variant="row-center-start-nowrap-16">
1138
+ <Typography variant="smallCaption-regular" color="color-text-subtle">
1139
+ {`Tokens used: ${swarmStatus.totalTokensUsed.toLocaleString()}`}
1140
+ </Typography>
1141
+ <Typography variant="smallCaption-regular" color="color-text-subtle">
1142
+ {`Cost: $${swarmStatus.totalCost.toFixed(2)}`}
1143
+ </Typography>
1144
+ </Flex>
1145
+ )}
1146
+ </Flex>
1147
+
1148
+ {/* Routing Decisions */}
1149
+ <DecisionLog decisions={orchestratorDecisions as unknown[]} />
1150
+ </Flex>
1151
+ </Section>
1152
+ )}
1153
+
1154
+ {/* Config tab — static settings, read-only info, resets */}
1155
+ {activeTab === 'config' && (
1156
+ <Flex
1157
+ variant="column-stretch-start-nowrap-12"
1158
+ style={{ padding: '12px 16px' }}
1159
+ >
1160
+ {/* Provider */}
1161
+ <Flex variant="row-center-space-between-nowrap-8">
1162
+ <Typography variant="smallCaption-regular" color="color-text-subtle">Provider</Typography>
1163
+ <Flex variant="row-center-start-nowrap-4">
1164
+ <Typography variant="smallCaption-regular" color="color-text-high">
1165
+ {providers.find((p) => p.envVarsSet)?.name ?? 'None detected'}
1166
+ </Typography>
1167
+ {providers.find((p) => p.envVarsSet) && (
1168
+ <Chip label="active" size="small" color="color-status-info" />
1169
+ )}
1170
+ </Flex>
1171
+ </Flex>
1172
+
1173
+ {/* Trust */}
1174
+ {insights?.trust && (
1175
+ <Flex variant="row-center-space-between-nowrap-8">
1176
+ <Typography variant="smallCaption-regular" color="color-text-subtle">Trust</Typography>
1177
+ <Typography variant="smallCaption-regular" color="color-text-high">
1178
+ {`Phase ${(insights.trust as Record<string, unknown>).phase} · ${(insights.trust as Record<string, unknown>).score}/100`}
1179
+ </Typography>
1180
+ </Flex>
1181
+ )}
1182
+
1183
+ {/* Health */}
1184
+ {insights?.health && (
1185
+ <Flex variant="row-center-space-between-nowrap-8">
1186
+ <Typography variant="smallCaption-regular" color="color-text-subtle">Health</Typography>
1187
+ <Typography variant="smallCaption-regular" color="color-text-high">
1188
+ {`${(insights.health as Record<string, unknown>).overall}/100`}
1189
+ </Typography>
1190
+ </Flex>
1191
+ )}
1192
+
1193
+ {/* Available providers */}
1194
+ {providers.length > 0 && (
1195
+ <Flex variant="column-stretch-start-nowrap-4">
1196
+ <Typography variant="smallCaption-regular" color="color-text-subtle">Available Providers</Typography>
1197
+ <Flex variant="row-center-start-wrap-4">
1198
+ {providers.map((p) => (
1199
+ <Chip
1200
+ key={p.name}
1201
+ label={p.name}
1202
+ size="small"
1203
+ color={p.envVarsSet ? 'color-status-positive' : 'color-brand-alt'}
1204
+ />
1205
+ ))}
1206
+ </Flex>
1207
+ </Flex>
1208
+ )}
1209
+
1210
+ {/* Reset section */}
1211
+ <Flex variant="column-stretch-start-nowrap-8" style={{ marginTop: 16, paddingTop: 12, borderTop: '1px solid var(--color-border-default)' }}>
1212
+ <SectionTitle size="xs">Reset</SectionTitle>
1213
+ <Flex variant="row-center-start-nowrap-8">
1214
+ <Button
1215
+ size="xs"
1216
+ variant="outlined"
1217
+ color="secondary"
1218
+ leftIcon="undo"
1219
+ onClick={async () => {
1220
+ const ok = await ctx.confirm('Reset Weaver configuration to defaults? Your .weaver.json will be deleted.', {
1221
+ title: 'Reset Config',
1222
+ confirmLabel: 'Reset',
1223
+ state: 'warning',
1224
+ });
1225
+ if (!ok) return;
1226
+ try {
1227
+ await callTool('fw_weaver_config_reset', {});
1228
+ toast('Config reset to defaults', { type: 'success' });
1229
+ refreshAll();
1230
+ } catch (err: unknown) {
1231
+ toast(err instanceof Error ? err.message : 'Failed to reset config', { type: 'error' });
1232
+ }
1233
+ }}
1234
+ >Reset Config</Button>
1235
+ <Button
1236
+ size="xs"
1237
+ variant="outlined"
1238
+ color="danger"
1239
+ leftIcon="warning"
1240
+ onClick={async () => {
1241
+ const ok = await ctx.confirm(
1242
+ 'This will stop the swarm, delete ALL tasks, profiles, run history, and configuration. This cannot be undone.',
1243
+ {
1244
+ title: 'Reset Everything',
1245
+ confirmLabel: 'Reset Everything',
1246
+ state: 'danger',
1247
+ },
1248
+ );
1249
+ if (!ok) return;
1250
+ try {
1251
+ const raw = await callTool('fw_weaver_reset_all', {});
1252
+ const data = parseToolResult(raw) as { tasksCleared?: number; profilesCleared?: number } | null;
1253
+ toast(`Full reset complete — ${data?.tasksCleared ?? 0} tasks, ${data?.profilesCleared ?? 0} profiles cleared`, { type: 'success' });
1254
+ refreshAll();
1255
+ } catch (err: unknown) {
1256
+ toast(err instanceof Error ? err.message : 'Failed to reset', { type: 'error' });
1257
+ }
1258
+ }}
1259
+ >Reset Everything</Button>
1260
+ </Flex>
1261
+ </Flex>
1262
+
1263
+ {/* Evolution */}
1264
+ <Flex variant="column-stretch-start-nowrap-8" style={{ marginTop: 16, paddingTop: 12, borderTop: '1px solid var(--color-border-default)' }}>
1265
+ <SectionTitle size="xs">Evolution</SectionTitle>
1266
+ <Flex variant="row-center-start-nowrap-8">
1267
+ <Button
1268
+ size="xs"
1269
+ variant="outlined"
1270
+ color="primary"
1271
+ leftIcon="psychology"
1272
+ onClick={async () => {
1273
+ const ok = await ctx.confirm('Run a Genesis self-evolution cycle? This analyzes your workflows and suggests improvements.', {
1274
+ title: 'Run Genesis',
1275
+ confirmLabel: 'Run',
1276
+ state: 'info',
1277
+ });
1278
+ if (!ok) return;
1279
+ try {
1280
+ toast('Genesis cycle started...', { type: 'info' });
1281
+ const raw = await callTool('fw_weaver_genesis', {});
1282
+ const data = typeof raw === 'string' ? JSON.parse(raw) : raw;
1283
+ if (data?.success) {
1284
+ toast('Genesis cycle completed', { type: 'success' });
1285
+ } else {
1286
+ toast(`Genesis: ${data?.summary ?? 'Cycle finished'}`, { type: 'info' });
1287
+ }
1288
+ refreshAll();
1289
+ } catch (err: unknown) {
1290
+ toast(err instanceof Error ? err.message : 'Genesis failed', { type: 'error' });
1291
+ }
1292
+ }}
1293
+ >Run Genesis Cycle</Button>
1294
+ </Flex>
1295
+ </Flex>
1296
+
1297
+ {/* Pack version */}
1298
+ {swarmStatus?.packVersion && (
1299
+ <Typography
1300
+ variant="smallCaption-regular"
1301
+ color="color-text-low"
1302
+ style={{ textAlign: 'right', paddingTop: 8 }}
1303
+ >{`Weaver v${swarmStatus.packVersion}`}</Typography>
1304
+ )}
1305
+ </Flex>
1306
+ )}
1307
+ </Flex>
1308
+ </Flex>
775
1309
  );
776
1310
  }
777
1311
 
778
1312
  export { SwarmDashboard };
779
1313
  export default SwarmDashboard;
780
- module.exports = SwarmDashboard;