@geminilight/mindos 0.6.7 → 0.6.12
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 +2 -0
- package/README_zh.md +2 -0
- package/app/app/api/ask/route.ts +35 -2
- package/app/app/api/file/route.ts +27 -0
- package/app/app/api/mcp/install/route.ts +4 -1
- package/app/app/api/setup/check-path/route.ts +2 -7
- package/app/app/api/setup/check-port/route.ts +18 -13
- package/app/app/api/setup/ls/route.ts +3 -9
- package/app/app/api/setup/path-utils.ts +8 -0
- package/app/app/api/setup/route.ts +2 -7
- package/app/app/api/uninstall/route.ts +47 -0
- package/app/app/globals.css +11 -0
- package/app/components/ActivityBar.tsx +10 -3
- package/app/components/AskFab.tsx +7 -3
- package/app/components/CreateSpaceModal.tsx +1 -1
- package/app/components/DirView.tsx +1 -1
- package/app/components/FileTree.tsx +30 -23
- package/app/components/GuideCard.tsx +1 -1
- package/app/components/HomeContent.tsx +137 -109
- package/app/components/ImportModal.tsx +104 -60
- package/app/components/MarkdownView.tsx +3 -0
- package/app/components/OnboardingView.tsx +1 -1
- package/app/components/OrganizeToast.tsx +386 -0
- package/app/components/Panel.tsx +23 -2
- package/app/components/Sidebar.tsx +1 -1
- package/app/components/SidebarLayout.tsx +44 -1
- package/app/components/agents/AgentDetailContent.tsx +33 -12
- package/app/components/agents/AgentsMcpSection.tsx +1 -1
- package/app/components/agents/AgentsOverviewSection.tsx +3 -4
- package/app/components/agents/AgentsPrimitives.tsx +2 -2
- package/app/components/agents/AgentsSkillsSection.tsx +2 -2
- package/app/components/agents/SkillDetailPopover.tsx +24 -8
- package/app/components/ask/AskContent.tsx +124 -70
- package/app/components/ask/HighlightMatch.tsx +14 -0
- package/app/components/ask/MentionPopover.tsx +5 -3
- package/app/components/ask/MessageList.tsx +39 -11
- package/app/components/ask/SlashCommandPopover.tsx +4 -2
- package/app/components/changes/ChangesBanner.tsx +20 -2
- package/app/components/changes/ChangesContentPage.tsx +10 -2
- package/app/components/echo/EchoHero.tsx +1 -1
- package/app/components/echo/EchoInsightCollapsible.tsx +1 -1
- package/app/components/echo/EchoPageSections.tsx +1 -1
- package/app/components/explore/UseCaseCard.tsx +1 -1
- package/app/components/panels/DiscoverPanel.tsx +29 -25
- package/app/components/panels/ImportHistoryPanel.tsx +195 -0
- package/app/components/panels/PluginsPanel.tsx +2 -2
- package/app/components/settings/AiTab.tsx +24 -0
- package/app/components/settings/KnowledgeTab.tsx +1 -1
- package/app/components/settings/McpSkillCreateForm.tsx +1 -1
- package/app/components/settings/McpSkillRow.tsx +1 -1
- package/app/components/settings/McpSkillsSection.tsx +2 -2
- package/app/components/settings/McpTab.tsx +2 -2
- package/app/components/settings/PluginsTab.tsx +1 -1
- package/app/components/settings/Primitives.tsx +118 -6
- package/app/components/settings/SettingsContent.tsx +5 -2
- package/app/components/settings/UninstallTab.tsx +179 -0
- package/app/components/settings/UpdateTab.tsx +17 -5
- package/app/components/settings/types.ts +2 -1
- package/app/components/ui/dialog.tsx +1 -1
- package/app/hooks/useAiOrganize.ts +450 -0
- package/app/hooks/useFileImport.ts +39 -2
- package/app/hooks/useMention.ts +21 -3
- package/app/hooks/useSlashCommand.ts +18 -4
- package/app/lib/agent/reconnect.ts +40 -0
- package/app/lib/core/backlinks.ts +2 -2
- package/app/lib/core/git.ts +14 -10
- package/app/lib/fs.ts +2 -1
- package/app/lib/i18n-en.ts +85 -4
- package/app/lib/i18n-zh.ts +85 -4
- package/app/lib/organize-history.ts +74 -0
- package/app/lib/settings.ts +2 -0
- package/app/lib/types.ts +2 -0
- package/app/next-env.d.ts +1 -1
- package/app/next.config.ts +23 -5
- package/app/package.json +1 -1
- package/bin/cli.js +21 -18
- package/bin/lib/mcp-build.js +74 -0
- package/bin/lib/mcp-spawn.js +8 -5
- package/bin/lib/port.js +17 -2
- package/bin/lib/stop.js +12 -2
- package/mcp/dist/index.cjs +43 -43
- package/mcp/src/index.ts +58 -12
- package/package.json +1 -1
- package/scripts/release.sh +1 -1
- package/scripts/setup.js +2 -2
package/README.md
CHANGED
|
@@ -272,9 +272,11 @@ Join our WeChat group for early access, feedback, and AI workflow discussions:
|
|
|
272
272
|
## 👥 Contributors
|
|
273
273
|
|
|
274
274
|
<a href="https://github.com/GeminiLight"><img src="https://github.com/GeminiLight.png" width="60" style="border-radius:50%" alt="GeminiLight" /></a>
|
|
275
|
+
<a href="https://github.com/U-rara"><img src="https://github.com/U-rara.png" width="60" style="border-radius:50%" alt="U-rara" /></a>
|
|
275
276
|
<a href="https://github.com/yeahjack"><img src="https://github.com/yeahjack.png" width="60" style="border-radius:50%" alt="yeahjack" /></a>
|
|
276
277
|
<a href="https://github.com/USTChandsomeboy"><img src="https://github.com/USTChandsomeboy.png" width="60" style="border-radius:50%" alt="USTChandsomeboy" /></a>
|
|
277
278
|
<a href="https://github.com/ppsmk388"><img src="https://github.com/ppsmk388.png" width="60" style="border-radius:50%" alt="ppsmk388" /></a>
|
|
279
|
+
<a href="https://github.com/one2piece2hello"><img src="https://github.com/one2piece2hello.png" width="60" style="border-radius:50%" alt="one2piece2hello" /></a>
|
|
278
280
|
|
|
279
281
|
### 🙏 Acknowledgements
|
|
280
282
|
|
package/README_zh.md
CHANGED
|
@@ -275,6 +275,8 @@ MindOS/
|
|
|
275
275
|
<a href="https://github.com/yeahjack"><img src="https://github.com/yeahjack.png" width="60" style="border-radius:50%" alt="yeahjack" /></a>
|
|
276
276
|
<a href="https://github.com/USTChandsomeboy"><img src="https://github.com/USTChandsomeboy.png" width="60" style="border-radius:50%" alt="USTChandsomeboy" /></a>
|
|
277
277
|
<a href="https://github.com/ppsmk388"><img src="https://github.com/ppsmk388.png" width="60" style="border-radius:50%" alt="ppsmk388" /></a>
|
|
278
|
+
<a href="https://github.com/U-rara"><img src="https://github.com/U-rara.png" width="60" style="border-radius:50%" alt="U-rara" /></a>
|
|
279
|
+
<a href="https://github.com/one2piece2hello"><img src="https://github.com/one2piece2hello.png" width="60" style="border-radius:50%" alt="one2piece2hello" /></a>
|
|
278
280
|
|
|
279
281
|
### 🙏 致谢
|
|
280
282
|
|
package/app/app/api/ask/route.ts
CHANGED
|
@@ -103,6 +103,34 @@ function getTurnEndData(e: AgentEvent): { toolResults: Array<{ toolName: string;
|
|
|
103
103
|
// Helpers
|
|
104
104
|
// ---------------------------------------------------------------------------
|
|
105
105
|
|
|
106
|
+
/**
|
|
107
|
+
* Strip large fields (file content) from tool args before SSE serialization.
|
|
108
|
+
* The client only needs path/name for progress display, not the full content.
|
|
109
|
+
* This prevents JSON.stringify failures on oversized payloads.
|
|
110
|
+
*/
|
|
111
|
+
function sanitizeToolArgs(toolName: string, args: unknown): unknown {
|
|
112
|
+
if (!args || typeof args !== 'object') return args;
|
|
113
|
+
const a = args as Record<string, unknown>;
|
|
114
|
+
|
|
115
|
+
if (toolName === 'batch_create_files' && Array.isArray(a.files)) {
|
|
116
|
+
return {
|
|
117
|
+
...a,
|
|
118
|
+
files: (a.files as Array<Record<string, unknown>>).map(f => ({
|
|
119
|
+
path: f.path,
|
|
120
|
+
...(f.description ? { description: f.description } : {}),
|
|
121
|
+
})),
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (typeof a.content === 'string' && a.content.length > 200) {
|
|
126
|
+
return { ...a, content: `[${a.content.length} chars]` };
|
|
127
|
+
}
|
|
128
|
+
if (typeof a.text === 'string' && a.text.length > 200) {
|
|
129
|
+
return { ...a, text: `[${a.text.length} chars]` };
|
|
130
|
+
}
|
|
131
|
+
return args;
|
|
132
|
+
}
|
|
133
|
+
|
|
106
134
|
function readKnowledgeFile(filePath: string): { ok: boolean; content: string; truncated: boolean; error?: string } {
|
|
107
135
|
try {
|
|
108
136
|
const raw = getFileContent(filePath);
|
|
@@ -457,7 +485,11 @@ export async function POST(req: NextRequest) {
|
|
|
457
485
|
function send(event: MindOSSSEvent) {
|
|
458
486
|
try {
|
|
459
487
|
controller.enqueue(encoder.encode(`data:${JSON.stringify(event)}\n\n`));
|
|
460
|
-
} catch {
|
|
488
|
+
} catch (err) {
|
|
489
|
+
if (err instanceof TypeError) {
|
|
490
|
+
console.error('[ask] SSE send failed (serialization):', (err as Error).message, 'event type:', (event as { type?: string }).type);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
461
493
|
}
|
|
462
494
|
|
|
463
495
|
session.subscribe((event: AgentEvent) => {
|
|
@@ -467,11 +499,12 @@ export async function POST(req: NextRequest) {
|
|
|
467
499
|
send({ type: 'thinking_delta', delta: getThinkingDelta(event) });
|
|
468
500
|
} else if (isToolExecutionStartEvent(event)) {
|
|
469
501
|
const { toolCallId, toolName, args } = getToolExecutionStart(event);
|
|
502
|
+
const safeArgs = sanitizeToolArgs(toolName, args);
|
|
470
503
|
send({
|
|
471
504
|
type: 'tool_start',
|
|
472
505
|
toolCallId,
|
|
473
506
|
toolName,
|
|
474
|
-
args,
|
|
507
|
+
args: safeArgs,
|
|
475
508
|
});
|
|
476
509
|
} else if (isToolExecutionEndEvent(event)) {
|
|
477
510
|
const { toolCallId, output, isError } = getToolExecutionEnd(event);
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
export const dynamic = 'force-dynamic';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
2
4
|
import { NextRequest, NextResponse } from 'next/server';
|
|
3
5
|
import { revalidatePath } from 'next/cache';
|
|
6
|
+
import { resolveSafe } from '@/lib/core/security';
|
|
7
|
+
import { sanitizeFileName, convertToMarkdown } from '@/lib/core/file-convert';
|
|
8
|
+
import { effectiveSopRoot } from '@/lib/settings';
|
|
4
9
|
import {
|
|
5
10
|
getFileContent,
|
|
6
11
|
saveFileContent,
|
|
@@ -57,6 +62,28 @@ export async function GET(req: NextRequest) {
|
|
|
57
62
|
}
|
|
58
63
|
}
|
|
59
64
|
|
|
65
|
+
if (op === 'check_conflicts') {
|
|
66
|
+
const names = req.nextUrl.searchParams.get('names');
|
|
67
|
+
const space = req.nextUrl.searchParams.get('space') ?? '';
|
|
68
|
+
if (!names) return err('missing names');
|
|
69
|
+
try {
|
|
70
|
+
const mindRoot = effectiveSopRoot().trim();
|
|
71
|
+
if (!mindRoot) return err('MIND_ROOT not configured');
|
|
72
|
+
const fileNames = names.split(',').map(n => n.trim()).filter(Boolean);
|
|
73
|
+
const conflicts: string[] = [];
|
|
74
|
+
for (const name of fileNames) {
|
|
75
|
+
const sanitized = sanitizeFileName(name);
|
|
76
|
+
const { targetName } = convertToMarkdown(sanitized, '');
|
|
77
|
+
const rel = space ? path.posix.join(space, targetName) : targetName;
|
|
78
|
+
const resolved = resolveSafe(mindRoot, rel);
|
|
79
|
+
if (fs.existsSync(resolved)) conflicts.push(name);
|
|
80
|
+
}
|
|
81
|
+
return NextResponse.json({ conflicts });
|
|
82
|
+
} catch (e) {
|
|
83
|
+
return err((e as Error).message, 500);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
60
87
|
if (!filePath) return err('missing path');
|
|
61
88
|
|
|
62
89
|
try {
|
|
@@ -3,6 +3,7 @@ import { NextRequest, NextResponse } from 'next/server';
|
|
|
3
3
|
import fs from 'fs';
|
|
4
4
|
import path from 'path';
|
|
5
5
|
import { MCP_AGENTS, expandHome } from '@/lib/mcp-agents';
|
|
6
|
+
import { readSettings } from '@/lib/settings';
|
|
6
7
|
|
|
7
8
|
/** Parse JSONC — strips single-line (//) and block comments before JSON.parse */
|
|
8
9
|
function parseJsonc(text: string): Record<string, unknown> {
|
|
@@ -104,7 +105,9 @@ function buildEntry(transport: string, url?: string, token?: string) {
|
|
|
104
105
|
if (transport === 'stdio') {
|
|
105
106
|
return { type: 'stdio', command: 'mindos', args: ['mcp'], env: { MCP_TRANSPORT: 'stdio' } };
|
|
106
107
|
}
|
|
107
|
-
|
|
108
|
+
// Resolve MCP port from env → config → default, not hardcoded
|
|
109
|
+
const fallbackPort = Number(process.env.MINDOS_MCP_PORT) || readSettings().mcpPort || 8781;
|
|
110
|
+
const entry: Record<string, unknown> = { url: url || `http://localhost:${fallbackPort}/mcp` };
|
|
108
111
|
if (token) entry.headers = { Authorization: `Bearer ${token}` };
|
|
109
112
|
return entry;
|
|
110
113
|
}
|
|
@@ -1,12 +1,7 @@
|
|
|
1
1
|
export const dynamic = 'force-dynamic';
|
|
2
2
|
import { NextRequest, NextResponse } from 'next/server';
|
|
3
3
|
import { existsSync, readdirSync } from 'node:fs';
|
|
4
|
-
import {
|
|
5
|
-
import { resolve } from 'node:path';
|
|
6
|
-
|
|
7
|
-
function expandHome(p: string): string {
|
|
8
|
-
return p.startsWith('~/') ? resolve(homedir(), p.slice(2)) : p;
|
|
9
|
-
}
|
|
4
|
+
import { expandSetupPathHome } from '../path-utils';
|
|
10
5
|
|
|
11
6
|
export async function POST(req: NextRequest) {
|
|
12
7
|
try {
|
|
@@ -14,7 +9,7 @@ export async function POST(req: NextRequest) {
|
|
|
14
9
|
if (!path || typeof path !== 'string') {
|
|
15
10
|
return NextResponse.json({ error: 'Invalid path' }, { status: 400 });
|
|
16
11
|
}
|
|
17
|
-
const abs =
|
|
12
|
+
const abs = expandSetupPathHome(path.trim());
|
|
18
13
|
const exists = existsSync(abs);
|
|
19
14
|
let empty = true;
|
|
20
15
|
let count = 0;
|
|
@@ -37,16 +37,20 @@ async function findFreePort(start: number, selfPorts: Set<number>): Promise<numb
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
/**
|
|
40
|
-
*
|
|
41
|
-
* Derived from the incoming request URL — always reliable, no network round-trip.
|
|
40
|
+
* Ports this MindOS instance is known to be using.
|
|
42
41
|
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
42
|
+
* myWebPort: derived from the incoming request URL — always reliable.
|
|
43
|
+
* myMcpPort: from MINDOS_MCP_PORT env var set by CLI / Desktop ProcessManager.
|
|
44
|
+
*
|
|
45
|
+
* We do NOT read ~/.mindos/config.json here. Config contains *configured* ports
|
|
46
|
+
* which may not actually be listening yet (e.g. first onboard before MCP starts).
|
|
47
|
+
* Env vars are only set when a process IS running, so they're safe to trust.
|
|
47
48
|
*/
|
|
48
|
-
function
|
|
49
|
-
return
|
|
49
|
+
function getKnownPorts(req: NextRequest): { myWebPort: number; myMcpPort: number } {
|
|
50
|
+
return {
|
|
51
|
+
myWebPort: parseInt(req.nextUrl.port || '0', 10),
|
|
52
|
+
myMcpPort: Number(process.env.MINDOS_MCP_PORT) || 0,
|
|
53
|
+
};
|
|
50
54
|
}
|
|
51
55
|
|
|
52
56
|
export async function POST(req: NextRequest) {
|
|
@@ -56,10 +60,10 @@ export async function POST(req: NextRequest) {
|
|
|
56
60
|
return NextResponse.json({ error: 'Invalid port' }, { status: 400 });
|
|
57
61
|
}
|
|
58
62
|
|
|
59
|
-
const
|
|
63
|
+
const { myWebPort, myMcpPort } = getKnownPorts(req);
|
|
60
64
|
|
|
61
|
-
// Fast path:
|
|
62
|
-
if (
|
|
65
|
+
// Fast path: port belongs to this MindOS instance (deterministic, no network)
|
|
66
|
+
if ((myWebPort > 0 && port === myWebPort) || (myMcpPort > 0 && port === myMcpPort)) {
|
|
63
67
|
return NextResponse.json({ available: true, isSelf: true });
|
|
64
68
|
}
|
|
65
69
|
|
|
@@ -67,13 +71,14 @@ export async function POST(req: NextRequest) {
|
|
|
67
71
|
if (!inUse) {
|
|
68
72
|
return NextResponse.json({ available: true, isSelf: false });
|
|
69
73
|
}
|
|
70
|
-
// Port is occupied — check if it's another MindOS instance
|
|
74
|
+
// Port is occupied by something else — check if it's another MindOS instance
|
|
71
75
|
const self = await isSelfPort(port);
|
|
72
76
|
if (self) {
|
|
73
77
|
return NextResponse.json({ available: true, isSelf: true });
|
|
74
78
|
}
|
|
75
79
|
const skipPorts = new Set<number>();
|
|
76
|
-
if (
|
|
80
|
+
if (myWebPort > 0) skipPorts.add(myWebPort);
|
|
81
|
+
if (myMcpPort > 0) skipPorts.add(myMcpPort);
|
|
77
82
|
const suggestion = await findFreePort(port + 1, skipPorts);
|
|
78
83
|
return NextResponse.json({ available: false, isSelf: false, suggestion });
|
|
79
84
|
} catch (err) {
|
|
@@ -1,14 +1,8 @@
|
|
|
1
1
|
export const dynamic = 'force-dynamic';
|
|
2
2
|
import { NextRequest, NextResponse } from 'next/server';
|
|
3
3
|
import { existsSync, readdirSync, statSync } from 'node:fs';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
function expandHome(p: string): string {
|
|
8
|
-
if (p === '~') return homedir();
|
|
9
|
-
if (p.startsWith('~/') || p.startsWith('~\\')) return resolve(homedir(), p.slice(2));
|
|
10
|
-
return p;
|
|
11
|
-
}
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { expandSetupPathHome } from '../path-utils';
|
|
12
6
|
|
|
13
7
|
export async function POST(req: NextRequest) {
|
|
14
8
|
try {
|
|
@@ -16,7 +10,7 @@ export async function POST(req: NextRequest) {
|
|
|
16
10
|
if (!path || typeof path !== 'string') {
|
|
17
11
|
return NextResponse.json({ dirs: [] });
|
|
18
12
|
}
|
|
19
|
-
const abs =
|
|
13
|
+
const abs = expandSetupPathHome(path.trim());
|
|
20
14
|
if (!existsSync(abs)) {
|
|
21
15
|
return NextResponse.json({ dirs: [] });
|
|
22
16
|
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { homedir } from 'node:os';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
|
|
4
|
+
export function expandSetupPathHome(p: string): string {
|
|
5
|
+
if (p === '~') return homedir();
|
|
6
|
+
if (p.startsWith('~/') || p.startsWith('~\\')) return resolve(homedir(), p.slice(2));
|
|
7
|
+
return p;
|
|
8
|
+
}
|
|
@@ -4,6 +4,7 @@ import fs from 'fs';
|
|
|
4
4
|
import os from 'os';
|
|
5
5
|
import { readSettings, writeSettings, ServerSettings } from '@/lib/settings';
|
|
6
6
|
import { applyTemplate } from '@/lib/template';
|
|
7
|
+
import { expandSetupPathHome } from './path-utils';
|
|
7
8
|
|
|
8
9
|
function maskApiKey(key: string): string {
|
|
9
10
|
if (!key || key.length < 6) return key ? '***' : '';
|
|
@@ -40,12 +41,6 @@ export async function GET() {
|
|
|
40
41
|
}
|
|
41
42
|
}
|
|
42
43
|
|
|
43
|
-
function expandHome(p: string): string {
|
|
44
|
-
if (p.startsWith('~/')) return p.replace('~', os.homedir());
|
|
45
|
-
if (p === '~') return os.homedir();
|
|
46
|
-
return p;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
44
|
export async function POST(req: NextRequest) {
|
|
50
45
|
try {
|
|
51
46
|
const body = await req.json();
|
|
@@ -56,7 +51,7 @@ export async function POST(req: NextRequest) {
|
|
|
56
51
|
return NextResponse.json({ error: 'mindRoot is required' }, { status: 400 });
|
|
57
52
|
}
|
|
58
53
|
|
|
59
|
-
const resolvedRoot =
|
|
54
|
+
const resolvedRoot = expandSetupPathHome(mindRoot.trim());
|
|
60
55
|
|
|
61
56
|
// Validate ports
|
|
62
57
|
const webPort = typeof port === 'number' ? port : 3456;
|
|
@@ -0,0 +1,47 @@
|
|
|
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/uninstall
|
|
8
|
+
*
|
|
9
|
+
* Accepts JSON body: { removeConfig?: boolean }
|
|
10
|
+
*
|
|
11
|
+
* Always: stops services + removes daemon + npm uninstall -g.
|
|
12
|
+
* Optionally: removes ~/.mindos/ config directory.
|
|
13
|
+
* Knowledge base is NEVER touched from the Web UI.
|
|
14
|
+
*/
|
|
15
|
+
export async function POST(req: Request) {
|
|
16
|
+
try {
|
|
17
|
+
const body = await req.json().catch(() => ({}));
|
|
18
|
+
const removeConfig = body.removeConfig !== false; // default true
|
|
19
|
+
|
|
20
|
+
const cliPath = process.env.MINDOS_CLI_PATH || resolve(process.env.MINDOS_PROJECT_ROOT || process.cwd(), '..', 'bin', 'cli.js');
|
|
21
|
+
const nodeBin = process.env.MINDOS_NODE_BIN || process.execPath;
|
|
22
|
+
|
|
23
|
+
// Build stdin answers for the interactive CLI prompts:
|
|
24
|
+
// 1. "Proceed with uninstall?" → always Y
|
|
25
|
+
// 2. "Remove config directory?" → Y or N based on option
|
|
26
|
+
// 3. "Remove knowledge base?" → always N (never delete KB from Web UI)
|
|
27
|
+
const answers = `Y\n${removeConfig ? 'Y' : 'N'}\nN\n`;
|
|
28
|
+
|
|
29
|
+
const child = spawn(nodeBin, [cliPath, 'uninstall'], {
|
|
30
|
+
detached: true,
|
|
31
|
+
stdio: ['pipe', 'ignore', 'ignore'],
|
|
32
|
+
env: { ...process.env },
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
if (child.stdin) {
|
|
36
|
+
child.stdin.write(answers);
|
|
37
|
+
child.stdin.end();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
child.unref();
|
|
41
|
+
|
|
42
|
+
return NextResponse.json({ ok: true });
|
|
43
|
+
} catch (err) {
|
|
44
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
45
|
+
return NextResponse.json({ error: message }, { status: 500 });
|
|
46
|
+
}
|
|
47
|
+
}
|
package/app/app/globals.css
CHANGED
|
@@ -75,6 +75,7 @@ body {
|
|
|
75
75
|
--ring: var(--amber);
|
|
76
76
|
--radius: 0.5rem;
|
|
77
77
|
--amber: #c8873a;
|
|
78
|
+
--amber-text: #9a6a2b;
|
|
78
79
|
--amber-dim: rgba(200, 135, 58, 0.18);
|
|
79
80
|
--amber-subtle: rgba(200, 135, 30, 0.08);
|
|
80
81
|
--amber-foreground: #131210;
|
|
@@ -110,6 +111,7 @@ body {
|
|
|
110
111
|
--input: rgba(232, 228, 220, 0.1);
|
|
111
112
|
--ring: var(--amber);
|
|
112
113
|
--amber: #d4954a;
|
|
114
|
+
--amber-text: #e0a85e;
|
|
113
115
|
--amber-dim: rgba(212, 149, 74, 0.20);
|
|
114
116
|
--amber-subtle: rgba(212, 149, 74, 0.10);
|
|
115
117
|
--amber-foreground: #131210;
|
|
@@ -304,6 +306,15 @@ body {
|
|
|
304
306
|
background: rgba(0, 0, 0, 0.65);
|
|
305
307
|
}
|
|
306
308
|
|
|
309
|
+
.overlay-backdrop {
|
|
310
|
+
background: rgba(10, 9, 6, 0.35);
|
|
311
|
+
backdrop-filter: blur(2px);
|
|
312
|
+
-webkit-backdrop-filter: blur(2px);
|
|
313
|
+
}
|
|
314
|
+
.dark .overlay-backdrop {
|
|
315
|
+
background: rgba(0, 0, 0, 0.4);
|
|
316
|
+
}
|
|
317
|
+
|
|
307
318
|
/* Micro type scale: text-2xs = 10px (between nothing and text-xs 12px) */
|
|
308
319
|
@layer utilities {
|
|
309
320
|
.text-2xs { font-size: 10px; line-height: 1.4; }
|
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
import { useRef, useCallback, useState, useEffect } from 'react';
|
|
4
4
|
import Link from 'next/link';
|
|
5
|
-
import { FolderTree, Search, Settings, RefreshCw, Bot, Compass, HelpCircle, ChevronLeft, ChevronRight, Radio } from 'lucide-react';
|
|
5
|
+
import { FolderTree, Search, Settings, RefreshCw, Bot, Compass, HelpCircle, ChevronLeft, ChevronRight, Radio, History } from 'lucide-react';
|
|
6
6
|
import { useLocale } from '@/lib/LocaleContext';
|
|
7
7
|
import { DOT_COLORS, getStatusLevel } from './SyncStatusBar';
|
|
8
8
|
import type { SyncStatus } from './settings/SyncTab';
|
|
9
9
|
import Logo from './Logo';
|
|
10
10
|
|
|
11
|
-
export type PanelId = 'files' | 'search' | 'echo' | 'agents' | 'discover';
|
|
11
|
+
export type PanelId = 'files' | 'search' | 'echo' | 'agents' | 'discover' | 'history';
|
|
12
12
|
|
|
13
13
|
export const RAIL_WIDTH_COLLAPSED = 48;
|
|
14
14
|
export const RAIL_WIDTH_EXPANDED = 180;
|
|
@@ -129,9 +129,15 @@ export default function ActivityBar({
|
|
|
129
129
|
setHasUpdate(true);
|
|
130
130
|
} catch { /* silent */ }
|
|
131
131
|
}, 5000);
|
|
132
|
+
const onAvail = () => setHasUpdate(true);
|
|
132
133
|
const onDismiss = () => setHasUpdate(false);
|
|
134
|
+
window.addEventListener('mindos:update-available', onAvail);
|
|
133
135
|
window.addEventListener('mindos:update-dismissed', onDismiss);
|
|
134
|
-
return () => {
|
|
136
|
+
return () => {
|
|
137
|
+
clearTimeout(timer);
|
|
138
|
+
window.removeEventListener('mindos:update-available', onAvail);
|
|
139
|
+
window.removeEventListener('mindos:update-dismissed', onDismiss);
|
|
140
|
+
};
|
|
135
141
|
}, []);
|
|
136
142
|
|
|
137
143
|
/** Debounce rapid clicks (300ms) — shared across all Rail buttons */
|
|
@@ -192,6 +198,7 @@ export default function ActivityBar({
|
|
|
192
198
|
walkthroughId="agents-panel"
|
|
193
199
|
/>
|
|
194
200
|
<RailButton icon={<Compass size={18} />} label={t.sidebar.discover} active={activePanel === 'discover'} expanded={expanded} onClick={() => toggle('discover')} />
|
|
201
|
+
<RailButton icon={<History size={18} />} label={t.sidebar.history} active={activePanel === 'history'} expanded={expanded} onClick={() => toggle('history')} />
|
|
195
202
|
</div>
|
|
196
203
|
|
|
197
204
|
{/* ── Spacer ── */}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { Sparkles } from 'lucide-react';
|
|
4
|
+
import { useLocale } from '@/lib/LocaleContext';
|
|
4
5
|
|
|
5
6
|
interface AskFabProps {
|
|
6
7
|
/** Toggle the right-side Ask AI panel */
|
|
@@ -10,6 +11,9 @@ interface AskFabProps {
|
|
|
10
11
|
}
|
|
11
12
|
|
|
12
13
|
export default function AskFab({ onToggle, askPanelOpen }: AskFabProps) {
|
|
14
|
+
const { t } = useLocale();
|
|
15
|
+
const label = `${t.ask?.fabLabel ?? 'Ask AI'} (⌘/)`;
|
|
16
|
+
|
|
13
17
|
return (
|
|
14
18
|
<button
|
|
15
19
|
onClick={onToggle}
|
|
@@ -29,8 +33,8 @@ export default function AskFab({ onToggle, askPanelOpen }: AskFabProps) {
|
|
|
29
33
|
style={{
|
|
30
34
|
background: 'linear-gradient(135deg, var(--amber), color-mix(in srgb, var(--amber) 80%, white))',
|
|
31
35
|
}}
|
|
32
|
-
title=
|
|
33
|
-
aria-label=
|
|
36
|
+
title={label}
|
|
37
|
+
aria-label={label}
|
|
34
38
|
>
|
|
35
39
|
<Sparkles size={16} className="relative z-10 shrink-0" />
|
|
36
40
|
<span className="
|
|
@@ -40,7 +44,7 @@ export default function AskFab({ onToggle, askPanelOpen }: AskFabProps) {
|
|
|
40
44
|
transition-all duration-200 ease-out
|
|
41
45
|
whitespace-nowrap overflow-hidden
|
|
42
46
|
">
|
|
43
|
-
|
|
47
|
+
{t.ask?.fabLabel ?? 'Ask AI'}
|
|
44
48
|
</span>
|
|
45
49
|
</button>
|
|
46
50
|
);
|
|
@@ -158,7 +158,7 @@ export default function CreateSpaceModal({ t, dirPaths }: { t: ReturnType<typeof
|
|
|
158
158
|
return createPortal(
|
|
159
159
|
<div className="fixed inset-0 z-50 flex items-center justify-center" onKeyDown={handleKeyDown}>
|
|
160
160
|
{/* Backdrop */}
|
|
161
|
-
<div className="absolute inset-0
|
|
161
|
+
<div className="absolute inset-0 overlay-backdrop" onClick={close} />
|
|
162
162
|
{/* Dialog */}
|
|
163
163
|
<div
|
|
164
164
|
role="dialog"
|
|
@@ -76,7 +76,7 @@ function SpacePreviewCard({ icon, title, lines, viewAllHref, viewAllLabel }: {
|
|
|
76
76
|
</div>
|
|
77
77
|
<div className="space-y-1">
|
|
78
78
|
{lines.map((line, i) => (
|
|
79
|
-
<p key={i} className="text-sm text-muted-foreground/80 leading-relaxed">
|
|
79
|
+
<p key={i} className="text-sm text-muted-foreground/80 leading-relaxed" suppressHydrationWarning>
|
|
80
80
|
· {line}
|
|
81
81
|
</p>
|
|
82
82
|
))}
|
|
@@ -301,7 +301,12 @@ function DirectoryNode({ node, depth, currentPath, onNavigate, maxOpenDepth, onI
|
|
|
301
301
|
return;
|
|
302
302
|
}
|
|
303
303
|
if (prevMaxOpenDepth.current !== maxOpenDepth) {
|
|
304
|
-
|
|
304
|
+
const enteringControlled = prevMaxOpenDepth.current === null || prevMaxOpenDepth.current === undefined;
|
|
305
|
+
if (enteringControlled) {
|
|
306
|
+
if (depth > maxOpenDepth) setOpen(false);
|
|
307
|
+
} else {
|
|
308
|
+
setOpen(depth <= maxOpenDepth);
|
|
309
|
+
}
|
|
305
310
|
prevMaxOpenDepth.current = maxOpenDepth;
|
|
306
311
|
}
|
|
307
312
|
}, [maxOpenDepth, depth]);
|
|
@@ -466,29 +471,31 @@ function DirectoryNode({ node, depth, currentPath, onNavigate, maxOpenDepth, onI
|
|
|
466
471
|
</div>
|
|
467
472
|
|
|
468
473
|
<div
|
|
469
|
-
className={`
|
|
470
|
-
style={{
|
|
471
|
-
maxHeight: open ? '9999px' : '0px',
|
|
472
|
-
...(showBorder ? { borderColor: 'color-mix(in srgb, var(--amber) 30%, transparent)' } : {}),
|
|
473
|
-
}}
|
|
474
|
+
className={`grid transition-[grid-template-rows] duration-200 ease-out ${open ? 'grid-rows-[1fr]' : 'grid-rows-[0fr]'}`}
|
|
474
475
|
>
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
476
|
+
<div
|
|
477
|
+
className={`overflow-hidden ${showBorder ? 'border-l-2 ml-[18px]' : ''}`}
|
|
478
|
+
style={showBorder ? { borderColor: 'color-mix(in srgb, var(--amber) 30%, transparent)' } : undefined}
|
|
479
|
+
{...(!open && { inert: true } as React.HTMLAttributes<HTMLDivElement>)}
|
|
480
|
+
>
|
|
481
|
+
{node.children && (
|
|
482
|
+
<FileTree
|
|
483
|
+
nodes={node.children}
|
|
484
|
+
depth={showBorder ? 1 : depth + 1}
|
|
485
|
+
onNavigate={onNavigate}
|
|
486
|
+
maxOpenDepth={maxOpenDepth}
|
|
487
|
+
parentIsSpace={isSpace}
|
|
488
|
+
onImport={onImport}
|
|
489
|
+
/>
|
|
490
|
+
)}
|
|
491
|
+
{showNewFile && (
|
|
492
|
+
<NewFileInline
|
|
493
|
+
dirPath={node.path}
|
|
494
|
+
depth={showBorder ? 0 : depth}
|
|
495
|
+
onDone={() => setShowNewFile(false)}
|
|
496
|
+
/>
|
|
497
|
+
)}
|
|
498
|
+
</div>
|
|
492
499
|
</div>
|
|
493
500
|
|
|
494
501
|
{contextMenu && (isSpace ? (
|
|
@@ -388,7 +388,7 @@ function TaskCard({ icon, title, cta, done, active, dimmed, onClick }: {
|
|
|
388
388
|
{title}
|
|
389
389
|
</span>
|
|
390
390
|
{!done && !dimmed && (
|
|
391
|
-
<span className="text-2xs text-[var(--amber)]">
|
|
391
|
+
<span className="text-2xs text-[var(--amber-text)]">
|
|
392
392
|
{cta} →
|
|
393
393
|
</span>
|
|
394
394
|
)}
|