@geminilight/mindos 0.2.1 → 0.4.0

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 (82) hide show
  1. package/app/app/api/init/route.ts +7 -41
  2. package/app/app/api/mcp/agents/route.ts +72 -0
  3. package/app/app/api/mcp/install/route.ts +95 -0
  4. package/app/app/api/mcp/status/route.ts +47 -0
  5. package/app/app/api/settings/route.ts +3 -0
  6. package/app/app/api/setup/generate-token/route.ts +23 -0
  7. package/app/app/api/setup/route.ts +81 -0
  8. package/app/app/api/skills/route.ts +208 -0
  9. package/app/app/api/sync/route.ts +54 -3
  10. package/app/app/api/update-check/route.ts +52 -0
  11. package/app/app/globals.css +12 -0
  12. package/app/app/layout.tsx +4 -2
  13. package/app/app/login/page.tsx +20 -13
  14. package/app/app/page.tsx +22 -2
  15. package/app/app/setup/page.tsx +9 -0
  16. package/app/app/view/[...path]/ViewPageClient.tsx +47 -21
  17. package/app/app/view/[...path]/loading.tsx +1 -1
  18. package/app/app/view/[...path]/not-found.tsx +101 -0
  19. package/app/components/AskFab.tsx +1 -1
  20. package/app/components/AskModal.tsx +1 -1
  21. package/app/components/Backlinks.tsx +1 -1
  22. package/app/components/Breadcrumb.tsx +13 -3
  23. package/app/components/CsvView.tsx +5 -6
  24. package/app/components/DirView.tsx +42 -21
  25. package/app/components/FindInPage.tsx +211 -0
  26. package/app/components/HomeContent.tsx +97 -44
  27. package/app/components/JsonView.tsx +1 -2
  28. package/app/components/MarkdownEditor.tsx +1 -2
  29. package/app/components/OnboardingView.tsx +6 -7
  30. package/app/components/SettingsModal.tsx +5 -2
  31. package/app/components/SetupWizard.tsx +479 -0
  32. package/app/components/Sidebar.tsx +1 -1
  33. package/app/components/UpdateBanner.tsx +101 -0
  34. package/app/components/renderers/{AgentInspectorRenderer.tsx → agent-inspector/AgentInspectorRenderer.tsx} +13 -11
  35. package/app/components/renderers/agent-inspector/manifest.ts +14 -0
  36. package/app/components/renderers/{BacklinksRenderer.tsx → backlinks/BacklinksRenderer.tsx} +6 -6
  37. package/app/components/renderers/backlinks/manifest.ts +14 -0
  38. package/app/components/renderers/config/manifest.ts +14 -0
  39. package/app/components/renderers/csv/BoardView.tsx +12 -12
  40. package/app/components/renderers/csv/ConfigPanel.tsx +7 -8
  41. package/app/components/renderers/{CsvRenderer.tsx → csv/CsvRenderer.tsx} +8 -9
  42. package/app/components/renderers/csv/GalleryView.tsx +3 -3
  43. package/app/components/renderers/csv/TableView.tsx +4 -5
  44. package/app/components/renderers/csv/manifest.ts +14 -0
  45. package/app/components/renderers/{DiffRenderer.tsx → diff/DiffRenderer.tsx} +10 -9
  46. package/app/components/renderers/diff/manifest.ts +14 -0
  47. package/app/components/renderers/{GraphRenderer.tsx → graph/GraphRenderer.tsx} +4 -5
  48. package/app/components/renderers/graph/manifest.ts +14 -0
  49. package/app/components/renderers/{SummaryRenderer.tsx → summary/SummaryRenderer.tsx} +6 -6
  50. package/app/components/renderers/summary/manifest.ts +14 -0
  51. package/app/components/renderers/{TimelineRenderer.tsx → timeline/TimelineRenderer.tsx} +6 -6
  52. package/app/components/renderers/timeline/manifest.ts +14 -0
  53. package/app/components/renderers/{TodoRenderer.tsx → todo/TodoRenderer.tsx} +2 -2
  54. package/app/components/renderers/todo/manifest.ts +14 -0
  55. package/app/components/renderers/{WorkflowRenderer.tsx → workflow/WorkflowRenderer.tsx} +13 -13
  56. package/app/components/renderers/workflow/manifest.ts +14 -0
  57. package/app/components/settings/McpTab.tsx +549 -0
  58. package/app/components/settings/SyncTab.tsx +139 -50
  59. package/app/components/settings/types.ts +1 -1
  60. package/app/data/pages/home.png +0 -0
  61. package/app/lib/i18n.ts +270 -10
  62. package/app/lib/renderers/index.ts +20 -89
  63. package/app/lib/renderers/registry.ts +4 -1
  64. package/app/lib/settings.ts +15 -1
  65. package/app/lib/template.ts +45 -0
  66. package/app/package.json +1 -0
  67. package/app/types/semver.d.ts +8 -0
  68. package/bin/cli.js +137 -24
  69. package/bin/lib/build.js +53 -18
  70. package/bin/lib/colors.js +3 -1
  71. package/bin/lib/config.js +4 -0
  72. package/bin/lib/constants.js +2 -0
  73. package/bin/lib/debug.js +10 -0
  74. package/bin/lib/startup.js +21 -20
  75. package/bin/lib/stop.js +41 -3
  76. package/bin/lib/sync.js +65 -53
  77. package/bin/lib/update-check.js +94 -0
  78. package/bin/lib/utils.js +2 -2
  79. package/package.json +1 -1
  80. package/scripts/gen-renderer-index.js +57 -0
  81. package/scripts/setup.js +117 -1
  82. /package/app/components/renderers/{ConfigRenderer.tsx → config/ConfigRenderer.tsx} +0 -0
@@ -0,0 +1,52 @@
1
+ export const dynamic = 'force-dynamic';
2
+ import { NextResponse } from 'next/server';
3
+ import { gt } from 'semver';
4
+ import { readFileSync } from 'fs';
5
+ import { resolve } from 'path';
6
+
7
+ // Read version from package.json (not process.env.npm_package_version — unavailable in daemon mode)
8
+ let current = '0.0.0';
9
+ try {
10
+ const pkgPath = resolve(process.cwd(), '..', 'package.json');
11
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
12
+ current = pkg.version;
13
+ } catch {}
14
+
15
+ // npm registry sources: prefer China mirror, fallback to official
16
+ const REGISTRIES = [
17
+ 'https://registry.npmmirror.com/@geminilight/mindos/latest',
18
+ 'https://registry.npmjs.org/@geminilight/mindos/latest',
19
+ ];
20
+
21
+ export async function GET() {
22
+ let latest = current;
23
+
24
+ for (const url of REGISTRIES) {
25
+ try {
26
+ const res = await fetch(url, {
27
+ signal: AbortSignal.timeout(3000),
28
+ next: { revalidate: 300 }, // 5-minute ISR cache
29
+ });
30
+ if (res.ok) {
31
+ const data = await res.json();
32
+ latest = data.version;
33
+ break;
34
+ }
35
+ } catch {
36
+ continue;
37
+ }
38
+ }
39
+
40
+ let hasUpdate = false;
41
+ try {
42
+ hasUpdate = gt(latest, current);
43
+ } catch {
44
+ // Invalid version string from registry — treat as no update
45
+ }
46
+
47
+ return NextResponse.json({
48
+ current,
49
+ latest,
50
+ hasUpdate,
51
+ });
52
+ }
@@ -119,6 +119,9 @@ body {
119
119
  body { @apply bg-background text-foreground; }
120
120
  }
121
121
 
122
+ /* Utility: IBM Plex Mono display heading font */
123
+ .font-display { font-family: 'IBM Plex Mono', var(--font-ibm-plex-mono), monospace; }
124
+
122
125
  /* Content width — controlled by Settings > Appearance */
123
126
  :root { --content-width: 780px; }
124
127
  .content-width { max-width: var(--content-width-override, var(--content-width)); margin-left: auto; margin-right: auto; }
@@ -287,6 +290,15 @@ body {
287
290
  button, a { -webkit-tap-highlight-color: transparent; }
288
291
  }
289
292
 
293
+ /* Global focus-visible ring for interactive elements */
294
+ button:focus-visible,
295
+ a:focus-visible,
296
+ [role="button"]:focus-visible {
297
+ outline: 2px solid var(--amber);
298
+ outline-offset: 2px;
299
+ border-radius: 4px;
300
+ }
301
+
290
302
  /* ─── Tiptap WYSIWYG editor ─────────────────────────────────────────── */
291
303
  .wysiwyg-wrapper {
292
304
  height: 100%;
@@ -7,6 +7,7 @@ import { TooltipProvider } from '@/components/ui/tooltip';
7
7
  import { LocaleProvider } from '@/lib/LocaleContext';
8
8
  import ErrorBoundary from '@/components/ErrorBoundary';
9
9
  import RegisterSW from './register-sw';
10
+ import UpdateBanner from '@/components/UpdateBanner';
10
11
 
11
12
  const geistSans = Geist({
12
13
  variable: '--font-geist-sans',
@@ -20,13 +21,13 @@ const geistMono = Geist_Mono({
20
21
 
21
22
  const ibmPlexMono = IBM_Plex_Mono({
22
23
  variable: '--font-ibm-plex-mono',
23
- weight: ['400', '500', '600'],
24
+ weight: ['400', '600'],
24
25
  subsets: ['latin'],
25
26
  });
26
27
 
27
28
  const ibmPlexSans = IBM_Plex_Sans({
28
29
  variable: '--font-ibm-plex-sans',
29
- weight: ['300', '400', '500', '600'],
30
+ weight: ['400', '500', '600'],
30
31
  subsets: ['latin'],
31
32
  });
32
33
 
@@ -91,6 +92,7 @@ export default function RootLayout({
91
92
  suppressHydrationWarning
92
93
  >
93
94
  <LocaleProvider>
95
+ <UpdateBanner />
94
96
  <TooltipProvider delay={300}>
95
97
  <ErrorBoundary>
96
98
  <ShellLayout fileTree={fileTree}>
@@ -3,14 +3,18 @@
3
3
  import { useState, Suspense } from 'react';
4
4
  import { useRouter, useSearchParams } from 'next/navigation';
5
5
  import { Loader2, Lock } from 'lucide-react';
6
+ import { useLocale } from '@/lib/LocaleContext';
6
7
 
7
8
  function LoginForm() {
8
9
  const router = useRouter();
9
10
  const searchParams = useSearchParams();
11
+ const { t } = useLocale();
10
12
  const [password, setPassword] = useState('');
11
13
  const [loading, setLoading] = useState(false);
12
14
  const [error, setError] = useState('');
13
15
 
16
+ const loginT = t.login;
17
+
14
18
  async function handleSubmit(e: React.FormEvent) {
15
19
  e.preventDefault();
16
20
  setLoading(true);
@@ -29,11 +33,11 @@ function LoginForm() {
29
33
  : '/';
30
34
  router.replace(safe);
31
35
  } else {
32
- setError('Incorrect password. Please try again.');
36
+ setError(loginT?.incorrectPassword ?? 'Incorrect password. Please try again.');
33
37
  setPassword('');
34
38
  }
35
39
  } catch {
36
- setError('Connection error. Please try again.');
40
+ setError(loginT?.connectionError ?? 'Connection error. Please try again.');
37
41
  } finally {
38
42
  setLoading(false);
39
43
  }
@@ -61,26 +65,28 @@ function LoginForm() {
61
65
  <path d="M35,17.5 Q35,20 37.5,20 Q35,20 35,22.5 Q35,20 32.5,20 Q35,20 35,17.5 Z" fill="#FEF3C7"/>
62
66
  </g>
63
67
  </svg>
64
- <h1
65
- className="text-xl font-semibold text-foreground tracking-tight"
66
- style={{ fontFamily: "'IBM Plex Mono', monospace" }}
67
- >
68
+ <h1 className="text-xl font-semibold text-foreground tracking-tight font-display">
68
69
  MindOS
69
70
  </h1>
70
- <p className="text-sm text-muted-foreground">Enter your password to continue</p>
71
+ <p className="text-xs text-muted-foreground/70 italic">
72
+ {loginT?.tagline ?? 'You think here, Agents act there.'}
73
+ </p>
74
+ <p className="text-sm text-muted-foreground">
75
+ {loginT?.subtitle ?? 'Enter your password to continue'}
76
+ </p>
71
77
  </div>
72
78
 
73
79
  <form onSubmit={handleSubmit} className="space-y-4">
74
80
  <div className="space-y-1.5">
75
81
  <label className="text-sm font-medium text-foreground" htmlFor="password">
76
- Password
82
+ {loginT?.passwordLabel ?? 'Password'}
77
83
  </label>
78
84
  <input
79
85
  id="password"
80
86
  type="password"
81
87
  value={password}
82
88
  onChange={e => setPassword(e.target.value)}
83
- placeholder="Enter password"
89
+ placeholder={loginT?.passwordPlaceholder ?? 'Enter password'}
84
90
  autoFocus
85
91
  autoComplete="current-password"
86
92
  required
@@ -89,21 +95,22 @@ function LoginForm() {
89
95
  </div>
90
96
 
91
97
  {error && (
92
- <p className="text-xs text-destructive">{error}</p>
98
+ <p className="text-xs text-destructive" role="alert" aria-live="polite">{error}</p>
93
99
  )}
94
100
 
95
101
  <button
96
102
  type="submit"
97
103
  disabled={loading || !password}
98
- className="w-full flex items-center justify-center gap-2 py-2 px-4 rounded-lg text-sm font-medium transition-opacity disabled:opacity-50 disabled:cursor-not-allowed mt-2"
99
- style={{ background: 'var(--amber)', color: '#131210' }}
104
+ className="w-full flex items-center justify-center gap-2 py-2 px-4 rounded-lg text-sm font-medium transition-opacity disabled:opacity-50 disabled:cursor-not-allowed mt-2 bg-[var(--amber)] text-[#131210]"
100
105
  >
101
106
  {loading ? (
102
107
  <Loader2 size={14} className="animate-spin" />
103
108
  ) : (
104
109
  <Lock size={14} />
105
110
  )}
106
- {loading ? 'Signing in…' : 'Sign in'}
111
+ {loading
112
+ ? (loginT?.signingIn ?? 'Signing in…')
113
+ : (loginT?.signIn ?? 'Sign in')}
107
114
  </button>
108
115
  </form>
109
116
  </div>
package/app/app/page.tsx CHANGED
@@ -1,12 +1,32 @@
1
- import { getRecentlyModified } from '@/lib/fs';
1
+ import { redirect } from 'next/navigation';
2
+ import { readSettings } from '@/lib/settings';
3
+ import { getRecentlyModified, getFileContent } from '@/lib/fs';
4
+ import { getAllRenderers } from '@/lib/renderers/registry';
5
+ import '@/lib/renderers/index'; // registers all renderers
2
6
  import HomeContent from '@/components/HomeContent';
3
7
 
8
+ function getExistingFiles(paths: string[]): string[] {
9
+ return paths.filter(p => {
10
+ try { getFileContent(p); return true; } catch { return false; }
11
+ });
12
+ }
13
+
4
14
  export default function HomePage() {
15
+ const settings = readSettings();
16
+ if (settings.setupPending) redirect('/setup');
17
+
5
18
  let recent: { path: string; mtime: number }[] = [];
6
19
  try {
7
20
  recent = getRecentlyModified(15);
8
21
  } catch (err) {
9
22
  console.error('[HomePage] Failed to load recent files:', err);
10
23
  }
11
- return <HomeContent recent={recent} />;
24
+
25
+ // Derive plugin entry paths from registry — no hardcoded list needed
26
+ const entryPaths = getAllRenderers()
27
+ .map(r => r.entryPath)
28
+ .filter((p): p is string => !!p);
29
+ const existingFiles = getExistingFiles(entryPaths);
30
+
31
+ return <HomeContent recent={recent} existingFiles={existingFiles} />;
12
32
  }
@@ -0,0 +1,9 @@
1
+ import { redirect } from 'next/navigation';
2
+ import { readSettings } from '@/lib/settings';
3
+ import SetupWizard from '@/components/SetupWizard';
4
+
5
+ export default function SetupPage() {
6
+ const settings = readSettings();
7
+ if (!settings.setupPending) redirect('/');
8
+ return <SetupWizard />;
9
+ }
@@ -1,8 +1,9 @@
1
1
  'use client';
2
2
 
3
- import { useState, useTransition, useCallback, useEffect, useSyncExternalStore } from 'react';
3
+ import { useState, useTransition, useCallback, useEffect, useRef, useSyncExternalStore, useMemo, Suspense } from 'react';
4
4
  import { useRouter } from 'next/navigation';
5
- import { Edit3, Save, X, Loader2, LayoutTemplate } from 'lucide-react';
5
+ import { Edit3, Save, X, Loader2, LayoutTemplate, ArrowLeft } from 'lucide-react';
6
+ import { lazy } from 'react';
6
7
  import MarkdownView from '@/components/MarkdownView';
7
8
  import JsonView from '@/components/JsonView';
8
9
  import CsvView from '@/components/CsvView';
@@ -10,6 +11,7 @@ import Backlinks from '@/components/Backlinks';
10
11
  import Breadcrumb from '@/components/Breadcrumb';
11
12
  import MarkdownEditor, { MdViewMode } from '@/components/MarkdownEditor';
12
13
  import TableOfContents from '@/components/TableOfContents';
14
+ import FindInPage from '@/components/FindInPage';
13
15
  import { resolveRenderer } from '@/lib/renderers/registry';
14
16
  import { encodePath } from '@/lib/utils';
15
17
  import '@/lib/renderers/index'; // registers all renderers
@@ -67,6 +69,8 @@ export default function ViewPageClient({
67
69
  const [saveError, setSaveError] = useState<string | null>(null);
68
70
  const [saveSuccess, setSaveSuccess] = useState(false);
69
71
  const [mdViewMode, setMdViewMode] = useState<MdViewMode>('wysiwyg');
72
+ const [findOpen, setFindOpen] = useState(false);
73
+ const contentRef = useRef<HTMLDivElement>(null);
70
74
 
71
75
  const inferredName = filePath.split('/').pop() || 'Untitled.md';
72
76
  const [showSaveAs, setShowSaveAs] = useState(isDraft);
@@ -86,6 +90,14 @@ export default function ViewPageClient({
86
90
  const isCsv = extension === 'csv';
87
91
  const showRenderer = !editing && !effectiveUseRaw && !!renderer;
88
92
 
93
+ // Lazily resolve the renderer component for code-splitting
94
+ const LazyComponent = useMemo(() => {
95
+ if (!renderer) return null;
96
+ if (renderer.component) return renderer.component;
97
+ if (renderer.load) return lazy(renderer.load);
98
+ return null;
99
+ }, [renderer]);
100
+
89
101
  const handleEdit = useCallback(() => {
90
102
  setEditContent(savedContent);
91
103
  setEditing(true);
@@ -172,6 +184,10 @@ export default function ViewPageClient({
172
184
  e.preventDefault();
173
185
  if (editing) handleSave();
174
186
  }
187
+ if ((e.metaKey || e.ctrlKey) && e.key === 'f' && !editing) {
188
+ e.preventDefault();
189
+ setFindOpen(true);
190
+ }
175
191
  if (e.key === 'e' && !editing && document.activeElement?.tagName === 'BODY') {
176
192
  handleEdit();
177
193
  }
@@ -186,13 +202,20 @@ export default function ViewPageClient({
186
202
  {/* Top bar */}
187
203
  <div className="sticky top-[52px] md:top-0 z-20 border-b border-border px-4 md:px-6 py-2.5" style={{ background: 'var(--background)' }}>
188
204
  <div className="content-width xl:mr-[220px] flex items-center justify-between gap-2">
189
- <div className="min-w-0 flex-1">
205
+ <div className="min-w-0 flex-1 flex items-center gap-1.5">
206
+ <button
207
+ onClick={() => router.back()}
208
+ className="md:hidden p-1 -ml-1 rounded text-muted-foreground hover:text-foreground transition-colors shrink-0"
209
+ aria-label="Go back"
210
+ >
211
+ <ArrowLeft size={16} />
212
+ </button>
190
213
  <Breadcrumb filePath={filePath} />
191
214
  </div>
192
215
 
193
216
  <div className="flex items-center gap-1.5 md:gap-2 shrink-0">
194
217
  {saveSuccess && (
195
- <span className="text-xs flex items-center gap-1.5" style={{ color: '#7aad80', fontFamily: "'IBM Plex Mono', monospace" }}>
218
+ <span className="text-xs flex items-center gap-1.5 font-display" style={{ color: '#7aad80' }}>
196
219
  <span className="w-1.5 h-1.5 rounded-full" style={{ background: '#7aad80' }} />
197
220
  <span className="hidden sm:inline">saved</span>
198
221
  </span>
@@ -205,11 +228,10 @@ export default function ViewPageClient({
205
228
  {renderer && !editing && !isDraft && (
206
229
  <button
207
230
  onClick={handleToggleRaw}
208
- className="flex items-center gap-1.5 px-3 py-1.5 rounded-md text-xs font-medium transition-colors"
231
+ className="flex items-center gap-1.5 px-3 py-1.5 rounded-md text-xs font-medium transition-colors font-display"
209
232
  style={{
210
233
  background: effectiveUseRaw ? 'var(--muted)' : `${'var(--amber)'}22`,
211
234
  color: effectiveUseRaw ? 'var(--muted-foreground)' : 'var(--amber)',
212
- fontFamily: "'IBM Plex Mono', monospace",
213
235
  }}
214
236
  title={effectiveUseRaw ? `Switch to ${renderer.name}` : 'View raw'}
215
237
  >
@@ -221,8 +243,8 @@ export default function ViewPageClient({
221
243
  {!editing && !showRenderer && !isDraft && (
222
244
  <button
223
245
  onClick={handleEdit}
224
- className="flex items-center gap-1.5 px-3 py-1.5 rounded-md text-xs font-medium transition-colors"
225
- style={{ background: 'var(--muted)', color: 'var(--muted-foreground)', fontFamily: "'IBM Plex Mono', monospace" }}
246
+ className="flex items-center gap-1.5 px-3 py-1.5 rounded-md text-xs font-medium transition-colors font-display"
247
+ style={{ background: 'var(--muted)', color: 'var(--muted-foreground)' }}
226
248
  onMouseEnter={(e) => { e.currentTarget.style.color = 'var(--foreground)'; e.currentTarget.style.background = 'var(--accent)'; }}
227
249
  onMouseLeave={(e) => { e.currentTarget.style.color = 'var(--muted-foreground)'; e.currentTarget.style.background = 'var(--muted)'; }}
228
250
  >
@@ -235,8 +257,8 @@ export default function ViewPageClient({
235
257
  <button
236
258
  onClick={handleCancel}
237
259
  disabled={isPending}
238
- className="flex items-center gap-1.5 px-3 py-1.5 rounded-md text-xs font-medium transition-colors disabled:opacity-50"
239
- style={{ background: 'var(--muted)', color: 'var(--muted-foreground)', fontFamily: "'IBM Plex Mono', monospace" }}
260
+ className="flex items-center gap-1.5 px-3 py-1.5 rounded-md text-xs font-medium transition-colors disabled:opacity-50 font-display"
261
+ style={{ background: 'var(--muted)', color: 'var(--muted-foreground)' }}
240
262
  onMouseEnter={(e) => { e.currentTarget.style.background = 'var(--accent)'; }}
241
263
  onMouseLeave={(e) => { e.currentTarget.style.background = 'var(--muted)'; }}
242
264
  >
@@ -246,8 +268,8 @@ export default function ViewPageClient({
246
268
  <button
247
269
  onClick={isDraft && showSaveAs ? handleConfirmDraftSave : handleSave}
248
270
  disabled={isPending}
249
- className="flex items-center gap-1.5 px-3 py-1.5 rounded-md text-xs font-medium disabled:opacity-50"
250
- style={{ background: 'var(--amber)', color: '#131210', fontFamily: "'IBM Plex Mono', monospace" }}
271
+ className="flex items-center gap-1.5 px-3 py-1.5 rounded-md text-xs font-medium disabled:opacity-50 font-display"
272
+ style={{ background: 'var(--amber)', color: '#131210' }}
251
273
  >
252
274
  {isPending ? <Loader2 size={13} className="animate-spin" /> : <Save size={13} />}
253
275
  <span className="hidden sm:inline">Save</span>
@@ -309,18 +331,22 @@ export default function ViewPageClient({
309
331
  />
310
332
  )}
311
333
  </div>
312
- ) : showRenderer ? (
313
- <div className="content-width xl:mr-[220px]">
314
- <renderer.component
315
- filePath={filePath}
316
- content={savedContent}
317
- extension={extension}
318
- saveAction={handleRendererSave}
319
- />
334
+ ) : showRenderer && LazyComponent ? (
335
+ <div ref={contentRef} className="content-width xl:mr-[220px]">
336
+ {findOpen && <FindInPage containerRef={contentRef} onClose={() => setFindOpen(false)} />}
337
+ <Suspense fallback={<div className="flex items-center justify-center py-12"><Loader2 size={20} className="animate-spin text-muted-foreground" /></div>}>
338
+ <LazyComponent
339
+ filePath={filePath}
340
+ content={savedContent}
341
+ extension={extension}
342
+ saveAction={handleRendererSave}
343
+ />
344
+ </Suspense>
320
345
  <Backlinks filePath={filePath} />
321
346
  </div>
322
347
  ) : (
323
- <div className="content-width xl:mr-[220px]">
348
+ <div ref={contentRef} className="content-width xl:mr-[220px]">
349
+ {findOpen && <FindInPage containerRef={contentRef} onClose={() => setFindOpen(false)} />}
324
350
  {extension === 'csv' ? (
325
351
  <CsvView
326
352
  content={savedContent}
@@ -6,7 +6,7 @@ export default function Loading() {
6
6
  className="w-6 h-6 border-2 rounded-full animate-spin"
7
7
  style={{ borderColor: 'var(--border)', borderTopColor: 'var(--amber)' }}
8
8
  />
9
- <span className="text-xs text-muted-foreground" style={{ fontFamily: "'IBM Plex Mono', monospace" }}>
9
+ <span className="text-xs text-muted-foreground font-display">
10
10
  Loading...
11
11
  </span>
12
12
  </div>
@@ -0,0 +1,101 @@
1
+ 'use client';
2
+
3
+ import { usePathname, useRouter } from 'next/navigation';
4
+ import { FilePlus, Home, ArrowLeft } from 'lucide-react';
5
+ import { useState } from 'react';
6
+ import { useLocale } from '@/lib/LocaleContext';
7
+ import { encodePath } from '@/lib/utils';
8
+
9
+ export default function ViewNotFound() {
10
+ const pathname = usePathname();
11
+ const router = useRouter();
12
+ const { t } = useLocale();
13
+ const [creating, setCreating] = useState(false);
14
+
15
+ // Extract the attempted file path from /view/...
16
+ const filePath = pathname.startsWith('/view/')
17
+ ? decodeURIComponent(pathname.slice('/view/'.length))
18
+ : '';
19
+
20
+ const parentDir = filePath.includes('/')
21
+ ? filePath.split('/').slice(0, -1).join('/')
22
+ : '';
23
+
24
+ const isMd = filePath.endsWith('.md') || !filePath.includes('.');
25
+ const displayPath = filePath || 'unknown';
26
+
27
+ const notFoundT = t.notFound;
28
+
29
+ const handleCreate = async () => {
30
+ setCreating(true);
31
+ try {
32
+ const target = isMd && !filePath.includes('.') ? `${filePath}.md` : filePath;
33
+ const res = await fetch('/api/files', {
34
+ method: 'PUT',
35
+ headers: { 'Content-Type': 'application/json' },
36
+ body: JSON.stringify({ path: target, content: '' }),
37
+ });
38
+ if (res.ok) {
39
+ router.replace(`/view/${encodePath(target)}`);
40
+ router.refresh();
41
+ }
42
+ } catch {
43
+ // silent — user can retry
44
+ } finally {
45
+ setCreating(false);
46
+ }
47
+ };
48
+
49
+ return (
50
+ <div className="content-width px-4 md:px-6 py-16 md:py-24 flex flex-col items-center text-center">
51
+ <div className="w-12 h-12 rounded-xl bg-muted flex items-center justify-center mb-6">
52
+ <FilePlus size={24} className="text-muted-foreground" />
53
+ </div>
54
+
55
+ <h1 className="text-lg font-semibold text-foreground mb-2 font-display">
56
+ {notFoundT?.title ?? 'File not found'}
57
+ </h1>
58
+
59
+ <p className="text-sm text-muted-foreground mb-1">
60
+ <code className="px-1.5 py-0.5 rounded bg-muted font-mono text-xs">{displayPath}</code>
61
+ </p>
62
+ <p className="text-sm text-muted-foreground mb-8">
63
+ {notFoundT?.description ?? 'This file does not exist in your knowledge base.'}
64
+ </p>
65
+
66
+ <div className="flex flex-wrap items-center justify-center gap-3">
67
+ {isMd && (
68
+ <button
69
+ onClick={handleCreate}
70
+ disabled={creating}
71
+ className="flex items-center gap-2 px-4 py-2.5 text-sm font-medium rounded-lg transition-colors disabled:opacity-50"
72
+ style={{ background: 'var(--amber)', color: '#131210' }}
73
+ >
74
+ <FilePlus size={14} />
75
+ {creating
76
+ ? (notFoundT?.creating ?? 'Creating...')
77
+ : (notFoundT?.createButton ?? 'Create this file')}
78
+ </button>
79
+ )}
80
+
81
+ {parentDir && (
82
+ <button
83
+ onClick={() => router.push(`/view/${encodePath(parentDir)}`)}
84
+ className="flex items-center gap-2 px-4 py-2.5 text-sm font-medium rounded-lg border border-border text-muted-foreground hover:text-foreground hover:bg-muted transition-colors"
85
+ >
86
+ <ArrowLeft size={14} />
87
+ {notFoundT?.goToParent ?? 'Go to parent folder'}
88
+ </button>
89
+ )}
90
+
91
+ <button
92
+ onClick={() => router.push('/')}
93
+ className="flex items-center gap-2 px-4 py-2.5 text-sm font-medium rounded-lg border border-border text-muted-foreground hover:text-foreground hover:bg-muted transition-colors"
94
+ >
95
+ <Home size={14} />
96
+ {notFoundT?.goHome ?? 'Home'}
97
+ </button>
98
+ </div>
99
+ </div>
100
+ );
101
+ }
@@ -31,9 +31,9 @@ export default function AskFab() {
31
31
  active:scale-95
32
32
  cursor-pointer
33
33
  overflow-hidden
34
+ font-display
34
35
  "
35
36
  style={{
36
- fontFamily: "'IBM Plex Mono', monospace",
37
37
  background: 'linear-gradient(135deg, #b07c2e 0%, #c8873a 50%, #d4943f 100%)',
38
38
  marginBottom: 'env(safe-area-inset-bottom, 0px)',
39
39
  }}
@@ -244,7 +244,7 @@ export default function AskModal({ open, onClose, currentFile }: AskModalProps)
244
244
  <div className="absolute top-2 left-1/2 -translate-x-1/2 w-8 h-1 rounded-full bg-muted-foreground/20 md:hidden" />
245
245
  <div className="flex items-center gap-2 text-sm font-medium text-foreground">
246
246
  <Sparkles size={15} style={{ color: 'var(--amber)' }} />
247
- <span style={{ fontFamily: "'IBM Plex Mono', monospace" }}>{t.ask.title}</span>
247
+ <span className="font-display">{t.ask.title}</span>
248
248
  {currentFile && (
249
249
  <span className="text-xs text-muted-foreground font-normal truncate max-w-[200px]">
250
250
  — {currentFile.split('/').pop()}
@@ -31,7 +31,7 @@ export default function Backlinks({ filePath }: { filePath: string }) {
31
31
  <div className="mt-12 pt-8 border-t border-border">
32
32
  <div className="flex items-center gap-2 mb-6 text-muted-foreground">
33
33
  <LinkIcon size={16} className="text-amber-500/70" />
34
- <h3 className="text-sm font-semibold tracking-wider uppercase" style={{ fontFamily: "'IBM Plex Mono', monospace" }}>
34
+ <h3 className="text-sm font-semibold tracking-wider uppercase font-display">
35
35
  {t.common?.relatedFiles || 'Related Files'}
36
36
  </h3>
37
37
  <span className="text-xs bg-muted px-1.5 py-0.5 rounded-full font-mono">
@@ -1,12 +1,19 @@
1
1
  'use client';
2
2
 
3
3
  import Link from 'next/link';
4
- import { ChevronRight, Home } from 'lucide-react';
4
+ import { ChevronRight, Home, FileText, Table, Folder } from 'lucide-react';
5
+
6
+ function FileTypeIcon({ name }: { name: string }) {
7
+ const ext = name.includes('.') ? name.slice(name.lastIndexOf('.')).toLowerCase() : '';
8
+ if (ext === '.csv') return <Table size={13} className="text-emerald-400 shrink-0" />;
9
+ if (ext) return <FileText size={13} className="text-zinc-400 shrink-0" />;
10
+ return <Folder size={13} className="text-yellow-400 shrink-0" />;
11
+ }
5
12
 
6
13
  export default function Breadcrumb({ filePath }: { filePath: string }) {
7
14
  const parts = filePath.split('/');
8
15
  return (
9
- <nav className="flex items-center gap-1 text-xs text-muted-foreground flex-wrap" style={{ fontFamily: "'IBM Plex Mono', monospace" }}>
16
+ <nav className="flex items-center gap-1 text-xs text-muted-foreground flex-wrap font-display">
10
17
  <Link href="/" className="hover:text-foreground transition-colors">
11
18
  <Home size={14} />
12
19
  </Link>
@@ -17,7 +24,10 @@ export default function Breadcrumb({ filePath }: { filePath: string }) {
17
24
  <span key={i} className="flex items-center gap-1">
18
25
  <ChevronRight size={12} className="text-muted-foreground/50" />
19
26
  {isLast ? (
20
- <span className="text-foreground font-medium" suppressHydrationWarning>{part}</span>
27
+ <span className="flex items-center gap-1.5 text-foreground font-medium" suppressHydrationWarning>
28
+ <FileTypeIcon name={part} />
29
+ {part}
30
+ </span>
21
31
  ) : (
22
32
  <Link href={href} className="hover:text-foreground transition-colors truncate max-w-[200px]" suppressHydrationWarning>
23
33
  {part}
@@ -218,7 +218,6 @@ export default function CsvView({ content: initialContent, appendAction, saveAct
218
218
  style={{
219
219
  color: 'var(--foreground)',
220
220
  borderBottom: '1px solid var(--border)',
221
- fontFamily: "'IBM Plex Sans', sans-serif",
222
221
  fontSize: '0.75rem',
223
222
  letterSpacing: '0.04em',
224
223
  textTransform: 'uppercase',
@@ -296,15 +295,15 @@ export default function CsvView({ content: initialContent, appendAction, saveAct
296
295
  className="px-4 py-2 flex items-center justify-between"
297
296
  style={{ background: 'var(--muted)', borderTop: '1px solid var(--border)' }}
298
297
  >
299
- <span className="text-xs" style={{ color: 'var(--muted-foreground)', fontFamily: "'IBM Plex Mono', monospace" }}>
298
+ <span className="text-xs font-display" style={{ color: 'var(--muted-foreground)' }}>
300
299
  {rows.length} rows · {headers.length} cols
301
300
  </span>
302
301
 
303
302
  {canEdit && !showAdd && (
304
303
  <button
305
304
  onClick={() => setShowAdd(true)}
306
- className="flex items-center gap-1 text-xs px-2.5 py-1 rounded-md transition-colors"
307
- style={{ color: 'var(--amber)', background: 'var(--amber-dim)', fontFamily: "'IBM Plex Mono', monospace" }}
305
+ className="flex items-center gap-1 text-xs px-2.5 py-1 rounded-md transition-colors font-display"
306
+ style={{ color: 'var(--amber)', background: 'var(--amber-dim)' }}
308
307
  >
309
308
  <Plus size={12} />
310
309
  Add row
@@ -313,8 +312,8 @@ export default function CsvView({ content: initialContent, appendAction, saveAct
313
312
  {showAdd && (
314
313
  <button
315
314
  onClick={() => setShowAdd(false)}
316
- className="text-xs px-2.5 py-1 rounded-md transition-colors"
317
- style={{ color: 'var(--muted-foreground)', fontFamily: "'IBM Plex Mono', monospace" }}
315
+ className="text-xs px-2.5 py-1 rounded-md transition-colors font-display"
316
+ style={{ color: 'var(--muted-foreground)' }}
318
317
  >
319
318
  Cancel
320
319
  </button>