@geminilight/mindos 0.5.8 → 0.5.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -9
- package/README_zh.md +8 -9
- package/app/app/api/mcp/agents/route.ts +7 -0
- package/app/app/api/mcp/install-skill/route.ts +6 -0
- package/app/app/api/setup/check-port/route.ts +27 -3
- package/app/app/api/setup/route.ts +2 -9
- package/app/app/globals.css +18 -2
- package/app/app/login/page.tsx +1 -1
- package/app/app/view/[...path]/ViewPageClient.tsx +9 -9
- package/app/components/AskModal.tsx +1 -1
- package/app/components/FileTree.tsx +5 -5
- package/app/components/HomeContent.tsx +1 -1
- package/app/components/SetupWizard.tsx +283 -141
- package/app/components/SyncStatusBar.tsx +3 -3
- package/app/components/ask/MessageList.tsx +2 -2
- package/app/components/ask/SessionHistory.tsx +1 -1
- package/app/components/renderers/agent-inspector/AgentInspectorRenderer.tsx +5 -5
- package/app/components/renderers/config/ConfigRenderer.tsx +3 -3
- package/app/components/renderers/csv/types.ts +1 -1
- package/app/components/renderers/diff/DiffRenderer.tsx +9 -9
- package/app/components/renderers/timeline/TimelineRenderer.tsx +1 -1
- package/app/components/renderers/workflow/WorkflowRenderer.tsx +2 -2
- package/app/components/settings/McpTab.tsx +66 -24
- package/app/components/settings/Primitives.tsx +3 -3
- package/app/components/settings/SyncTab.tsx +5 -5
- package/app/lib/i18n.ts +48 -4
- package/app/lib/mcp-agents.ts +81 -0
- package/bin/lib/gateway.js +44 -4
- package/bin/lib/mcp-agents.js +81 -0
- package/bin/lib/mcp-install.js +34 -4
- package/package.json +3 -1
- package/scripts/setup.js +43 -6
- package/app/public/landing/index.html +0 -353
- package/app/public/landing/style.css +0 -216
|
@@ -22,8 +22,8 @@ export function getStatusLevel(status: SyncStatus | null, syncing: boolean): Sta
|
|
|
22
22
|
export const DOT_COLORS: Record<StatusLevel, string> = {
|
|
23
23
|
synced: 'bg-green-500',
|
|
24
24
|
unpushed: 'bg-yellow-500',
|
|
25
|
-
conflicts: 'bg-
|
|
26
|
-
error: 'bg-
|
|
25
|
+
conflicts: 'bg-error', // #6 — conflicts more prominent than unpushed
|
|
26
|
+
error: 'bg-error',
|
|
27
27
|
off: 'bg-muted-foreground/40',
|
|
28
28
|
syncing: 'bg-blue-500',
|
|
29
29
|
};
|
|
@@ -232,7 +232,7 @@ export default function SyncStatusBar({ collapsed, onOpenSyncSettings }: SyncSta
|
|
|
232
232
|
<div className="flex items-center gap-1 shrink-0 ml-2">
|
|
233
233
|
{/* #2 — sync result flash */}
|
|
234
234
|
{(syncResult === 'success' || toast) && <CheckCircle2 size={12} className="text-green-500 animate-in fade-in duration-200" />}
|
|
235
|
-
{syncResult === 'error' && <XCircle size={12} className="text-
|
|
235
|
+
{syncResult === 'error' && <XCircle size={12} className="text-error animate-in fade-in duration-200" />}
|
|
236
236
|
<button
|
|
237
237
|
onClick={handleSyncNow}
|
|
238
238
|
disabled={syncing}
|
|
@@ -94,8 +94,8 @@ export default function MessageList({
|
|
|
94
94
|
{m.content}
|
|
95
95
|
</div>
|
|
96
96
|
) : m.content.startsWith('__error__') ? (
|
|
97
|
-
<div className="max-w-[85%] px-3 py-2.5 rounded-xl rounded-bl-sm border border-
|
|
98
|
-
<div className="flex items-start gap-2 text-
|
|
97
|
+
<div className="max-w-[85%] px-3 py-2.5 rounded-xl rounded-bl-sm border border-error/20 bg-error/8 text-sm">
|
|
98
|
+
<div className="flex items-start gap-2 text-error">
|
|
99
99
|
<AlertCircle size={14} className="shrink-0 mt-0.5" />
|
|
100
100
|
<span className="leading-relaxed">{m.content.slice(9)}</span>
|
|
101
101
|
</div>
|
|
@@ -36,7 +36,7 @@ export default function SessionHistory({ sessions, activeSessionId, onLoad, onDe
|
|
|
36
36
|
<button
|
|
37
37
|
type="button"
|
|
38
38
|
onClick={() => onDelete(s.id)}
|
|
39
|
-
className="p-1 rounded text-muted-foreground hover:text-
|
|
39
|
+
className="p-1 rounded text-muted-foreground hover:text-error hover:bg-muted"
|
|
40
40
|
title="Delete session"
|
|
41
41
|
>
|
|
42
42
|
<Trash2 size={12} />
|
|
@@ -59,8 +59,8 @@ function opKind(tool: string): OpKind {
|
|
|
59
59
|
const KIND_STYLE: Record<OpKind, { bg: string; text: string; border: string }> = {
|
|
60
60
|
read: { bg: 'rgba(138,180,216,0.10)', text: '#8ab4d8', border: 'rgba(138,180,216,0.25)' },
|
|
61
61
|
write: { bg: 'rgba(200,135,58,0.10)', text: 'var(--amber)', border: 'rgba(200,135,58,0.25)' },
|
|
62
|
-
create: { bg: 'rgba(122,173,128,0.10)', text: '
|
|
63
|
-
delete: { bg: 'rgba(200,80,80,0.10)', text: '
|
|
62
|
+
create: { bg: 'rgba(122,173,128,0.10)', text: 'var(--success)', border: 'rgba(122,173,128,0.25)' },
|
|
63
|
+
delete: { bg: 'rgba(200,80,80,0.10)', text: 'var(--error)', border: 'rgba(200,80,80,0.25)' },
|
|
64
64
|
search: { bg: 'rgba(200,160,216,0.10)', text: '#c8a0d8', border: 'rgba(200,160,216,0.25)' },
|
|
65
65
|
other: { bg: 'var(--muted)', text: 'var(--muted-foreground)', border: 'var(--border)' },
|
|
66
66
|
};
|
|
@@ -159,8 +159,8 @@ function OpCard({ op }: { op: AgentOp }) {
|
|
|
159
159
|
<div style={{ marginLeft: 'auto', display: 'flex', alignItems: 'center', gap: 8, flexShrink: 0 }}>
|
|
160
160
|
{/* result */}
|
|
161
161
|
{op.result === 'ok'
|
|
162
|
-
? <CheckCircle2 size={13} style={{ color: '
|
|
163
|
-
: <AlertCircle size={13} style={{ color: '
|
|
162
|
+
? <CheckCircle2 size={13} style={{ color: 'var(--success)' }} />
|
|
163
|
+
: <AlertCircle size={13} style={{ color: 'var(--error)' }} />
|
|
164
164
|
}
|
|
165
165
|
{/* timestamp */}
|
|
166
166
|
<span className="font-display" style={{ fontSize: '0.68rem', color: 'var(--muted-foreground)', opacity: 0.6 }} title={formatTs(op.ts)}>
|
|
@@ -191,7 +191,7 @@ function OpCard({ op }: { op: AgentOp }) {
|
|
|
191
191
|
{op.message && (
|
|
192
192
|
<div className="font-display" style={{ marginTop: 6, padding: '5px 9px', borderRadius: 5, fontSize: '0.72rem',
|
|
193
193
|
background: op.result === 'error' ? 'rgba(200,80,80,0.08)' : 'rgba(122,173,128,0.08)',
|
|
194
|
-
color: op.result === 'error' ? '
|
|
194
|
+
color: op.result === 'error' ? 'var(--error)' : 'var(--success)',
|
|
195
195
|
border: `1px solid ${op.result === 'error' ? 'rgba(200,80,80,0.2)' : 'rgba(122,173,128,0.2)'}`,
|
|
196
196
|
}}>
|
|
197
197
|
{op.message}
|
|
@@ -98,7 +98,7 @@ export function ConfigRenderer({ content, saveAction }: RendererContext) {
|
|
|
98
98
|
|
|
99
99
|
if (!parsed || !data) {
|
|
100
100
|
return (
|
|
101
|
-
<div className="rounded-xl border border-border p-4 text-sm text-
|
|
101
|
+
<div className="rounded-xl border border-border p-4 text-sm text-error">
|
|
102
102
|
CONFIG.json parse failed. Please check JSON format.
|
|
103
103
|
</div>
|
|
104
104
|
);
|
|
@@ -110,11 +110,11 @@ export function ConfigRenderer({ content, saveAction }: RendererContext) {
|
|
|
110
110
|
<div className="text-xs text-muted-foreground">CONFIG Control Panel</div>
|
|
111
111
|
<div className="text-xs flex items-center gap-2">
|
|
112
112
|
{saving && <span className="inline-flex items-center gap-1 text-muted-foreground"><Loader2 size={12} className="animate-spin" />Saving</span>}
|
|
113
|
-
{!saving && saved && <span className="inline-flex items-center gap-1" style={{ color: '
|
|
113
|
+
{!saving && saved && <span className="inline-flex items-center gap-1" style={{ color: 'var(--success)' }}><Check size={12} />Saved</span>}
|
|
114
114
|
</div>
|
|
115
115
|
</div>
|
|
116
116
|
|
|
117
|
-
{error && <div className="rounded-lg border border-
|
|
117
|
+
{error && <div className="rounded-lg border border-error/30 bg-error/10 px-3 py-2 text-xs text-error">{error}</div>}
|
|
118
118
|
|
|
119
119
|
{sections.map((section) => (
|
|
120
120
|
<div key={section.id} className="rounded-xl border border-border bg-card p-4">
|
|
@@ -73,7 +73,7 @@ export function serializeCSV(headers: string[], rows: string[][]) {
|
|
|
73
73
|
|
|
74
74
|
const TAG_COLORS = [
|
|
75
75
|
{ bg: 'rgba(200,135,58,0.12)', text: 'var(--amber)' },
|
|
76
|
-
{ bg: 'rgba(122,173,128,0.12)', text: '
|
|
76
|
+
{ bg: 'rgba(122,173,128,0.12)', text: 'var(--success)' },
|
|
77
77
|
{ bg: 'rgba(138,180,216,0.12)', text: '#8ab4d8' },
|
|
78
78
|
{ bg: 'rgba(200,160,216,0.12)', text: '#c8a0d8' },
|
|
79
79
|
{ bg: 'rgba(200,96,96,0.12)', text: '#c86060' },
|
|
@@ -191,8 +191,8 @@ function DiffCard({ entry, saveAction, fullContent }: {
|
|
|
191
191
|
</span>
|
|
192
192
|
|
|
193
193
|
{/* diff stats */}
|
|
194
|
-
<span className="font-display" style={{ fontSize: '0.7rem', color: '
|
|
195
|
-
<span className="font-display" style={{ fontSize: '0.7rem', color: '
|
|
194
|
+
<span className="font-display" style={{ fontSize: '0.7rem', color: 'var(--success)', flexShrink: 0 }}>+{added}</span>
|
|
195
|
+
<span className="font-display" style={{ fontSize: '0.7rem', color: 'var(--error)', flexShrink: 0 }}>−{removed}</span>
|
|
196
196
|
|
|
197
197
|
{/* tool badge */}
|
|
198
198
|
<span className="font-display" style={{ fontSize: '0.65rem', padding: '1px 7px', borderRadius: 999, background: 'var(--muted)', color: 'var(--muted-foreground)', flexShrink: 0 }}>
|
|
@@ -210,20 +210,20 @@ function DiffCard({ entry, saveAction, fullContent }: {
|
|
|
210
210
|
<button
|
|
211
211
|
onClick={handleApprove}
|
|
212
212
|
title="Approve this change"
|
|
213
|
-
style={{ background: 'none', border: 'none', cursor: 'pointer', padding: 2, color: '
|
|
213
|
+
style={{ background: 'none', border: 'none', cursor: 'pointer', padding: 2, color: 'var(--success)', display: 'flex', alignItems: 'center' }}
|
|
214
214
|
>
|
|
215
215
|
<CheckCircle2 size={15} />
|
|
216
216
|
</button>
|
|
217
217
|
<button
|
|
218
218
|
onClick={handleReject}
|
|
219
219
|
title="Reject & revert this change"
|
|
220
|
-
style={{ background: 'none', border: 'none', cursor: 'pointer', padding: 2, color: '
|
|
220
|
+
style={{ background: 'none', border: 'none', cursor: 'pointer', padding: 2, color: 'var(--error)', display: 'flex', alignItems: 'center' }}
|
|
221
221
|
>
|
|
222
222
|
<XCircle size={15} />
|
|
223
223
|
</button>
|
|
224
224
|
</>
|
|
225
225
|
) : (
|
|
226
|
-
<span className="font-display" style={{ fontSize: '0.68rem', color: approved ? '
|
|
226
|
+
<span className="font-display" style={{ fontSize: '0.68rem', color: approved ? 'var(--success)' : 'var(--error)' }}>
|
|
227
227
|
{approved ? '✓ approved' : '✕ reverted'}
|
|
228
228
|
</span>
|
|
229
229
|
)}
|
|
@@ -252,8 +252,8 @@ function DiffCard({ entry, saveAction, fullContent }: {
|
|
|
252
252
|
line.type === 'delete' ? 'rgba(200,80,80,0.10)' :
|
|
253
253
|
'transparent';
|
|
254
254
|
const color =
|
|
255
|
-
line.type === 'insert' ? '
|
|
256
|
-
line.type === 'delete' ? '
|
|
255
|
+
line.type === 'insert' ? 'var(--success)' :
|
|
256
|
+
line.type === 'delete' ? 'var(--error)' :
|
|
257
257
|
'var(--muted-foreground)';
|
|
258
258
|
const prefix =
|
|
259
259
|
line.type === 'insert' ? '+' :
|
|
@@ -299,8 +299,8 @@ export function DiffRenderer({ content, saveAction }: RendererContext) {
|
|
|
299
299
|
{/* stats bar */}
|
|
300
300
|
<div className="font-display" style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: '1.2rem', fontSize: 11, color: 'var(--muted-foreground)' }}>
|
|
301
301
|
<span>{entries.length} change{entries.length !== 1 ? 's' : ''}</span>
|
|
302
|
-
<span style={{ color: '
|
|
303
|
-
<span style={{ color: '
|
|
302
|
+
<span style={{ color: 'var(--success)' }}>+{totalAdded}</span>
|
|
303
|
+
<span style={{ color: 'var(--error)' }}>−{totalRemoved}</span>
|
|
304
304
|
</div>
|
|
305
305
|
|
|
306
306
|
{entries.map((entry, i) => (
|
|
@@ -110,7 +110,7 @@ function renderBody(body: string): string {
|
|
|
110
110
|
|
|
111
111
|
const TAG_PALETTE = [
|
|
112
112
|
{ bg: 'rgba(200,135,58,0.12)', text: 'var(--amber)' },
|
|
113
|
-
{ bg: 'rgba(122,173,128,0.12)', text: '
|
|
113
|
+
{ bg: 'rgba(122,173,128,0.12)', text: 'var(--success)' },
|
|
114
114
|
{ bg: 'rgba(138,180,216,0.12)', text: '#8ab4d8' },
|
|
115
115
|
{ bg: 'rgba(200,160,216,0.12)', text: '#c8a0d8' },
|
|
116
116
|
];
|
|
@@ -92,9 +92,9 @@ function renderBody(body: string): string {
|
|
|
92
92
|
function StatusIcon({ status }: { status: StepStatus }) {
|
|
93
93
|
if (status === 'pending') return <Circle size={15} style={{ color: 'var(--border)' }} />;
|
|
94
94
|
if (status === 'running') return <Loader2 size={15} style={{ color: 'var(--amber)', animation: 'spin 1s linear infinite' }} />;
|
|
95
|
-
if (status === 'done') return <CheckCircle2 size={15} style={{ color: '
|
|
95
|
+
if (status === 'done') return <CheckCircle2 size={15} style={{ color: 'var(--success)' }} />;
|
|
96
96
|
if (status === 'skipped') return <SkipForward size={15} style={{ color: 'var(--muted-foreground)', opacity: .5 }} />;
|
|
97
|
-
return <AlertCircle size={15} style={{ color: '
|
|
97
|
+
return <AlertCircle size={15} style={{ color: 'var(--error)' }} />;
|
|
98
98
|
}
|
|
99
99
|
|
|
100
100
|
const STATUS_BORDER: Record<StepStatus, string> = {
|
|
@@ -5,7 +5,6 @@ import {
|
|
|
5
5
|
Plug, CheckCircle2, AlertCircle, Loader2, Copy, Check,
|
|
6
6
|
ChevronDown, ChevronRight, Trash2, Plus, X,
|
|
7
7
|
} from 'lucide-react';
|
|
8
|
-
import { SectionLabel } from './Primitives';
|
|
9
8
|
import { apiFetch } from '@/lib/api';
|
|
10
9
|
|
|
11
10
|
/* ── Types ─────────────────────────────────────────────────────── */
|
|
@@ -206,9 +205,7 @@ function AgentInstall({ agents, t, onRefresh }: { agents: AgentInfo[]; t: any; o
|
|
|
206
205
|
}));
|
|
207
206
|
|
|
208
207
|
return (
|
|
209
|
-
<div className="space-y-3">
|
|
210
|
-
<SectionLabel>{m?.agentsTitle ?? 'Agent Configuration'}</SectionLabel>
|
|
211
|
-
|
|
208
|
+
<div className="space-y-3 pt-2">
|
|
212
209
|
{/* Agent list */}
|
|
213
210
|
<div className="space-y-1">
|
|
214
211
|
{agents.map(agent => (
|
|
@@ -251,6 +248,23 @@ function AgentInstall({ agents, t, onRefresh }: { agents: AgentInfo[]; t: any; o
|
|
|
251
248
|
))}
|
|
252
249
|
</div>
|
|
253
250
|
|
|
251
|
+
{/* Select detected / Clear buttons */}
|
|
252
|
+
<div className="flex gap-2 text-xs pt-1">
|
|
253
|
+
<button type="button"
|
|
254
|
+
onClick={() => setSelected(new Set(
|
|
255
|
+
agents.filter(a => !a.installed && a.present).map(a => a.key)
|
|
256
|
+
))}
|
|
257
|
+
className="px-2.5 py-1 rounded-md border transition-colors hover:bg-muted/50"
|
|
258
|
+
style={{ borderColor: 'var(--amber)', color: 'var(--amber)' }}>
|
|
259
|
+
{m?.selectDetected ?? 'Select Detected'}
|
|
260
|
+
</button>
|
|
261
|
+
<button type="button"
|
|
262
|
+
onClick={() => setSelected(new Set())}
|
|
263
|
+
className="px-2.5 py-1 rounded-md border border-border text-muted-foreground transition-colors hover:bg-muted/50 hover:text-foreground">
|
|
264
|
+
{m?.clearSelection ?? 'Clear'}
|
|
265
|
+
</button>
|
|
266
|
+
</div>
|
|
267
|
+
|
|
254
268
|
{/* Transport selector */}
|
|
255
269
|
<div className="flex items-center gap-4 text-xs pt-1">
|
|
256
270
|
<label className="flex items-center gap-1.5 cursor-pointer">
|
|
@@ -294,7 +308,7 @@ function AgentInstall({ agents, t, onRefresh }: { agents: AgentInfo[]; t: any; o
|
|
|
294
308
|
type="text"
|
|
295
309
|
value={httpUrl}
|
|
296
310
|
onChange={e => setHttpUrl(e.target.value)}
|
|
297
|
-
className="w-full px-2.5 py-1.5 text-xs rounded-md border border-border bg-background font-mono text-foreground outline-none focus:ring-1 focus:ring-ring"
|
|
311
|
+
className="w-full px-2.5 py-1.5 text-xs rounded-md border border-border bg-background font-mono text-foreground outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
|
298
312
|
/>
|
|
299
313
|
</div>
|
|
300
314
|
<div className="space-y-1">
|
|
@@ -304,7 +318,7 @@ function AgentInstall({ agents, t, onRefresh }: { agents: AgentInfo[]; t: any; o
|
|
|
304
318
|
value={httpToken}
|
|
305
319
|
onChange={e => setHttpToken(e.target.value)}
|
|
306
320
|
placeholder="Bearer token"
|
|
307
|
-
className="w-full px-2.5 py-1.5 text-xs rounded-md border border-border bg-background font-mono text-foreground outline-none focus:ring-1 focus:ring-ring"
|
|
321
|
+
className="w-full px-2.5 py-1.5 text-xs rounded-md border border-border bg-background font-mono text-foreground outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
|
308
322
|
/>
|
|
309
323
|
</div>
|
|
310
324
|
</div>
|
|
@@ -414,9 +428,7 @@ function SkillsSection({ t }: { t: any }) {
|
|
|
414
428
|
}
|
|
415
429
|
|
|
416
430
|
return (
|
|
417
|
-
<div className="space-y-3">
|
|
418
|
-
<SectionLabel>{m?.skillsTitle ?? 'Skills'}</SectionLabel>
|
|
419
|
-
|
|
431
|
+
<div className="space-y-3 pt-2">
|
|
420
432
|
{/* Skill language switcher */}
|
|
421
433
|
{(() => {
|
|
422
434
|
const mindosEnabled = skills.find(s => s.name === 'mindos')?.enabled ?? true;
|
|
@@ -520,7 +532,7 @@ function SkillsSection({ t }: { t: any }) {
|
|
|
520
532
|
value={newName}
|
|
521
533
|
onChange={e => setNewName(e.target.value.replace(/[^a-z0-9-]/g, ''))}
|
|
522
534
|
placeholder="my-skill"
|
|
523
|
-
className="w-full px-2.5 py-1.5 text-xs rounded-md border border-border bg-background font-mono text-foreground outline-none focus:ring-1 focus:ring-ring"
|
|
535
|
+
className="w-full px-2.5 py-1.5 text-xs rounded-md border border-border bg-background font-mono text-foreground outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
|
524
536
|
/>
|
|
525
537
|
</div>
|
|
526
538
|
<div className="space-y-1">
|
|
@@ -530,7 +542,7 @@ function SkillsSection({ t }: { t: any }) {
|
|
|
530
542
|
value={newDesc}
|
|
531
543
|
onChange={e => setNewDesc(e.target.value)}
|
|
532
544
|
placeholder="What does this skill do?"
|
|
533
|
-
className="w-full px-2.5 py-1.5 text-xs rounded-md border border-border bg-background text-foreground outline-none focus:ring-1 focus:ring-ring"
|
|
545
|
+
className="w-full px-2.5 py-1.5 text-xs rounded-md border border-border bg-background text-foreground outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
|
534
546
|
/>
|
|
535
547
|
</div>
|
|
536
548
|
<div className="space-y-1">
|
|
@@ -540,7 +552,7 @@ function SkillsSection({ t }: { t: any }) {
|
|
|
540
552
|
onChange={e => setNewContent(e.target.value)}
|
|
541
553
|
rows={6}
|
|
542
554
|
placeholder="Skill instructions (markdown)..."
|
|
543
|
-
className="w-full px-2.5 py-1.5 text-xs rounded-md border border-border bg-background text-foreground outline-none focus:ring-1 focus:ring-ring resize-y font-mono"
|
|
555
|
+
className="w-full px-2.5 py-1.5 text-xs rounded-md border border-border bg-background text-foreground outline-none focus-visible:ring-1 focus-visible:ring-ring resize-y font-mono"
|
|
544
556
|
/>
|
|
545
557
|
</div>
|
|
546
558
|
{error && (
|
|
@@ -586,6 +598,8 @@ export function McpTab({ t }: McpTabProps) {
|
|
|
586
598
|
const [mcpStatus, setMcpStatus] = useState<McpStatus | null>(null);
|
|
587
599
|
const [agents, setAgents] = useState<AgentInfo[]>([]);
|
|
588
600
|
const [loading, setLoading] = useState(true);
|
|
601
|
+
const [showAgents, setShowAgents] = useState(false);
|
|
602
|
+
const [showSkills, setShowSkills] = useState(false);
|
|
589
603
|
|
|
590
604
|
const fetchAll = useCallback(async () => {
|
|
591
605
|
try {
|
|
@@ -609,22 +623,50 @@ export function McpTab({ t }: McpTabProps) {
|
|
|
609
623
|
);
|
|
610
624
|
}
|
|
611
625
|
|
|
626
|
+
const m = t.settings?.mcp;
|
|
627
|
+
|
|
612
628
|
return (
|
|
613
629
|
<div className="space-y-6">
|
|
614
|
-
{/* MCP Server Status */}
|
|
615
|
-
<
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
<div className="border-t border-border" />
|
|
619
|
-
|
|
620
|
-
{/* Agent Install */}
|
|
621
|
-
<AgentInstall agents={agents} t={t} onRefresh={fetchAll} />
|
|
630
|
+
{/* MCP Server Status — prominent card */}
|
|
631
|
+
<div className="rounded-xl border p-4" style={{ borderColor: 'var(--border)', background: 'var(--card)' }}>
|
|
632
|
+
<ServerStatus status={mcpStatus} t={t} />
|
|
633
|
+
</div>
|
|
622
634
|
|
|
623
|
-
{/*
|
|
624
|
-
<div className="
|
|
635
|
+
{/* Agent Install — collapsible */}
|
|
636
|
+
<div className="rounded-xl border overflow-hidden" style={{ borderColor: 'var(--border)' }}>
|
|
637
|
+
<button
|
|
638
|
+
type="button"
|
|
639
|
+
onClick={() => setShowAgents(!showAgents)}
|
|
640
|
+
className="w-full flex items-center justify-between px-4 py-3 text-sm font-medium hover:bg-muted/50 transition-colors"
|
|
641
|
+
style={{ color: 'var(--foreground)' }}
|
|
642
|
+
>
|
|
643
|
+
<span>{m?.agentsTitle ?? 'Agent Configuration'}</span>
|
|
644
|
+
<ChevronDown size={14} className={`transition-transform text-muted-foreground ${showAgents ? 'rotate-180' : ''}`} />
|
|
645
|
+
</button>
|
|
646
|
+
{showAgents && (
|
|
647
|
+
<div className="px-4 pb-4 border-t" style={{ borderColor: 'var(--border)' }}>
|
|
648
|
+
<AgentInstall agents={agents} t={t} onRefresh={fetchAll} />
|
|
649
|
+
</div>
|
|
650
|
+
)}
|
|
651
|
+
</div>
|
|
625
652
|
|
|
626
|
-
{/* Skills */}
|
|
627
|
-
<
|
|
653
|
+
{/* Skills — collapsible */}
|
|
654
|
+
<div className="rounded-xl border overflow-hidden" style={{ borderColor: 'var(--border)' }}>
|
|
655
|
+
<button
|
|
656
|
+
type="button"
|
|
657
|
+
onClick={() => setShowSkills(!showSkills)}
|
|
658
|
+
className="w-full flex items-center justify-between px-4 py-3 text-sm font-medium hover:bg-muted/50 transition-colors"
|
|
659
|
+
style={{ color: 'var(--foreground)' }}
|
|
660
|
+
>
|
|
661
|
+
<span>{m?.skillsTitle ?? 'Skills'}</span>
|
|
662
|
+
<ChevronDown size={14} className={`transition-transform text-muted-foreground ${showSkills ? 'rotate-180' : ''}`} />
|
|
663
|
+
</button>
|
|
664
|
+
{showSkills && (
|
|
665
|
+
<div className="px-4 pb-4 border-t" style={{ borderColor: 'var(--border)' }}>
|
|
666
|
+
<SkillsSection t={t} />
|
|
667
|
+
</div>
|
|
668
|
+
)}
|
|
669
|
+
</div>
|
|
628
670
|
</div>
|
|
629
671
|
);
|
|
630
672
|
}
|
|
@@ -18,7 +18,7 @@ export function Input({ className = '', ...props }: React.InputHTMLAttributes<HT
|
|
|
18
18
|
return (
|
|
19
19
|
<input
|
|
20
20
|
{...props}
|
|
21
|
-
className={`w-full px-3 py-2 text-sm bg-background border border-border rounded-lg text-foreground placeholder:text-muted-foreground outline-none focus:ring-1 focus:ring-ring disabled:opacity-50 ${className}`}
|
|
21
|
+
className={`w-full px-3 py-2 text-sm bg-background border border-border rounded-lg text-foreground placeholder:text-muted-foreground outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:opacity-50 ${className}`}
|
|
22
22
|
/>
|
|
23
23
|
);
|
|
24
24
|
}
|
|
@@ -27,7 +27,7 @@ export function Select({ className = '', ...props }: React.SelectHTMLAttributes<
|
|
|
27
27
|
return (
|
|
28
28
|
<select
|
|
29
29
|
{...props}
|
|
30
|
-
className={`w-full px-3 py-2 text-sm bg-background border border-border rounded-lg text-foreground outline-none focus:ring-1 focus:ring-ring disabled:opacity-50 ${className}`}
|
|
30
|
+
className={`w-full px-3 py-2 text-sm bg-background border border-border rounded-lg text-foreground outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:opacity-50 ${className}`}
|
|
31
31
|
/>
|
|
32
32
|
);
|
|
33
33
|
}
|
|
@@ -53,7 +53,7 @@ export function ApiKeyInput({ value, onChange, placeholder, disabled }: {
|
|
|
53
53
|
onChange={e => onChange(e.target.value)}
|
|
54
54
|
placeholder={placeholder ?? 'sk-...'}
|
|
55
55
|
disabled={disabled}
|
|
56
|
-
className="w-full px-3 py-2 text-sm bg-background border border-border rounded-lg text-foreground placeholder:text-muted-foreground outline-none focus:ring-1 focus:ring-ring disabled:opacity-50"
|
|
56
|
+
className="w-full px-3 py-2 text-sm bg-background border border-border rounded-lg text-foreground placeholder:text-muted-foreground outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:opacity-50"
|
|
57
57
|
onFocus={() => { if (isMasked) onChange(''); }}
|
|
58
58
|
/>
|
|
59
59
|
);
|
|
@@ -104,7 +104,7 @@ function SyncEmptyState({ t, onInitComplete }: { t: any; onInitComplete: () => v
|
|
|
104
104
|
value={remoteUrl}
|
|
105
105
|
onChange={e => { setRemoteUrl(e.target.value); setError(''); }}
|
|
106
106
|
placeholder="https://github.com/user/my-mind.git"
|
|
107
|
-
className="w-full px-3 py-2 text-sm rounded-lg border bg-transparent font-mono text-xs transition-colors focus:outline-none focus:ring-1"
|
|
107
|
+
className="w-full px-3 py-2 text-sm rounded-lg border bg-transparent font-mono text-xs transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
|
108
108
|
style={{
|
|
109
109
|
borderColor: remoteUrl.trim() && !isValid ? 'var(--destructive, red)' : 'var(--border)',
|
|
110
110
|
color: 'var(--foreground)',
|
|
@@ -136,7 +136,7 @@ function SyncEmptyState({ t, onInitComplete }: { t: any; onInitComplete: () => v
|
|
|
136
136
|
value={token}
|
|
137
137
|
onChange={e => setToken(e.target.value)}
|
|
138
138
|
placeholder="ghp_xxxxxxxxxxxx"
|
|
139
|
-
className="w-full px-3 py-2 pr-9 text-sm rounded-lg border bg-transparent font-mono text-xs transition-colors focus:outline-none focus:ring-1"
|
|
139
|
+
className="w-full px-3 py-2 pr-9 text-sm rounded-lg border bg-transparent font-mono text-xs transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
|
140
140
|
style={{ borderColor: 'var(--border)', color: 'var(--foreground)' }}
|
|
141
141
|
/>
|
|
142
142
|
<button
|
|
@@ -163,7 +163,7 @@ function SyncEmptyState({ t, onInitComplete }: { t: any; onInitComplete: () => v
|
|
|
163
163
|
value={branch}
|
|
164
164
|
onChange={e => setBranch(e.target.value)}
|
|
165
165
|
placeholder="main"
|
|
166
|
-
className="w-full max-w-[200px] px-3 py-2 text-sm rounded-lg border bg-transparent font-mono text-xs transition-colors focus:outline-none focus:ring-1"
|
|
166
|
+
className="w-full max-w-[200px] px-3 py-2 text-sm rounded-lg border bg-transparent font-mono text-xs transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
|
167
167
|
style={{ borderColor: 'var(--border)', color: 'var(--foreground)' }}
|
|
168
168
|
/>
|
|
169
169
|
</div>
|
|
@@ -184,7 +184,7 @@ function SyncEmptyState({ t, onInitComplete }: { t: any; onInitComplete: () => v
|
|
|
184
184
|
|
|
185
185
|
{/* Error */}
|
|
186
186
|
{error && (
|
|
187
|
-
<div className="flex items-start gap-2 text-xs p-3 rounded-lg" role="alert" aria-live="polite" style={{ background: 'rgba(
|
|
187
|
+
<div className="flex items-start gap-2 text-xs p-3 rounded-lg" role="alert" aria-live="polite" style={{ background: 'rgba(200,80,80,0.1)', color: 'var(--error)' }}>
|
|
188
188
|
<AlertCircle size={13} className="shrink-0 mt-0.5" />
|
|
189
189
|
<span>{error}</span>
|
|
190
190
|
</div>
|
|
@@ -361,7 +361,7 @@ export function SyncTab({ t }: SyncTabProps) {
|
|
|
361
361
|
<div className="space-y-1.5">
|
|
362
362
|
{conflicts.map((c, i) => (
|
|
363
363
|
<div key={i} className="flex items-center gap-2 text-xs group">
|
|
364
|
-
<AlertCircle size={12} className="text-
|
|
364
|
+
<AlertCircle size={12} className="text-error shrink-0" />
|
|
365
365
|
<a
|
|
366
366
|
href={`/view/${encodeURIComponent(c.file)}`}
|
|
367
367
|
className="font-mono truncate hover:text-foreground hover:underline transition-colors"
|
package/app/lib/i18n.ts
CHANGED
|
@@ -228,6 +228,8 @@ export const messages = {
|
|
|
228
228
|
skillLanguage: 'Skill Language',
|
|
229
229
|
skillLangEn: 'English',
|
|
230
230
|
skillLangZh: '中文',
|
|
231
|
+
selectDetected: 'Select Detected',
|
|
232
|
+
clearSelection: 'Clear',
|
|
231
233
|
}, save: 'Save',
|
|
232
234
|
saved: 'Saved',
|
|
233
235
|
saveFailed: 'Save failed',
|
|
@@ -283,7 +285,11 @@ export const messages = {
|
|
|
283
285
|
kbPath: 'Knowledge base path',
|
|
284
286
|
kbPathHint: 'Absolute path to your notes directory.',
|
|
285
287
|
kbPathDefault: '~/MindOS/mind',
|
|
288
|
+
kbPathUseDefault: (path: string) => `Use ${path}`,
|
|
286
289
|
kbPathExists: (n: number) => `Directory already has ${n} file(s) — template will not be applied.`,
|
|
290
|
+
kbPathHasFiles: (n: number) => `This directory already contains ${n} file${n > 1 ? 's' : ''}. You can skip the template or merge (existing files won't be overwritten).`,
|
|
291
|
+
kbTemplateSkip: 'Skip template',
|
|
292
|
+
kbTemplateMerge: 'Choose a template to merge',
|
|
287
293
|
template: 'Starter template',
|
|
288
294
|
templateSkip: 'Skip (directory already has files)',
|
|
289
295
|
// Step 2
|
|
@@ -325,7 +331,16 @@ export const messages = {
|
|
|
325
331
|
agentToolsLoading: 'Loading agents…',
|
|
326
332
|
agentToolsEmpty: 'No supported agents detected.',
|
|
327
333
|
agentNoneSelected: 'No agents selected — you can configure later in Settings → MCP.',
|
|
328
|
-
agentSkipLater: 'Skip —
|
|
334
|
+
agentSkipLater: 'Skip — install agents later in Settings > MCP',
|
|
335
|
+
agentSelectDetected: 'Select detected agents',
|
|
336
|
+
agentNoneDetected: 'No agents detected on your system.',
|
|
337
|
+
agentShowMore: (n: number) => `Show ${n} more agents`,
|
|
338
|
+
agentAdvanced: 'Advanced options',
|
|
339
|
+
agentScopeGlobal: 'Install for all projects',
|
|
340
|
+
agentScopeProject: 'This project only',
|
|
341
|
+
badgeInstalled: 'Installed',
|
|
342
|
+
badgeDetected: 'Detected',
|
|
343
|
+
badgeNotFound: 'Not found',
|
|
329
344
|
agentNotInstalled: 'not installed',
|
|
330
345
|
agentDetected: 'detected',
|
|
331
346
|
agentNotFound: 'not found',
|
|
@@ -338,6 +353,7 @@ export const messages = {
|
|
|
338
353
|
agentUnverified: 'unverified',
|
|
339
354
|
agentVerifyNote: 'stdio agents are verified after restart',
|
|
340
355
|
// Skill auto-install
|
|
356
|
+
skillWhat: 'Skills teach AI agents how to use your knowledge base — like instructions for reading, writing, and organizing your notes.',
|
|
341
357
|
skillAutoHint: (name: string) => `Based on your template, the "${name}" skill will be installed to selected agents.`,
|
|
342
358
|
skillLabel: 'Skill',
|
|
343
359
|
skillInstalling: 'Installing skill…',
|
|
@@ -347,9 +363,15 @@ export const messages = {
|
|
|
347
363
|
aiSkipTitle: 'Skip for now',
|
|
348
364
|
aiSkipDesc: 'You can add an API key later in Settings → AI.',
|
|
349
365
|
// Step 6 — Review
|
|
350
|
-
reviewHint: 'Verify your settings
|
|
366
|
+
reviewHint: 'Verify your settings, then press Complete Setup.',
|
|
351
367
|
reviewInstallResults: 'Agent configuration results:',
|
|
368
|
+
phaseSaving: 'Saving configuration…',
|
|
369
|
+
phaseAgents: 'Configuring agents…',
|
|
370
|
+
phaseSkill: 'Installing skill…',
|
|
371
|
+
phaseDone: 'Setup complete!',
|
|
352
372
|
retryAgent: 'Retry',
|
|
373
|
+
agentFailedCount: (n: number) => `${n} agent${n > 1 ? 's' : ''} failed`,
|
|
374
|
+
agentCountSummary: (n: number) => `${n} agent${n > 1 ? 's' : ''}`,
|
|
353
375
|
agentFailureNote: 'Agent failures are non-blocking — you can enter MindOS and retry from Settings → MCP.',
|
|
354
376
|
portAvailable: 'Available',
|
|
355
377
|
portChanged: 'Port changed — please restart the server for it to take effect.',
|
|
@@ -601,6 +623,8 @@ export const messages = {
|
|
|
601
623
|
skillLanguage: 'Skill 语言',
|
|
602
624
|
skillLangEn: 'English',
|
|
603
625
|
skillLangZh: '中文',
|
|
626
|
+
selectDetected: '选择已检测',
|
|
627
|
+
clearSelection: '清除',
|
|
604
628
|
}, save: '保存',
|
|
605
629
|
saved: '已保存',
|
|
606
630
|
saveFailed: '保存失败',
|
|
@@ -656,7 +680,11 @@ export const messages = {
|
|
|
656
680
|
kbPath: '知识库路径',
|
|
657
681
|
kbPathHint: '笔记目录的绝对路径。',
|
|
658
682
|
kbPathDefault: '~/MindOS/mind',
|
|
683
|
+
kbPathUseDefault: (path: string) => `使用 ${path}`,
|
|
659
684
|
kbPathExists: (n: number) => `目录已有 ${n} 个文件 — 将不会应用模板。`,
|
|
685
|
+
kbPathHasFiles: (n: number) => `该目录已有 ${n} 个文件。可以跳过模板,或选择合并(已有文件不会被覆盖)。`,
|
|
686
|
+
kbTemplateSkip: '跳过模板',
|
|
687
|
+
kbTemplateMerge: '选择模板合并',
|
|
660
688
|
template: '初始模板',
|
|
661
689
|
templateSkip: '跳过(目录已有文件)',
|
|
662
690
|
// Step 2
|
|
@@ -698,7 +726,16 @@ export const messages = {
|
|
|
698
726
|
agentToolsLoading: '正在加载 Agent…',
|
|
699
727
|
agentToolsEmpty: '未检测到受支持的 Agent。',
|
|
700
728
|
agentNoneSelected: '未选择 agent — 可稍后在 设置 → MCP 中配置。',
|
|
701
|
-
agentSkipLater: '跳过 —
|
|
729
|
+
agentSkipLater: '跳过 — 稍后在 设置 > MCP 中安装',
|
|
730
|
+
agentSelectDetected: '选择已检测到的 Agent',
|
|
731
|
+
agentNoneDetected: '未在系统中检测到已安装的 Agent。',
|
|
732
|
+
agentShowMore: (n: number) => `显示另外 ${n} 个 Agent`,
|
|
733
|
+
agentAdvanced: '高级选项',
|
|
734
|
+
agentScopeGlobal: '为所有项目安装',
|
|
735
|
+
agentScopeProject: '仅当前项目',
|
|
736
|
+
badgeInstalled: '已安装',
|
|
737
|
+
badgeDetected: '已检测到',
|
|
738
|
+
badgeNotFound: '未找到',
|
|
702
739
|
agentNotInstalled: '未安装',
|
|
703
740
|
agentDetected: '已检测到',
|
|
704
741
|
agentNotFound: '未找到',
|
|
@@ -711,6 +748,7 @@ export const messages = {
|
|
|
711
748
|
agentUnverified: '未验证',
|
|
712
749
|
agentVerifyNote: 'stdio agent 需重启后验证',
|
|
713
750
|
// Skill auto-install
|
|
751
|
+
skillWhat: 'Skill 教 AI Agent 如何使用你的知识库 — 包括笔记的读取、写入和整理规则。',
|
|
714
752
|
skillAutoHint: (name: string) => `根据您选择的模板,将向选中的 Agent 安装「${name}」Skill。`,
|
|
715
753
|
skillLabel: 'Skill',
|
|
716
754
|
skillInstalling: '正在安装 Skill…',
|
|
@@ -720,9 +758,15 @@ export const messages = {
|
|
|
720
758
|
aiSkipTitle: '暂时跳过',
|
|
721
759
|
aiSkipDesc: '稍后可在 设置 → AI 中添加 API 密钥。',
|
|
722
760
|
// Step 6 — Review
|
|
723
|
-
reviewHint: '
|
|
761
|
+
reviewHint: '确认设置无误后,点击完成。',
|
|
724
762
|
reviewInstallResults: 'Agent 配置结果:',
|
|
763
|
+
phaseSaving: '正在保存配置…',
|
|
764
|
+
phaseAgents: '正在配置 Agent…',
|
|
765
|
+
phaseSkill: '正在安装 Skill…',
|
|
766
|
+
phaseDone: '设置完成!',
|
|
725
767
|
retryAgent: '重试',
|
|
768
|
+
agentFailedCount: (n: number) => `${n} 个 Agent 配置失败`,
|
|
769
|
+
agentCountSummary: (n: number) => `${n} 个 Agent`,
|
|
726
770
|
agentFailureNote: 'Agent 安装失败不影响进入 MindOS — 可稍后在 设置 → MCP 中重试。',
|
|
727
771
|
portAvailable: '可用',
|
|
728
772
|
portChanged: '端口已变更 — 请重启服务以使其生效。',
|