@geminilight/mindos 0.6.28 → 0.6.30
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/README.md +10 -4
- package/app/app/api/a2a/agents/route.ts +9 -0
- package/app/app/api/a2a/delegations/route.ts +9 -0
- package/app/app/api/a2a/discover/route.ts +2 -0
- package/app/app/api/a2a/route.ts +6 -6
- package/app/app/api/acp/config/route.ts +82 -0
- package/app/app/api/acp/detect/route.ts +114 -0
- package/app/app/api/acp/install/route.ts +51 -0
- package/app/app/api/acp/registry/route.ts +31 -0
- package/app/app/api/acp/session/route.ts +185 -0
- package/app/app/api/ask/route.ts +116 -13
- package/app/app/api/workflows/route.ts +156 -0
- package/app/app/layout.tsx +2 -0
- package/app/app/page.tsx +7 -2
- package/app/components/ActivityBar.tsx +12 -4
- package/app/components/AskModal.tsx +4 -1
- package/app/components/DirView.tsx +64 -2
- package/app/components/FileTree.tsx +40 -10
- package/app/components/GuideCard.tsx +7 -17
- package/app/components/HomeContent.tsx +1 -0
- package/app/components/MarkdownView.tsx +2 -0
- package/app/components/Panel.tsx +1 -0
- package/app/components/RightAskPanel.tsx +5 -1
- package/app/components/SearchModal.tsx +234 -80
- package/app/components/SidebarLayout.tsx +6 -0
- package/app/components/agents/AgentDetailContent.tsx +266 -52
- package/app/components/agents/AgentsContentPage.tsx +32 -6
- package/app/components/agents/AgentsPanelA2aTab.tsx +684 -0
- package/app/components/agents/AgentsPanelSessionsTab.tsx +166 -0
- package/app/components/agents/SkillDetailPopover.tsx +4 -9
- package/app/components/agents/agents-content-model.ts +2 -2
- package/app/components/ask/AgentSelectorCapsule.tsx +218 -0
- package/app/components/ask/AskContent.tsx +197 -239
- package/app/components/ask/FileChip.tsx +82 -17
- package/app/components/ask/MentionPopover.tsx +21 -3
- package/app/components/ask/MessageList.tsx +30 -9
- package/app/components/ask/SlashCommandPopover.tsx +21 -3
- package/app/components/help/HelpContent.tsx +9 -9
- package/app/components/panels/AgentsPanel.tsx +2 -0
- package/app/components/panels/AgentsPanelAgentDetail.tsx +5 -8
- package/app/components/panels/AgentsPanelHubNav.tsx +16 -2
- package/app/components/panels/EchoPanel.tsx +5 -1
- package/app/components/panels/EchoSidebarStats.tsx +136 -0
- package/app/components/panels/WorkflowsPanel.tsx +206 -0
- package/app/components/renderers/workflow-yaml/StepEditor.tsx +157 -0
- package/app/components/renderers/workflow-yaml/WorkflowEditor.tsx +201 -0
- package/app/components/renderers/workflow-yaml/WorkflowRunner.tsx +226 -0
- package/app/components/renderers/workflow-yaml/WorkflowYamlRenderer.tsx +126 -0
- package/app/components/renderers/workflow-yaml/execution.ts +177 -0
- package/app/components/renderers/workflow-yaml/index.ts +6 -0
- package/app/components/renderers/workflow-yaml/manifest.ts +21 -0
- package/app/components/renderers/workflow-yaml/parser.ts +172 -0
- package/app/components/renderers/workflow-yaml/selectors.tsx +522 -0
- package/app/components/renderers/workflow-yaml/serializer.ts +56 -0
- package/app/components/renderers/workflow-yaml/types.ts +46 -0
- package/app/components/settings/KnowledgeTab.tsx +3 -6
- package/app/components/settings/McpSkillsSection.tsx +4 -5
- package/app/components/settings/McpTab.tsx +6 -8
- package/app/components/setup/StepSecurity.tsx +4 -5
- package/app/components/setup/index.tsx +5 -11
- package/app/components/ui/Toaster.tsx +39 -0
- package/app/hooks/useA2aRegistry.ts +6 -1
- package/app/hooks/useAcpConfig.ts +96 -0
- package/app/hooks/useAcpDetection.ts +120 -0
- package/app/hooks/useAcpRegistry.ts +86 -0
- package/app/hooks/useAskModal.ts +12 -5
- package/app/hooks/useAskPanel.ts +8 -5
- package/app/hooks/useAskSession.ts +19 -2
- package/app/hooks/useDelegationHistory.ts +49 -0
- package/app/hooks/useImageUpload.ts +152 -0
- package/app/lib/a2a/client.ts +49 -5
- package/app/lib/a2a/orchestrator.ts +0 -1
- package/app/lib/a2a/task-handler.ts +4 -4
- package/app/lib/a2a/types.ts +15 -0
- package/app/lib/acp/acp-tools.ts +95 -0
- package/app/lib/acp/agent-descriptors.ts +274 -0
- package/app/lib/acp/bridge.ts +144 -0
- package/app/lib/acp/index.ts +40 -0
- package/app/lib/acp/registry.ts +202 -0
- package/app/lib/acp/session.ts +717 -0
- package/app/lib/acp/subprocess.ts +495 -0
- package/app/lib/acp/types.ts +274 -0
- package/app/lib/agent/model.ts +18 -3
- package/app/lib/agent/to-agent-messages.ts +25 -2
- package/app/lib/agent/tools.ts +2 -1
- package/app/lib/i18n/_core.ts +22 -0
- package/app/lib/i18n/index.ts +35 -0
- package/app/lib/i18n/modules/ai-chat.ts +215 -0
- package/app/lib/i18n/modules/common.ts +71 -0
- package/app/lib/i18n/modules/features.ts +153 -0
- package/app/lib/i18n/modules/knowledge.ts +429 -0
- package/app/lib/i18n/modules/navigation.ts +153 -0
- package/app/lib/i18n/modules/onboarding.ts +523 -0
- package/app/lib/i18n/modules/panels.ts +1196 -0
- package/app/lib/i18n/modules/settings.ts +585 -0
- package/app/lib/i18n-en.ts +2 -1518
- package/app/lib/i18n-zh.ts +2 -1542
- package/app/lib/i18n.ts +3 -6
- package/app/lib/pi-integration/skills.ts +21 -6
- package/app/lib/renderers/index.ts +2 -2
- package/app/lib/settings.ts +10 -0
- package/app/lib/toast.ts +79 -0
- package/app/lib/types.ts +12 -1
- package/app/next-env.d.ts +1 -1
- package/app/package.json +3 -1
- package/bin/cli.js +25 -25
- package/bin/commands/file.js +29 -2
- package/bin/commands/space.js +249 -91
- package/package.json +1 -1
- package/templates/en/.mindos/workflows/Sprint Release.flow.yaml +130 -0
- package/templates/zh/.mindos/workflows//345/221/250/350/277/255/344/273/243/346/243/200/346/237/245.flow.yaml +84 -0
- package/app/components/renderers/workflow/WorkflowRenderer.tsx +0 -409
- package/app/components/renderers/workflow/manifest.ts +0 -14
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useRef, useCallback, useEffect } from 'react';
|
|
4
|
+
import { Play, SkipForward, RotateCcw, CheckCircle2, Circle, Loader2, AlertCircle, ChevronDown, Sparkles, XCircle, Clock } from 'lucide-react';
|
|
5
|
+
import { runStepWithAI, clearSkillCache } from './execution';
|
|
6
|
+
import type { WorkflowYaml, WorkflowStepRuntime, StepStatus } from './types';
|
|
7
|
+
|
|
8
|
+
function initSteps(workflow: WorkflowYaml): WorkflowStepRuntime[] {
|
|
9
|
+
return workflow.steps.map((s, idx) => ({
|
|
10
|
+
...s, index: idx, status: 'pending' as const, output: '', error: undefined,
|
|
11
|
+
}));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function StatusIcon({ status }: { status: StepStatus }) {
|
|
15
|
+
if (status === 'pending') return <Circle size={15} className="text-border" />;
|
|
16
|
+
if (status === 'running') return <Loader2 size={15} className="text-[var(--amber)] animate-spin" />;
|
|
17
|
+
if (status === 'done') return <CheckCircle2 size={15} className="text-[var(--success)]" />;
|
|
18
|
+
if (status === 'skipped') return <SkipForward size={15} className="text-muted-foreground opacity-50" />;
|
|
19
|
+
return <AlertCircle size={15} className="text-[var(--error)]" />;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function Badge({ label, variant }: { label: string; variant?: 'amber' | 'default' }) {
|
|
23
|
+
return (
|
|
24
|
+
<span className={`inline-flex items-center gap-1 text-2xs px-2 py-0.5 rounded whitespace-nowrap ${
|
|
25
|
+
variant === 'amber'
|
|
26
|
+
? 'bg-[var(--amber)]/10 text-[var(--amber)] border border-[var(--amber)]/20'
|
|
27
|
+
: 'bg-muted text-muted-foreground'
|
|
28
|
+
}`}>
|
|
29
|
+
{label}
|
|
30
|
+
</span>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function formatDuration(ms: number): string {
|
|
35
|
+
if (ms < 1000) return `${ms}ms`;
|
|
36
|
+
const secs = Math.round(ms / 1000);
|
|
37
|
+
if (secs < 60) return `${secs}s`;
|
|
38
|
+
return `${Math.floor(secs / 60)}m ${secs % 60}s`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function RunStepCard({ step, canRun, onRun, onSkip, onCancel }: {
|
|
42
|
+
step: WorkflowStepRuntime; canRun: boolean;
|
|
43
|
+
onRun: () => void; onSkip: () => void; onCancel: () => void;
|
|
44
|
+
}) {
|
|
45
|
+
const [expanded, setExpanded] = useState(false);
|
|
46
|
+
const hasContent = !!(step.description || step.output || step.error);
|
|
47
|
+
const borderColor = {
|
|
48
|
+
pending: 'border-border', running: 'border-[var(--amber)]/50',
|
|
49
|
+
done: 'border-[var(--success)]/40', skipped: 'border-border', error: 'border-[var(--error)]/40',
|
|
50
|
+
}[step.status];
|
|
51
|
+
|
|
52
|
+
// Merge skills for display
|
|
53
|
+
const allSkills = step.skills?.length ? step.skills : (step.skill ? [step.skill] : []);
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<div className={`rounded-xl border overflow-hidden bg-card transition-all ${borderColor} ${step.status === 'skipped' ? 'opacity-60' : ''}`}>
|
|
57
|
+
<div className="flex items-center gap-2.5 px-3.5 py-2.5 justify-between flex-wrap">
|
|
58
|
+
<div className="flex items-center gap-2 flex-1 min-w-0">
|
|
59
|
+
<StatusIcon status={step.status} />
|
|
60
|
+
<div className="min-w-0">
|
|
61
|
+
<div className="font-semibold text-sm text-foreground cursor-pointer" onClick={() => hasContent && setExpanded(v => !v)}>
|
|
62
|
+
{step.name}
|
|
63
|
+
</div>
|
|
64
|
+
{(step.agent || allSkills.length > 0 || step.model) && (
|
|
65
|
+
<div className="flex gap-1.5 mt-1 flex-wrap">
|
|
66
|
+
{allSkills.map(s => <Badge key={s} label={`🎓 ${s}`} variant="amber" />)}
|
|
67
|
+
{step.agent && <Badge label={`🤖 ${step.agent}`} />}
|
|
68
|
+
{step.model && <Badge label={`🧠 ${step.model}`} />}
|
|
69
|
+
</div>
|
|
70
|
+
)}
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
<div className="flex items-center gap-1.5 shrink-0">
|
|
74
|
+
{/* Duration display */}
|
|
75
|
+
{step.durationMs != null && (step.status === 'done' || step.status === 'error') && (
|
|
76
|
+
<span className="flex items-center gap-1 text-2xs text-muted-foreground/60">
|
|
77
|
+
<Clock size={10} />
|
|
78
|
+
{formatDuration(step.durationMs)}
|
|
79
|
+
</span>
|
|
80
|
+
)}
|
|
81
|
+
|
|
82
|
+
{step.status === 'pending' && (
|
|
83
|
+
<>
|
|
84
|
+
<button onClick={onRun} disabled={!canRun}
|
|
85
|
+
title={!canRun ? 'Another step is running' : undefined}
|
|
86
|
+
className="flex items-center gap-1 px-2.5 py-1 rounded-md text-2xs font-medium border-none transition-colors disabled:opacity-50 disabled:cursor-not-allowed bg-[var(--amber)] text-[var(--amber-foreground)] disabled:bg-muted disabled:text-muted-foreground"
|
|
87
|
+
><Play size={10} /> Run</button>
|
|
88
|
+
<button onClick={onSkip} className="px-2 py-1 rounded-md text-2xs border border-border bg-transparent text-muted-foreground hover:bg-muted transition-colors">Skip</button>
|
|
89
|
+
</>
|
|
90
|
+
)}
|
|
91
|
+
{step.status === 'running' && (
|
|
92
|
+
<button onClick={onCancel} className="flex items-center gap-1 px-2.5 py-1 rounded-md text-2xs border border-[var(--error)] bg-transparent text-[var(--error)] hover:bg-[var(--error)]/10 transition-colors">
|
|
93
|
+
<XCircle size={10} /> Cancel
|
|
94
|
+
</button>
|
|
95
|
+
)}
|
|
96
|
+
{(step.status === 'done' || step.status === 'error') && (
|
|
97
|
+
<button onClick={() => setExpanded(v => !v)} className="px-2 py-1 rounded-md text-2xs border border-border bg-transparent text-muted-foreground hover:bg-muted transition-colors">
|
|
98
|
+
<ChevronDown size={11} className={`inline transition-transform ${expanded ? 'rotate-180' : ''}`} />
|
|
99
|
+
</button>
|
|
100
|
+
)}
|
|
101
|
+
</div>
|
|
102
|
+
</div>
|
|
103
|
+
{(expanded || step.status === 'running') && hasContent && (
|
|
104
|
+
<div className="border-t border-border">
|
|
105
|
+
{step.description && (
|
|
106
|
+
<div className={`px-3.5 py-2.5 text-xs leading-relaxed text-muted-foreground ${(step.output || step.error) ? 'border-b border-border' : ''}`}>
|
|
107
|
+
{step.description}
|
|
108
|
+
</div>
|
|
109
|
+
)}
|
|
110
|
+
{step.error && (
|
|
111
|
+
<div className="px-3.5 py-2.5 bg-[var(--error)]/10">
|
|
112
|
+
<div className="flex items-center gap-1.5 mb-1.5">
|
|
113
|
+
<AlertCircle size={11} className="text-[var(--error)]" />
|
|
114
|
+
<span className="text-2xs text-[var(--error)] uppercase font-medium">Error</span>
|
|
115
|
+
</div>
|
|
116
|
+
<div className="text-xs text-[var(--error)] whitespace-pre-wrap break-words">{step.error}</div>
|
|
117
|
+
</div>
|
|
118
|
+
)}
|
|
119
|
+
{step.output && (
|
|
120
|
+
<div className="px-3.5 py-2.5 bg-background">
|
|
121
|
+
<div className="flex items-center gap-1.5 mb-1.5">
|
|
122
|
+
<Sparkles size={11} className="text-[var(--amber)]" />
|
|
123
|
+
<span className="text-2xs text-muted-foreground uppercase tracking-wide">AI Output</span>
|
|
124
|
+
{step.status === 'running' && <span className="w-1.5 h-1.5 rounded-full bg-[var(--amber)] animate-pulse ml-1" />}
|
|
125
|
+
</div>
|
|
126
|
+
<div className="text-xs leading-relaxed text-foreground whitespace-pre-wrap break-words">{step.output}</div>
|
|
127
|
+
</div>
|
|
128
|
+
)}
|
|
129
|
+
</div>
|
|
130
|
+
)}
|
|
131
|
+
</div>
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ─── Main Runner ──────────────────────────────────────────────────────────
|
|
136
|
+
|
|
137
|
+
export default function WorkflowRunner({ workflow, filePath }: { workflow: WorkflowYaml; filePath: string }) {
|
|
138
|
+
const [steps, setSteps] = useState<WorkflowStepRuntime[]>(() => initSteps(workflow));
|
|
139
|
+
const [running, setRunning] = useState(false);
|
|
140
|
+
const abortRef = useRef<AbortController | null>(null);
|
|
141
|
+
|
|
142
|
+
useEffect(() => {
|
|
143
|
+
setSteps(initSteps(workflow));
|
|
144
|
+
setRunning(false);
|
|
145
|
+
}, [workflow]);
|
|
146
|
+
|
|
147
|
+
const cancelExecution = useCallback(() => {
|
|
148
|
+
abortRef.current?.abort();
|
|
149
|
+
abortRef.current = null;
|
|
150
|
+
setRunning(false);
|
|
151
|
+
setSteps(prev => prev.map(s => s.status === 'running' ? { ...s, status: 'error', error: 'Cancelled by user' } : s));
|
|
152
|
+
}, []);
|
|
153
|
+
|
|
154
|
+
const runStep = useCallback(async (idx: number) => {
|
|
155
|
+
if (running) return;
|
|
156
|
+
abortRef.current?.abort();
|
|
157
|
+
const ctrl = new AbortController();
|
|
158
|
+
abortRef.current = ctrl;
|
|
159
|
+
setRunning(true);
|
|
160
|
+
|
|
161
|
+
const startTime = Date.now();
|
|
162
|
+
setSteps(prev => prev.map((s, i) => i === idx ? { ...s, status: 'running', output: '', error: undefined, startedAt: new Date() } : s));
|
|
163
|
+
|
|
164
|
+
try {
|
|
165
|
+
const step = workflow.steps[idx];
|
|
166
|
+
const runtimeStep: WorkflowStepRuntime = { ...step, index: idx, status: 'running', output: '' };
|
|
167
|
+
await runStepWithAI(runtimeStep, workflow, filePath,
|
|
168
|
+
(acc) => setSteps(prev => prev.map((s, i) => i === idx ? { ...s, output: acc } : s)),
|
|
169
|
+
ctrl.signal);
|
|
170
|
+
const duration = Date.now() - startTime;
|
|
171
|
+
setSteps(prev => prev.map((s, i) => i === idx ? { ...s, status: 'done', completedAt: new Date(), durationMs: duration } : s));
|
|
172
|
+
} catch (err: unknown) {
|
|
173
|
+
if (err instanceof Error && err.name === 'AbortError') return;
|
|
174
|
+
const duration = Date.now() - startTime;
|
|
175
|
+
setSteps(prev => prev.map((s, i) => i === idx ? { ...s, status: 'error', error: err instanceof Error ? err.message : String(err), durationMs: duration } : s));
|
|
176
|
+
} finally { setRunning(false); }
|
|
177
|
+
}, [running, workflow, filePath]);
|
|
178
|
+
|
|
179
|
+
const skipStep = useCallback((idx: number) => {
|
|
180
|
+
setSteps(prev => prev.map((s, i) => i === idx ? { ...s, status: 'skipped' } : s));
|
|
181
|
+
}, []);
|
|
182
|
+
|
|
183
|
+
const reset = useCallback(() => {
|
|
184
|
+
abortRef.current?.abort();
|
|
185
|
+
abortRef.current = null;
|
|
186
|
+
setRunning(false);
|
|
187
|
+
clearSkillCache();
|
|
188
|
+
setSteps(initSteps(workflow));
|
|
189
|
+
}, [workflow]);
|
|
190
|
+
|
|
191
|
+
const doneCount = steps.filter(s => s.status === 'done').length;
|
|
192
|
+
const progress = steps.length > 0 ? Math.round((doneCount / steps.length) * 100) : 0;
|
|
193
|
+
const allDone = doneCount === steps.length && steps.length > 0;
|
|
194
|
+
const nextPendingIdx = steps.findIndex(s => s.status === 'pending');
|
|
195
|
+
|
|
196
|
+
return (
|
|
197
|
+
<div>
|
|
198
|
+
{workflow.description && (
|
|
199
|
+
<p className="text-xs text-muted-foreground leading-relaxed mb-3">{workflow.description}</p>
|
|
200
|
+
)}
|
|
201
|
+
<div className="flex items-center gap-2.5 flex-wrap mb-4">
|
|
202
|
+
<div className="flex-1 min-w-[120px] h-1 rounded-full bg-border overflow-hidden">
|
|
203
|
+
<div className={`h-full rounded-full transition-all duration-300 ${allDone ? 'bg-[var(--success)]' : 'bg-[var(--amber)]'}`} style={{ width: `${progress}%` }} />
|
|
204
|
+
</div>
|
|
205
|
+
<span className={`text-2xs shrink-0 ${allDone ? 'text-[var(--success)] font-medium' : 'text-muted-foreground'}`}>
|
|
206
|
+
{doneCount}/{steps.length}{allDone ? ' Complete' : ''}
|
|
207
|
+
</span>
|
|
208
|
+
{nextPendingIdx >= 0 && (
|
|
209
|
+
<button onClick={() => runStep(nextPendingIdx)} disabled={running}
|
|
210
|
+
title={running ? 'A step is currently running' : undefined}
|
|
211
|
+
className="flex items-center gap-1.5 px-3 py-1 rounded-lg text-xs font-medium border-none transition-colors disabled:opacity-60 disabled:cursor-not-allowed bg-[var(--amber)] text-[var(--amber-foreground)] disabled:bg-muted disabled:text-muted-foreground"
|
|
212
|
+
>{running ? <Loader2 size={11} className="animate-spin" /> : <Play size={11} />} Run next</button>
|
|
213
|
+
)}
|
|
214
|
+
<button onClick={reset} className="flex items-center gap-1 px-2.5 py-1 rounded-lg text-xs border border-border bg-transparent text-muted-foreground hover:bg-muted transition-colors">
|
|
215
|
+
<RotateCcw size={11} /> Reset
|
|
216
|
+
</button>
|
|
217
|
+
</div>
|
|
218
|
+
<div className="flex flex-col gap-2">
|
|
219
|
+
{steps.map((step, i) => (
|
|
220
|
+
<RunStepCard key={step.id} step={step} canRun={!running}
|
|
221
|
+
onRun={() => runStep(i)} onSkip={() => skipStep(i)} onCancel={cancelExecution} />
|
|
222
|
+
))}
|
|
223
|
+
</div>
|
|
224
|
+
</div>
|
|
225
|
+
);
|
|
226
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useMemo, useState, useCallback, useRef } from 'react';
|
|
4
|
+
import { Pencil, Play, AlertCircle } from 'lucide-react';
|
|
5
|
+
import type { RendererContext } from '@/lib/renderers/registry';
|
|
6
|
+
import { parseWorkflowYaml } from './parser';
|
|
7
|
+
import WorkflowEditor from './WorkflowEditor';
|
|
8
|
+
import WorkflowRunner from './WorkflowRunner';
|
|
9
|
+
import type { WorkflowYaml } from './types';
|
|
10
|
+
|
|
11
|
+
type Mode = 'edit' | 'run';
|
|
12
|
+
|
|
13
|
+
export function WorkflowYamlRenderer({ filePath, content }: RendererContext) {
|
|
14
|
+
const parsed = useMemo(() => parseWorkflowYaml(content), [content]);
|
|
15
|
+
const [mode, setMode] = useState<Mode>('edit');
|
|
16
|
+
const [dirty, setDirty] = useState(false);
|
|
17
|
+
|
|
18
|
+
// Editor state: start from parsed workflow or a blank template
|
|
19
|
+
const [editWorkflow, setEditWorkflow] = useState<WorkflowYaml>(() => {
|
|
20
|
+
if (parsed.workflow) return structuredClone(parsed.workflow);
|
|
21
|
+
return { title: '', description: '', steps: [] };
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
// Track original content for dirty detection
|
|
25
|
+
const savedContentRef = useRef(content);
|
|
26
|
+
|
|
27
|
+
const latestParsed = useMemo(() => parsed.workflow ?? null, [parsed.workflow]);
|
|
28
|
+
|
|
29
|
+
const handleEditorChange = useCallback((wf: WorkflowYaml) => {
|
|
30
|
+
setEditWorkflow(wf);
|
|
31
|
+
setDirty(true);
|
|
32
|
+
}, []);
|
|
33
|
+
|
|
34
|
+
const handleSaved = useCallback(() => {
|
|
35
|
+
setDirty(false);
|
|
36
|
+
}, []);
|
|
37
|
+
|
|
38
|
+
// Runner uses latest parsed workflow (from file), not editor state
|
|
39
|
+
const runWorkflow = latestParsed ?? editWorkflow;
|
|
40
|
+
const canRun = runWorkflow.steps.length > 0 && !!runWorkflow.title;
|
|
41
|
+
const hasParseErrors = parsed.errors.length > 0;
|
|
42
|
+
|
|
43
|
+
// Explain why Run is disabled
|
|
44
|
+
const runDisabledReason = !runWorkflow.title
|
|
45
|
+
? 'Add a title first'
|
|
46
|
+
: runWorkflow.steps.length === 0
|
|
47
|
+
? 'Add at least one step'
|
|
48
|
+
: dirty
|
|
49
|
+
? 'Save changes first'
|
|
50
|
+
: undefined;
|
|
51
|
+
|
|
52
|
+
const handleModeSwitch = (target: Mode) => {
|
|
53
|
+
if (target === 'run' && dirty) {
|
|
54
|
+
// Don't block, but show the tooltip
|
|
55
|
+
}
|
|
56
|
+
if (target === 'run' && !canRun) return;
|
|
57
|
+
setMode(target);
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<div className="max-w-[720px] mx-auto py-6 px-1">
|
|
62
|
+
{/* Mode switcher + title */}
|
|
63
|
+
<div className="flex items-center gap-3 mb-5">
|
|
64
|
+
<div className="flex rounded-lg border border-border overflow-hidden text-xs">
|
|
65
|
+
<button
|
|
66
|
+
onClick={() => handleModeSwitch('edit')}
|
|
67
|
+
className={`flex items-center gap-1.5 px-3.5 py-1.5 transition-colors ${
|
|
68
|
+
mode === 'edit'
|
|
69
|
+
? 'bg-[var(--amber)] text-[var(--amber-foreground)] font-medium'
|
|
70
|
+
: 'bg-card text-muted-foreground hover:bg-muted'
|
|
71
|
+
}`}
|
|
72
|
+
>
|
|
73
|
+
<Pencil size={12} />
|
|
74
|
+
Edit
|
|
75
|
+
</button>
|
|
76
|
+
<button
|
|
77
|
+
onClick={() => handleModeSwitch('run')}
|
|
78
|
+
disabled={!canRun}
|
|
79
|
+
title={runDisabledReason}
|
|
80
|
+
className={`flex items-center gap-1.5 px-3.5 py-1.5 transition-colors disabled:opacity-40 disabled:cursor-not-allowed ${
|
|
81
|
+
mode === 'run'
|
|
82
|
+
? 'bg-[var(--amber)] text-[var(--amber-foreground)] font-medium'
|
|
83
|
+
: 'bg-card text-muted-foreground hover:bg-muted'
|
|
84
|
+
}`}
|
|
85
|
+
>
|
|
86
|
+
<Play size={12} />
|
|
87
|
+
Run
|
|
88
|
+
</button>
|
|
89
|
+
</div>
|
|
90
|
+
|
|
91
|
+
<h1 className="text-base font-semibold text-foreground truncate flex-1">
|
|
92
|
+
{editWorkflow.title || 'New Workflow'}
|
|
93
|
+
{dirty && <span className="text-[var(--amber)] ml-1" title="Unsaved changes">*</span>}
|
|
94
|
+
</h1>
|
|
95
|
+
</div>
|
|
96
|
+
|
|
97
|
+
{/* Parse error banner */}
|
|
98
|
+
{hasParseErrors && mode === 'edit' && (
|
|
99
|
+
<div className="mb-4 px-3 py-2.5 rounded-lg bg-[var(--error)]/10 border border-[var(--error)]/30">
|
|
100
|
+
<div className="flex items-center gap-2 mb-1">
|
|
101
|
+
<AlertCircle size={13} className="text-[var(--error)] shrink-0" />
|
|
102
|
+
<span className="text-xs font-medium text-[var(--error)]">File has parse issues</span>
|
|
103
|
+
</div>
|
|
104
|
+
<ul className="text-2xs text-[var(--error)]/80 pl-5 list-disc">
|
|
105
|
+
{parsed.errors.slice(0, 3).map((e, i) => <li key={i}>{e}</li>)}
|
|
106
|
+
</ul>
|
|
107
|
+
</div>
|
|
108
|
+
)}
|
|
109
|
+
|
|
110
|
+
{/* Content */}
|
|
111
|
+
{mode === 'edit' ? (
|
|
112
|
+
<WorkflowEditor
|
|
113
|
+
workflow={editWorkflow}
|
|
114
|
+
filePath={filePath}
|
|
115
|
+
onChange={handleEditorChange}
|
|
116
|
+
onSaved={handleSaved}
|
|
117
|
+
/>
|
|
118
|
+
) : (
|
|
119
|
+
<WorkflowRunner
|
|
120
|
+
workflow={runWorkflow}
|
|
121
|
+
filePath={filePath}
|
|
122
|
+
/>
|
|
123
|
+
)}
|
|
124
|
+
</div>
|
|
125
|
+
);
|
|
126
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
// Workflow step execution logic — fetches skills, constructs prompts, streams AI responses
|
|
2
|
+
|
|
3
|
+
import type { WorkflowYaml, WorkflowStepRuntime } from './types';
|
|
4
|
+
|
|
5
|
+
// ─── Skill Fetching ───────────────────────────────────────────────────────
|
|
6
|
+
|
|
7
|
+
/** Cache skill content for the duration of a workflow run to avoid redundant fetches */
|
|
8
|
+
const skillCache = new Map<string, string | null>();
|
|
9
|
+
|
|
10
|
+
export function clearSkillCache() {
|
|
11
|
+
skillCache.clear();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Fetch skill content from /api/skills. Returns the full SKILL.md content,
|
|
16
|
+
* or null if the skill doesn't exist. Results are cached per skill name.
|
|
17
|
+
*/
|
|
18
|
+
async function fetchSkillContent(name: string, signal: AbortSignal): Promise<string | null> {
|
|
19
|
+
if (skillCache.has(name)) return skillCache.get(name)!;
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
const res = await fetch('/api/skills', {
|
|
23
|
+
method: 'POST',
|
|
24
|
+
headers: { 'Content-Type': 'application/json' },
|
|
25
|
+
body: JSON.stringify({ action: 'read', name }),
|
|
26
|
+
signal,
|
|
27
|
+
});
|
|
28
|
+
if (!res.ok) {
|
|
29
|
+
skillCache.set(name, null);
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
const data = await res.json();
|
|
33
|
+
const content = data.content as string | undefined;
|
|
34
|
+
skillCache.set(name, content ?? null);
|
|
35
|
+
return content ?? null;
|
|
36
|
+
} catch {
|
|
37
|
+
// Network error or abort — don't cache failures
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Collect all unique skill names referenced by a step (step-level + workflow-level).
|
|
44
|
+
* Step-level skill takes priority; workflow-level skills provide additional context.
|
|
45
|
+
*/
|
|
46
|
+
function collectSkillNames(step: WorkflowStepRuntime, workflow: WorkflowYaml): string[] {
|
|
47
|
+
const names: string[] = [];
|
|
48
|
+
if (step.skill) names.push(step.skill);
|
|
49
|
+
for (const s of workflow.skills ?? []) {
|
|
50
|
+
if (!names.includes(s)) names.push(s);
|
|
51
|
+
}
|
|
52
|
+
return names;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ─── Prompt Construction ──────────────────────────────────────────────────
|
|
56
|
+
|
|
57
|
+
function buildPrompt(
|
|
58
|
+
step: WorkflowStepRuntime,
|
|
59
|
+
workflow: WorkflowYaml,
|
|
60
|
+
skillContents: Map<string, string>,
|
|
61
|
+
): string {
|
|
62
|
+
const allStepsSummary = workflow.steps.map((s, i) => `${i + 1}. ${s.name}`).join('\n');
|
|
63
|
+
|
|
64
|
+
// Build skill context block
|
|
65
|
+
let skillBlock = '';
|
|
66
|
+
if (skillContents.size > 0) {
|
|
67
|
+
const sections: string[] = [];
|
|
68
|
+
for (const [name, content] of skillContents) {
|
|
69
|
+
const isPrimary = name === step.skill;
|
|
70
|
+
sections.push(
|
|
71
|
+
`### ${isPrimary ? '[Primary] ' : ''}Skill: ${name}\n\n${content}`
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
skillBlock = `\n\n---\n## Skill Reference\n\nFollow the guidelines from these skills when executing this step:\n\n${sections.join('\n\n---\n\n')}`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return `You are executing step ${step.index + 1} of a workflow: "${step.name}".
|
|
78
|
+
|
|
79
|
+
Context of the full workflow "${workflow.title}":
|
|
80
|
+
${allStepsSummary}
|
|
81
|
+
|
|
82
|
+
Current step instructions:
|
|
83
|
+
${step.prompt || '(No specific instructions — use common sense.)'}${skillBlock}
|
|
84
|
+
|
|
85
|
+
Execute concisely. Provide:
|
|
86
|
+
1. What you did / what the output is
|
|
87
|
+
2. Any decisions made
|
|
88
|
+
3. What the next step should watch out for
|
|
89
|
+
|
|
90
|
+
Be specific and actionable. Format in Markdown.`;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ─── Streaming Execution ──────────────────────────────────────────────────
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Execute a workflow step with AI:
|
|
97
|
+
* 1. Fetch referenced skill(s) content
|
|
98
|
+
* 2. Build prompt with injected skill context
|
|
99
|
+
* 3. Stream response from /api/ask
|
|
100
|
+
*/
|
|
101
|
+
export async function runStepWithAI(
|
|
102
|
+
step: WorkflowStepRuntime,
|
|
103
|
+
workflow: WorkflowYaml,
|
|
104
|
+
filePath: string,
|
|
105
|
+
onChunk: (accumulated: string) => void,
|
|
106
|
+
signal: AbortSignal,
|
|
107
|
+
): Promise<void> {
|
|
108
|
+
// 1. Fetch skill content
|
|
109
|
+
const skillNames = collectSkillNames(step, workflow);
|
|
110
|
+
const skillContents = new Map<string, string>();
|
|
111
|
+
|
|
112
|
+
if (skillNames.length > 0) {
|
|
113
|
+
const results = await Promise.all(
|
|
114
|
+
skillNames.map(async (name) => {
|
|
115
|
+
const content = await fetchSkillContent(name, signal);
|
|
116
|
+
return [name, content] as const;
|
|
117
|
+
})
|
|
118
|
+
);
|
|
119
|
+
for (const [name, content] of results) {
|
|
120
|
+
if (content) skillContents.set(name, content);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// 2. Build prompt
|
|
125
|
+
const prompt = buildPrompt(step, workflow, skillContents);
|
|
126
|
+
|
|
127
|
+
// 3. Stream from /api/ask
|
|
128
|
+
const res = await fetch('/api/ask', {
|
|
129
|
+
method: 'POST',
|
|
130
|
+
headers: { 'Content-Type': 'application/json' },
|
|
131
|
+
body: JSON.stringify({
|
|
132
|
+
messages: [{ role: 'user', content: prompt }],
|
|
133
|
+
currentFile: filePath,
|
|
134
|
+
}),
|
|
135
|
+
signal,
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
if (!res.ok) throw new Error(`Request failed (HTTP ${res.status})`);
|
|
139
|
+
if (!res.body) throw new Error('No response body');
|
|
140
|
+
|
|
141
|
+
const reader = res.body.getReader();
|
|
142
|
+
const decoder = new TextDecoder();
|
|
143
|
+
let acc = '';
|
|
144
|
+
|
|
145
|
+
while (true) {
|
|
146
|
+
const { done, value } = await reader.read();
|
|
147
|
+
if (done) break;
|
|
148
|
+
const raw = decoder.decode(value, { stream: true });
|
|
149
|
+
for (const line of raw.split('\n')) {
|
|
150
|
+
// SSE format: data:{"type":"text_delta","delta":"..."}
|
|
151
|
+
const sseMatch = line.match(/^data:(.+)$/);
|
|
152
|
+
if (sseMatch) {
|
|
153
|
+
try {
|
|
154
|
+
const event = JSON.parse(sseMatch[1]);
|
|
155
|
+
if (event.type === 'text_delta' && typeof event.delta === 'string') {
|
|
156
|
+
acc += event.delta;
|
|
157
|
+
onChunk(acc);
|
|
158
|
+
}
|
|
159
|
+
} catch {
|
|
160
|
+
// Not valid JSON — try legacy Vercel AI SDK format: 0:"..."
|
|
161
|
+
const legacyMatch = line.match(/^0:"((?:[^"\\]|\\.)*)"$/);
|
|
162
|
+
if (legacyMatch) {
|
|
163
|
+
acc += legacyMatch[1].replace(/\\n/g, '\n').replace(/\\"/g, '"').replace(/\\\\/g, '\\');
|
|
164
|
+
onChunk(acc);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
// Legacy Vercel AI SDK format (without SSE prefix)
|
|
170
|
+
const legacyMatch = line.match(/^0:"((?:[^"\\]|\\.)*)"$/);
|
|
171
|
+
if (legacyMatch) {
|
|
172
|
+
acc += legacyMatch[1].replace(/\\n/g, '\n').replace(/\\"/g, '"').replace(/\\\\/g, '\\');
|
|
173
|
+
onChunk(acc);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { manifest } from './manifest';
|
|
2
|
+
export { WorkflowYamlRenderer } from './WorkflowYamlRenderer';
|
|
3
|
+
export { parseWorkflowYaml, validateWorkflowSchema, getStepDescription } from './parser';
|
|
4
|
+
export { runStepWithAI, clearSkillCache } from './execution';
|
|
5
|
+
export { serializeWorkflowYaml, generateStepId } from './serializer';
|
|
6
|
+
export type { WorkflowStep, WorkflowYaml, WorkflowStepRuntime, StepStatus, ParseResult, ValidationResult } from './types';
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { RendererDefinition } from '@/lib/renderers/registry';
|
|
2
|
+
|
|
3
|
+
export const manifest: RendererDefinition = {
|
|
4
|
+
id: 'workflow-yaml',
|
|
5
|
+
name: 'Workflow Runner',
|
|
6
|
+
description: 'Execute step-by-step YAML workflows with Skills & Agent support.',
|
|
7
|
+
author: 'MindOS',
|
|
8
|
+
icon: '⚡',
|
|
9
|
+
tags: ['workflow', 'automation', 'steps', 'ai', 'yaml'],
|
|
10
|
+
builtin: true,
|
|
11
|
+
core: true,
|
|
12
|
+
appBuiltinFeature: true,
|
|
13
|
+
entryPath: '.mindos/workflows/',
|
|
14
|
+
match: ({ extension, filePath }) => {
|
|
15
|
+
if (extension !== 'yaml' && extension !== 'yml') return false;
|
|
16
|
+
return /\.flow\.(yaml|yml)$/i.test(filePath);
|
|
17
|
+
},
|
|
18
|
+
load: () => import('./WorkflowYamlRenderer').then(m => ({
|
|
19
|
+
default: m.WorkflowYamlRenderer
|
|
20
|
+
})),
|
|
21
|
+
};
|