@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.
- 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 +72 -72
- package/app/components/renderers/workflow-yaml/WorkflowRunner.tsx +175 -119
- 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 +65 -13
- 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 { 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-
|
|
62
|
-
{/*
|
|
63
|
-
<div className="
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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-
|
|
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)]">
|
|
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)]/
|
|
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
|
-
*
|
|
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
|
-
|
|
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 ===
|
|
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.
|
|
99
|
-
* 3.
|
|
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.
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
{
|
|
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-
|
|
59
|
-
(
|
|
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
|
-
{
|
|
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>
|