@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.
- package/README.md +1 -1
- package/README_zh.md +1 -1
- package/app/app/api/ask/route.ts +4 -1
- package/app/app/api/extract-pdf/route.ts +4 -2
- package/app/app/api/file/route.ts +54 -2
- package/app/app/api/health/route.ts +3 -1
- package/app/app/api/mcp/install-skill/route.ts +4 -2
- package/app/app/api/mcp/restart/route.ts +1 -1
- package/app/app/api/restart/route.ts +1 -1
- package/app/app/api/skills/route.ts +1 -1
- package/app/app/api/sync/route.ts +2 -2
- package/app/app/api/update/route.ts +1 -1
- package/app/app/api/update-check/route.ts +11 -3
- package/app/app/globals.css +4 -0
- package/app/app/layout.tsx +6 -0
- package/app/app/login/page.tsx +12 -1
- package/app/app/register-sw.tsx +4 -0
- package/app/app/view/[...path]/page.tsx +3 -3
- package/app/components/DirView.tsx +96 -9
- package/app/components/FileTree.tsx +245 -27
- package/app/components/Sidebar.tsx +1 -0
- package/app/components/SidebarLayout.tsx +1 -1
- package/app/components/ask/AskContent.tsx +56 -14
- package/app/instrumentation.ts +2 -1
- package/app/lib/actions.ts +53 -25
- package/app/lib/core/create-space.ts +36 -0
- package/app/lib/core/fs-ops.ts +78 -1
- package/app/lib/core/index.ts +8 -0
- package/app/lib/core/list-spaces.ts +58 -0
- package/app/lib/core/types.ts +7 -0
- package/app/lib/fs.ts +78 -5
- package/app/lib/i18n-en.ts +10 -0
- package/app/lib/i18n-zh.ts +10 -0
- package/app/lib/project-root.ts +13 -0
- package/app/lib/template.ts +13 -6
- package/app/next-env.d.ts +1 -1
- package/bin/lib/mcp-spawn.js +38 -1
- package/mcp/README.md +5 -2
- package/mcp/src/index.ts +80 -0
- package/package.json +1 -1
- package/scripts/setup.js +1 -1
- package/skills/mindos/SKILL.md +4 -1
- package/skills/mindos/references/README.md +11 -0
- package/skills/mindos/references/post-task-hooks.md +53 -0
- package/skills/mindos/references/preference-capture.md +41 -0
- package/skills/mindos/references/sop-template.md +74 -0
- package/skills/mindos-zh/SKILL.md +4 -1
- package/skills/mindos-zh/references/README.md +11 -0
- package/skills/mindos-zh/references/post-task-hooks.md +53 -0
- package/skills/mindos-zh/references/preference-capture.md +41 -0
- package/skills/mindos-zh/references/sop-template.md +74 -0
- package/templates/empty/CONFIG.json +7 -5
- package/templates/empty/INSTRUCTION.md +5 -5
- package/templates/empty/README.md +1 -2
- package/templates/en/CONFIG.json +7 -5
- package/templates/en/INSTRUCTION.md +5 -5
- package/templates/en/README.md +1 -2
- package/templates/en//360/237/223/232 Resources/README.md" +4 -4
- package/templates/en//360/237/223/232 Resources//360/237/247/276 Books.csv" +1 -0
- package/templates/en//360/237/223/232 Resources//360/237/247/276 Learning Resources.csv" +1 -0
- package/templates/en//360/237/223/232 Resources//360/237/247/276 People to Follow.csv" +1 -0
- package/templates/en//360/237/223/232 Resources//360/237/247/276 Tools.csv" +1 -0
- package/templates/en//360/237/223/235 Notes/Ideas//360/237/247/252_example_product_idea.md" +9 -12
- package/templates/en//360/237/224/204 Workflows/Configurations//360/237/247/252_example_config_update_sop.md" +2 -3
- package/templates/en//360/237/224/204 Workflows/INSTRUCTION.md" +13 -5
- package/templates/en//360/237/232/200 Projects/Products//360/237/247/252_example_product_project_brief.md" +12 -14
- package/templates/template-generation-skill.md +4 -5
- package/templates/zh/CONFIG.json +7 -5
- package/templates/zh/INSTRUCTION.md +5 -5
- package/templates/zh/README.md +1 -2
- package/templates/zh//360/237/221/244 /347/224/273/345/203/217/README.md" +4 -4
- 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
- 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
- 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
- package/templates/zh//360/237/223/232 /350/265/204/346/272/220/README.md" +4 -4
- 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
- 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
- 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
- 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
- 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
- package/templates/zh//360/237/224/204 /346/265/201/347/250/213/README.md" +1 -0
- package/templates/zh//360/237/224/204 /346/265/201/347/250/213//345/210/233/344/270/232/README.md" +3 -0
- 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
- 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
- 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
- package/templates/empty/CONFIG.md +0 -73
- package/templates/en/CONFIG.md +0 -73
- package/templates/en//360/237/223/232 Resources//360/237/247/276 AI Influencers.csv" +0 -1
- package/templates/en//360/237/223/232 Resources//360/237/247/276 AI Products.csv" +0 -1
- package/templates/en//360/237/223/232 Resources//360/237/247/276 AI Scholars.csv" +0 -1
- package/templates/en//360/237/223/232 Resources//360/237/247/276 AI Tools.csv" +0 -1
- package/templates/zh/CONFIG.md +0 -66
- package/templates/zh//360/237/223/232 /350/265/204/346/272/220//360/237/247/276 AI Inferencers.csv" +0 -1
- 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
- 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
- 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
|
|
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
|
|
58
|
+
切换工具带来上下文割裂,个人深度背景散落各处,导致知识无法复用。**MindOS 内置 MCP Server,所有 Agent 零配置直连核心知识库。项目记忆与 SOP 仅需一处记录,全局赋能所有 AI 工具。**
|
|
59
59
|
|
|
60
60
|
**2. 透明可控 — 消除记忆黑箱**
|
|
61
61
|
|
package/app/app/api/ask/route.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
31
|
-
|
|
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([
|
|
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
|
|
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'),
|
|
41
|
-
path.
|
|
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
|
|
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
|
|
11
|
-
const
|
|
12
|
-
|
|
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
|
package/app/app/globals.css
CHANGED
|
@@ -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; }
|
package/app/app/layout.tsx
CHANGED
|
@@ -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={{
|
package/app/app/login/page.tsx
CHANGED
|
@@ -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
|
);
|
package/app/app/register-sw.tsx
CHANGED
|
@@ -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
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
|
146
|
+
for (const e of visibleEntries) map.set(e.path, countFiles(e));
|
|
64
147
|
return map;
|
|
65
|
-
}, [
|
|
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
|
-
{
|
|
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
|
-
{
|
|
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
|
-
{
|
|
227
|
+
{visibleEntries.map(entry => (
|
|
141
228
|
<Link
|
|
142
229
|
key={entry.path}
|
|
143
230
|
href={`/view/${encodePath(entry.path)}`}
|