@aion0/forge 0.5.20 → 0.5.22
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/.forge/agent-context.json +1 -1
- package/RELEASE_NOTES.md +32 -6
- package/app/api/code/route.ts +10 -4
- package/app/api/plugins/route.ts +75 -0
- package/components/Dashboard.tsx +1 -0
- package/components/PipelineEditor.tsx +135 -9
- package/components/PluginsPanel.tsx +472 -0
- package/components/ProjectDetail.tsx +36 -98
- package/components/SessionView.tsx +4 -4
- package/components/SettingsModal.tsx +160 -66
- package/components/SkillsPanel.tsx +14 -5
- package/components/TerminalLauncher.tsx +398 -0
- package/components/WebTerminal.tsx +84 -84
- package/components/WorkspaceView.tsx +371 -87
- package/lib/agents/index.ts +7 -4
- package/lib/builtin-plugins/docker.yaml +70 -0
- package/lib/builtin-plugins/http.yaml +66 -0
- package/lib/builtin-plugins/jenkins.yaml +92 -0
- package/lib/builtin-plugins/llm-vision.yaml +85 -0
- package/lib/builtin-plugins/playwright.yaml +111 -0
- package/lib/builtin-plugins/shell-command.yaml +60 -0
- package/lib/builtin-plugins/slack.yaml +48 -0
- package/lib/builtin-plugins/webhook.yaml +56 -0
- package/lib/forge-mcp-server.ts +116 -2
- package/lib/pipeline.ts +62 -5
- package/lib/plugins/executor.ts +347 -0
- package/lib/plugins/registry.ts +228 -0
- package/lib/plugins/types.ts +103 -0
- package/lib/project-sessions.ts +7 -2
- package/lib/session-utils.ts +7 -3
- package/lib/terminal-standalone.ts +6 -34
- package/lib/workspace/agent-worker.ts +1 -1
- package/lib/workspace/orchestrator.ts +414 -136
- package/lib/workspace/presets.ts +5 -3
- package/lib/workspace/session-monitor.ts +14 -10
- package/lib/workspace/types.ts +3 -1
- package/lib/workspace-standalone.ts +38 -21
- package/next-env.d.ts +1 -1
- package/package.json +1 -1
- package/qa/.forge/agent-context.json +1 -1
package/RELEASE_NOTES.md
CHANGED
|
@@ -1,12 +1,38 @@
|
|
|
1
|
-
# Forge v0.5.
|
|
1
|
+
# Forge v0.5.22
|
|
2
2
|
|
|
3
|
-
Released: 2026-04-
|
|
3
|
+
Released: 2026-04-03
|
|
4
4
|
|
|
5
|
-
## Changes since v0.5.
|
|
5
|
+
## Changes since v0.5.21
|
|
6
|
+
|
|
7
|
+
### Features
|
|
8
|
+
- feat: workspace model config, headless mode for non-claude agents
|
|
9
|
+
- feat: unified terminal picker, model resolution, VibeCoding agent fix
|
|
10
|
+
- feat: smith 'starting' state, terminal picker, boundSessionId preservation
|
|
11
|
+
- feat: playwright plugin supports headed mode (show browser window)
|
|
12
|
+
- feat: QA preset auto-creates playwright config, test dir, and starts dev server
|
|
13
|
+
- feat: Plugin system enhancements — instances, MCP tools, agent integration
|
|
14
|
+
- feat: Pipeline supports plugin nodes (mode: plugin)
|
|
15
|
+
- feat: Plugin system — types, registry, executor, built-in plugins, API
|
|
6
16
|
|
|
7
17
|
### Bug Fixes
|
|
8
|
-
- fix:
|
|
9
|
-
- fix:
|
|
18
|
+
- fix: reduce verbose agent-to-agent notifications
|
|
19
|
+
- fix: plugin config defaults and UI improvements
|
|
20
|
+
- fix: settings agent config save, cliType unification, add form improvements
|
|
21
|
+
- fix: smith restart race condition and session binding improvements
|
|
22
|
+
- fix: terminal-standalone cleanup and correctness fixes
|
|
23
|
+
- fix: workspace-standalone and session-monitor correctness/perf fixes
|
|
24
|
+
- fix: plugin shell executor hardened against child process crashes
|
|
25
|
+
- fix: plugin shell executor uses async exec instead of execSync
|
|
26
|
+
- fix: QA preset uses bash commands as primary, MCP tools as optional
|
|
27
|
+
- fix: playwright plugin uses mode-prefixed actions to avoid shell multiline issues
|
|
28
|
+
- fix: plugin instance config saves schema defaults when user doesn't modify fields
|
|
29
|
+
- fix: playwright check_url falls back to config.base_url when params.url empty
|
|
30
|
+
- fix: plugin instance form uses proper input types (select/boolean/number)
|
|
31
|
+
- fix: plugin system bug fixes from code review
|
|
32
|
+
- fix: plugin executor handles empty cwd + builtin dir fallback to source
|
|
33
|
+
|
|
34
|
+
### Refactoring
|
|
35
|
+
- refactor: orchestrator perf + correctness improvements
|
|
10
36
|
|
|
11
37
|
|
|
12
|
-
**Full Changelog**: https://github.com/aiwatching/forge/compare/v0.5.
|
|
38
|
+
**Full Changelog**: https://github.com/aiwatching/forge/compare/v0.5.21...v0.5.22
|
package/app/api/code/route.ts
CHANGED
|
@@ -96,10 +96,16 @@ export async function GET(req: Request) {
|
|
|
96
96
|
const { execSync } = require('node:child_process');
|
|
97
97
|
const safeQuery = searchQuery.replace(/['"\\]/g, '\\$&');
|
|
98
98
|
// Use grep -rn with limits to prevent huge output
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
99
|
+
let result = '';
|
|
100
|
+
try {
|
|
101
|
+
result = execSync(
|
|
102
|
+
`grep -rn --exclude-dir=node_modules --exclude-dir=.next --exclude-dir=.git --exclude-dir=dist --exclude-dir=build --include='*.ts' --include='*.tsx' --include='*.js' --include='*.jsx' --include='*.py' --include='*.java' --include='*.go' --include='*.rs' --include='*.md' --include='*.json' --include='*.yaml' --include='*.yml' --include='*.css' --include='*.html' --include='*.vue' --include='*.svelte' -m 5 '${safeQuery}' . | head -100`,
|
|
103
|
+
{ cwd: resolvedDir, encoding: 'utf-8', timeout: 10000, stdio: ['pipe', 'pipe', 'pipe'] }
|
|
104
|
+
).trim();
|
|
105
|
+
} catch (e: any) {
|
|
106
|
+
// grep exit code 1 = no match (not an error)
|
|
107
|
+
result = e.stdout?.trim() || '';
|
|
108
|
+
}
|
|
103
109
|
const matches = result ? result.split('\n').map((line: string) => {
|
|
104
110
|
const match = line.match(/^\.\/(.+?):(\d+):(.*)$/);
|
|
105
111
|
if (!match) return null;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server';
|
|
2
|
+
import { listPlugins, getPlugin, installPlugin, uninstallPlugin, updatePluginConfig, listInstalledPlugins, getInstalledPlugin } from '@/lib/plugins/registry';
|
|
3
|
+
import { executePluginAction } from '@/lib/plugins/executor';
|
|
4
|
+
|
|
5
|
+
// GET: list plugins or get plugin details
|
|
6
|
+
export async function GET(req: Request) {
|
|
7
|
+
const url = new URL(req.url);
|
|
8
|
+
const id = url.searchParams.get('id');
|
|
9
|
+
const installed = url.searchParams.get('installed');
|
|
10
|
+
|
|
11
|
+
if (id) {
|
|
12
|
+
// Try as plugin definition first, then as installed instance
|
|
13
|
+
const plugin = getPlugin(id);
|
|
14
|
+
const inst = getInstalledPlugin(id);
|
|
15
|
+
if (!plugin && !inst) return NextResponse.json({ error: 'Plugin not found' }, { status: 404 });
|
|
16
|
+
return NextResponse.json({
|
|
17
|
+
plugin: inst?.definition || plugin,
|
|
18
|
+
installed: !!inst,
|
|
19
|
+
config: inst?.config,
|
|
20
|
+
instanceName: inst?.instanceName,
|
|
21
|
+
source: inst?.source,
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (installed === 'true') {
|
|
26
|
+
return NextResponse.json({ plugins: listInstalledPlugins() });
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return NextResponse.json({ plugins: listPlugins() });
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// POST: install, uninstall, update config, or test a plugin action
|
|
33
|
+
export async function POST(req: Request) {
|
|
34
|
+
const body = await req.json();
|
|
35
|
+
const { action, id, config, actionName, params } = body;
|
|
36
|
+
|
|
37
|
+
switch (action) {
|
|
38
|
+
case 'install': {
|
|
39
|
+
if (!id) return NextResponse.json({ error: 'id required' }, { status: 400 });
|
|
40
|
+
const ok = installPlugin(id, config || {}, body.source ? { source: body.source, name: body.name } : undefined);
|
|
41
|
+
return NextResponse.json({ ok });
|
|
42
|
+
}
|
|
43
|
+
case 'create_instance': {
|
|
44
|
+
const { source, name, instanceId } = body;
|
|
45
|
+
if (!source || !name) return NextResponse.json({ error: 'source and name required' }, { status: 400 });
|
|
46
|
+
const iid = instanceId || name.toLowerCase().replace(/[^a-z0-9]+/g, '-');
|
|
47
|
+
// Check for duplicate ID
|
|
48
|
+
const existing = getInstalledPlugin(iid);
|
|
49
|
+
if (existing) {
|
|
50
|
+
return NextResponse.json({ error: `Instance ID "${iid}" already exists (used by ${existing.instanceName || existing.definition.name}). Choose a different name.` }, { status: 409 });
|
|
51
|
+
}
|
|
52
|
+
const ok = installPlugin(iid, config || {}, { source, name });
|
|
53
|
+
return NextResponse.json({ ok, instanceId: iid });
|
|
54
|
+
}
|
|
55
|
+
case 'uninstall': {
|
|
56
|
+
if (!id) return NextResponse.json({ error: 'id required' }, { status: 400 });
|
|
57
|
+
const ok = uninstallPlugin(id);
|
|
58
|
+
return NextResponse.json({ ok });
|
|
59
|
+
}
|
|
60
|
+
case 'update_config': {
|
|
61
|
+
if (!id || !config) return NextResponse.json({ error: 'id and config required' }, { status: 400 });
|
|
62
|
+
const ok = updatePluginConfig(id, config);
|
|
63
|
+
return NextResponse.json({ ok });
|
|
64
|
+
}
|
|
65
|
+
case 'test': {
|
|
66
|
+
if (!id || !actionName) return NextResponse.json({ error: 'id and actionName required' }, { status: 400 });
|
|
67
|
+
const inst = getInstalledPlugin(id);
|
|
68
|
+
if (!inst) return NextResponse.json({ error: 'Plugin not installed' }, { status: 400 });
|
|
69
|
+
const result = await executePluginAction(inst, actionName, params || {});
|
|
70
|
+
return NextResponse.json(result);
|
|
71
|
+
}
|
|
72
|
+
default:
|
|
73
|
+
return NextResponse.json({ error: 'Unknown action' }, { status: 400 });
|
|
74
|
+
}
|
|
75
|
+
}
|
package/components/Dashboard.tsx
CHANGED
|
@@ -710,6 +710,7 @@ export default function Dashboard({ user }: { user: any }) {
|
|
|
710
710
|
</Suspense>
|
|
711
711
|
)}
|
|
712
712
|
|
|
713
|
+
|
|
713
714
|
{/* Usage */}
|
|
714
715
|
{viewMode === 'usage' && (
|
|
715
716
|
<Suspense fallback={<div className="flex-1 flex items-center justify-center text-[var(--text-secondary)]">Loading...</div>}>
|
|
@@ -37,8 +37,10 @@ function PipelineNode({ id, data }: NodeProps<Node<NodeData>>) {
|
|
|
37
37
|
|
|
38
38
|
<div className="px-3 py-2 border-b border-[#3a3a5a] flex items-center gap-2">
|
|
39
39
|
<span className={`text-[8px] px-1 py-0.5 rounded font-medium ${
|
|
40
|
-
(data as any).mode === 'shell' ? 'bg-yellow-500/20 text-yellow-400' :
|
|
41
|
-
|
|
40
|
+
(data as any).mode === 'shell' ? 'bg-yellow-500/20 text-yellow-400' :
|
|
41
|
+
(data as any).mode === 'plugin' ? 'bg-green-500/20 text-green-400' :
|
|
42
|
+
'bg-purple-500/20 text-purple-400'
|
|
43
|
+
}`}>{(data as any).mode === 'plugin' ? `🔌 ${(data as any).plugin || 'plugin'}` : (data as any).mode === 'shell' ? 'shell' : ((data as any).agent || 'default')}</span>
|
|
42
44
|
<span className="text-xs font-semibold text-white">{data.label}</span>
|
|
43
45
|
<div className="ml-auto flex gap-1">
|
|
44
46
|
<button onClick={() => data.onEdit(id)} className="text-[9px] text-[var(--accent)] hover:text-white">edit</button>
|
|
@@ -66,10 +68,10 @@ const nodeTypes = { pipeline: PipelineNode };
|
|
|
66
68
|
// ─── Node Edit Modal ──────────────────────────────────────
|
|
67
69
|
|
|
68
70
|
function NodeEditModal({ node, projects, agents, onSave, onClose }: {
|
|
69
|
-
node: { id: string; project: string; prompt: string; agent?: string; mode?: string; outputs: { name: string; extract: string }[] };
|
|
71
|
+
node: { id: string; project: string; prompt: string; agent?: string; mode?: string; plugin?: string; pluginAction?: string; pluginParams?: Record<string, any>; pluginWait?: boolean; outputs: { name: string; extract: string }[] };
|
|
70
72
|
projects: { name: string; root: string }[];
|
|
71
73
|
agents: { id: string; name: string }[];
|
|
72
|
-
onSave: (data:
|
|
74
|
+
onSave: (data: any) => void;
|
|
73
75
|
onClose: () => void;
|
|
74
76
|
}) {
|
|
75
77
|
const [id, setId] = useState(node.id);
|
|
@@ -77,6 +79,32 @@ function NodeEditModal({ node, projects, agents, onSave, onClose }: {
|
|
|
77
79
|
const [prompt, setPrompt] = useState(node.prompt);
|
|
78
80
|
const [agent, setAgent] = useState(node.agent || '');
|
|
79
81
|
const [mode, setMode] = useState(node.mode || 'claude');
|
|
82
|
+
// Plugin fields
|
|
83
|
+
const [pluginId, setPluginId] = useState(node.plugin || '');
|
|
84
|
+
const [pluginAction, setPluginAction] = useState(node.pluginAction || '');
|
|
85
|
+
const [pluginParams, setPluginParams] = useState(JSON.stringify(node.pluginParams || {}, null, 2));
|
|
86
|
+
const [pluginWait, setPluginWait] = useState(node.pluginWait || false);
|
|
87
|
+
const [availablePlugins, setAvailablePlugins] = useState<{ id: string; name: string; icon: string; installed: boolean; actions?: Record<string, any> }[]>([]);
|
|
88
|
+
const [selectedPluginDef, setSelectedPluginDef] = useState<any>(null);
|
|
89
|
+
|
|
90
|
+
// Fetch installed plugins
|
|
91
|
+
useEffect(() => {
|
|
92
|
+
fetch('/api/plugins?installed=true')
|
|
93
|
+
.then(r => r.json())
|
|
94
|
+
.then(d => {
|
|
95
|
+
const plugins = (d.plugins || []).map((p: any) => ({
|
|
96
|
+
id: p.id, name: p.definition?.name || p.id, icon: p.definition?.icon || '🔌',
|
|
97
|
+
installed: true, actions: p.definition?.actions || {},
|
|
98
|
+
params: p.definition?.params || {},
|
|
99
|
+
}));
|
|
100
|
+
setAvailablePlugins(plugins);
|
|
101
|
+
if (pluginId) {
|
|
102
|
+
const sel = plugins.find((p: any) => p.id === pluginId);
|
|
103
|
+
if (sel) setSelectedPluginDef(sel);
|
|
104
|
+
}
|
|
105
|
+
})
|
|
106
|
+
.catch(() => {});
|
|
107
|
+
}, []);
|
|
80
108
|
const [outputs, setOutputs] = useState(node.outputs);
|
|
81
109
|
|
|
82
110
|
return (
|
|
@@ -116,11 +144,12 @@ function NodeEditModal({ node, projects, agents, onSave, onClose }: {
|
|
|
116
144
|
<label className="text-[10px] text-gray-400 block mb-1">Mode</label>
|
|
117
145
|
<select
|
|
118
146
|
value={mode}
|
|
119
|
-
onChange={e => setMode(e.target.value)}
|
|
147
|
+
onChange={e => { setMode(e.target.value); if (e.target.value !== 'plugin') { setPluginId(''); setSelectedPluginDef(null); } }}
|
|
120
148
|
className="w-full text-xs bg-[#12122a] border border-[#3a3a5a] rounded px-2 py-1.5 text-white"
|
|
121
149
|
>
|
|
122
150
|
<option value="claude">Agent</option>
|
|
123
151
|
<option value="shell">Shell</option>
|
|
152
|
+
<option value="plugin">Plugin</option>
|
|
124
153
|
</select>
|
|
125
154
|
</div>
|
|
126
155
|
{mode !== 'shell' && (
|
|
@@ -139,8 +168,71 @@ function NodeEditModal({ node, projects, agents, onSave, onClose }: {
|
|
|
139
168
|
</div>
|
|
140
169
|
)}
|
|
141
170
|
</div>
|
|
171
|
+
{/* Plugin config */}
|
|
172
|
+
{mode === 'plugin' && (
|
|
173
|
+
<div className="space-y-2 p-2 bg-[#12122a] rounded border border-[#3a3a5a]">
|
|
174
|
+
<div>
|
|
175
|
+
<label className="text-[10px] text-gray-400 block mb-1">Plugin</label>
|
|
176
|
+
<select
|
|
177
|
+
value={pluginId}
|
|
178
|
+
onChange={e => {
|
|
179
|
+
setPluginId(e.target.value);
|
|
180
|
+
const sel = availablePlugins.find(p => p.id === e.target.value);
|
|
181
|
+
setSelectedPluginDef(sel || null);
|
|
182
|
+
setPluginAction('');
|
|
183
|
+
}}
|
|
184
|
+
className="w-full text-xs bg-[#1e1e3a] border border-[#3a3a5a] rounded px-2 py-1.5 text-white"
|
|
185
|
+
>
|
|
186
|
+
<option value="">Select plugin...</option>
|
|
187
|
+
{availablePlugins.map(p => (
|
|
188
|
+
<option key={p.id} value={p.id}>{p.icon} {p.name}</option>
|
|
189
|
+
))}
|
|
190
|
+
</select>
|
|
191
|
+
{availablePlugins.length === 0 && (
|
|
192
|
+
<div className="text-[9px] text-yellow-400 mt-1">No plugins installed. Install from Settings → Plugins.</div>
|
|
193
|
+
)}
|
|
194
|
+
</div>
|
|
195
|
+
{selectedPluginDef && (
|
|
196
|
+
<>
|
|
197
|
+
<div>
|
|
198
|
+
<label className="text-[10px] text-gray-400 block mb-1">Action</label>
|
|
199
|
+
<select
|
|
200
|
+
value={pluginAction}
|
|
201
|
+
onChange={e => setPluginAction(e.target.value)}
|
|
202
|
+
className="w-full text-xs bg-[#1e1e3a] border border-[#3a3a5a] rounded px-2 py-1.5 text-white"
|
|
203
|
+
>
|
|
204
|
+
<option value="">Default</option>
|
|
205
|
+
{Object.keys(selectedPluginDef.actions || {}).map((a: string) => (
|
|
206
|
+
<option key={a} value={a}>{a}</option>
|
|
207
|
+
))}
|
|
208
|
+
</select>
|
|
209
|
+
</div>
|
|
210
|
+
<div>
|
|
211
|
+
<label className="text-[10px] text-gray-400 block mb-1">Parameters (JSON)</label>
|
|
212
|
+
<textarea
|
|
213
|
+
value={pluginParams}
|
|
214
|
+
onChange={e => setPluginParams(e.target.value)}
|
|
215
|
+
rows={4}
|
|
216
|
+
className="w-full text-xs bg-[#1e1e3a] border border-[#3a3a5a] rounded px-2 py-1.5 text-white font-mono resize-y"
|
|
217
|
+
placeholder='{ "key": "value" }'
|
|
218
|
+
/>
|
|
219
|
+
{selectedPluginDef.params && Object.keys(selectedPluginDef.params).length > 0 && (
|
|
220
|
+
<div className="text-[8px] text-gray-500 mt-0.5">
|
|
221
|
+
Available: {Object.entries(selectedPluginDef.params).map(([k, v]: [string, any]) => `${k}${v.required ? '*' : ''}`).join(', ')}
|
|
222
|
+
</div>
|
|
223
|
+
)}
|
|
224
|
+
</div>
|
|
225
|
+
<div className="flex items-center gap-2">
|
|
226
|
+
<input type="checkbox" id="pluginWait" checked={pluginWait} onChange={e => setPluginWait(e.target.checked)} className="accent-[var(--accent)]" />
|
|
227
|
+
<label htmlFor="pluginWait" className="text-[10px] text-gray-400">Wait for completion (poll)</label>
|
|
228
|
+
</div>
|
|
229
|
+
</>
|
|
230
|
+
)}
|
|
231
|
+
</div>
|
|
232
|
+
)}
|
|
233
|
+
|
|
142
234
|
<div>
|
|
143
|
-
<label className="text-[10px] text-gray-400 block mb-1">Prompt</label>
|
|
235
|
+
<label className="text-[10px] text-gray-400 block mb-1">{mode === 'plugin' ? 'Notes (optional)' : 'Prompt'}</label>
|
|
144
236
|
<textarea
|
|
145
237
|
value={prompt}
|
|
146
238
|
onChange={e => setPrompt(e.target.value)}
|
|
@@ -181,7 +273,18 @@ function NodeEditModal({ node, projects, agents, onSave, onClose }: {
|
|
|
181
273
|
<div className="px-4 py-3 border-t border-[#3a3a5a] flex gap-2 justify-end">
|
|
182
274
|
<button onClick={onClose} className="text-xs px-3 py-1 text-gray-400 hover:text-white">Cancel</button>
|
|
183
275
|
<button
|
|
184
|
-
onClick={() =>
|
|
276
|
+
onClick={() => {
|
|
277
|
+
let parsedParams: Record<string, any> = {};
|
|
278
|
+
try { parsedParams = JSON.parse(pluginParams || '{}'); } catch {}
|
|
279
|
+
onSave({
|
|
280
|
+
id, project, prompt, agent: agent || undefined, mode,
|
|
281
|
+
plugin: mode === 'plugin' ? pluginId : undefined,
|
|
282
|
+
pluginAction: mode === 'plugin' ? pluginAction || undefined : undefined,
|
|
283
|
+
pluginParams: mode === 'plugin' ? parsedParams : undefined,
|
|
284
|
+
pluginWait: mode === 'plugin' ? pluginWait : undefined,
|
|
285
|
+
outputs: outputs.filter(o => o.name),
|
|
286
|
+
});
|
|
287
|
+
}}
|
|
185
288
|
className="text-xs px-3 py-1 bg-[var(--accent)] text-white rounded hover:opacity-90"
|
|
186
289
|
>
|
|
187
290
|
Save
|
|
@@ -201,7 +304,7 @@ export default function PipelineEditor({ onSave, onClose, initialYaml }: {
|
|
|
201
304
|
}) {
|
|
202
305
|
const [nodes, setNodes, onNodesChange] = useNodesState<Node<NodeData>>([]);
|
|
203
306
|
const [edges, setEdges, onEdgesChange] = useEdgesState<Edge>([]);
|
|
204
|
-
const [editingNode, setEditingNode] = useState<{ id: string; project: string; prompt: string; agent?: string; mode?: string; outputs: { name: string; extract: string }[] } | null>(null);
|
|
307
|
+
const [editingNode, setEditingNode] = useState<{ id: string; project: string; prompt: string; agent?: string; mode?: string; plugin?: string; pluginAction?: string; pluginParams?: Record<string, any>; pluginWait?: boolean; outputs: { name: string; extract: string }[] } | null>(null);
|
|
205
308
|
const [workflowName, setWorkflowName] = useState('');
|
|
206
309
|
const [availableAgents, setAvailableAgents] = useState<{ id: string; name: string }[]>([]);
|
|
207
310
|
const [workflowDesc, setWorkflowDesc] = useState('');
|
|
@@ -240,6 +343,12 @@ export default function PipelineEditor({ onSave, onClose, initialYaml }: {
|
|
|
240
343
|
label: id,
|
|
241
344
|
project: def.project || '',
|
|
242
345
|
prompt: def.prompt || '',
|
|
346
|
+
agent: def.agent,
|
|
347
|
+
mode: def.mode || (def.plugin ? 'plugin' : undefined),
|
|
348
|
+
plugin: def.plugin,
|
|
349
|
+
pluginAction: def.plugin_action,
|
|
350
|
+
pluginParams: def.params,
|
|
351
|
+
pluginWait: def.wait,
|
|
243
352
|
outputs: (def.outputs || []).map((o: any) => ({ name: o.name, extract: o.extract || 'result' })),
|
|
244
353
|
onEdit: (nid: string) => handleEditNode(nid),
|
|
245
354
|
onDelete: (nid: string) => handleDeleteNode(nid),
|
|
@@ -297,6 +406,12 @@ export default function PipelineEditor({ onSave, onClose, initialYaml }: {
|
|
|
297
406
|
id: node.id,
|
|
298
407
|
project: node.data.project,
|
|
299
408
|
prompt: node.data.prompt,
|
|
409
|
+
agent: (node.data as any).agent,
|
|
410
|
+
mode: (node.data as any).mode,
|
|
411
|
+
plugin: (node.data as any).plugin,
|
|
412
|
+
pluginAction: (node.data as any).pluginAction,
|
|
413
|
+
pluginParams: (node.data as any).pluginParams,
|
|
414
|
+
pluginWait: (node.data as any).pluginWait,
|
|
300
415
|
outputs: node.data.outputs,
|
|
301
416
|
});
|
|
302
417
|
}
|
|
@@ -309,7 +424,7 @@ export default function PipelineEditor({ onSave, onClose, initialYaml }: {
|
|
|
309
424
|
setEdges(eds => eds.filter(e => e.source !== id && e.target !== id));
|
|
310
425
|
}, [setNodes, setEdges]);
|
|
311
426
|
|
|
312
|
-
const handleSaveNode = useCallback((data: { id: string; project: string; prompt: string; agent?: string; mode?: string; outputs: { name: string; extract: string }[] }) => {
|
|
427
|
+
const handleSaveNode = useCallback((data: { id: string; project: string; prompt: string; agent?: string; mode?: string; plugin?: string; pluginAction?: string; pluginParams?: Record<string, any>; pluginWait?: boolean; outputs: { name: string; extract: string }[] }) => {
|
|
313
428
|
setNodes(nds => nds.map(n => {
|
|
314
429
|
if (n.id === editingNode?.id) {
|
|
315
430
|
return {
|
|
@@ -322,6 +437,10 @@ export default function PipelineEditor({ onSave, onClose, initialYaml }: {
|
|
|
322
437
|
prompt: data.prompt,
|
|
323
438
|
agent: data.agent,
|
|
324
439
|
mode: data.mode,
|
|
440
|
+
plugin: data.plugin,
|
|
441
|
+
pluginAction: data.pluginAction,
|
|
442
|
+
pluginParams: data.pluginParams,
|
|
443
|
+
pluginWait: data.pluginWait,
|
|
325
444
|
outputs: data.outputs,
|
|
326
445
|
},
|
|
327
446
|
};
|
|
@@ -356,6 +475,13 @@ export default function PipelineEditor({ onSave, onClose, initialYaml }: {
|
|
|
356
475
|
prompt: node.data.prompt,
|
|
357
476
|
};
|
|
358
477
|
if ((node.data as any).mode === 'shell') nodeDef.mode = 'shell';
|
|
478
|
+
if ((node.data as any).mode === 'plugin') {
|
|
479
|
+
nodeDef.mode = 'plugin';
|
|
480
|
+
nodeDef.plugin = (node.data as any).plugin;
|
|
481
|
+
if ((node.data as any).pluginAction) nodeDef.plugin_action = (node.data as any).pluginAction;
|
|
482
|
+
if ((node.data as any).pluginParams && Object.keys((node.data as any).pluginParams).length > 0) nodeDef.params = (node.data as any).pluginParams;
|
|
483
|
+
if ((node.data as any).pluginWait) nodeDef.wait = true;
|
|
484
|
+
}
|
|
359
485
|
if ((node.data as any).agent) nodeDef.agent = (node.data as any).agent;
|
|
360
486
|
if (deps.length > 0) nodeDef.depends_on = deps;
|
|
361
487
|
if (node.data.outputs.length > 0) nodeDef.outputs = node.data.outputs;
|