@geminilight/mindos 0.6.30 → 0.6.32

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.
Files changed (52) hide show
  1. package/README_zh.md +10 -4
  2. package/app/app/api/ask/route.ts +12 -7
  3. package/app/app/api/export/route.ts +105 -0
  4. package/app/app/globals.css +2 -2
  5. package/app/app/trash/page.tsx +7 -0
  6. package/app/app/view/[...path]/ViewPageClient.tsx +234 -2
  7. package/app/components/ExportModal.tsx +220 -0
  8. package/app/components/FileTree.tsx +22 -2
  9. package/app/components/HomeContent.tsx +91 -20
  10. package/app/components/MarkdownView.tsx +45 -10
  11. package/app/components/Sidebar.tsx +10 -1
  12. package/app/components/TrashPageClient.tsx +263 -0
  13. package/app/components/ask/ToolCallBlock.tsx +102 -18
  14. package/app/components/changes/ChangesContentPage.tsx +58 -14
  15. package/app/components/explore/ExploreContent.tsx +4 -7
  16. package/app/components/explore/UseCaseCard.tsx +18 -1
  17. package/app/components/explore/use-cases.generated.ts +76 -0
  18. package/app/components/explore/use-cases.yaml +185 -0
  19. package/app/components/panels/DiscoverPanel.tsx +1 -1
  20. package/app/components/renderers/workflow-yaml/StepEditor.tsx +98 -91
  21. package/app/components/renderers/workflow-yaml/WorkflowEditor.tsx +72 -72
  22. package/app/components/renderers/workflow-yaml/WorkflowRunner.tsx +175 -119
  23. package/app/components/renderers/workflow-yaml/WorkflowYamlRenderer.tsx +61 -61
  24. package/app/components/renderers/workflow-yaml/execution.ts +64 -12
  25. package/app/components/renderers/workflow-yaml/selectors.tsx +65 -13
  26. package/app/components/settings/AiTab.tsx +191 -174
  27. package/app/components/settings/AppearanceTab.tsx +168 -77
  28. package/app/components/settings/KnowledgeTab.tsx +131 -136
  29. package/app/components/settings/McpTab.tsx +11 -11
  30. package/app/components/settings/Primitives.tsx +60 -0
  31. package/app/components/settings/SettingsContent.tsx +15 -8
  32. package/app/components/settings/SyncTab.tsx +12 -12
  33. package/app/components/settings/UninstallTab.tsx +8 -18
  34. package/app/components/settings/UpdateTab.tsx +82 -82
  35. package/app/components/settings/types.ts +17 -8
  36. package/app/lib/acp/session.ts +12 -3
  37. package/app/lib/actions.ts +57 -3
  38. package/app/lib/agent/stream-consumer.ts +18 -0
  39. package/app/lib/agent/tools.ts +56 -9
  40. package/app/lib/core/export.ts +116 -0
  41. package/app/lib/core/trash.ts +241 -0
  42. package/app/lib/fs.ts +47 -0
  43. package/app/lib/hooks/usePinnedFiles.ts +90 -0
  44. package/app/lib/i18n/generated/explore-i18n.generated.ts +138 -0
  45. package/app/lib/i18n/index.ts +3 -0
  46. package/app/lib/i18n/modules/knowledge.ts +120 -6
  47. package/app/lib/i18n/modules/onboarding.ts +2 -134
  48. package/app/lib/i18n/modules/settings.ts +12 -0
  49. package/app/package.json +8 -2
  50. package/app/scripts/generate-explore.ts +145 -0
  51. package/package.json +1 -1
  52. 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';
@@ -19,16 +19,13 @@ export default function WorkflowEditor({ workflow, filePath, onChange, onSaved }
19
19
  const [saveError, setSaveError] = useState('');
20
20
  const [saveSuccess, setSaveSuccess] = useState(false);
21
21
 
22
- // Clear success indicator after 3s
23
22
  useEffect(() => {
24
23
  if (!saveSuccess) return;
25
24
  const t = setTimeout(() => setSaveSuccess(false), 3000);
26
25
  return () => clearTimeout(t);
27
26
  }, [saveSuccess]);
28
27
 
29
- const updateMeta = (patch: Partial<WorkflowYaml>) => {
30
- onChange({ ...workflow, ...patch });
31
- };
28
+ const updateMeta = (patch: Partial<WorkflowYaml>) => onChange({ ...workflow, ...patch });
32
29
 
33
30
  const updateStep = useCallback((index: number, step: WorkflowStep) => {
34
31
  const steps = [...workflow.steps];
@@ -83,14 +80,11 @@ export default function WorkflowEditor({ workflow, filePath, onChange, onSaved }
83
80
  }
84
81
  };
85
82
 
86
- // Keyboard shortcut: Cmd/Ctrl+S to save
87
83
  useEffect(() => {
88
84
  const handler = (e: KeyboardEvent) => {
89
85
  if ((e.metaKey || e.ctrlKey) && e.key === 's') {
90
86
  e.preventDefault();
91
- if (!saving && workflow.title.trim() && workflow.steps.length > 0) {
92
- handleSave();
93
- }
87
+ if (!saving && workflow.title.trim() && workflow.steps.length > 0) handleSave();
94
88
  }
95
89
  };
96
90
  window.addEventListener('keydown', handler);
@@ -101,100 +95,106 @@ export default function WorkflowEditor({ workflow, filePath, onChange, onSaved }
101
95
 
102
96
  return (
103
97
  <div>
104
- {/* Metadata */}
105
- <div className="space-y-3 mb-6">
106
- <div>
107
- <label className="block text-2xs font-medium text-muted-foreground mb-1">Title</label>
108
- <input type="text" value={workflow.title} onChange={e => updateMeta({ title: e.target.value })}
109
- placeholder="Workflow title"
110
- className="w-full px-3 py-2 text-sm font-medium rounded-lg border border-border bg-background text-foreground placeholder:text-muted-foreground focus:outline-none focus-visible:ring-1 focus-visible:ring-ring"
111
- />
112
- </div>
113
- <div className="grid grid-cols-[1fr,auto] gap-3">
114
- <div>
115
- <label className="block text-2xs font-medium text-muted-foreground mb-1">Description <span className="text-muted-foreground/50">(optional)</span></label>
116
- <input type="text" value={workflow.description || ''} onChange={e => updateMeta({ description: e.target.value || undefined })}
117
- placeholder="What does this workflow do?"
118
- className="w-full px-3 py-1.5 text-xs rounded-lg border border-border bg-background text-foreground placeholder:text-muted-foreground focus:outline-none focus-visible:ring-1 focus-visible:ring-ring"
119
- />
120
- </div>
121
- <div>
122
- <label className="block text-2xs font-medium text-muted-foreground mb-1">
123
- <FolderOpen size={10} className="inline mr-0.5 -mt-0.5" />
124
- Working dir
125
- </label>
126
- <DirPicker value={workflow.workDir || ''} onChange={v => updateMeta({ workDir: v || undefined })} />
127
- </div>
98
+ {/* ── Metadata Section ── */}
99
+ <div className="space-y-3 mb-8">
100
+ {/* Title — large, inline feel */}
101
+ <input type="text" value={workflow.title} onChange={e => updateMeta({ title: e.target.value })}
102
+ placeholder="Flow title..."
103
+ 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"
104
+ />
105
+ {/* Description — subtle underline */}
106
+ <input type="text" value={workflow.description || ''} onChange={e => updateMeta({ description: e.target.value || undefined })}
107
+ placeholder="Add a description..."
108
+ className="w-full text-sm bg-transparent text-muted-foreground placeholder:text-muted-foreground/30 focus:outline-none border-none p-0"
109
+ />
110
+
111
+ {/* Working directory always visible */}
112
+ <div className="flex items-center gap-2">
113
+ <FolderOpen size={12} className="text-muted-foreground/40 shrink-0" />
114
+ <DirPicker value={workflow.workDir || ''} onChange={v => updateMeta({ workDir: v || undefined })} />
128
115
  </div>
129
116
  </div>
130
117
 
131
- {/* Steps section */}
118
+ {/* ── Steps Section — Timeline style ── */}
132
119
  {workflow.steps.length > 0 ? (
133
- <>
134
- {/* Steps header */}
135
- <div className="flex items-center justify-between mb-3">
136
- <h3 className="text-xs font-semibold text-muted-foreground uppercase tracking-wide">
137
- Steps ({workflow.steps.length})
138
- </h3>
139
- </div>
120
+ <div className="relative">
121
+ {/* Vertical timeline line */}
122
+ {workflow.steps.length > 1 && (
123
+ <div className="absolute left-[19px] top-6 bottom-16 w-px bg-border" />
124
+ )}
140
125
 
141
126
  {/* Step list */}
142
- <div className="flex flex-col gap-2 mb-4">
127
+ <div className="flex flex-col gap-3 mb-5 relative">
143
128
  {workflow.steps.map((step, i) => (
144
- <StepEditor
145
- key={step.id}
146
- step={step}
147
- index={i}
148
- onChange={s => updateStep(i, s)}
149
- onDelete={() => deleteStep(i)}
150
- onMoveUp={i > 0 ? () => moveStep(i, i - 1) : undefined}
151
- onMoveDown={i < workflow.steps.length - 1 ? () => moveStep(i, i + 1) : undefined}
152
- />
129
+ <div key={step.id} className="relative pl-11">
130
+ {/* Timeline node */}
131
+ <div className="absolute left-[7px] top-3 w-[22px] h-[22px] rounded-full border-2 border-border bg-background z-10 flex items-center justify-center">
132
+ <span className="text-[10px] font-bold text-muted-foreground/60">{i + 1}</span>
133
+ </div>
134
+ <StepEditor
135
+ step={step}
136
+ index={i}
137
+ onChange={s => updateStep(i, s)}
138
+ onDelete={() => deleteStep(i)}
139
+ onMoveUp={i > 0 ? () => moveStep(i, i - 1) : undefined}
140
+ onMoveDown={i < workflow.steps.length - 1 ? () => moveStep(i, i + 1) : undefined}
141
+ />
142
+ </div>
153
143
  ))}
154
144
  </div>
155
145
 
156
- {/* Add step */}
157
- <button onClick={addStep}
158
- className="w-full flex items-center justify-center gap-1.5 px-3 py-2 rounded-xl border border-dashed border-border text-xs text-muted-foreground hover:text-foreground hover:border-[var(--amber)]/30 hover:bg-muted/30 transition-colors">
159
- <Plus size={13} />
160
- Add step
161
- </button>
162
- </>
146
+ {/* Add step — at the end of timeline */}
147
+ <div className="relative pl-11">
148
+ <div className="absolute left-[7px] top-2.5 w-[22px] h-[22px] rounded-full border-2 border-dashed border-border bg-background z-10 flex items-center justify-center">
149
+ <Plus size={9} className="text-muted-foreground/40" />
150
+ </div>
151
+ <button onClick={addStep}
152
+ 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">
153
+ Add step...
154
+ </button>
155
+ </div>
156
+ </div>
163
157
  ) : (
164
- /* Empty state: prominent CTA */
165
- <div className="flex flex-col items-center justify-center py-10 px-4 text-center rounded-xl border border-dashed border-border bg-muted/10">
166
- <Zap size={28} className="text-muted-foreground/30 mb-3" />
167
- <p className="text-sm font-medium text-muted-foreground mb-1">No steps yet</p>
168
- <p className="text-xs text-muted-foreground/60 mb-4 max-w-[260px]">
169
- Add your first step to define what the AI should do.
158
+ /* Empty state */
159
+ <div className="flex flex-col items-center justify-center py-16 text-center">
160
+ <div className="w-12 h-12 rounded-2xl bg-[var(--amber)]/8 flex items-center justify-center mb-4">
161
+ <Zap size={22} className="text-[var(--amber)]/60" />
162
+ </div>
163
+ <p className="text-sm font-medium text-foreground mb-1">Build your flow</p>
164
+ <p className="text-xs text-muted-foreground/60 mb-5 max-w-[240px]">
165
+ Each step is a task for an AI agent. Chain them together to automate complex workflows.
170
166
  </p>
171
167
  <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)] transition-colors hover:opacity-90">
173
- <Plus size={13} />
168
+ 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">
169
+ <Plus size={12} />
174
170
  Add first step
175
171
  </button>
176
172
  </div>
177
173
  )}
178
174
 
179
- {/* Save bar */}
180
- <div className="flex items-center gap-3 mt-6 pt-4 border-t border-border">
175
+ {/* ── Save bar — sticky bottom feel ── */}
176
+ <div className="flex items-center gap-3 mt-8 pt-4 border-t border-border/50">
181
177
  <button onClick={handleSave} disabled={!canSave}
182
178
  title={!workflow.title.trim() ? 'Title is required' : workflow.steps.length === 0 ? 'Add at least one step' : undefined}
183
- className="flex items-center gap-1.5 px-4 py-2 rounded-lg text-xs font-medium transition-colors disabled:opacity-50 disabled:cursor-not-allowed bg-[var(--amber)] text-[var(--amber-foreground)]">
184
- {saving ? <Loader2 size={13} className="animate-spin" /> : <Save size={13} />}
179
+ 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 ${
180
+ canSave
181
+ ? 'bg-[var(--amber)] text-[var(--amber-foreground)] hover:opacity-90'
182
+ : 'bg-muted text-muted-foreground'
183
+ }`}>
184
+ {saving ? <Loader2 size={12} className="animate-spin" /> : <Save size={12} />}
185
185
  {saving ? 'Saving...' : 'Save'}
186
186
  </button>
187
187
 
188
188
  {saveError && <span className="text-xs text-[var(--error)]">{saveError}</span>}
189
189
 
190
190
  {saveSuccess && !saveError && (
191
- <span className="flex items-center gap-1 text-2xs text-[var(--success)] animate-in fade-in">
191
+ <span className="flex items-center gap-1 text-2xs text-[var(--success)]">
192
192
  <CheckCircle2 size={11} />
193
193
  Saved
194
194
  </span>
195
195
  )}
196
196
 
197
- <span className="text-2xs text-muted-foreground/40 ml-auto">Ctrl+S</span>
197
+ <kbd className="text-2xs text-muted-foreground/30 ml-auto font-mono">Ctrl+S</kbd>
198
198
  </div>
199
199
  </div>
200
200
  );
@@ -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, FolderOpen } 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-[11px] font-bold text-muted-foreground/50">{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 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
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={`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
+ <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
- {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>
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
- {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>
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 by user' } : s));
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,81 @@ 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
- {workflow.description && (
199
- <p className="text-xs text-muted-foreground leading-relaxed mb-3">{workflow.description}</p>
208
+ {/* Working directory + description */}
209
+ {(workflow.workDir || workflow.description) && (
210
+ <div className="flex items-center gap-3 mb-4 text-xs text-muted-foreground flex-wrap">
211
+ {workflow.workDir && (
212
+ <span className="flex items-center gap-1.5 font-mono text-2xs bg-muted/50 px-2 py-0.5 rounded">
213
+ <FolderOpen size={10} className="shrink-0" />
214
+ {workflow.workDir}
215
+ </span>
216
+ )}
217
+ {workflow.description && <span className="leading-relaxed">{workflow.description}</span>}
218
+ </div>
200
219
  )}
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}%` }} />
220
+
221
+ {/* Progress bar full width, thin, elegant */}
222
+ <div className="mb-6">
223
+ <div className="flex items-center justify-between mb-2">
224
+ <div className="flex items-center gap-2">
225
+ {allDone ? (
226
+ <span className="flex items-center gap-1.5 text-xs font-medium text-[var(--success)]">
227
+ <CheckCircle2 size={13} />
228
+ Complete
229
+ </span>
230
+ ) : hasErrors ? (
231
+ <span className="flex items-center gap-1.5 text-xs font-medium text-[var(--error)]">
232
+ <AlertCircle size={13} />
233
+ {doneCount}/{steps.length} done
234
+ </span>
235
+ ) : (
236
+ <span className="text-xs text-muted-foreground">
237
+ {doneCount}/{steps.length}
238
+ </span>
239
+ )}
240
+ </div>
241
+ <div className="flex items-center gap-1.5">
242
+ {nextPendingIdx >= 0 && (
243
+ <button onClick={() => runStep(nextPendingIdx)} disabled={running}
244
+ 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)]">
245
+ {running ? <Loader2 size={11} className="animate-spin" /> : <ArrowRight size={11} />}
246
+ {running ? 'Running...' : 'Run next'}
247
+ </button>
248
+ )}
249
+ <button onClick={reset}
250
+ 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">
251
+ <RotateCcw size={10} />
252
+ Reset
253
+ </button>
254
+ </div>
255
+ </div>
256
+ <div className="h-1 rounded-full bg-border/50 overflow-hidden">
257
+ <div
258
+ className={`h-full rounded-full transition-all duration-500 ease-out ${
259
+ allDone ? 'bg-[var(--success)]' : hasErrors ? 'bg-[var(--error)]' : 'bg-[var(--amber)]'
260
+ }`}
261
+ style={{ width: `${progress}%` }}
262
+ />
204
263
  </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
264
  </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
- ))}
265
+
266
+ {/* Timeline step list */}
267
+ <div className="relative">
268
+ {/* Vertical line */}
269
+ {steps.length > 1 && (
270
+ <div className="absolute left-[12px] top-5 bottom-5 w-px bg-border/50" />
271
+ )}
272
+
273
+ <div className="flex flex-col gap-4">
274
+ {steps.map((step, i) => (
275
+ <RunStepCard key={step.id} step={step} canRun={!running}
276
+ onRun={() => runStep(i)} onSkip={() => skipStep(i)} onCancel={cancelExecution} />
277
+ ))}
278
+ </div>
223
279
  </div>
224
280
  </div>
225
281
  );