@geminilight/mindos 0.5.3 → 0.5.6

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/README.md CHANGED
@@ -14,11 +14,12 @@
14
14
 
15
15
  <p align="center">
16
16
  <a href="https://tianfuwang.tech/MindOS"><img src="https://img.shields.io/badge/Website-MindOS-0ea5e9.svg?style=for-the-badge" alt="Website"></a>
17
- <a href="https://deepwiki.com/GeminiLight/MindOS"><img src="https://img.shields.io/badge/DeepWiki-MindOS-blue.svg?style=for-the-badge" alt="DeepWiki"></a>
18
- <a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-blue.svg?style=for-the-badge" alt="MIT License"></a>
17
+ <a href="https://www.npmjs.com/package/@geminilight/mindos"><img src="https://img.shields.io/npm/v/@geminilight/mindos.svg?style=for-the-badge&color=f59e0b" alt="npm version"></a>
18
+ <a href="#wechat"><img src="https://img.shields.io/badge/WeChat-Group-07C160.svg?style=for-the-badge&logo=wechat&logoColor=white" alt="WeChat"></a>
19
+ <a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-6366f1.svg?style=for-the-badge" alt="MIT License"></a>
19
20
  </p>
20
21
 
21
- MindOS is a **Human-AI Collaborative Mind System**—a local-first knowledge base that ensures your notes, workflows, and personal context are both human-readable and directly executable by AI Agents. **Globally sync your mind for all agents: transparent, controllable, and evolving symbiotically.**
22
+ MindOS is a **Human-AI Collaborative Mind System**—a local-first knowledge base that ensures your notes, workflows, and personal context are both human-readable and directly executable by Agents. **One shared memory layer for all Agents — auditable, correctable, and smarter with every use.**
22
23
 
23
24
  ---
24
25
 
@@ -49,7 +50,7 @@ MindOS is a **Human-AI Collaborative Mind System**—a local-first knowledge bas
49
50
 
50
51
  ## 🧠 Core Value: Human-AI Shared Mind
51
52
 
52
- **1. Global Sync — Break Mind Silos**
53
+ **1. Global Sync — Break Memory Silos**
53
54
 
54
55
  Traditional notes are scattered across tools and APIs, so agents miss your real context when it matters. MindOS turns your local knowledge into one MCP-ready source, so every agent can sync your Profile, SOPs, and live working memory.
55
56
 
@@ -57,9 +58,9 @@ Traditional notes are scattered across tools and APIs, so agents miss your real
57
58
 
58
59
  Most assistant memory lives in black boxes, leaving humans unable to inspect or correct how decisions are made. MindOS writes retrieval and execution traces into local plain text, so you can audit, intervene, and improve continuously.
59
60
 
60
- **3. Symbiotic Evolution — Dynamic Instruction Flow**
61
+ **3. Symbiotic Evolution — Experience Flows Back as Instructions**
61
62
 
62
- Static documents are hard to synchronize and weak as execution systems in real human-agent collaboration. MindOS makes notes prompt-native and reference-linked, so daily writing naturally becomes executable workflows that evolve with you.
63
+ Static documents are hard to synchronize and weak as execution systems in real human-agent collaboration. MindOS makes notes agent-ready and reference-linked, so daily writing naturally becomes executable workflows that evolve with you.
63
64
 
64
65
  > **Foundation:** Local-first by default - all data stays in local plain text for privacy, ownership, and speed.
65
66
 
@@ -374,10 +375,10 @@ graph LR
374
375
 
375
376
  **Who is this for?**
376
377
 
377
- - **AI Independent Developer** — Store personal SOPs, tech stack preferences, and project context in MindOS. Any Agent instantly inherits your work habits.
378
+ - **Independent Developer** — Store personal SOPs, tech stack preferences, and project context in MindOS. Any Agent instantly inherits your work habits.
378
379
  - **Knowledge Worker** — Manage research materials with bi-directional links. Your AI assistant answers questions grounded in your full context, not generic knowledge.
379
380
  - **Team Collaboration** — Share a MindOS knowledge base across team members as a single source of truth. Humans and Agents read from the same playbook, keeping everyone aligned.
380
- - **Automated Agent Operations** — Write standard workflows as Prompt-Driven documents. Agents execute directly, humans audit the results.
381
+ - **Automated Agent Operations** — Write standard workflows as Agent-Ready documents. Agents execute directly, humans audit the results.
381
382
 
382
383
  ---
383
384
 
@@ -470,6 +471,18 @@ MindOS/
470
471
 
471
472
  ---
472
473
 
474
+ ## 💬 Community <a name="wechat"></a>
475
+
476
+ Join our WeChat group for early access, feedback, and AI workflow discussions:
477
+
478
+ <p align="center">
479
+ <img src="assets/images/wechat-qr.png" alt="WeChat Group QR Code" width="200" />
480
+ </p>
481
+
482
+ > Scan the QR code or ask an existing member to invite you.
483
+
484
+ ---
485
+
473
486
  ## 📄 License
474
487
 
475
488
  MIT © GeminiLight
package/README_zh.md CHANGED
@@ -14,11 +14,12 @@
14
14
 
15
15
  <p align="center">
16
16
  <a href="https://tianfuwang.tech/MindOS"><img src="https://img.shields.io/badge/Website-MindOS-0ea5e9.svg?style=for-the-badge" alt="Website"></a>
17
- <a href="https://deepwiki.com/GeminiLight/MindOS"><img src="https://img.shields.io/badge/DeepWiki-MindOS-blue.svg?style=for-the-badge" alt="DeepWiki"></a>
18
- <a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-blue.svg?style=for-the-badge" alt="MIT License"></a>
17
+ <a href="https://www.npmjs.com/package/@geminilight/mindos"><img src="https://img.shields.io/npm/v/@geminilight/mindos.svg?style=for-the-badge&color=f59e0b" alt="npm version"></a>
18
+ <a href="#wechat"><img src="https://img.shields.io/badge/WeChat-群聊-07C160.svg?style=for-the-badge&logo=wechat&logoColor=white" alt="WeChat"></a>
19
+ <a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-6366f1.svg?style=for-the-badge" alt="MIT License"></a>
19
20
  </p>
20
21
 
21
- MindOS 是一个**人机协同心智系统**——基于本地优先的协作知识库,让你的笔记、工作流、个人上下文既对人类阅读友好,也能直接被 AI Agent 调用和执行。**为所有 Agents 全局同步你的心智,透明可控,共生演进。**
22
+ MindOS 是一个**人机协同心智系统**——基于本地优先的协作知识库,让你的笔记、工作流、个人上下文既对人类阅读友好,也能直接被 Agent 调用和执行。**让所有 Agent 共享一个记忆层——可审计、可修正、越用越聪明。**
22
23
 
23
24
  ---
24
25
 
@@ -49,7 +50,7 @@ MindOS 是一个**人机协同心智系统**——基于本地优先的协作知
49
50
 
50
51
  ## 🧠 核心价值:人机共享心智
51
52
 
52
- **1. 全局同步 — 打破心智孤岛**
53
+ **1. 全局同步 — 打破记忆割裂**
53
54
 
54
55
  传统笔记分散在不同工具和接口中,Agent 在关键时刻拿不到你的真实上下文。MindOS 把本地知识统一为 MCP 可读的单一来源,让所有 Agent 同步你的 Profile、SOP 与实时记忆。
55
56
 
@@ -57,9 +58,9 @@ MindOS 是一个**人机协同心智系统**——基于本地优先的协作知
57
58
 
58
59
  多数助手记忆封闭在黑箱里,人类难以审查和纠正决策过程。MindOS 将检索与执行轨迹沉淀为本地纯文本,让你可以持续审计、干预与优化。
59
60
 
60
- **3. 共生演进 — 动态指令流转**
61
+ **3. 共生演进 — 经验回流为指令**
61
62
 
62
- 静态文档难同步,也难在真实人机协作中承担执行系统角色。MindOS Prompt-Native 与引用链接组织知识,让日常记录自然变成可执行工作流并持续进化。
63
+ 静态文档难同步,也难在真实人机协作中承担执行系统角色。MindOS 让笔记天然成为 Agent 可执行的指令,通过引用链接组织知识,日常记录自然变成可执行工作流并持续进化。
63
64
 
64
65
  > **底层原则:** 默认本地优先,全部数据以本地纯文本保存,兼顾隐私、主权与性能。
65
66
 
@@ -379,7 +380,7 @@ graph LR
379
380
  - **AI 独立开发者** — 将个人 SOP、技术栈偏好、项目上下文存入 MindOS,任何 Agent 即插即用你的工作习惯。
380
381
  - **知识工作者** — 用双链笔记管理研究资料,AI 助手基于你的完整上下文回答问题,而非泛泛而谈。
381
382
  - **团队协作** — 团队成员共享同一个 MindOS 知识库作为 Single Source of Truth,人与 Agent 读同一份剧本,保持对齐。
382
- - **Agent 自动运维** — 将标准流程写成 Prompt-Driven 文档,Agent 直接执行,人类审计结果。
383
+ - **Agent 自动运维** — 将标准流程写成笔记即指令的文档,Agent 直接执行,人类审计结果。
383
384
 
384
385
  ---
385
386
 
@@ -473,6 +474,18 @@ MindOS/
473
474
 
474
475
  ---
475
476
 
477
+ ## 💬 社区 <a name="wechat"></a>
478
+
479
+ 加入微信内测群,抢先体验、反馈建议、交流 AI 工作流:
480
+
481
+ <p align="center">
482
+ <img src="assets/images/wechat-qr.png" alt="微信群二维码" width="200" />
483
+ </p>
484
+
485
+ > 扫码加入,或请群内成员邀请你。
486
+
487
+ ---
488
+
476
489
  ## 📄 License
477
490
 
478
491
  MIT © GeminiLight
@@ -15,6 +15,7 @@ export async function GET() {
15
15
  configPath: status.configPath,
16
16
  hasProjectScope: !!agent.project,
17
17
  hasGlobalScope: !!agent.global,
18
+ preferredTransport: agent.preferredTransport,
18
19
  };
19
20
  });
20
21
  return NextResponse.json({ agents });
@@ -4,9 +4,15 @@ import fs from 'fs';
4
4
  import path from 'path';
5
5
  import { MCP_AGENTS, expandHome } from '@/lib/mcp-agents';
6
6
 
7
+ interface AgentInstallItem {
8
+ key: string;
9
+ scope: 'project' | 'global';
10
+ transport?: 'stdio' | 'http';
11
+ }
12
+
7
13
  interface InstallRequest {
8
- agents: Array<{ key: string; scope: 'project' | 'global' }>;
9
- transport: 'stdio' | 'http';
14
+ agents: AgentInstallItem[];
15
+ transport: 'stdio' | 'http' | 'auto';
10
16
  url?: string;
11
17
  token?: string;
12
18
  }
@@ -20,20 +26,59 @@ function buildEntry(transport: string, url?: string, token?: string) {
20
26
  return entry;
21
27
  }
22
28
 
29
+ async function verifyHttpConnection(url: string, token?: string): Promise<{ verified: boolean; verifyError?: string }> {
30
+ try {
31
+ const headers: Record<string, string> = { 'Content-Type': 'application/json' };
32
+ if (token) headers['Authorization'] = `Bearer ${token}`;
33
+ const controller = new AbortController();
34
+ const timeout = setTimeout(() => controller.abort(), 2000);
35
+ const res = await fetch(url, {
36
+ method: 'POST',
37
+ headers,
38
+ body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'tools/list', params: {} }),
39
+ signal: controller.signal,
40
+ });
41
+ clearTimeout(timeout);
42
+ if (res.ok) return { verified: true };
43
+ return { verified: false, verifyError: `HTTP ${res.status}` };
44
+ } catch (err) {
45
+ return { verified: false, verifyError: err instanceof Error ? err.message : String(err) };
46
+ }
47
+ }
48
+
23
49
  export async function POST(req: NextRequest) {
24
50
  try {
25
51
  const body = (await req.json()) as InstallRequest;
26
- const { agents, transport, url, token } = body;
27
- const entry = buildEntry(transport, url, token);
28
- const results: Array<{ agent: string; status: string; path?: string; message?: string }> = [];
52
+ const { agents, transport: globalTransport, url, token } = body;
53
+ const results: Array<{
54
+ agent: string;
55
+ status: string;
56
+ path?: string;
57
+ message?: string;
58
+ transport?: string;
59
+ verified?: boolean;
60
+ verifyError?: string;
61
+ }> = [];
29
62
 
30
- for (const { key, scope } of agents) {
63
+ for (const item of agents) {
64
+ const { key, scope } = item;
31
65
  const agent = MCP_AGENTS[key];
32
66
  if (!agent) {
33
67
  results.push({ agent: key, status: 'error', message: `Unknown agent: ${key}` });
34
68
  continue;
35
69
  }
36
70
 
71
+ // Resolve effective transport: agent-level > global-level > auto (use preferredTransport)
72
+ let effectiveTransport: 'stdio' | 'http';
73
+ if (item.transport && item.transport !== 'auto' as string) {
74
+ effectiveTransport = item.transport;
75
+ } else if (globalTransport && globalTransport !== 'auto') {
76
+ effectiveTransport = globalTransport;
77
+ } else {
78
+ effectiveTransport = agent.preferredTransport;
79
+ }
80
+
81
+ const entry = buildEntry(effectiveTransport, url, token);
37
82
  const isGlobal = scope === 'global';
38
83
  const configPath = isGlobal ? agent.global : agent.project;
39
84
  if (!configPath) {
@@ -59,7 +104,17 @@ export async function POST(req: NextRequest) {
59
104
  if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
60
105
  fs.writeFileSync(absPath, JSON.stringify(config, null, 2) + '\n', 'utf-8');
61
106
 
62
- results.push({ agent: key, status: 'ok', path: configPath });
107
+ const result: typeof results[number] = { agent: key, status: 'ok', path: configPath, transport: effectiveTransport };
108
+
109
+ // Verify http connections
110
+ if (effectiveTransport === 'http') {
111
+ const mcpUrl = (entry as Record<string, unknown>).url as string;
112
+ const verification = await verifyHttpConnection(mcpUrl, token);
113
+ result.verified = verification.verified;
114
+ if (verification.verifyError) result.verifyError = verification.verifyError;
115
+ }
116
+
117
+ results.push(result);
63
118
  } catch (err) {
64
119
  results.push({ agent: key, status: 'error', message: String(err) });
65
120
  }
@@ -0,0 +1,127 @@
1
+ export const dynamic = 'force-dynamic';
2
+ import { NextRequest, NextResponse } from 'next/server';
3
+ import { execSync } from 'child_process';
4
+ import path from 'path';
5
+ import fs from 'fs';
6
+
7
+ /* ── Constants ────────────────────────────────────────────────── */
8
+
9
+ const GITHUB_SOURCE = 'GeminiLight/MindOS';
10
+
11
+ // Universal agents read directly from ~/.agents/skills/ — no symlink needed.
12
+ const UNIVERSAL_AGENTS = new Set([
13
+ 'amp', 'cline', 'codex', 'cursor', 'gemini-cli',
14
+ 'github-copilot', 'kimi-cli', 'opencode', 'warp',
15
+ ]);
16
+
17
+ // Agents that do NOT support Skills at all
18
+ const SKILL_UNSUPPORTED = new Set(['claude-desktop']);
19
+
20
+ // MCP agent key → npx skills agent name (for non-universal agents)
21
+ const AGENT_NAME_MAP: Record<string, string> = {
22
+ 'claude-code': 'claude-code',
23
+ 'windsurf': 'windsurf',
24
+ 'trae': 'trae',
25
+ 'openclaw': 'openclaw',
26
+ 'codebuddy': 'codebuddy',
27
+ };
28
+
29
+ /* ── Helpers ──────────────────────────────────────────────────── */
30
+
31
+ /** Fallback: find local skills directory for offline installs */
32
+ function findLocalSkillsDir(): string | null {
33
+ const candidates = [
34
+ path.resolve(process.cwd(), 'data/skills'), // app/data/skills/
35
+ path.resolve(process.cwd(), '..', 'skills'), // project-root/skills/
36
+ ];
37
+ for (const dir of candidates) {
38
+ if (fs.existsSync(dir)) return dir;
39
+ }
40
+ return null;
41
+ }
42
+
43
+ function buildCommand(
44
+ source: string,
45
+ skill: string,
46
+ additionalAgents: string[],
47
+ ): string {
48
+ // Each agent needs its own -a flag (skills CLI does NOT accept comma-separated)
49
+ const agentFlags = additionalAgents.length > 0
50
+ ? additionalAgents.map(a => `-a ${a}`).join(' ')
51
+ : '-a universal';
52
+ // Quote source if it looks like a local path (contains / or \)
53
+ const quotedSource = /[/\\]/.test(source) ? `"${source}"` : source;
54
+ return `npx skills add ${quotedSource} --skill ${skill} ${agentFlags} -g -y`;
55
+ }
56
+
57
+ /* ── POST handler ─────────────────────────────────────────────── */
58
+
59
+ interface SkillInstallRequest {
60
+ skill: 'mindos' | 'mindos-zh';
61
+ agents: string[];
62
+ }
63
+
64
+ export async function POST(req: NextRequest) {
65
+ try {
66
+ const body: SkillInstallRequest = await req.json();
67
+ const { skill, agents } = body;
68
+
69
+ if (!skill || !['mindos', 'mindos-zh'].includes(skill)) {
70
+ return NextResponse.json({ error: 'Invalid skill name' }, { status: 400 });
71
+ }
72
+
73
+ const additionalAgents = (agents || [])
74
+ .filter(key => !UNIVERSAL_AGENTS.has(key) && !SKILL_UNSUPPORTED.has(key))
75
+ .map(key => AGENT_NAME_MAP[key] || key);
76
+
77
+ // Try GitHub source first, fall back to local path
78
+ const sources = [GITHUB_SOURCE];
79
+ const localDir = findLocalSkillsDir();
80
+ if (localDir) sources.push(localDir);
81
+
82
+ let lastCmd = '';
83
+ let lastStdout = '';
84
+ let lastStderr = '';
85
+
86
+ for (const source of sources) {
87
+ const cmd = buildCommand(source, skill, additionalAgents);
88
+ lastCmd = cmd;
89
+ try {
90
+ lastStdout = execSync(cmd, {
91
+ encoding: 'utf-8',
92
+ timeout: 30_000,
93
+ env: { ...process.env, NODE_ENV: 'production' },
94
+ stdio: 'pipe',
95
+ });
96
+ // Success — return immediately
97
+ return NextResponse.json({
98
+ ok: true,
99
+ skill,
100
+ agents: additionalAgents,
101
+ cmd,
102
+ stdout: lastStdout.trim(),
103
+ });
104
+ } catch (err: unknown) {
105
+ const e = err as { stdout?: string; stderr?: string; message?: string };
106
+ lastStdout = e.stdout || '';
107
+ lastStderr = e.stderr || e.message || 'Unknown error';
108
+ // Try next source
109
+ }
110
+ }
111
+
112
+ // All sources failed
113
+ return NextResponse.json({
114
+ ok: false,
115
+ skill,
116
+ agents: additionalAgents,
117
+ cmd: lastCmd,
118
+ stdout: lastStdout,
119
+ stderr: lastStderr,
120
+ });
121
+ } catch (e) {
122
+ return NextResponse.json(
123
+ { error: e instanceof Error ? e.message : String(e) },
124
+ { status: 500 },
125
+ );
126
+ }
127
+ }
@@ -90,9 +90,10 @@ export async function POST(req: NextRequest) {
90
90
  // Use the same resolved values that will actually be written to config
91
91
  const resolvedAuthToken = authToken ?? current.authToken ?? '';
92
92
  const resolvedWebPassword = webPassword ?? '';
93
- // Only compute needsRestart for re-onboard (setupPending=true means first-time setup)
93
+ // First-time onboard always needs restart (temporary setup port → user's chosen port).
94
+ // Re-onboard only needs restart if port/path/auth/password actually changed.
94
95
  const isFirstTime = current.setupPending === true || !current.mindRoot;
95
- const needsRestart = !isFirstTime && (
96
+ const needsRestart = isFirstTime || (
96
97
  webPort !== (current.port ?? 3000) ||
97
98
  mcpPortNum !== (current.mcpPort ?? 8787) ||
98
99
  resolvedRoot !== (current.mindRoot || '') ||
@@ -101,6 +102,7 @@ export async function POST(req: NextRequest) {
101
102
  );
102
103
 
103
104
  // Build config
105
+ const disabledSkills = body.template === 'zh' ? ['mindos'] : ['mindos-zh'];
104
106
  const config: ServerSettings = {
105
107
  ai: ai ?? current.ai,
106
108
  mindRoot: resolvedRoot,
@@ -110,6 +112,7 @@ export async function POST(req: NextRequest) {
110
112
  webPassword: webPassword ?? '',
111
113
  startMode: current.startMode,
112
114
  setupPending: false, // clear the flag
115
+ disabledSkills,
113
116
  };
114
117
 
115
118
  writeSettings(config);
@@ -8,11 +8,12 @@ import MarkdownView from '@/components/MarkdownView';
8
8
  import JsonView from '@/components/JsonView';
9
9
  import CsvView from '@/components/CsvView';
10
10
  import Backlinks from '@/components/Backlinks';
11
+ import { useRendererState } from '@/lib/renderers/useRendererState';
11
12
  import Breadcrumb from '@/components/Breadcrumb';
12
13
  import MarkdownEditor, { MdViewMode } from '@/components/MarkdownEditor';
13
14
  import TableOfContents from '@/components/TableOfContents';
14
15
  import FindInPage from '@/components/FindInPage';
15
- import { resolveRenderer } from '@/lib/renderers/registry';
16
+ import { resolveRenderer, isRendererEnabled } from '@/lib/renderers/registry';
16
17
  import { encodePath } from '@/lib/utils';
17
18
  import '@/lib/renderers/index'; // registers all renderers
18
19
 
@@ -45,22 +46,9 @@ export default function ViewPageClient({
45
46
  () => false,
46
47
  );
47
48
 
48
- const useRaw = useSyncExternalStore(
49
- (onStoreChange) => {
50
- const listener = () => onStoreChange();
51
- window.addEventListener('storage', listener);
52
- window.addEventListener('mindos-use-raw-change', listener);
53
- return () => {
54
- window.removeEventListener('storage', listener);
55
- window.removeEventListener('mindos-use-raw-change', listener);
56
- };
57
- },
58
- () => {
59
- const saved = localStorage.getItem('mindos-use-raw');
60
- return saved !== null ? saved === 'true' : false;
61
- },
62
- () => false,
63
- );
49
+ const [useRaw, setUseRaw] = useRendererState<boolean>('_raw', filePath, false);
50
+ // Global graph mode — shared across all md files (not per-file)
51
+ const [graphMode, setGraphMode] = useRendererState<boolean>('_graphMode', '_global', false);
64
52
  const router = useRouter();
65
53
  const [editing, setEditing] = useState(initialEditing || content === '');
66
54
  const [editContent, setEditContent] = useState(content);
@@ -81,14 +69,24 @@ export default function ViewPageClient({
81
69
  const effectiveUseRaw = hydrated ? useRaw : false;
82
70
 
83
71
  const handleToggleRaw = useCallback(() => {
84
- const next = !useRaw;
85
- localStorage.setItem('mindos-use-raw', String(next));
86
- window.dispatchEvent(new Event('mindos-use-raw-change'));
87
- }, [useRaw]);
72
+ setUseRaw(prev => !prev);
73
+ }, [setUseRaw]);
74
+
75
+ const handleToggleGraph = useCallback(() => {
76
+ setGraphMode(prev => !prev);
77
+ }, [setGraphMode]);
88
78
 
89
- const renderer = resolveRenderer(filePath, extension);
79
+ const effectiveGraphMode = hydrated ? graphMode : false;
80
+
81
+ // Resolve renderer: for md files, graph mode overrides normal resolution
82
+ const registryRenderer = resolveRenderer(filePath, extension);
83
+ const graphRenderer = extension === 'md' && effectiveGraphMode
84
+ ? resolveRenderer(filePath, extension, 'graph')
85
+ : undefined;
86
+ const renderer = graphRenderer || registryRenderer;
90
87
  const isCsv = extension === 'csv';
91
- const showRenderer = !editing && !effectiveUseRaw && !!renderer;
88
+ // Graph mode overrides Raw when graph is active, always show the renderer
89
+ const showRenderer = !editing && !!renderer && (!effectiveUseRaw || !!graphRenderer);
92
90
 
93
91
  // Lazily resolve the renderer component for code-splitting
94
92
  const LazyComponent = useMemo(() => {
@@ -224,8 +222,24 @@ export default function ViewPageClient({
224
222
  <span className="text-xs text-red-400 hidden sm:inline">{saveError}</span>
225
223
  )}
226
224
 
227
- {/* Renderer toggle — only shown when a custom renderer exists */}
228
- {renderer && !editing && !isDraft && (
225
+ {/* Graph toggle — only for md files, hidden when graph plugin is disabled */}
226
+ {extension === 'md' && !editing && !isDraft && isRendererEnabled('graph') && (
227
+ <button
228
+ onClick={handleToggleGraph}
229
+ className="flex items-center gap-1.5 px-3 py-1.5 rounded-md text-xs font-medium transition-colors font-display"
230
+ style={{
231
+ background: effectiveGraphMode ? `${'var(--amber)'}22` : 'var(--muted)',
232
+ color: effectiveGraphMode ? 'var(--amber)' : 'var(--muted-foreground)',
233
+ }}
234
+ title={effectiveGraphMode ? 'Switch to document view' : 'Switch to Wiki Graph'}
235
+ >
236
+ <span>🕸️</span>
237
+ <span className="hidden sm:inline">Graph</span>
238
+ </button>
239
+ )}
240
+
241
+ {/* Renderer toggle — only shown when a custom renderer exists (excludes graph-mode override) */}
242
+ {registryRenderer && !editing && !isDraft && !graphRenderer && (
229
243
  <button
230
244
  onClick={handleToggleRaw}
231
245
  className="flex items-center gap-1.5 px-3 py-1.5 rounded-md text-xs font-medium transition-colors font-display"
@@ -233,10 +247,10 @@ export default function ViewPageClient({
233
247
  background: effectiveUseRaw ? 'var(--muted)' : `${'var(--amber)'}22`,
234
248
  color: effectiveUseRaw ? 'var(--muted-foreground)' : 'var(--amber)',
235
249
  }}
236
- title={effectiveUseRaw ? `Switch to ${renderer.name}` : 'View raw'}
250
+ title={effectiveUseRaw ? `Switch to ${registryRenderer?.name}` : 'View raw'}
237
251
  >
238
252
  <LayoutTemplate size={13} />
239
- <span className="hidden sm:inline">{effectiveUseRaw ? renderer.name : 'Raw'}</span>
253
+ <span className="hidden sm:inline">{effectiveUseRaw ? registryRenderer.name : 'Raw'}</span>
240
254
  </button>
241
255
  )}
242
256
 
@@ -62,7 +62,9 @@ export default function HomeContent({ recent, existingFiles }: { recent: RecentF
62
62
 
63
63
  const formatTime = (mtime: number) => relativeTime(mtime, t.home.relativeTime);
64
64
 
65
- const renderers = getAllRenderers();
65
+ // Only show renderers with an entryPath on the home page grid.
66
+ // Opt-in renderers (like Graph) have no entryPath and are toggled from the view toolbar.
67
+ const renderers = getAllRenderers().filter(r => r.entryPath);
66
68
 
67
69
  const lastFile = recent[0];
68
70