@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.
@@ -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(), description: newDesc.trim(), content: newContent }),
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
- {skills.map(skill => (
579
- <div key={skill.name} className="border border-border rounded-lg overflow-hidden">
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-2 px-3 py-2 cursor-pointer hover:bg-muted/50 transition-colors"
582
- onClick={() => setExpanded(expanded === skill.name ? null : skill.name)}
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
- {expanded === skill.name ? <ChevronDown size={12} /> : <ChevronRight size={12} />}
585
- <span className="text-xs font-medium flex-1">{skill.name}</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
- {expanded === skill.name && (
605
- <div className="px-3 py-2 border-t border-border text-xs space-y-1.5 bg-muted/20">
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 => setNewName(e.target.value.replace(/[^a-z0-9-]/g, ''))}
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?.skillDesc ?? 'Description'}</label>
643
- <input
644
- type="text"
645
- value={newDesc}
646
- onChange={e => setNewDesc(e.target.value)}
647
- placeholder="What does this skill do?"
648
- 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"
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={6}
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: '快速配置',
@@ -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;