@geminilight/mindos 0.5.19 → 0.5.20
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/api/file/route.ts +35 -11
- package/app/app/api/skills/route.ts +22 -3
- package/app/components/Sidebar.tsx +21 -1
- package/app/components/settings/McpTab.tsx +286 -56
- package/app/lib/i18n.ts +16 -0
- package/app/next.config.ts +7 -0
- package/bin/cli.js +133 -1
- package/package.json +1 -1
- package/scripts/setup.js +13 -0
- package/skills/mindos/SKILL.md +10 -168
- package/skills/mindos-zh/SKILL.md +14 -172
- package/templates/skill-rules/en/skill-rules.md +222 -0
- package/templates/skill-rules/en/user-rules.md +20 -0
- package/templates/skill-rules/zh/skill-rules.md +222 -0
- package/templates/skill-rules/zh/user-rules.md +20 -0
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { useState, useEffect, useCallback } from 'react';
|
|
3
|
+
import { useState, useEffect, useCallback, useMemo } from 'react';
|
|
4
4
|
import {
|
|
5
5
|
Plug, CheckCircle2, AlertCircle, Loader2, Copy, Check,
|
|
6
|
-
ChevronDown, ChevronRight, Trash2, Plus, X,
|
|
6
|
+
ChevronDown, ChevronRight, Trash2, Plus, X, Search, Pencil,
|
|
7
7
|
} from 'lucide-react';
|
|
8
8
|
import { apiFetch } from '@/lib/api';
|
|
9
|
+
import dynamic from 'next/dynamic';
|
|
10
|
+
|
|
11
|
+
const MarkdownView = dynamic(() => import('@/components/MarkdownView'), { ssr: false });
|
|
9
12
|
|
|
10
13
|
/* ── Types ─────────────────────────────────────────────────────── */
|
|
11
14
|
|
|
@@ -454,6 +457,12 @@ function AgentInstall({ agents, t, onRefresh }: { agents: AgentInfo[]; t: any; o
|
|
|
454
457
|
|
|
455
458
|
/* ── Skills Section ────────────────────────────────────────────── */
|
|
456
459
|
|
|
460
|
+
const SKILL_TEMPLATES: Record<string, (name: string) => string> = {
|
|
461
|
+
general: (n: string) => `---\nname: ${n}\ndescription: >\n Describe WHEN the agent should use this\n skill. Be specific about trigger conditions.\n---\n\n# Instructions\n\n## Context\n<!-- Background knowledge for the agent -->\n\n## Steps\n1. \n2. \n\n## Rules\n<!-- Constraints, edge cases, formats -->\n- `,
|
|
462
|
+
'tool-use': (n: string) => `---\nname: ${n}\ndescription: >\n Describe WHEN the agent should use this\n skill. Be specific about trigger conditions.\n---\n\n# Instructions\n\n## Available Tools\n<!-- List tools the agent can use -->\n- \n\n## When to Use\n<!-- Conditions that trigger this skill -->\n\n## Output Format\n<!-- Expected response structure -->\n`,
|
|
463
|
+
workflow: (n: string) => `---\nname: ${n}\ndescription: >\n Describe WHEN the agent should use this\n skill. Be specific about trigger conditions.\n---\n\n# Instructions\n\n## Trigger\n<!-- What triggers this workflow -->\n\n## Steps\n1. \n2. \n\n## Validation\n<!-- How to verify success -->\n\n## Rollback\n<!-- What to do on failure -->\n`,
|
|
464
|
+
};
|
|
465
|
+
|
|
457
466
|
function SkillsSection({ t }: { t: any }) {
|
|
458
467
|
const m = t.settings?.mcp;
|
|
459
468
|
const [skills, setSkills] = useState<SkillInfo[]>([]);
|
|
@@ -461,11 +470,19 @@ function SkillsSection({ t }: { t: any }) {
|
|
|
461
470
|
const [expanded, setExpanded] = useState<string | null>(null);
|
|
462
471
|
const [adding, setAdding] = useState(false);
|
|
463
472
|
const [newName, setNewName] = useState('');
|
|
464
|
-
const [newDesc, setNewDesc] = useState('');
|
|
465
473
|
const [newContent, setNewContent] = useState('');
|
|
466
474
|
const [saving, setSaving] = useState(false);
|
|
467
475
|
const [error, setError] = useState('');
|
|
468
476
|
|
|
477
|
+
// New state for search, grouping, full content, editing
|
|
478
|
+
const [search, setSearch] = useState('');
|
|
479
|
+
const [builtinCollapsed, setBuiltinCollapsed] = useState(true);
|
|
480
|
+
const [editing, setEditing] = useState<string | null>(null);
|
|
481
|
+
const [editContent, setEditContent] = useState('');
|
|
482
|
+
const [fullContent, setFullContent] = useState<Record<string, string>>({});
|
|
483
|
+
const [loadingContent, setLoadingContent] = useState<string | null>(null);
|
|
484
|
+
const [selectedTemplate, setSelectedTemplate] = useState<'general' | 'tool-use' | 'workflow'>('general');
|
|
485
|
+
|
|
469
486
|
const fetchSkills = useCallback(async () => {
|
|
470
487
|
try {
|
|
471
488
|
const data = await apiFetch<{ skills: SkillInfo[] }>('/api/skills');
|
|
@@ -476,6 +493,16 @@ function SkillsSection({ t }: { t: any }) {
|
|
|
476
493
|
|
|
477
494
|
useEffect(() => { fetchSkills(); }, [fetchSkills]);
|
|
478
495
|
|
|
496
|
+
// Filtered + grouped
|
|
497
|
+
const filtered = useMemo(() => {
|
|
498
|
+
if (!search) return skills;
|
|
499
|
+
const q = search.toLowerCase();
|
|
500
|
+
return skills.filter(s => s.name.toLowerCase().includes(q) || s.description.toLowerCase().includes(q));
|
|
501
|
+
}, [skills, search]);
|
|
502
|
+
|
|
503
|
+
const customSkills = useMemo(() => filtered.filter(s => s.source === 'user'), [filtered]);
|
|
504
|
+
const builtinSkills = useMemo(() => filtered.filter(s => s.source === 'builtin'), [filtered]);
|
|
505
|
+
|
|
479
506
|
const handleToggle = async (name: string, enabled: boolean) => {
|
|
480
507
|
try {
|
|
481
508
|
await apiFetch('/api/skills', {
|
|
@@ -496,23 +523,84 @@ function SkillsSection({ t }: { t: any }) {
|
|
|
496
523
|
headers: { 'Content-Type': 'application/json' },
|
|
497
524
|
body: JSON.stringify({ action: 'delete', name }),
|
|
498
525
|
});
|
|
526
|
+
setFullContent(prev => { const n = { ...prev }; delete n[name]; return n; });
|
|
527
|
+
if (editing === name) setEditing(null);
|
|
528
|
+
if (expanded === name) setExpanded(null);
|
|
499
529
|
fetchSkills();
|
|
500
530
|
} catch { /* ignore */ }
|
|
501
531
|
};
|
|
502
532
|
|
|
533
|
+
const loadFullContent = async (name: string) => {
|
|
534
|
+
if (fullContent[name]) return;
|
|
535
|
+
setLoadingContent(name);
|
|
536
|
+
try {
|
|
537
|
+
const data = await apiFetch<{ content: string }>('/api/skills', {
|
|
538
|
+
method: 'POST',
|
|
539
|
+
headers: { 'Content-Type': 'application/json' },
|
|
540
|
+
body: JSON.stringify({ action: 'read', name }),
|
|
541
|
+
});
|
|
542
|
+
setFullContent(prev => ({ ...prev, [name]: data.content }));
|
|
543
|
+
} catch {
|
|
544
|
+
// Store empty marker so UI shows "No description" rather than stuck loading
|
|
545
|
+
setFullContent(prev => ({ ...prev, [name]: '' }));
|
|
546
|
+
} finally {
|
|
547
|
+
setLoadingContent(null);
|
|
548
|
+
}
|
|
549
|
+
};
|
|
550
|
+
|
|
551
|
+
const handleExpand = (name: string) => {
|
|
552
|
+
const next = expanded === name ? null : name;
|
|
553
|
+
setExpanded(next);
|
|
554
|
+
if (next) loadFullContent(name);
|
|
555
|
+
if (editing && editing !== name) setEditing(null);
|
|
556
|
+
};
|
|
557
|
+
|
|
558
|
+
const handleEditStart = (name: string) => {
|
|
559
|
+
setEditing(name);
|
|
560
|
+
setEditContent(fullContent[name] || '');
|
|
561
|
+
};
|
|
562
|
+
|
|
563
|
+
const handleEditSave = async (name: string) => {
|
|
564
|
+
setSaving(true);
|
|
565
|
+
try {
|
|
566
|
+
await apiFetch('/api/skills', {
|
|
567
|
+
method: 'POST',
|
|
568
|
+
headers: { 'Content-Type': 'application/json' },
|
|
569
|
+
body: JSON.stringify({ action: 'update', name, content: editContent }),
|
|
570
|
+
});
|
|
571
|
+
setFullContent(prev => ({ ...prev, [name]: editContent }));
|
|
572
|
+
setEditing(null);
|
|
573
|
+
fetchSkills(); // refresh description from updated frontmatter
|
|
574
|
+
} catch { /* ignore */ } finally {
|
|
575
|
+
setSaving(false);
|
|
576
|
+
}
|
|
577
|
+
};
|
|
578
|
+
|
|
579
|
+
const handleEditCancel = () => {
|
|
580
|
+
setEditing(null);
|
|
581
|
+
setEditContent('');
|
|
582
|
+
};
|
|
583
|
+
|
|
584
|
+
const getTemplate = (skillName: string, tmpl?: string) => {
|
|
585
|
+
const key = tmpl || selectedTemplate;
|
|
586
|
+
const fn = SKILL_TEMPLATES[key] || SKILL_TEMPLATES.general;
|
|
587
|
+
return fn(skillName || 'my-skill');
|
|
588
|
+
};
|
|
589
|
+
|
|
503
590
|
const handleCreate = async () => {
|
|
504
591
|
if (!newName.trim()) return;
|
|
505
592
|
setSaving(true);
|
|
506
593
|
setError('');
|
|
507
594
|
try {
|
|
595
|
+
// Content is the full SKILL.md (with frontmatter)
|
|
596
|
+
const content = newContent || getTemplate(newName.trim());
|
|
508
597
|
await apiFetch('/api/skills', {
|
|
509
598
|
method: 'POST',
|
|
510
599
|
headers: { 'Content-Type': 'application/json' },
|
|
511
|
-
body: JSON.stringify({ action: 'create', name: newName.trim(),
|
|
600
|
+
body: JSON.stringify({ action: 'create', name: newName.trim(), content }),
|
|
512
601
|
});
|
|
513
602
|
setAdding(false);
|
|
514
603
|
setNewName('');
|
|
515
|
-
setNewDesc('');
|
|
516
604
|
setNewContent('');
|
|
517
605
|
fetchSkills();
|
|
518
606
|
} catch (err: unknown) {
|
|
@@ -522,6 +610,25 @@ function SkillsSection({ t }: { t: any }) {
|
|
|
522
610
|
}
|
|
523
611
|
};
|
|
524
612
|
|
|
613
|
+
// Sync template name when newName changes (only if content matches a template)
|
|
614
|
+
const handleNameChange = (val: string) => {
|
|
615
|
+
const cleaned = val.replace(/[^a-z0-9-]/g, '');
|
|
616
|
+
const oldTemplate = getTemplate(newName || 'my-skill');
|
|
617
|
+
if (!newContent || newContent === oldTemplate) {
|
|
618
|
+
setNewContent(getTemplate(cleaned || 'my-skill'));
|
|
619
|
+
}
|
|
620
|
+
setNewName(cleaned);
|
|
621
|
+
};
|
|
622
|
+
|
|
623
|
+
const handleTemplateChange = (tmpl: 'general' | 'tool-use' | 'workflow') => {
|
|
624
|
+
const oldTemplate = getTemplate(newName || 'my-skill', selectedTemplate);
|
|
625
|
+
setSelectedTemplate(tmpl);
|
|
626
|
+
// Only replace content if it matches the old template (user hasn't customized)
|
|
627
|
+
if (!newContent || newContent === oldTemplate) {
|
|
628
|
+
setNewContent(getTemplate(newName || 'my-skill', tmpl));
|
|
629
|
+
}
|
|
630
|
+
};
|
|
631
|
+
|
|
525
632
|
if (loading) {
|
|
526
633
|
return (
|
|
527
634
|
<div className="flex justify-center py-4">
|
|
@@ -530,8 +637,128 @@ function SkillsSection({ t }: { t: any }) {
|
|
|
530
637
|
);
|
|
531
638
|
}
|
|
532
639
|
|
|
640
|
+
const renderSkillRow = (skill: SkillInfo) => (
|
|
641
|
+
<div key={skill.name} className="border border-border rounded-lg overflow-hidden">
|
|
642
|
+
<div
|
|
643
|
+
className="flex items-center gap-2 px-3 py-2 cursor-pointer hover:bg-muted/50 transition-colors"
|
|
644
|
+
onClick={() => handleExpand(skill.name)}
|
|
645
|
+
>
|
|
646
|
+
{expanded === skill.name ? <ChevronDown size={12} /> : <ChevronRight size={12} />}
|
|
647
|
+
<span className="text-xs font-medium flex-1">{skill.name}</span>
|
|
648
|
+
<span className={`text-2xs px-1.5 py-0.5 rounded ${
|
|
649
|
+
skill.source === 'builtin' ? 'bg-blue-500/15 text-blue-500' : 'bg-purple-500/15 text-purple-500'
|
|
650
|
+
}`}>
|
|
651
|
+
{skill.source === 'builtin' ? (m?.skillBuiltin ?? 'Built-in') : (m?.skillUser ?? 'Custom')}
|
|
652
|
+
</span>
|
|
653
|
+
<button
|
|
654
|
+
onClick={e => { e.stopPropagation(); handleToggle(skill.name, !skill.enabled); }}
|
|
655
|
+
className={`relative inline-flex h-4 w-7 items-center rounded-full transition-colors ${
|
|
656
|
+
skill.enabled ? 'bg-success' : 'bg-muted-foreground/30'
|
|
657
|
+
}`}
|
|
658
|
+
>
|
|
659
|
+
<span className={`inline-block h-3 w-3 rounded-full bg-white transition-transform ${
|
|
660
|
+
skill.enabled ? 'translate-x-3.5' : 'translate-x-0.5'
|
|
661
|
+
}`} />
|
|
662
|
+
</button>
|
|
663
|
+
</div>
|
|
664
|
+
|
|
665
|
+
{expanded === skill.name && (
|
|
666
|
+
<div className="px-3 py-2 border-t border-border text-xs space-y-2 bg-muted/20">
|
|
667
|
+
<p className="text-muted-foreground">{skill.description || 'No description'}</p>
|
|
668
|
+
<p className="text-muted-foreground font-mono text-2xs">{skill.path}</p>
|
|
669
|
+
|
|
670
|
+
{/* Full content display / edit */}
|
|
671
|
+
{loadingContent === skill.name ? (
|
|
672
|
+
<div className="flex items-center gap-1.5 text-muted-foreground">
|
|
673
|
+
<Loader2 size={10} className="animate-spin" />
|
|
674
|
+
<span className="text-2xs">Loading...</span>
|
|
675
|
+
</div>
|
|
676
|
+
) : fullContent[skill.name] ? (
|
|
677
|
+
<div className="space-y-1.5">
|
|
678
|
+
<div className="flex items-center justify-between">
|
|
679
|
+
<span className="text-2xs text-muted-foreground font-medium">{m?.skillContent ?? 'Content'}</span>
|
|
680
|
+
<div className="flex items-center gap-2">
|
|
681
|
+
{skill.editable && editing !== skill.name && (
|
|
682
|
+
<button
|
|
683
|
+
onClick={() => handleEditStart(skill.name)}
|
|
684
|
+
className="flex items-center gap-1 text-2xs text-muted-foreground hover:text-foreground transition-colors"
|
|
685
|
+
>
|
|
686
|
+
<Pencil size={10} />
|
|
687
|
+
{m?.editSkill ?? 'Edit'}
|
|
688
|
+
</button>
|
|
689
|
+
)}
|
|
690
|
+
{skill.editable && (
|
|
691
|
+
<button
|
|
692
|
+
onClick={() => handleDelete(skill.name)}
|
|
693
|
+
className="flex items-center gap-1 text-2xs text-destructive hover:underline"
|
|
694
|
+
>
|
|
695
|
+
<Trash2 size={10} />
|
|
696
|
+
{m?.deleteSkill ?? 'Delete'}
|
|
697
|
+
</button>
|
|
698
|
+
)}
|
|
699
|
+
</div>
|
|
700
|
+
</div>
|
|
701
|
+
|
|
702
|
+
{editing === skill.name ? (
|
|
703
|
+
<div className="space-y-1.5">
|
|
704
|
+
<textarea
|
|
705
|
+
value={editContent}
|
|
706
|
+
onChange={e => setEditContent(e.target.value)}
|
|
707
|
+
rows={Math.min(20, (editContent.match(/\n/g) || []).length + 3)}
|
|
708
|
+
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"
|
|
709
|
+
/>
|
|
710
|
+
<div className="flex items-center gap-2">
|
|
711
|
+
<button
|
|
712
|
+
onClick={() => handleEditSave(skill.name)}
|
|
713
|
+
disabled={saving}
|
|
714
|
+
className="flex items-center gap-1 px-2.5 py-1 text-xs rounded-md disabled:opacity-40 disabled:cursor-not-allowed transition-colors"
|
|
715
|
+
style={{ background: 'var(--amber)', color: 'var(--amber-foreground)' }}
|
|
716
|
+
>
|
|
717
|
+
{saving && <Loader2 size={10} className="animate-spin" />}
|
|
718
|
+
{m?.saveSkill ?? 'Save'}
|
|
719
|
+
</button>
|
|
720
|
+
<button
|
|
721
|
+
onClick={handleEditCancel}
|
|
722
|
+
className="px-2.5 py-1 text-xs rounded-md border border-border text-muted-foreground hover:text-foreground transition-colors"
|
|
723
|
+
>
|
|
724
|
+
{m?.cancelSkill ?? 'Cancel'}
|
|
725
|
+
</button>
|
|
726
|
+
</div>
|
|
727
|
+
</div>
|
|
728
|
+
) : (
|
|
729
|
+
<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">
|
|
730
|
+
<MarkdownView content={fullContent[skill.name].replace(/^---\n[\s\S]*?\n---\n*/, '')} />
|
|
731
|
+
</div>
|
|
732
|
+
)}
|
|
733
|
+
</div>
|
|
734
|
+
) : null}
|
|
735
|
+
</div>
|
|
736
|
+
)}
|
|
737
|
+
</div>
|
|
738
|
+
);
|
|
739
|
+
|
|
533
740
|
return (
|
|
534
741
|
<div className="space-y-3 pt-2">
|
|
742
|
+
{/* Search */}
|
|
743
|
+
<div className="relative">
|
|
744
|
+
<Search size={12} className="absolute left-2.5 top-1/2 -translate-y-1/2 text-muted-foreground" />
|
|
745
|
+
<input
|
|
746
|
+
type="text"
|
|
747
|
+
value={search}
|
|
748
|
+
onChange={e => setSearch(e.target.value)}
|
|
749
|
+
placeholder={m?.searchSkills ?? 'Search skills...'}
|
|
750
|
+
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"
|
|
751
|
+
/>
|
|
752
|
+
{search && (
|
|
753
|
+
<button
|
|
754
|
+
onClick={() => setSearch('')}
|
|
755
|
+
className="absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
|
|
756
|
+
>
|
|
757
|
+
<X size={10} />
|
|
758
|
+
</button>
|
|
759
|
+
)}
|
|
760
|
+
</div>
|
|
761
|
+
|
|
535
762
|
{/* Skill language switcher */}
|
|
536
763
|
{(() => {
|
|
537
764
|
const mindosEnabled = skills.find(s => s.name === 'mindos')?.enabled ?? true;
|
|
@@ -575,56 +802,49 @@ function SkillsSection({ t }: { t: any }) {
|
|
|
575
802
|
);
|
|
576
803
|
})()}
|
|
577
804
|
|
|
578
|
-
{
|
|
579
|
-
|
|
805
|
+
{/* Empty search result */}
|
|
806
|
+
{filtered.length === 0 && search && (
|
|
807
|
+
<p className="text-xs text-muted-foreground text-center py-3">
|
|
808
|
+
{m?.noSkillsMatch ? m.noSkillsMatch(search) : `No skills match "${search}"`}
|
|
809
|
+
</p>
|
|
810
|
+
)}
|
|
811
|
+
|
|
812
|
+
{/* Custom group — always open */}
|
|
813
|
+
{customSkills.length > 0 && (
|
|
814
|
+
<div className="space-y-1.5">
|
|
815
|
+
<div className="flex items-center gap-2 text-xs font-medium text-muted-foreground">
|
|
816
|
+
<span>{m?.customGroup ?? 'Custom'} ({customSkills.length})</span>
|
|
817
|
+
</div>
|
|
818
|
+
<div className="space-y-1.5">
|
|
819
|
+
{customSkills.map(renderSkillRow)}
|
|
820
|
+
</div>
|
|
821
|
+
</div>
|
|
822
|
+
)}
|
|
823
|
+
|
|
824
|
+
{/* Built-in group — collapsible, default collapsed */}
|
|
825
|
+
{builtinSkills.length > 0 && (
|
|
826
|
+
<div className="space-y-1.5">
|
|
580
827
|
<div
|
|
581
|
-
className="flex items-center gap-
|
|
582
|
-
onClick={() =>
|
|
828
|
+
className="flex items-center gap-1.5 text-xs font-medium text-muted-foreground cursor-pointer hover:text-foreground transition-colors"
|
|
829
|
+
onClick={() => setBuiltinCollapsed(!builtinCollapsed)}
|
|
583
830
|
>
|
|
584
|
-
{
|
|
585
|
-
<span
|
|
586
|
-
<span className={`text-2xs px-1.5 py-0.5 rounded ${
|
|
587
|
-
skill.source === 'builtin' ? 'bg-blue-500/15 text-blue-500' : 'bg-purple-500/15 text-purple-500'
|
|
588
|
-
}`}>
|
|
589
|
-
{skill.source === 'builtin' ? (m?.skillBuiltin ?? 'Built-in') : (m?.skillUser ?? 'Custom')}
|
|
590
|
-
</span>
|
|
591
|
-
{/* Toggle */}
|
|
592
|
-
<button
|
|
593
|
-
onClick={e => { e.stopPropagation(); handleToggle(skill.name, !skill.enabled); }}
|
|
594
|
-
className={`relative inline-flex h-4 w-7 items-center rounded-full transition-colors ${
|
|
595
|
-
skill.enabled ? 'bg-success' : 'bg-muted-foreground/30'
|
|
596
|
-
}`}
|
|
597
|
-
>
|
|
598
|
-
<span className={`inline-block h-3 w-3 rounded-full bg-white transition-transform ${
|
|
599
|
-
skill.enabled ? 'translate-x-3.5' : 'translate-x-0.5'
|
|
600
|
-
}`} />
|
|
601
|
-
</button>
|
|
831
|
+
{builtinCollapsed ? <ChevronRight size={12} /> : <ChevronDown size={12} />}
|
|
832
|
+
<span>{m?.builtinGroup ?? 'Built-in'} ({builtinSkills.length})</span>
|
|
602
833
|
</div>
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
<p className="text-muted-foreground">{skill.description || 'No description'}</p>
|
|
607
|
-
<p className="text-muted-foreground font-mono text-2xs">{skill.path}</p>
|
|
608
|
-
{skill.editable && (
|
|
609
|
-
<button
|
|
610
|
-
onClick={() => handleDelete(skill.name)}
|
|
611
|
-
className="flex items-center gap-1 text-2xs text-destructive hover:underline"
|
|
612
|
-
>
|
|
613
|
-
<Trash2 size={10} />
|
|
614
|
-
{m?.deleteSkill ?? 'Delete'}
|
|
615
|
-
</button>
|
|
616
|
-
)}
|
|
834
|
+
{!builtinCollapsed && (
|
|
835
|
+
<div className="space-y-1.5">
|
|
836
|
+
{builtinSkills.map(renderSkillRow)}
|
|
617
837
|
</div>
|
|
618
838
|
)}
|
|
619
839
|
</div>
|
|
620
|
-
)
|
|
840
|
+
)}
|
|
621
841
|
|
|
622
|
-
{/* Add skill form */}
|
|
842
|
+
{/* Add skill form — template-based */}
|
|
623
843
|
{adding ? (
|
|
624
844
|
<div className="border border-border rounded-lg p-3 space-y-2">
|
|
625
845
|
<div className="flex items-center justify-between">
|
|
626
846
|
<span className="text-xs font-medium">{m?.addSkill ?? '+ Add Skill'}</span>
|
|
627
|
-
<button onClick={() => setAdding(false)} className="p-0.5 rounded hover:bg-muted text-muted-foreground">
|
|
847
|
+
<button onClick={() => { setAdding(false); setNewName(''); setNewContent(''); setError(''); }} className="p-0.5 rounded hover:bg-muted text-muted-foreground">
|
|
628
848
|
<X size={12} />
|
|
629
849
|
</button>
|
|
630
850
|
</div>
|
|
@@ -633,27 +853,37 @@ function SkillsSection({ t }: { t: any }) {
|
|
|
633
853
|
<input
|
|
634
854
|
type="text"
|
|
635
855
|
value={newName}
|
|
636
|
-
onChange={e =>
|
|
856
|
+
onChange={e => handleNameChange(e.target.value)}
|
|
637
857
|
placeholder="my-skill"
|
|
638
858
|
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"
|
|
639
859
|
/>
|
|
640
860
|
</div>
|
|
641
861
|
<div className="space-y-1">
|
|
642
|
-
<label className="text-2xs text-muted-foreground">{m?.
|
|
643
|
-
<
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
862
|
+
<label className="text-2xs text-muted-foreground">{m?.skillTemplate ?? 'Template'}</label>
|
|
863
|
+
<div className="flex rounded-md border border-border overflow-hidden w-fit">
|
|
864
|
+
{(['general', 'tool-use', 'workflow'] as const).map((tmpl, i) => (
|
|
865
|
+
<button
|
|
866
|
+
key={tmpl}
|
|
867
|
+
onClick={() => handleTemplateChange(tmpl)}
|
|
868
|
+
className={`px-2.5 py-1 text-xs transition-colors ${i > 0 ? 'border-l border-border' : ''} ${
|
|
869
|
+
selectedTemplate === tmpl
|
|
870
|
+
? 'bg-amber-500/15 text-amber-600 font-medium'
|
|
871
|
+
: 'text-muted-foreground hover:bg-muted'
|
|
872
|
+
}`}
|
|
873
|
+
>
|
|
874
|
+
{tmpl === 'general' ? (m?.skillTemplateGeneral ?? 'General')
|
|
875
|
+
: tmpl === 'tool-use' ? (m?.skillTemplateToolUse ?? 'Tool-use')
|
|
876
|
+
: (m?.skillTemplateWorkflow ?? 'Workflow')}
|
|
877
|
+
</button>
|
|
878
|
+
))}
|
|
879
|
+
</div>
|
|
650
880
|
</div>
|
|
651
881
|
<div className="space-y-1">
|
|
652
882
|
<label className="text-2xs text-muted-foreground">{m?.skillContent ?? 'Content'}</label>
|
|
653
883
|
<textarea
|
|
654
884
|
value={newContent}
|
|
655
885
|
onChange={e => setNewContent(e.target.value)}
|
|
656
|
-
rows={
|
|
886
|
+
rows={16}
|
|
657
887
|
placeholder="Skill instructions (markdown)..."
|
|
658
888
|
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"
|
|
659
889
|
/>
|
|
@@ -675,7 +905,7 @@ function SkillsSection({ t }: { t: any }) {
|
|
|
675
905
|
{m?.saveSkill ?? 'Save'}
|
|
676
906
|
</button>
|
|
677
907
|
<button
|
|
678
|
-
onClick={() => setAdding(false)}
|
|
908
|
+
onClick={() => { setAdding(false); setNewName(''); setNewContent(''); setError(''); }}
|
|
679
909
|
className="px-2.5 py-1 text-xs rounded-md border border-border text-muted-foreground hover:text-foreground transition-colors"
|
|
680
910
|
>
|
|
681
911
|
{m?.cancelSkill ?? 'Cancel'}
|
|
@@ -684,7 +914,7 @@ function SkillsSection({ t }: { t: any }) {
|
|
|
684
914
|
</div>
|
|
685
915
|
) : (
|
|
686
916
|
<button
|
|
687
|
-
onClick={() => setAdding(true)}
|
|
917
|
+
onClick={() => { setAdding(true); setSelectedTemplate('general'); setNewContent(getTemplate('my-skill', 'general')); }}
|
|
688
918
|
className="flex items-center gap-1.5 text-xs text-muted-foreground hover:text-foreground transition-colors"
|
|
689
919
|
>
|
|
690
920
|
<Plus size={12} />
|
package/app/lib/i18n.ts
CHANGED
|
@@ -251,6 +251,14 @@ export const messages = {
|
|
|
251
251
|
skillLanguage: 'Skill Language',
|
|
252
252
|
skillLangEn: 'English',
|
|
253
253
|
skillLangZh: '中文',
|
|
254
|
+
searchSkills: 'Search skills...',
|
|
255
|
+
customGroup: 'Custom',
|
|
256
|
+
builtinGroup: 'Built-in',
|
|
257
|
+
noSkillsMatch: (query: string) => `No skills match "${query}"`,
|
|
258
|
+
skillTemplate: 'Template',
|
|
259
|
+
skillTemplateGeneral: 'General',
|
|
260
|
+
skillTemplateToolUse: 'Tool-use',
|
|
261
|
+
skillTemplateWorkflow: 'Workflow',
|
|
254
262
|
selectDetected: 'Select Detected',
|
|
255
263
|
clearSelection: 'Clear',
|
|
256
264
|
quickSetup: 'Quick Setup',
|
|
@@ -722,6 +730,14 @@ export const messages = {
|
|
|
722
730
|
skillLanguage: 'Skill 语言',
|
|
723
731
|
skillLangEn: 'English',
|
|
724
732
|
skillLangZh: '中文',
|
|
733
|
+
searchSkills: '搜索 Skill...',
|
|
734
|
+
customGroup: '自定义',
|
|
735
|
+
builtinGroup: '内置',
|
|
736
|
+
noSkillsMatch: (query: string) => `没有匹配「${query}」的 Skill`,
|
|
737
|
+
skillTemplate: '模板',
|
|
738
|
+
skillTemplateGeneral: '通用',
|
|
739
|
+
skillTemplateToolUse: '工具调用',
|
|
740
|
+
skillTemplateWorkflow: '工作流',
|
|
725
741
|
selectDetected: '选择已检测',
|
|
726
742
|
clearSelection: '清除',
|
|
727
743
|
quickSetup: '快速配置',
|
package/app/next.config.ts
CHANGED
|
@@ -8,6 +8,13 @@ const nextConfig: NextConfig = {
|
|
|
8
8
|
turbopack: {
|
|
9
9
|
root: path.join(__dirname),
|
|
10
10
|
},
|
|
11
|
+
// Disable client-side router cache for dynamic layouts so that
|
|
12
|
+
// router.refresh() always fetches a fresh file tree from the server.
|
|
13
|
+
experimental: {
|
|
14
|
+
staleTimes: {
|
|
15
|
+
dynamic: 0,
|
|
16
|
+
},
|
|
17
|
+
},
|
|
11
18
|
};
|
|
12
19
|
|
|
13
20
|
export default nextConfig;
|