@geminilight/mindos 0.5.37 → 0.5.38
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/app/app/globals.css +0 -1
- package/app/components/SidebarLayout.tsx +70 -235
- package/app/components/settings/McpSkillCreateForm.tsx +178 -0
- package/app/components/settings/McpSkillRow.tsx +145 -0
- package/app/components/settings/McpSkillsSection.tsx +71 -307
- package/app/hooks/useAskPanel.ts +117 -0
- package/app/hooks/useLeftPanel.ts +81 -0
- package/package.json +1 -1
- package/scripts/release.sh +18 -4
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import { ChevronDown, ChevronRight, Trash2, Pencil, Loader2, AlertCircle } from 'lucide-react';
|
|
5
|
+
import { Toggle } from './Primitives';
|
|
6
|
+
import dynamic from 'next/dynamic';
|
|
7
|
+
import type { SkillInfo } from './types';
|
|
8
|
+
|
|
9
|
+
const MarkdownView = dynamic(() => import('@/components/MarkdownView'), { ssr: false });
|
|
10
|
+
|
|
11
|
+
/** Strip YAML frontmatter (first `---` … `---` block) from markdown content. */
|
|
12
|
+
function stripFrontmatter(content: string): string {
|
|
13
|
+
if (!content.startsWith('---')) return content;
|
|
14
|
+
const end = content.indexOf('\n---', 3);
|
|
15
|
+
if (end === -1) return content;
|
|
16
|
+
return content.slice(end + 4).replace(/^\n+/, '');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface SkillRowProps {
|
|
20
|
+
skill: SkillInfo;
|
|
21
|
+
expanded: boolean;
|
|
22
|
+
onExpand: (name: string) => void;
|
|
23
|
+
onToggle: (name: string, enabled: boolean) => void;
|
|
24
|
+
onDelete: (name: string) => void;
|
|
25
|
+
onEditStart: (name: string) => void;
|
|
26
|
+
onEditSave: (name: string) => void;
|
|
27
|
+
onEditCancel: () => void;
|
|
28
|
+
editing: string | null;
|
|
29
|
+
editContent: string;
|
|
30
|
+
setEditContent: (v: string) => void;
|
|
31
|
+
editError: string;
|
|
32
|
+
saving: boolean;
|
|
33
|
+
fullContent: Record<string, string>;
|
|
34
|
+
loadingContent: string | null;
|
|
35
|
+
loadErrors: Record<string, string>;
|
|
36
|
+
m: Record<string, any> | undefined;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export default function SkillRow({
|
|
40
|
+
skill, expanded, onExpand, onToggle, onDelete,
|
|
41
|
+
onEditStart, onEditSave, onEditCancel,
|
|
42
|
+
editing, editContent, setEditContent, editError, saving,
|
|
43
|
+
fullContent, loadingContent, loadErrors, m,
|
|
44
|
+
}: SkillRowProps) {
|
|
45
|
+
return (
|
|
46
|
+
<div className="border border-border rounded-lg overflow-hidden">
|
|
47
|
+
<div
|
|
48
|
+
className="flex items-center gap-2 px-3 py-2 cursor-pointer hover:bg-muted/50 transition-colors"
|
|
49
|
+
onClick={() => onExpand(skill.name)}
|
|
50
|
+
>
|
|
51
|
+
{expanded ? <ChevronDown size={12} /> : <ChevronRight size={12} />}
|
|
52
|
+
<span className="text-xs font-medium flex-1">{skill.name}</span>
|
|
53
|
+
<span className={`text-2xs px-1.5 py-0.5 rounded ${
|
|
54
|
+
skill.source === 'builtin' ? 'bg-blue-500/15 text-blue-500' : 'bg-purple-500/15 text-purple-500'
|
|
55
|
+
}`}>
|
|
56
|
+
{skill.source === 'builtin' ? (m?.skillBuiltin ?? 'Built-in') : (m?.skillUser ?? 'Custom')}
|
|
57
|
+
</span>
|
|
58
|
+
<Toggle size="sm" checked={skill.enabled} onClick={(e: React.MouseEvent) => { e.stopPropagation(); onToggle(skill.name, !skill.enabled); }} />
|
|
59
|
+
</div>
|
|
60
|
+
|
|
61
|
+
{expanded && (
|
|
62
|
+
<div className="px-3 py-2 border-t border-border text-xs space-y-2 bg-muted/20">
|
|
63
|
+
<p className="text-muted-foreground">{skill.description || 'No description'}</p>
|
|
64
|
+
<p className="text-muted-foreground font-mono text-2xs">{skill.path}</p>
|
|
65
|
+
|
|
66
|
+
{loadingContent === skill.name ? (
|
|
67
|
+
<div className="flex items-center gap-1.5 text-muted-foreground">
|
|
68
|
+
<Loader2 size={10} className="animate-spin" />
|
|
69
|
+
<span className="text-2xs">Loading...</span>
|
|
70
|
+
</div>
|
|
71
|
+
) : fullContent[skill.name] ? (
|
|
72
|
+
<div className="space-y-1.5">
|
|
73
|
+
<div className="flex items-center justify-between">
|
|
74
|
+
<span className="text-2xs text-muted-foreground font-medium">{m?.skillContent ?? 'Content'}</span>
|
|
75
|
+
<div className="flex items-center gap-2">
|
|
76
|
+
{skill.editable && editing !== skill.name && (
|
|
77
|
+
<button
|
|
78
|
+
onClick={() => onEditStart(skill.name)}
|
|
79
|
+
className="flex items-center gap-1 text-2xs text-muted-foreground hover:text-foreground transition-colors"
|
|
80
|
+
>
|
|
81
|
+
<Pencil size={10} />
|
|
82
|
+
{m?.editSkill ?? 'Edit'}
|
|
83
|
+
</button>
|
|
84
|
+
)}
|
|
85
|
+
{skill.editable && (
|
|
86
|
+
<button
|
|
87
|
+
onClick={() => onDelete(skill.name)}
|
|
88
|
+
className="flex items-center gap-1 text-2xs text-destructive hover:underline"
|
|
89
|
+
>
|
|
90
|
+
<Trash2 size={10} />
|
|
91
|
+
{m?.deleteSkill ?? 'Delete'}
|
|
92
|
+
</button>
|
|
93
|
+
)}
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
|
|
97
|
+
{editing === skill.name ? (
|
|
98
|
+
<div className="space-y-1.5">
|
|
99
|
+
<textarea
|
|
100
|
+
value={editContent}
|
|
101
|
+
onChange={e => setEditContent(e.target.value)}
|
|
102
|
+
rows={Math.min(20, (editContent.match(/\n/g) || []).length + 3)}
|
|
103
|
+
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"
|
|
104
|
+
/>
|
|
105
|
+
<div className="flex items-center gap-2">
|
|
106
|
+
<button
|
|
107
|
+
onClick={() => onEditSave(skill.name)}
|
|
108
|
+
disabled={saving}
|
|
109
|
+
className="flex items-center gap-1 px-2.5 py-1 text-xs rounded-md disabled:opacity-40 disabled:cursor-not-allowed transition-colors"
|
|
110
|
+
style={{ background: 'var(--amber)', color: 'var(--amber-foreground)' }}
|
|
111
|
+
>
|
|
112
|
+
{saving && <Loader2 size={10} className="animate-spin" />}
|
|
113
|
+
{m?.saveSkill ?? 'Save'}
|
|
114
|
+
</button>
|
|
115
|
+
<button
|
|
116
|
+
onClick={onEditCancel}
|
|
117
|
+
className="px-2.5 py-1 text-xs rounded-md border border-border text-muted-foreground hover:text-foreground transition-colors"
|
|
118
|
+
>
|
|
119
|
+
{m?.cancelSkill ?? 'Cancel'}
|
|
120
|
+
</button>
|
|
121
|
+
</div>
|
|
122
|
+
{editError && (
|
|
123
|
+
<p className="text-2xs text-destructive flex items-center gap-1">
|
|
124
|
+
<AlertCircle size={10} />
|
|
125
|
+
{editError}
|
|
126
|
+
</p>
|
|
127
|
+
)}
|
|
128
|
+
</div>
|
|
129
|
+
) : (
|
|
130
|
+
<div className="w-full rounded-md border border-border bg-background/50 max-h-[300px] overflow-y-auto px-2.5 py-1.5 text-xs [&_.prose]:max-w-none [&_.prose]:text-xs [&_h1]:text-sm [&_h2]:text-xs [&_h3]:text-xs [&_pre]:text-2xs [&_code]:text-2xs">
|
|
131
|
+
<MarkdownView content={stripFrontmatter(fullContent[skill.name])} />
|
|
132
|
+
</div>
|
|
133
|
+
)}
|
|
134
|
+
</div>
|
|
135
|
+
) : loadErrors[skill.name] ? (
|
|
136
|
+
<p className="text-2xs text-destructive flex items-center gap-1">
|
|
137
|
+
<AlertCircle size={10} />
|
|
138
|
+
{loadErrors[skill.name]}
|
|
139
|
+
</p>
|
|
140
|
+
) : null}
|
|
141
|
+
</div>
|
|
142
|
+
)}
|
|
143
|
+
</div>
|
|
144
|
+
);
|
|
145
|
+
}
|
|
@@ -2,83 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
import { useState, useEffect, useCallback, useMemo } from 'react';
|
|
4
4
|
import {
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
Loader2, ChevronDown, ChevronRight,
|
|
6
|
+
Plus, X, Search,
|
|
7
7
|
} from 'lucide-react';
|
|
8
8
|
import { apiFetch } from '@/lib/api';
|
|
9
9
|
import { useMcpDataOptional } from '@/hooks/useMcpData';
|
|
10
|
-
import { Toggle } from './Primitives';
|
|
11
|
-
import dynamic from 'next/dynamic';
|
|
12
10
|
import type { SkillInfo, McpSkillsSectionProps } from './types';
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
/* ── Helpers ───────────────────────────────────────────────────── */
|
|
17
|
-
|
|
18
|
-
/** Strip YAML frontmatter (first `---` … `---` block) from markdown content. */
|
|
19
|
-
function stripFrontmatter(content: string): string {
|
|
20
|
-
if (!content.startsWith('---')) return content;
|
|
21
|
-
const end = content.indexOf('\n---', 3);
|
|
22
|
-
if (end === -1) return content;
|
|
23
|
-
return content.slice(end + 4).replace(/^\n+/, '');
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const skillFrontmatter = (n: string) => `---
|
|
27
|
-
name: ${n}
|
|
28
|
-
description: >
|
|
29
|
-
Describe WHEN the agent should use this
|
|
30
|
-
skill. Be specific about trigger conditions.
|
|
31
|
-
---`;
|
|
32
|
-
|
|
33
|
-
const SKILL_TEMPLATES: Record<string, (name: string) => string> = {
|
|
34
|
-
general: (n) => `${skillFrontmatter(n)}
|
|
35
|
-
|
|
36
|
-
# Instructions
|
|
37
|
-
|
|
38
|
-
## Context
|
|
39
|
-
<!-- Background knowledge for the agent -->
|
|
40
|
-
|
|
41
|
-
## Steps
|
|
42
|
-
1.
|
|
43
|
-
2.
|
|
44
|
-
|
|
45
|
-
## Rules
|
|
46
|
-
<!-- Constraints, edge cases, formats -->
|
|
47
|
-
- `,
|
|
48
|
-
|
|
49
|
-
'tool-use': (n) => `${skillFrontmatter(n)}
|
|
50
|
-
|
|
51
|
-
# Instructions
|
|
52
|
-
|
|
53
|
-
## Available Tools
|
|
54
|
-
<!-- List tools the agent can use -->
|
|
55
|
-
-
|
|
56
|
-
|
|
57
|
-
## When to Use
|
|
58
|
-
<!-- Conditions that trigger this skill -->
|
|
59
|
-
|
|
60
|
-
## Output Format
|
|
61
|
-
<!-- Expected response structure -->
|
|
62
|
-
`,
|
|
63
|
-
|
|
64
|
-
workflow: (n) => `${skillFrontmatter(n)}
|
|
65
|
-
|
|
66
|
-
# Instructions
|
|
67
|
-
|
|
68
|
-
## Trigger
|
|
69
|
-
<!-- What triggers this workflow -->
|
|
70
|
-
|
|
71
|
-
## Steps
|
|
72
|
-
1.
|
|
73
|
-
2.
|
|
74
|
-
|
|
75
|
-
## Validation
|
|
76
|
-
<!-- How to verify success -->
|
|
77
|
-
|
|
78
|
-
## Rollback
|
|
79
|
-
<!-- What to do on failure -->
|
|
80
|
-
`,
|
|
81
|
-
};
|
|
11
|
+
import SkillRow from './McpSkillRow';
|
|
12
|
+
import SkillCreateForm from './McpSkillCreateForm';
|
|
82
13
|
|
|
83
14
|
/* ── Skills Section ────────────────────────────────────────────── */
|
|
84
15
|
|
|
@@ -89,8 +20,6 @@ export default function SkillsSection({ t }: McpSkillsSectionProps) {
|
|
|
89
20
|
const [loading, setLoading] = useState(true);
|
|
90
21
|
const [expanded, setExpanded] = useState<string | null>(null);
|
|
91
22
|
const [adding, setAdding] = useState(false);
|
|
92
|
-
const [newName, setNewName] = useState('');
|
|
93
|
-
const [newContent, setNewContent] = useState('');
|
|
94
23
|
const [saving, setSaving] = useState(false);
|
|
95
24
|
const [createError, setCreateError] = useState('');
|
|
96
25
|
|
|
@@ -102,20 +31,17 @@ export default function SkillsSection({ t }: McpSkillsSectionProps) {
|
|
|
102
31
|
const [fullContent, setFullContent] = useState<Record<string, string>>({});
|
|
103
32
|
const [loadingContent, setLoadingContent] = useState<string | null>(null);
|
|
104
33
|
const [loadErrors, setLoadErrors] = useState<Record<string, string>>({});
|
|
105
|
-
const [selectedTemplate, setSelectedTemplate] = useState<'general' | 'tool-use' | 'workflow'>('general');
|
|
106
|
-
// 🟡 MAJOR #3: Prevent race condition in lang switch
|
|
107
34
|
const [switchingLang, setSwitchingLang] = useState(false);
|
|
108
35
|
|
|
109
36
|
const fetchSkills = useCallback(async () => {
|
|
110
37
|
try {
|
|
111
38
|
const data = await apiFetch<{ skills: SkillInfo[] }>('/api/skills');
|
|
112
39
|
setSkills(data.skills);
|
|
113
|
-
setLoadErrors({});
|
|
40
|
+
setLoadErrors({});
|
|
114
41
|
} catch (err) {
|
|
115
42
|
const msg = err instanceof Error ? err.message : 'Failed to load skills';
|
|
116
43
|
console.error('fetchSkills error:', msg);
|
|
117
44
|
setLoadErrors(prev => ({ ...prev, _root: msg }));
|
|
118
|
-
// Keep existing skills data rather than clearing
|
|
119
45
|
}
|
|
120
46
|
setLoading(false);
|
|
121
47
|
}, []);
|
|
@@ -132,14 +58,14 @@ export default function SkillsSection({ t }: McpSkillsSectionProps) {
|
|
|
132
58
|
const customSkills = useMemo(() => filtered.filter(s => s.source === 'user'), [filtered]);
|
|
133
59
|
const builtinSkills = useMemo(() => filtered.filter(s => s.source === 'builtin'), [filtered]);
|
|
134
60
|
|
|
61
|
+
// ── Handlers ──
|
|
62
|
+
|
|
135
63
|
const handleToggle = async (name: string, enabled: boolean) => {
|
|
136
|
-
// Delegate to McpProvider when available — single API call, no event storm
|
|
137
64
|
if (mcp) {
|
|
138
65
|
await mcp.toggleSkill(name, enabled);
|
|
139
66
|
setSkills(prev => prev.map(s => s.name === name ? { ...s, enabled } : s));
|
|
140
67
|
return;
|
|
141
68
|
}
|
|
142
|
-
// Fallback: direct API call (no McpProvider context)
|
|
143
69
|
try {
|
|
144
70
|
await apiFetch('/api/skills', {
|
|
145
71
|
method: 'POST',
|
|
@@ -198,9 +124,7 @@ export default function SkillsSection({ t }: McpSkillsSectionProps) {
|
|
|
198
124
|
const handleExpand = (name: string) => {
|
|
199
125
|
const next = expanded === name ? null : name;
|
|
200
126
|
setExpanded(next);
|
|
201
|
-
if (next)
|
|
202
|
-
loadFullContent(name);
|
|
203
|
-
}
|
|
127
|
+
if (next) loadFullContent(name);
|
|
204
128
|
if (editing && editing !== name) setEditing(null);
|
|
205
129
|
};
|
|
206
130
|
|
|
@@ -221,7 +145,7 @@ export default function SkillsSection({ t }: McpSkillsSectionProps) {
|
|
|
221
145
|
});
|
|
222
146
|
setFullContent(prev => ({ ...prev, [name]: editContent }));
|
|
223
147
|
setEditing(null);
|
|
224
|
-
fetchSkills();
|
|
148
|
+
fetchSkills();
|
|
225
149
|
window.dispatchEvent(new Event('mindos:skills-changed'));
|
|
226
150
|
} catch (err: unknown) {
|
|
227
151
|
setEditError(err instanceof Error ? err.message : 'Failed to save skill');
|
|
@@ -236,27 +160,17 @@ export default function SkillsSection({ t }: McpSkillsSectionProps) {
|
|
|
236
160
|
setEditError('');
|
|
237
161
|
};
|
|
238
162
|
|
|
239
|
-
const
|
|
240
|
-
|
|
241
|
-
const fn = SKILL_TEMPLATES[key] || SKILL_TEMPLATES.general;
|
|
242
|
-
return fn(skillName || 'my-skill');
|
|
243
|
-
};
|
|
244
|
-
|
|
245
|
-
const handleCreate = async () => {
|
|
246
|
-
if (!newName.trim()) return;
|
|
163
|
+
const handleCreate = async (name: string, content: string) => {
|
|
164
|
+
if (!name) return;
|
|
247
165
|
setSaving(true);
|
|
248
166
|
setCreateError('');
|
|
249
167
|
try {
|
|
250
|
-
// Content is the full SKILL.md (with frontmatter)
|
|
251
|
-
const content = newContent || getTemplate(newName.trim());
|
|
252
168
|
await apiFetch('/api/skills', {
|
|
253
169
|
method: 'POST',
|
|
254
170
|
headers: { 'Content-Type': 'application/json' },
|
|
255
|
-
body: JSON.stringify({ action: 'create', name
|
|
171
|
+
body: JSON.stringify({ action: 'create', name, content }),
|
|
256
172
|
});
|
|
257
173
|
setAdding(false);
|
|
258
|
-
setNewName('');
|
|
259
|
-
setNewContent('');
|
|
260
174
|
fetchSkills();
|
|
261
175
|
window.dispatchEvent(new Event('mindos:skills-changed'));
|
|
262
176
|
} catch (err: unknown) {
|
|
@@ -266,25 +180,6 @@ export default function SkillsSection({ t }: McpSkillsSectionProps) {
|
|
|
266
180
|
}
|
|
267
181
|
};
|
|
268
182
|
|
|
269
|
-
// Sync template name when newName changes (only if content matches a template)
|
|
270
|
-
const handleNameChange = (val: string) => {
|
|
271
|
-
const cleaned = val.toLowerCase().replace(/[^a-z0-9-]/g, '');
|
|
272
|
-
const oldTemplate = getTemplate(newName || 'my-skill');
|
|
273
|
-
if (!newContent || newContent === oldTemplate) {
|
|
274
|
-
setNewContent(getTemplate(cleaned || 'my-skill'));
|
|
275
|
-
}
|
|
276
|
-
setNewName(cleaned);
|
|
277
|
-
};
|
|
278
|
-
|
|
279
|
-
const handleTemplateChange = (tmpl: 'general' | 'tool-use' | 'workflow') => {
|
|
280
|
-
const oldTemplate = getTemplate(newName || 'my-skill', selectedTemplate);
|
|
281
|
-
setSelectedTemplate(tmpl);
|
|
282
|
-
// Only replace content if it matches the old template (user hasn't customized)
|
|
283
|
-
if (!newContent || newContent === oldTemplate) {
|
|
284
|
-
setNewContent(getTemplate(newName || 'my-skill', tmpl));
|
|
285
|
-
}
|
|
286
|
-
};
|
|
287
|
-
|
|
288
183
|
if (loading) {
|
|
289
184
|
return (
|
|
290
185
|
<div className="flex justify-center py-4">
|
|
@@ -293,107 +188,7 @@ export default function SkillsSection({ t }: McpSkillsSectionProps) {
|
|
|
293
188
|
);
|
|
294
189
|
}
|
|
295
190
|
|
|
296
|
-
|
|
297
|
-
<div key={skill.name} className="border border-border rounded-lg overflow-hidden">
|
|
298
|
-
<div
|
|
299
|
-
className="flex items-center gap-2 px-3 py-2 cursor-pointer hover:bg-muted/50 transition-colors"
|
|
300
|
-
onClick={() => handleExpand(skill.name)}
|
|
301
|
-
>
|
|
302
|
-
{expanded === skill.name ? <ChevronDown size={12} /> : <ChevronRight size={12} />}
|
|
303
|
-
<span className="text-xs font-medium flex-1">{skill.name}</span>
|
|
304
|
-
<span className={`text-2xs px-1.5 py-0.5 rounded ${
|
|
305
|
-
skill.source === 'builtin' ? 'bg-blue-500/15 text-blue-500' : 'bg-purple-500/15 text-purple-500'
|
|
306
|
-
}`}>
|
|
307
|
-
{skill.source === 'builtin' ? (m?.skillBuiltin ?? 'Built-in') : (m?.skillUser ?? 'Custom')}
|
|
308
|
-
</span>
|
|
309
|
-
<Toggle size="sm" checked={skill.enabled} onClick={e => { e.stopPropagation(); handleToggle(skill.name, !skill.enabled); }} />
|
|
310
|
-
</div>
|
|
311
|
-
|
|
312
|
-
{expanded === skill.name && (
|
|
313
|
-
<div className="px-3 py-2 border-t border-border text-xs space-y-2 bg-muted/20">
|
|
314
|
-
<p className="text-muted-foreground">{skill.description || 'No description'}</p>
|
|
315
|
-
<p className="text-muted-foreground font-mono text-2xs">{skill.path}</p>
|
|
316
|
-
|
|
317
|
-
{/* Full content display / edit */}
|
|
318
|
-
{loadingContent === skill.name ? (
|
|
319
|
-
<div className="flex items-center gap-1.5 text-muted-foreground">
|
|
320
|
-
<Loader2 size={10} className="animate-spin" />
|
|
321
|
-
<span className="text-2xs">Loading...</span>
|
|
322
|
-
</div>
|
|
323
|
-
) : fullContent[skill.name] ? (
|
|
324
|
-
<div className="space-y-1.5">
|
|
325
|
-
<div className="flex items-center justify-between">
|
|
326
|
-
<span className="text-2xs text-muted-foreground font-medium">{m?.skillContent ?? 'Content'}</span>
|
|
327
|
-
<div className="flex items-center gap-2">
|
|
328
|
-
{skill.editable && editing !== skill.name && (
|
|
329
|
-
<button
|
|
330
|
-
onClick={() => handleEditStart(skill.name)}
|
|
331
|
-
className="flex items-center gap-1 text-2xs text-muted-foreground hover:text-foreground transition-colors"
|
|
332
|
-
>
|
|
333
|
-
<Pencil size={10} />
|
|
334
|
-
{m?.editSkill ?? 'Edit'}
|
|
335
|
-
</button>
|
|
336
|
-
)}
|
|
337
|
-
{skill.editable && (
|
|
338
|
-
<button
|
|
339
|
-
onClick={() => handleDelete(skill.name)}
|
|
340
|
-
className="flex items-center gap-1 text-2xs text-destructive hover:underline"
|
|
341
|
-
>
|
|
342
|
-
<Trash2 size={10} />
|
|
343
|
-
{m?.deleteSkill ?? 'Delete'}
|
|
344
|
-
</button>
|
|
345
|
-
)}
|
|
346
|
-
</div>
|
|
347
|
-
</div>
|
|
348
|
-
|
|
349
|
-
{editing === skill.name ? (
|
|
350
|
-
<div className="space-y-1.5">
|
|
351
|
-
<textarea
|
|
352
|
-
value={editContent}
|
|
353
|
-
onChange={e => setEditContent(e.target.value)}
|
|
354
|
-
rows={Math.min(20, (editContent.match(/\n/g) || []).length + 3)}
|
|
355
|
-
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"
|
|
356
|
-
/>
|
|
357
|
-
<div className="flex items-center gap-2">
|
|
358
|
-
<button
|
|
359
|
-
onClick={() => handleEditSave(skill.name)}
|
|
360
|
-
disabled={saving}
|
|
361
|
-
className="flex items-center gap-1 px-2.5 py-1 text-xs rounded-md disabled:opacity-40 disabled:cursor-not-allowed transition-colors"
|
|
362
|
-
style={{ background: 'var(--amber)', color: 'var(--amber-foreground)' }}
|
|
363
|
-
>
|
|
364
|
-
{saving && <Loader2 size={10} className="animate-spin" />}
|
|
365
|
-
{m?.saveSkill ?? 'Save'}
|
|
366
|
-
</button>
|
|
367
|
-
<button
|
|
368
|
-
onClick={handleEditCancel}
|
|
369
|
-
className="px-2.5 py-1 text-xs rounded-md border border-border text-muted-foreground hover:text-foreground transition-colors"
|
|
370
|
-
>
|
|
371
|
-
{m?.cancelSkill ?? 'Cancel'}
|
|
372
|
-
</button>
|
|
373
|
-
</div>
|
|
374
|
-
{editError && editing === skill.name && (
|
|
375
|
-
<p className="text-2xs text-destructive flex items-center gap-1">
|
|
376
|
-
<AlertCircle size={10} />
|
|
377
|
-
{editError}
|
|
378
|
-
</p>
|
|
379
|
-
)}
|
|
380
|
-
</div>
|
|
381
|
-
) : (
|
|
382
|
-
<div className="w-full rounded-md border border-border bg-background/50 max-h-[300px] overflow-y-auto px-2.5 py-1.5 text-xs [&_.prose]:max-w-none [&_.prose]:text-xs [&_h1]:text-sm [&_h2]:text-xs [&_h3]:text-xs [&_pre]:text-2xs [&_code]:text-2xs">
|
|
383
|
-
<MarkdownView content={stripFrontmatter(fullContent[skill.name])} />
|
|
384
|
-
</div>
|
|
385
|
-
)}
|
|
386
|
-
</div>
|
|
387
|
-
) : loadErrors[skill.name] ? (
|
|
388
|
-
<p className="text-2xs text-destructive flex items-center gap-1">
|
|
389
|
-
<AlertCircle size={10} />
|
|
390
|
-
{loadErrors[skill.name]}
|
|
391
|
-
</p>
|
|
392
|
-
) : null}
|
|
393
|
-
</div>
|
|
394
|
-
)}
|
|
395
|
-
</div>
|
|
396
|
-
);
|
|
191
|
+
// ── Render ──
|
|
397
192
|
|
|
398
193
|
return (
|
|
399
194
|
<div className="space-y-3 pt-2">
|
|
@@ -408,10 +203,7 @@ export default function SkillsSection({ t }: McpSkillsSectionProps) {
|
|
|
408
203
|
className="w-full pl-7 pr-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"
|
|
409
204
|
/>
|
|
410
205
|
{search && (
|
|
411
|
-
<button
|
|
412
|
-
onClick={() => setSearch('')}
|
|
413
|
-
className="absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
|
|
414
|
-
>
|
|
206
|
+
<button onClick={() => setSearch('')} className="absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground">
|
|
415
207
|
<X size={10} />
|
|
416
208
|
</button>
|
|
417
209
|
)}
|
|
@@ -426,7 +218,6 @@ export default function SkillsSection({ t }: McpSkillsSectionProps) {
|
|
|
426
218
|
setSwitchingLang(true);
|
|
427
219
|
try {
|
|
428
220
|
if (lang === 'en') {
|
|
429
|
-
// Sequential to ensure both complete or both revert on failure
|
|
430
221
|
await handleToggle('mindos', true);
|
|
431
222
|
await handleToggle('mindos-zh', false);
|
|
432
223
|
} else {
|
|
@@ -435,7 +226,6 @@ export default function SkillsSection({ t }: McpSkillsSectionProps) {
|
|
|
435
226
|
}
|
|
436
227
|
} catch (err) {
|
|
437
228
|
console.error('Lang switch failed:', err);
|
|
438
|
-
// Errors are already set by handleToggle; no further action needed
|
|
439
229
|
} finally {
|
|
440
230
|
setSwitchingLang(false);
|
|
441
231
|
}
|
|
@@ -448,9 +238,7 @@ export default function SkillsSection({ t }: McpSkillsSectionProps) {
|
|
|
448
238
|
onClick={() => handleLangSwitch('en')}
|
|
449
239
|
disabled={switchingLang}
|
|
450
240
|
className={`px-2.5 py-1 text-xs transition-colors disabled:opacity-50 disabled:cursor-not-allowed ${
|
|
451
|
-
currentLang === 'en'
|
|
452
|
-
? 'bg-amber-500/15 text-amber-600 font-medium'
|
|
453
|
-
: 'text-muted-foreground hover:bg-muted'
|
|
241
|
+
currentLang === 'en' ? 'bg-amber-500/15 text-amber-600 font-medium' : 'text-muted-foreground hover:bg-muted'
|
|
454
242
|
}`}
|
|
455
243
|
>
|
|
456
244
|
{m?.skillLangEn ?? 'English'}
|
|
@@ -459,9 +247,7 @@ export default function SkillsSection({ t }: McpSkillsSectionProps) {
|
|
|
459
247
|
onClick={() => handleLangSwitch('zh')}
|
|
460
248
|
disabled={switchingLang}
|
|
461
249
|
className={`px-2.5 py-1 text-xs transition-colors disabled:opacity-50 disabled:cursor-not-allowed border-l border-border ${
|
|
462
|
-
currentLang === 'zh'
|
|
463
|
-
? 'bg-amber-500/15 text-amber-600 font-medium'
|
|
464
|
-
: 'text-muted-foreground hover:bg-muted'
|
|
250
|
+
currentLang === 'zh' ? 'bg-amber-500/15 text-amber-600 font-medium' : 'text-muted-foreground hover:bg-muted'
|
|
465
251
|
}`}
|
|
466
252
|
>
|
|
467
253
|
{m?.skillLangZh ?? '中文'}
|
|
@@ -478,19 +264,40 @@ export default function SkillsSection({ t }: McpSkillsSectionProps) {
|
|
|
478
264
|
</p>
|
|
479
265
|
)}
|
|
480
266
|
|
|
481
|
-
{/* Custom group
|
|
267
|
+
{/* Custom group */}
|
|
482
268
|
{customSkills.length > 0 && (
|
|
483
269
|
<div className="space-y-1.5">
|
|
484
270
|
<div className="flex items-center gap-2 text-xs font-medium text-muted-foreground">
|
|
485
271
|
<span>{m?.customGroup ?? 'Custom'} ({customSkills.length})</span>
|
|
486
272
|
</div>
|
|
487
273
|
<div className="space-y-1.5">
|
|
488
|
-
{customSkills.map(
|
|
274
|
+
{customSkills.map(skill => (
|
|
275
|
+
<SkillRow
|
|
276
|
+
key={skill.name}
|
|
277
|
+
skill={skill}
|
|
278
|
+
expanded={expanded === skill.name}
|
|
279
|
+
onExpand={handleExpand}
|
|
280
|
+
onToggle={handleToggle}
|
|
281
|
+
onDelete={handleDelete}
|
|
282
|
+
onEditStart={handleEditStart}
|
|
283
|
+
onEditSave={handleEditSave}
|
|
284
|
+
onEditCancel={handleEditCancel}
|
|
285
|
+
editing={editing}
|
|
286
|
+
editContent={editContent}
|
|
287
|
+
setEditContent={setEditContent}
|
|
288
|
+
editError={editError}
|
|
289
|
+
saving={saving}
|
|
290
|
+
fullContent={fullContent}
|
|
291
|
+
loadingContent={loadingContent}
|
|
292
|
+
loadErrors={loadErrors}
|
|
293
|
+
m={m}
|
|
294
|
+
/>
|
|
295
|
+
))}
|
|
489
296
|
</div>
|
|
490
297
|
</div>
|
|
491
298
|
)}
|
|
492
299
|
|
|
493
|
-
{/* Built-in group
|
|
300
|
+
{/* Built-in group */}
|
|
494
301
|
{builtinSkills.length > 0 && (
|
|
495
302
|
<div className="space-y-1.5">
|
|
496
303
|
<div
|
|
@@ -502,88 +309,45 @@ export default function SkillsSection({ t }: McpSkillsSectionProps) {
|
|
|
502
309
|
</div>
|
|
503
310
|
{!builtinCollapsed && (
|
|
504
311
|
<div className="space-y-1.5">
|
|
505
|
-
{builtinSkills.map(
|
|
312
|
+
{builtinSkills.map(skill => (
|
|
313
|
+
<SkillRow
|
|
314
|
+
key={skill.name}
|
|
315
|
+
skill={skill}
|
|
316
|
+
expanded={expanded === skill.name}
|
|
317
|
+
onExpand={handleExpand}
|
|
318
|
+
onToggle={handleToggle}
|
|
319
|
+
onDelete={handleDelete}
|
|
320
|
+
onEditStart={handleEditStart}
|
|
321
|
+
onEditSave={handleEditSave}
|
|
322
|
+
onEditCancel={handleEditCancel}
|
|
323
|
+
editing={editing}
|
|
324
|
+
editContent={editContent}
|
|
325
|
+
setEditContent={setEditContent}
|
|
326
|
+
editError={editError}
|
|
327
|
+
saving={saving}
|
|
328
|
+
fullContent={fullContent}
|
|
329
|
+
loadingContent={loadingContent}
|
|
330
|
+
loadErrors={loadErrors}
|
|
331
|
+
m={m}
|
|
332
|
+
/>
|
|
333
|
+
))}
|
|
506
334
|
</div>
|
|
507
335
|
)}
|
|
508
336
|
</div>
|
|
509
337
|
)}
|
|
510
338
|
|
|
511
|
-
{/* Add skill
|
|
339
|
+
{/* Add skill */}
|
|
512
340
|
{adding ? (
|
|
513
|
-
<
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
<div className="space-y-1">
|
|
521
|
-
<label className="text-2xs text-muted-foreground">{m?.skillName ?? 'Name'}</label>
|
|
522
|
-
<input
|
|
523
|
-
type="text"
|
|
524
|
-
value={newName}
|
|
525
|
-
onChange={e => handleNameChange(e.target.value)}
|
|
526
|
-
placeholder="my-skill"
|
|
527
|
-
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"
|
|
528
|
-
/>
|
|
529
|
-
</div>
|
|
530
|
-
<div className="space-y-1">
|
|
531
|
-
<label className="text-2xs text-muted-foreground">{m?.skillTemplate ?? 'Template'}</label>
|
|
532
|
-
<div className="flex rounded-md border border-border overflow-hidden w-fit">
|
|
533
|
-
{(['general', 'tool-use', 'workflow'] as const).map((tmpl, i) => (
|
|
534
|
-
<button
|
|
535
|
-
key={tmpl}
|
|
536
|
-
onClick={() => handleTemplateChange(tmpl)}
|
|
537
|
-
className={`px-2.5 py-1 text-xs transition-colors ${i > 0 ? 'border-l border-border' : ''} ${
|
|
538
|
-
selectedTemplate === tmpl
|
|
539
|
-
? 'bg-amber-500/15 text-amber-600 font-medium'
|
|
540
|
-
: 'text-muted-foreground hover:bg-muted'
|
|
541
|
-
}`}
|
|
542
|
-
>
|
|
543
|
-
{tmpl === 'general' ? (m?.skillTemplateGeneral ?? 'General')
|
|
544
|
-
: tmpl === 'tool-use' ? (m?.skillTemplateToolUse ?? 'Tool-use')
|
|
545
|
-
: (m?.skillTemplateWorkflow ?? 'Workflow')}
|
|
546
|
-
</button>
|
|
547
|
-
))}
|
|
548
|
-
</div>
|
|
549
|
-
</div>
|
|
550
|
-
<div className="space-y-1">
|
|
551
|
-
<label className="text-2xs text-muted-foreground">{m?.skillContent ?? 'Content'}</label>
|
|
552
|
-
<textarea
|
|
553
|
-
value={newContent}
|
|
554
|
-
onChange={e => setNewContent(e.target.value)}
|
|
555
|
-
rows={16}
|
|
556
|
-
placeholder="Skill instructions (markdown)..."
|
|
557
|
-
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"
|
|
558
|
-
/>
|
|
559
|
-
</div>
|
|
560
|
-
{createError && (
|
|
561
|
-
<p className="text-2xs text-destructive flex items-center gap-1">
|
|
562
|
-
<AlertCircle size={10} />
|
|
563
|
-
{createError}
|
|
564
|
-
</p>
|
|
565
|
-
)}
|
|
566
|
-
<div className="flex items-center gap-2">
|
|
567
|
-
<button
|
|
568
|
-
onClick={handleCreate}
|
|
569
|
-
disabled={!newName.trim() || saving}
|
|
570
|
-
className="flex items-center gap-1 px-2.5 py-1 text-xs rounded-md disabled:opacity-40 disabled:cursor-not-allowed transition-colors"
|
|
571
|
-
style={{ background: 'var(--amber)', color: 'var(--amber-foreground)' }}
|
|
572
|
-
>
|
|
573
|
-
{saving && <Loader2 size={10} className="animate-spin" />}
|
|
574
|
-
{m?.saveSkill ?? 'Save'}
|
|
575
|
-
</button>
|
|
576
|
-
<button
|
|
577
|
-
onClick={() => { setAdding(false); setNewName(''); setNewContent(''); setCreateError(''); }}
|
|
578
|
-
className="px-2.5 py-1 text-xs rounded-md border border-border text-muted-foreground hover:text-foreground transition-colors"
|
|
579
|
-
>
|
|
580
|
-
{m?.cancelSkill ?? 'Cancel'}
|
|
581
|
-
</button>
|
|
582
|
-
</div>
|
|
583
|
-
</div>
|
|
341
|
+
<SkillCreateForm
|
|
342
|
+
onSave={handleCreate}
|
|
343
|
+
onCancel={() => { setAdding(false); setCreateError(''); }}
|
|
344
|
+
saving={saving}
|
|
345
|
+
error={createError}
|
|
346
|
+
m={m}
|
|
347
|
+
/>
|
|
584
348
|
) : (
|
|
585
349
|
<button
|
|
586
|
-
onClick={() =>
|
|
350
|
+
onClick={() => setAdding(true)}
|
|
587
351
|
className="flex items-center gap-1.5 text-xs text-muted-foreground hover:text-foreground transition-colors"
|
|
588
352
|
>
|
|
589
353
|
<Plus size={12} />
|