@geminilight/mindos 0.5.19 → 0.5.21
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/ask/route.ts +308 -172
- package/app/app/api/file/route.ts +35 -11
- package/app/app/api/skills/route.ts +22 -3
- package/app/components/SettingsModal.tsx +52 -58
- package/app/components/Sidebar.tsx +21 -1
- package/app/components/settings/AiTab.tsx +4 -25
- package/app/components/settings/AppearanceTab.tsx +31 -13
- package/app/components/settings/KnowledgeTab.tsx +13 -28
- package/app/components/settings/McpAgentInstall.tsx +227 -0
- package/app/components/settings/McpServerStatus.tsx +172 -0
- package/app/components/settings/McpSkillsSection.tsx +583 -0
- package/app/components/settings/McpTab.tsx +16 -728
- package/app/components/settings/PluginsTab.tsx +4 -27
- package/app/components/settings/Primitives.tsx +69 -0
- package/app/components/settings/ShortcutsTab.tsx +2 -4
- package/app/components/settings/SyncTab.tsx +8 -24
- package/app/components/settings/types.ts +116 -2
- package/app/lib/agent/context.ts +151 -87
- package/app/lib/agent/index.ts +4 -3
- package/app/lib/agent/model.ts +76 -10
- package/app/lib/agent/stream-consumer.ts +73 -77
- package/app/lib/agent/to-agent-messages.ts +106 -0
- package/app/lib/agent/tools.ts +260 -266
- package/app/lib/i18n-en.ts +480 -0
- package/app/lib/i18n-zh.ts +505 -0
- package/app/lib/i18n.ts +4 -947
- package/app/next-env.d.ts +1 -1
- package/app/next.config.ts +7 -0
- package/app/package-lock.json +3258 -3093
- package/app/package.json +6 -3
- package/bin/cli.js +140 -5
- package/package.json +4 -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,5 +1,6 @@
|
|
|
1
1
|
export const dynamic = 'force-dynamic';
|
|
2
2
|
import { NextRequest, NextResponse } from 'next/server';
|
|
3
|
+
import { revalidatePath } from 'next/cache';
|
|
3
4
|
import {
|
|
4
5
|
getFileContent,
|
|
5
6
|
saveFileContent,
|
|
@@ -37,6 +38,9 @@ export async function GET(req: NextRequest) {
|
|
|
37
38
|
}
|
|
38
39
|
}
|
|
39
40
|
|
|
41
|
+
// Ops that change file tree structure (sidebar needs refresh)
|
|
42
|
+
const TREE_CHANGING_OPS = new Set(['create_file', 'delete_file', 'rename_file', 'move_file']);
|
|
43
|
+
|
|
40
44
|
// POST /api/file body: { op, path, ...params }
|
|
41
45
|
export async function POST(req: NextRequest) {
|
|
42
46
|
let body: Record<string, unknown>;
|
|
@@ -47,20 +51,24 @@ export async function POST(req: NextRequest) {
|
|
|
47
51
|
if (!filePath || typeof filePath !== 'string') return err('missing path');
|
|
48
52
|
|
|
49
53
|
try {
|
|
54
|
+
let resp: NextResponse;
|
|
55
|
+
|
|
50
56
|
switch (op) {
|
|
51
57
|
|
|
52
58
|
case 'save_file': {
|
|
53
59
|
const { content } = params as { content: string };
|
|
54
60
|
if (typeof content !== 'string') return err('missing content');
|
|
55
61
|
saveFileContent(filePath, content);
|
|
56
|
-
|
|
62
|
+
resp = NextResponse.json({ ok: true });
|
|
63
|
+
break;
|
|
57
64
|
}
|
|
58
65
|
|
|
59
66
|
case 'append_to_file': {
|
|
60
67
|
const { content } = params as { content: string };
|
|
61
68
|
if (typeof content !== 'string') return err('missing content');
|
|
62
69
|
appendToFile(filePath, content);
|
|
63
|
-
|
|
70
|
+
resp = NextResponse.json({ ok: true });
|
|
71
|
+
break;
|
|
64
72
|
}
|
|
65
73
|
|
|
66
74
|
case 'insert_lines': {
|
|
@@ -68,7 +76,8 @@ export async function POST(req: NextRequest) {
|
|
|
68
76
|
if (typeof after_index !== 'number') return err('missing after_index');
|
|
69
77
|
if (!Array.isArray(lines)) return err('lines must be array');
|
|
70
78
|
insertLines(filePath, after_index, lines);
|
|
71
|
-
|
|
79
|
+
resp = NextResponse.json({ ok: true });
|
|
80
|
+
break;
|
|
72
81
|
}
|
|
73
82
|
|
|
74
83
|
case 'update_lines': {
|
|
@@ -78,7 +87,8 @@ export async function POST(req: NextRequest) {
|
|
|
78
87
|
if (start < 0 || end < 0) return err('start/end must be >= 0');
|
|
79
88
|
if (start > end) return err('start must be <= end');
|
|
80
89
|
updateLines(filePath, start, end, lines);
|
|
81
|
-
|
|
90
|
+
resp = NextResponse.json({ ok: true });
|
|
91
|
+
break;
|
|
82
92
|
}
|
|
83
93
|
|
|
84
94
|
case 'insert_after_heading': {
|
|
@@ -86,7 +96,8 @@ export async function POST(req: NextRequest) {
|
|
|
86
96
|
if (typeof heading !== 'string') return err('missing heading');
|
|
87
97
|
if (typeof content !== 'string') return err('missing content');
|
|
88
98
|
insertAfterHeading(filePath, heading, content);
|
|
89
|
-
|
|
99
|
+
resp = NextResponse.json({ ok: true });
|
|
100
|
+
break;
|
|
90
101
|
}
|
|
91
102
|
|
|
92
103
|
case 'update_section': {
|
|
@@ -94,44 +105,57 @@ export async function POST(req: NextRequest) {
|
|
|
94
105
|
if (typeof heading !== 'string') return err('missing heading');
|
|
95
106
|
if (typeof content !== 'string') return err('missing content');
|
|
96
107
|
updateSection(filePath, heading, content);
|
|
97
|
-
|
|
108
|
+
resp = NextResponse.json({ ok: true });
|
|
109
|
+
break;
|
|
98
110
|
}
|
|
99
111
|
|
|
100
112
|
case 'delete_file': {
|
|
101
113
|
deleteFile(filePath);
|
|
102
|
-
|
|
114
|
+
resp = NextResponse.json({ ok: true });
|
|
115
|
+
break;
|
|
103
116
|
}
|
|
104
117
|
|
|
105
118
|
case 'rename_file': {
|
|
106
119
|
const { new_name } = params as { new_name: string };
|
|
107
120
|
if (typeof new_name !== 'string' || !new_name) return err('missing new_name');
|
|
108
121
|
const newPath = renameFile(filePath, new_name);
|
|
109
|
-
|
|
122
|
+
resp = NextResponse.json({ ok: true, newPath });
|
|
123
|
+
break;
|
|
110
124
|
}
|
|
111
125
|
|
|
112
126
|
case 'create_file': {
|
|
113
127
|
const { content } = params as { content?: string };
|
|
114
128
|
createFile(filePath, typeof content === 'string' ? content : '');
|
|
115
|
-
|
|
129
|
+
resp = NextResponse.json({ ok: true });
|
|
130
|
+
break;
|
|
116
131
|
}
|
|
117
132
|
|
|
118
133
|
case 'move_file': {
|
|
119
134
|
const { to_path } = params as { to_path: string };
|
|
120
135
|
if (typeof to_path !== 'string' || !to_path) return err('missing to_path');
|
|
121
136
|
const result = moveFile(filePath, to_path);
|
|
122
|
-
|
|
137
|
+
resp = NextResponse.json({ ok: true, ...result });
|
|
138
|
+
break;
|
|
123
139
|
}
|
|
124
140
|
|
|
125
141
|
case 'append_csv': {
|
|
126
142
|
const { row } = params as { row: string[] };
|
|
127
143
|
if (!Array.isArray(row) || row.length === 0) return err('row must be non-empty array');
|
|
128
144
|
const result = appendCsvRow(filePath, row);
|
|
129
|
-
|
|
145
|
+
resp = NextResponse.json({ ok: true, ...result });
|
|
146
|
+
break;
|
|
130
147
|
}
|
|
131
148
|
|
|
132
149
|
default:
|
|
133
150
|
return err(`unknown op: ${op}`);
|
|
134
151
|
}
|
|
152
|
+
|
|
153
|
+
// Invalidate Next.js router cache so sidebar file tree updates
|
|
154
|
+
if (TREE_CHANGING_OPS.has(op)) {
|
|
155
|
+
try { revalidatePath('/', 'layout'); } catch { /* noop in test env */ }
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return resp;
|
|
135
159
|
} catch (e) {
|
|
136
160
|
return err((e as Error).message, 500);
|
|
137
161
|
}
|
|
@@ -129,7 +129,7 @@ export async function POST(req: NextRequest) {
|
|
|
129
129
|
try {
|
|
130
130
|
const body = await req.json();
|
|
131
131
|
const { action, name, description, content, enabled } = body as {
|
|
132
|
-
action: 'create' | 'update' | 'delete' | 'toggle';
|
|
132
|
+
action: 'create' | 'update' | 'delete' | 'toggle' | 'read';
|
|
133
133
|
name?: string;
|
|
134
134
|
description?: string;
|
|
135
135
|
content?: string;
|
|
@@ -172,8 +172,11 @@ export async function POST(req: NextRequest) {
|
|
|
172
172
|
return NextResponse.json({ error: 'A skill with this name already exists' }, { status: 409 });
|
|
173
173
|
}
|
|
174
174
|
fs.mkdirSync(skillDir, { recursive: true });
|
|
175
|
-
|
|
176
|
-
|
|
175
|
+
// If content already has frontmatter, use it as-is; otherwise build frontmatter
|
|
176
|
+
const fileContent = content && content.trimStart().startsWith('---')
|
|
177
|
+
? content
|
|
178
|
+
: `---\nname: ${name}\ndescription: ${description || name}\n---\n\n${content || ''}`;
|
|
179
|
+
fs.writeFileSync(path.join(skillDir, 'SKILL.md'), fileContent, 'utf-8');
|
|
177
180
|
return NextResponse.json({ ok: true });
|
|
178
181
|
}
|
|
179
182
|
|
|
@@ -199,6 +202,22 @@ export async function POST(req: NextRequest) {
|
|
|
199
202
|
return NextResponse.json({ ok: true });
|
|
200
203
|
}
|
|
201
204
|
|
|
205
|
+
case 'read': {
|
|
206
|
+
if (!name) return NextResponse.json({ error: 'name required' }, { status: 400 });
|
|
207
|
+
const dirs = [
|
|
208
|
+
path.join(PROJECT_ROOT, 'app', 'data', 'skills', name),
|
|
209
|
+
path.join(PROJECT_ROOT, 'skills', name),
|
|
210
|
+
path.join(userSkillsDir, name),
|
|
211
|
+
];
|
|
212
|
+
for (const dir of dirs) {
|
|
213
|
+
const file = path.join(dir, 'SKILL.md');
|
|
214
|
+
if (fs.existsSync(file)) {
|
|
215
|
+
return NextResponse.json({ content: fs.readFileSync(file, 'utf-8') });
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return NextResponse.json({ error: 'Skill not found' }, { status: 404 });
|
|
219
|
+
}
|
|
220
|
+
|
|
202
221
|
default:
|
|
203
222
|
return NextResponse.json({ error: `Unknown action: ${action}` }, { status: 400 });
|
|
204
223
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { useEffect, useState, useCallback } from 'react';
|
|
4
|
-
import { X, Settings,
|
|
3
|
+
import { useEffect, useState, useCallback, useRef } from 'react';
|
|
4
|
+
import { X, Settings, Loader2, AlertCircle, CheckCircle2, RotateCcw, Sparkles, Palette, Database, RefreshCw, Plug, Puzzle } from 'lucide-react';
|
|
5
5
|
import { useLocale } from '@/lib/LocaleContext';
|
|
6
6
|
import { getAllRenderers, loadDisabledState, isRendererEnabled } from '@/lib/renderers/registry';
|
|
7
7
|
import { apiFetch } from '@/lib/api';
|
|
@@ -12,7 +12,6 @@ import { AiTab } from './settings/AiTab';
|
|
|
12
12
|
import { AppearanceTab } from './settings/AppearanceTab';
|
|
13
13
|
import { KnowledgeTab } from './settings/KnowledgeTab';
|
|
14
14
|
import { PluginsTab } from './settings/PluginsTab';
|
|
15
|
-
import { ShortcutsTab } from './settings/ShortcutsTab';
|
|
16
15
|
import { SyncTab } from './settings/SyncTab';
|
|
17
16
|
import { McpTab } from './settings/McpTab';
|
|
18
17
|
|
|
@@ -28,6 +27,8 @@ export default function SettingsModal({ open, onClose, initialTab }: SettingsMod
|
|
|
28
27
|
const [saving, setSaving] = useState(false);
|
|
29
28
|
const [status, setStatus] = useState<'idle' | 'saved' | 'error' | 'load-error'>('idle');
|
|
30
29
|
const { t, locale, setLocale } = useLocale();
|
|
30
|
+
const saveTimer = useRef<ReturnType<typeof setTimeout>>(undefined);
|
|
31
|
+
const dataLoaded = useRef(false);
|
|
31
32
|
|
|
32
33
|
// Appearance state (localStorage-based)
|
|
33
34
|
const [font, setFont] = useState('lora');
|
|
@@ -37,8 +38,8 @@ export default function SettingsModal({ open, onClose, initialTab }: SettingsMod
|
|
|
37
38
|
const [pluginStates, setPluginStates] = useState<Record<string, boolean>>({});
|
|
38
39
|
|
|
39
40
|
useEffect(() => {
|
|
40
|
-
if (!open) return;
|
|
41
|
-
apiFetch<SettingsData>('/api/settings').then(setData).catch(() => setStatus('load-error'));
|
|
41
|
+
if (!open) { dataLoaded.current = false; return; }
|
|
42
|
+
apiFetch<SettingsData>('/api/settings').then(d => { setData(d); dataLoaded.current = true; }).catch(() => setStatus('load-error'));
|
|
42
43
|
setFont(localStorage.getItem('prose-font') ?? 'lora');
|
|
43
44
|
setContentWidth(localStorage.getItem('content-width') ?? '780px');
|
|
44
45
|
const stored = localStorage.getItem('theme');
|
|
@@ -81,14 +82,14 @@ export default function SettingsModal({ open, onClose, initialTab }: SettingsMod
|
|
|
81
82
|
return () => window.removeEventListener('keydown', handler);
|
|
82
83
|
}, [open, onClose]);
|
|
83
84
|
|
|
84
|
-
|
|
85
|
-
|
|
85
|
+
// Auto-save with debounce when data changes
|
|
86
|
+
const doSave = useCallback(async (d: SettingsData) => {
|
|
86
87
|
setSaving(true);
|
|
87
88
|
try {
|
|
88
89
|
await apiFetch('/api/settings', {
|
|
89
90
|
method: 'POST',
|
|
90
91
|
headers: { 'Content-Type': 'application/json' },
|
|
91
|
-
body: JSON.stringify({ ai:
|
|
92
|
+
body: JSON.stringify({ ai: d.ai, agent: d.agent, mindRoot: d.mindRoot, webPassword: d.webPassword, authToken: d.authToken }),
|
|
92
93
|
});
|
|
93
94
|
setStatus('saved');
|
|
94
95
|
setTimeout(() => setStatus('idle'), 2500);
|
|
@@ -98,7 +99,14 @@ export default function SettingsModal({ open, onClose, initialTab }: SettingsMod
|
|
|
98
99
|
} finally {
|
|
99
100
|
setSaving(false);
|
|
100
101
|
}
|
|
101
|
-
}, [
|
|
102
|
+
}, []);
|
|
103
|
+
|
|
104
|
+
useEffect(() => {
|
|
105
|
+
if (!data || !dataLoaded.current) return;
|
|
106
|
+
clearTimeout(saveTimer.current);
|
|
107
|
+
saveTimer.current = setTimeout(() => doSave(data), 800);
|
|
108
|
+
return () => clearTimeout(saveTimer.current);
|
|
109
|
+
}, [data, doSave]);
|
|
102
110
|
|
|
103
111
|
const updateAi = useCallback((patch: Partial<AiSettings>) => {
|
|
104
112
|
setData(d => d ? { ...d, ai: { ...d.ai, ...patch } } : d);
|
|
@@ -117,36 +125,28 @@ export default function SettingsModal({ open, onClose, initialTab }: SettingsMod
|
|
|
117
125
|
openai: { apiKey: '', model: '', baseUrl: '' },
|
|
118
126
|
},
|
|
119
127
|
};
|
|
128
|
+
// Set defaults — auto-save will persist them
|
|
120
129
|
setData(d => d ? { ...d, ai: defaults } : d);
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
setStatus('saved');
|
|
129
|
-
} catch {
|
|
130
|
-
setStatus('error');
|
|
131
|
-
} finally {
|
|
132
|
-
setSaving(false);
|
|
133
|
-
}
|
|
134
|
-
apiFetch<SettingsData>('/api/settings').then(setData).catch(() => setStatus('error'));
|
|
135
|
-
setTimeout(() => setStatus('idle'), 2500);
|
|
130
|
+
// 🟢 MINOR #4: Refetch after auto-save completes (800ms debounce + 500ms save operation)
|
|
131
|
+
// Rather than magic 1200ms, wait for save to finish before refetching env-resolved values
|
|
132
|
+
const DEBOUNCE_DELAY = 800;
|
|
133
|
+
const SAVE_OPERATION_TIME = 500;
|
|
134
|
+
setTimeout(() => {
|
|
135
|
+
apiFetch<SettingsData>('/api/settings').then(d => { setData(d); }).catch(() => setStatus('error'));
|
|
136
|
+
}, DEBOUNCE_DELAY + SAVE_OPERATION_TIME);
|
|
136
137
|
}, [data]);
|
|
137
138
|
|
|
138
139
|
if (!open) return null;
|
|
139
140
|
|
|
140
141
|
const env = data?.envOverrides ?? {};
|
|
141
142
|
|
|
142
|
-
const TABS: { id: Tab; label: string }[] = [
|
|
143
|
-
{ id: 'ai', label: t.settings.tabs.ai },
|
|
144
|
-
{ id: 'appearance', label: t.settings.tabs.appearance },
|
|
145
|
-
{ id: 'knowledge', label: t.settings.tabs.knowledge },
|
|
146
|
-
{ id: 'sync', label: t.settings.tabs.sync ?? 'Sync' },
|
|
147
|
-
{ id: 'mcp', label: t.settings.tabs.mcp ?? 'MCP' },
|
|
148
|
-
{ id: 'plugins', label: t.settings.tabs.plugins },
|
|
149
|
-
{ id: 'shortcuts', label: t.settings.tabs.shortcuts },
|
|
143
|
+
const TABS: { id: Tab; label: string; icon: React.ReactNode }[] = [
|
|
144
|
+
{ id: 'ai', label: t.settings.tabs.ai, icon: <Sparkles size={13} /> },
|
|
145
|
+
{ id: 'appearance', label: t.settings.tabs.appearance, icon: <Palette size={13} /> },
|
|
146
|
+
{ id: 'knowledge', label: t.settings.tabs.knowledge, icon: <Database size={13} /> },
|
|
147
|
+
{ id: 'sync', label: t.settings.tabs.sync ?? 'Sync', icon: <RefreshCw size={13} /> },
|
|
148
|
+
{ id: 'mcp', label: t.settings.tabs.mcp ?? 'MCP', icon: <Plug size={13} /> },
|
|
149
|
+
{ id: 'plugins', label: t.settings.tabs.plugins, icon: <Puzzle size={13} /> },
|
|
150
150
|
];
|
|
151
151
|
|
|
152
152
|
return (
|
|
@@ -177,12 +177,13 @@ export default function SettingsModal({ open, onClose, initialTab }: SettingsMod
|
|
|
177
177
|
<button
|
|
178
178
|
key={t.id}
|
|
179
179
|
onClick={() => setTab(t.id)}
|
|
180
|
-
className={`px-3 py-2.5 text-xs font-medium transition-colors border-b-2 -mb-px whitespace-nowrap ${
|
|
180
|
+
className={`flex items-center gap-1.5 px-3 py-2.5 text-xs font-medium transition-colors border-b-2 -mb-px whitespace-nowrap ${
|
|
181
181
|
tab === t.id
|
|
182
182
|
? 'border-amber-500 text-foreground'
|
|
183
183
|
: 'border-transparent text-muted-foreground hover:text-foreground'
|
|
184
184
|
}`}
|
|
185
185
|
>
|
|
186
|
+
{t.icon}
|
|
186
187
|
{t.label}
|
|
187
188
|
</button>
|
|
188
189
|
))}
|
|
@@ -196,7 +197,7 @@ export default function SettingsModal({ open, onClose, initialTab }: SettingsMod
|
|
|
196
197
|
<p className="text-sm text-destructive font-medium">Failed to load settings</p>
|
|
197
198
|
<p className="text-xs text-muted-foreground">Check that the server is running and AUTH_TOKEN is configured correctly.</p>
|
|
198
199
|
</div>
|
|
199
|
-
) : !data && tab !== '
|
|
200
|
+
) : !data && tab !== 'appearance' && tab !== 'mcp' && tab !== 'sync' ? (
|
|
200
201
|
<div className="flex justify-center py-8">
|
|
201
202
|
<Loader2 size={18} className="animate-spin text-muted-foreground" />
|
|
202
203
|
</div>
|
|
@@ -206,54 +207,47 @@ export default function SettingsModal({ open, onClose, initialTab }: SettingsMod
|
|
|
206
207
|
{tab === 'appearance' && <AppearanceTab font={font} setFont={setFont} contentWidth={contentWidth} setContentWidth={setContentWidth} dark={dark} setDark={setDark} locale={locale} setLocale={setLocale} t={t} />}
|
|
207
208
|
{tab === 'knowledge' && data && <KnowledgeTab data={data} setData={setData} t={t} />}
|
|
208
209
|
{tab === 'plugins' && <PluginsTab pluginStates={pluginStates} setPluginStates={setPluginStates} t={t} />}
|
|
209
|
-
{tab === 'shortcuts' && <ShortcutsTab t={t} />}
|
|
210
210
|
{tab === 'sync' && <SyncTab t={t} />}
|
|
211
211
|
{tab === 'mcp' && <McpTab t={t} />}
|
|
212
212
|
</>
|
|
213
213
|
)}
|
|
214
214
|
</div>
|
|
215
215
|
|
|
216
|
-
{/* Footer */}
|
|
216
|
+
{/* Footer — status bar + contextual actions */}
|
|
217
217
|
{(tab === 'ai' || tab === 'knowledge') && (
|
|
218
|
-
<div className="px-5 py-
|
|
218
|
+
<div className="px-5 py-2.5 border-t border-border shrink-0 flex items-center justify-between">
|
|
219
219
|
<div className="flex items-center gap-3">
|
|
220
220
|
{tab === 'ai' && Object.values(env).some(Boolean) && (
|
|
221
221
|
<button
|
|
222
222
|
onClick={restoreFromEnv}
|
|
223
223
|
disabled={saving || !data}
|
|
224
|
-
className="flex items-center gap-1.5 px-
|
|
224
|
+
className="flex items-center gap-1.5 px-3 py-1 text-xs rounded-lg border border-border text-muted-foreground hover:text-foreground hover:bg-muted disabled:opacity-40 disabled:cursor-not-allowed transition-colors"
|
|
225
225
|
>
|
|
226
|
-
<RotateCcw size={
|
|
226
|
+
<RotateCcw size={12} />
|
|
227
227
|
{t.settings.ai.restoreFromEnv}
|
|
228
228
|
</button>
|
|
229
229
|
)}
|
|
230
230
|
{tab === 'knowledge' && (
|
|
231
231
|
<a
|
|
232
232
|
href="/setup?force=1"
|
|
233
|
-
className="flex items-center gap-1.5 px-
|
|
233
|
+
className="flex items-center gap-1.5 px-3 py-1 text-xs rounded-lg border border-border text-muted-foreground hover:text-foreground hover:bg-muted transition-colors"
|
|
234
234
|
>
|
|
235
|
-
<RotateCcw size={
|
|
235
|
+
<RotateCcw size={12} />
|
|
236
236
|
{t.settings.reconfigure}
|
|
237
237
|
</a>
|
|
238
238
|
)}
|
|
239
|
-
<div className="flex items-center gap-1.5 text-xs">
|
|
240
|
-
{status === 'saved' && (
|
|
241
|
-
<><CheckCircle2 size={13} className="text-success" /><span className="text-success">{t.settings.saved}</span></>
|
|
242
|
-
)}
|
|
243
|
-
{status === 'error' && (
|
|
244
|
-
<><AlertCircle size={13} className="text-destructive" /><span className="text-destructive">{t.settings.saveFailed}</span></>
|
|
245
|
-
)}
|
|
246
|
-
</div>
|
|
247
239
|
</div>
|
|
248
|
-
<
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
{
|
|
256
|
-
|
|
240
|
+
<div className="flex items-center gap-1.5 text-xs" role="status" aria-live="polite">
|
|
241
|
+
{saving && (
|
|
242
|
+
<><Loader2 size={12} className="animate-spin text-muted-foreground" /><span className="text-muted-foreground">{t.settings.save}...</span></>
|
|
243
|
+
)}
|
|
244
|
+
{status === 'saved' && (
|
|
245
|
+
<><CheckCircle2 size={12} className="text-success" /><span className="text-success">{t.settings.saved}</span></>
|
|
246
|
+
)}
|
|
247
|
+
{status === 'error' && (
|
|
248
|
+
<><AlertCircle size={12} className="text-destructive" /><span className="text-destructive">{t.settings.saveFailed}</span></>
|
|
249
|
+
)}
|
|
250
|
+
</div>
|
|
257
251
|
</div>
|
|
258
252
|
)}
|
|
259
253
|
</div>
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { useState, useEffect } from 'react';
|
|
4
4
|
import Link from 'next/link';
|
|
5
|
-
import { usePathname } from 'next/navigation';
|
|
5
|
+
import { useRouter, usePathname } from 'next/navigation';
|
|
6
6
|
import { Search, PanelLeftClose, PanelLeftOpen, Menu, X, Settings } from 'lucide-react';
|
|
7
7
|
import FileTree from './FileTree';
|
|
8
8
|
import SearchModal from './SearchModal';
|
|
@@ -45,6 +45,7 @@ export default function Sidebar({ fileTree, collapsed = false, onCollapse, onExp
|
|
|
45
45
|
const [settingsTab, setSettingsTab] = useState<Tab | undefined>(undefined);
|
|
46
46
|
const [mobileOpen, setMobileOpen] = useState(false);
|
|
47
47
|
const { t } = useLocale();
|
|
48
|
+
const router = useRouter();
|
|
48
49
|
|
|
49
50
|
// Shared sync status for collapsed dot & mobile dot
|
|
50
51
|
const { status: syncStatus } = useSyncStatus();
|
|
@@ -54,6 +55,25 @@ export default function Sidebar({ fileTree, collapsed = false, onCollapse, onExp
|
|
|
54
55
|
? pathname.slice('/view/'.length).split('/').map(decodeURIComponent).join('/')
|
|
55
56
|
: undefined;
|
|
56
57
|
|
|
58
|
+
// Refresh file tree when tab becomes visible (catches external changes from
|
|
59
|
+
// MCP agents, CLI edits, or other browser tabs) and periodically while visible.
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
const onVisible = () => {
|
|
62
|
+
if (document.visibilityState === 'visible') router.refresh();
|
|
63
|
+
};
|
|
64
|
+
document.addEventListener('visibilitychange', onVisible);
|
|
65
|
+
|
|
66
|
+
// Light periodic refresh every 30s while tab is visible
|
|
67
|
+
const interval = setInterval(() => {
|
|
68
|
+
if (document.visibilityState === 'visible') router.refresh();
|
|
69
|
+
}, 30_000);
|
|
70
|
+
|
|
71
|
+
return () => {
|
|
72
|
+
document.removeEventListener('visibilitychange', onVisible);
|
|
73
|
+
clearInterval(interval);
|
|
74
|
+
};
|
|
75
|
+
}, [router]);
|
|
76
|
+
|
|
57
77
|
useEffect(() => {
|
|
58
78
|
const handler = (e: KeyboardEvent) => {
|
|
59
79
|
if ((e.metaKey || e.ctrlKey) && e.key === 'k') { e.preventDefault(); setSearchOpen(v => !v); }
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
import { useState, useRef, useCallback, useEffect } from 'react';
|
|
4
4
|
import { AlertCircle, Loader2 } from 'lucide-react';
|
|
5
|
-
import type { AiSettings, AgentSettings, ProviderConfig, SettingsData } from './types';
|
|
6
|
-
import { Field, Select, Input, EnvBadge, ApiKeyInput } from './Primitives';
|
|
5
|
+
import type { AiSettings, AgentSettings, ProviderConfig, SettingsData, AiTabProps } from './types';
|
|
6
|
+
import { Field, Select, Input, EnvBadge, ApiKeyInput, Toggle } from './Primitives';
|
|
7
7
|
|
|
8
8
|
type TestState = 'idle' | 'testing' | 'ok' | 'error';
|
|
9
9
|
type ErrorCode = 'auth_error' | 'model_not_found' | 'rate_limited' | 'network_error' | 'unknown';
|
|
@@ -15,7 +15,7 @@ interface TestResult {
|
|
|
15
15
|
code?: ErrorCode;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
function errorMessage(t:
|
|
18
|
+
function errorMessage(t: AiTabProps['t'], code?: ErrorCode): string {
|
|
19
19
|
switch (code) {
|
|
20
20
|
case 'auth_error': return t.settings.ai.testKeyAuthError;
|
|
21
21
|
case 'model_not_found': return t.settings.ai.testKeyModelNotFound;
|
|
@@ -25,13 +25,6 @@ function errorMessage(t: any, code?: ErrorCode): string {
|
|
|
25
25
|
}
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
interface AiTabProps {
|
|
29
|
-
data: SettingsData;
|
|
30
|
-
updateAi: (patch: Partial<AiSettings>) => void;
|
|
31
|
-
updateAgent: (patch: Partial<AgentSettings>) => void;
|
|
32
|
-
t: any;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
28
|
export function AiTab({ data, updateAi, updateAgent, t }: AiTabProps) {
|
|
36
29
|
const env = data.envOverrides ?? {};
|
|
37
30
|
const envVal = data.envValues ?? {};
|
|
@@ -262,21 +255,7 @@ export function AiTab({ data, updateAi, updateAgent, t }: AiTabProps) {
|
|
|
262
255
|
<div className="text-sm text-foreground">{t.settings.agent.thinking}</div>
|
|
263
256
|
<div className="text-xs text-muted-foreground mt-0.5">{t.settings.agent.thinkingHint}</div>
|
|
264
257
|
</div>
|
|
265
|
-
<
|
|
266
|
-
type="button"
|
|
267
|
-
role="switch"
|
|
268
|
-
aria-checked={data.agent?.enableThinking ?? false}
|
|
269
|
-
onClick={() => updateAgent({ enableThinking: !(data.agent?.enableThinking ?? false) })}
|
|
270
|
-
className={`relative inline-flex h-5 w-9 shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring ${
|
|
271
|
-
data.agent?.enableThinking ? 'bg-amber-500' : 'bg-muted'
|
|
272
|
-
}`}
|
|
273
|
-
>
|
|
274
|
-
<span
|
|
275
|
-
className={`pointer-events-none inline-block h-4 w-4 rounded-full bg-white shadow-sm transition-transform ${
|
|
276
|
-
data.agent?.enableThinking ? 'translate-x-4' : 'translate-x-0'
|
|
277
|
-
}`}
|
|
278
|
-
/>
|
|
279
|
-
</button>
|
|
258
|
+
<Toggle checked={data.agent?.enableThinking ?? false} onChange={() => updateAgent({ enableThinking: !(data.agent?.enableThinking ?? false) })} />
|
|
280
259
|
</div>
|
|
281
260
|
|
|
282
261
|
{data.agent?.enableThinking && (
|
|
@@ -1,22 +1,14 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import { ChevronDown, ChevronRight } from 'lucide-react';
|
|
3
5
|
import { Locale } from '@/lib/i18n';
|
|
4
|
-
import { CONTENT_WIDTHS, FONTS } from './types';
|
|
6
|
+
import { CONTENT_WIDTHS, FONTS, AppearanceTabProps } from './types';
|
|
5
7
|
import { Field, Select } from './Primitives';
|
|
6
8
|
|
|
7
|
-
interface AppearanceTabProps {
|
|
8
|
-
font: string;
|
|
9
|
-
setFont: (v: string) => void;
|
|
10
|
-
contentWidth: string;
|
|
11
|
-
setContentWidth: (v: string) => void;
|
|
12
|
-
dark: boolean;
|
|
13
|
-
setDark: (v: boolean) => void;
|
|
14
|
-
locale: Locale;
|
|
15
|
-
setLocale: (v: Locale) => void;
|
|
16
|
-
t: any;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
9
|
export function AppearanceTab({ font, setFont, contentWidth, setContentWidth, dark, setDark, locale, setLocale, t }: AppearanceTabProps) {
|
|
10
|
+
const [showShortcuts, setShowShortcuts] = useState(false);
|
|
11
|
+
|
|
20
12
|
return (
|
|
21
13
|
<div className="space-y-5">
|
|
22
14
|
<Field label={t.settings.appearance.readingFont}>
|
|
@@ -96,6 +88,32 @@ export function AppearanceTab({ font, setFont, contentWidth, setContentWidth, da
|
|
|
96
88
|
</Field>
|
|
97
89
|
|
|
98
90
|
<p className="text-xs text-muted-foreground">{t.settings.appearance.browserNote}</p>
|
|
91
|
+
|
|
92
|
+
{/* Keyboard Shortcuts */}
|
|
93
|
+
<div className="border-t border-border pt-4">
|
|
94
|
+
<button
|
|
95
|
+
type="button"
|
|
96
|
+
onClick={() => setShowShortcuts(!showShortcuts)}
|
|
97
|
+
className="flex items-center gap-1.5 text-xs font-medium text-muted-foreground uppercase tracking-wider hover:text-foreground transition-colors"
|
|
98
|
+
>
|
|
99
|
+
{showShortcuts ? <ChevronDown size={12} /> : <ChevronRight size={12} />}
|
|
100
|
+
{t.settings.tabs.shortcuts}
|
|
101
|
+
</button>
|
|
102
|
+
{showShortcuts && (
|
|
103
|
+
<div className="mt-3 space-y-1">
|
|
104
|
+
{t.shortcuts.map((s: { readonly description: string; readonly keys: readonly string[] }, i: number) => (
|
|
105
|
+
<div key={i} className="flex items-center justify-between py-2 border-b border-border last:border-0">
|
|
106
|
+
<span className="text-sm text-foreground">{s.description}</span>
|
|
107
|
+
<div className="flex items-center gap-1">
|
|
108
|
+
{s.keys.map((k: string, j: number) => (
|
|
109
|
+
<kbd key={j} className="px-2 py-0.5 text-xs font-mono bg-muted border border-border rounded text-foreground">{k}</kbd>
|
|
110
|
+
))}
|
|
111
|
+
</div>
|
|
112
|
+
</div>
|
|
113
|
+
))}
|
|
114
|
+
</div>
|
|
115
|
+
)}
|
|
116
|
+
</div>
|
|
99
117
|
</div>
|
|
100
118
|
);
|
|
101
119
|
}
|
|
@@ -2,16 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
import { useState, useEffect, useCallback, useSyncExternalStore } from 'react';
|
|
4
4
|
import { Copy, Check, RefreshCw, Trash2, Sparkles } from 'lucide-react';
|
|
5
|
-
import type {
|
|
6
|
-
import { Field, Input, EnvBadge, SectionLabel } from './Primitives';
|
|
5
|
+
import type { KnowledgeTabProps } from './types';
|
|
6
|
+
import { Field, Input, EnvBadge, SectionLabel, Toggle } from './Primitives';
|
|
7
7
|
import { apiFetch } from '@/lib/api';
|
|
8
8
|
|
|
9
|
-
interface KnowledgeTabProps {
|
|
10
|
-
data: SettingsData;
|
|
11
|
-
setData: React.Dispatch<React.SetStateAction<SettingsData | null>>;
|
|
12
|
-
t: any;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
9
|
export function KnowledgeTab({ data, setData, t }: KnowledgeTabProps) {
|
|
16
10
|
const env = data.envOverrides ?? {};
|
|
17
11
|
const k = t.settings.knowledge;
|
|
@@ -21,8 +15,8 @@ export function KnowledgeTab({ data, setData, t }: KnowledgeTabProps) {
|
|
|
21
15
|
const [guideDismissed, setGuideDismissed] = useState(false);
|
|
22
16
|
|
|
23
17
|
useEffect(() => {
|
|
24
|
-
fetch
|
|
25
|
-
|
|
18
|
+
// 🟢 MINOR #5: Use apiFetch instead of raw fetch for consistency
|
|
19
|
+
apiFetch<{ guideState?: { active: boolean; dismissed: boolean } }>('/api/setup')
|
|
26
20
|
.then(d => {
|
|
27
21
|
const gs = d.guideState;
|
|
28
22
|
if (gs) {
|
|
@@ -30,7 +24,9 @@ export function KnowledgeTab({ data, setData, t }: KnowledgeTabProps) {
|
|
|
30
24
|
setGuideDismissed(!!gs.dismissed);
|
|
31
25
|
}
|
|
32
26
|
})
|
|
33
|
-
.catch(
|
|
27
|
+
.catch(err => {
|
|
28
|
+
console.error('Failed to fetch guide state:', err);
|
|
29
|
+
});
|
|
34
30
|
}, []);
|
|
35
31
|
|
|
36
32
|
const handleGuideToggle = useCallback(() => {
|
|
@@ -39,13 +35,16 @@ export function KnowledgeTab({ data, setData, t }: KnowledgeTabProps) {
|
|
|
39
35
|
// If re-enabling, also ensure active is true
|
|
40
36
|
const patch: Record<string, boolean> = { dismissed: newDismissed };
|
|
41
37
|
if (!newDismissed) patch.active = true;
|
|
42
|
-
|
|
38
|
+
apiFetch('/api/setup', {
|
|
43
39
|
method: 'PATCH',
|
|
44
40
|
headers: { 'Content-Type': 'application/json' },
|
|
45
41
|
body: JSON.stringify({ guideState: patch }),
|
|
46
42
|
})
|
|
47
43
|
.then(() => window.dispatchEvent(new Event('guide-state-updated')))
|
|
48
|
-
.catch(
|
|
44
|
+
.catch(err => {
|
|
45
|
+
console.error('Failed to update guide state:', err);
|
|
46
|
+
setGuideDismissed(!newDismissed); // rollback on failure
|
|
47
|
+
});
|
|
49
48
|
}, [guideDismissed]);
|
|
50
49
|
|
|
51
50
|
const origin = useSyncExternalStore(
|
|
@@ -202,21 +201,7 @@ export function KnowledgeTab({ data, setData, t }: KnowledgeTabProps) {
|
|
|
202
201
|
<div className="text-sm text-foreground">{t.guide?.showGuide ?? 'Show getting started guide'}</div>
|
|
203
202
|
</div>
|
|
204
203
|
</div>
|
|
205
|
-
<
|
|
206
|
-
type="button"
|
|
207
|
-
role="switch"
|
|
208
|
-
aria-checked={!guideDismissed}
|
|
209
|
-
onClick={handleGuideToggle}
|
|
210
|
-
className={`relative inline-flex h-5 w-9 shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring ${
|
|
211
|
-
!guideDismissed ? 'bg-amber-500' : 'bg-muted'
|
|
212
|
-
}`}
|
|
213
|
-
>
|
|
214
|
-
<span
|
|
215
|
-
className={`pointer-events-none inline-block h-4 w-4 rounded-full bg-white shadow-sm transition-transform ${
|
|
216
|
-
!guideDismissed ? 'translate-x-4' : 'translate-x-0'
|
|
217
|
-
}`}
|
|
218
|
-
/>
|
|
219
|
-
</button>
|
|
204
|
+
<Toggle checked={!guideDismissed} onChange={() => handleGuideToggle()} />
|
|
220
205
|
</div>
|
|
221
206
|
</div>
|
|
222
207
|
)}
|