@geminilight/mindos 0.6.29 → 0.6.31
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/README_zh.md +10 -4
- package/app/app/api/acp/config/route.ts +82 -0
- package/app/app/api/acp/detect/route.ts +71 -48
- package/app/app/api/acp/install/route.ts +51 -0
- package/app/app/api/acp/session/route.ts +141 -11
- package/app/app/api/ask/route.ts +126 -18
- package/app/app/api/export/route.ts +105 -0
- package/app/app/api/workflows/route.ts +156 -0
- package/app/app/globals.css +2 -2
- package/app/app/page.tsx +7 -2
- package/app/app/trash/page.tsx +7 -0
- package/app/app/view/[...path]/ViewPageClient.tsx +234 -2
- package/app/components/ActivityBar.tsx +12 -4
- package/app/components/AskModal.tsx +4 -1
- package/app/components/ExportModal.tsx +220 -0
- package/app/components/FileTree.tsx +42 -11
- package/app/components/HomeContent.tsx +92 -20
- package/app/components/MarkdownView.tsx +45 -10
- package/app/components/Panel.tsx +1 -0
- package/app/components/RightAskPanel.tsx +5 -1
- package/app/components/Sidebar.tsx +10 -1
- package/app/components/SidebarLayout.tsx +6 -0
- package/app/components/TrashPageClient.tsx +263 -0
- package/app/components/agents/AgentDetailContent.tsx +263 -47
- package/app/components/agents/AgentsContentPage.tsx +11 -0
- package/app/components/agents/AgentsPanelA2aTab.tsx +285 -46
- package/app/components/agents/AgentsPanelSessionsTab.tsx +166 -0
- 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/ask/ToolCallBlock.tsx +102 -18
- package/app/components/changes/ChangesContentPage.tsx +58 -14
- package/app/components/explore/ExploreContent.tsx +4 -7
- package/app/components/explore/UseCaseCard.tsx +18 -1
- package/app/components/explore/use-cases.generated.ts +76 -0
- package/app/components/explore/use-cases.yaml +185 -0
- package/app/components/panels/AgentsPanel.tsx +1 -0
- package/app/components/panels/AgentsPanelHubNav.tsx +9 -2
- package/app/components/panels/DiscoverPanel.tsx +1 -1
- package/app/components/panels/WorkflowsPanel.tsx +206 -0
- package/app/components/renderers/workflow-yaml/StepEditor.tsx +164 -0
- package/app/components/renderers/workflow-yaml/WorkflowEditor.tsx +211 -0
- package/app/components/renderers/workflow-yaml/WorkflowRunner.tsx +269 -0
- package/app/components/renderers/workflow-yaml/WorkflowYamlRenderer.tsx +126 -0
- package/app/components/renderers/workflow-yaml/execution.ts +229 -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 +574 -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/AiTab.tsx +191 -174
- package/app/components/settings/AppearanceTab.tsx +168 -77
- package/app/components/settings/KnowledgeTab.tsx +131 -136
- package/app/components/settings/McpTab.tsx +11 -11
- package/app/components/settings/Primitives.tsx +60 -0
- package/app/components/settings/SettingsContent.tsx +15 -8
- package/app/components/settings/SyncTab.tsx +12 -12
- package/app/components/settings/UninstallTab.tsx +8 -18
- package/app/components/settings/UpdateTab.tsx +82 -82
- package/app/components/settings/types.ts +17 -8
- package/app/hooks/useAcpConfig.ts +96 -0
- package/app/hooks/useAcpDetection.ts +69 -14
- package/app/hooks/useAcpRegistry.ts +46 -11
- package/app/hooks/useAskModal.ts +12 -5
- package/app/hooks/useAskPanel.ts +8 -5
- package/app/hooks/useAskSession.ts +19 -2
- package/app/hooks/useImageUpload.ts +152 -0
- package/app/lib/acp/acp-tools.ts +3 -1
- package/app/lib/acp/agent-descriptors.ts +274 -0
- package/app/lib/acp/bridge.ts +6 -0
- package/app/lib/acp/index.ts +20 -4
- package/app/lib/acp/registry.ts +74 -7
- package/app/lib/acp/session.ts +490 -28
- package/app/lib/acp/subprocess.ts +307 -21
- package/app/lib/acp/types.ts +158 -20
- package/app/lib/actions.ts +57 -3
- package/app/lib/agent/model.ts +18 -3
- package/app/lib/agent/stream-consumer.ts +18 -0
- package/app/lib/agent/to-agent-messages.ts +25 -2
- package/app/lib/agent/tools.ts +56 -9
- package/app/lib/core/export.ts +116 -0
- package/app/lib/core/trash.ts +241 -0
- package/app/lib/fs.ts +47 -0
- package/app/lib/hooks/usePinnedFiles.ts +90 -0
- package/app/lib/i18n/generated/explore-i18n.generated.ts +138 -0
- package/app/lib/i18n/index.ts +3 -0
- package/app/lib/i18n/modules/knowledge.ts +124 -6
- package/app/lib/i18n/modules/navigation.ts +2 -0
- package/app/lib/i18n/modules/onboarding.ts +2 -134
- package/app/lib/i18n/modules/panels.ts +146 -2
- package/app/lib/i18n/modules/settings.ts +12 -0
- 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/types.ts +12 -1
- package/app/next-env.d.ts +1 -1
- package/app/package.json +11 -3
- package/app/scripts/generate-explore.ts +145 -0
- 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/explore/use-cases.ts +0 -58
- package/app/components/renderers/workflow/WorkflowRenderer.tsx +0 -409
- package/app/components/renderers/workflow/manifest.ts +0 -14
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useMemo, useState, useCallback, useRef } from 'react';
|
|
4
|
+
import { Pencil, Play, AlertCircle, Zap } 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
|
+
const [editWorkflow, setEditWorkflow] = useState<WorkflowYaml>(() => {
|
|
19
|
+
if (parsed.workflow) return structuredClone(parsed.workflow);
|
|
20
|
+
return { title: '', description: '', steps: [] };
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const savedContentRef = useRef(content);
|
|
24
|
+
const latestParsed = useMemo(() => parsed.workflow ?? null, [parsed.workflow]);
|
|
25
|
+
|
|
26
|
+
const handleEditorChange = useCallback((wf: WorkflowYaml) => {
|
|
27
|
+
setEditWorkflow(wf);
|
|
28
|
+
setDirty(true);
|
|
29
|
+
}, []);
|
|
30
|
+
|
|
31
|
+
const handleSaved = useCallback(() => setDirty(false), []);
|
|
32
|
+
|
|
33
|
+
const runWorkflow = latestParsed ?? editWorkflow;
|
|
34
|
+
const canRun = runWorkflow.steps.length > 0 && !!runWorkflow.title;
|
|
35
|
+
const hasParseErrors = parsed.errors.length > 0;
|
|
36
|
+
|
|
37
|
+
const runDisabledReason = !runWorkflow.title
|
|
38
|
+
? 'Add a title first'
|
|
39
|
+
: runWorkflow.steps.length === 0
|
|
40
|
+
? 'Add at least one step'
|
|
41
|
+
: dirty ? 'Save changes first' : undefined;
|
|
42
|
+
|
|
43
|
+
const handleModeSwitch = (target: Mode) => {
|
|
44
|
+
if (target === 'run' && !canRun) return;
|
|
45
|
+
setMode(target);
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<div className="max-w-[720px] mx-auto py-8 px-2">
|
|
50
|
+
{/* Hero header — gives the page identity */}
|
|
51
|
+
<div className="mb-8">
|
|
52
|
+
{/* Mode tabs + step count */}
|
|
53
|
+
<div className="flex items-center justify-between mb-4">
|
|
54
|
+
<div className="flex items-center gap-1 p-0.5 rounded-lg bg-muted/50">
|
|
55
|
+
<button
|
|
56
|
+
onClick={() => handleModeSwitch('edit')}
|
|
57
|
+
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-md text-xs font-medium transition-all ${
|
|
58
|
+
mode === 'edit'
|
|
59
|
+
? 'bg-background text-foreground shadow-sm'
|
|
60
|
+
: 'text-muted-foreground hover:text-foreground'
|
|
61
|
+
}`}
|
|
62
|
+
>
|
|
63
|
+
<Pencil size={11} />
|
|
64
|
+
Edit
|
|
65
|
+
</button>
|
|
66
|
+
<button
|
|
67
|
+
onClick={() => handleModeSwitch('run')}
|
|
68
|
+
disabled={!canRun}
|
|
69
|
+
title={runDisabledReason}
|
|
70
|
+
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-md text-xs font-medium transition-all disabled:opacity-30 disabled:cursor-not-allowed ${
|
|
71
|
+
mode === 'run'
|
|
72
|
+
? 'bg-background text-foreground shadow-sm'
|
|
73
|
+
: 'text-muted-foreground hover:text-foreground'
|
|
74
|
+
}`}
|
|
75
|
+
>
|
|
76
|
+
<Play size={11} />
|
|
77
|
+
Run
|
|
78
|
+
</button>
|
|
79
|
+
</div>
|
|
80
|
+
|
|
81
|
+
{editWorkflow.steps.length > 0 && (
|
|
82
|
+
<span className="text-2xs text-muted-foreground/60 font-mono">
|
|
83
|
+
{editWorkflow.steps.length} step{editWorkflow.steps.length !== 1 ? 's' : ''}
|
|
84
|
+
</span>
|
|
85
|
+
)}
|
|
86
|
+
</div>
|
|
87
|
+
|
|
88
|
+
{/* Title */}
|
|
89
|
+
<div className="flex items-start gap-3">
|
|
90
|
+
<div className="mt-1 w-8 h-8 rounded-lg bg-[var(--amber)]/10 flex items-center justify-center shrink-0">
|
|
91
|
+
<Zap size={16} className="text-[var(--amber)]" />
|
|
92
|
+
</div>
|
|
93
|
+
<div className="min-w-0 flex-1">
|
|
94
|
+
<h1 className="text-lg font-semibold text-foreground truncate leading-tight">
|
|
95
|
+
{editWorkflow.title || 'Untitled Flow'}
|
|
96
|
+
{dirty && <span className="text-[var(--amber)] ml-1.5 text-sm" title="Unsaved changes">*</span>}
|
|
97
|
+
</h1>
|
|
98
|
+
{editWorkflow.description && (
|
|
99
|
+
<p className="text-xs text-muted-foreground mt-0.5 line-clamp-1">{editWorkflow.description}</p>
|
|
100
|
+
)}
|
|
101
|
+
</div>
|
|
102
|
+
</div>
|
|
103
|
+
</div>
|
|
104
|
+
|
|
105
|
+
{/* Parse error banner */}
|
|
106
|
+
{hasParseErrors && mode === 'edit' && (
|
|
107
|
+
<div className="mb-5 px-3.5 py-3 rounded-lg bg-[var(--error)]/8 border border-[var(--error)]/20">
|
|
108
|
+
<div className="flex items-center gap-2 mb-1.5">
|
|
109
|
+
<AlertCircle size={13} className="text-[var(--error)] shrink-0" />
|
|
110
|
+
<span className="text-xs font-medium text-[var(--error)]">Parse issues found</span>
|
|
111
|
+
</div>
|
|
112
|
+
<ul className="text-2xs text-[var(--error)]/70 pl-5 list-disc space-y-0.5">
|
|
113
|
+
{parsed.errors.slice(0, 3).map((e, i) => <li key={i}>{e}</li>)}
|
|
114
|
+
</ul>
|
|
115
|
+
</div>
|
|
116
|
+
)}
|
|
117
|
+
|
|
118
|
+
{/* Content */}
|
|
119
|
+
{mode === 'edit' ? (
|
|
120
|
+
<WorkflowEditor workflow={editWorkflow} filePath={filePath} onChange={handleEditorChange} onSaved={handleSaved} />
|
|
121
|
+
) : (
|
|
122
|
+
<WorkflowRunner workflow={runWorkflow} filePath={filePath} />
|
|
123
|
+
)}
|
|
124
|
+
</div>
|
|
125
|
+
);
|
|
126
|
+
}
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
// Workflow step execution logic — fetches skills, resolves agents, 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
|
+
* Handles both legacy `skill` (singular) and new `skills` (array) fields.
|
|
45
|
+
*/
|
|
46
|
+
function collectSkillNames(step: WorkflowStepRuntime, workflow: WorkflowYaml): string[] {
|
|
47
|
+
const names: string[] = [];
|
|
48
|
+
// Step-level skills (new array format takes priority)
|
|
49
|
+
if (step.skills?.length) {
|
|
50
|
+
for (const s of step.skills) {
|
|
51
|
+
if (!names.includes(s)) names.push(s);
|
|
52
|
+
}
|
|
53
|
+
} else if (step.skill) {
|
|
54
|
+
names.push(step.skill);
|
|
55
|
+
}
|
|
56
|
+
// Workflow-level skills
|
|
57
|
+
for (const s of workflow.skills ?? []) {
|
|
58
|
+
if (!names.includes(s)) names.push(s);
|
|
59
|
+
}
|
|
60
|
+
return names;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ─── ACP Agent Resolution ────────────────────────────────────────────────
|
|
64
|
+
|
|
65
|
+
/** Resolve an agent name/id to an ACP agent selection { id, name } */
|
|
66
|
+
async function resolveAcpAgent(
|
|
67
|
+
agentId: string,
|
|
68
|
+
signal: AbortSignal,
|
|
69
|
+
): Promise<{ id: string; name: string } | null> {
|
|
70
|
+
try {
|
|
71
|
+
const res = await fetch(`/api/acp/registry?agent=${encodeURIComponent(agentId)}`, { signal });
|
|
72
|
+
if (!res.ok) return null;
|
|
73
|
+
const data = await res.json();
|
|
74
|
+
if (data.agent?.id) {
|
|
75
|
+
return { id: data.agent.id, name: data.agent.name || agentId };
|
|
76
|
+
}
|
|
77
|
+
return null;
|
|
78
|
+
} catch {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ─── Prompt Construction ──────────────────────────────────────────────────
|
|
84
|
+
|
|
85
|
+
function buildPrompt(
|
|
86
|
+
step: WorkflowStepRuntime,
|
|
87
|
+
workflow: WorkflowYaml,
|
|
88
|
+
skillContents: Map<string, string>,
|
|
89
|
+
): string {
|
|
90
|
+
const allStepsSummary = workflow.steps.map((s, i) => `${i + 1}. ${s.name}`).join('\n');
|
|
91
|
+
|
|
92
|
+
// Determine primary skill name for labeling
|
|
93
|
+
const primarySkill = step.skills?.length ? step.skills[0] : step.skill;
|
|
94
|
+
|
|
95
|
+
// Build skill context block
|
|
96
|
+
let skillBlock = '';
|
|
97
|
+
if (skillContents.size > 0) {
|
|
98
|
+
const sections: string[] = [];
|
|
99
|
+
for (const [name, content] of skillContents) {
|
|
100
|
+
const isPrimary = name === primarySkill;
|
|
101
|
+
sections.push(
|
|
102
|
+
`### ${isPrimary ? '[Primary] ' : ''}Skill: ${name}\n\n${content}`
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
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')}`;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return `You are executing step ${step.index + 1} of a workflow: "${step.name}".
|
|
109
|
+
|
|
110
|
+
Context of the full workflow "${workflow.title}":
|
|
111
|
+
${allStepsSummary}
|
|
112
|
+
|
|
113
|
+
Current step instructions:
|
|
114
|
+
${step.prompt || '(No specific instructions — use common sense.)'}${skillBlock}
|
|
115
|
+
|
|
116
|
+
Execute concisely. Provide:
|
|
117
|
+
1. What you did / what the output is
|
|
118
|
+
2. Any decisions made
|
|
119
|
+
3. What the next step should watch out for
|
|
120
|
+
|
|
121
|
+
Be specific and actionable. Format in Markdown.`;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ─── Streaming Execution ──────────────────────────────────────────────────
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Execute a workflow step with AI:
|
|
128
|
+
* 1. Fetch referenced skill(s) content
|
|
129
|
+
* 2. Resolve ACP agent (if step.agent is set)
|
|
130
|
+
* 3. Build prompt with injected skill context
|
|
131
|
+
* 4. Stream response from /api/ask (routed to ACP agent if resolved)
|
|
132
|
+
*/
|
|
133
|
+
export async function runStepWithAI(
|
|
134
|
+
step: WorkflowStepRuntime,
|
|
135
|
+
workflow: WorkflowYaml,
|
|
136
|
+
filePath: string,
|
|
137
|
+
onChunk: (accumulated: string) => void,
|
|
138
|
+
signal: AbortSignal,
|
|
139
|
+
): Promise<void> {
|
|
140
|
+
// 1. Fetch skill content
|
|
141
|
+
const skillNames = collectSkillNames(step, workflow);
|
|
142
|
+
const skillContents = new Map<string, string>();
|
|
143
|
+
|
|
144
|
+
if (skillNames.length > 0) {
|
|
145
|
+
const results = await Promise.all(
|
|
146
|
+
skillNames.map(async (name) => {
|
|
147
|
+
const content = await fetchSkillContent(name, signal);
|
|
148
|
+
return [name, content] as const;
|
|
149
|
+
})
|
|
150
|
+
);
|
|
151
|
+
for (const [name, content] of results) {
|
|
152
|
+
if (content) skillContents.set(name, content);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// 2. Resolve ACP agent
|
|
157
|
+
let selectedAcpAgent: { id: string; name: string } | null = null;
|
|
158
|
+
if (step.agent) {
|
|
159
|
+
selectedAcpAgent = await resolveAcpAgent(step.agent, signal);
|
|
160
|
+
// If agent name doesn't resolve in ACP registry, skip silently
|
|
161
|
+
// (the step will run with the default MindOS agent)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// 3. Build prompt
|
|
165
|
+
const prompt = buildPrompt(step, workflow, skillContents);
|
|
166
|
+
|
|
167
|
+
// 4. Stream from /api/ask
|
|
168
|
+
const body: Record<string, unknown> = {
|
|
169
|
+
messages: [{ role: 'user', content: prompt }],
|
|
170
|
+
currentFile: filePath,
|
|
171
|
+
};
|
|
172
|
+
if (selectedAcpAgent) {
|
|
173
|
+
body.selectedAcpAgent = selectedAcpAgent;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const res = await fetch('/api/ask', {
|
|
177
|
+
method: 'POST',
|
|
178
|
+
headers: { 'Content-Type': 'application/json' },
|
|
179
|
+
body: JSON.stringify(body),
|
|
180
|
+
signal,
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
if (!res.ok) throw new Error(`Request failed (HTTP ${res.status})`);
|
|
184
|
+
if (!res.body) throw new Error('No response body');
|
|
185
|
+
|
|
186
|
+
const reader = res.body.getReader();
|
|
187
|
+
const decoder = new TextDecoder();
|
|
188
|
+
let acc = '';
|
|
189
|
+
|
|
190
|
+
while (true) {
|
|
191
|
+
const { done, value } = await reader.read();
|
|
192
|
+
if (done) break;
|
|
193
|
+
const raw = decoder.decode(value, { stream: true });
|
|
194
|
+
for (const line of raw.split('\n')) {
|
|
195
|
+
// SSE format: data:{"type":"text_delta","delta":"..."}
|
|
196
|
+
const sseMatch = line.match(/^data:(.+)$/);
|
|
197
|
+
if (sseMatch) {
|
|
198
|
+
try {
|
|
199
|
+
const event = JSON.parse(sseMatch[1]);
|
|
200
|
+
if (event.type === 'text_delta' && typeof event.delta === 'string') {
|
|
201
|
+
acc += event.delta;
|
|
202
|
+
onChunk(acc);
|
|
203
|
+
} else if (event.type === 'thinking_delta' && typeof event.delta === 'string') {
|
|
204
|
+
// Show agent thinking as dimmed text
|
|
205
|
+
acc += event.delta;
|
|
206
|
+
onChunk(acc);
|
|
207
|
+
} else if (event.type === 'error' && event.message) {
|
|
208
|
+
// ACP agent error — throw so WorkflowRunner shows it in step error state
|
|
209
|
+
throw new Error(event.message);
|
|
210
|
+
}
|
|
211
|
+
} catch {
|
|
212
|
+
// Not valid JSON — try legacy Vercel AI SDK format: 0:"..."
|
|
213
|
+
const legacyMatch = line.match(/^0:"((?:[^"\\]|\\.)*)"$/);
|
|
214
|
+
if (legacyMatch) {
|
|
215
|
+
acc += legacyMatch[1].replace(/\\n/g, '\n').replace(/\\"/g, '"').replace(/\\\\/g, '\\');
|
|
216
|
+
onChunk(acc);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
// Legacy Vercel AI SDK format (without SSE prefix)
|
|
222
|
+
const legacyMatch = line.match(/^0:"((?:[^"\\]|\\.)*)"$/);
|
|
223
|
+
if (legacyMatch) {
|
|
224
|
+
acc += legacyMatch[1].replace(/\\n/g, '\n').replace(/\\"/g, '"').replace(/\\\\/g, '\\');
|
|
225
|
+
onChunk(acc);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
@@ -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
|
+
};
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import YAML from 'js-yaml';
|
|
2
|
+
import type { WorkflowYaml, ParseResult, ValidationResult, WorkflowStep } from './types';
|
|
3
|
+
|
|
4
|
+
export function parseWorkflowYaml(content: string): ParseResult {
|
|
5
|
+
try {
|
|
6
|
+
const parsed = YAML.load(content, { schema: YAML.JSON_SCHEMA }) as unknown;
|
|
7
|
+
|
|
8
|
+
if (!parsed) {
|
|
9
|
+
return {
|
|
10
|
+
workflow: null,
|
|
11
|
+
errors: ['File is empty'],
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (typeof parsed !== 'object') {
|
|
16
|
+
return {
|
|
17
|
+
workflow: null,
|
|
18
|
+
errors: ['Content is not valid YAML'],
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const validation = validateWorkflowSchema(parsed);
|
|
23
|
+
if (!validation.valid) {
|
|
24
|
+
return {
|
|
25
|
+
workflow: null,
|
|
26
|
+
errors: validation.errors,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
workflow: parsed as WorkflowYaml,
|
|
32
|
+
errors: [],
|
|
33
|
+
};
|
|
34
|
+
} catch (err) {
|
|
35
|
+
return {
|
|
36
|
+
workflow: null,
|
|
37
|
+
errors: [err instanceof Error ? err.message : String(err)],
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function validateWorkflowSchema(obj: unknown): ValidationResult {
|
|
43
|
+
const errors: string[] = [];
|
|
44
|
+
|
|
45
|
+
if (!obj || typeof obj !== 'object') {
|
|
46
|
+
return { valid: false, errors: ['Must be a valid YAML object'] };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const w = obj as Record<string, any>;
|
|
50
|
+
|
|
51
|
+
// Validate title
|
|
52
|
+
if (!w.title || typeof w.title !== 'string' || !w.title.trim()) {
|
|
53
|
+
errors.push("Missing required field 'title' (string)");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Validate description (optional)
|
|
57
|
+
if (w.description !== undefined && typeof w.description !== 'string') {
|
|
58
|
+
errors.push("'description' must be a string");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Validate skills (optional)
|
|
62
|
+
if (w.skills !== undefined) {
|
|
63
|
+
if (!Array.isArray(w.skills)) {
|
|
64
|
+
errors.push("'skills' must be an array of strings");
|
|
65
|
+
} else if (!w.skills.every((s: any) => typeof s === 'string')) {
|
|
66
|
+
errors.push("'skills' must contain only strings");
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Validate tools (optional)
|
|
71
|
+
if (w.tools !== undefined) {
|
|
72
|
+
if (!Array.isArray(w.tools)) {
|
|
73
|
+
errors.push("'tools' must be an array of strings");
|
|
74
|
+
} else if (!w.tools.every((t: any) => typeof t === 'string')) {
|
|
75
|
+
errors.push("'tools' must contain only strings");
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Validate steps
|
|
80
|
+
if (!w.steps) {
|
|
81
|
+
errors.push("Missing required field 'steps' (array)");
|
|
82
|
+
} else if (!Array.isArray(w.steps)) {
|
|
83
|
+
errors.push("'steps' must be an array");
|
|
84
|
+
} else if (w.steps.length === 0) {
|
|
85
|
+
errors.push("'steps' must have at least 1 step");
|
|
86
|
+
} else {
|
|
87
|
+
const seenIds = new Set<string>();
|
|
88
|
+
w.steps.forEach((step: any, idx: number) => {
|
|
89
|
+
const stepErrors = validateStep(step, idx);
|
|
90
|
+
errors.push(...stepErrors);
|
|
91
|
+
// Check for duplicate IDs
|
|
92
|
+
const id = step?.id;
|
|
93
|
+
if (typeof id === 'string' && id.trim()) {
|
|
94
|
+
if (seenIds.has(id)) {
|
|
95
|
+
errors.push(`steps[${idx}].id: duplicate id '${id}' (each step must have a unique id)`);
|
|
96
|
+
}
|
|
97
|
+
seenIds.add(id);
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
valid: errors.length === 0,
|
|
104
|
+
errors,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function validateStep(step: unknown, index: number): string[] {
|
|
109
|
+
const errors: string[] = [];
|
|
110
|
+
const prefix = `steps[${index}]`;
|
|
111
|
+
|
|
112
|
+
if (!step || typeof step !== 'object') {
|
|
113
|
+
errors.push(`${prefix}: must be a valid object`);
|
|
114
|
+
return errors;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const s = step as Record<string, any>;
|
|
118
|
+
|
|
119
|
+
// Validate id
|
|
120
|
+
if (!s.id || typeof s.id !== 'string' || !s.id.trim()) {
|
|
121
|
+
errors.push(`${prefix}: missing required field 'id' (non-empty string)`);
|
|
122
|
+
} else if (!/^[a-z0-9_-]+$/.test(s.id)) {
|
|
123
|
+
errors.push(`${prefix}.id: '${s.id}' is invalid — use only lowercase letters, numbers, hyphens, underscores (e.g., 'run-tests')`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Validate name
|
|
127
|
+
if (!s.name || typeof s.name !== 'string' || !s.name.trim()) {
|
|
128
|
+
errors.push(`${prefix}: missing required field 'name' (non-empty string)`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Validate description (optional)
|
|
132
|
+
if (s.description !== undefined && typeof s.description !== 'string') {
|
|
133
|
+
errors.push(`${prefix}.description: must be a string`);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Validate agent (optional)
|
|
137
|
+
if (s.agent !== undefined && (typeof s.agent !== 'string' || !s.agent.trim())) {
|
|
138
|
+
errors.push(`${prefix}.agent: must be a non-empty string`);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Validate skill (optional)
|
|
142
|
+
if (s.skill !== undefined && (typeof s.skill !== 'string' || !s.skill.trim())) {
|
|
143
|
+
errors.push(`${prefix}.skill: must be a non-empty string`);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Validate tools (optional)
|
|
147
|
+
if (s.tools !== undefined) {
|
|
148
|
+
if (!Array.isArray(s.tools)) {
|
|
149
|
+
errors.push(`${prefix}.tools: must be an array of strings`);
|
|
150
|
+
} else if (!s.tools.every((t: any) => typeof t === 'string')) {
|
|
151
|
+
errors.push(`${prefix}.tools: must contain only strings`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Validate prompt (required)
|
|
156
|
+
if (!s.prompt || typeof s.prompt !== 'string' || !s.prompt.trim()) {
|
|
157
|
+
errors.push(`${prefix}: missing required field 'prompt' (non-empty string)`);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Validate timeout (optional)
|
|
161
|
+
if (s.timeout !== undefined) {
|
|
162
|
+
if (typeof s.timeout !== 'number' || s.timeout <= 0) {
|
|
163
|
+
errors.push(`${prefix}.timeout: must be a positive number (seconds)`);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return errors;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function getStepDescription(step: WorkflowStep): string {
|
|
171
|
+
return step.description || step.prompt.split('\n')[0].slice(0, 100);
|
|
172
|
+
}
|