@geminilight/mindos 0.1.2 → 0.1.3
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/layout.tsx +5 -11
- package/app/app/view/[...path]/ViewPageClient.tsx +1 -1
- package/app/components/DirView.tsx +14 -8
- package/app/components/SettingsModal.tsx +7 -8
- package/app/components/ShellLayout.tsx +16 -0
- package/app/components/ThemeToggle.tsx +14 -12
- package/app/components/settings/KnowledgeTab.tsx +8 -2
- package/app/lib/LocaleContext.tsx +16 -8
- package/bin/cli.js +83 -2
- package/package.json +1 -1
package/app/app/layout.tsx
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import type { Metadata } from 'next';
|
|
2
2
|
import { Geist, Geist_Mono, IBM_Plex_Mono, IBM_Plex_Sans, Lora } from 'next/font/google';
|
|
3
|
-
import { headers } from 'next/headers';
|
|
4
3
|
import './globals.css';
|
|
5
4
|
import { getFileTree } from '@/lib/fs';
|
|
6
|
-
import
|
|
5
|
+
import ShellLayout from '@/components/ShellLayout';
|
|
7
6
|
import { TooltipProvider } from '@/components/ui/tooltip';
|
|
8
7
|
import { LocaleProvider } from '@/lib/LocaleContext';
|
|
9
8
|
import ErrorBoundary from '@/components/ErrorBoundary';
|
|
@@ -49,7 +48,7 @@ export const viewport = {
|
|
|
49
48
|
viewportFit: 'cover' as const,
|
|
50
49
|
};
|
|
51
50
|
|
|
52
|
-
export default
|
|
51
|
+
export default function RootLayout({
|
|
53
52
|
children,
|
|
54
53
|
}: Readonly<{
|
|
55
54
|
children: React.ReactNode;
|
|
@@ -61,9 +60,6 @@ export default async function RootLayout({
|
|
|
61
60
|
console.error('[RootLayout] Failed to load file tree:', err);
|
|
62
61
|
}
|
|
63
62
|
|
|
64
|
-
const headersList = await headers();
|
|
65
|
-
const isLoginPage = headersList.get('x-pathname') === '/login';
|
|
66
|
-
|
|
67
63
|
return (
|
|
68
64
|
<html lang="en" suppressHydrationWarning>
|
|
69
65
|
<head>
|
|
@@ -89,11 +85,9 @@ export default async function RootLayout({
|
|
|
89
85
|
<LocaleProvider>
|
|
90
86
|
<TooltipProvider delay={300}>
|
|
91
87
|
<ErrorBoundary>
|
|
92
|
-
{
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
</SidebarLayout>
|
|
96
|
-
)}
|
|
88
|
+
<ShellLayout fileTree={fileTree}>
|
|
89
|
+
{children}
|
|
90
|
+
</ShellLayout>
|
|
97
91
|
</ErrorBoundary>
|
|
98
92
|
</TooltipProvider>
|
|
99
93
|
</LocaleProvider>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { useState,
|
|
3
|
+
import { useState, useSyncExternalStore, useMemo } from 'react';
|
|
4
4
|
import Link from 'next/link';
|
|
5
5
|
import { FileText, Table, Folder, FolderOpen, LayoutGrid, List } from 'lucide-react';
|
|
6
6
|
import Breadcrumb from '@/components/Breadcrumb';
|
|
@@ -33,16 +33,22 @@ function countFiles(node: FileNode): number {
|
|
|
33
33
|
const DIR_VIEW_KEY = 'mindos-dir-view';
|
|
34
34
|
|
|
35
35
|
function useDirViewPref() {
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
36
|
+
const view = useSyncExternalStore(
|
|
37
|
+
(onStoreChange) => {
|
|
38
|
+
const listener = () => onStoreChange();
|
|
39
|
+
window.addEventListener('mindos-dir-view-change', listener);
|
|
40
|
+
return () => window.removeEventListener('mindos-dir-view-change', listener);
|
|
41
|
+
},
|
|
42
|
+
() => {
|
|
43
|
+
const saved = localStorage.getItem(DIR_VIEW_KEY);
|
|
44
|
+
return (saved === 'list' || saved === 'grid') ? saved : 'grid';
|
|
45
|
+
},
|
|
46
|
+
() => 'grid' as const,
|
|
47
|
+
);
|
|
42
48
|
|
|
43
49
|
const setView = (v: 'grid' | 'list') => {
|
|
44
|
-
setViewState(v);
|
|
45
50
|
localStorage.setItem(DIR_VIEW_KEY, v);
|
|
51
|
+
window.dispatchEvent(new Event('mindos-dir-view-change'));
|
|
46
52
|
};
|
|
47
53
|
|
|
48
54
|
return [view, setView] as const;
|
|
@@ -26,20 +26,19 @@ export default function SettingsModal({ open, onClose }: SettingsModalProps) {
|
|
|
26
26
|
const [status, setStatus] = useState<'idle' | 'saved' | 'error' | 'load-error'>('idle');
|
|
27
27
|
const { t, locale, setLocale } = useLocale();
|
|
28
28
|
|
|
29
|
-
// Appearance state (localStorage-based)
|
|
30
|
-
const [font, setFont] = useState('lora');
|
|
31
|
-
const [contentWidth, setContentWidth] = useState('780px');
|
|
32
|
-
const [dark, setDark] = useState(
|
|
29
|
+
// Appearance state (localStorage-based) — read directly on mount; this component is client-only
|
|
30
|
+
const [font, setFont] = useState(() => localStorage.getItem('prose-font') ?? 'lora');
|
|
31
|
+
const [contentWidth, setContentWidth] = useState(() => localStorage.getItem('content-width') ?? '780px');
|
|
32
|
+
const [dark, setDark] = useState(() => {
|
|
33
|
+
const stored = localStorage.getItem('theme');
|
|
34
|
+
return stored ? stored === 'dark' : window.matchMedia('(prefers-color-scheme: dark)').matches;
|
|
35
|
+
});
|
|
33
36
|
// Plugin enabled state
|
|
34
37
|
const [pluginStates, setPluginStates] = useState<Record<string, boolean>>({});
|
|
35
38
|
|
|
36
39
|
useEffect(() => {
|
|
37
40
|
if (!open) return;
|
|
38
41
|
apiFetch<SettingsData>('/api/settings').then(setData).catch(() => setStatus('load-error'));
|
|
39
|
-
setFont(localStorage.getItem('prose-font') ?? 'lora');
|
|
40
|
-
setContentWidth(localStorage.getItem('content-width') ?? '780px');
|
|
41
|
-
const stored = localStorage.getItem('theme');
|
|
42
|
-
setDark(stored ? stored === 'dark' : window.matchMedia('(prefers-color-scheme: dark)').matches);
|
|
43
42
|
loadDisabledState();
|
|
44
43
|
const initial: Record<string, boolean> = {};
|
|
45
44
|
for (const r of getAllRenderers()) initial[r.id] = isRendererEnabled(r.id);
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { usePathname } from 'next/navigation';
|
|
4
|
+
import SidebarLayout from './SidebarLayout';
|
|
5
|
+
import { FileNode } from '@/lib/types';
|
|
6
|
+
|
|
7
|
+
interface ShellLayoutProps {
|
|
8
|
+
fileTree: FileNode[];
|
|
9
|
+
children: React.ReactNode;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export default function ShellLayout({ fileTree, children }: ShellLayoutProps) {
|
|
13
|
+
const pathname = usePathname();
|
|
14
|
+
if (pathname === '/login') return <>{children}</>;
|
|
15
|
+
return <SidebarLayout fileTree={fileTree}>{children}</SidebarLayout>;
|
|
16
|
+
}
|
|
@@ -1,25 +1,27 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { useSyncExternalStore } from 'react';
|
|
4
4
|
import { Sun, Moon } from 'lucide-react';
|
|
5
5
|
|
|
6
6
|
export default function ThemeToggle() {
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
7
|
+
const dark = useSyncExternalStore(
|
|
8
|
+
(onStoreChange) => {
|
|
9
|
+
const listener = () => onStoreChange();
|
|
10
|
+
window.addEventListener('mindos-theme-change', listener);
|
|
11
|
+
return () => window.removeEventListener('mindos-theme-change', listener);
|
|
12
|
+
},
|
|
13
|
+
() => {
|
|
14
|
+
const stored = localStorage.getItem('theme');
|
|
15
|
+
return stored ? stored === 'dark' : window.matchMedia('(prefers-color-scheme: dark)').matches;
|
|
16
|
+
},
|
|
17
|
+
() => true,
|
|
18
|
+
);
|
|
17
19
|
|
|
18
20
|
const toggle = () => {
|
|
19
21
|
const next = !dark;
|
|
20
|
-
setDark(next);
|
|
21
22
|
document.documentElement.classList.toggle('dark', next);
|
|
22
23
|
localStorage.setItem('theme', next ? 'dark' : 'light');
|
|
24
|
+
window.dispatchEvent(new Event('mindos-theme-change'));
|
|
23
25
|
};
|
|
24
26
|
|
|
25
27
|
return (
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { useState } from 'react';
|
|
3
|
+
import { useState, useSyncExternalStore } from 'react';
|
|
4
4
|
import { Copy, Check, RefreshCw, Trash2 } from 'lucide-react';
|
|
5
5
|
import type { SettingsData } from './types';
|
|
6
6
|
import { Field, Input, EnvBadge, SectionLabel } from './Primitives';
|
|
@@ -16,6 +16,12 @@ export function KnowledgeTab({ data, setData, t }: KnowledgeTabProps) {
|
|
|
16
16
|
const env = data.envOverrides ?? {};
|
|
17
17
|
const k = t.settings.knowledge;
|
|
18
18
|
|
|
19
|
+
const origin = useSyncExternalStore(
|
|
20
|
+
() => () => {},
|
|
21
|
+
() => `${window.location.protocol}//${window.location.hostname}`,
|
|
22
|
+
() => 'http://localhost',
|
|
23
|
+
);
|
|
24
|
+
|
|
19
25
|
const [showPassword, setShowPassword] = useState(false);
|
|
20
26
|
const isPasswordMasked = data.webPassword === '***set***';
|
|
21
27
|
|
|
@@ -118,7 +124,7 @@ export function KnowledgeTab({ data, setData, t }: KnowledgeTabProps) {
|
|
|
118
124
|
{k.authTokenMcpPort}: <code className="font-mono">{data.mcpPort}</code>
|
|
119
125
|
{displayToken && (
|
|
120
126
|
<> · MCP URL: <code className="font-mono select-all">
|
|
121
|
-
{
|
|
127
|
+
{`${origin}:${data.mcpPort}/mcp`}
|
|
122
128
|
</code></>
|
|
123
129
|
)}
|
|
124
130
|
</p>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { createContext, useContext,
|
|
3
|
+
import { createContext, useContext, useSyncExternalStore, ReactNode } from 'react';
|
|
4
4
|
import { Locale, messages, Messages } from './i18n';
|
|
5
5
|
|
|
6
6
|
interface LocaleContextValue {
|
|
@@ -15,17 +15,25 @@ const LocaleContext = createContext<LocaleContextValue>({
|
|
|
15
15
|
t: messages['en'],
|
|
16
16
|
});
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
const
|
|
18
|
+
function getLocaleSnapshot(): Locale {
|
|
19
|
+
const saved = localStorage.getItem('locale');
|
|
20
|
+
return saved === 'zh' ? 'zh' : 'en';
|
|
21
|
+
}
|
|
20
22
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
export function LocaleProvider({ children }: { children: ReactNode }) {
|
|
24
|
+
const locale = useSyncExternalStore(
|
|
25
|
+
(onStoreChange) => {
|
|
26
|
+
const listener = () => onStoreChange();
|
|
27
|
+
window.addEventListener('mindos-locale-change', listener);
|
|
28
|
+
return () => window.removeEventListener('mindos-locale-change', listener);
|
|
29
|
+
},
|
|
30
|
+
getLocaleSnapshot,
|
|
31
|
+
() => 'en' as Locale,
|
|
32
|
+
);
|
|
25
33
|
|
|
26
34
|
const setLocale = (l: Locale) => {
|
|
27
|
-
setLocaleState(l);
|
|
28
35
|
localStorage.setItem('locale', l);
|
|
36
|
+
window.dispatchEvent(new Event('mindos-locale-change'));
|
|
29
37
|
};
|
|
30
38
|
|
|
31
39
|
return (
|
package/bin/cli.js
CHANGED
|
@@ -131,6 +131,21 @@ function ensureAppDeps() {
|
|
|
131
131
|
// next (and other deps) must be resolvable from app/ for Turbopack to work.
|
|
132
132
|
const appNext = resolve(ROOT, 'app', 'node_modules', 'next', 'package.json');
|
|
133
133
|
if (!existsSync(appNext)) {
|
|
134
|
+
// Check npm is accessible before trying to run it.
|
|
135
|
+
try {
|
|
136
|
+
execSync('npm --version', { stdio: 'pipe' });
|
|
137
|
+
} catch {
|
|
138
|
+
console.error(red('\n✘ npm not found in PATH.\n'));
|
|
139
|
+
console.error(' MindOS needs npm to install its app dependencies on first run.');
|
|
140
|
+
console.error(' This usually means Node.js is installed via a version manager (nvm, fnm, volta, etc.)');
|
|
141
|
+
console.error(' that only loads in interactive shells, but not in /bin/sh.\n');
|
|
142
|
+
console.error(' Fix: add your Node.js bin directory to a profile that /bin/sh reads (~/.profile).');
|
|
143
|
+
console.error(' Example:');
|
|
144
|
+
console.error(dim(' echo \'export PATH="$HOME/.nvm/versions/node/$(node --version)/bin:$PATH"\' >> ~/.profile'));
|
|
145
|
+
console.error(dim(' source ~/.profile\n'));
|
|
146
|
+
console.error(' Then run `mindos start` again.\n');
|
|
147
|
+
process.exit(1);
|
|
148
|
+
}
|
|
134
149
|
console.log(yellow('Installing app dependencies (first run)...\n'));
|
|
135
150
|
// --no-workspaces: prevent npm from hoisting deps to monorepo root.
|
|
136
151
|
// When globally installed, deps must live in app/node_modules/ so that
|
|
@@ -397,6 +412,27 @@ async function waitForService(check, { retries = 10, intervalMs = 1000 } = {}) {
|
|
|
397
412
|
return check();
|
|
398
413
|
}
|
|
399
414
|
|
|
415
|
+
async function waitForHttp(port, { retries = 120, intervalMs = 2000, label = 'service' } = {}) {
|
|
416
|
+
process.stdout.write(cyan(` Waiting for ${label} to be ready`));
|
|
417
|
+
for (let i = 0; i < retries; i++) {
|
|
418
|
+
try {
|
|
419
|
+
const { request } = await import('node:http');
|
|
420
|
+
const ok = await new Promise((resolve) => {
|
|
421
|
+
const req = request({ hostname: '127.0.0.1', port, path: '/', method: 'HEAD', timeout: 1500 },
|
|
422
|
+
(res) => { res.resume(); resolve(res.statusCode < 500); });
|
|
423
|
+
req.on('error', () => resolve(false));
|
|
424
|
+
req.on('timeout', () => { req.destroy(); resolve(false); });
|
|
425
|
+
req.end();
|
|
426
|
+
});
|
|
427
|
+
if (ok) { process.stdout.write(` ${green('✔')}\n`); return true; }
|
|
428
|
+
} catch { /* not ready yet */ }
|
|
429
|
+
process.stdout.write('.');
|
|
430
|
+
await new Promise(r => setTimeout(r, intervalMs));
|
|
431
|
+
}
|
|
432
|
+
process.stdout.write(` ${red('✘')}\n`);
|
|
433
|
+
return false;
|
|
434
|
+
}
|
|
435
|
+
|
|
400
436
|
async function runGatewayCommand(sub) {
|
|
401
437
|
const platform = getPlatform();
|
|
402
438
|
if (!platform) {
|
|
@@ -551,6 +587,14 @@ const commands = {
|
|
|
551
587
|
console.log(cyan(`Installing MindOS as a background service (${platform})...`));
|
|
552
588
|
await runGatewayCommand('install');
|
|
553
589
|
await runGatewayCommand('start');
|
|
590
|
+
console.log(dim(' (First run may take a few minutes to install dependencies and build the app.)'));
|
|
591
|
+
console.log(dim(' Follow live progress with: mindos logs\n'));
|
|
592
|
+
const ready = await waitForHttp(Number(webPort), { retries: 120, intervalMs: 2000, label: 'Web UI' });
|
|
593
|
+
if (!ready) {
|
|
594
|
+
console.error(red('\n✘ Service started but Web UI did not become ready in time.'));
|
|
595
|
+
console.error(dim(' Check logs with: mindos logs\n'));
|
|
596
|
+
process.exit(1);
|
|
597
|
+
}
|
|
554
598
|
printStartupInfo(webPort, mcpPort);
|
|
555
599
|
console.log(`${green('✔ MindOS is running as a background service')}`);
|
|
556
600
|
console.log(dim(' View logs: mindos logs'));
|
|
@@ -686,6 +730,17 @@ ${dim('Shortcut: mindos start --daemon → install + start in one step')}
|
|
|
686
730
|
ok(`Node.js ${nodeVersion}`);
|
|
687
731
|
}
|
|
688
732
|
|
|
733
|
+
// 4b. npm reachable from /bin/sh
|
|
734
|
+
try {
|
|
735
|
+
const npmVersion = execSync('npm --version', { stdio: 'pipe' }).toString().trim();
|
|
736
|
+
ok(`npm ${npmVersion} reachable`);
|
|
737
|
+
} catch {
|
|
738
|
+
err('npm not found in PATH — app dependencies cannot be installed');
|
|
739
|
+
console.log(dim(' Node.js may be installed via nvm/fnm/volta and not visible to /bin/sh.'));
|
|
740
|
+
console.log(dim(' Fix: add your Node.js bin path to ~/.profile so non-interactive shells can find it.'));
|
|
741
|
+
hasError = true;
|
|
742
|
+
}
|
|
743
|
+
|
|
689
744
|
// 5. Build
|
|
690
745
|
if (!existsSync(resolve(ROOT, 'app', '.next'))) {
|
|
691
746
|
warn(`App not built yet — will build automatically on next ${dim('mindos start')}`);
|
|
@@ -736,7 +791,7 @@ ${dim('Shortcut: mindos start --daemon → install + start in one step')}
|
|
|
736
791
|
},
|
|
737
792
|
|
|
738
793
|
// ── update ─────────────────────────────────────────────────────────────────
|
|
739
|
-
update: () => {
|
|
794
|
+
update: async () => {
|
|
740
795
|
const currentVersion = (() => {
|
|
741
796
|
try { return JSON.parse(readFileSync(resolve(ROOT, 'package.json'), 'utf-8')).version; } catch { return '?'; }
|
|
742
797
|
})();
|
|
@@ -754,9 +809,35 @@ ${dim('Shortcut: mindos start --daemon → install + start in one step')}
|
|
|
754
809
|
})();
|
|
755
810
|
if (newVersion !== currentVersion) {
|
|
756
811
|
console.log(`\n${green(`✔ Updated ${currentVersion} → ${newVersion}`)}`);
|
|
757
|
-
console.log(dim(' Run `mindos start` — it will rebuild automatically.\n'));
|
|
758
812
|
} else {
|
|
759
813
|
console.log(`\n${green('✔ Already on the latest version')} ${dim(`(${currentVersion})`)}\n`);
|
|
814
|
+
return;
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
// If daemon is running, restart it so the new version takes effect immediately
|
|
818
|
+
const platform = getPlatform();
|
|
819
|
+
let daemonRunning = false;
|
|
820
|
+
if (platform === 'systemd') {
|
|
821
|
+
try { execSync('systemctl --user is-active mindos', { stdio: 'pipe' }); daemonRunning = true; } catch {}
|
|
822
|
+
} else if (platform === 'launchd') {
|
|
823
|
+
try { execSync(`launchctl print gui/${launchctlUid()}/com.mindos.app`, { stdio: 'pipe' }); daemonRunning = true; } catch {}
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
if (daemonRunning) {
|
|
827
|
+
console.log(cyan('\n Daemon is running — restarting to apply the new version...'));
|
|
828
|
+
await runGatewayCommand('stop');
|
|
829
|
+
await runGatewayCommand('start');
|
|
830
|
+
const webPort = process.env.MINDOS_WEB_PORT || '3000';
|
|
831
|
+
console.log(dim(' (Waiting for Web UI to come back up...)'));
|
|
832
|
+
const ready = await waitForHttp(Number(webPort), { retries: 120, intervalMs: 2000, label: 'Web UI' });
|
|
833
|
+
if (ready) {
|
|
834
|
+
console.log(green('✔ MindOS restarted and ready.\n'));
|
|
835
|
+
} else {
|
|
836
|
+
console.error(red('✘ MindOS did not come back up in time. Check logs: mindos logs\n'));
|
|
837
|
+
process.exit(1);
|
|
838
|
+
}
|
|
839
|
+
} else {
|
|
840
|
+
console.log(dim(' Run `mindos start` — it will rebuild automatically.\n'));
|
|
760
841
|
}
|
|
761
842
|
},
|
|
762
843
|
|
package/package.json
CHANGED