@geminilight/mindos 0.5.57 → 0.5.59

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 (96) hide show
  1. package/README.md +1 -1
  2. package/README_zh.md +1 -1
  3. package/app/app/api/ask/route.ts +4 -1
  4. package/app/app/api/extract-pdf/route.ts +4 -2
  5. package/app/app/api/file/route.ts +54 -2
  6. package/app/app/api/health/route.ts +3 -1
  7. package/app/app/api/mcp/install-skill/route.ts +4 -2
  8. package/app/app/api/mcp/restart/route.ts +1 -1
  9. package/app/app/api/restart/route.ts +1 -1
  10. package/app/app/api/skills/route.ts +1 -1
  11. package/app/app/api/sync/route.ts +2 -2
  12. package/app/app/api/update/route.ts +1 -1
  13. package/app/app/api/update-check/route.ts +11 -3
  14. package/app/app/globals.css +4 -0
  15. package/app/app/layout.tsx +6 -0
  16. package/app/app/login/page.tsx +12 -1
  17. package/app/app/register-sw.tsx +4 -0
  18. package/app/app/view/[...path]/page.tsx +3 -3
  19. package/app/components/DirView.tsx +96 -9
  20. package/app/components/FileTree.tsx +245 -27
  21. package/app/components/Sidebar.tsx +1 -0
  22. package/app/components/SidebarLayout.tsx +1 -1
  23. package/app/components/ask/AskContent.tsx +56 -14
  24. package/app/instrumentation.ts +2 -1
  25. package/app/lib/actions.ts +53 -25
  26. package/app/lib/core/create-space.ts +36 -0
  27. package/app/lib/core/fs-ops.ts +78 -1
  28. package/app/lib/core/index.ts +8 -0
  29. package/app/lib/core/list-spaces.ts +58 -0
  30. package/app/lib/core/types.ts +7 -0
  31. package/app/lib/fs.ts +78 -5
  32. package/app/lib/i18n-en.ts +10 -0
  33. package/app/lib/i18n-zh.ts +10 -0
  34. package/app/lib/project-root.ts +13 -0
  35. package/app/lib/template.ts +13 -6
  36. package/app/next-env.d.ts +1 -1
  37. package/bin/lib/mcp-spawn.js +38 -1
  38. package/mcp/README.md +5 -2
  39. package/mcp/src/index.ts +80 -0
  40. package/package.json +1 -1
  41. package/scripts/setup.js +1 -1
  42. package/skills/mindos/SKILL.md +4 -1
  43. package/skills/mindos/references/README.md +11 -0
  44. package/skills/mindos/references/post-task-hooks.md +53 -0
  45. package/skills/mindos/references/preference-capture.md +41 -0
  46. package/skills/mindos/references/sop-template.md +74 -0
  47. package/skills/mindos-zh/SKILL.md +4 -1
  48. package/skills/mindos-zh/references/README.md +11 -0
  49. package/skills/mindos-zh/references/post-task-hooks.md +53 -0
  50. package/skills/mindos-zh/references/preference-capture.md +41 -0
  51. package/skills/mindos-zh/references/sop-template.md +74 -0
  52. package/templates/empty/CONFIG.json +7 -5
  53. package/templates/empty/INSTRUCTION.md +5 -5
  54. package/templates/empty/README.md +1 -2
  55. package/templates/en/CONFIG.json +7 -5
  56. package/templates/en/INSTRUCTION.md +5 -5
  57. package/templates/en/README.md +1 -2
  58. package/templates/en//360/237/223/232 Resources/README.md" +4 -4
  59. package/templates/en//360/237/223/232 Resources//360/237/247/276 Books.csv" +1 -0
  60. package/templates/en//360/237/223/232 Resources//360/237/247/276 Learning Resources.csv" +1 -0
  61. package/templates/en//360/237/223/232 Resources//360/237/247/276 People to Follow.csv" +1 -0
  62. package/templates/en//360/237/223/232 Resources//360/237/247/276 Tools.csv" +1 -0
  63. package/templates/en//360/237/223/235 Notes/Ideas//360/237/247/252_example_product_idea.md" +9 -12
  64. package/templates/en//360/237/224/204 Workflows/Configurations//360/237/247/252_example_config_update_sop.md" +2 -3
  65. package/templates/en//360/237/224/204 Workflows/INSTRUCTION.md" +13 -5
  66. package/templates/en//360/237/232/200 Projects/Products//360/237/247/252_example_product_project_brief.md" +12 -14
  67. package/templates/template-generation-skill.md +4 -5
  68. package/templates/zh/CONFIG.json +7 -5
  69. package/templates/zh/INSTRUCTION.md +5 -5
  70. package/templates/zh/README.md +1 -2
  71. package/templates/zh//360/237/221/244 /347/224/273/345/203/217/README.md" +4 -4
  72. package/templates/zh//360/237/221/244 /347/224/273/345/203/217//342/232/231/357/270/{217 Preferences.md" → 217 /345/201/217/345/245/275.md" } +1 -1
  73. package/templates/zh//360/237/221/244 /347/224/273/345/203/217//360/237/216/{257 Focus.md" → 257 /350/201/232/347/204/246.md" } +1 -1
  74. package/templates/zh//360/237/221/244 /347/224/273/345/203/217//360/237/221/{244 Identity.md" → 244 /350/272/253/344/273/275.md" } +1 -1
  75. package/templates/zh//360/237/223/232 /350/265/204/346/272/220/README.md" +4 -4
  76. package/templates/zh//360/237/223/232 /350/265/204/346/272/220//360/237/247/276 /344/271/246/345/215/225.csv" +1 -0
  77. package/templates/zh//360/237/223/232 /350/265/204/346/272/220//360/237/247/276 /345/200/274/345/276/227/345/205/263/346/263/250/347/232/204/344/272/272.csv" +1 -0
  78. package/templates/zh//360/237/223/232 /350/265/204/346/272/220//360/237/247/276 /345/255/246/344/271/240/350/265/204/346/272/220.csv" +1 -0
  79. package/templates/zh//360/237/223/232 /350/265/204/346/272/220//360/237/247/276 /345/267/245/345/205/267/346/270/205/345/215/225.csv" +1 -0
  80. package/templates/zh//360/237/223/235 /347/254/224/350/256/260//346/203/263/346/263/225//360/237/247/252_example_/344/272/247/345/223/201/346/203/263/346/263/225.md" +8 -11
  81. package/templates/zh//360/237/224/204 /346/265/201/347/250/213/README.md" +1 -0
  82. package/templates/zh//360/237/224/204 /346/265/201/347/250/213//345/210/233/344/270/232/README.md" +3 -0
  83. package/templates/zh//360/237/224/204 /346/265/201/347/250/213//345/210/233/344/270/232//360/237/247/252_example_/346/257/217/345/221/250/345/210/233/345/247/213/344/272/272/350/277/220/350/220/245/350/212/202/345/245/217.md" +22 -0
  84. package/templates/zh//360/237/224/204 /346/265/201/347/250/213//351/205/215/347/275/256//360/237/247/252_example_/351/205/215/347/275/256/346/233/264/346/226/260/346/265/201/347/250/213.md" +1 -1
  85. package/templates/zh//360/237/232/200 /351/241/271/347/233/256//344/272/247/345/223/201//360/237/247/252_example_/344/272/247/345/223/201/351/241/271/347/233/256/347/256/200/346/212/245.md" +12 -14
  86. package/templates/empty/CONFIG.md +0 -73
  87. package/templates/en/CONFIG.md +0 -73
  88. package/templates/en//360/237/223/232 Resources//360/237/247/276 AI Influencers.csv" +0 -1
  89. package/templates/en//360/237/223/232 Resources//360/237/247/276 AI Products.csv" +0 -1
  90. package/templates/en//360/237/223/232 Resources//360/237/247/276 AI Scholars.csv" +0 -1
  91. package/templates/en//360/237/223/232 Resources//360/237/247/276 AI Tools.csv" +0 -1
  92. package/templates/zh/CONFIG.md +0 -66
  93. package/templates/zh//360/237/223/232 /350/265/204/346/272/220//360/237/247/276 AI Inferencers.csv" +0 -1
  94. package/templates/zh//360/237/223/232 /350/265/204/346/272/220//360/237/247/276 AI /344/272/247/345/223/201.csv" +0 -1
  95. package/templates/zh//360/237/223/232 /350/265/204/346/272/220//360/237/247/276 AI /345/255/246/350/200/205/346/270/205/345/215/225.csv" +0 -1
  96. package/templates/zh//360/237/223/232 /350/265/204/346/272/220//360/237/247/276 AI /345/267/245/345/205/267/346/270/205/345/215/225.csv" +0 -1
package/README.md CHANGED
@@ -55,7 +55,7 @@ MindOS is where you think, and where your AI agents act — a local-first knowle
55
55
 
56
56
  **1. Global Sync — Breaking Memory Silos**
57
57
 
58
- Switch tools or start a new chat and you're re-transporting context, scattering knowledge. **With a built-in MCP server (20+ tools), MindOS connects all Agents to your core knowledge base with zero config. Record profile and project memory once to empower all AI tools.**
58
+ Switch tools or start a new chat and you're re-transporting context, scattering knowledge. **With a built-in MCP server, MindOS connects all Agents to your core knowledge base with zero config. Record profile and project memory once to empower all AI tools.**
59
59
 
60
60
  **2. Transparent & Controllable — No Black Boxes**
61
61
 
package/README_zh.md CHANGED
@@ -55,7 +55,7 @@ MindOS 是你思考的地方,也是 AI Agent 行动的起点——一个你和
55
55
 
56
56
  **1. 全局同步 — 打破记忆割裂**
57
57
 
58
- 切换工具带来上下文割裂,个人深度背景散落各处,导致知识无法复用。**MindOS 内置 MCP Server (支持 20+ 工具),所有 Agent 零配置直连核心知识库。项目记忆与 SOP 仅需一处记录,全局赋能所有 AI 工具。**
58
+ 切换工具带来上下文割裂,个人深度背景散落各处,导致知识无法复用。**MindOS 内置 MCP Server,所有 Agent 零配置直连核心知识库。项目记忆与 SOP 仅需一处记录,全局赋能所有 AI 工具。**
59
59
 
60
60
  **2. 透明可控 — 消除记忆黑箱**
61
61
 
@@ -182,7 +182,10 @@ export async function POST(req: NextRequest) {
182
182
  // 2. user-skill-rules.md — user's personalized rules from KB root (if exists)
183
183
  const isZh = serverSettings.disabledSkills?.includes('mindos') ?? false;
184
184
  const skillDirName = isZh ? 'mindos-zh' : 'mindos';
185
- const skillPath = path.resolve(process.cwd(), `data/skills/${skillDirName}/SKILL.md`);
185
+ const appDir = process.env.MINDOS_PROJECT_ROOT
186
+ ? path.join(process.env.MINDOS_PROJECT_ROOT, 'app')
187
+ : process.cwd();
188
+ const skillPath = path.join(appDir, `data/skills/${skillDirName}/SKILL.md`);
186
189
  const skill = readAbsoluteFile(skillPath);
187
190
 
188
191
  const mindRoot = getMindRoot();
@@ -27,8 +27,10 @@ function extractPdf(buf: Buffer): { text: string; pages: number } {
27
27
  // Write PDF to a temp file so the child script can read it.
28
28
  const tmpDir = os.tmpdir();
29
29
  const tmpPdf = path.join(tmpDir, `pdf-extract-${Date.now()}.pdf`);
30
- // Dynamic path construction to prevent Turbopack static analysis
31
- const scriptPath = [process.cwd(), 'scripts', 'extract-pdf.cjs'].join(path.sep);
30
+ const appDir = process.env.MINDOS_PROJECT_ROOT
31
+ ? path.join(process.env.MINDOS_PROJECT_ROOT, 'app')
32
+ : process.cwd();
33
+ const scriptPath = path.join(appDir, 'scripts', 'extract-pdf.cjs');
32
34
 
33
35
  fs.writeFileSync(tmpPdf, buf);
34
36
  try {
@@ -13,18 +13,32 @@ import {
13
13
  updateSection,
14
14
  deleteFile,
15
15
  renameFile,
16
+ renameSpace,
16
17
  moveFile,
17
18
  appendCsvRow,
19
+ getMindRoot,
20
+ invalidateCache,
21
+ listMindSpaces,
18
22
  } from '@/lib/fs';
23
+ import { createSpaceFilesystem } from '@/lib/core/create-space';
19
24
 
20
25
  function err(msg: string, status = 400) {
21
26
  return NextResponse.json({ error: msg }, { status });
22
27
  }
23
28
 
24
- // GET /api/file?path=foo.md&op=read_file|read_lines
29
+ // GET /api/file?path=foo.md&op=read_file|read_lines | GET ?op=list_spaces (no path)
25
30
  export async function GET(req: NextRequest) {
26
31
  const filePath = req.nextUrl.searchParams.get('path');
27
32
  const op = req.nextUrl.searchParams.get('op') ?? 'read_file';
33
+
34
+ if (op === 'list_spaces') {
35
+ try {
36
+ return NextResponse.json({ spaces: listMindSpaces() });
37
+ } catch (e) {
38
+ return err((e as Error).message, 500);
39
+ }
40
+ }
41
+
28
42
  if (!filePath) return err('missing path');
29
43
 
30
44
  try {
@@ -39,7 +53,14 @@ export async function GET(req: NextRequest) {
39
53
  }
40
54
 
41
55
  // Ops that change file tree structure (sidebar needs refresh)
42
- const TREE_CHANGING_OPS = new Set(['create_file', 'delete_file', 'rename_file', 'move_file']);
56
+ const TREE_CHANGING_OPS = new Set([
57
+ 'create_file',
58
+ 'delete_file',
59
+ 'rename_file',
60
+ 'move_file',
61
+ 'create_space',
62
+ 'rename_space',
63
+ ]);
43
64
 
44
65
  // POST /api/file body: { op, path, ...params }
45
66
  export async function POST(req: NextRequest) {
@@ -138,6 +159,37 @@ export async function POST(req: NextRequest) {
138
159
  break;
139
160
  }
140
161
 
162
+ case 'create_space': {
163
+ const name = params.name;
164
+ const description = typeof params.description === 'string' ? params.description : '';
165
+ const parent_path = typeof params.parent_path === 'string' ? params.parent_path : '';
166
+ if (typeof name !== 'string' || !name.trim()) {
167
+ return err('missing or empty name');
168
+ }
169
+ try {
170
+ const { path: spacePath } = createSpaceFilesystem(getMindRoot(), name, description, parent_path);
171
+ invalidateCache();
172
+ resp = NextResponse.json({ ok: true, path: spacePath });
173
+ } catch (e) {
174
+ const msg = (e as Error).message;
175
+ const code400 =
176
+ msg.includes('required') ||
177
+ msg.includes('must not contain') ||
178
+ msg.includes('Invalid parent') ||
179
+ msg.includes('already exists');
180
+ return err(msg, code400 ? 400 : 500);
181
+ }
182
+ break;
183
+ }
184
+
185
+ case 'rename_space': {
186
+ const { new_name } = params as { new_name: string };
187
+ if (typeof new_name !== 'string' || !new_name.trim()) return err('missing new_name');
188
+ const newPath = renameSpace(filePath, new_name.trim());
189
+ resp = NextResponse.json({ ok: true, newPath });
190
+ break;
191
+ }
192
+
141
193
  case 'append_csv': {
142
194
  const { row } = params as { row: string[] };
143
195
  if (!Array.isArray(row) || row.length === 0) return err('row must be non-empty array');
@@ -11,8 +11,10 @@ function readVersion(): string {
11
11
  // 1. Env var set by CLI (most reliable)
12
12
  if (process.env.npm_package_version) return process.env.npm_package_version;
13
13
 
14
- // 2. Try known relative paths from Next.js app directory
14
+ // 2. Try known relative paths MINDOS_PROJECT_ROOT is reliable in standalone mode
15
+ const projRoot = process.env.MINDOS_PROJECT_ROOT;
15
16
  const candidates = [
17
+ ...(projRoot ? [join(projRoot, 'package.json')] : []),
16
18
  join(process.cwd(), '..', 'package.json'), // dev: app/ → root
17
19
  join(process.cwd(), 'package.json'), // if cwd is root
18
20
  join(dirname(process.cwd()), 'package.json'), // standalone edge case
@@ -36,9 +36,11 @@ const AGENT_NAME_MAP: Record<string, string> = {
36
36
 
37
37
  /** Fallback: find local skills directory for offline installs */
38
38
  function findLocalSkillsDir(): string | null {
39
+ const projRoot = process.env.MINDOS_PROJECT_ROOT || path.resolve(process.cwd(), '..');
39
40
  const candidates = [
40
- path.resolve(process.cwd(), 'data/skills'), // app/data/skills/
41
- path.resolve(process.cwd(), '..', 'skills'), // project-root/skills/
41
+ path.resolve(process.cwd(), 'data/skills'), // app/data/skills/
42
+ path.join(projRoot, 'skills'), // project-root/skills/
43
+ path.join(projRoot, 'app', 'data', 'skills'), // standalone fallback
42
44
  ];
43
45
  for (const dir of candidates) {
44
46
  if (fs.existsSync(dir)) return dir;
@@ -57,7 +57,7 @@ export async function POST() {
57
57
  await new Promise(r => setTimeout(r, 1000));
58
58
 
59
59
  // Step 3: Spawn new MCP server
60
- const root = resolve(process.cwd(), '..');
60
+ const root = process.env.MINDOS_PROJECT_ROOT || resolve(process.cwd(), '..');
61
61
  const mcpDir = resolve(root, 'mcp');
62
62
 
63
63
  if (!existsSync(resolve(mcpDir, 'node_modules'))) {
@@ -5,7 +5,7 @@ import { resolve } from 'node:path';
5
5
 
6
6
  export async function POST() {
7
7
  try {
8
- const cliPath = process.env.MINDOS_CLI_PATH || resolve(process.cwd(), '..', 'bin', 'cli.js');
8
+ const cliPath = process.env.MINDOS_CLI_PATH || resolve(process.env.MINDOS_PROJECT_ROOT || process.cwd(), '..', 'bin', 'cli.js');
9
9
  const nodeBin = process.env.MINDOS_NODE_BIN || process.execPath;
10
10
  // Use 'restart' (stop all → wait for ports free → start) instead of bare
11
11
  // 'start' which would fail assertPortFree because the current process and
@@ -5,7 +5,7 @@ import path from 'path';
5
5
  import os from 'os';
6
6
  import { readSettings, writeSettings } from '@/lib/settings';
7
7
 
8
- const PROJECT_ROOT = path.resolve(process.cwd(), '..');
8
+ const PROJECT_ROOT = process.env.MINDOS_PROJECT_ROOT || path.resolve(process.cwd(), '..');
9
9
 
10
10
  function getMindRoot(): string {
11
11
  const s = readSettings();
@@ -37,9 +37,9 @@ function isGitRepo(dir: string) {
37
37
  return existsSync(join(dir, '.git'));
38
38
  }
39
39
 
40
- /** Resolve path to bin/cli.js — prefer env var set by CLI launcher, fall back to cwd. */
40
+ /** Resolve path to bin/cli.js — prefer env var set by CLI launcher, fall back to project root. */
41
41
  function getCliPath() {
42
- return process.env.MINDOS_CLI_PATH || resolve(process.cwd(), '..', 'bin', 'cli' + '.js');
42
+ return process.env.MINDOS_CLI_PATH || resolve(process.env.MINDOS_PROJECT_ROOT || process.cwd(), '..', 'bin', 'cli' + '.js');
43
43
  }
44
44
 
45
45
  /** Run CLI command via execFile — avoids shell injection by passing args as array */
@@ -12,7 +12,7 @@ import { resolve } from 'node:path';
12
12
  */
13
13
  export async function POST() {
14
14
  try {
15
- const cliPath = process.env.MINDOS_CLI_PATH || resolve(process.cwd(), '..', 'bin', 'cli.js');
15
+ const cliPath = process.env.MINDOS_CLI_PATH || resolve(process.env.MINDOS_PROJECT_ROOT || process.cwd(), '..', 'bin', 'cli.js');
16
16
  const nodeBin = process.env.MINDOS_NODE_BIN || process.execPath;
17
17
 
18
18
  // Strip MINDOS_* env vars so the child reads fresh config
@@ -7,9 +7,17 @@ import { resolve } from 'path';
7
7
  // Read version from package.json (not process.env.npm_package_version — unavailable in daemon mode)
8
8
  let current = '0.0.0';
9
9
  try {
10
- const pkgPath = resolve(process.cwd(), '..', 'package.json');
11
- const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
12
- current = pkg.version;
10
+ const projRoot = process.env.MINDOS_PROJECT_ROOT;
11
+ const candidates = [
12
+ ...(projRoot ? [resolve(projRoot, 'package.json')] : []),
13
+ resolve(process.cwd(), '..', 'package.json'),
14
+ ];
15
+ for (const p of candidates) {
16
+ try {
17
+ const pkg = JSON.parse(readFileSync(p, 'utf-8'));
18
+ if (pkg.version) { current = pkg.version; break; }
19
+ } catch { /* try next */ }
20
+ }
13
21
  } catch {}
14
22
 
15
23
  // npm registry sources: prefer China mirror, fallback to official
@@ -422,3 +422,7 @@ a:focus-visible,
422
422
  .wysiwyg-wrapper:focus-within {
423
423
  outline: none;
424
424
  }
425
+
426
+ /* macOS Electron: traffic-light safe zone. Actual CSS is injected by Electron
427
+ main process via webContents.insertCSS(); this is a no-op fallback. */
428
+ .electron-mac-titlebar-pad { display: none; }
@@ -88,6 +88,12 @@ export default async function RootLayout({
88
88
  __html: `(function(){if(typeof Node!=='undefined'){var o=Node.prototype.removeChild;Node.prototype.removeChild=function(c){if(c.parentNode!==this){try{return o.call(c.parentNode,c)}catch(e){return c}}return o.call(this,c)};var i=Node.prototype.insertBefore;Node.prototype.insertBefore=function(n,r){if(r&&r.parentNode!==this){try{return i.call(r.parentNode,n,r)}catch(e){return i.call(this,n,null)}}return i.call(this,n,r)}}})();`,
89
89
  }}
90
90
  />
91
+ {/* Electron macOS: set data-electron-mac before first paint so sidebar clears traffic lights */}
92
+ <script
93
+ dangerouslySetInnerHTML={{
94
+ __html: `(function(){try{if(/electron/i.test(navigator.userAgent)&&/macintosh/i.test(navigator.userAgent)){document.documentElement.setAttribute('data-electron-mac','')}}catch(e){}})();`,
95
+ }}
96
+ />
91
97
  {/* Apply user appearance settings before first paint, preventing flash */}
92
98
  <script
93
99
  dangerouslySetInnerHTML={{
@@ -118,9 +118,20 @@ function LoginForm() {
118
118
  );
119
119
  }
120
120
 
121
+ function LoginFallback() {
122
+ return (
123
+ <div className="min-h-screen bg-background flex items-center justify-center px-4">
124
+ <div className="flex flex-col items-center gap-3 text-muted-foreground">
125
+ <Loader2 className="h-8 w-8 animate-spin text-[var(--amber)]" aria-hidden />
126
+ <p className="text-sm">Loading…</p>
127
+ </div>
128
+ </div>
129
+ );
130
+ }
131
+
121
132
  export default function LoginPage() {
122
133
  return (
123
- <Suspense>
134
+ <Suspense fallback={<LoginFallback />}>
124
135
  <LoginForm />
125
136
  </Suspense>
126
137
  );
@@ -4,6 +4,10 @@ import { useEffect } from 'react';
4
4
 
5
5
  export default function RegisterSW() {
6
6
  useEffect(() => {
7
+ // Electron embedded Chromium: SW can leave the UI blank or stall hydration; skip.
8
+ if (typeof navigator !== 'undefined' && /electron/i.test(navigator.userAgent)) {
9
+ return;
10
+ }
7
11
  if ('serviceWorker' in navigator) {
8
12
  navigator.serviceWorker.register('/sw.js').catch((err) => {
9
13
  console.warn('[SW] Registration failed:', err);
@@ -1,5 +1,5 @@
1
1
  import { notFound } from 'next/navigation';
2
- import { getFileContent, saveFileContent, isDirectory, getDirEntries, createFile, getFileTree } from '@/lib/fs';
2
+ import { getFileContent, saveFileContent, isDirectory, getDirEntries, createFile, getFileTree, getSpacePreview } from '@/lib/fs';
3
3
  import type { FileNode } from '@/lib/types';
4
4
  import ViewPageClient from './ViewPageClient';
5
5
  import DirView from '@/components/DirView';
@@ -24,10 +24,10 @@ export default async function ViewPage({ params }: PageProps) {
24
24
  const { path: segments } = await params;
25
25
  const filePath = segments.map(decodeURIComponent).join('/');
26
26
 
27
- // Directory: show folder listing
28
27
  if (isDirectory(filePath)) {
29
28
  const entries = getDirEntries(filePath);
30
- return <DirView dirPath={filePath} entries={entries} />;
29
+ const spacePreview = getSpacePreview(filePath);
30
+ return <DirView dirPath={filePath} entries={entries} spacePreview={spacePreview} />;
31
31
  }
32
32
 
33
33
  const extension = filePath.split('.').pop()?.toLowerCase() || '';
@@ -1,16 +1,20 @@
1
1
  'use client';
2
2
 
3
- import { useState, useSyncExternalStore, useMemo } from 'react';
3
+ import { useSyncExternalStore, useMemo } from 'react';
4
4
  import Link from 'next/link';
5
- import { FileText, Table, Folder, FolderOpen, LayoutGrid, List, FilePlus } from 'lucide-react';
5
+ import { FileText, Table, Folder, FolderOpen, LayoutGrid, List, FilePlus, ScrollText, BookOpen } from 'lucide-react';
6
6
  import Breadcrumb from '@/components/Breadcrumb';
7
7
  import { encodePath, relativeTime } from '@/lib/utils';
8
8
  import { FileNode } from '@/lib/types';
9
+ import type { SpacePreview } from '@/lib/core/types';
9
10
  import { useLocale } from '@/lib/LocaleContext';
10
11
 
12
+ const SYSTEM_FILES = new Set(['INSTRUCTION.md', 'README.md']);
13
+
11
14
  interface DirViewProps {
12
15
  dirPath: string;
13
16
  entries: FileNode[];
17
+ spacePreview?: SpacePreview | null;
14
18
  }
15
19
 
16
20
  function FileIcon({ node }: { node: FileNode }) {
@@ -54,15 +58,94 @@ function useDirViewPref() {
54
58
  return [view, setView] as const;
55
59
  }
56
60
 
57
- export default function DirView({ dirPath, entries }: DirViewProps) {
61
+ // ─── Space Preview Cards ──────────────────────────────────────────────────────
62
+
63
+ function SpacePreviewCard({ icon, title, lines, viewAllHref, viewAllLabel }: {
64
+ icon: React.ReactNode;
65
+ title: string;
66
+ lines: string[];
67
+ viewAllHref: string;
68
+ viewAllLabel: string;
69
+ }) {
70
+ if (lines.length === 0) return null;
71
+ return (
72
+ <div className="bg-muted/30 border border-border/40 rounded-lg px-4 py-3">
73
+ <div className="flex items-center gap-1.5 mb-2">
74
+ {icon}
75
+ <span className="text-sm font-medium text-muted-foreground">{title}</span>
76
+ </div>
77
+ <div className="space-y-1">
78
+ {lines.map((line, i) => (
79
+ <p key={i} className="text-sm text-muted-foreground/80 leading-relaxed">
80
+ · {line}
81
+ </p>
82
+ ))}
83
+ </div>
84
+ <div className="flex justify-end mt-2">
85
+ <Link
86
+ href={viewAllHref}
87
+ className="text-xs hover:underline transition-colors"
88
+ style={{ color: 'var(--amber)' }}
89
+ >
90
+ {viewAllLabel}
91
+ </Link>
92
+ </div>
93
+ </div>
94
+ );
95
+ }
96
+
97
+ function SpacePreviewSection({ preview, dirPath }: {
98
+ preview: SpacePreview;
99
+ dirPath: string;
100
+ }) {
101
+ const { t } = useLocale();
102
+ const hasRules = preview.instructionLines.length > 0;
103
+ const hasAbout = preview.readmeLines.length > 0;
104
+ if (!hasRules && !hasAbout) return null;
105
+
106
+ return (
107
+ <div className="grid grid-cols-1 sm:grid-cols-2 gap-3 mb-6">
108
+ {hasRules && (
109
+ <SpacePreviewCard
110
+ icon={<ScrollText size={14} className="text-muted-foreground shrink-0" />}
111
+ title={t.fileTree.rules}
112
+ lines={preview.instructionLines}
113
+ viewAllHref={`/view/${encodePath(`${dirPath}/INSTRUCTION.md`)}`}
114
+ viewAllLabel={t.fileTree.viewAll}
115
+ />
116
+ )}
117
+ {hasAbout && (
118
+ <SpacePreviewCard
119
+ icon={<BookOpen size={14} className="text-muted-foreground shrink-0" />}
120
+ title={t.fileTree.about}
121
+ lines={preview.readmeLines}
122
+ viewAllHref={`/view/${encodePath(`${dirPath}/README.md`)}`}
123
+ viewAllLabel={t.fileTree.viewAll}
124
+ />
125
+ )}
126
+ </div>
127
+ );
128
+ }
129
+
130
+ // ─── DirView ──────────────────────────────────────────────────────────────────
131
+
132
+ export default function DirView({ dirPath, entries, spacePreview }: DirViewProps) {
58
133
  const [view, setView] = useDirViewPref();
59
134
  const { t } = useLocale();
60
135
  const formatTime = (mtime: number) => relativeTime(mtime, t.home.relativeTime);
136
+
137
+ const visibleEntries = useMemo(() => {
138
+ if (spacePreview) {
139
+ return entries.filter(e => e.type !== 'file' || !SYSTEM_FILES.has(e.name));
140
+ }
141
+ return entries.filter(e => e.type !== 'file' || e.name !== 'README.md');
142
+ }, [entries, spacePreview]);
143
+
61
144
  const fileCounts = useMemo(() => {
62
145
  const map = new Map<string, number>();
63
- for (const e of entries) map.set(e.path, countFiles(e));
146
+ for (const e of visibleEntries) map.set(e.path, countFiles(e));
64
147
  return map;
65
- }, [entries]);
148
+ }, [visibleEntries]);
66
149
 
67
150
  return (
68
151
  <div className="flex flex-col min-h-screen">
@@ -72,7 +155,6 @@ export default function DirView({ dirPath, entries }: DirViewProps) {
72
155
  <div className="min-w-0 flex-1">
73
156
  <Breadcrumb filePath={dirPath} />
74
157
  </div>
75
- {/* New file + View toggle */}
76
158
  <div className="flex items-center gap-2 shrink-0">
77
159
  <Link
78
160
  href={`/view/${encodePath(dirPath ? `${dirPath}/Untitled.md` : 'Untitled.md')}`}
@@ -104,11 +186,16 @@ export default function DirView({ dirPath, entries }: DirViewProps) {
104
186
  {/* Content */}
105
187
  <div className="flex-1 px-4 md:px-6 py-6">
106
188
  <div className="max-w-[860px] mx-auto">
107
- {entries.length === 0 ? (
189
+ {/* Space preview cards */}
190
+ {spacePreview && (
191
+ <SpacePreviewSection preview={spacePreview} dirPath={dirPath} />
192
+ )}
193
+
194
+ {visibleEntries.length === 0 ? (
108
195
  <p className="text-muted-foreground text-sm">{t.dirView.emptyFolder}</p>
109
196
  ) : view === 'grid' ? (
110
197
  <div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 xl:grid-cols-5 gap-3">
111
- {entries.map(entry => (
198
+ {visibleEntries.map(entry => (
112
199
  <Link
113
200
  key={entry.path}
114
201
  href={`/view/${encodePath(entry.path)}`}
@@ -137,7 +224,7 @@ export default function DirView({ dirPath, entries }: DirViewProps) {
137
224
  </div>
138
225
  ) : (
139
226
  <div className="flex flex-col divide-y divide-border border border-border rounded-xl overflow-hidden">
140
- {entries.map(entry => (
227
+ {visibleEntries.map(entry => (
141
228
  <Link
142
229
  key={entry.path}
143
230
  href={`/view/${encodePath(entry.path)}`}