@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 { useMemo, useState, useCallback, useRef } from 'react';
4
- import { Pencil, Play, AlertCircle } from 'lucide-react';
4
+ import { Pencil, Play, AlertCircle, Zap } from 'lucide-react';
5
5
  import type { RendererContext } from '@/lib/renderers/registry';
6
6
  import { parseWorkflowYaml } from './parser';
7
7
  import WorkflowEditor from './WorkflowEditor';
@@ -15,15 +15,12 @@ export function WorkflowYamlRenderer({ filePath, content }: RendererContext) {
15
15
  const [mode, setMode] = useState<Mode>('edit');
16
16
  const [dirty, setDirty] = useState(false);
17
17
 
18
- // Editor state: start from parsed workflow or a blank template
19
18
  const [editWorkflow, setEditWorkflow] = useState<WorkflowYaml>(() => {
20
19
  if (parsed.workflow) return structuredClone(parsed.workflow);
21
20
  return { title: '', description: '', steps: [] };
22
21
  });
23
22
 
24
- // Track original content for dirty detection
25
23
  const savedContentRef = useRef(content);
26
-
27
24
  const latestParsed = useMemo(() => parsed.workflow ?? null, [parsed.workflow]);
28
25
 
29
26
  const handleEditorChange = useCallback((wf: WorkflowYaml) => {
@@ -31,77 +28,88 @@ export function WorkflowYamlRenderer({ filePath, content }: RendererContext) {
31
28
  setDirty(true);
32
29
  }, []);
33
30
 
34
- const handleSaved = useCallback(() => {
35
- setDirty(false);
36
- }, []);
31
+ const handleSaved = useCallback(() => setDirty(false), []);
37
32
 
38
- // Runner uses latest parsed workflow (from file), not editor state
39
33
  const runWorkflow = latestParsed ?? editWorkflow;
40
34
  const canRun = runWorkflow.steps.length > 0 && !!runWorkflow.title;
41
35
  const hasParseErrors = parsed.errors.length > 0;
42
36
 
43
- // Explain why Run is disabled
44
37
  const runDisabledReason = !runWorkflow.title
45
38
  ? 'Add a title first'
46
39
  : runWorkflow.steps.length === 0
47
40
  ? 'Add at least one step'
48
- : dirty
49
- ? 'Save changes first'
50
- : undefined;
41
+ : dirty ? 'Save changes first' : undefined;
51
42
 
52
43
  const handleModeSwitch = (target: Mode) => {
53
- if (target === 'run' && dirty) {
54
- // Don't block, but show the tooltip
55
- }
56
44
  if (target === 'run' && !canRun) return;
57
45
  setMode(target);
58
46
  };
59
47
 
60
48
  return (
61
- <div className="max-w-[720px] mx-auto py-6 px-1">
62
- {/* Mode switcher + title */}
63
- <div className="flex items-center gap-3 mb-5">
64
- <div className="flex rounded-lg border border-border overflow-hidden text-xs">
65
- <button
66
- onClick={() => handleModeSwitch('edit')}
67
- className={`flex items-center gap-1.5 px-3.5 py-1.5 transition-colors ${
68
- mode === 'edit'
69
- ? 'bg-[var(--amber)] text-[var(--amber-foreground)] font-medium'
70
- : 'bg-card text-muted-foreground hover:bg-muted'
71
- }`}
72
- >
73
- <Pencil size={12} />
74
- Edit
75
- </button>
76
- <button
77
- onClick={() => handleModeSwitch('run')}
78
- disabled={!canRun}
79
- title={runDisabledReason}
80
- className={`flex items-center gap-1.5 px-3.5 py-1.5 transition-colors disabled:opacity-40 disabled:cursor-not-allowed ${
81
- mode === 'run'
82
- ? 'bg-[var(--amber)] text-[var(--amber-foreground)] font-medium'
83
- : 'bg-card text-muted-foreground hover:bg-muted'
84
- }`}
85
- >
86
- <Play size={12} />
87
- Run
88
- </button>
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
+ )}
89
86
  </div>
90
87
 
91
- <h1 className="text-base font-semibold text-foreground truncate flex-1">
92
- {editWorkflow.title || 'New Workflow'}
93
- {dirty && <span className="text-[var(--amber)] ml-1" title="Unsaved changes">*</span>}
94
- </h1>
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>
95
103
  </div>
96
104
 
97
105
  {/* Parse error banner */}
98
106
  {hasParseErrors && mode === 'edit' && (
99
- <div className="mb-4 px-3 py-2.5 rounded-lg bg-[var(--error)]/10 border border-[var(--error)]/30">
100
- <div className="flex items-center gap-2 mb-1">
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">
101
109
  <AlertCircle size={13} className="text-[var(--error)] shrink-0" />
102
- <span className="text-xs font-medium text-[var(--error)]">File has parse issues</span>
110
+ <span className="text-xs font-medium text-[var(--error)]">Parse issues found</span>
103
111
  </div>
104
- <ul className="text-2xs text-[var(--error)]/80 pl-5 list-disc">
112
+ <ul className="text-2xs text-[var(--error)]/70 pl-5 list-disc space-y-0.5">
105
113
  {parsed.errors.slice(0, 3).map((e, i) => <li key={i}>{e}</li>)}
106
114
  </ul>
107
115
  </div>
@@ -109,17 +117,9 @@ export function WorkflowYamlRenderer({ filePath, content }: RendererContext) {
109
117
 
110
118
  {/* Content */}
111
119
  {mode === 'edit' ? (
112
- <WorkflowEditor
113
- workflow={editWorkflow}
114
- filePath={filePath}
115
- onChange={handleEditorChange}
116
- onSaved={handleSaved}
117
- />
120
+ <WorkflowEditor workflow={editWorkflow} filePath={filePath} onChange={handleEditorChange} onSaved={handleSaved} />
118
121
  ) : (
119
- <WorkflowRunner
120
- workflow={runWorkflow}
121
- filePath={filePath}
122
- />
122
+ <WorkflowRunner workflow={runWorkflow} filePath={filePath} />
123
123
  )}
124
124
  </div>
125
125
  );
@@ -1,4 +1,4 @@
1
- // Workflow step execution logic — fetches skills, constructs prompts, streams AI responses
1
+ // Workflow step execution logic — fetches skills, resolves agents, constructs prompts, streams AI responses
2
2
 
3
3
  import type { WorkflowYaml, WorkflowStepRuntime } from './types';
4
4
 
@@ -41,17 +41,45 @@ async function fetchSkillContent(name: string, signal: AbortSignal): Promise<str
41
41
 
42
42
  /**
43
43
  * Collect all unique skill names referenced by a step (step-level + workflow-level).
44
- * Step-level skill takes priority; workflow-level skills provide additional context.
44
+ * Handles both legacy `skill` (singular) and new `skills` (array) fields.
45
45
  */
46
46
  function collectSkillNames(step: WorkflowStepRuntime, workflow: WorkflowYaml): string[] {
47
47
  const names: string[] = [];
48
- if (step.skill) names.push(step.skill);
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
49
57
  for (const s of workflow.skills ?? []) {
50
58
  if (!names.includes(s)) names.push(s);
51
59
  }
52
60
  return names;
53
61
  }
54
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
+
55
83
  // ─── Prompt Construction ──────────────────────────────────────────────────
56
84
 
57
85
  function buildPrompt(
@@ -61,12 +89,15 @@ function buildPrompt(
61
89
  ): string {
62
90
  const allStepsSummary = workflow.steps.map((s, i) => `${i + 1}. ${s.name}`).join('\n');
63
91
 
92
+ // Determine primary skill name for labeling
93
+ const primarySkill = step.skills?.length ? step.skills[0] : step.skill;
94
+
64
95
  // Build skill context block
65
96
  let skillBlock = '';
66
97
  if (skillContents.size > 0) {
67
98
  const sections: string[] = [];
68
99
  for (const [name, content] of skillContents) {
69
- const isPrimary = name === step.skill;
100
+ const isPrimary = name === primarySkill;
70
101
  sections.push(
71
102
  `### ${isPrimary ? '[Primary] ' : ''}Skill: ${name}\n\n${content}`
72
103
  );
@@ -95,8 +126,9 @@ Be specific and actionable. Format in Markdown.`;
95
126
  /**
96
127
  * Execute a workflow step with AI:
97
128
  * 1. Fetch referenced skill(s) content
98
- * 2. Build prompt with injected skill context
99
- * 3. Stream response from /api/ask
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)
100
132
  */
101
133
  export async function runStepWithAI(
102
134
  step: WorkflowStepRuntime,
@@ -121,17 +153,30 @@ export async function runStepWithAI(
121
153
  }
122
154
  }
123
155
 
124
- // 2. Build prompt
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
125
165
  const prompt = buildPrompt(step, workflow, skillContents);
126
166
 
127
- // 3. Stream from /api/ask
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
+
128
176
  const res = await fetch('/api/ask', {
129
177
  method: 'POST',
130
178
  headers: { 'Content-Type': 'application/json' },
131
- body: JSON.stringify({
132
- messages: [{ role: 'user', content: prompt }],
133
- currentFile: filePath,
134
- }),
179
+ body: JSON.stringify(body),
135
180
  signal,
136
181
  });
137
182
 
@@ -155,6 +200,13 @@ export async function runStepWithAI(
155
200
  if (event.type === 'text_delta' && typeof event.delta === 'string') {
156
201
  acc += event.delta;
157
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);
158
210
  }
159
211
  } catch {
160
212
  // Not valid JSON — try legacy Vercel AI SDK format: 0:"..."
@@ -32,42 +32,94 @@ function Dropdown({ trigger, children, open, onClose }: {
32
32
 
33
33
  // ─── Agent Selector ───────────────────────────────────────────────────────
34
34
 
35
- const KNOWN_AGENTS = ['cursor', 'claude-code', 'mindos', 'gemini'];
35
+ interface AcpAgentInfo { id: string; name: string }
36
+
37
+ /** Cache ACP agents globally */
38
+ let _agentsCache: AcpAgentInfo[] | null = null;
39
+ let _agentsFetching = false;
40
+ const _agentsListeners: Array<(agents: AcpAgentInfo[]) => void> = [];
41
+
42
+ function fetchAgentsOnce(cb: (agents: AcpAgentInfo[]) => void) {
43
+ if (_agentsCache) { cb(_agentsCache); return; }
44
+ _agentsListeners.push(cb);
45
+ if (_agentsFetching) return;
46
+ _agentsFetching = true;
47
+ fetch('/api/acp/registry').then(r => r.json()).then(data => {
48
+ const agents: AcpAgentInfo[] = (data.registry?.agents ?? []).map((a: { id: string; name?: string }) => ({
49
+ id: a.id, name: a.name || a.id,
50
+ }));
51
+ _agentsCache = agents;
52
+ _agentsListeners.forEach(fn => fn(agents));
53
+ _agentsListeners.length = 0;
54
+ }).catch(() => {
55
+ _agentsCache = [];
56
+ _agentsListeners.forEach(fn => fn([]));
57
+ _agentsListeners.length = 0;
58
+ });
59
+ }
36
60
 
37
61
  export function AgentSelector({ value, onChange }: { value?: string; onChange: (v: string | undefined) => void }) {
38
62
  const [open, setOpen] = useState(false);
39
63
  const [custom, setCustom] = useState('');
64
+ const [agents, setAgents] = useState<AcpAgentInfo[]>(_agentsCache ?? []);
65
+ const [query, setQuery] = useState('');
40
66
 
41
- const select = (v: string | undefined) => { onChange(v); setOpen(false); };
67
+ useEffect(() => {
68
+ fetchAgentsOnce(setAgents);
69
+ }, []);
70
+
71
+ const filtered = query
72
+ ? agents.filter(a => a.id.toLowerCase().includes(query.toLowerCase()) || a.name.toLowerCase().includes(query.toLowerCase()))
73
+ : agents;
74
+
75
+ const select = (v: string | undefined) => { onChange(v); setOpen(false); setQuery(''); };
76
+
77
+ // Find display name for current value
78
+ const displayName = value ? (agents.find(a => a.id === value)?.name || value) : undefined;
42
79
 
43
80
  return (
44
81
  <Dropdown
45
82
  open={open}
46
- onClose={() => setOpen(false)}
83
+ onClose={() => { setOpen(false); setQuery(''); }}
47
84
  trigger={
48
85
  <button type="button" onClick={() => setOpen(v => !v)}
49
86
  className="w-full flex items-center justify-between px-2.5 py-1.5 text-xs rounded-md border border-border bg-background text-foreground hover:bg-muted transition-colors">
50
- <span className={value ? 'text-foreground' : 'text-muted-foreground'}>
51
- {value || 'Select agent'}
87
+ <span className={value ? 'text-foreground truncate' : 'text-muted-foreground'}>
88
+ {displayName || 'MindOS'}
52
89
  </span>
53
- <ChevronDown size={12} className="text-muted-foreground" />
90
+ <ChevronDown size={12} className="text-muted-foreground shrink-0" />
54
91
  </button>
55
92
  }
56
93
  >
94
+ {/* Search */}
95
+ <div className="sticky top-0 bg-card border-b border-border px-2.5 py-1.5">
96
+ <div className="flex items-center gap-1.5 px-2 py-1 rounded border border-border bg-background">
97
+ <Search size={11} className="text-muted-foreground shrink-0" />
98
+ <input type="text" value={query} onChange={e => setQuery(e.target.value)} autoFocus
99
+ placeholder="Search agents..."
100
+ className="flex-1 text-xs bg-transparent text-foreground placeholder:text-muted-foreground focus:outline-none"
101
+ />
102
+ </div>
103
+ </div>
104
+
57
105
  <button onClick={() => select(undefined)}
58
- className={`w-full text-left px-3 py-1.5 text-xs hover:bg-muted transition-colors ${!value ? 'text-[var(--amber)] font-medium' : 'text-muted-foreground'}`}>
59
- (none)
106
+ className={`w-full text-left px-3 py-1.5 text-xs hover:bg-muted transition-colors ${!value ? 'text-[var(--amber)] font-medium' : 'text-foreground'}`}>
107
+ MindOS <span className="text-muted-foreground/50 ml-1">(default)</span>
60
108
  </button>
61
- {KNOWN_AGENTS.map(a => (
62
- <button key={a} onClick={() => select(a)}
63
- className={`w-full text-left px-3 py-1.5 text-xs hover:bg-muted transition-colors ${value === a ? 'text-[var(--amber)] font-medium' : 'text-foreground'}`}>
64
- {a}
109
+ {filtered.slice(0, 30).map(a => (
110
+ <button key={a.id} onClick={() => select(a.id)}
111
+ className={`w-full text-left px-3 py-1.5 text-xs hover:bg-muted transition-colors ${value === a.id ? 'text-[var(--amber)] font-medium' : 'text-foreground'}`}>
112
+ {a.name} {a.name !== a.id && <span className="text-muted-foreground/50 ml-1">({a.id})</span>}
65
113
  </button>
66
114
  ))}
115
+ {agents.length === 0 && <div className="px-3 py-2 text-xs text-muted-foreground">Loading...</div>}
116
+ {agents.length > 0 && filtered.length === 0 && !query && (
117
+ <div className="px-3 py-2 text-xs text-muted-foreground">No ACP agents found</div>
118
+ )}
67
119
  <div className="border-t border-border px-2.5 py-1.5">
68
120
  <input type="text" value={custom} onChange={e => setCustom(e.target.value)}
69
121
  onKeyDown={e => { if (e.key === 'Enter' && custom.trim()) { select(custom.trim()); setCustom(''); } }}
70
- placeholder="Custom agent..."
122
+ placeholder="Custom agent ID..."
71
123
  className="w-full px-2 py-1 text-xs rounded border border-border bg-background text-foreground placeholder:text-muted-foreground focus:outline-none focus-visible:ring-1 focus-visible:ring-ring"
72
124
  />
73
125
  </div>