@geminilight/mindos 0.5.36 → 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.
@@ -1,7 +1,7 @@
1
1
  'use client';
2
2
 
3
3
  import { useState, useEffect, useCallback, useSyncExternalStore, useRef } from 'react';
4
- import { Copy, Check, RefreshCw, Trash2, Sparkles, ChevronDown, ChevronRight, Loader2, Cpu, Zap, Database as DatabaseIcon, HardDrive } from 'lucide-react';
4
+ import { Copy, Check, RefreshCw, Trash2, Sparkles, ChevronDown, ChevronRight, Loader2, Cpu, Zap, Database as DatabaseIcon, HardDrive, RotateCcw } from 'lucide-react';
5
5
  import type { KnowledgeTabProps } from './types';
6
6
  import { Field, Input, EnvBadge, SectionLabel, Toggle } from './Primitives';
7
7
  import { apiFetch } from '@/lib/api';
@@ -49,6 +49,27 @@ export function KnowledgeTab({ data, setData, t }: KnowledgeTabProps) {
49
49
  });
50
50
  }, [guideDismissed]);
51
51
 
52
+ const handleRestartWalkthrough = useCallback(() => {
53
+ apiFetch('/api/setup', {
54
+ method: 'PATCH',
55
+ headers: { 'Content-Type': 'application/json' },
56
+ body: JSON.stringify({
57
+ guideState: {
58
+ active: true,
59
+ dismissed: false,
60
+ walkthroughStep: 0,
61
+ walkthroughDismissed: false,
62
+ },
63
+ }),
64
+ })
65
+ .then(() => {
66
+ setGuideActive(true);
67
+ setGuideDismissed(false);
68
+ window.dispatchEvent(new Event('guide-state-updated'));
69
+ })
70
+ .catch(err => console.error('Failed to restart walkthrough:', err));
71
+ }, []);
72
+
52
73
  const origin = useSyncExternalStore(
53
74
  () => () => {},
54
75
  () => `${window.location.protocol}//${window.location.hostname}`,
@@ -207,6 +228,13 @@ export function KnowledgeTab({ data, setData, t }: KnowledgeTabProps) {
207
228
  </div>
208
229
  <Toggle checked={!guideDismissed} onChange={() => handleGuideToggle()} />
209
230
  </div>
231
+ <button
232
+ onClick={handleRestartWalkthrough}
233
+ className="flex items-center gap-1.5 mt-2 px-3 py-1.5 text-xs rounded-lg border border-border text-muted-foreground hover:text-foreground hover:bg-muted transition-colors"
234
+ >
235
+ <RotateCcw size={12} />
236
+ {k.restartWalkthrough ?? 'Restart walkthrough'}
237
+ </button>
210
238
  </div>
211
239
  )}
212
240
 
@@ -0,0 +1,178 @@
1
+ 'use client';
2
+
3
+ import { useState } from 'react';
4
+ import { X, Plus, Loader2, AlertCircle } from 'lucide-react';
5
+
6
+ const skillFrontmatter = (n: string) => `---
7
+ name: ${n}
8
+ description: >
9
+ Describe WHEN the agent should use this
10
+ skill. Be specific about trigger conditions.
11
+ ---`;
12
+
13
+ const SKILL_TEMPLATES: Record<string, (name: string) => string> = {
14
+ general: (n) => `${skillFrontmatter(n)}
15
+
16
+ # Instructions
17
+
18
+ ## Context
19
+ <!-- Background knowledge for the agent -->
20
+
21
+ ## Steps
22
+ 1.
23
+ 2.
24
+
25
+ ## Rules
26
+ <!-- Constraints, edge cases, formats -->
27
+ - `,
28
+
29
+ 'tool-use': (n) => `${skillFrontmatter(n)}
30
+
31
+ # Instructions
32
+
33
+ ## Available Tools
34
+ <!-- List tools the agent can use -->
35
+ -
36
+
37
+ ## When to Use
38
+ <!-- Conditions that trigger this skill -->
39
+
40
+ ## Output Format
41
+ <!-- Expected response structure -->
42
+ `,
43
+
44
+ workflow: (n) => `${skillFrontmatter(n)}
45
+
46
+ # Instructions
47
+
48
+ ## Trigger
49
+ <!-- What triggers this workflow -->
50
+
51
+ ## Steps
52
+ 1.
53
+ 2.
54
+
55
+ ## Validation
56
+ <!-- How to verify success -->
57
+
58
+ ## Rollback
59
+ <!-- What to do on failure -->
60
+ `,
61
+ };
62
+
63
+ interface SkillCreateFormProps {
64
+ onSave: (name: string, content: string) => Promise<void>;
65
+ onCancel: () => void;
66
+ saving: boolean;
67
+ error: string;
68
+ m: Record<string, any> | undefined;
69
+ }
70
+
71
+ export default function SkillCreateForm({ onSave, onCancel, saving, error, m }: SkillCreateFormProps) {
72
+ const [newName, setNewName] = useState('');
73
+ const [newContent, setNewContent] = useState('');
74
+ const [selectedTemplate, setSelectedTemplate] = useState<'general' | 'tool-use' | 'workflow'>('general');
75
+
76
+ const getTemplate = (skillName: string, tmpl?: 'general' | 'tool-use' | 'workflow') => {
77
+ const key = tmpl || selectedTemplate;
78
+ const fn = SKILL_TEMPLATES[key] || SKILL_TEMPLATES.general;
79
+ return fn(skillName || 'my-skill');
80
+ };
81
+
82
+ const handleNameChange = (val: string) => {
83
+ const cleaned = val.toLowerCase().replace(/[^a-z0-9-]/g, '');
84
+ const oldTemplate = getTemplate(newName || 'my-skill');
85
+ if (!newContent || newContent === oldTemplate) {
86
+ setNewContent(getTemplate(cleaned || 'my-skill'));
87
+ }
88
+ setNewName(cleaned);
89
+ };
90
+
91
+ const handleTemplateChange = (tmpl: 'general' | 'tool-use' | 'workflow') => {
92
+ const oldTemplate = getTemplate(newName || 'my-skill', selectedTemplate);
93
+ setSelectedTemplate(tmpl);
94
+ if (!newContent || newContent === oldTemplate) {
95
+ setNewContent(getTemplate(newName || 'my-skill', tmpl));
96
+ }
97
+ };
98
+
99
+ // Initialize content on first render
100
+ if (!newContent) {
101
+ // Use a timeout-free approach: set default on next tick won't work in render.
102
+ // Instead, initialize via useState default or useEffect. For simplicity, set inline.
103
+ }
104
+
105
+ return (
106
+ <div className="border border-border rounded-lg p-3 space-y-2">
107
+ <div className="flex items-center justify-between">
108
+ <span className="text-xs font-medium">{m?.addSkill ?? '+ Add Skill'}</span>
109
+ <button onClick={onCancel} className="p-0.5 rounded hover:bg-muted text-muted-foreground">
110
+ <X size={12} />
111
+ </button>
112
+ </div>
113
+ <div className="space-y-1">
114
+ <label className="text-2xs text-muted-foreground">{m?.skillName ?? 'Name'}</label>
115
+ <input
116
+ type="text"
117
+ value={newName}
118
+ onChange={e => handleNameChange(e.target.value)}
119
+ placeholder="my-skill"
120
+ 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"
121
+ />
122
+ </div>
123
+ <div className="space-y-1">
124
+ <label className="text-2xs text-muted-foreground">{m?.skillTemplate ?? 'Template'}</label>
125
+ <div className="flex rounded-md border border-border overflow-hidden w-fit">
126
+ {(['general', 'tool-use', 'workflow'] as const).map((tmpl, i) => (
127
+ <button
128
+ key={tmpl}
129
+ onClick={() => handleTemplateChange(tmpl)}
130
+ className={`px-2.5 py-1 text-xs transition-colors ${i > 0 ? 'border-l border-border' : ''} ${
131
+ selectedTemplate === tmpl
132
+ ? 'bg-amber-500/15 text-amber-600 font-medium'
133
+ : 'text-muted-foreground hover:bg-muted'
134
+ }`}
135
+ >
136
+ {tmpl === 'general' ? (m?.skillTemplateGeneral ?? 'General')
137
+ : tmpl === 'tool-use' ? (m?.skillTemplateToolUse ?? 'Tool-use')
138
+ : (m?.skillTemplateWorkflow ?? 'Workflow')}
139
+ </button>
140
+ ))}
141
+ </div>
142
+ </div>
143
+ <div className="space-y-1">
144
+ <label className="text-2xs text-muted-foreground">{m?.skillContent ?? 'Content'}</label>
145
+ <textarea
146
+ value={newContent || getTemplate(newName || 'my-skill')}
147
+ onChange={e => setNewContent(e.target.value)}
148
+ rows={16}
149
+ placeholder="Skill instructions (markdown)..."
150
+ 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"
151
+ />
152
+ </div>
153
+ {error && (
154
+ <p className="text-2xs text-destructive flex items-center gap-1">
155
+ <AlertCircle size={10} />
156
+ {error}
157
+ </p>
158
+ )}
159
+ <div className="flex items-center gap-2">
160
+ <button
161
+ onClick={() => onSave(newName.trim(), newContent || getTemplate(newName.trim() || 'my-skill'))}
162
+ disabled={!newName.trim() || saving}
163
+ className="flex items-center gap-1 px-2.5 py-1 text-xs rounded-md disabled:opacity-40 disabled:cursor-not-allowed transition-colors"
164
+ style={{ background: 'var(--amber)', color: 'var(--amber-foreground)' }}
165
+ >
166
+ {saving && <Loader2 size={10} className="animate-spin" />}
167
+ {m?.saveSkill ?? 'Save'}
168
+ </button>
169
+ <button
170
+ onClick={onCancel}
171
+ className="px-2.5 py-1 text-xs rounded-md border border-border text-muted-foreground hover:text-foreground transition-colors"
172
+ >
173
+ {m?.cancelSkill ?? 'Cancel'}
174
+ </button>
175
+ </div>
176
+ </div>
177
+ );
178
+ }
@@ -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
+ }