@aion0/forge 0.9.12 → 0.9.13
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/RELEASE_NOTES.md +12 -6
- package/app/api/prompts/[name]/route.ts +37 -0
- package/app/api/prompts/route.ts +35 -0
- package/app/api/schedules/extract/route.ts +184 -0
- package/app/api/schedules/route.ts +14 -37
- package/components/ScheduleCreateModal.tsx +237 -537
- package/components/ScheduleQuickCreate.tsx +404 -0
- package/components/SchedulesView.tsx +18 -6
- package/lib/forge-mcp-server.ts +84 -0
- package/lib/init.ts +7 -0
- package/lib/projects.ts +67 -2
- package/lib/prompts/store.ts +142 -0
- package/lib/prompts/types.ts +53 -0
- package/lib/schedules/scheduler.ts +51 -143
- package/lib/schedules/store.ts +6 -15
- package/lib/schedules/types.ts +10 -14
- package/package.json +1 -1
|
@@ -2,17 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* ScheduleCreateModal — 3-step wizard:
|
|
5
|
-
* 1. Pick body (kind: pipeline |
|
|
6
|
-
* 2. Fill input (schema-driven for pipeline; project +
|
|
7
|
-
* 3. Trigger (period / cron / once / manual + name)
|
|
8
|
-
*
|
|
9
|
-
* V2 Phase 2: pipeline + skill body. connector_tool + action types
|
|
10
|
-
* (chat / email / telegram) land in phases 3-6.
|
|
5
|
+
* 1. Pick body (kind: pipeline | prompt, then ref)
|
|
6
|
+
* 2. Fill input (schema-driven for pipeline; project + agent + skills for prompt)
|
|
7
|
+
* 3. Trigger (period / cron / once / manual + name) + action (none / chat / email / telegram)
|
|
11
8
|
*/
|
|
12
9
|
|
|
13
10
|
import { useEffect, useState } from 'react';
|
|
14
11
|
|
|
15
|
-
type BodyKind = 'pipeline' | '
|
|
12
|
+
type BodyKind = 'pipeline' | 'prompt';
|
|
13
|
+
|
|
14
|
+
interface AgentInfo { id: string; label?: string; backendType?: string; }
|
|
16
15
|
|
|
17
16
|
interface Workflow { name: string; description?: string; }
|
|
18
17
|
interface SkillItem { name: string; displayName?: string; description?: string; installedGlobal?: boolean; installedProjects?: string[]; }
|
|
@@ -28,52 +27,6 @@ interface PipelineInputField {
|
|
|
28
27
|
multiline?: boolean;
|
|
29
28
|
}
|
|
30
29
|
interface PipelineSchema { name: string; description: string | null; input: PipelineInputField[]; }
|
|
31
|
-
interface ConnectorToolParam {
|
|
32
|
-
type?: 'string' | 'number' | 'integer' | 'boolean' | 'object' | 'array' | string;
|
|
33
|
-
label?: string;
|
|
34
|
-
description?: string;
|
|
35
|
-
required?: boolean;
|
|
36
|
-
default?: unknown;
|
|
37
|
-
enum?: unknown[];
|
|
38
|
-
}
|
|
39
|
-
interface ConnectorTool {
|
|
40
|
-
name: string;
|
|
41
|
-
description?: string;
|
|
42
|
-
parameters?: Record<string, ConnectorToolParam>;
|
|
43
|
-
input_schema?: any;
|
|
44
|
-
destructive?: boolean;
|
|
45
|
-
}
|
|
46
|
-
interface ConnectorEntry { id?: string; tools?: Record<string, ConnectorTool>; }
|
|
47
|
-
interface ConnectorPayload {
|
|
48
|
-
plugin_id: string; name: string; description?: string;
|
|
49
|
-
installed: boolean;
|
|
50
|
-
entries?: ConnectorEntry[];
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/** Flatten connector → list of {ref, tool} for the picker.
|
|
54
|
-
* ref = "<plugin_id>.<tool_name>" — matches the body_ref shape the
|
|
55
|
-
* scheduler / dispatchTool expect. */
|
|
56
|
-
function flattenConnectorTools(connectors: ConnectorPayload[]): Array<{
|
|
57
|
-
pluginId: string; pluginName: string; toolName: string; ref: string; tool: ConnectorTool;
|
|
58
|
-
}> {
|
|
59
|
-
const out: Array<{ pluginId: string; pluginName: string; toolName: string; ref: string; tool: ConnectorTool }> = [];
|
|
60
|
-
for (const c of connectors) {
|
|
61
|
-
if (!c.installed) continue;
|
|
62
|
-
for (const e of c.entries || []) {
|
|
63
|
-
const tools = e.tools || {};
|
|
64
|
-
for (const [toolName, t] of Object.entries(tools)) {
|
|
65
|
-
out.push({
|
|
66
|
-
pluginId: c.plugin_id,
|
|
67
|
-
pluginName: c.name || c.plugin_id,
|
|
68
|
-
toolName,
|
|
69
|
-
ref: `${c.plugin_id}.${toolName}`,
|
|
70
|
-
tool: { ...t, name: toolName },
|
|
71
|
-
});
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
return out;
|
|
76
|
-
}
|
|
77
30
|
|
|
78
31
|
/** Subset of Schedule fields needed to seed the modal in edit mode. */
|
|
79
32
|
export interface EditableSchedule {
|
|
@@ -128,37 +81,37 @@ export default function ScheduleCreateModal({ onClose, onCreated, existing }: Pr
|
|
|
128
81
|
const [bodyKind, setBodyKind] = useState<BodyKind>(existing?.body_kind ?? 'pipeline');
|
|
129
82
|
const [bodyRef, setBodyRef] = useState<string>(existing?.body_ref ?? '');
|
|
130
83
|
const [workflows, setWorkflows] = useState<Workflow[]>([]);
|
|
131
|
-
const [skills, setSkills] = useState<SkillItem[]>([]);
|
|
132
84
|
const [projects, setProjects] = useState<ProjectInfo[]>([]);
|
|
133
|
-
const [connectors, setConnectors] = useState<ConnectorPayload[]>([]);
|
|
134
85
|
|
|
135
86
|
// Step 2 — for pipeline body
|
|
136
87
|
const [pipelineSchema, setPipelineSchema] = useState<PipelineSchema | null>(null);
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
const [
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
);
|
|
148
|
-
const [
|
|
88
|
+
|
|
89
|
+
// Step 2 — for prompt body (V3). prompt text + agent + skills are
|
|
90
|
+
// persisted in prompts/<body_ref>.yaml; the schedule row only carries
|
|
91
|
+
// body_ref + input.project. In edit mode we GET the yaml to seed these.
|
|
92
|
+
const [promptProject, setPromptProject] = useState(() => {
|
|
93
|
+
if (existing?.body_kind !== 'prompt') return '';
|
|
94
|
+
const p = String(existing.input?.project ?? '');
|
|
95
|
+
// Backend stores 'scratch' for prompts that opted into the default. Map
|
|
96
|
+
// it back to '' so the "— scratch (default) —" placeholder option matches.
|
|
97
|
+
return p === 'scratch' ? '' : p;
|
|
98
|
+
});
|
|
99
|
+
const [promptText, setPromptText] = useState('');
|
|
100
|
+
const [promptAgent, setPromptAgent] = useState<string>('');
|
|
101
|
+
const [promptSkills, setPromptSkills] = useState<string[]>(['ai-orchestration']);
|
|
102
|
+
const [agents, setAgents] = useState<AgentInfo[]>([]);
|
|
103
|
+
const [promptLoadedRef, setPromptLoadedRef] = useState<string>('');
|
|
149
104
|
|
|
150
105
|
const [input, setInput] = useState<Record<string, string>>(
|
|
151
106
|
existing?.body_kind === 'pipeline' ? inputToStrings(existing.input) : {}
|
|
152
107
|
);
|
|
153
108
|
|
|
154
|
-
// Extra skills attached to the dispatched body (pipeline tasks
|
|
109
|
+
// Extra skills attached to the dispatched body (pipeline tasks).
|
|
155
110
|
// Parallel to Job.skills — forwarded as --append-system-prompt; auto-installed
|
|
156
|
-
// into the target project on dispatch.
|
|
111
|
+
// into the target project on dispatch.
|
|
157
112
|
const [extraSkills, setExtraSkills] = useState<string[]>(Array.isArray(existing?.skills) ? existing!.skills! : []);
|
|
158
113
|
const [extraSkillsPickerOpen, setExtraSkillsPickerOpen] = useState(false);
|
|
159
|
-
//
|
|
160
|
-
// picker (auto-install handles missing ones) but body_kind=skill picker uses
|
|
161
|
-
// a filtered list (separate state `skills`).
|
|
114
|
+
// Auto-install handles missing ones — list ALL skills.
|
|
162
115
|
const [allSkills, setAllSkills] = useState<SkillItem[]>([]);
|
|
163
116
|
|
|
164
117
|
// Step 3
|
|
@@ -209,25 +162,22 @@ export default function ScheduleCreateModal({ onClose, onCreated, existing }: Pr
|
|
|
209
162
|
useEffect(() => {
|
|
210
163
|
(async () => {
|
|
211
164
|
try {
|
|
212
|
-
const [wfR, skR,
|
|
165
|
+
const [wfR, skR, csR, agR] = await Promise.all([
|
|
213
166
|
fetch('/api/pipelines?type=workflows').then((r) => r.json()),
|
|
214
167
|
fetch('/api/skills').then((r) => r.json()),
|
|
215
|
-
fetch('/api/connectors').then((r) => r.json()).catch(() => ({ connectors: [] })),
|
|
216
168
|
fetch('/api/chat-proxy/sessions').then((r) => r.json()).catch(() => ({ sessions: [] })),
|
|
169
|
+
fetch('/api/agents?include=all').then((r) => r.json()).catch(() => ({ agents: [] })),
|
|
217
170
|
]);
|
|
171
|
+
if (Array.isArray(agR?.agents)) setAgents(agR.agents);
|
|
218
172
|
if (Array.isArray(csR?.sessions)) setChatSessions(csR.sessions);
|
|
219
173
|
if (Array.isArray(wfR)) setWorkflows(wfR);
|
|
220
174
|
else if (wfR?.workflows) setWorkflows(wfR.workflows);
|
|
221
175
|
if (Array.isArray(skR?.skills)) {
|
|
222
|
-
// Body picker only shows installed skills (forces user to install first).
|
|
223
|
-
setSkills(skR.skills.filter((s: SkillItem) => s.installedGlobal || (s.installedProjects?.length ?? 0) > 0));
|
|
224
176
|
// Extra-skills picker lists everything — Forge auto-installs missing
|
|
225
177
|
// ones into the target project at dispatch time.
|
|
226
178
|
setAllSkills(skR.skills);
|
|
227
179
|
}
|
|
228
180
|
if (skR?.projects) setProjects(skR.projects);
|
|
229
|
-
const conns: ConnectorPayload[] = Array.isArray(cnR) ? cnR : (cnR?.connectors || []);
|
|
230
|
-
setConnectors(conns.filter((c) => c.installed));
|
|
231
181
|
} catch (e) {
|
|
232
182
|
setErr(e instanceof Error ? e.message : String(e));
|
|
233
183
|
}
|
|
@@ -266,13 +216,27 @@ export default function ScheduleCreateModal({ onClose, onCreated, existing }: Pr
|
|
|
266
216
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
267
217
|
}, [bodyKind, bodyRef]);
|
|
268
218
|
|
|
269
|
-
//
|
|
219
|
+
// Edit mode: load existing prompt yaml so Step 2 form is pre-filled.
|
|
220
|
+
// For new schedules nothing to load; user types the prompt fresh.
|
|
270
221
|
useEffect(() => {
|
|
271
|
-
if (
|
|
272
|
-
|
|
273
|
-
|
|
222
|
+
if (!editing) return;
|
|
223
|
+
if (existing?.body_kind !== 'prompt') return;
|
|
224
|
+
if (promptLoadedRef === existing.body_ref) return;
|
|
225
|
+
(async () => {
|
|
226
|
+
try {
|
|
227
|
+
const r = await fetch(`/api/prompts/${encodeURIComponent(existing.body_ref)}`);
|
|
228
|
+
if (!r.ok) { setErr(`failed to load prompt definition: ${r.status}`); return; }
|
|
229
|
+
const j = await r.json();
|
|
230
|
+
setPromptText(j.prompt ?? '');
|
|
231
|
+
setPromptAgent(j.executor?.agent ?? '');
|
|
232
|
+
setPromptSkills(Array.isArray(j.executor?.skills) ? j.executor.skills : []);
|
|
233
|
+
setPromptLoadedRef(existing.body_ref);
|
|
234
|
+
} catch (e) {
|
|
235
|
+
setErr(e instanceof Error ? e.message : String(e));
|
|
236
|
+
}
|
|
237
|
+
})();
|
|
274
238
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
275
|
-
}, [
|
|
239
|
+
}, [editing, existing?.body_kind, existing?.body_ref]);
|
|
276
240
|
|
|
277
241
|
// Reset body_ref when body_kind switches
|
|
278
242
|
function onBodyKindChange(k: BodyKind) {
|
|
@@ -283,7 +247,13 @@ export default function ScheduleCreateModal({ onClose, onCreated, existing }: Pr
|
|
|
283
247
|
}
|
|
284
248
|
|
|
285
249
|
function canProceed(): boolean {
|
|
286
|
-
if (step === 1)
|
|
250
|
+
if (step === 1) {
|
|
251
|
+
// prompt body has no body_ref picker — the schedule name doubles as
|
|
252
|
+
// the prompt name, derived at submit time. Let user proceed past
|
|
253
|
+
// Step 1 just by picking the tab.
|
|
254
|
+
if (bodyKind === 'prompt') return true;
|
|
255
|
+
return !!bodyRef;
|
|
256
|
+
}
|
|
287
257
|
if (step === 2) {
|
|
288
258
|
if (bodyKind === 'pipeline') {
|
|
289
259
|
if (!pipelineSchema) return false;
|
|
@@ -292,11 +262,9 @@ export default function ScheduleCreateModal({ onClose, onCreated, existing }: Pr
|
|
|
292
262
|
}
|
|
293
263
|
return true;
|
|
294
264
|
}
|
|
295
|
-
if (bodyKind === '
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
if (bodyKind === 'connector_tool') {
|
|
299
|
-
return !toolInputJsonErr;
|
|
265
|
+
if (bodyKind === 'prompt') {
|
|
266
|
+
// project is optional — empty falls back to the synthetic 'scratch' project
|
|
267
|
+
return !!promptText.trim();
|
|
300
268
|
}
|
|
301
269
|
return false;
|
|
302
270
|
}
|
|
@@ -316,17 +284,52 @@ export default function ScheduleCreateModal({ onClose, onCreated, existing }: Pr
|
|
|
316
284
|
setSubmitting(true);
|
|
317
285
|
try {
|
|
318
286
|
let body_input: Record<string, unknown>;
|
|
287
|
+
let promptNameToUse: string | null = null;
|
|
319
288
|
if (bodyKind === 'pipeline') {
|
|
320
289
|
body_input = input;
|
|
321
|
-
} else if (bodyKind === '
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
290
|
+
} else if (bodyKind === 'prompt') {
|
|
291
|
+
// Create/update the prompts/<name>.yaml first; schedule references it by name.
|
|
292
|
+
// In edit mode body_ref is locked and refers to the existing yaml — PATCH it.
|
|
293
|
+
// In create mode derive name from schedule name (slugify) and POST with
|
|
294
|
+
// automatic suffix retry on 409.
|
|
295
|
+
const skillsClean = promptSkills.filter((s) => s && s.trim());
|
|
296
|
+
const executor = { agent: promptAgent || null, skills: skillsClean };
|
|
297
|
+
if (editing && existing?.body_kind === 'prompt') {
|
|
298
|
+
promptNameToUse = existing.body_ref;
|
|
299
|
+
const r = await fetch(`/api/prompts/${encodeURIComponent(existing.body_ref)}`, {
|
|
300
|
+
method: 'PATCH',
|
|
301
|
+
headers: { 'Content-Type': 'application/json' },
|
|
302
|
+
body: JSON.stringify({ prompt: promptText, executor }),
|
|
303
|
+
});
|
|
304
|
+
if (!r.ok) {
|
|
305
|
+
const j = await r.json().catch(() => ({}));
|
|
306
|
+
setErr(j.error || `prompt yaml save failed (${r.status})`);
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
} else {
|
|
310
|
+
const base = slugifyName(name) || `prompt-${Date.now().toString(36).slice(-6)}`;
|
|
311
|
+
let candidate = base;
|
|
312
|
+
for (let i = 2; i <= 50; i++) {
|
|
313
|
+
const r = await fetch('/api/prompts', {
|
|
314
|
+
method: 'POST',
|
|
315
|
+
headers: { 'Content-Type': 'application/json' },
|
|
316
|
+
body: JSON.stringify({ name: candidate, prompt: promptText, executor }),
|
|
317
|
+
});
|
|
318
|
+
if (r.ok) { promptNameToUse = candidate; break; }
|
|
319
|
+
if (r.status === 409) { candidate = `${base}-${i}`; continue; }
|
|
320
|
+
const j = await r.json().catch(() => ({}));
|
|
321
|
+
setErr(j.error || `prompt yaml create failed (${r.status})`);
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
if (!promptNameToUse) {
|
|
325
|
+
setErr(`couldn't pick a free prompt name starting from "${base}"`);
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
329
328
|
}
|
|
329
|
+
body_input = { project: promptProject };
|
|
330
|
+
} else {
|
|
331
|
+
setErr(`unsupported body_kind: ${bodyKind}`);
|
|
332
|
+
return;
|
|
330
333
|
}
|
|
331
334
|
const action_config: Record<string, unknown> = {};
|
|
332
335
|
if (actionKind === 'chat') {
|
|
@@ -344,9 +347,7 @@ export default function ScheduleCreateModal({ onClose, onCreated, existing }: Pr
|
|
|
344
347
|
const body: any = {
|
|
345
348
|
name: name.trim(),
|
|
346
349
|
input: body_input,
|
|
347
|
-
|
|
348
|
-
// PATCH doesn't preserve a stale value from before kind switch.
|
|
349
|
-
skills: bodyKind === 'connector_tool' ? [] : extraSkills,
|
|
350
|
+
skills: extraSkills,
|
|
350
351
|
action_kind: actionKind,
|
|
351
352
|
action_config,
|
|
352
353
|
schedule_kind: trigger,
|
|
@@ -357,7 +358,7 @@ export default function ScheduleCreateModal({ onClose, onCreated, existing }: Pr
|
|
|
357
358
|
};
|
|
358
359
|
if (!editing) {
|
|
359
360
|
body.body_kind = bodyKind;
|
|
360
|
-
body.body_ref = bodyRef;
|
|
361
|
+
body.body_ref = bodyKind === 'prompt' ? promptNameToUse! : bodyRef;
|
|
361
362
|
body.enabled = true;
|
|
362
363
|
}
|
|
363
364
|
const url = editing ? `/api/schedules/${encodeURIComponent(existing!.id)}` : '/api/schedules';
|
|
@@ -395,35 +396,25 @@ export default function ScheduleCreateModal({ onClose, onCreated, existing }: Pr
|
|
|
395
396
|
<Step1
|
|
396
397
|
bodyKind={bodyKind} onBodyKindChange={onBodyKindChange}
|
|
397
398
|
bodyRef={bodyRef} onBodyRefChange={setBodyRef}
|
|
398
|
-
workflows={workflows}
|
|
399
|
-
projects={projects}
|
|
400
|
-
skillProject={skillProject} onSkillProject={setSkillProject}
|
|
399
|
+
workflows={workflows}
|
|
401
400
|
/>
|
|
402
401
|
)}
|
|
403
402
|
{step === 2 && bodyKind === 'pipeline' && pipelineSchema && (
|
|
404
403
|
<Step2Pipeline schema={pipelineSchema} input={input} onChange={setInput} />
|
|
405
404
|
)}
|
|
406
|
-
{step === 2 && bodyKind === '
|
|
407
|
-
<
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
toolRef={bodyRef}
|
|
417
|
-
json={toolInputJson}
|
|
418
|
-
onJsonChange={(v) => {
|
|
419
|
-
setToolInputJson(v);
|
|
420
|
-
try { JSON.parse(v || '{}'); setToolInputJsonErr(''); }
|
|
421
|
-
catch (e) { setToolInputJsonErr((e as Error).message); }
|
|
422
|
-
}}
|
|
423
|
-
jsonErr={toolInputJsonErr}
|
|
405
|
+
{step === 2 && bodyKind === 'prompt' && (
|
|
406
|
+
<Step2Prompt
|
|
407
|
+
projects={projects}
|
|
408
|
+
project={promptProject} onProject={setPromptProject}
|
|
409
|
+
promptText={promptText} onPromptText={setPromptText}
|
|
410
|
+
agent={promptAgent} onAgent={setPromptAgent}
|
|
411
|
+
agents={agents}
|
|
412
|
+
skills={promptSkills} onSkills={setPromptSkills}
|
|
413
|
+
allSkills={allSkills}
|
|
414
|
+
editing={editing}
|
|
424
415
|
/>
|
|
425
416
|
)}
|
|
426
|
-
{step === 2 && bodyKind
|
|
417
|
+
{step === 2 && bodyKind === 'pipeline' && (
|
|
427
418
|
<ExtraSkillsSection
|
|
428
419
|
skills={extraSkills}
|
|
429
420
|
onChange={setExtraSkills}
|
|
@@ -514,14 +505,11 @@ function Stepper({ step }: { step: 1 | 2 | 3 }) {
|
|
|
514
505
|
function Step1({
|
|
515
506
|
bodyKind, onBodyKindChange,
|
|
516
507
|
bodyRef, onBodyRefChange,
|
|
517
|
-
workflows,
|
|
518
|
-
projects, skillProject, onSkillProject,
|
|
508
|
+
workflows,
|
|
519
509
|
}: {
|
|
520
510
|
bodyKind: BodyKind; onBodyKindChange: (k: BodyKind) => void;
|
|
521
511
|
bodyRef: string; onBodyRefChange: (n: string) => void;
|
|
522
|
-
workflows: Workflow[];
|
|
523
|
-
projects: ProjectInfo[];
|
|
524
|
-
skillProject: string; onSkillProject: (n: string) => void;
|
|
512
|
+
workflows: Workflow[];
|
|
525
513
|
}) {
|
|
526
514
|
return (
|
|
527
515
|
<div>
|
|
@@ -529,7 +517,7 @@ function Step1({
|
|
|
529
517
|
|
|
530
518
|
{/* Body kind tabs */}
|
|
531
519
|
<div className="flex gap-1 mb-3 border-b border-[var(--border)]">
|
|
532
|
-
{(['pipeline', '
|
|
520
|
+
{(['pipeline', 'prompt'] as BodyKind[]).map((k) => (
|
|
533
521
|
<button
|
|
534
522
|
key={k}
|
|
535
523
|
onClick={() => onBodyKindChange(k)}
|
|
@@ -539,9 +527,7 @@ function Step1({
|
|
|
539
527
|
: 'border-transparent text-[var(--text-secondary)] hover:text-[var(--text-primary)]'
|
|
540
528
|
}`}
|
|
541
529
|
>
|
|
542
|
-
{k === 'pipeline' ? '📋 Pipeline'
|
|
543
|
-
: k === 'skill' ? '🧩 Skill'
|
|
544
|
-
: '🔌 Connector Tool'}
|
|
530
|
+
{k === 'pipeline' ? '📋 Pipeline' : '💬 Prompt'}
|
|
545
531
|
</button>
|
|
546
532
|
))}
|
|
547
533
|
</div>
|
|
@@ -558,108 +544,12 @@ function Step1({
|
|
|
558
544
|
))}
|
|
559
545
|
</div>
|
|
560
546
|
)
|
|
561
|
-
) : bodyKind === 'skill' ? (
|
|
562
|
-
// Skill body: project first (required), then skill list scoped to that project (or global).
|
|
563
|
-
<SkillPicker
|
|
564
|
-
skills={skills} projects={projects}
|
|
565
|
-
skillProject={skillProject} onSkillProject={onSkillProject}
|
|
566
|
-
bodyRef={bodyRef} onBodyRefChange={onBodyRefChange}
|
|
567
|
-
/>
|
|
568
547
|
) : (
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
return (
|
|
575
|
-
<div className="text-[11px] text-[var(--text-secondary)]">
|
|
576
|
-
No installed connectors with tools. Install one from Settings → Connectors first.
|
|
577
|
-
</div>
|
|
578
|
-
);
|
|
579
|
-
}
|
|
580
|
-
// Group by plugin for nice section headers.
|
|
581
|
-
const byPlugin = new Map<string, { name: string; tools: typeof all }>();
|
|
582
|
-
for (const x of all) {
|
|
583
|
-
if (!byPlugin.has(x.pluginId)) byPlugin.set(x.pluginId, { name: x.pluginName, tools: [] });
|
|
584
|
-
byPlugin.get(x.pluginId)!.tools.push(x);
|
|
585
|
-
}
|
|
586
|
-
return (
|
|
587
|
-
<div className="space-y-3">
|
|
588
|
-
{Array.from(byPlugin.entries()).map(([pluginId, group]) => (
|
|
589
|
-
<div key={pluginId}>
|
|
590
|
-
<div className="text-[10px] font-semibold text-[var(--text-secondary)] uppercase mb-1">{group.name}</div>
|
|
591
|
-
<div className="space-y-1.5">
|
|
592
|
-
{group.tools.map(({ ref, tool }) => (
|
|
593
|
-
<Card key={ref}
|
|
594
|
-
title={ref}
|
|
595
|
-
desc={(tool.description || '') + (tool.destructive ? ' ⚠ destructive' : '')}
|
|
596
|
-
checked={bodyRef === ref}
|
|
597
|
-
onPick={() => onBodyRefChange(ref)} />
|
|
598
|
-
))}
|
|
599
|
-
</div>
|
|
600
|
-
</div>
|
|
601
|
-
))}
|
|
602
|
-
</div>
|
|
603
|
-
);
|
|
604
|
-
})()
|
|
605
|
-
)}
|
|
606
|
-
</div>
|
|
607
|
-
);
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
function SkillPicker({ skills, projects, skillProject, onSkillProject, bodyRef, onBodyRefChange }: {
|
|
611
|
-
skills: SkillItem[]; projects: ProjectInfo[];
|
|
612
|
-
skillProject: string; onSkillProject: (n: string) => void;
|
|
613
|
-
bodyRef: string; onBodyRefChange: (n: string) => void;
|
|
614
|
-
}) {
|
|
615
|
-
// Filter skills to those installed globally OR in the chosen project.
|
|
616
|
-
// If no project selected yet, show nothing in the list (prompt user to pick project first).
|
|
617
|
-
const visibleSkills = skillProject
|
|
618
|
-
? skills.filter((s) => s.installedGlobal || (s.installedProjects || []).includes(skillProject)
|
|
619
|
-
|| (s.installedProjects || []).some((p) => p.endsWith('/' + skillProject) || p === skillProject))
|
|
620
|
-
: [];
|
|
621
|
-
return (
|
|
622
|
-
<div>
|
|
623
|
-
<label className="block mb-3">
|
|
624
|
-
<div className="flex items-baseline gap-1 text-[11px] mb-0.5">
|
|
625
|
-
<span className="font-mono">project</span>
|
|
626
|
-
<span className="text-[var(--red)]">*</span>
|
|
627
|
-
</div>
|
|
628
|
-
<select
|
|
629
|
-
value={skillProject}
|
|
630
|
-
onChange={(e) => { onSkillProject(e.target.value); onBodyRefChange(''); }}
|
|
631
|
-
className="w-full text-[11px] font-mono px-2 py-1 border border-[var(--border)] rounded bg-[var(--bg-secondary)]"
|
|
632
|
-
>
|
|
633
|
-
<option value="">— pick a project —</option>
|
|
634
|
-
{projects.map((p) => (
|
|
635
|
-
<option key={p.name} value={p.name}>{p.name}</option>
|
|
636
|
-
))}
|
|
637
|
-
</select>
|
|
638
|
-
<div className="text-[10px] text-[var(--text-secondary)] mt-0.5">
|
|
639
|
-
Working directory for the Claude task. Skills installed in this project + global skills will appear below.
|
|
640
|
-
</div>
|
|
641
|
-
</label>
|
|
642
|
-
|
|
643
|
-
<div className="text-[10px] font-semibold text-[var(--text-secondary)] uppercase mb-1">
|
|
644
|
-
Skill
|
|
645
|
-
{skillProject && visibleSkills.length > 0 && (
|
|
646
|
-
<span className="ml-2 normal-case text-[var(--text-secondary)] font-normal">
|
|
647
|
-
({visibleSkills.length} available in <code className="font-mono">{skillProject}</code>)
|
|
648
|
-
</span>
|
|
649
|
-
)}
|
|
650
|
-
</div>
|
|
651
|
-
{!skillProject ? (
|
|
652
|
-
<div className="text-[11px] text-[var(--text-secondary)] italic">Pick a project first.</div>
|
|
653
|
-
) : visibleSkills.length === 0 ? (
|
|
654
|
-
<div className="text-[11px] text-[var(--text-secondary)]">
|
|
655
|
-
No skills installed globally or in <code className="font-mono">{skillProject}</code>. Install one from Settings → Skills.
|
|
656
|
-
</div>
|
|
657
|
-
) : (
|
|
658
|
-
<div className="space-y-1.5">
|
|
659
|
-
{visibleSkills.map((s) => (
|
|
660
|
-
<Card key={s.name} title={s.name} desc={s.description}
|
|
661
|
-
checked={bodyRef === s.name} onPick={() => onBodyRefChange(s.name)} />
|
|
662
|
-
))}
|
|
548
|
+
<div className="text-[11px] text-[var(--text-secondary)] p-3 border border-dashed border-[var(--border)] rounded">
|
|
549
|
+
Prompt mode runs a one-shot Claude task with your prompt text + chosen agent + skills.
|
|
550
|
+
You'll author the prompt on the next step. The schedule's name becomes the prompt's
|
|
551
|
+
filename (<code className="font-mono">prompts/<name>.yaml</code>) so future edits stay
|
|
552
|
+
in one place.
|
|
663
553
|
</div>
|
|
664
554
|
)}
|
|
665
555
|
</div>
|
|
@@ -812,335 +702,145 @@ function PipelineInputControl({ field, value, onChange }: {
|
|
|
812
702
|
);
|
|
813
703
|
}
|
|
814
704
|
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
return (
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
project <code className="font-mono">{project}</code>.
|
|
824
|
-
</div>
|
|
825
|
-
|
|
826
|
-
<label className="block">
|
|
827
|
-
<div className="flex items-baseline gap-1 text-[11px] mb-0.5">
|
|
828
|
-
<span className="font-mono">user_prompt</span>
|
|
829
|
-
<span className="text-[var(--red)]">*</span>
|
|
830
|
-
</div>
|
|
831
|
-
<textarea
|
|
832
|
-
rows={6}
|
|
833
|
-
value={userPrompt}
|
|
834
|
-
onChange={(e) => onUserPrompt(e.target.value)}
|
|
835
|
-
placeholder="e.g. Fetch all my open Mantis bugs, summarize by category"
|
|
836
|
-
className="w-full text-[11px] font-mono px-2 py-1 border border-[var(--border)] rounded bg-[var(--bg-secondary)]"
|
|
837
|
-
/>
|
|
838
|
-
<div className="text-[10px] text-[var(--text-secondary)] mt-0.5">
|
|
839
|
-
The user message sent to Claude. The skill's SKILL.md prompts Claude on how to handle it.
|
|
840
|
-
</div>
|
|
841
|
-
</label>
|
|
842
|
-
</div>
|
|
843
|
-
);
|
|
705
|
+
// Lower-case, alphanumeric + dash. Used to derive the prompts/<name>.yaml
|
|
706
|
+
// filename from the schedule's display name. The store's name regex is
|
|
707
|
+
// `[a-zA-Z0-9][a-zA-Z0-9._-]{0,63}` so this is a strict subset.
|
|
708
|
+
function slugifyName(s: string): string {
|
|
709
|
+
return s.toLowerCase()
|
|
710
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
711
|
+
.replace(/^-+|-+$/g, '')
|
|
712
|
+
.slice(0, 50);
|
|
844
713
|
}
|
|
845
714
|
|
|
846
|
-
function
|
|
847
|
-
|
|
848
|
-
|
|
715
|
+
function Step2Prompt({
|
|
716
|
+
projects, project, onProject,
|
|
717
|
+
promptText, onPromptText,
|
|
718
|
+
agent, onAgent, agents,
|
|
719
|
+
skills, onSkills, allSkills,
|
|
720
|
+
editing,
|
|
721
|
+
}: {
|
|
722
|
+
projects: ProjectInfo[];
|
|
723
|
+
project: string; onProject: (v: string) => void;
|
|
724
|
+
promptText: string; onPromptText: (v: string) => void;
|
|
725
|
+
agent: string; onAgent: (v: string) => void;
|
|
726
|
+
agents: AgentInfo[];
|
|
727
|
+
skills: string[]; onSkills: (v: string[]) => void;
|
|
728
|
+
allSkills: SkillItem[];
|
|
729
|
+
editing: boolean;
|
|
849
730
|
}) {
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
const params = tool?.parameters || {};
|
|
853
|
-
const paramNames = Object.keys(params);
|
|
854
|
-
|
|
855
|
-
// Parse json → object for the form to read. If json is invalid the
|
|
856
|
-
// form shows current "field" values from whatever we last parsed
|
|
857
|
-
// successfully (the editor is the source of truth either way).
|
|
858
|
-
const parsed: Record<string, unknown> = (() => {
|
|
859
|
-
try { const v = JSON.parse(json || '{}'); return v && typeof v === 'object' && !Array.isArray(v) ? v : {}; }
|
|
860
|
-
catch { return {}; }
|
|
861
|
-
})();
|
|
862
|
-
|
|
863
|
-
// Toggle: form vs raw json. Default to form if the tool declares
|
|
864
|
-
// parameters; fall back to raw JSON for tools without a schema.
|
|
865
|
-
const [showRaw, setShowRaw] = useState(paramNames.length === 0);
|
|
866
|
-
|
|
867
|
-
// Test result panel — populated by the Test button below the form.
|
|
868
|
-
const [testing, setTesting] = useState(false);
|
|
869
|
-
const [testResult, setTestResult] = useState<null | {
|
|
870
|
-
ok: boolean;
|
|
871
|
-
is_error: boolean;
|
|
872
|
-
content: string;
|
|
873
|
-
duration_ms: number;
|
|
874
|
-
error?: string;
|
|
875
|
-
}>(null);
|
|
876
|
-
const pluginId = toolRef.split('.')[0] || '';
|
|
877
|
-
const toolName = toolRef.split('.').slice(1).join('.');
|
|
878
|
-
async function runTest() {
|
|
879
|
-
setTesting(true);
|
|
880
|
-
setTestResult(null);
|
|
881
|
-
try {
|
|
882
|
-
const r = await fetch('/api/connectors/tool-test', {
|
|
883
|
-
method: 'POST',
|
|
884
|
-
headers: { 'Content-Type': 'application/json' },
|
|
885
|
-
body: JSON.stringify({ plugin_id: pluginId, tool: toolName, input: parsed }),
|
|
886
|
-
});
|
|
887
|
-
const j = await r.json();
|
|
888
|
-
setTestResult(j);
|
|
889
|
-
} catch (e) {
|
|
890
|
-
setTestResult({ ok: false, is_error: true, content: '', duration_ms: 0, error: (e as Error).message });
|
|
891
|
-
} finally { setTesting(false); }
|
|
892
|
-
}
|
|
893
|
-
|
|
894
|
-
function updateField(name: string, value: unknown) {
|
|
895
|
-
const next = { ...parsed };
|
|
896
|
-
// Drop empty optional fields so they don't pollute the payload.
|
|
897
|
-
if (value === '' || value === null || value === undefined) delete next[name];
|
|
898
|
-
else next[name] = value;
|
|
899
|
-
onJsonChange(JSON.stringify(next, null, 2));
|
|
731
|
+
function toggleSkill(name: string) {
|
|
732
|
+
onSkills(skills.includes(name) ? skills.filter((s) => s !== name) : [...skills, name]);
|
|
900
733
|
}
|
|
901
|
-
|
|
734
|
+
// Surface a small curated subset by default; the picker further down
|
|
735
|
+
// lets the user add anything. ai-orchestration is the V3 default so a
|
|
736
|
+
// prompt task can call connectors.
|
|
737
|
+
const featured = ['ai-orchestration', 'temper-memory', 'knowledge-base'];
|
|
738
|
+
const featuredAvailable = featured.filter((n) =>
|
|
739
|
+
skills.includes(n) || allSkills.some((s) => s.name === n)
|
|
740
|
+
);
|
|
741
|
+
const extras = skills.filter((s) => !featured.includes(s));
|
|
902
742
|
return (
|
|
903
743
|
<div>
|
|
904
744
|
<div className="text-[11px] text-[var(--text-secondary)] mb-3">
|
|
905
|
-
Step 2 / 3 —
|
|
906
|
-
|
|
745
|
+
Step 2 / 3 — Author the prompt task.{' '}
|
|
746
|
+
{editing && <span>(Editing existing prompt definition.)</span>}
|
|
907
747
|
</div>
|
|
908
748
|
|
|
909
|
-
|
|
910
|
-
<div className="text-[
|
|
911
|
-
)}
|
|
912
|
-
{tool?.destructive && (
|
|
913
|
-
<div className="text-[10px] text-[var(--yellow)] bg-[var(--yellow)]/10 rounded p-2 mb-3">
|
|
914
|
-
⚠ This tool is marked <code className="font-mono">destructive</code>. Schedule will invoke it
|
|
915
|
-
every trigger — make sure that's what you want.
|
|
916
|
-
</div>
|
|
917
|
-
)}
|
|
918
|
-
|
|
919
|
-
{/* mode toggle */}
|
|
920
|
-
{paramNames.length > 0 && (
|
|
921
|
-
<div className="flex gap-2 mb-3 text-[10px]">
|
|
922
|
-
<button
|
|
923
|
-
onClick={() => setShowRaw(false)}
|
|
924
|
-
className={`px-2 py-0.5 rounded border ${!showRaw ? 'border-[var(--text-primary)] bg-[var(--bg-secondary)]' : 'border-[var(--border)] text-[var(--text-secondary)]'}`}
|
|
925
|
-
>Form</button>
|
|
926
|
-
<button
|
|
927
|
-
onClick={() => setShowRaw(true)}
|
|
928
|
-
className={`px-2 py-0.5 rounded border ${showRaw ? 'border-[var(--text-primary)] bg-[var(--bg-secondary)]' : 'border-[var(--border)] text-[var(--text-secondary)]'}`}
|
|
929
|
-
>Raw JSON</button>
|
|
930
|
-
</div>
|
|
931
|
-
)}
|
|
932
|
-
|
|
933
|
-
{/* form mode */}
|
|
934
|
-
{!showRaw && paramNames.length > 0 && (
|
|
935
|
-
<div className="space-y-3">
|
|
936
|
-
{paramNames.map((name) => {
|
|
937
|
-
const p = params[name];
|
|
938
|
-
const cur = parsed[name];
|
|
939
|
-
return (
|
|
940
|
-
<ConnectorToolField
|
|
941
|
-
key={name}
|
|
942
|
-
name={name}
|
|
943
|
-
param={p}
|
|
944
|
-
value={cur}
|
|
945
|
-
onChange={(v) => updateField(name, v)}
|
|
946
|
-
/>
|
|
947
|
-
);
|
|
948
|
-
})}
|
|
949
|
-
{jsonErr && (
|
|
950
|
-
<div className="text-[10px] text-[var(--red)]">underlying JSON is invalid: {jsonErr}</div>
|
|
951
|
-
)}
|
|
952
|
-
</div>
|
|
953
|
-
)}
|
|
954
|
-
|
|
955
|
-
{/* raw json mode (or fallback when tool has no parameters) */}
|
|
956
|
-
{(showRaw || paramNames.length === 0) && (
|
|
957
|
-
<label className="block">
|
|
958
|
-
<div className="flex items-baseline gap-1 text-[11px] mb-0.5">
|
|
959
|
-
<span className="font-mono">input (JSON)</span>
|
|
960
|
-
</div>
|
|
961
|
-
<textarea
|
|
962
|
-
rows={8}
|
|
963
|
-
value={json}
|
|
964
|
-
onChange={(e) => onJsonChange(e.target.value)}
|
|
965
|
-
placeholder='{ "bug_id": 1213844 }'
|
|
966
|
-
spellCheck={false}
|
|
967
|
-
className={`w-full text-[11px] font-mono px-2 py-1 border rounded bg-[var(--bg-secondary)] ${
|
|
968
|
-
jsonErr ? 'border-[var(--red)]' : 'border-[var(--border)]'
|
|
969
|
-
}`}
|
|
970
|
-
/>
|
|
971
|
-
{jsonErr && (
|
|
972
|
-
<div className="text-[10px] text-[var(--red)] mt-0.5">JSON parse error: {jsonErr}</div>
|
|
973
|
-
)}
|
|
974
|
-
<div className="text-[10px] text-[var(--text-secondary)] mt-0.5">
|
|
975
|
-
Passed verbatim as the tool's input. Leave as <code className="font-mono">{`{}`}</code> if the tool takes no args.
|
|
976
|
-
</div>
|
|
977
|
-
</label>
|
|
978
|
-
)}
|
|
979
|
-
|
|
980
|
-
{/* Test button + result — actually dispatch the tool with the current
|
|
981
|
-
input so the user can validate before saving. */}
|
|
982
|
-
<div className="mt-3 pt-3 border-t border-[var(--border)]">
|
|
983
|
-
<div className="flex items-center gap-2 mb-2">
|
|
984
|
-
<button
|
|
985
|
-
onClick={() => void runTest()}
|
|
986
|
-
disabled={testing || !!jsonErr}
|
|
987
|
-
className="text-[11px] px-3 py-1 border border-[var(--border)] rounded hover:bg-[var(--bg-secondary)] disabled:opacity-50"
|
|
988
|
-
>{testing ? 'Testing…' : '▶ Test'}</button>
|
|
989
|
-
<span className="text-[10px] text-[var(--text-secondary)]">
|
|
990
|
-
Dispatch the tool once with the current input. Read-only check — doesn't save the schedule.
|
|
991
|
-
</span>
|
|
992
|
-
</div>
|
|
993
|
-
{testResult && (
|
|
994
|
-
<div className={`text-[10px] rounded p-2 ${
|
|
995
|
-
testResult.ok ? 'bg-[var(--green)]/10' : 'bg-[var(--red)]/10'
|
|
996
|
-
}`}>
|
|
997
|
-
<div className={testResult.ok ? 'text-[var(--green)]' : 'text-[var(--red)]'}>
|
|
998
|
-
{testResult.ok ? '✓ ok' : '✗ failed'} · {testResult.duration_ms}ms
|
|
999
|
-
{testResult.error && <> · {testResult.error}</>}
|
|
1000
|
-
</div>
|
|
1001
|
-
{testResult.content && (
|
|
1002
|
-
<pre className="mt-1 font-mono whitespace-pre-wrap break-words max-h-60 overflow-auto text-[var(--text-primary)]">
|
|
1003
|
-
{testResult.content}
|
|
1004
|
-
</pre>
|
|
1005
|
-
)}
|
|
1006
|
-
</div>
|
|
1007
|
-
)}
|
|
1008
|
-
</div>
|
|
1009
|
-
</div>
|
|
1010
|
-
);
|
|
1011
|
-
}
|
|
1012
|
-
|
|
1013
|
-
function ConnectorToolField({ name, param, value, onChange }: {
|
|
1014
|
-
name: string; param: ConnectorToolParam; value: unknown;
|
|
1015
|
-
onChange: (v: unknown) => void;
|
|
1016
|
-
}) {
|
|
1017
|
-
const label = param.label || name;
|
|
1018
|
-
const type = (param.type || 'string').toLowerCase();
|
|
1019
|
-
const required = !!param.required;
|
|
1020
|
-
const hint = param.description;
|
|
1021
|
-
|
|
1022
|
-
const labelEl = (
|
|
1023
|
-
<div className="flex items-baseline gap-1 text-[11px] mb-0.5">
|
|
1024
|
-
<span className="font-mono">{name}</span>
|
|
1025
|
-
{required && <span className="text-[var(--red)]">*</span>}
|
|
1026
|
-
{param.label && <span className="text-[10px] text-[var(--text-secondary)]">— {label}</span>}
|
|
1027
|
-
</div>
|
|
1028
|
-
);
|
|
1029
|
-
|
|
1030
|
-
// enum → select
|
|
1031
|
-
if (Array.isArray(param.enum) && param.enum.length > 0) {
|
|
1032
|
-
const cur = value == null ? '' : String(value);
|
|
1033
|
-
return (
|
|
1034
|
-
<label className="block">
|
|
1035
|
-
{labelEl}
|
|
749
|
+
<label className="block mb-3">
|
|
750
|
+
<div className="text-[11px] mb-0.5"><span className="font-mono">project</span></div>
|
|
1036
751
|
<select
|
|
1037
|
-
value={
|
|
1038
|
-
onChange={(e) =>
|
|
752
|
+
value={project}
|
|
753
|
+
onChange={(e) => onProject(e.target.value)}
|
|
1039
754
|
className="w-full text-[11px] font-mono px-2 py-1 border border-[var(--border)] rounded bg-[var(--bg-secondary)]"
|
|
1040
755
|
>
|
|
1041
|
-
<option value="">—
|
|
1042
|
-
{
|
|
1043
|
-
<option key={
|
|
756
|
+
<option value="">— scratch (default) —</option>
|
|
757
|
+
{projects.filter((p) => p.name !== 'scratch').map((p) => (
|
|
758
|
+
<option key={p.name} value={p.name}>{p.name}</option>
|
|
1044
759
|
))}
|
|
1045
760
|
</select>
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
}
|
|
1050
|
-
|
|
1051
|
-
// boolean → checkbox
|
|
1052
|
-
if (type === 'boolean') {
|
|
1053
|
-
return (
|
|
1054
|
-
<label className="block">
|
|
1055
|
-
{labelEl}
|
|
1056
|
-
<label className="flex items-center gap-2 text-[11px]">
|
|
1057
|
-
<input
|
|
1058
|
-
type="checkbox"
|
|
1059
|
-
checked={!!value}
|
|
1060
|
-
onChange={(e) => onChange(e.target.checked || undefined)}
|
|
1061
|
-
/>
|
|
1062
|
-
<span className="text-[11px] text-[var(--text-secondary)]">{value ? 'true' : 'false'}</span>
|
|
1063
|
-
</label>
|
|
1064
|
-
{hint && <div className="text-[10px] text-[var(--text-secondary)] mt-0.5">{hint}</div>}
|
|
761
|
+
<div className="text-[10px] text-[var(--text-secondary)] mt-0.5">
|
|
762
|
+
Working directory for the Claude task. Leave on default for prompts that just talk to connectors / APIs and don't need a real project on disk.
|
|
763
|
+
</div>
|
|
1065
764
|
</label>
|
|
1066
|
-
);
|
|
1067
|
-
}
|
|
1068
765
|
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
const v = e.target.value;
|
|
1081
|
-
if (v === '') onChange(undefined);
|
|
1082
|
-
else {
|
|
1083
|
-
const n = type === 'integer' ? parseInt(v, 10) : parseFloat(v);
|
|
1084
|
-
onChange(Number.isFinite(n) ? n : undefined);
|
|
1085
|
-
}
|
|
1086
|
-
}}
|
|
1087
|
-
className="w-full text-[11px] font-mono px-2 py-1 border border-[var(--border)] rounded bg-[var(--bg-secondary)]"
|
|
766
|
+
<label className="block mb-3">
|
|
767
|
+
<div className="flex items-baseline gap-1 text-[11px] mb-0.5">
|
|
768
|
+
<span className="font-mono">prompt</span>
|
|
769
|
+
<span className="text-[var(--red)]">*</span>
|
|
770
|
+
</div>
|
|
771
|
+
<textarea
|
|
772
|
+
value={promptText}
|
|
773
|
+
onChange={(e) => onPromptText(e.target.value)}
|
|
774
|
+
rows={8}
|
|
775
|
+
placeholder="e.g. 查一下我今天分到的 bug"
|
|
776
|
+
className="w-full text-[11px] font-mono px-2 py-1.5 border border-[var(--border)] rounded bg-[var(--bg-secondary)] resize-y"
|
|
1088
777
|
/>
|
|
1089
|
-
|
|
778
|
+
<div className="text-[10px] text-[var(--text-secondary)] mt-0.5">
|
|
779
|
+
Sent to the agent verbatim. Keep your own wording — Forge won't rewrite it.
|
|
780
|
+
</div>
|
|
1090
781
|
</label>
|
|
1091
|
-
);
|
|
1092
|
-
}
|
|
1093
782
|
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
{labelEl}
|
|
1100
|
-
<textarea
|
|
1101
|
-
rows={3}
|
|
1102
|
-
value={cur}
|
|
1103
|
-
onChange={(e) => {
|
|
1104
|
-
const raw = e.target.value;
|
|
1105
|
-
if (!raw.trim()) { onChange(undefined); return; }
|
|
1106
|
-
try { onChange(JSON.parse(raw)); }
|
|
1107
|
-
catch { onChange(raw); /* keep raw text; underlying json becomes invalid */ }
|
|
1108
|
-
}}
|
|
1109
|
-
spellCheck={false}
|
|
1110
|
-
placeholder={type === 'array' ? '[ ]' : '{ }'}
|
|
783
|
+
<label className="block mb-3">
|
|
784
|
+
<div className="text-[11px] mb-0.5"><span className="font-mono">agent</span></div>
|
|
785
|
+
<select
|
|
786
|
+
value={agent}
|
|
787
|
+
onChange={(e) => onAgent(e.target.value)}
|
|
1111
788
|
className="w-full text-[11px] font-mono px-2 py-1 border border-[var(--border)] rounded bg-[var(--bg-secondary)]"
|
|
1112
|
-
|
|
1113
|
-
|
|
789
|
+
>
|
|
790
|
+
<option value="">— system default —</option>
|
|
791
|
+
{agents.map((a) => (
|
|
792
|
+
<option key={a.id} value={a.id}>
|
|
793
|
+
{a.label || a.id}{a.backendType ? ` (${a.backendType})` : ''}
|
|
794
|
+
</option>
|
|
795
|
+
))}
|
|
796
|
+
</select>
|
|
1114
797
|
</label>
|
|
1115
|
-
);
|
|
1116
|
-
}
|
|
1117
798
|
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
799
|
+
<div className="block mb-1">
|
|
800
|
+
<div className="text-[11px] mb-1"><span className="font-mono">skills</span></div>
|
|
801
|
+
<div className="flex flex-wrap gap-1.5">
|
|
802
|
+
{featuredAvailable.map((name) => (
|
|
803
|
+
<label
|
|
804
|
+
key={name}
|
|
805
|
+
className={`text-[10px] font-mono px-2 py-1 border rounded cursor-pointer ${
|
|
806
|
+
skills.includes(name)
|
|
807
|
+
? 'border-[var(--accent)] bg-[var(--accent)]/10 text-[var(--accent)]'
|
|
808
|
+
: 'border-[var(--border)] hover:bg-[var(--bg-secondary)]/50'
|
|
809
|
+
}`}
|
|
810
|
+
>
|
|
811
|
+
<input
|
|
812
|
+
type="checkbox"
|
|
813
|
+
checked={skills.includes(name)}
|
|
814
|
+
onChange={() => toggleSkill(name)}
|
|
815
|
+
className="mr-1"
|
|
816
|
+
/>
|
|
817
|
+
{name}
|
|
818
|
+
</label>
|
|
819
|
+
))}
|
|
820
|
+
{extras.map((name) => (
|
|
821
|
+
<span
|
|
822
|
+
key={name}
|
|
823
|
+
className="text-[10px] font-mono px-2 py-1 border border-[var(--accent)] bg-[var(--accent)]/10 text-[var(--accent)] rounded inline-flex items-center gap-1"
|
|
824
|
+
>
|
|
825
|
+
{name}
|
|
826
|
+
<button
|
|
827
|
+
type="button"
|
|
828
|
+
onClick={() => toggleSkill(name)}
|
|
829
|
+
className="hover:opacity-70"
|
|
830
|
+
aria-label="remove"
|
|
831
|
+
>×</button>
|
|
832
|
+
</span>
|
|
833
|
+
))}
|
|
834
|
+
</div>
|
|
835
|
+
<div className="text-[10px] text-[var(--text-secondary)] mt-1">
|
|
836
|
+
<code className="font-mono">ai-orchestration</code> lets the agent discover + call your installed connectors.
|
|
837
|
+
</div>
|
|
838
|
+
</div>
|
|
839
|
+
</div>
|
|
1141
840
|
);
|
|
1142
841
|
}
|
|
1143
842
|
|
|
843
|
+
|
|
1144
844
|
function Step3({
|
|
1145
845
|
name, onName,
|
|
1146
846
|
trigger, onTrigger,
|