@geminilight/mindos 0.6.29 → 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/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 +116 -13
- package/app/app/api/workflows/route.ts +156 -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/FileTree.tsx +21 -10
- package/app/components/HomeContent.tsx +1 -0
- package/app/components/Panel.tsx +1 -0
- package/app/components/RightAskPanel.tsx +5 -1
- package/app/components/SidebarLayout.tsx +6 -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/panels/AgentsPanel.tsx +1 -0
- package/app/components/panels/AgentsPanelHubNav.tsx +9 -2
- 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/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 +481 -28
- package/app/lib/acp/subprocess.ts +307 -21
- package/app/lib/acp/types.ts +158 -20
- package/app/lib/agent/model.ts +18 -3
- package/app/lib/agent/to-agent-messages.ts +25 -2
- package/app/lib/i18n/modules/knowledge.ts +4 -0
- package/app/lib/i18n/modules/navigation.ts +2 -0
- package/app/lib/i18n/modules/panels.ts +146 -2
- 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 +3 -1
- 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,84 @@
|
|
|
1
|
+
title: 周迭代检查工作流
|
|
2
|
+
description: 每周进行代码审查、测试运行和文档更新的标准工作流
|
|
3
|
+
|
|
4
|
+
# 全局配置 - 此工作流可用的技能
|
|
5
|
+
skills:
|
|
6
|
+
- software-architecture
|
|
7
|
+
- code-review-quality
|
|
8
|
+
|
|
9
|
+
# 工作流步骤 - 按顺序执行
|
|
10
|
+
steps:
|
|
11
|
+
# 第 1 步:运行测试套件
|
|
12
|
+
- id: run_tests
|
|
13
|
+
name: 运行测试
|
|
14
|
+
description: 执行完整测试套件并报告覆盖率
|
|
15
|
+
agent: cursor
|
|
16
|
+
prompt: |
|
|
17
|
+
执行此项目的完整测试套件。
|
|
18
|
+
|
|
19
|
+
提供以下信息:
|
|
20
|
+
1. 运行的总测试数、通过数、失败数
|
|
21
|
+
2. 覆盖率报告(行/分支/函数)
|
|
22
|
+
3. 任何严重失败及修复建议
|
|
23
|
+
4. 耗时
|
|
24
|
+
|
|
25
|
+
如果有失败,列出前 3 个最严重的失败及建议的修复方案。
|
|
26
|
+
timeout: 120
|
|
27
|
+
|
|
28
|
+
# 第 2 步:代码审查
|
|
29
|
+
- id: code_review
|
|
30
|
+
name: 代码审查
|
|
31
|
+
description: 使用标准进行代码审查
|
|
32
|
+
agent: claude-code
|
|
33
|
+
skill: code-review-quality
|
|
34
|
+
prompt: |
|
|
35
|
+
使用代码审查标准对最近的代码变更进行审查。
|
|
36
|
+
|
|
37
|
+
评估以下方面:
|
|
38
|
+
1. 正确性 - 错误处理、边界情况
|
|
39
|
+
2. 安全性 - 认证、输入验证、密钥管理
|
|
40
|
+
3. 性能 - 查询优化、缓存策略、复杂度
|
|
41
|
+
4. 可维护性 - 命名规范、测试覆盖、文档完整性
|
|
42
|
+
5. 架构 - 是否遵循项目模式,有无反模式
|
|
43
|
+
|
|
44
|
+
给出评分(如 8.5/10)和主要建议。
|
|
45
|
+
timeout: 120
|
|
46
|
+
|
|
47
|
+
# 第 3 步:更新文档
|
|
48
|
+
- id: update_docs
|
|
49
|
+
name: 更新文档
|
|
50
|
+
description: 同步 CHANGELOG 和 README
|
|
51
|
+
skill: document-release
|
|
52
|
+
prompt: |
|
|
53
|
+
根据本次发布的变更更新项目文档:
|
|
54
|
+
|
|
55
|
+
1. 更新 CHANGELOG.md,遵循语义化版本规范
|
|
56
|
+
2. 如果功能或 API 变更,更新 README.md
|
|
57
|
+
3. 如果端点变更,更新 API 文档
|
|
58
|
+
4. 验证所有文档链接有效
|
|
59
|
+
5. 检查代码示例是否匹配当前实现
|
|
60
|
+
|
|
61
|
+
提供所有文档变更的摘要。
|
|
62
|
+
timeout: 60
|
|
63
|
+
|
|
64
|
+
# 第 4 步:发布前最终检查
|
|
65
|
+
- id: final_check
|
|
66
|
+
name: 发布前验证
|
|
67
|
+
description: 最终确认一切就绪
|
|
68
|
+
agent: mindos
|
|
69
|
+
prompt: |
|
|
70
|
+
进行最终的发布前验证检查:
|
|
71
|
+
|
|
72
|
+
清单:
|
|
73
|
+
☐ 所有测试通过
|
|
74
|
+
☐ 代码审查完成并问题已解决
|
|
75
|
+
☐ 文档已更新并链接有效
|
|
76
|
+
☐ 版本号正确升级(语义化版本)
|
|
77
|
+
☐ Git 标签和提交干净
|
|
78
|
+
☐ 无未提交的更改
|
|
79
|
+
☐ CHANGELOG 条目存在
|
|
80
|
+
☐ 依赖项已更新并锁定
|
|
81
|
+
☐ 性能基准可接受
|
|
82
|
+
|
|
83
|
+
确认发布就绪或列出任何阻碍因素。
|
|
84
|
+
timeout: 60
|
|
@@ -1,409 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import { useMemo, useState, useRef, useCallback } from 'react';
|
|
4
|
-
import { Play, SkipForward, RotateCcw, CheckCircle2, Circle, Loader2, AlertCircle, ChevronDown, Sparkles } from 'lucide-react';
|
|
5
|
-
import type { RendererContext } from '@/lib/renderers/registry';
|
|
6
|
-
import { useLocale } from '@/lib/LocaleContext';
|
|
7
|
-
|
|
8
|
-
// ─── Types ────────────────────────────────────────────────────────────────────
|
|
9
|
-
|
|
10
|
-
type StepStatus = 'pending' | 'running' | 'done' | 'skipped' | 'error';
|
|
11
|
-
|
|
12
|
-
interface WorkflowStep {
|
|
13
|
-
index: number;
|
|
14
|
-
heading: string; // full heading text e.g. "Step 1: Gather requirements"
|
|
15
|
-
body: string; // body text below heading
|
|
16
|
-
status: StepStatus;
|
|
17
|
-
output: string; // AI output for this step
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
interface WorkflowMeta {
|
|
21
|
-
title: string;
|
|
22
|
-
description: string;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
// ─── Parser ───────────────────────────────────────────────────────────────────
|
|
26
|
-
|
|
27
|
-
function parseWorkflow(content: string): { meta: WorkflowMeta; steps: WorkflowStep[] } {
|
|
28
|
-
const lines = content.split('\n');
|
|
29
|
-
let title = '';
|
|
30
|
-
let description = '';
|
|
31
|
-
const steps: WorkflowStep[] = [];
|
|
32
|
-
let currentStep: { heading: string; bodyLines: string[] } | null = null;
|
|
33
|
-
let inMeta = true;
|
|
34
|
-
const metaLines: string[] = [];
|
|
35
|
-
|
|
36
|
-
const flushStep = () => {
|
|
37
|
-
if (!currentStep) return;
|
|
38
|
-
steps.push({
|
|
39
|
-
index: steps.length,
|
|
40
|
-
heading: currentStep.heading,
|
|
41
|
-
body: currentStep.bodyLines.join('\n').trim(),
|
|
42
|
-
status: 'pending',
|
|
43
|
-
output: '',
|
|
44
|
-
});
|
|
45
|
-
currentStep = null;
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
for (const line of lines) {
|
|
49
|
-
if (/^# /.test(line)) {
|
|
50
|
-
title = line.slice(2).trim();
|
|
51
|
-
inMeta = true;
|
|
52
|
-
continue;
|
|
53
|
-
}
|
|
54
|
-
// H2 = step
|
|
55
|
-
if (/^## /.test(line)) {
|
|
56
|
-
flushStep();
|
|
57
|
-
inMeta = false;
|
|
58
|
-
currentStep = { heading: line.slice(3).trim(), bodyLines: [] };
|
|
59
|
-
continue;
|
|
60
|
-
}
|
|
61
|
-
if (currentStep) {
|
|
62
|
-
currentStep.bodyLines.push(line);
|
|
63
|
-
} else if (inMeta) {
|
|
64
|
-
metaLines.push(line);
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
flushStep();
|
|
68
|
-
|
|
69
|
-
description = metaLines.filter(l => l.trim() && !/^#/.test(l)).join(' ').trim().slice(0, 200);
|
|
70
|
-
|
|
71
|
-
return { meta: { title, description }, steps };
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// ─── Inline markdown renderer ─────────────────────────────────────────────────
|
|
75
|
-
|
|
76
|
-
function renderInline(text: string): string {
|
|
77
|
-
return text
|
|
78
|
-
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
|
|
79
|
-
.replace(/`(.+?)`/g, `<code class="font-display" style="font-size:.82em;padding:1px 5px;border-radius:4px;background:var(--muted)">$1</code>`)
|
|
80
|
-
.replace(/\*(.+?)\*/g, '<em>$1</em>');
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function renderBody(body: string): string {
|
|
84
|
-
return body.split('\n').map(line => {
|
|
85
|
-
if (!line.trim()) return '';
|
|
86
|
-
if (/^- /.test(line)) return `<li style="margin:.2em 0;font-size:.82rem;color:var(--muted-foreground)">${renderInline(line.slice(2))}</li>`;
|
|
87
|
-
return `<p style="margin:.3em 0;font-size:.82rem;line-height:1.6;color:var(--muted-foreground)">${renderInline(line)}</p>`;
|
|
88
|
-
}).join('');
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// ─── Status icon ─────────────────────────────────────────────────────────────
|
|
92
|
-
|
|
93
|
-
function StatusIcon({ status }: { status: StepStatus }) {
|
|
94
|
-
if (status === 'pending') return <Circle size={15} style={{ color: 'var(--border)' }} />;
|
|
95
|
-
if (status === 'running') return <Loader2 size={15} style={{ color: 'var(--amber)', animation: 'spin 1s linear infinite' }} />;
|
|
96
|
-
if (status === 'done') return <CheckCircle2 size={15} style={{ color: 'var(--success)' }} />;
|
|
97
|
-
if (status === 'skipped') return <SkipForward size={15} style={{ color: 'var(--muted-foreground)', opacity: .5 }} />;
|
|
98
|
-
return <AlertCircle size={15} style={{ color: 'var(--error)' }} />;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
const STATUS_BORDER: Record<StepStatus, string> = {
|
|
102
|
-
pending: 'var(--border)',
|
|
103
|
-
running: 'rgba(200,135,58,0.5)',
|
|
104
|
-
done: 'rgba(122,173,128,0.4)',
|
|
105
|
-
skipped: 'var(--border)',
|
|
106
|
-
error: 'rgba(200,80,80,0.4)',
|
|
107
|
-
};
|
|
108
|
-
|
|
109
|
-
// ─── AI execution ─────────────────────────────────────────────────────────────
|
|
110
|
-
|
|
111
|
-
async function runStepWithAI(
|
|
112
|
-
step: WorkflowStep,
|
|
113
|
-
filePath: string,
|
|
114
|
-
allStepsSummary: string,
|
|
115
|
-
onChunk: (chunk: string) => void,
|
|
116
|
-
signal: AbortSignal,
|
|
117
|
-
): Promise<void> {
|
|
118
|
-
const prompt = `You are executing step ${step.index + 1} of a SOP/Workflow: "${step.heading}".
|
|
119
|
-
|
|
120
|
-
Context of the full workflow:
|
|
121
|
-
${allStepsSummary}
|
|
122
|
-
|
|
123
|
-
Current step instructions:
|
|
124
|
-
${step.body || '(No specific instructions — use common sense for this step.)'}
|
|
125
|
-
|
|
126
|
-
Execute this step concisely. Provide:
|
|
127
|
-
1. What you did / what the output is
|
|
128
|
-
2. Any decisions made
|
|
129
|
-
3. What the next step should watch out for
|
|
130
|
-
|
|
131
|
-
Be specific and actionable. Format in Markdown.`;
|
|
132
|
-
|
|
133
|
-
const res = await fetch('/api/ask', {
|
|
134
|
-
method: 'POST',
|
|
135
|
-
headers: { 'Content-Type': 'application/json' },
|
|
136
|
-
body: JSON.stringify({
|
|
137
|
-
messages: [{ role: 'user', content: prompt }],
|
|
138
|
-
currentFile: filePath,
|
|
139
|
-
}),
|
|
140
|
-
signal,
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
144
|
-
if (!res.body) throw new Error('No response body');
|
|
145
|
-
|
|
146
|
-
const reader = res.body.getReader();
|
|
147
|
-
const decoder = new TextDecoder();
|
|
148
|
-
let acc = '';
|
|
149
|
-
|
|
150
|
-
while (true) {
|
|
151
|
-
const { done, value } = await reader.read();
|
|
152
|
-
if (done) break;
|
|
153
|
-
const raw = decoder.decode(value, { stream: true });
|
|
154
|
-
for (const line of raw.split('\n')) {
|
|
155
|
-
const m = line.match(/^0:"((?:[^"\\]|\\.)*)"$/);
|
|
156
|
-
if (m) {
|
|
157
|
-
acc += m[1].replace(/\\n/g, '\n').replace(/\\"/g, '"').replace(/\\\\/g, '\\');
|
|
158
|
-
onChunk(acc);
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
// ─── Step card ────────────────────────────────────────────────────────────────
|
|
165
|
-
|
|
166
|
-
function StepCard({
|
|
167
|
-
step, isActive, onRun, onSkip, canRun,
|
|
168
|
-
}: {
|
|
169
|
-
step: WorkflowStep;
|
|
170
|
-
isActive: boolean;
|
|
171
|
-
onRun: () => void;
|
|
172
|
-
onSkip: () => void;
|
|
173
|
-
canRun: boolean;
|
|
174
|
-
}) {
|
|
175
|
-
const { t } = useLocale();
|
|
176
|
-
const [expanded, setExpanded] = useState(false);
|
|
177
|
-
const hasBody = step.body.trim().length > 0;
|
|
178
|
-
const hasOutput = step.output.length > 0;
|
|
179
|
-
|
|
180
|
-
return (
|
|
181
|
-
<div style={{
|
|
182
|
-
border: `1px solid ${STATUS_BORDER[step.status]}`,
|
|
183
|
-
borderRadius: 10,
|
|
184
|
-
overflow: 'hidden',
|
|
185
|
-
background: 'var(--card)',
|
|
186
|
-
opacity: step.status === 'skipped' ? 0.6 : 1,
|
|
187
|
-
transition: 'border-color .2s, opacity .2s',
|
|
188
|
-
}}>
|
|
189
|
-
{/* header */}
|
|
190
|
-
<div style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '11px 14px' }}>
|
|
191
|
-
<StatusIcon status={step.status} />
|
|
192
|
-
<span
|
|
193
|
-
style={{ flex: 1, fontWeight: 600, fontSize: '.88rem', color: 'var(--foreground)', cursor: hasBody || hasOutput ? 'pointer' : 'default' }}
|
|
194
|
-
onClick={() => (hasBody || hasOutput) && setExpanded(v => !v)}
|
|
195
|
-
>
|
|
196
|
-
{step.heading}
|
|
197
|
-
</span>
|
|
198
|
-
|
|
199
|
-
{/* action buttons */}
|
|
200
|
-
<div style={{ display: 'flex', gap: 5, flexShrink: 0 }}>
|
|
201
|
-
{step.status === 'pending' && (
|
|
202
|
-
<>
|
|
203
|
-
<button
|
|
204
|
-
onClick={onRun}
|
|
205
|
-
disabled={!canRun}
|
|
206
|
-
title={!canRun ? t.hints.workflowStepRunning : undefined}
|
|
207
|
-
style={{
|
|
208
|
-
display: 'flex', alignItems: 'center', gap: 4,
|
|
209
|
-
padding: '3px 10px', borderRadius: 6, fontSize: '0.72rem',
|
|
210
|
-
cursor: canRun ? 'pointer' : 'not-allowed',
|
|
211
|
-
border: 'none', background: canRun ? 'var(--amber)' : 'var(--muted)',
|
|
212
|
-
color: canRun ? 'var(--amber-foreground)' : 'var(--muted-foreground)',
|
|
213
|
-
opacity: canRun ? 1 : 0.5,
|
|
214
|
-
}}
|
|
215
|
-
>
|
|
216
|
-
<Play size={10} /> Run
|
|
217
|
-
</button>
|
|
218
|
-
<button
|
|
219
|
-
onClick={onSkip}
|
|
220
|
-
style={{
|
|
221
|
-
padding: '3px 8px', borderRadius: 6, fontSize: '0.72rem',
|
|
222
|
-
cursor: 'pointer',
|
|
223
|
-
border: '1px solid var(--border)', background: 'transparent',
|
|
224
|
-
color: 'var(--muted-foreground)',
|
|
225
|
-
}}
|
|
226
|
-
>
|
|
227
|
-
Skip
|
|
228
|
-
</button>
|
|
229
|
-
</>
|
|
230
|
-
)}
|
|
231
|
-
{step.status === 'running' && (
|
|
232
|
-
<span className="font-display" style={{ fontSize: '0.7rem', color: 'var(--amber)' }}>executing…</span>
|
|
233
|
-
)}
|
|
234
|
-
{(step.status === 'done' || step.status === 'error') && (
|
|
235
|
-
<button
|
|
236
|
-
onClick={() => setExpanded(v => !v)}
|
|
237
|
-
style={{ padding: '3px 8px', borderRadius: 6, fontSize: '0.72rem', cursor: 'pointer', border: '1px solid var(--border)', background: 'transparent', color: 'var(--muted-foreground)' }}
|
|
238
|
-
>
|
|
239
|
-
<ChevronDown size={11} style={{ display: 'inline', transform: expanded ? 'rotate(180deg)' : 'none', transition: 'transform .15s' }} />
|
|
240
|
-
</button>
|
|
241
|
-
)}
|
|
242
|
-
</div>
|
|
243
|
-
</div>
|
|
244
|
-
|
|
245
|
-
{/* body / output */}
|
|
246
|
-
{(expanded || step.status === 'running') && (hasBody || hasOutput) && (
|
|
247
|
-
<div style={{ borderTop: '1px solid var(--border)' }}>
|
|
248
|
-
{hasBody && (
|
|
249
|
-
<div style={{ padding: '10px 14px', borderBottom: hasOutput ? '1px solid var(--border)' : 'none' }}>
|
|
250
|
-
<div dangerouslySetInnerHTML={{ __html: renderBody(step.body) }} />
|
|
251
|
-
</div>
|
|
252
|
-
)}
|
|
253
|
-
{hasOutput && (
|
|
254
|
-
<div style={{ padding: '10px 14px', background: 'var(--background)', position: 'relative' }}>
|
|
255
|
-
<div style={{ display: 'flex', alignItems: 'center', gap: 5, marginBottom: 6 }}>
|
|
256
|
-
<Sparkles size={11} style={{ color: 'var(--amber)' }} />
|
|
257
|
-
<span className="font-display" style={{ fontSize: '0.68rem', color: 'var(--muted-foreground)', textTransform: 'uppercase', letterSpacing: '.06em' }}>AI Output</span>
|
|
258
|
-
{step.status === 'running' && <span style={{ width: 5, height: 5, borderRadius: '50%', background: 'var(--amber)', animation: 'pulse 1.2s ease-in-out infinite', marginLeft: 4 }} />}
|
|
259
|
-
</div>
|
|
260
|
-
<div style={{ fontSize: '.82rem', lineHeight: 1.7, color: 'var(--foreground)', whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}>
|
|
261
|
-
{step.output}
|
|
262
|
-
</div>
|
|
263
|
-
</div>
|
|
264
|
-
)}
|
|
265
|
-
</div>
|
|
266
|
-
)}
|
|
267
|
-
</div>
|
|
268
|
-
);
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
// ─── Main renderer ────────────────────────────────────────────────────────────
|
|
272
|
-
|
|
273
|
-
export function WorkflowRenderer({ filePath, content }: RendererContext) {
|
|
274
|
-
const { t } = useLocale();
|
|
275
|
-
const parsed = useMemo(() => parseWorkflow(content), [content]);
|
|
276
|
-
const [steps, setSteps] = useState<WorkflowStep[]>(() => parsed.steps);
|
|
277
|
-
const [running, setRunning] = useState(false);
|
|
278
|
-
const abortRef = useRef<AbortController | null>(null);
|
|
279
|
-
|
|
280
|
-
// Reset when content changes externally
|
|
281
|
-
useMemo(() => { setSteps(parsed.steps.map(s => ({ ...s, status: 'pending' as StepStatus, output: '' }))); }, [parsed]);
|
|
282
|
-
|
|
283
|
-
const allStepsSummary = useMemo(() =>
|
|
284
|
-
parsed.steps.map((s, i) => `${i + 1}. ${s.heading}`).join('\n'),
|
|
285
|
-
[parsed]);
|
|
286
|
-
|
|
287
|
-
const runStep = useCallback(async (idx: number) => {
|
|
288
|
-
if (running) return;
|
|
289
|
-
abortRef.current?.abort();
|
|
290
|
-
const ctrl = new AbortController();
|
|
291
|
-
abortRef.current = ctrl;
|
|
292
|
-
setRunning(true);
|
|
293
|
-
|
|
294
|
-
setSteps(prev => prev.map((s, i) =>
|
|
295
|
-
i === idx ? { ...s, status: 'running', output: '' } : s));
|
|
296
|
-
|
|
297
|
-
try {
|
|
298
|
-
await runStepWithAI(
|
|
299
|
-
steps[idx], filePath, allStepsSummary,
|
|
300
|
-
(chunk) => setSteps(prev => prev.map((s, i) =>
|
|
301
|
-
i === idx ? { ...s, output: chunk } : s)),
|
|
302
|
-
ctrl.signal,
|
|
303
|
-
);
|
|
304
|
-
setSteps(prev => prev.map((s, i) =>
|
|
305
|
-
i === idx ? { ...s, status: 'done' } : s));
|
|
306
|
-
} catch (err: unknown) {
|
|
307
|
-
if (err instanceof Error && err.name === 'AbortError') return;
|
|
308
|
-
setSteps(prev => prev.map((s, i) =>
|
|
309
|
-
i === idx ? { ...s, status: 'error', output: (err instanceof Error ? err.message : String(err)) } : s));
|
|
310
|
-
} finally {
|
|
311
|
-
setRunning(false);
|
|
312
|
-
}
|
|
313
|
-
}, [running, steps, filePath, allStepsSummary]);
|
|
314
|
-
|
|
315
|
-
const skipStep = useCallback((idx: number) => {
|
|
316
|
-
setSteps(prev => prev.map((s, i) =>
|
|
317
|
-
i === idx ? { ...s, status: 'skipped' } : s));
|
|
318
|
-
}, []);
|
|
319
|
-
|
|
320
|
-
const reset = useCallback(() => {
|
|
321
|
-
abortRef.current?.abort();
|
|
322
|
-
setRunning(false);
|
|
323
|
-
setSteps(parsed.steps.map(s => ({ ...s, status: 'pending' as StepStatus, output: '' })));
|
|
324
|
-
}, [parsed]);
|
|
325
|
-
|
|
326
|
-
// Next runnable step = first pending step
|
|
327
|
-
const nextPendingIdx = steps.findIndex(s => s.status === 'pending');
|
|
328
|
-
const doneCount = steps.filter(s => s.status === 'done').length;
|
|
329
|
-
const progress = steps.length > 0 ? Math.round((doneCount / steps.length) * 100) : 0;
|
|
330
|
-
|
|
331
|
-
if (steps.length === 0) {
|
|
332
|
-
return (
|
|
333
|
-
<div className="font-display" style={{ padding: '3rem 1rem', textAlign: 'center', color: 'var(--muted-foreground)', fontSize: 12 }}>
|
|
334
|
-
No steps found. Add <code style={{ background: 'var(--muted)', padding: '1px 5px', borderRadius: 4 }}>## Step N: …</code> headings to define workflow steps.
|
|
335
|
-
</div>
|
|
336
|
-
);
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
return (
|
|
340
|
-
<div style={{ maxWidth: 720, margin: '0 auto', padding: '1.5rem 0' }}>
|
|
341
|
-
{/* header */}
|
|
342
|
-
<div style={{ marginBottom: '1.2rem' }}>
|
|
343
|
-
{parsed.meta.description && (
|
|
344
|
-
<p style={{ fontSize: '.82rem', color: 'var(--muted-foreground)', lineHeight: 1.6, marginBottom: 12 }}>
|
|
345
|
-
{parsed.meta.description}
|
|
346
|
-
</p>
|
|
347
|
-
)}
|
|
348
|
-
|
|
349
|
-
{/* progress + actions */}
|
|
350
|
-
<div style={{ display: 'flex', alignItems: 'center', gap: 10, flexWrap: 'wrap' }}>
|
|
351
|
-
{/* progress bar */}
|
|
352
|
-
<div style={{ flex: 1, minWidth: 120, height: 4, borderRadius: 999, background: 'var(--border)', overflow: 'hidden' }}>
|
|
353
|
-
<div style={{ height: '100%', width: `${progress}%`, background: 'var(--amber)', borderRadius: 999, transition: 'width .3s' }} />
|
|
354
|
-
</div>
|
|
355
|
-
<span className="font-display" style={{ fontSize: '0.7rem', color: 'var(--muted-foreground)', flexShrink: 0 }}>
|
|
356
|
-
{doneCount}/{steps.length} done
|
|
357
|
-
</span>
|
|
358
|
-
|
|
359
|
-
{/* run next */}
|
|
360
|
-
{nextPendingIdx >= 0 && (
|
|
361
|
-
<button
|
|
362
|
-
onClick={() => runStep(nextPendingIdx)}
|
|
363
|
-
disabled={running}
|
|
364
|
-
title={running ? t.hints.workflowRunning : undefined}
|
|
365
|
-
style={{
|
|
366
|
-
display: 'flex', alignItems: 'center', gap: 5,
|
|
367
|
-
padding: '4px 12px', borderRadius: 7, fontSize: '0.75rem',
|
|
368
|
-
cursor: running ? 'not-allowed' : 'pointer',
|
|
369
|
-
border: 'none', background: running ? 'var(--muted)' : 'var(--amber)',
|
|
370
|
-
color: running ? 'var(--muted-foreground)' : 'var(--amber-foreground)',
|
|
371
|
-
opacity: running ? 0.7 : 1,
|
|
372
|
-
}}
|
|
373
|
-
>
|
|
374
|
-
{running ? <Loader2 size={11} style={{ animation: 'spin 1s linear infinite' }} /> : <Play size={11} />}
|
|
375
|
-
Run next
|
|
376
|
-
</button>
|
|
377
|
-
)}
|
|
378
|
-
|
|
379
|
-
{/* reset */}
|
|
380
|
-
<button
|
|
381
|
-
onClick={reset}
|
|
382
|
-
style={{ padding: '4px 10px', borderRadius: 7, fontSize: '0.75rem', cursor: 'pointer', border: '1px solid var(--border)', background: 'transparent', color: 'var(--muted-foreground)', display: 'flex', alignItems: 'center', gap: 4 }}
|
|
383
|
-
>
|
|
384
|
-
<RotateCcw size={11} /> Reset
|
|
385
|
-
</button>
|
|
386
|
-
</div>
|
|
387
|
-
</div>
|
|
388
|
-
|
|
389
|
-
{/* step list */}
|
|
390
|
-
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
|
391
|
-
{steps.map((step, i) => (
|
|
392
|
-
<StepCard
|
|
393
|
-
key={i}
|
|
394
|
-
step={step}
|
|
395
|
-
isActive={i === nextPendingIdx}
|
|
396
|
-
canRun={!running}
|
|
397
|
-
onRun={() => runStep(i)}
|
|
398
|
-
onSkip={() => skipStep(i)}
|
|
399
|
-
/>
|
|
400
|
-
))}
|
|
401
|
-
</div>
|
|
402
|
-
|
|
403
|
-
<style>{`
|
|
404
|
-
@keyframes spin { to { transform: rotate(360deg); } }
|
|
405
|
-
@keyframes pulse { 0%,100% { opacity:1; } 50% { opacity:.3; } }
|
|
406
|
-
`}</style>
|
|
407
|
-
</div>
|
|
408
|
-
);
|
|
409
|
-
}
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import type { RendererDefinition } from '@/lib/renderers/registry';
|
|
2
|
-
|
|
3
|
-
export const manifest: RendererDefinition = {
|
|
4
|
-
id: 'workflow',
|
|
5
|
-
name: 'Workflow Runner',
|
|
6
|
-
description: 'Parses step-by-step workflow markdown into an interactive runner. Execute steps sequentially with AI assistance.',
|
|
7
|
-
author: 'MindOS',
|
|
8
|
-
icon: '⚡',
|
|
9
|
-
tags: ['workflow', 'automation', 'steps', 'ai'],
|
|
10
|
-
builtin: true,
|
|
11
|
-
entryPath: 'Workflow.md',
|
|
12
|
-
match: ({ filePath }) => /\b(Workflow|workflow|WORKFLOW)\b.*\.md$/i.test(filePath),
|
|
13
|
-
load: () => import('./WorkflowRenderer').then(m => ({ default: m.WorkflowRenderer })),
|
|
14
|
-
};
|