@geminilight/mindos 0.6.30 → 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_zh.md +10 -4
- package/app/app/api/ask/route.ts +12 -7
- package/app/app/api/export/route.ts +105 -0
- package/app/app/globals.css +2 -2
- package/app/app/trash/page.tsx +7 -0
- package/app/app/view/[...path]/ViewPageClient.tsx +234 -2
- package/app/components/ExportModal.tsx +220 -0
- package/app/components/FileTree.tsx +22 -2
- package/app/components/HomeContent.tsx +91 -20
- package/app/components/MarkdownView.tsx +45 -10
- package/app/components/Sidebar.tsx +10 -1
- package/app/components/TrashPageClient.tsx +263 -0
- 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/DiscoverPanel.tsx +1 -1
- package/app/components/renderers/workflow-yaml/StepEditor.tsx +98 -91
- package/app/components/renderers/workflow-yaml/WorkflowEditor.tsx +82 -72
- package/app/components/renderers/workflow-yaml/WorkflowRunner.tsx +163 -120
- package/app/components/renderers/workflow-yaml/WorkflowYamlRenderer.tsx +61 -61
- package/app/components/renderers/workflow-yaml/execution.ts +64 -12
- package/app/components/renderers/workflow-yaml/selectors.tsx +64 -12
- 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/lib/acp/session.ts +12 -3
- package/app/lib/actions.ts +57 -3
- package/app/lib/agent/stream-consumer.ts +18 -0
- 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 +120 -6
- package/app/lib/i18n/modules/onboarding.ts +2 -134
- package/app/lib/i18n/modules/settings.ts +12 -0
- package/app/package.json +8 -2
- package/app/scripts/generate-explore.ts +145 -0
- package/package.json +1 -1
- package/app/components/explore/use-cases.ts +0 -58
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { useState, useCallback, useEffect } from 'react';
|
|
4
|
-
import { Plus, Save, Loader2, FolderOpen, Zap, CheckCircle2 } from 'lucide-react';
|
|
4
|
+
import { Plus, Save, Loader2, FolderOpen, Zap, CheckCircle2, GripVertical } from 'lucide-react';
|
|
5
5
|
import StepEditor from './StepEditor';
|
|
6
6
|
import { serializeWorkflowYaml, generateStepId } from './serializer';
|
|
7
7
|
import type { WorkflowYaml, WorkflowStep } from './types';
|
|
@@ -18,17 +18,15 @@ export default function WorkflowEditor({ workflow, filePath, onChange, onSaved }
|
|
|
18
18
|
const [saving, setSaving] = useState(false);
|
|
19
19
|
const [saveError, setSaveError] = useState('');
|
|
20
20
|
const [saveSuccess, setSaveSuccess] = useState(false);
|
|
21
|
+
const [showAdvanced, setShowAdvanced] = useState(!!(workflow.workDir));
|
|
21
22
|
|
|
22
|
-
// Clear success indicator after 3s
|
|
23
23
|
useEffect(() => {
|
|
24
24
|
if (!saveSuccess) return;
|
|
25
25
|
const t = setTimeout(() => setSaveSuccess(false), 3000);
|
|
26
26
|
return () => clearTimeout(t);
|
|
27
27
|
}, [saveSuccess]);
|
|
28
28
|
|
|
29
|
-
const updateMeta = (patch: Partial<WorkflowYaml>) => {
|
|
30
|
-
onChange({ ...workflow, ...patch });
|
|
31
|
-
};
|
|
29
|
+
const updateMeta = (patch: Partial<WorkflowYaml>) => onChange({ ...workflow, ...patch });
|
|
32
30
|
|
|
33
31
|
const updateStep = useCallback((index: number, step: WorkflowStep) => {
|
|
34
32
|
const steps = [...workflow.steps];
|
|
@@ -83,14 +81,11 @@ export default function WorkflowEditor({ workflow, filePath, onChange, onSaved }
|
|
|
83
81
|
}
|
|
84
82
|
};
|
|
85
83
|
|
|
86
|
-
// Keyboard shortcut: Cmd/Ctrl+S to save
|
|
87
84
|
useEffect(() => {
|
|
88
85
|
const handler = (e: KeyboardEvent) => {
|
|
89
86
|
if ((e.metaKey || e.ctrlKey) && e.key === 's') {
|
|
90
87
|
e.preventDefault();
|
|
91
|
-
if (!saving && workflow.title.trim() && workflow.steps.length > 0)
|
|
92
|
-
handleSave();
|
|
93
|
-
}
|
|
88
|
+
if (!saving && workflow.title.trim() && workflow.steps.length > 0) handleSave();
|
|
94
89
|
}
|
|
95
90
|
};
|
|
96
91
|
window.addEventListener('keydown', handler);
|
|
@@ -101,100 +96,115 @@ export default function WorkflowEditor({ workflow, filePath, onChange, onSaved }
|
|
|
101
96
|
|
|
102
97
|
return (
|
|
103
98
|
<div>
|
|
104
|
-
{/* Metadata */}
|
|
105
|
-
<div className="space-y-3 mb-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
99
|
+
{/* ── Metadata Section ── */}
|
|
100
|
+
<div className="space-y-3 mb-8">
|
|
101
|
+
{/* Title — large, inline feel */}
|
|
102
|
+
<input type="text" value={workflow.title} onChange={e => updateMeta({ title: e.target.value })}
|
|
103
|
+
placeholder="Flow title..."
|
|
104
|
+
className="w-full text-lg font-semibold bg-transparent text-foreground placeholder:text-muted-foreground/40 focus:outline-none border-none p-0 leading-tight"
|
|
105
|
+
/>
|
|
106
|
+
{/* Description — subtle underline */}
|
|
107
|
+
<input type="text" value={workflow.description || ''} onChange={e => updateMeta({ description: e.target.value || undefined })}
|
|
108
|
+
placeholder="Add a description..."
|
|
109
|
+
className="w-full text-sm bg-transparent text-muted-foreground placeholder:text-muted-foreground/30 focus:outline-none border-none p-0"
|
|
110
|
+
/>
|
|
111
|
+
|
|
112
|
+
{/* Advanced toggle */}
|
|
113
|
+
<div className="flex items-center gap-3">
|
|
114
|
+
{!showAdvanced ? (
|
|
115
|
+
<button onClick={() => setShowAdvanced(true)}
|
|
116
|
+
className="text-2xs text-muted-foreground/50 hover:text-muted-foreground transition-colors">
|
|
117
|
+
+ Working directory
|
|
118
|
+
</button>
|
|
119
|
+
) : (
|
|
120
|
+
<div className="flex items-center gap-2 flex-1">
|
|
121
|
+
<FolderOpen size={12} className="text-muted-foreground/40 shrink-0" />
|
|
122
|
+
<DirPicker value={workflow.workDir || ''} onChange={v => updateMeta({ workDir: v || undefined })} />
|
|
123
|
+
</div>
|
|
124
|
+
)}
|
|
128
125
|
</div>
|
|
129
126
|
</div>
|
|
130
127
|
|
|
131
|
-
{/* Steps
|
|
128
|
+
{/* ── Steps Section — Timeline style ── */}
|
|
132
129
|
{workflow.steps.length > 0 ? (
|
|
133
|
-
|
|
134
|
-
{/*
|
|
135
|
-
|
|
136
|
-
<
|
|
137
|
-
|
|
138
|
-
</h3>
|
|
139
|
-
</div>
|
|
130
|
+
<div className="relative">
|
|
131
|
+
{/* Vertical timeline line */}
|
|
132
|
+
{workflow.steps.length > 1 && (
|
|
133
|
+
<div className="absolute left-[15px] top-6 bottom-16 w-px bg-border" />
|
|
134
|
+
)}
|
|
140
135
|
|
|
141
136
|
{/* Step list */}
|
|
142
|
-
<div className="flex flex-col gap-
|
|
137
|
+
<div className="flex flex-col gap-3 mb-5 relative">
|
|
143
138
|
{workflow.steps.map((step, i) => (
|
|
144
|
-
<
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
139
|
+
<div key={step.id} className="relative pl-9">
|
|
140
|
+
{/* Timeline node */}
|
|
141
|
+
<div className="absolute left-[9px] top-3 w-[13px] h-[13px] rounded-full border-2 border-border bg-background z-10 flex items-center justify-center">
|
|
142
|
+
<span className="text-[7px] font-bold text-muted-foreground/60">{i + 1}</span>
|
|
143
|
+
</div>
|
|
144
|
+
<StepEditor
|
|
145
|
+
step={step}
|
|
146
|
+
index={i}
|
|
147
|
+
onChange={s => updateStep(i, s)}
|
|
148
|
+
onDelete={() => deleteStep(i)}
|
|
149
|
+
onMoveUp={i > 0 ? () => moveStep(i, i - 1) : undefined}
|
|
150
|
+
onMoveDown={i < workflow.steps.length - 1 ? () => moveStep(i, i + 1) : undefined}
|
|
151
|
+
/>
|
|
152
|
+
</div>
|
|
153
153
|
))}
|
|
154
154
|
</div>
|
|
155
155
|
|
|
156
|
-
{/* Add step */}
|
|
157
|
-
<
|
|
158
|
-
className="
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
156
|
+
{/* Add step — at the end of timeline */}
|
|
157
|
+
<div className="relative pl-9">
|
|
158
|
+
<div className="absolute left-[9px] top-2.5 w-[13px] h-[13px] rounded-full border-2 border-dashed border-border bg-background z-10 flex items-center justify-center">
|
|
159
|
+
<Plus size={7} className="text-muted-foreground/40" />
|
|
160
|
+
</div>
|
|
161
|
+
<button onClick={addStep}
|
|
162
|
+
className="w-full text-left px-3 py-2 rounded-lg text-xs text-muted-foreground/60 hover:text-muted-foreground hover:bg-muted/40 transition-colors">
|
|
163
|
+
Add step...
|
|
164
|
+
</button>
|
|
165
|
+
</div>
|
|
166
|
+
</div>
|
|
163
167
|
) : (
|
|
164
|
-
/* Empty state
|
|
165
|
-
<div className="flex flex-col items-center justify-center py-
|
|
166
|
-
<
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
168
|
+
/* Empty state */
|
|
169
|
+
<div className="flex flex-col items-center justify-center py-16 text-center">
|
|
170
|
+
<div className="w-12 h-12 rounded-2xl bg-[var(--amber)]/8 flex items-center justify-center mb-4">
|
|
171
|
+
<Zap size={22} className="text-[var(--amber)]/60" />
|
|
172
|
+
</div>
|
|
173
|
+
<p className="text-sm font-medium text-foreground mb-1">Build your flow</p>
|
|
174
|
+
<p className="text-xs text-muted-foreground/60 mb-5 max-w-[240px]">
|
|
175
|
+
Each step is a task for an AI agent. Chain them together to automate complex workflows.
|
|
170
176
|
</p>
|
|
171
177
|
<button onClick={addStep}
|
|
172
|
-
className="inline-flex items-center gap-1.5 px-4 py-2 rounded-lg text-xs font-medium bg-[var(--amber)] text-[var(--amber-foreground)]
|
|
173
|
-
<Plus size={
|
|
178
|
+
className="inline-flex items-center gap-1.5 px-4 py-2 rounded-lg text-xs font-medium bg-[var(--amber)] text-[var(--amber-foreground)] hover:opacity-90 transition-opacity">
|
|
179
|
+
<Plus size={12} />
|
|
174
180
|
Add first step
|
|
175
181
|
</button>
|
|
176
182
|
</div>
|
|
177
183
|
)}
|
|
178
184
|
|
|
179
|
-
{/* Save bar */}
|
|
180
|
-
<div className="flex items-center gap-3 mt-
|
|
185
|
+
{/* ── Save bar — sticky bottom feel ── */}
|
|
186
|
+
<div className="flex items-center gap-3 mt-8 pt-4 border-t border-border/50">
|
|
181
187
|
<button onClick={handleSave} disabled={!canSave}
|
|
182
188
|
title={!workflow.title.trim() ? 'Title is required' : workflow.steps.length === 0 ? 'Add at least one step' : undefined}
|
|
183
|
-
className=
|
|
184
|
-
|
|
189
|
+
className={`flex items-center gap-1.5 px-4 py-2 rounded-lg text-xs font-medium transition-all disabled:opacity-40 disabled:cursor-not-allowed ${
|
|
190
|
+
canSave
|
|
191
|
+
? 'bg-[var(--amber)] text-[var(--amber-foreground)] hover:opacity-90'
|
|
192
|
+
: 'bg-muted text-muted-foreground'
|
|
193
|
+
}`}>
|
|
194
|
+
{saving ? <Loader2 size={12} className="animate-spin" /> : <Save size={12} />}
|
|
185
195
|
{saving ? 'Saving...' : 'Save'}
|
|
186
196
|
</button>
|
|
187
197
|
|
|
188
198
|
{saveError && <span className="text-xs text-[var(--error)]">{saveError}</span>}
|
|
189
199
|
|
|
190
200
|
{saveSuccess && !saveError && (
|
|
191
|
-
<span className="flex items-center gap-1 text-2xs text-[var(--success)]
|
|
201
|
+
<span className="flex items-center gap-1 text-2xs text-[var(--success)]">
|
|
192
202
|
<CheckCircle2 size={11} />
|
|
193
203
|
Saved
|
|
194
204
|
</span>
|
|
195
205
|
)}
|
|
196
206
|
|
|
197
|
-
<
|
|
207
|
+
<kbd className="text-2xs text-muted-foreground/30 ml-auto font-mono">Ctrl+S</kbd>
|
|
198
208
|
</div>
|
|
199
209
|
</div>
|
|
200
210
|
);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { useState, useRef, useCallback, useEffect } from 'react';
|
|
4
|
-
import { Play, SkipForward, RotateCcw, CheckCircle2, Circle, Loader2, AlertCircle, ChevronDown, Sparkles, XCircle, Clock } from 'lucide-react';
|
|
4
|
+
import { Play, SkipForward, RotateCcw, CheckCircle2, Circle, Loader2, AlertCircle, ChevronDown, Sparkles, XCircle, Clock, ArrowRight } from 'lucide-react';
|
|
5
5
|
import { runStepWithAI, clearSkillCache } from './execution';
|
|
6
6
|
import type { WorkflowYaml, WorkflowStepRuntime, StepStatus } from './types';
|
|
7
7
|
|
|
@@ -11,26 +11,6 @@ function initSteps(workflow: WorkflowYaml): WorkflowStepRuntime[] {
|
|
|
11
11
|
}));
|
|
12
12
|
}
|
|
13
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
14
|
function formatDuration(ms: number): string {
|
|
35
15
|
if (ms < 1000) return `${ms}ms`;
|
|
36
16
|
const secs = Math.round(ms / 1000);
|
|
@@ -38,96 +18,127 @@ function formatDuration(ms: number): string {
|
|
|
38
18
|
return `${Math.floor(secs / 60)}m ${secs % 60}s`;
|
|
39
19
|
}
|
|
40
20
|
|
|
21
|
+
/** Timeline node — the colored circle on the left */
|
|
22
|
+
function TimelineNode({ status, index }: { status: StepStatus; index: number }) {
|
|
23
|
+
const base = 'w-[26px] h-[26px] rounded-full flex items-center justify-center shrink-0 transition-all';
|
|
24
|
+
if (status === 'running') return (
|
|
25
|
+
<div className={`${base} bg-[var(--amber)]/15 ring-2 ring-[var(--amber)]/30`}>
|
|
26
|
+
<Loader2 size={12} className="text-[var(--amber)] animate-spin" />
|
|
27
|
+
</div>
|
|
28
|
+
);
|
|
29
|
+
if (status === 'done') return (
|
|
30
|
+
<div className={`${base} bg-[var(--success)]/15`}>
|
|
31
|
+
<CheckCircle2 size={13} className="text-[var(--success)]" />
|
|
32
|
+
</div>
|
|
33
|
+
);
|
|
34
|
+
if (status === 'error') return (
|
|
35
|
+
<div className={`${base} bg-[var(--error)]/15`}>
|
|
36
|
+
<AlertCircle size={13} className="text-[var(--error)]" />
|
|
37
|
+
</div>
|
|
38
|
+
);
|
|
39
|
+
if (status === 'skipped') return (
|
|
40
|
+
<div className={`${base} bg-muted/50`}>
|
|
41
|
+
<SkipForward size={11} className="text-muted-foreground/40" />
|
|
42
|
+
</div>
|
|
43
|
+
);
|
|
44
|
+
// pending
|
|
45
|
+
return (
|
|
46
|
+
<div className={`${base} border-2 border-border bg-background`}>
|
|
47
|
+
<span className="text-[9px] font-bold text-muted-foreground/40">{index + 1}</span>
|
|
48
|
+
</div>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
41
52
|
function RunStepCard({ step, canRun, onRun, onSkip, onCancel }: {
|
|
42
53
|
step: WorkflowStepRuntime; canRun: boolean;
|
|
43
54
|
onRun: () => void; onSkip: () => void; onCancel: () => void;
|
|
44
55
|
}) {
|
|
45
56
|
const [expanded, setExpanded] = useState(false);
|
|
46
|
-
const
|
|
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
|
|
57
|
+
const hasOutput = !!(step.output || step.error);
|
|
53
58
|
const allSkills = step.skills?.length ? step.skills : (step.skill ? [step.skill] : []);
|
|
59
|
+
const isActive = step.status === 'running' || step.status === 'pending';
|
|
54
60
|
|
|
55
61
|
return (
|
|
56
|
-
<div className={`
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
+
<div className={`transition-opacity ${step.status === 'skipped' ? 'opacity-50' : ''}`}>
|
|
63
|
+
{/* Main row */}
|
|
64
|
+
<div className="flex items-start gap-3">
|
|
65
|
+
<TimelineNode status={step.status} index={step.index} />
|
|
66
|
+
<div className="flex-1 min-w-0 pt-0.5">
|
|
67
|
+
{/* Name + meta */}
|
|
68
|
+
<div className="flex items-center gap-2 justify-between">
|
|
69
|
+
<span className={`text-sm font-medium truncate ${
|
|
70
|
+
step.status === 'done' ? 'text-foreground' :
|
|
71
|
+
step.status === 'running' ? 'text-[var(--amber)]' :
|
|
72
|
+
step.status === 'error' ? 'text-[var(--error)]' :
|
|
73
|
+
'text-foreground/70'
|
|
74
|
+
}`}>
|
|
62
75
|
{step.name}
|
|
76
|
+
</span>
|
|
77
|
+
<div className="flex items-center gap-1.5 shrink-0">
|
|
78
|
+
{step.durationMs != null && (step.status === 'done' || step.status === 'error') && (
|
|
79
|
+
<span className="text-2xs text-muted-foreground/40 font-mono">{formatDuration(step.durationMs)}</span>
|
|
80
|
+
)}
|
|
81
|
+
{step.status === 'pending' && (
|
|
82
|
+
<>
|
|
83
|
+
<button onClick={onRun} disabled={!canRun}
|
|
84
|
+
className="flex items-center gap-1 px-2 py-0.5 rounded-md text-2xs font-medium transition-all disabled:opacity-30 bg-[var(--amber)] text-[var(--amber-foreground)]"
|
|
85
|
+
><Play size={9} /> Run</button>
|
|
86
|
+
<button onClick={onSkip}
|
|
87
|
+
className="px-1.5 py-0.5 rounded-md text-2xs text-muted-foreground/50 hover:text-muted-foreground hover:bg-muted transition-colors">
|
|
88
|
+
Skip
|
|
89
|
+
</button>
|
|
90
|
+
</>
|
|
91
|
+
)}
|
|
92
|
+
{step.status === 'running' && (
|
|
93
|
+
<button onClick={onCancel}
|
|
94
|
+
className="flex items-center gap-1 px-2 py-0.5 rounded-md text-2xs text-[var(--error)]/70 hover:text-[var(--error)] hover:bg-[var(--error)]/10 transition-colors">
|
|
95
|
+
<XCircle size={9} /> Stop
|
|
96
|
+
</button>
|
|
97
|
+
)}
|
|
98
|
+
{hasOutput && step.status !== 'running' && (
|
|
99
|
+
<button onClick={() => setExpanded(v => !v)}
|
|
100
|
+
className="p-0.5 rounded text-muted-foreground/30 hover:text-muted-foreground transition-colors">
|
|
101
|
+
<ChevronDown size={12} className={`transition-transform ${expanded ? 'rotate-180' : ''}`} />
|
|
102
|
+
</button>
|
|
103
|
+
)}
|
|
63
104
|
</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
105
|
</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
106
|
|
|
82
|
-
{
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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>
|
|
107
|
+
{/* Agent/skill badges */}
|
|
108
|
+
{(step.agent || allSkills.length > 0) && (
|
|
109
|
+
<div className="flex gap-1 mt-0.5 flex-wrap">
|
|
110
|
+
{step.agent && <span className="text-2xs text-muted-foreground/50">{step.agent}</span>}
|
|
111
|
+
{step.agent && allSkills.length > 0 && <span className="text-2xs text-muted-foreground/20">/</span>}
|
|
112
|
+
{allSkills.map(s => <span key={s} className="text-2xs text-[var(--amber)]/60">{s}</span>)}
|
|
117
113
|
</div>
|
|
118
114
|
)}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
115
|
+
|
|
116
|
+
{/* Output area — auto-show when running */}
|
|
117
|
+
{(expanded || step.status === 'running') && hasOutput && (
|
|
118
|
+
<div className="mt-2 rounded-lg overflow-hidden border border-border/50">
|
|
119
|
+
{step.error && (
|
|
120
|
+
<div className="px-3 py-2.5 bg-[var(--error)]/5">
|
|
121
|
+
<div className="flex items-center gap-1.5 mb-1">
|
|
122
|
+
<AlertCircle size={10} className="text-[var(--error)]" />
|
|
123
|
+
<span className="text-2xs font-medium text-[var(--error)]">Error</span>
|
|
124
|
+
</div>
|
|
125
|
+
<div className="text-xs text-[var(--error)]/80 whitespace-pre-wrap break-words leading-relaxed">{step.error}</div>
|
|
126
|
+
</div>
|
|
127
|
+
)}
|
|
128
|
+
{step.output && (
|
|
129
|
+
<div className="px-3 py-2.5 bg-muted/20">
|
|
130
|
+
<div className="flex items-center gap-1.5 mb-1.5">
|
|
131
|
+
<Sparkles size={10} className="text-[var(--amber)]" />
|
|
132
|
+
<span className="text-2xs text-muted-foreground/50 uppercase tracking-wider font-medium">Output</span>
|
|
133
|
+
{step.status === 'running' && <span className="w-1.5 h-1.5 rounded-full bg-[var(--amber)] animate-pulse" />}
|
|
134
|
+
</div>
|
|
135
|
+
<div className="text-xs leading-relaxed text-foreground/80 whitespace-pre-wrap break-words">{step.output}</div>
|
|
136
|
+
</div>
|
|
137
|
+
)}
|
|
127
138
|
</div>
|
|
128
139
|
)}
|
|
129
140
|
</div>
|
|
130
|
-
|
|
141
|
+
</div>
|
|
131
142
|
</div>
|
|
132
143
|
);
|
|
133
144
|
}
|
|
@@ -148,7 +159,7 @@ export default function WorkflowRunner({ workflow, filePath }: { workflow: Workf
|
|
|
148
159
|
abortRef.current?.abort();
|
|
149
160
|
abortRef.current = null;
|
|
150
161
|
setRunning(false);
|
|
151
|
-
setSteps(prev => prev.map(s => s.status === 'running' ? { ...s, status: 'error', error: 'Cancelled
|
|
162
|
+
setSteps(prev => prev.map(s => s.status === 'running' ? { ...s, status: 'error', error: 'Cancelled' } : s));
|
|
152
163
|
}, []);
|
|
153
164
|
|
|
154
165
|
const runStep = useCallback(async (idx: number) => {
|
|
@@ -157,10 +168,8 @@ export default function WorkflowRunner({ workflow, filePath }: { workflow: Workf
|
|
|
157
168
|
const ctrl = new AbortController();
|
|
158
169
|
abortRef.current = ctrl;
|
|
159
170
|
setRunning(true);
|
|
160
|
-
|
|
161
171
|
const startTime = Date.now();
|
|
162
172
|
setSteps(prev => prev.map((s, i) => i === idx ? { ...s, status: 'running', output: '', error: undefined, startedAt: new Date() } : s));
|
|
163
|
-
|
|
164
173
|
try {
|
|
165
174
|
const step = workflow.steps[idx];
|
|
166
175
|
const runtimeStep: WorkflowStepRuntime = { ...step, index: idx, status: 'running', output: '' };
|
|
@@ -192,34 +201,68 @@ export default function WorkflowRunner({ workflow, filePath }: { workflow: Workf
|
|
|
192
201
|
const progress = steps.length > 0 ? Math.round((doneCount / steps.length) * 100) : 0;
|
|
193
202
|
const allDone = doneCount === steps.length && steps.length > 0;
|
|
194
203
|
const nextPendingIdx = steps.findIndex(s => s.status === 'pending');
|
|
204
|
+
const hasErrors = steps.some(s => s.status === 'error');
|
|
195
205
|
|
|
196
206
|
return (
|
|
197
207
|
<div>
|
|
198
|
-
{
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
208
|
+
{/* Progress bar — full width, thin, elegant */}
|
|
209
|
+
<div className="mb-6">
|
|
210
|
+
<div className="flex items-center justify-between mb-2">
|
|
211
|
+
<div className="flex items-center gap-2">
|
|
212
|
+
{allDone ? (
|
|
213
|
+
<span className="flex items-center gap-1.5 text-xs font-medium text-[var(--success)]">
|
|
214
|
+
<CheckCircle2 size={13} />
|
|
215
|
+
Complete
|
|
216
|
+
</span>
|
|
217
|
+
) : hasErrors ? (
|
|
218
|
+
<span className="flex items-center gap-1.5 text-xs font-medium text-[var(--error)]">
|
|
219
|
+
<AlertCircle size={13} />
|
|
220
|
+
{doneCount}/{steps.length} done
|
|
221
|
+
</span>
|
|
222
|
+
) : (
|
|
223
|
+
<span className="text-xs text-muted-foreground">
|
|
224
|
+
{doneCount}/{steps.length}
|
|
225
|
+
</span>
|
|
226
|
+
)}
|
|
227
|
+
</div>
|
|
228
|
+
<div className="flex items-center gap-1.5">
|
|
229
|
+
{nextPendingIdx >= 0 && (
|
|
230
|
+
<button onClick={() => runStep(nextPendingIdx)} disabled={running}
|
|
231
|
+
className="flex items-center gap-1.5 px-3 py-1 rounded-md text-xs font-medium transition-all disabled:opacity-40 bg-[var(--amber)] text-[var(--amber-foreground)]">
|
|
232
|
+
{running ? <Loader2 size={11} className="animate-spin" /> : <ArrowRight size={11} />}
|
|
233
|
+
{running ? 'Running...' : 'Run next'}
|
|
234
|
+
</button>
|
|
235
|
+
)}
|
|
236
|
+
<button onClick={reset}
|
|
237
|
+
className="flex items-center gap-1 px-2 py-1 rounded-md text-2xs text-muted-foreground/50 hover:text-muted-foreground hover:bg-muted transition-colors">
|
|
238
|
+
<RotateCcw size={10} />
|
|
239
|
+
Reset
|
|
240
|
+
</button>
|
|
241
|
+
</div>
|
|
242
|
+
</div>
|
|
243
|
+
<div className="h-1 rounded-full bg-border/50 overflow-hidden">
|
|
244
|
+
<div
|
|
245
|
+
className={`h-full rounded-full transition-all duration-500 ease-out ${
|
|
246
|
+
allDone ? 'bg-[var(--success)]' : hasErrors ? 'bg-[var(--error)]' : 'bg-[var(--amber)]'
|
|
247
|
+
}`}
|
|
248
|
+
style={{ width: `${progress}%` }}
|
|
249
|
+
/>
|
|
204
250
|
</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
251
|
</div>
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
252
|
+
|
|
253
|
+
{/* Timeline step list */}
|
|
254
|
+
<div className="relative">
|
|
255
|
+
{/* Vertical line */}
|
|
256
|
+
{steps.length > 1 && (
|
|
257
|
+
<div className="absolute left-[12px] top-5 bottom-5 w-px bg-border/50" />
|
|
258
|
+
)}
|
|
259
|
+
|
|
260
|
+
<div className="flex flex-col gap-4">
|
|
261
|
+
{steps.map((step, i) => (
|
|
262
|
+
<RunStepCard key={step.id} step={step} canRun={!running}
|
|
263
|
+
onRun={() => runStep(i)} onSkip={() => skipStep(i)} onCancel={cancelExecution} />
|
|
264
|
+
))}
|
|
265
|
+
</div>
|
|
223
266
|
</div>
|
|
224
267
|
</div>
|
|
225
268
|
);
|