@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.
- package/dist/ai-chat-provider.d.ts.map +1 -1
- package/dist/ai-chat-provider.js +96 -4
- package/dist/ai-chat-provider.js.map +1 -1
- package/dist/bot/bot-registry.d.ts +33 -0
- package/dist/bot/bot-registry.d.ts.map +1 -0
- package/dist/bot/bot-registry.js +98 -0
- package/dist/bot/bot-registry.js.map +1 -0
- package/dist/bot/runner.d.ts.map +1 -1
- package/dist/bot/runner.js +11 -5
- package/dist/bot/runner.js.map +1 -1
- package/dist/ui/bot-panel.js +388 -0
- package/dist/ui/bot-workspace.js +76 -153
- package/dist/ui/chat-bot-result.js +71 -0
- package/flowweaver.manifest.json +128 -1
- package/package.json +1 -1
- package/src/ai-chat-provider.ts +101 -7
- package/src/bot/bot-registry.ts +116 -0
- package/src/bot/runner.ts +8 -5
- package/src/ui/bot-panel.tsx +382 -0
- package/src/ui/bot-workspace.tsx +42 -6
- package/src/ui/chat-bot-result.tsx +81 -0
package/src/ai-chat-provider.ts
CHANGED
|
@@ -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
|
|
141
|
-
if (
|
|
142
|
-
workflowPath =
|
|
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
|
-
/**
|
|
261
|
-
function
|
|
262
|
-
if (typeof value === 'string'
|
|
263
|
-
|
|
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:
|
|
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;
|