@geminilight/mindos 0.5.22 → 0.5.23

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.
Files changed (46) hide show
  1. package/app/app/api/ask/route.ts +7 -14
  2. package/app/app/api/bootstrap/route.ts +1 -0
  3. package/app/app/globals.css +14 -0
  4. package/app/app/setup/page.tsx +3 -2
  5. package/app/components/ActivityBar.tsx +183 -0
  6. package/app/components/AskFab.tsx +39 -97
  7. package/app/components/AskModal.tsx +13 -371
  8. package/app/components/Breadcrumb.tsx +4 -4
  9. package/app/components/FileTree.tsx +21 -4
  10. package/app/components/Logo.tsx +39 -0
  11. package/app/components/Panel.tsx +152 -0
  12. package/app/components/RightAskPanel.tsx +72 -0
  13. package/app/components/SettingsModal.tsx +9 -241
  14. package/app/components/SidebarLayout.tsx +426 -12
  15. package/app/components/SyncStatusBar.tsx +74 -53
  16. package/app/components/TableOfContents.tsx +4 -2
  17. package/app/components/ask/AskContent.tsx +418 -0
  18. package/app/components/ask/MessageList.tsx +2 -2
  19. package/app/components/panels/AgentsPanel.tsx +231 -0
  20. package/app/components/panels/PanelHeader.tsx +35 -0
  21. package/app/components/panels/PluginsPanel.tsx +106 -0
  22. package/app/components/panels/SearchPanel.tsx +178 -0
  23. package/app/components/panels/SyncPopover.tsx +105 -0
  24. package/app/components/renderers/csv/TableView.tsx +4 -4
  25. package/app/components/settings/AiTab.tsx +39 -1
  26. package/app/components/settings/KnowledgeTab.tsx +116 -2
  27. package/app/components/settings/McpTab.tsx +6 -6
  28. package/app/components/settings/SettingsContent.tsx +343 -0
  29. package/app/components/settings/types.ts +1 -1
  30. package/app/components/setup/index.tsx +2 -23
  31. package/app/hooks/useResizeDrag.ts +78 -0
  32. package/app/lib/agent/index.ts +0 -1
  33. package/app/lib/agent/model.ts +33 -10
  34. package/app/lib/format.ts +19 -0
  35. package/app/lib/i18n-en.ts +6 -6
  36. package/app/lib/i18n-zh.ts +5 -5
  37. package/app/next-env.d.ts +1 -1
  38. package/app/next.config.ts +1 -1
  39. package/app/package.json +2 -2
  40. package/bin/cli.js +27 -97
  41. package/package.json +4 -2
  42. package/scripts/setup.js +2 -12
  43. package/skills/mindos/SKILL.md +226 -8
  44. package/skills/mindos-zh/SKILL.md +226 -8
  45. package/app/lib/agent/skill-rules.ts +0 -70
  46. package/app/package-lock.json +0 -15736
@@ -0,0 +1,343 @@
1
+ 'use client';
2
+
3
+ import { useEffect, useState, useCallback, useRef } from 'react';
4
+ import { Settings, Loader2, AlertCircle, CheckCircle2, RotateCcw, Sparkles, Palette, Database, RefreshCw, Plug, X } from 'lucide-react';
5
+ import { useLocale } from '@/lib/LocaleContext';
6
+ import { apiFetch } from '@/lib/api';
7
+ import type { AiSettings, AgentSettings, SettingsData, Tab } from './types';
8
+ import { AiTab } from './AiTab';
9
+ import { AppearanceTab } from './AppearanceTab';
10
+ import { KnowledgeTab } from './KnowledgeTab';
11
+ import { SyncTab } from './SyncTab';
12
+ import { McpTab } from './McpTab';
13
+
14
+ interface SettingsContentProps {
15
+ visible: boolean;
16
+ initialTab?: Tab;
17
+ variant: 'modal' | 'panel';
18
+ onClose?: () => void;
19
+ }
20
+
21
+ export default function SettingsContent({ visible, initialTab, variant, onClose }: SettingsContentProps) {
22
+ const [tab, setTab] = useState<Tab>('ai');
23
+ const [data, setData] = useState<SettingsData | null>(null);
24
+ const [saving, setSaving] = useState(false);
25
+ const [status, setStatus] = useState<'idle' | 'saved' | 'error' | 'load-error'>('idle');
26
+ const { t, locale, setLocale } = useLocale();
27
+ const saveTimer = useRef<ReturnType<typeof setTimeout>>(undefined);
28
+ const dataLoaded = useRef(false);
29
+
30
+ const [font, setFont] = useState('lora');
31
+ const [contentWidth, setContentWidth] = useState('780px');
32
+ const [dark, setDark] = useState(true);
33
+
34
+ const isPanel = variant === 'panel';
35
+
36
+ // Init data when becoming visible
37
+ const prevVisibleRef = useRef(false);
38
+ useEffect(() => {
39
+ const justOpened = isPanel
40
+ ? (visible && !prevVisibleRef.current)
41
+ : visible;
42
+
43
+ if (justOpened) {
44
+ apiFetch<SettingsData>('/api/settings').then(d => { setData(d); dataLoaded.current = true; }).catch(() => setStatus('load-error'));
45
+ setFont(localStorage.getItem('prose-font') ?? 'lora');
46
+ setContentWidth(localStorage.getItem('content-width') ?? '780px');
47
+ const stored = localStorage.getItem('theme');
48
+ setDark(stored ? stored === 'dark' : window.matchMedia('(prefers-color-scheme: dark)').matches);
49
+ setStatus('idle');
50
+ }
51
+ if (!visible) { dataLoaded.current = false; }
52
+ prevVisibleRef.current = visible;
53
+ }, [visible, isPanel]);
54
+
55
+ useEffect(() => {
56
+ if (visible && initialTab) setTab(initialTab);
57
+ }, [visible, initialTab]);
58
+
59
+ useEffect(() => {
60
+ const fontMap: Record<string, string> = {
61
+ 'lora': "'Lora', Georgia, serif",
62
+ 'ibm-plex-sans': "'IBM Plex Sans', sans-serif",
63
+ 'geist': 'var(--font-geist-sans), sans-serif',
64
+ 'ibm-plex-mono': "'IBM Plex Mono', monospace",
65
+ };
66
+ document.documentElement.style.setProperty('--prose-font-override', fontMap[font] ?? '');
67
+ localStorage.setItem('prose-font', font);
68
+ }, [font]);
69
+
70
+ useEffect(() => {
71
+ document.documentElement.style.setProperty('--content-width-override', contentWidth);
72
+ localStorage.setItem('content-width', contentWidth);
73
+ }, [contentWidth]);
74
+
75
+ // Esc to close — modal only
76
+ useEffect(() => {
77
+ if (variant !== 'modal' || !visible || !onClose) return;
78
+ const handler = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose(); };
79
+ window.addEventListener('keydown', handler);
80
+ return () => window.removeEventListener('keydown', handler);
81
+ }, [variant, visible, onClose]);
82
+
83
+ const doSave = useCallback(async (d: SettingsData) => {
84
+ setSaving(true);
85
+ try {
86
+ await apiFetch('/api/settings', {
87
+ method: 'POST',
88
+ headers: { 'Content-Type': 'application/json' },
89
+ body: JSON.stringify({ ai: d.ai, agent: d.agent, mindRoot: d.mindRoot, webPassword: d.webPassword, authToken: d.authToken }),
90
+ });
91
+ setStatus('saved');
92
+ setTimeout(() => setStatus('idle'), 2500);
93
+ } catch {
94
+ setStatus('error');
95
+ setTimeout(() => setStatus('idle'), 2500);
96
+ } finally {
97
+ setSaving(false);
98
+ }
99
+ }, []);
100
+
101
+ useEffect(() => {
102
+ if (!data || !dataLoaded.current) return;
103
+ clearTimeout(saveTimer.current);
104
+ saveTimer.current = setTimeout(() => doSave(data), 800);
105
+ return () => clearTimeout(saveTimer.current);
106
+ }, [data, doSave]);
107
+
108
+ const updateAi = useCallback((patch: Partial<AiSettings>) => {
109
+ setData(d => d ? { ...d, ai: { ...d.ai, ...patch } } : d);
110
+ }, []);
111
+
112
+ const updateAgent = useCallback((patch: Partial<AgentSettings>) => {
113
+ setData(d => d ? { ...d, agent: { ...(d.agent ?? {}), ...patch } } : d);
114
+ }, []);
115
+
116
+ const restoreFromEnv = useCallback(async () => {
117
+ if (!data) return;
118
+ const defaults: AiSettings = {
119
+ provider: 'anthropic',
120
+ providers: { anthropic: { apiKey: '', model: '' }, openai: { apiKey: '', model: '', baseUrl: '' } },
121
+ };
122
+ setData(d => d ? { ...d, ai: defaults } : d);
123
+ const DEBOUNCE_DELAY = 800;
124
+ const SAVE_OPERATION_TIME = 500;
125
+ setTimeout(() => {
126
+ apiFetch<SettingsData>('/api/settings').then(d => { setData(d); }).catch(() => setStatus('error'));
127
+ }, DEBOUNCE_DELAY + SAVE_OPERATION_TIME);
128
+ }, [data]);
129
+
130
+ const env = data?.envOverrides ?? {};
131
+ const iconSize = isPanel ? 12 : 13;
132
+
133
+ const TABS: { id: Tab; label: string; icon: React.ReactNode }[] = [
134
+ { id: 'ai', label: t.settings.tabs.ai, icon: <Sparkles size={iconSize} /> },
135
+ { id: 'mcp', label: t.settings.tabs.mcp ?? 'MCP & Skills', icon: <Plug size={iconSize} /> },
136
+ { id: 'knowledge', label: t.settings.tabs.knowledge, icon: <Settings size={iconSize} /> },
137
+ { id: 'sync', label: t.settings.tabs.sync ?? 'Sync', icon: <RefreshCw size={iconSize} /> },
138
+ { id: 'appearance', label: t.settings.tabs.appearance, icon: <Palette size={iconSize} /> },
139
+ ];
140
+
141
+ const activeTabLabel = TABS.find(t2 => t2.id === tab)?.label ?? '';
142
+
143
+ /* ── Shared content & footer ── */
144
+ const renderContent = () => (
145
+ <div className={`flex-1 overflow-y-auto min-h-0 ${isPanel ? 'px-4 py-4 space-y-4' : 'px-5 py-5 space-y-5'}`}>
146
+ {status === 'load-error' && (tab === 'ai' || tab === 'knowledge') ? (
147
+ <div className="flex flex-col items-center gap-2 py-8 text-center">
148
+ <AlertCircle size={isPanel ? 18 : 20} className="text-destructive" />
149
+ <p className={`${isPanel ? 'text-xs' : 'text-sm'} text-destructive font-medium`}>Failed to load settings</p>
150
+ {!isPanel && <p className="text-xs text-muted-foreground">Check that the server is running and AUTH_TOKEN is configured correctly.</p>}
151
+ </div>
152
+ ) : !data && tab !== 'appearance' && tab !== 'mcp' && tab !== 'sync' ? (
153
+ <div className="flex justify-center py-8">
154
+ <Loader2 size={isPanel ? 16 : 18} className="animate-spin text-muted-foreground" />
155
+ </div>
156
+ ) : (
157
+ <>
158
+ {tab === 'ai' && data?.ai && <AiTab data={data} updateAi={updateAi} updateAgent={updateAgent} t={t} />}
159
+ {tab === 'appearance' && <AppearanceTab font={font} setFont={setFont} contentWidth={contentWidth} setContentWidth={setContentWidth} dark={dark} setDark={setDark} locale={locale} setLocale={setLocale} t={t} />}
160
+ {tab === 'knowledge' && data && <KnowledgeTab data={data} setData={setData} t={t} />}
161
+ {tab === 'sync' && <SyncTab t={t} />}
162
+ {tab === 'mcp' && <McpTab t={t} />}
163
+ </>
164
+ )}
165
+ </div>
166
+ );
167
+
168
+ const renderFooter = () => (
169
+ (tab === 'ai' || tab === 'knowledge') ? (
170
+ <div className={`${isPanel ? 'px-4 py-2' : 'px-5 py-2.5'} border-t border-border shrink-0 flex items-center justify-between`}>
171
+ <div className="flex items-center gap-3">
172
+ {tab === 'ai' && Object.values(env).some(Boolean) && (
173
+ <button
174
+ onClick={restoreFromEnv}
175
+ disabled={saving || !data}
176
+ className={`flex items-center gap-1.5 ${isPanel ? 'px-2.5 py-1 text-[11px] rounded-md' : '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`}
177
+ >
178
+ <RotateCcw size={isPanel ? 11 : 12} />
179
+ {t.settings.ai.restoreFromEnv}
180
+ </button>
181
+ )}
182
+ {tab === 'knowledge' && (
183
+ <a
184
+ href="/setup?force=1"
185
+ className={`flex items-center gap-1.5 ${isPanel ? 'px-2.5 py-1 text-[11px] rounded-md' : 'px-3 py-1 text-xs rounded-lg'} border border-border text-muted-foreground hover:text-foreground hover:bg-muted transition-colors`}
186
+ >
187
+ <RotateCcw size={isPanel ? 11 : 12} />
188
+ {t.settings.reconfigure}
189
+ </a>
190
+ )}
191
+ </div>
192
+ {!isPanel && (
193
+ <div className="flex items-center gap-1.5 text-xs" role="status" aria-live="polite">
194
+ {saving && <><Loader2 size={12} className="animate-spin text-muted-foreground" /><span className="text-muted-foreground">{t.settings.save}...</span></>}
195
+ {status === 'saved' && <><CheckCircle2 size={12} className="text-success" /><span className="text-success">{t.settings.saved}</span></>}
196
+ {status === 'error' && <><AlertCircle size={12} className="text-destructive" /><span className="text-destructive">{t.settings.saveFailed}</span></>}
197
+ </div>
198
+ )}
199
+ </div>
200
+ ) : null
201
+ );
202
+
203
+ /* ── Panel variant: unchanged (horizontal tabs) ── */
204
+ if (isPanel) {
205
+ return (
206
+ <>
207
+ <div className="flex items-center justify-between px-4 py-3 border-b border-border shrink-0">
208
+ <div className="flex items-center gap-2 text-sm font-medium text-foreground">
209
+ <Settings size={14} className="text-muted-foreground" />
210
+ <span className="text-xs font-medium text-muted-foreground uppercase tracking-wider font-display">Settings</span>
211
+ </div>
212
+ <div className="flex items-center gap-1.5">
213
+ <div className="flex items-center gap-1.5 text-[10px]" role="status" aria-live="polite">
214
+ {saving && <Loader2 size={10} className="animate-spin text-muted-foreground" />}
215
+ {status === 'saved' && <CheckCircle2 size={10} className="text-success" />}
216
+ {status === 'error' && <AlertCircle size={10} className="text-destructive" />}
217
+ </div>
218
+ </div>
219
+ </div>
220
+ <div className="flex border-b border-border px-3 shrink-0 overflow-x-auto scrollbar-none gap-0">
221
+ {TABS.map(tabItem => (
222
+ <button
223
+ key={tabItem.id}
224
+ onClick={() => setTab(tabItem.id)}
225
+ className={`flex items-center gap-1 px-2 py-2 text-[11px] font-medium transition-colors border-b-2 -mb-px whitespace-nowrap ${
226
+ tab === tabItem.id
227
+ ? 'border-amber-500 text-foreground'
228
+ : 'border-transparent text-muted-foreground hover:text-foreground'
229
+ }`}
230
+ >
231
+ {tabItem.icon}
232
+ {tabItem.label}
233
+ </button>
234
+ ))}
235
+ </div>
236
+ {renderContent()}
237
+ {renderFooter()}
238
+ </>
239
+ );
240
+ }
241
+
242
+ /* ── Modal variant ── */
243
+ return (
244
+ <>
245
+ {/* Mobile: original vertical layout */}
246
+ <div className="flex flex-col h-full md:hidden">
247
+ {/* Mobile header */}
248
+ <div className="flex items-center justify-between px-4 py-3 border-b border-border shrink-0">
249
+ <div className="flex justify-center pt-2 pb-0 absolute top-0 left-1/2 -translate-x-1/2">
250
+ <div className="w-8 h-1 rounded-full bg-muted-foreground/20" />
251
+ </div>
252
+ <div className="flex items-center gap-2 text-sm font-medium text-foreground">
253
+ <Settings size={15} className="text-muted-foreground" />
254
+ <span className="font-display">{t.settings.title}</span>
255
+ </div>
256
+ <div className="flex items-center gap-1.5">
257
+ <div className="flex items-center gap-1.5 text-[10px]" role="status" aria-live="polite">
258
+ {saving && <Loader2 size={12} className="animate-spin text-muted-foreground" />}
259
+ {status === 'saved' && <><CheckCircle2 size={12} className="text-success" /><span className="text-success">{t.settings.saved}</span></>}
260
+ {status === 'error' && <><AlertCircle size={12} className="text-destructive" /><span className="text-destructive">{t.settings.saveFailed}</span></>}
261
+ </div>
262
+ {onClose && (
263
+ <button onClick={onClose} className="p-1 rounded hover:bg-muted text-muted-foreground hover:text-foreground transition-colors">
264
+ <X size={15} />
265
+ </button>
266
+ )}
267
+ </div>
268
+ </div>
269
+ {/* Mobile horizontal tabs */}
270
+ <div className="flex border-b border-border px-4 shrink-0 overflow-x-auto scrollbar-none gap-0">
271
+ {TABS.map(tabItem => (
272
+ <button
273
+ key={tabItem.id}
274
+ onClick={() => setTab(tabItem.id)}
275
+ 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 ${
276
+ tab === tabItem.id
277
+ ? 'border-amber-500 text-foreground'
278
+ : 'border-transparent text-muted-foreground hover:text-foreground'
279
+ }`}
280
+ >
281
+ {tabItem.icon}
282
+ {tabItem.label}
283
+ </button>
284
+ ))}
285
+ </div>
286
+ {renderContent()}
287
+ {renderFooter()}
288
+ </div>
289
+
290
+ {/* Desktop: left-right layout */}
291
+ <div className="hidden md:flex flex-row h-full min-h-0">
292
+ {/* Left sidebar — vertical tabs */}
293
+ <div className="w-[180px] shrink-0 border-r border-border flex flex-col">
294
+ <div className="flex items-center gap-2 px-4 py-3 border-b border-border">
295
+ <Settings size={15} className="text-muted-foreground" />
296
+ <span className="text-sm font-medium font-display text-foreground">{t.settings.title}</span>
297
+ </div>
298
+ <nav className="flex-1 overflow-y-auto py-1.5">
299
+ {TABS.map(tabItem => (
300
+ <button
301
+ key={tabItem.id}
302
+ onClick={() => setTab(tabItem.id)}
303
+ className={`flex items-center gap-2 w-full px-4 py-2 text-xs font-medium transition-colors relative ${
304
+ tab === tabItem.id
305
+ ? 'text-foreground bg-muted'
306
+ : 'text-muted-foreground hover:text-foreground hover:bg-muted/50'
307
+ }`}
308
+ >
309
+ {tab === tabItem.id && (
310
+ <div className="absolute left-0 top-1 bottom-1 w-[3px] rounded-r-full bg-amber-500" />
311
+ )}
312
+ {tabItem.icon}
313
+ {tabItem.label}
314
+ </button>
315
+ ))}
316
+ </nav>
317
+ </div>
318
+
319
+ {/* Right content area */}
320
+ <div className="flex-1 flex flex-col min-w-0 min-h-0">
321
+ {/* Right header: tab title + status + close */}
322
+ <div className="flex items-center justify-between px-5 py-3 border-b border-border shrink-0">
323
+ <span className="text-sm font-medium text-foreground font-display">{activeTabLabel}</span>
324
+ <div className="flex items-center gap-1.5">
325
+ <div className="flex items-center gap-1.5 text-[10px]" role="status" aria-live="polite">
326
+ {saving && <Loader2 size={12} className="animate-spin text-muted-foreground" />}
327
+ {status === 'saved' && <><CheckCircle2 size={12} className="text-success" /><span className="text-success">{t.settings.saved}</span></>}
328
+ {status === 'error' && <><AlertCircle size={12} className="text-destructive" /><span className="text-destructive">{t.settings.saveFailed}</span></>}
329
+ </div>
330
+ {onClose && (
331
+ <button onClick={onClose} className="p-1 rounded hover:bg-muted text-muted-foreground hover:text-foreground transition-colors">
332
+ <X size={15} />
333
+ </button>
334
+ )}
335
+ </div>
336
+ </div>
337
+ {renderContent()}
338
+ {renderFooter()}
339
+ </div>
340
+ </div>
341
+ </>
342
+ );
343
+ }
@@ -33,7 +33,7 @@ export interface SettingsData {
33
33
  envValues?: Record<string, string>;
34
34
  }
35
35
 
36
- export type Tab = 'ai' | 'appearance' | 'knowledge' | 'mcp' | 'plugins' | 'sync' | 'monitoring' | 'agents';
36
+ export type Tab = 'ai' | 'appearance' | 'knowledge' | 'mcp' | 'sync';
37
37
 
38
38
  export const CONTENT_WIDTHS = [
39
39
  { value: '680px', label: 'Narrow (680px)' },
@@ -102,20 +102,6 @@ async function installAgents(
102
102
  return updated;
103
103
  }
104
104
 
105
- /** Phase 3: Install skill to agents. Returns result. */
106
- async function installSkill(
107
- template: string,
108
- agentKeys: string[],
109
- ): Promise<{ ok?: boolean; skill?: string; error?: string }> {
110
- const skillName = template === 'zh' ? 'mindos-zh' : 'mindos';
111
- const res = await fetch('/api/mcp/install-skill', {
112
- method: 'POST',
113
- headers: { 'Content-Type': 'application/json' },
114
- body: JSON.stringify({ skill: skillName, agents: agentKeys }),
115
- });
116
- return await res.json();
117
- }
118
-
119
105
  // ─── Component ───────────────────────────────────────────────────────────────
120
106
 
121
107
  export default function SetupWizard() {
@@ -318,15 +304,8 @@ export default function SetupWizard() {
318
304
  }
319
305
  }
320
306
 
321
- // Phase 3: Install skill
322
- setSetupPhase('skill');
323
- try {
324
- const skillData = await installSkill(state.template, agentKeys);
325
- setSkillInstallResult(skillData);
326
- } catch (e) {
327
- console.warn('[SetupWizard] skill install failed:', e);
328
- setSkillInstallResult({ error: 'Failed to install skill' });
329
- }
307
+ // Phase 3: Skill is now built into SKILL.md — no install needed.
308
+ // user-skill-rules.md will be created on first preference capture.
330
309
 
331
310
  setSubmitting(false);
332
311
  setCompleted(true);
@@ -0,0 +1,78 @@
1
+ 'use client';
2
+
3
+ import { useCallback, useRef } from 'react';
4
+
5
+ export interface UseResizeDragOptions {
6
+ /** Current width */
7
+ width: number;
8
+ /** Min allowed width */
9
+ minWidth: number;
10
+ /** Max allowed width (absolute) */
11
+ maxWidth: number;
12
+ /** Max width as ratio of viewport */
13
+ maxWidthRatio: number;
14
+ /** 'right' = right-edge drag (mouse right → wider), 'left' = left-edge drag (mouse left → wider) */
15
+ direction: 'right' | 'left';
16
+ /** Skip if true (e.g. maximized state) */
17
+ disabled?: boolean;
18
+ /** Called on every mousemove with new width */
19
+ onResize: (width: number) => void;
20
+ /** Called on mouseup with final width */
21
+ onResizeEnd: (width: number) => void;
22
+ }
23
+
24
+ /**
25
+ * Shared drag-resize logic for panel edges.
26
+ * Returns a mousedown handler for the resize handle element.
27
+ */
28
+ export function useResizeDrag({
29
+ width,
30
+ minWidth,
31
+ maxWidth,
32
+ maxWidthRatio,
33
+ direction,
34
+ disabled,
35
+ onResize,
36
+ onResizeEnd,
37
+ }: UseResizeDragOptions) {
38
+ const dragging = useRef(false);
39
+ const startX = useRef(0);
40
+ const startWidth = useRef(0);
41
+ const latestWidthRef = useRef(width);
42
+ latestWidthRef.current = width;
43
+
44
+ const handleMouseDown = useCallback((e: React.MouseEvent) => {
45
+ if (disabled) return;
46
+ e.preventDefault();
47
+ dragging.current = true;
48
+ startX.current = e.clientX;
49
+ startWidth.current = width;
50
+
51
+ document.body.classList.add('select-none');
52
+ document.body.style.cursor = 'col-resize';
53
+
54
+ const onMouseMove = (ev: MouseEvent) => {
55
+ if (!dragging.current) return;
56
+ const delta = direction === 'right'
57
+ ? ev.clientX - startX.current
58
+ : startX.current - ev.clientX;
59
+ const maxW = Math.min(maxWidth, window.innerWidth * maxWidthRatio);
60
+ const newWidth = Math.round(Math.max(minWidth, Math.min(maxW, startWidth.current + delta)));
61
+ onResize(newWidth);
62
+ };
63
+
64
+ const onMouseUp = () => {
65
+ dragging.current = false;
66
+ document.body.classList.remove('select-none');
67
+ document.body.style.cursor = '';
68
+ onResizeEnd(latestWidthRef.current);
69
+ document.removeEventListener('mousemove', onMouseMove);
70
+ document.removeEventListener('mouseup', onMouseUp);
71
+ };
72
+
73
+ document.addEventListener('mousemove', onMouseMove);
74
+ document.addEventListener('mouseup', onMouseUp);
75
+ }, [width, minWidth, maxWidth, maxWidthRatio, direction, disabled, onResize, onResizeEnd]);
76
+
77
+ return handleMouseDown;
78
+ }
@@ -6,4 +6,3 @@ export {
6
6
  truncateToolOutputs, compactMessages, hardPrune, createTransformContext,
7
7
  } from './context';
8
8
  export { toAgentMessages } from './to-agent-messages';
9
- export { loadSkillRules } from './skill-rules';
@@ -22,20 +22,28 @@ export function getModelConfig(): {
22
22
  if (cfg.provider === 'openai') {
23
23
  const modelName = cfg.openaiModel;
24
24
  let model: Model<any>;
25
- let apiVariant: string = 'openai-responses'; // Default to responses API
26
25
 
27
- // Allow customization of API variant if using custom endpoint
28
- // Check if config specifies an alternative API type (for non-standard endpoints)
26
+ // API variant: 'openai-completions' = /chat/completions (widest compatibility),
27
+ // 'openai-responses' = /responses (OpenAI native). Custom proxies (baseUrl set)
28
+ // almost always only support chat completions, so default to that when baseUrl is set.
29
+ const hasCustomBase = !!cfg.openaiBaseUrl;
30
+ const defaultApi = hasCustomBase ? 'openai-completions' : 'openai-responses';
29
31
  const customApiVariant = (cfg as any).openaiApiVariant; // May exist in extended config
30
32
 
31
33
  try {
32
- model = piGetModel('openai', modelName as any);
34
+ const resolved = piGetModel('openai', modelName as any);
35
+ if (!resolved) throw new Error('Model not in registry');
36
+ model = resolved;
37
+ // If user has a custom baseUrl, override API to completions for compatibility
38
+ if (hasCustomBase && !customApiVariant) {
39
+ model = { ...model, api: defaultApi };
40
+ }
33
41
  } catch {
34
42
  // Model not in pi-ai registry — construct manually for custom/proxy endpoints
35
43
  model = {
36
44
  id: modelName,
37
45
  name: modelName,
38
- api: (customApiVariant ?? apiVariant) as any,
46
+ api: (customApiVariant ?? defaultApi) as any,
39
47
  provider: 'openai',
40
48
  baseUrl: 'https://api.openai.com/v1',
41
49
  reasoning: false,
@@ -46,10 +54,23 @@ export function getModelConfig(): {
46
54
  };
47
55
  }
48
56
 
49
- // Override baseUrl if user configured a custom endpoint
50
- if (cfg.openaiBaseUrl) {
51
- model = { ...model, baseUrl: cfg.openaiBaseUrl };
52
- // Also allow API variant override for custom endpoints
57
+ // For custom proxy endpoints, set conservative compat flags.
58
+ // Most proxies (Azure, Bedrock relays, corporate gateways) only support
59
+ // a subset of OpenAI's features. These defaults prevent silent failures.
60
+ if (hasCustomBase) {
61
+ model = {
62
+ ...model,
63
+ baseUrl: cfg.openaiBaseUrl,
64
+ compat: {
65
+ ...(model as any).compat,
66
+ supportsStore: false,
67
+ supportsDeveloperRole: false,
68
+ supportsReasoningEffort: false,
69
+ supportsUsageInStreaming: false,
70
+ supportsStrictMode: false,
71
+ maxTokensField: 'max_tokens' as const,
72
+ },
73
+ };
53
74
  if (customApiVariant) {
54
75
  model = { ...model, api: customApiVariant };
55
76
  }
@@ -63,7 +84,9 @@ export function getModelConfig(): {
63
84
  let model: Model<any>;
64
85
 
65
86
  try {
66
- model = piGetModel('anthropic', modelName as any);
87
+ const resolved = piGetModel('anthropic', modelName as any);
88
+ if (!resolved) throw new Error('Model not in registry');
89
+ model = resolved;
67
90
  } catch {
68
91
  // Unknown Anthropic model — construct manually
69
92
  model = {
@@ -0,0 +1,19 @@
1
+ /** Format bytes to human-readable string (B / KB / MB / GB) */
2
+ export function formatBytes(bytes: number): string {
3
+ if (bytes < 1024) return `${bytes} B`;
4
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
5
+ if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
6
+ return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
7
+ }
8
+
9
+ /** Format milliseconds to human-readable uptime string (s / m / h / d) */
10
+ export function formatUptime(ms: number): string {
11
+ const s = Math.floor(ms / 1000);
12
+ if (s < 60) return `${s}s`;
13
+ const m = Math.floor(s / 60);
14
+ if (m < 60) return `${m}m ${s % 60}s`;
15
+ const h = Math.floor(m / 60);
16
+ if (h < 24) return `${h}h ${m % 60}m`;
17
+ const d = Math.floor(h / 24);
18
+ return `${d}d ${h % 24}h`;
19
+ }
@@ -16,7 +16,7 @@ export const en = {
16
16
  createToActivate: 'Create {file} to activate',
17
17
  shortcuts: {
18
18
  searchFiles: 'Search files',
19
- askAI: 'Ask AI',
19
+ askAI: 'MindOS Agent',
20
20
  editFile: 'Edit file',
21
21
  save: 'Save',
22
22
  settings: 'Settings',
@@ -30,7 +30,7 @@ export const en = {
30
30
  },
31
31
  sidebar: {
32
32
  searchTitle: 'Search (⌘K)',
33
- askTitle: 'Ask AI (⌘/)',
33
+ askTitle: 'MindOS Agent (⌘/)',
34
34
  settingsTitle: 'Settings (⌘,)',
35
35
  collapseTitle: 'Collapse sidebar',
36
36
  expandTitle: 'Expand sidebar',
@@ -104,7 +104,7 @@ export const en = {
104
104
  },
105
105
  settings: {
106
106
  title: 'Settings',
107
- tabs: { ai: 'AI', appearance: 'Appearance', knowledge: 'Knowledge Base', sync: 'Sync', mcp: 'MCP', plugins: 'Plugins', shortcuts: 'Shortcuts', monitoring: 'Monitoring', agents: 'Agents' },
107
+ tabs: { ai: 'AI', appearance: 'Appearance', knowledge: 'General', sync: 'Sync', mcp: 'MCP & Skills', plugins: 'Plugins', shortcuts: 'Shortcuts', monitoring: 'Monitoring', agents: 'Agents' },
108
108
  ai: {
109
109
  provider: 'Provider',
110
110
  model: 'Model',
@@ -323,12 +323,12 @@ export const en = {
323
323
  },
324
324
  shortcuts: [
325
325
  { keys: ['⌘', 'K'], description: 'Search' },
326
- { keys: ['⌘', '/'], description: 'Ask AI' },
326
+ { keys: ['⌘', '/'], description: 'MindOS Agent' },
327
327
  { keys: ['⌘', ','], description: 'Settings' },
328
328
  { keys: ['E'], description: 'Edit current file' },
329
329
  { keys: ['⌘', 'S'], description: 'Save' },
330
330
  { keys: ['Esc'], description: 'Cancel edit / close modal' },
331
- { keys: ['@'], description: 'Attach file in Ask AI' },
331
+ { keys: ['@'], description: 'Attach file in MindOS Agent' },
332
332
  ],
333
333
  login: {
334
334
  tagline: 'You think here, Agents act there.',
@@ -468,7 +468,7 @@ export const en = {
468
468
  welcomeTitle: 'Welcome to MindOS!',
469
469
  welcomeDesc: 'Setup is complete. Start by asking AI a question, browsing your knowledge base, or configuring MCP agents.',
470
470
  welcomeLinkReconfigure: 'Reconfigure',
471
- welcomeLinkAskAI: 'Ask AI',
471
+ welcomeLinkAskAI: 'MindOS Agent',
472
472
  welcomeLinkMCP: 'MCP Settings',
473
473
  },
474
474
  guide: {
@@ -41,7 +41,7 @@ export const zh = {
41
41
  createToActivate: '创建 {file} 以启用此插件',
42
42
  shortcuts: {
43
43
  searchFiles: '搜索文件',
44
- askAI: ' AI',
44
+ askAI: 'MindOS Agent',
45
45
  editFile: '编辑文件',
46
46
  save: '保存',
47
47
  settings: '设置',
@@ -55,7 +55,7 @@ export const zh = {
55
55
  },
56
56
  sidebar: {
57
57
  searchTitle: '搜索 (⌘K)',
58
- askTitle: ' AI (⌘/)',
58
+ askTitle: 'MindOS Agent (⌘/)',
59
59
  settingsTitle: '设置 (⌘,)',
60
60
  collapseTitle: '收起侧栏',
61
61
  expandTitle: '展开侧栏',
@@ -129,7 +129,7 @@ export const zh = {
129
129
  },
130
130
  settings: {
131
131
  title: '设置',
132
- tabs: { ai: 'AI', appearance: '外观', knowledge: '知识库', sync: '同步', mcp: 'MCP', plugins: '插件', shortcuts: '快捷键', monitoring: '监控', agents: 'Agents' },
132
+ tabs: { ai: 'AI', appearance: '外观', knowledge: '通用', sync: '同步', mcp: 'MCP & Skills', plugins: '插件', shortcuts: '快捷键', monitoring: '监控', agents: 'Agents' },
133
133
  ai: {
134
134
  provider: '服务商',
135
135
  model: '模型',
@@ -348,7 +348,7 @@ export const zh = {
348
348
  },
349
349
  shortcuts: [
350
350
  { keys: ['⌘', 'K'], description: '搜索' },
351
- { keys: ['⌘', '/'], description: ' AI' },
351
+ { keys: ['⌘', '/'], description: 'MindOS Agent' },
352
352
  { keys: ['⌘', ','], description: '设置' },
353
353
  { keys: ['E'], description: '编辑当前文件' },
354
354
  { keys: ['⌘', 'S'], description: '保存' },
@@ -493,7 +493,7 @@ export const zh = {
493
493
  welcomeTitle: '欢迎使用 MindOS!',
494
494
  welcomeDesc: '初始化完成。可以开始向 AI 提问、浏览知识库,或配置 MCP Agent。',
495
495
  welcomeLinkReconfigure: '重新配置',
496
- welcomeLinkAskAI: ' AI',
496
+ welcomeLinkAskAI: 'MindOS Agent',
497
497
  welcomeLinkMCP: 'MCP 设置',
498
498
  },
499
499
  guide: {