@synergenius/flow-weaver-pack-weaver 0.9.57 → 0.9.59

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.
@@ -24,6 +24,7 @@ import { runRegistry } from './bot/run-registry.js';
24
24
  import { ProjectModelStore } from './bot/project-model.js';
25
25
  import { InsightEngine } from './bot/insight-engine.js';
26
26
  import { buildSystemPrompt } from './bot/system-prompt.js';
27
+ import { BotRegistry } from './bot/bot-registry.js';
27
28
 
28
29
  /** Throttle orphan recovery to max once per 30s. */
29
30
  let lastOrphanRecoveryTime = 0;
@@ -121,9 +122,24 @@ const toolHandlers: Record<
121
122
  },
122
123
 
123
124
  async fw_weaver_bot(args, ctx) {
124
-
125
-
126
-
125
+ // Resolve bot from registry if botId is provided
126
+ const botId = args.botId as string | undefined;
127
+ let registryWorkflowFile: string | undefined;
128
+
129
+ if (botId) {
130
+ const registry = new BotRegistry(ctx.workspacePath);
131
+ const bot = registry.get(botId);
132
+ if (!bot) {
133
+ return JSON.stringify({ error: `Bot '${botId}' not found` });
134
+ }
135
+ const validation = registry.validate(botId);
136
+ if (!validation.valid) {
137
+ return JSON.stringify({ error: `Bot '${botId}' is invalid: ${validation.error}` });
138
+ }
139
+ registryWorkflowFile = path.isAbsolute(bot.filePath)
140
+ ? bot.filePath
141
+ : path.join(ctx.workspacePath, bot.filePath);
142
+ }
127
143
 
128
144
 
129
145
  const task = {
@@ -137,9 +153,14 @@ const toolHandlers: Record<
137
153
  },
138
154
  };
139
155
  const packRoot = new URL('..', import.meta.url);
140
- let workflowPath = fileURLToPath(new URL('src/workflows/weaver-bot.ts', packRoot));
141
- if (!fs.existsSync(workflowPath)) {
142
- workflowPath = fileURLToPath(new URL('dist/workflows/weaver-bot.js', packRoot));
156
+ let workflowPath: string;
157
+ if (registryWorkflowFile) {
158
+ workflowPath = registryWorkflowFile;
159
+ } else {
160
+ workflowPath = fileURLToPath(new URL('src/workflows/weaver-bot.ts', packRoot));
161
+ if (!fs.existsSync(workflowPath)) {
162
+ workflowPath = fileURLToPath(new URL('dist/workflows/weaver-bot.js', packRoot));
163
+ }
143
164
  }
144
165
 
145
166
  const projectDir = ctx.workspacePath || process.cwd();
@@ -551,6 +572,57 @@ const toolHandlers: Record<
551
572
  },
552
573
  }, null, 2);
553
574
  },
575
+
576
+ async fw_weaver_list_bots(args: Record<string, unknown>, ctx: AiChatToolContext) {
577
+ const registry = new BotRegistry(ctx.workspacePath);
578
+ const bots = registry.list();
579
+ const withValidation = bots.map((bot) => ({
580
+ ...bot,
581
+ validation: registry.validate(bot.id),
582
+ }));
583
+ return JSON.stringify(withValidation, null, 2);
584
+ },
585
+
586
+ async fw_weaver_register_bot(args: Record<string, unknown>, ctx: AiChatToolContext) {
587
+ const registry = new BotRegistry(ctx.workspacePath);
588
+ const bot = {
589
+ id: (args.id as string) || `bot-${Date.now().toString(36)}`,
590
+ name: args.name as string,
591
+ description: (args.description as string) || '',
592
+ icon: args.icon as string | undefined,
593
+ color: args.color as string | undefined,
594
+ filePath: args.filePath as string,
595
+ workflowExport: args.workflowExport as string,
596
+ paramName: (args.paramName as string) || 'taskJson',
597
+ packId: args.packId as string | undefined,
598
+ ejected: args.ejected as boolean | undefined,
599
+ sourceTemplateId: args.sourceTemplateId as string | undefined,
600
+ };
601
+ registry.register(bot);
602
+ return JSON.stringify({ registered: true, bot });
603
+ },
604
+
605
+ async fw_weaver_unregister_bot(args: Record<string, unknown>, ctx: AiChatToolContext) {
606
+ const registry = new BotRegistry(ctx.workspacePath);
607
+ const id = args.id as string;
608
+ const removed = registry.unregister(id);
609
+ return JSON.stringify({ removed, id });
610
+ },
611
+
612
+ async fw_weaver_validate_bot(args: Record<string, unknown>, ctx: AiChatToolContext) {
613
+ const registry = new BotRegistry(ctx.workspacePath);
614
+ const id = args.id as string;
615
+ const result = registry.validate(id);
616
+ return JSON.stringify(result);
617
+ },
618
+
619
+ async fw_weaver_eject_bot(args: Record<string, unknown>, ctx: AiChatToolContext) {
620
+ const registry = new BotRegistry(ctx.workspacePath);
621
+ const id = args.id as string;
622
+ const newFilePath = args.filePath as string;
623
+ const ejected = registry.eject(id, newFilePath);
624
+ return JSON.stringify({ ejected, id, filePath: newFilePath });
625
+ },
554
626
  };
555
627
 
556
628
  // ---------------------------------------------------------------------------
@@ -602,7 +674,7 @@ export default {
602
674
  title: 'Weaver Capabilities',
603
675
  content: `Beyond standard Flow Weaver tools, you also have:
604
676
 
605
- - \`fw_weaver_bot\`: Autonomous workflow creation/modification from natural language tasks
677
+ - \`fw_weaver_bot\`: Autonomous workflow creation/modification from natural language tasks (supports \`botId\` to run registered bots)
606
678
  - \`fw_weaver_steer\`: Control running bots (pause, resume, cancel, redirect)
607
679
  - \`fw_weaver_status\`: Current bot session state
608
680
  - \`fw_weaver_queue\`: Background task queue management
@@ -612,6 +684,11 @@ export default {
612
684
  - \`fw_weaver_history\`: Past workflow execution history
613
685
  - \`fw_weaver_providers\`: Available AI providers
614
686
  - \`fw_weaver_genesis\`: Self-evolution cycle for workflows
687
+ - \`fw_weaver_list_bots\`: List all registered bots with validation status
688
+ - \`fw_weaver_register_bot\`: Register a new bot workflow
689
+ - \`fw_weaver_unregister_bot\`: Remove a bot registration
690
+ - \`fw_weaver_validate_bot\`: Validate a bot's file and export
691
+ - \`fw_weaver_eject_bot\`: Mark a bot as ejected with a new file path
615
692
 
616
693
  Proactively offer these when relevant.`,
617
694
  priority: 15,
@@ -663,6 +740,23 @@ Proactively offer these when relevant.`,
663
740
  }
664
741
  }
665
742
 
743
+ // Registered bots
744
+ if (context.workspacePath) {
745
+ try {
746
+ const registry = new BotRegistry(context.workspacePath);
747
+ const bots = registry.list();
748
+ if (bots.length > 0) {
749
+ const botLines = bots.map((b) => `- **${b.name}** (id: \`${b.id}\`): ${b.description}`).join('\n');
750
+ sections.push({
751
+ id: 'registered-bots',
752
+ title: 'Registered Bots',
753
+ content: `The user has registered bot workflows. To run a bot, use \`fw_weaver_bot\` with \`botId\` parameter.\n\n${botLines}`,
754
+ priority: 25,
755
+ });
756
+ }
757
+ } catch { /* non-fatal */ }
758
+ }
759
+
666
760
  return sections;
667
761
  },
668
762
  };
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Bot registry — manages .fw/bots.json in the user's workspace.
3
+ * Reads, writes, validates bot registrations.
4
+ */
5
+ import * as fs from 'node:fs';
6
+ import * as path from 'node:path';
7
+
8
+ export interface BotRegistration {
9
+ id: string;
10
+ name: string;
11
+ description: string;
12
+ icon?: string;
13
+ color?: string;
14
+ filePath: string;
15
+ workflowExport: string;
16
+ paramName: string;
17
+ sourceTemplateId?: string;
18
+ packId?: string;
19
+ ejected?: boolean;
20
+ }
21
+
22
+ const BOTS_FILE = '.fw/bots.json';
23
+
24
+ export class BotRegistry {
25
+ private _cache: BotRegistration[] | null = null;
26
+ private _cacheMtime: number = 0;
27
+
28
+ constructor(private readonly workspacePath: string) {}
29
+
30
+ private get botsPath(): string {
31
+ return path.join(this.workspacePath, BOTS_FILE);
32
+ }
33
+
34
+ list(): BotRegistration[] {
35
+ try {
36
+ const mtime = fs.statSync(this.botsPath).mtimeMs;
37
+ if (this._cache && mtime === this._cacheMtime) {
38
+ return this._cache;
39
+ }
40
+ const raw = fs.readFileSync(this.botsPath, 'utf-8');
41
+ const parsed = JSON.parse(raw);
42
+ const result = Array.isArray(parsed) ? parsed : [];
43
+ this._cache = result;
44
+ this._cacheMtime = mtime;
45
+ return result;
46
+ } catch {
47
+ return [];
48
+ }
49
+ }
50
+
51
+ get(id: string): BotRegistration | null {
52
+ return this.list().find((b) => b.id === id) ?? null;
53
+ }
54
+
55
+ register(bot: BotRegistration): void {
56
+ const bots = this.list();
57
+ const existing = bots.findIndex((b) => b.id === bot.id);
58
+ if (existing >= 0) {
59
+ bots[existing] = { ...bots[existing], ...bot };
60
+ } else {
61
+ bots.push(bot);
62
+ }
63
+ this.save(bots);
64
+ }
65
+
66
+ unregister(id: string): boolean {
67
+ const bots = this.list();
68
+ const filtered = bots.filter((b) => b.id !== id);
69
+ if (filtered.length === bots.length) return false;
70
+ this.save(filtered);
71
+ return true;
72
+ }
73
+
74
+ eject(id: string, newFilePath: string): boolean {
75
+ const bots = this.list();
76
+ const bot = bots.find((b) => b.id === id);
77
+ if (!bot) return false;
78
+ bot.filePath = newFilePath;
79
+ bot.ejected = true;
80
+ this.save(bots);
81
+ return true;
82
+ }
83
+
84
+ validate(id: string): { valid: boolean; fileExists: boolean; exportValid: boolean; error: string | null } {
85
+ const bot = this.get(id);
86
+ if (!bot) return { valid: false, fileExists: false, exportValid: false, error: 'Bot not found' };
87
+
88
+ const absPath = path.isAbsolute(bot.filePath)
89
+ ? bot.filePath
90
+ : path.join(this.workspacePath, bot.filePath);
91
+
92
+ if (!fs.existsSync(absPath)) {
93
+ return { valid: false, fileExists: false, exportValid: false, error: `File not found: ${bot.filePath}` };
94
+ }
95
+
96
+ // Check export exists by parsing the file
97
+ try {
98
+ const source = fs.readFileSync(absPath, 'utf-8');
99
+ const exportPattern = new RegExp(`export\\s+(?:(?:async\\s+)?function|const)\\s+${bot.workflowExport}\\b`);
100
+ if (!exportPattern.test(source)) {
101
+ return { valid: false, fileExists: true, exportValid: false, error: `Export '${bot.workflowExport}' not found in ${bot.filePath}` };
102
+ }
103
+ } catch (err) {
104
+ return { valid: false, fileExists: true, exportValid: false, error: `Cannot read file: ${(err as Error).message}` };
105
+ }
106
+
107
+ return { valid: true, fileExists: true, exportValid: true, error: null };
108
+ }
109
+
110
+ private save(bots: BotRegistration[]): void {
111
+ this._cache = null;
112
+ const dir = path.dirname(this.botsPath);
113
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
114
+ fs.writeFileSync(this.botsPath, JSON.stringify(bots, null, 2) + '\n');
115
+ }
116
+ }
package/src/bot/runner.ts CHANGED
@@ -257,10 +257,13 @@ export async function runWorkflow(
257
257
  const nodeTypes = new Map<string, string>(); // nodeId → nodeTypeName
258
258
  const nodeOutputs = new Map<string, Array<{ portLabel: string; value: unknown }>>();
259
259
 
260
- /** Truncate large string values; preserve objects as-is (UI handles overflow). */
261
- function truncateValue(value: unknown, maxLen = 8192): unknown {
262
- if (typeof value === 'string' && value.length > maxLen) {
263
- return value.slice(0, maxLen) + '…';
260
+ /** Try to parse JSON strings into objects so the UI renders them as structured JSON. */
261
+ function resolveOutputValue(value: unknown): unknown {
262
+ if (typeof value === 'string') {
263
+ const trimmed = value.trim();
264
+ if ((trimmed.startsWith('{') && trimmed.endsWith('}')) || (trimmed.startsWith('[') && trimmed.endsWith(']'))) {
265
+ try { return JSON.parse(value); } catch { /* not valid JSON, return as-is */ }
266
+ }
264
267
  }
265
268
  return value;
266
269
  }
@@ -286,7 +289,7 @@ export async function runWorkflow(
286
289
  if (nodeTypeName) nodeTypes.set(nodeId, nodeTypeName);
287
290
  const label = resolvePortLabel(nodeTypeName ?? nodeTypes.get(nodeId), portName);
288
291
  const outputs = nodeOutputs.get(nodeId) ?? [];
289
- outputs.push({ portLabel: label, value: truncateValue(traceEvent.data.value) });
292
+ outputs.push({ portLabel: label, value: resolveOutputValue(traceEvent.data.value) });
290
293
  nodeOutputs.set(nodeId, outputs);
291
294
  }
292
295
  }
@@ -0,0 +1,382 @@
1
+ /**
2
+ * Weaver Bot Panel — sidebar component.
3
+ *
4
+ * Replaces BotPanel.tsx + BotLibrary.tsx from the platform's cloud-bot-panel.
5
+ * Runs in the pack sandbox — uses CommonJS require for platform deps,
6
+ * React.createElement throughout (no JSX transform in sandbox).
7
+ *
8
+ * Features:
9
+ * - Lists registered bots via fw_weaver_list_bots
10
+ * - Validation badges per bot
11
+ * - Register existing workflow form
12
+ * - Open workspace action per bot
13
+ * - Unregister action per bot
14
+ */
15
+ const React = require('react');
16
+ const { useState, useEffect, useCallback, useRef } = React;
17
+ const {
18
+ Flex, Typography, Button, IconButton, Icon, Input, EmptyState,
19
+ ScrollArea, StatusIcon, toast, usePackWorkspace,
20
+ } = require('@fw/plugin-ui-kit');
21
+
22
+ // ---------------------------------------------------------------------------
23
+ // Types
24
+ // ---------------------------------------------------------------------------
25
+
26
+ interface Bot {
27
+ id: string;
28
+ name: string;
29
+ description: string;
30
+ icon?: string;
31
+ color?: string;
32
+ filePath: string;
33
+ workflowExport: string;
34
+ paramName: string;
35
+ packId?: string;
36
+ ejected?: boolean;
37
+ validation?: {
38
+ state: 'valid' | 'invalid' | 'checking' | 'unknown';
39
+ fileExists?: boolean;
40
+ exportValid?: boolean;
41
+ errorMessage?: string;
42
+ };
43
+ }
44
+
45
+ // ---------------------------------------------------------------------------
46
+ // Helpers
47
+ // ---------------------------------------------------------------------------
48
+
49
+ function parseToolResult(raw: unknown): unknown {
50
+ if (typeof raw === 'string') {
51
+ try { return JSON.parse(raw); } catch { return raw; }
52
+ }
53
+ return raw;
54
+ }
55
+
56
+ function validationIcon(state: string | undefined): { icon: string; color: string } {
57
+ switch (state) {
58
+ case 'valid': return { icon: 'checkCircle', color: 'var(--color-success)' };
59
+ case 'invalid': return { icon: 'error', color: 'var(--color-danger)' };
60
+ case 'checking': return { icon: 'sync', color: 'var(--color-warning)' };
61
+ default: return { icon: 'helpOutline', color: 'var(--color-text-subtle)' };
62
+ }
63
+ }
64
+
65
+ // ---------------------------------------------------------------------------
66
+ // Component
67
+ // ---------------------------------------------------------------------------
68
+
69
+ function BotPanel() {
70
+ const ctx = usePackWorkspace();
71
+ const { callTool, dispatchEvent } = ctx;
72
+ const packId = ctx.packId;
73
+
74
+ // Bot list state
75
+ const [bots, setBots] = useState<Bot[]>([]);
76
+ const [loading, setLoading] = useState(true);
77
+ const [error, setError] = useState<string | null>(null);
78
+
79
+ // Register form state
80
+ const [showRegForm, setShowRegForm] = useState(false);
81
+ const [regName, setRegName] = useState('');
82
+ const [regFilePath, setRegFilePath] = useState('');
83
+ const [regExport, setRegExport] = useState('');
84
+ const [registering, setRegistering] = useState(false);
85
+
86
+ // Unregister confirmation
87
+ const [confirmUnregId, setConfirmUnregId] = useState<string | null>(null);
88
+ const [unregistering, setUnregistering] = useState(false);
89
+
90
+ // ── Load bots ──────────────────────────────────────────────────
91
+ const loadBots = useCallback(async () => {
92
+ try {
93
+ setError(null);
94
+ const raw = await callTool('fw_weaver_list_bots');
95
+ const parsed = parseToolResult(raw);
96
+ if (Array.isArray(parsed)) {
97
+ setBots(parsed as Bot[]);
98
+ } else {
99
+ setBots([]);
100
+ }
101
+ } catch (err: unknown) {
102
+ const msg = err instanceof Error ? err.message : 'Failed to load bots';
103
+ setError(msg);
104
+ } finally {
105
+ setLoading(false);
106
+ }
107
+ }, [callTool]);
108
+
109
+ // Load on mount
110
+ const mountedRef = useRef(false);
111
+ useEffect(() => {
112
+ if (!mountedRef.current) {
113
+ mountedRef.current = true;
114
+ loadBots();
115
+ }
116
+ }, [loadBots]);
117
+
118
+ // ── Register bot ───────────────────────────────────────────────
119
+ const handleRegister = useCallback(async () => {
120
+ if (!regName.trim() || !regFilePath.trim() || !regExport.trim()) return;
121
+ setRegistering(true);
122
+ try {
123
+ await callTool('fw_weaver_register_bot', {
124
+ name: regName.trim(),
125
+ filePath: regFilePath.trim(),
126
+ workflowExport: regExport.trim(),
127
+ paramName: 'taskJson',
128
+ });
129
+ toast('Bot registered', { type: 'success' });
130
+ setRegName('');
131
+ setRegFilePath('');
132
+ setRegExport('');
133
+ setShowRegForm(false);
134
+ await loadBots();
135
+ } catch (err: unknown) {
136
+ toast(err instanceof Error ? err.message : 'Registration failed', { type: 'error' });
137
+ } finally {
138
+ setRegistering(false);
139
+ }
140
+ }, [regName, regFilePath, regExport, callTool, loadBots]);
141
+
142
+ // ── Unregister bot ─────────────────────────────────────────────
143
+ const handleUnregister = useCallback(async (id: string) => {
144
+ setUnregistering(true);
145
+ try {
146
+ await callTool('fw_weaver_unregister_bot', { id });
147
+ toast('Bot unregistered', { type: 'warning' });
148
+ setConfirmUnregId(null);
149
+ await loadBots();
150
+ } catch (err: unknown) {
151
+ toast(err instanceof Error ? err.message : 'Unregister failed', { type: 'error' });
152
+ } finally {
153
+ setUnregistering(false);
154
+ }
155
+ }, [callTool, loadBots]);
156
+
157
+ // ── Open workspace ─────────────────────────────────────────────
158
+ const handleOpenWorkspace = useCallback((botId: string) => {
159
+ dispatchEvent('fw:open-bot-workspace', { botId, packId, live: false });
160
+ }, [dispatchEvent, packId]);
161
+
162
+ // ── Render ─────────────────────────────────────────────────────
163
+
164
+ const hasBots = bots.length > 0;
165
+
166
+ // Header row with title + actions
167
+ const header = React.createElement(Flex, {
168
+ variant: 'row-center-between-nowrap-8',
169
+ style: { padding: '8px 12px', borderBottom: '1px solid var(--color-border-default)', flexShrink: 0 },
170
+ },
171
+ React.createElement(Flex, { variant: 'row-center-start-nowrap-6' },
172
+ React.createElement(Icon, { name: 'smartToy', size: 16, color: 'color-text-medium' }),
173
+ React.createElement(Typography, { variant: 'caption-thick', color: 'color-text-high' }, 'Bots'),
174
+ ),
175
+ React.createElement(Flex, { variant: 'row-center-start-nowrap-4' },
176
+ React.createElement(IconButton, {
177
+ icon: 'refresh', size: 'xs', variant: 'clear',
178
+ onClick: loadBots, title: 'Refresh',
179
+ }),
180
+ React.createElement(IconButton, {
181
+ icon: 'add', size: 'xs', variant: 'clear',
182
+ onClick: () => setShowRegForm((v: boolean) => !v), title: 'Register bot',
183
+ }),
184
+ ),
185
+ );
186
+
187
+ // Registration form
188
+ const regForm = showRegForm ? React.createElement(Flex, {
189
+ variant: 'column-start-start-nowrap-8',
190
+ style: {
191
+ padding: '10px 12px',
192
+ borderBottom: '1px solid var(--color-border-default)',
193
+ backgroundColor: 'var(--color-surface-low)',
194
+ },
195
+ },
196
+ React.createElement(Typography, { variant: 'smallCaption-thick', color: 'color-text-medium' }, 'Register Existing Workflow'),
197
+ React.createElement(Input, {
198
+ type: 'text', size: 'small', placeholder: 'Bot name',
199
+ value: regName, onChange: setRegName,
200
+ }),
201
+ React.createElement(Input, {
202
+ type: 'text', size: 'small', placeholder: 'File path (e.g. my-bot.ts)',
203
+ value: regFilePath, onChange: setRegFilePath,
204
+ }),
205
+ React.createElement(Input, {
206
+ type: 'text', size: 'small', placeholder: 'Export name (e.g. myBot)',
207
+ value: regExport, onChange: setRegExport,
208
+ }),
209
+ React.createElement(Flex, { variant: 'row-center-start-nowrap-6' },
210
+ React.createElement(Button, {
211
+ size: 'xs', variant: 'fill', color: 'primary',
212
+ onClick: handleRegister,
213
+ disabled: registering || !regName.trim() || !regFilePath.trim() || !regExport.trim(),
214
+ }, registering ? 'Registering...' : 'Register'),
215
+ React.createElement(Button, {
216
+ size: 'xs', variant: 'clear', color: 'secondary',
217
+ onClick: () => { setShowRegForm(false); setRegName(''); setRegFilePath(''); setRegExport(''); },
218
+ }, 'Cancel'),
219
+ ),
220
+ ) : null;
221
+
222
+ // Loading state
223
+ if (loading) {
224
+ return React.createElement(Flex, {
225
+ variant: 'column-stretch-start-nowrap-0',
226
+ style: { width: '100%', height: '100%' },
227
+ },
228
+ header,
229
+ React.createElement(Flex, {
230
+ variant: 'column-center-center-nowrap-8',
231
+ style: { flex: 1, padding: '24px' },
232
+ },
233
+ React.createElement(Typography, { variant: 'caption-regular', color: 'color-text-subtle' }, 'Loading bots...'),
234
+ ),
235
+ );
236
+ }
237
+
238
+ // Error state
239
+ if (error) {
240
+ return React.createElement(Flex, {
241
+ variant: 'column-stretch-start-nowrap-0',
242
+ style: { width: '100%', height: '100%' },
243
+ },
244
+ header,
245
+ React.createElement(Flex, {
246
+ variant: 'column-center-center-nowrap-8',
247
+ style: { flex: 1, padding: '24px' },
248
+ },
249
+ React.createElement(Typography, { variant: 'caption-regular', color: 'color-danger' }, error),
250
+ React.createElement(Button, { size: 'xs', variant: 'outlined', onClick: loadBots }, 'Retry'),
251
+ ),
252
+ );
253
+ }
254
+
255
+ // Bot cards
256
+ const botCards = bots.map((bot: Bot) => {
257
+ const vState = bot.validation?.state;
258
+ const vInfo = validationIcon(vState);
259
+ const isConfirming = confirmUnregId === bot.id;
260
+
261
+ return React.createElement(Flex, {
262
+ key: bot.id,
263
+ variant: 'row-center-start-nowrap-8',
264
+ style: {
265
+ padding: '7px 8px',
266
+ borderRadius: 'var(--border-radius-secondary)',
267
+ border: '1px solid var(--color-border-default)',
268
+ cursor: 'pointer',
269
+ transition: 'background-color 0.12s',
270
+ },
271
+ onClick: () => handleOpenWorkspace(bot.id),
272
+ onMouseEnter: (e: React.MouseEvent) => {
273
+ (e.currentTarget as HTMLElement).style.backgroundColor = 'var(--color-surface-raised)';
274
+ },
275
+ onMouseLeave: (e: React.MouseEvent) => {
276
+ (e.currentTarget as HTMLElement).style.backgroundColor = '';
277
+ },
278
+ },
279
+ // Icon
280
+ React.createElement('div', {
281
+ style: {
282
+ width: '26px', height: '26px',
283
+ borderRadius: 'var(--border-radius-compact)',
284
+ display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0,
285
+ background: bot.color
286
+ ? `color-mix(in srgb, ${bot.color} 14%, transparent)`
287
+ : 'var(--color-surface-low)',
288
+ },
289
+ },
290
+ React.createElement(Icon, {
291
+ name: bot.icon || 'smartToy', size: 14,
292
+ color: bot.color ? undefined : 'color-text-medium',
293
+ style: bot.color ? { color: bot.color } : undefined,
294
+ }),
295
+ ),
296
+
297
+ // Name + description
298
+ React.createElement(Flex, {
299
+ variant: 'column-start-start-nowrap-1',
300
+ style: { flex: 1, minWidth: 0 },
301
+ },
302
+ React.createElement(Flex, { variant: 'row-center-start-nowrap-4' },
303
+ React.createElement(Typography, {
304
+ variant: 'caption-thick', color: 'color-text-high',
305
+ style: { overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' },
306
+ }, bot.name),
307
+ // Validation badge
308
+ React.createElement(Icon, {
309
+ name: vInfo.icon, size: 12,
310
+ style: { color: vInfo.color, flexShrink: 0 },
311
+ }),
312
+ ),
313
+ bot.description ? React.createElement(Typography, {
314
+ variant: 'smallCaption-regular', color: 'color-text-medium',
315
+ style: { overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' },
316
+ }, bot.description) : null,
317
+ ),
318
+
319
+ // Actions
320
+ isConfirming
321
+ ? React.createElement(Flex, {
322
+ variant: 'row-center-start-nowrap-4',
323
+ onClick: (e: React.MouseEvent) => e.stopPropagation(),
324
+ },
325
+ React.createElement(Button, {
326
+ size: 'xs', variant: 'fill', color: 'danger',
327
+ onClick: () => handleUnregister(bot.id),
328
+ disabled: unregistering,
329
+ }, unregistering ? '...' : 'Yes'),
330
+ React.createElement(Button, {
331
+ size: 'xs', variant: 'clear', color: 'secondary',
332
+ onClick: () => setConfirmUnregId(null),
333
+ }, 'No'),
334
+ )
335
+ : React.createElement(Flex, {
336
+ variant: 'row-center-start-nowrap-2',
337
+ onClick: (e: React.MouseEvent) => e.stopPropagation(),
338
+ },
339
+ React.createElement(IconButton, {
340
+ icon: 'openInNew', size: 'xs', variant: 'clear',
341
+ onClick: () => handleOpenWorkspace(bot.id),
342
+ title: 'Open workspace',
343
+ }),
344
+ React.createElement(IconButton, {
345
+ icon: 'close', size: 'xs', variant: 'clear',
346
+ onClick: () => setConfirmUnregId(bot.id),
347
+ title: 'Unregister',
348
+ }),
349
+ ),
350
+ );
351
+ });
352
+
353
+ // Main content
354
+ const content = hasBots
355
+ ? React.createElement(ScrollArea, { style: { flex: 1 } },
356
+ React.createElement(Flex, {
357
+ variant: 'column-start-start-nowrap-4',
358
+ style: { padding: '8px 12px' },
359
+ }, ...botCards),
360
+ )
361
+ : React.createElement(Flex, {
362
+ variant: 'column-center-center-nowrap-0',
363
+ style: { flex: 1, padding: '24px' },
364
+ },
365
+ React.createElement(EmptyState, {
366
+ icon: 'smartToy',
367
+ message: 'No bots registered',
368
+ description: 'Register an existing workflow or ask the AI assistant to create one.',
369
+ }),
370
+ );
371
+
372
+ return React.createElement(Flex, {
373
+ variant: 'column-stretch-start-nowrap-0',
374
+ style: { width: '100%', height: '100%', overflow: 'hidden' },
375
+ },
376
+ header,
377
+ regForm,
378
+ content,
379
+ );
380
+ }
381
+
382
+ module.exports = BotPanel;