@geminilight/mindos 0.5.27 → 0.5.29

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 (32) hide show
  1. package/app/app/api/update/route.ts +41 -0
  2. package/app/app/explore/page.tsx +12 -0
  3. package/app/components/ActivityBar.tsx +14 -7
  4. package/app/components/GuideCard.tsx +21 -7
  5. package/app/components/HomeContent.tsx +12 -1
  6. package/app/components/KeyboardShortcuts.tsx +102 -0
  7. package/app/components/Panel.tsx +12 -7
  8. package/app/components/SidebarLayout.tsx +18 -1
  9. package/app/components/UpdateBanner.tsx +19 -21
  10. package/app/components/explore/ExploreContent.tsx +100 -0
  11. package/app/components/explore/UseCaseCard.tsx +50 -0
  12. package/app/components/explore/use-cases.ts +30 -0
  13. package/app/components/panels/AgentsPanel.tsx +86 -95
  14. package/app/components/panels/PluginsPanel.tsx +9 -6
  15. package/app/components/settings/AiTab.tsx +5 -3
  16. package/app/components/settings/McpServerStatus.tsx +12 -6
  17. package/app/components/settings/SettingsContent.tsx +5 -2
  18. package/app/components/settings/UpdateTab.tsx +195 -0
  19. package/app/components/settings/types.ts +1 -1
  20. package/app/components/walkthrough/WalkthroughOverlay.tsx +224 -0
  21. package/app/components/walkthrough/WalkthroughProvider.tsx +133 -0
  22. package/app/components/walkthrough/WalkthroughTooltip.tsx +129 -0
  23. package/app/components/walkthrough/index.ts +3 -0
  24. package/app/components/walkthrough/steps.ts +21 -0
  25. package/app/lib/i18n-en.ts +168 -8
  26. package/app/lib/i18n-zh.ts +167 -7
  27. package/app/lib/settings.ts +4 -0
  28. package/app/next.config.ts +1 -1
  29. package/app/package.json +1 -0
  30. package/bin/lib/mcp-spawn.js +13 -2
  31. package/mcp/src/index.ts +3 -2
  32. package/package.json +1 -1
@@ -0,0 +1,41 @@
1
+ export const dynamic = 'force-dynamic';
2
+ import { NextResponse } from 'next/server';
3
+ import { spawn } from 'node:child_process';
4
+ import { resolve } from 'node:path';
5
+
6
+ /**
7
+ * POST /api/update — trigger `mindos update` as a detached child process.
8
+ *
9
+ * Similar to /api/restart: spawns the CLI command and returns immediately.
10
+ * The update process will npm install, remove build stamp, and restart
11
+ * the server. The current process will be killed during restart.
12
+ */
13
+ export async function POST() {
14
+ try {
15
+ const cliPath = process.env.MINDOS_CLI_PATH || resolve(process.cwd(), '..', 'bin', 'cli.js');
16
+ const nodeBin = process.env.MINDOS_NODE_BIN || process.execPath;
17
+
18
+ // Strip MINDOS_* env vars so the child reads fresh config
19
+ const childEnv = { ...process.env };
20
+ delete childEnv.MINDOS_WEB_PORT;
21
+ delete childEnv.MINDOS_MCP_PORT;
22
+ delete childEnv.MIND_ROOT;
23
+ delete childEnv.AUTH_TOKEN;
24
+ delete childEnv.WEB_PASSWORD;
25
+
26
+ const child = spawn(nodeBin, [cliPath, 'update'], {
27
+ detached: true,
28
+ stdio: 'ignore',
29
+ env: childEnv,
30
+ });
31
+ child.unref();
32
+
33
+ // Unlike /api/restart, we do NOT process.exit() here.
34
+ // `mindos update` will npm install first (30s+), then restart which
35
+ // kills this process. Exiting early would break the response.
36
+ return NextResponse.json({ ok: true });
37
+ } catch (err) {
38
+ const message = err instanceof Error ? err.message : String(err);
39
+ return NextResponse.json({ error: message }, { status: 500 });
40
+ }
41
+ }
@@ -0,0 +1,12 @@
1
+ import { redirect } from 'next/navigation';
2
+ import { readSettings } from '@/lib/settings';
3
+ import ExploreContent from '@/components/explore/ExploreContent';
4
+
5
+ export const dynamic = 'force-dynamic';
6
+
7
+ export default function ExplorePage() {
8
+ const settings = readSettings();
9
+ if (settings.setupPending) redirect('/setup');
10
+
11
+ return <ExploreContent />;
12
+ }
@@ -3,6 +3,7 @@
3
3
  import { useRef, useCallback } from 'react';
4
4
  import Link from 'next/link';
5
5
  import { FolderTree, Search, Settings, RefreshCw, Blocks, Bot, ChevronLeft, ChevronRight } from 'lucide-react';
6
+ import { useLocale } from '@/lib/LocaleContext';
6
7
  import { DOT_COLORS, getStatusLevel } from './SyncStatusBar';
7
8
  import type { SyncStatus } from './settings/SyncTab';
8
9
  import Logo from './Logo';
@@ -32,9 +33,11 @@ interface RailButtonProps {
32
33
  buttonRef?: React.Ref<HTMLButtonElement>;
33
34
  /** Optional overlay badge (e.g. status dot) rendered inside the button */
34
35
  badge?: React.ReactNode;
36
+ /** Optional data-walkthrough attribute for interactive walkthrough targeting */
37
+ walkthroughId?: string;
35
38
  }
36
39
 
37
- function RailButton({ icon, label, shortcut, active = false, expanded, onClick, buttonRef, badge }: RailButtonProps) {
40
+ function RailButton({ icon, label, shortcut, active = false, expanded, onClick, buttonRef, badge, walkthroughId }: RailButtonProps) {
38
41
  return (
39
42
  <button
40
43
  ref={buttonRef}
@@ -42,6 +45,7 @@ function RailButton({ icon, label, shortcut, active = false, expanded, onClick,
42
45
  aria-pressed={active}
43
46
  aria-label={label}
44
47
  title={expanded ? undefined : (shortcut ? `${label} (${shortcut})` : label)}
48
+ data-walkthrough={walkthroughId}
45
49
  className={`
46
50
  relative flex items-center ${expanded ? 'justify-start px-3 w-full' : 'justify-center w-10'} h-10 rounded-md transition-colors
47
51
  ${active
@@ -79,6 +83,7 @@ export default function ActivityBar({
79
83
  }: ActivityBarProps) {
80
84
  const lastClickRef = useRef(0);
81
85
  const syncBtnRef = useRef<HTMLButtonElement>(null);
86
+ const { t } = useLocale();
82
87
 
83
88
  /** Debounce rapid clicks (300ms) — shared across all Rail buttons */
84
89
  const debounced = useCallback((fn: () => void) => {
@@ -109,6 +114,7 @@ export default function ActivityBar({
109
114
  role="toolbar"
110
115
  aria-label="Navigation"
111
116
  aria-orientation="vertical"
117
+ data-walkthrough="activity-bar"
112
118
  >
113
119
  {/* Content wrapper — overflow-hidden prevents text flash during width transitions */}
114
120
  <div className="flex flex-col h-full w-full overflow-hidden">
@@ -126,10 +132,10 @@ export default function ActivityBar({
126
132
 
127
133
  {/* ── Middle: Core panel toggles ── */}
128
134
  <div className={`flex flex-col ${expanded ? 'px-1.5' : 'items-center'} gap-1 py-2`}>
129
- <RailButton icon={<FolderTree size={18} />} label="Files" active={activePanel === 'files'} expanded={expanded} onClick={() => toggle('files')} />
130
- <RailButton icon={<Search size={18} />} label="Search" shortcut="⌘K" active={activePanel === 'search'} expanded={expanded} onClick={() => toggle('search')} />
131
- <RailButton icon={<Blocks size={18} />} label="Plugins" active={activePanel === 'plugins'} expanded={expanded} onClick={() => toggle('plugins')} />
132
- <RailButton icon={<Bot size={18} />} label="Agents" active={activePanel === 'agents'} expanded={expanded} onClick={() => toggle('agents')} />
135
+ <RailButton icon={<FolderTree size={18} />} label={t.sidebar.files} active={activePanel === 'files'} expanded={expanded} onClick={() => toggle('files')} walkthroughId="files-panel" />
136
+ <RailButton icon={<Search size={18} />} label={t.sidebar.searchTitle} shortcut="⌘K" active={activePanel === 'search'} expanded={expanded} onClick={() => toggle('search')} walkthroughId="search-button" />
137
+ <RailButton icon={<Blocks size={18} />} label={t.sidebar.plugins} active={activePanel === 'plugins'} expanded={expanded} onClick={() => toggle('plugins')} />
138
+ <RailButton icon={<Bot size={18} />} label={t.sidebar.agents} active={activePanel === 'agents'} expanded={expanded} onClick={() => toggle('agents')} />
133
139
  </div>
134
140
 
135
141
  {/* ── Spacer ── */}
@@ -140,14 +146,15 @@ export default function ActivityBar({
140
146
  <div className={`flex flex-col ${expanded ? 'px-1.5' : 'items-center'} gap-1 py-2`}>
141
147
  <RailButton
142
148
  icon={<Settings size={18} />}
143
- label="Settings"
149
+ label={t.sidebar.settingsTitle}
144
150
  shortcut="⌘,"
145
151
  expanded={expanded}
146
152
  onClick={() => debounced(onSettingsClick)}
153
+ walkthroughId="settings-button"
147
154
  />
148
155
  <RailButton
149
156
  icon={<RefreshCw size={18} />}
150
- label="Sync"
157
+ label={t.sidebar.syncLabel}
151
158
  expanded={expanded}
152
159
  buttonRef={syncBtnRef}
153
160
  badge={syncBadge}
@@ -2,8 +2,10 @@
2
2
 
3
3
  import { useState, useEffect, useCallback, useRef } from 'react';
4
4
  import { X, Sparkles, FolderOpen, MessageCircle, RefreshCw, Check, ChevronRight } from 'lucide-react';
5
+ import Link from 'next/link';
5
6
  import { useLocale } from '@/lib/LocaleContext';
6
7
  import { openAskModal } from '@/hooks/useAskModal';
8
+ import { walkthroughSteps } from './walkthrough/steps';
7
9
  import type { GuideState } from '@/lib/settings';
8
10
 
9
11
  const DIR_ICONS: Record<string, string> = {
@@ -47,20 +49,18 @@ export default function GuideCard({ onNavigate }: GuideCardProps) {
47
49
  useEffect(() => {
48
50
  fetchGuideState();
49
51
 
50
- // ?welcome=1 first visit, auto-expand explore task
51
- const params = new URLSearchParams(window.location.search);
52
- if (params.get('welcome') === '1') {
52
+ // Listen for walkthrough-triggered first visit (WalkthroughProvider owns ?welcome=1 detection)
53
+ const handleFirstVisit = () => {
53
54
  setIsFirstVisit(true);
54
- const url = new URL(window.location.href);
55
- url.searchParams.delete('welcome');
56
- window.history.replaceState({}, '', url.pathname + (url.search || ''));
57
- }
55
+ };
56
+ window.addEventListener('mindos:first-visit', handleFirstVisit);
58
57
 
59
58
  // Re-fetch when guide state is updated (e.g. after AskFab patches askedAI)
60
59
  const handleGuideUpdate = () => fetchGuideState();
61
60
  window.addEventListener('focus', handleGuideUpdate);
62
61
  window.addEventListener('guide-state-updated', handleGuideUpdate);
63
62
  return () => {
63
+ window.removeEventListener('mindos:first-visit', handleFirstVisit);
64
64
  window.removeEventListener('focus', handleGuideUpdate);
65
65
  window.removeEventListener('guide-state-updated', handleGuideUpdate);
66
66
  };
@@ -147,6 +147,13 @@ export default function GuideCard({ onNavigate }: GuideCardProps) {
147
147
  // Don't render if no active guide
148
148
  if (!guideState) return null;
149
149
 
150
+ // Hide GuideCard while walkthrough is active
151
+ const walkthroughActive = guideState.walkthroughStep !== undefined
152
+ && guideState.walkthroughStep >= 0
153
+ && guideState.walkthroughStep < walkthroughSteps.length
154
+ && !guideState.walkthroughDismissed;
155
+ if (walkthroughActive) return null;
156
+
150
157
  const step1Done = guideState.step1Done;
151
158
  const step2Done = guideState.askedAI;
152
159
  const allDone = step1Done && step2Done;
@@ -164,6 +171,13 @@ export default function GuideCard({ onNavigate }: GuideCardProps) {
164
171
  <span className="text-sm font-semibold flex-1" style={{ color: 'var(--foreground)' }}>
165
172
  ✨ {g.done.titleFinal}
166
173
  </span>
174
+ <Link
175
+ href="/explore"
176
+ className="text-xs font-medium transition-colors hover:opacity-80"
177
+ style={{ color: 'var(--amber)' }}
178
+ >
179
+ {t.walkthrough.exploreCta}
180
+ </Link>
167
181
  <button onClick={handleDismiss} className="p-1 rounded hover:bg-muted transition-colors"
168
182
  style={{ color: 'var(--muted-foreground)' }}>
169
183
  <X size={14} />
@@ -1,7 +1,7 @@
1
1
  'use client';
2
2
 
3
3
  import Link from 'next/link';
4
- import { FileText, Table, Clock, Sparkles, Puzzle, ArrowRight, FilePlus, Search, ChevronDown } from 'lucide-react';
4
+ import { FileText, Table, Clock, Sparkles, Puzzle, ArrowRight, FilePlus, Search, ChevronDown, Compass } from 'lucide-react';
5
5
  import { useState, useEffect, useRef } from 'react';
6
6
  import { useLocale } from '@/lib/LocaleContext';
7
7
  import { encodePath, relativeTime } from '@/lib/utils';
@@ -94,6 +94,7 @@ export default function HomeContent({ recent, existingFiles }: { recent: RecentF
94
94
  <button
95
95
  onClick={triggerAsk}
96
96
  title="⌘/"
97
+ data-walkthrough="ask-button"
97
98
  className="flex-1 flex items-center gap-3 px-4 py-3 rounded-xl border transition-all duration-150 hover:border-amber-500/50 hover:bg-amber-500/8"
98
99
  style={{ background: 'var(--card)', borderColor: 'var(--border)' }}
99
100
  >
@@ -153,6 +154,16 @@ export default function HomeContent({ recent, existingFiles }: { recent: RecentF
153
154
  <FilePlus size={14} />
154
155
  <span>{t.home.newNote}</span>
155
156
  </Link>
157
+ <Link
158
+ href="/explore"
159
+ className="inline-flex items-center gap-2 px-3.5 py-2 rounded-lg text-sm font-medium transition-all duration-150 hover:translate-x-0.5"
160
+ style={{
161
+ color: 'var(--amber)',
162
+ }}
163
+ >
164
+ <Compass size={14} />
165
+ <span>{t.explore.title}</span>
166
+ </Link>
156
167
  </div>
157
168
 
158
169
  </div>
@@ -0,0 +1,102 @@
1
+ 'use client';
2
+
3
+ import { useState, useEffect, useMemo } from 'react';
4
+ import { X } from 'lucide-react';
5
+ import { useLocale } from '@/lib/LocaleContext';
6
+
7
+ export default function KeyboardShortcuts() {
8
+ const [open, setOpen] = useState(false);
9
+ const { t } = useLocale();
10
+ const s = t.shortcutPanel;
11
+
12
+ const isMac = typeof navigator !== 'undefined' && /Mac|iPhone|iPad/.test(navigator.userAgent);
13
+ const mod = isMac ? '⌘' : 'Ctrl';
14
+
15
+ const shortcuts = useMemo(() => [
16
+ { keys: `${mod} K`, label: s.toggleSearch, section: s.navigation },
17
+ { keys: `${mod} /`, label: s.toggleAskAI, section: s.navigation },
18
+ { keys: `${mod} ,`, label: s.openSettings, section: s.navigation },
19
+ { keys: `${mod} ?`, label: s.keyboardShortcuts, section: s.navigation },
20
+ { keys: 'Esc', label: s.closePanel, section: s.panelsSection },
21
+ { keys: `${mod} S`, label: s.saveFile, section: s.editor },
22
+ { keys: `${mod} Z`, label: s.undo, section: s.editor },
23
+ { keys: `${mod} Shift Z`, label: s.redo, section: s.editor },
24
+ ], [mod, s]);
25
+
26
+ useEffect(() => {
27
+ const handler = (e: KeyboardEvent) => {
28
+ if ((e.metaKey || e.ctrlKey) && e.shiftKey && e.key === '/') {
29
+ e.preventDefault();
30
+ setOpen(v => !v);
31
+ }
32
+ if (e.key === 'Escape' && open) {
33
+ setOpen(false);
34
+ }
35
+ };
36
+ window.addEventListener('keydown', handler);
37
+ return () => window.removeEventListener('keydown', handler);
38
+ }, [open]);
39
+
40
+ if (!open) return null;
41
+
42
+ const sections = [...new Set(shortcuts.map(s => s.section))];
43
+
44
+ return (
45
+ <div
46
+ className="fixed inset-0 z-50 flex items-center justify-center modal-backdrop"
47
+ onClick={e => e.target === e.currentTarget && setOpen(false)}
48
+ >
49
+ <div
50
+ role="dialog"
51
+ aria-modal="true"
52
+ aria-label={s.title}
53
+ className="w-full max-w-md mx-4 bg-card border border-border rounded-xl shadow-2xl overflow-hidden"
54
+ >
55
+ {/* Header */}
56
+ <div className="flex items-center justify-between px-5 py-3.5 border-b border-border">
57
+ <span className="text-sm font-medium font-display text-foreground">{s.title}</span>
58
+ <button
59
+ onClick={() => setOpen(false)}
60
+ className="p-1 rounded hover:bg-muted text-muted-foreground hover:text-foreground transition-colors"
61
+ >
62
+ <X size={15} />
63
+ </button>
64
+ </div>
65
+
66
+ {/* Content */}
67
+ <div className="px-5 py-4 space-y-4 max-h-[60vh] overflow-y-auto">
68
+ {sections.map(section => (
69
+ <div key={section}>
70
+ <h3 className="text-[11px] font-medium text-muted-foreground uppercase tracking-wider mb-2">{section}</h3>
71
+ <div className="space-y-1">
72
+ {shortcuts.filter(s => s.section === section).map(s => (
73
+ <div key={s.keys} className="flex items-center justify-between py-1.5">
74
+ <span className="text-xs text-foreground">{s.label}</span>
75
+ <div className="flex items-center gap-1">
76
+ {s.keys.split(' ').map((key, i) => (
77
+ <kbd
78
+ key={i}
79
+ className="px-1.5 py-0.5 text-2xs rounded border border-border bg-muted text-muted-foreground font-mono min-w-[24px] text-center"
80
+ >
81
+ {key}
82
+ </kbd>
83
+ ))}
84
+ </div>
85
+ </div>
86
+ ))}
87
+ </div>
88
+ </div>
89
+ ))}
90
+ </div>
91
+
92
+ {/* Footer */}
93
+ <div className="px-5 py-2.5 border-t border-border">
94
+ <p className="text-2xs text-muted-foreground/60">
95
+ Press <kbd className="px-1 py-0.5 text-2xs rounded border border-border bg-muted font-mono">{mod}</kbd>
96
+ <kbd className="px-1 py-0.5 text-2xs rounded border border-border bg-muted font-mono ml-0.5">?</kbd> {s.toggleHint}
97
+ </p>
98
+ </div>
99
+ </div>
100
+ </div>
101
+ );
102
+ }
@@ -8,6 +8,9 @@ import FileTree from './FileTree';
8
8
  import SyncStatusBar from './SyncStatusBar';
9
9
  import PanelHeader from './panels/PanelHeader';
10
10
  import { useResizeDrag } from '@/hooks/useResizeDrag';
11
+ import { useLocale } from '@/lib/LocaleContext';
12
+
13
+ const noop = () => {};
11
14
 
12
15
  /** Compute the maximum directory depth of a file tree */
13
16
  function getMaxDepth(nodes: FileNode[], current = 0): number {
@@ -68,6 +71,8 @@ export default function Panel({
68
71
  const defaultWidth = activePanel ? DEFAULT_PANEL_WIDTH[activePanel] : 280;
69
72
  const width = maximized ? undefined : (panelWidth ?? defaultWidth);
70
73
 
74
+ const { t } = useLocale();
75
+
71
76
  // File tree depth control: null = manual (no override), number = forced max open depth
72
77
  const [maxOpenDepth, setMaxOpenDepth] = useState<number | null>(null);
73
78
  const treeMaxDepth = useMemo(() => getMaxDepth(fileTree), [fileTree]);
@@ -79,8 +84,8 @@ export default function Panel({
79
84
  maxWidthRatio: MAX_PANEL_WIDTH_RATIO,
80
85
  direction: 'right',
81
86
  disabled: maximized,
82
- onResize: onWidthChange ?? (() => {}),
83
- onResizeEnd: onWidthCommit ?? (() => {}),
87
+ onResize: onWidthChange ?? noop,
88
+ onResizeEnd: onWidthCommit ?? noop,
84
89
  });
85
90
 
86
91
  return (
@@ -97,7 +102,7 @@ export default function Panel({
97
102
  >
98
103
  {/* Files panel — always mounted to preserve tree expand/collapse state */}
99
104
  <div className={`flex flex-col h-full ${activePanel === 'files' ? '' : 'hidden'}`}>
100
- <PanelHeader title="Files">
105
+ <PanelHeader title={t.sidebar.files}>
101
106
  <div className="flex items-center gap-0.5">
102
107
  <button
103
108
  onClick={() => setMaxOpenDepth(prev => {
@@ -105,8 +110,8 @@ export default function Panel({
105
110
  return Math.max(-1, current - 1);
106
111
  })}
107
112
  className="p-1 rounded hover:bg-muted text-muted-foreground hover:text-foreground transition-colors"
108
- aria-label="Collapse one level"
109
- title="Collapse one level"
113
+ aria-label={t.sidebar.collapseLevel}
114
+ title={t.sidebar.collapseLevel}
110
115
  >
111
116
  <ChevronsDownUp size={13} />
112
117
  </button>
@@ -120,8 +125,8 @@ export default function Panel({
120
125
  return next;
121
126
  })}
122
127
  className="p-1 rounded hover:bg-muted text-muted-foreground hover:text-foreground transition-colors"
123
- aria-label="Expand one level"
124
- title="Expand one level"
128
+ aria-label={t.sidebar.expandLevel}
129
+ title={t.sidebar.expandLevel}
125
130
  >
126
131
  <ChevronsUpDown size={13} />
127
132
  </button>
@@ -17,10 +17,12 @@ import SyncPopover from './panels/SyncPopover';
17
17
  import SearchModal from './SearchModal';
18
18
  import AskModal from './AskModal';
19
19
  import SettingsModal from './SettingsModal';
20
+ import KeyboardShortcuts from './KeyboardShortcuts';
20
21
  import { MobileSyncDot, useSyncStatus } from './SyncStatusBar';
21
22
  import { useAskModal } from '@/hooks/useAskModal';
22
23
  import { FileNode } from '@/lib/types';
23
24
  import { useLocale } from '@/lib/LocaleContext';
25
+ import { WalkthroughProvider } from './walkthrough';
24
26
  import type { Tab } from './settings/types';
25
27
 
26
28
  interface SidebarLayoutProps {
@@ -177,6 +179,17 @@ export default function SidebarLayout({ fileTree, children }: SidebarLayoutProps
177
179
  try { localStorage.setItem('rail-expanded', String(expanded)); } catch {}
178
180
  }, []);
179
181
 
182
+ // Listen for cross-component "open settings" events (e.g. from UpdateBanner)
183
+ useEffect(() => {
184
+ const handler = (e: Event) => {
185
+ const tab = (e as CustomEvent).detail?.tab;
186
+ if (tab) setSettingsTab(tab);
187
+ setSettingsOpen(true);
188
+ };
189
+ window.addEventListener('mindos:open-settings', handler);
190
+ return () => window.removeEventListener('mindos:open-settings', handler);
191
+ }, []);
192
+
180
193
  // Bridge useAskModal store → right Ask panel or popup
181
194
  useEffect(() => {
182
195
  if (askModal.open) {
@@ -290,6 +303,7 @@ export default function SidebarLayout({ fileTree, children }: SidebarLayoutProps
290
303
  const effectivePanelWidth = panelWidth ?? (activePanel ? PANEL_WIDTH[activePanel] : 280);
291
304
 
292
305
  return (
306
+ <WalkthroughProvider>
293
307
  <>
294
308
  {/* Skip link */}
295
309
  <a
@@ -335,7 +349,6 @@ export default function SidebarLayout({ fileTree, children }: SidebarLayoutProps
335
349
  active={activePanel === 'agents'}
336
350
  maximized={panelMaximized}
337
351
  onMaximize={handlePanelMaximize}
338
- onOpenSettings={openSettingsTab}
339
352
  />
340
353
  </div>
341
354
  </Panel>
@@ -368,6 +381,9 @@ export default function SidebarLayout({ fileTree, children }: SidebarLayoutProps
368
381
  {/* ── Ask AI FAB (desktop only — toggles right panel or popup) ── */}
369
382
  <AskFab onToggle={toggleAskPanel} askPanelOpen={askPanelOpen || desktopAskPopupOpen} />
370
383
 
384
+ {/* ── Keyboard Shortcuts (⌘?) ── */}
385
+ <KeyboardShortcuts />
386
+
371
387
  {/* ── Settings Modal (desktop overlay — does not affect panel) ── */}
372
388
  <SettingsModal
373
389
  open={settingsOpen}
@@ -455,5 +471,6 @@ export default function SidebarLayout({ fileTree, children }: SidebarLayoutProps
455
471
  }
456
472
  `}</style>
457
473
  </>
474
+ </WalkthroughProvider>
458
475
  );
459
476
  }
@@ -45,6 +45,13 @@ export default function UpdateBanner() {
45
45
  setInfo(null);
46
46
  };
47
47
 
48
+ const handleOpenUpdate = () => {
49
+ window.dispatchEvent(new CustomEvent('mindos:open-settings', { detail: { tab: 'update' } }));
50
+ // Dismiss banner once user engages with Update tab
51
+ localStorage.setItem('mindos_update_dismissed', info.latest);
52
+ setInfo(null);
53
+ };
54
+
48
55
  const updateT = t.updateBanner;
49
56
 
50
57
  return (
@@ -58,32 +65,23 @@ export default function UpdateBanner() {
58
65
  ? updateT.newVersion(info.latest, info.current)
59
66
  : `MindOS v${info.latest} available (current: v${info.current})`}
60
67
  </span>
61
- <span className="text-muted-foreground">
62
- {updateT?.runUpdate ?? 'Run'}{' '}
63
- <code className="px-1 py-0.5 rounded bg-muted font-mono text-xs">mindos update</code>
68
+ <button
69
+ onClick={handleOpenUpdate}
70
+ className="px-2 py-0.5 rounded-md text-xs font-medium text-white transition-colors hover:opacity-90"
71
+ style={{ background: 'var(--amber)' }}
72
+ >
73
+ {updateT?.updateNow ?? 'Update'}
74
+ </button>
75
+ <span className="text-muted-foreground hidden sm:inline">
64
76
  {updateT?.orSee ? (
65
77
  <>
66
- {' '}{updateT.orSee}{' '}
67
- <a
68
- href="https://github.com/GeminiLight/mindos/releases"
69
- target="_blank"
70
- rel="noopener noreferrer"
71
- className="underline hover:text-foreground transition-colors"
72
- >
73
- {updateT.releaseNotes}
74
- </a>
78
+ {updateT.orSee}{' '}
79
+ <a href="https://github.com/GeminiLight/mindos/releases" target="_blank" rel="noopener noreferrer" className="underline hover:text-foreground transition-colors">{updateT.releaseNotes}</a>
75
80
  </>
76
81
  ) : (
77
82
  <>
78
- {' '}or{' '}
79
- <a
80
- href="https://github.com/GeminiLight/mindos/releases"
81
- target="_blank"
82
- rel="noopener noreferrer"
83
- className="underline hover:text-foreground transition-colors"
84
- >
85
- view release notes
86
- </a>
83
+ or{' '}
84
+ <a href="https://github.com/GeminiLight/mindos/releases" target="_blank" rel="noopener noreferrer" className="underline hover:text-foreground transition-colors">release notes</a>
87
85
  </>
88
86
  )}
89
87
  </span>
@@ -0,0 +1,100 @@
1
+ 'use client';
2
+
3
+ import { useState } from 'react';
4
+ import { useLocale } from '@/lib/LocaleContext';
5
+ import { useCases, categories, type UseCaseCategory } from './use-cases';
6
+ import UseCaseCard from './UseCaseCard';
7
+
8
+ export default function ExploreContent() {
9
+ const { t } = useLocale();
10
+ const e = t.explore;
11
+ const [activeCategory, setActiveCategory] = useState<UseCaseCategory | 'all'>('all');
12
+
13
+ const filtered = activeCategory === 'all'
14
+ ? useCases
15
+ : useCases.filter(uc => uc.category === activeCategory);
16
+
17
+ /** Type-safe lookup for use case i18n data by id */
18
+ const getUseCaseText = (id: string): { title: string; desc: string; prompt: string } | undefined => {
19
+ const map: Record<string, { title: string; desc: string; prompt: string }> = {
20
+ c1: e.c1, c2: e.c2, c3: e.c3, c4: e.c4, c5: e.c5,
21
+ c6: e.c6, c7: e.c7, c8: e.c8, c9: e.c9,
22
+ };
23
+ return map[id];
24
+ };
25
+
26
+ return (
27
+ <div className="content-width px-4 md:px-6 py-8 md:py-12">
28
+ {/* Header */}
29
+ <div className="mb-8">
30
+ <div className="flex items-center gap-2 mb-3">
31
+ <div className="w-1 h-5 rounded-full" style={{ background: 'var(--amber)' }} />
32
+ <h1
33
+ className="text-2xl font-semibold tracking-tight font-display"
34
+ style={{ color: 'var(--foreground)' }}
35
+ >
36
+ {e.title}
37
+ </h1>
38
+ </div>
39
+ <p
40
+ className="text-sm leading-relaxed"
41
+ style={{ color: 'var(--muted-foreground)', paddingLeft: '1rem' }}
42
+ >
43
+ {e.subtitle}
44
+ </p>
45
+ </div>
46
+
47
+ {/* Category tabs */}
48
+ <div className="flex flex-wrap gap-2 mb-6" style={{ paddingLeft: '1rem' }}>
49
+ <CategoryChip
50
+ label={e.all}
51
+ active={activeCategory === 'all'}
52
+ onClick={() => setActiveCategory('all')}
53
+ />
54
+ {categories.map(cat => (
55
+ <CategoryChip
56
+ key={cat}
57
+ label={(e.categories as Record<string, string>)[cat]}
58
+ active={activeCategory === cat}
59
+ onClick={() => setActiveCategory(cat)}
60
+ />
61
+ ))}
62
+ </div>
63
+
64
+ {/* Card grid */}
65
+ <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3" style={{ paddingLeft: '1rem' }}>
66
+ {filtered.map(uc => {
67
+ const data = getUseCaseText(uc.id);
68
+ if (!data) return null;
69
+ return (
70
+ <UseCaseCard
71
+ key={uc.id}
72
+ icon={uc.icon}
73
+ title={data.title}
74
+ description={data.desc}
75
+ prompt={data.prompt}
76
+ tryItLabel={e.tryIt}
77
+ />
78
+ );
79
+ })}
80
+ </div>
81
+ </div>
82
+ );
83
+ }
84
+
85
+ function CategoryChip({ label, active, onClick }: { label: string; active: boolean; onClick: () => void }) {
86
+ return (
87
+ <button
88
+ onClick={onClick}
89
+ className={`
90
+ px-3 py-1.5 rounded-full text-xs font-medium transition-all duration-150
91
+ ${active
92
+ ? 'text-[var(--amber)] bg-[var(--amber-dim)]'
93
+ : 'text-[var(--muted-foreground)] bg-[var(--muted)] hover:text-[var(--foreground)] hover:bg-[var(--muted)]/80'
94
+ }
95
+ `}
96
+ >
97
+ {label}
98
+ </button>
99
+ );
100
+ }
@@ -0,0 +1,50 @@
1
+ 'use client';
2
+
3
+ import { openAskModal } from '@/hooks/useAskModal';
4
+
5
+ interface UseCaseCardProps {
6
+ icon: string;
7
+ title: string;
8
+ description: string;
9
+ prompt: string;
10
+ tryItLabel: string;
11
+ }
12
+
13
+ export default function UseCaseCard({ icon, title, description, prompt, tryItLabel }: UseCaseCardProps) {
14
+ return (
15
+ <div
16
+ className="group flex flex-col gap-3 p-4 rounded-xl border transition-all duration-150 hover:border-amber-500/30 hover:bg-muted/50"
17
+ style={{ borderColor: 'var(--border)', background: 'var(--card)' }}
18
+ >
19
+ <div className="flex items-start gap-3">
20
+ <span className="text-xl leading-none shrink-0 mt-0.5" suppressHydrationWarning>
21
+ {icon}
22
+ </span>
23
+ <div className="flex-1 min-w-0">
24
+ <h3
25
+ className="text-sm font-semibold font-display truncate"
26
+ style={{ color: 'var(--foreground)' }}
27
+ >
28
+ {title}
29
+ </h3>
30
+ <p
31
+ className="text-xs leading-relaxed mt-1 line-clamp-2"
32
+ style={{ color: 'var(--muted-foreground)' }}
33
+ >
34
+ {description}
35
+ </p>
36
+ </div>
37
+ </div>
38
+ <button
39
+ onClick={() => openAskModal(prompt, 'user')}
40
+ className="self-start inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium transition-all duration-150 hover:opacity-80 cursor-pointer"
41
+ style={{
42
+ background: 'var(--amber-dim)',
43
+ color: 'var(--amber)',
44
+ }}
45
+ >
46
+ {tryItLabel} →
47
+ </button>
48
+ </div>
49
+ );
50
+ }