@geminilight/mindos 0.5.70 → 0.6.1

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 (42) hide show
  1. package/app/app/api/ask/route.ts +124 -92
  2. package/app/app/api/mcp/agents/route.ts +53 -2
  3. package/app/app/api/mcp/status/route.ts +1 -1
  4. package/app/app/api/skills/route.ts +10 -114
  5. package/app/components/ActivityBar.tsx +3 -4
  6. package/app/components/CreateSpaceModal.tsx +31 -6
  7. package/app/components/FileTree.tsx +33 -2
  8. package/app/components/GuideCard.tsx +197 -131
  9. package/app/components/HomeContent.tsx +85 -18
  10. package/app/components/SidebarLayout.tsx +13 -0
  11. package/app/components/SpaceInitToast.tsx +173 -0
  12. package/app/components/agents/AgentDetailContent.tsx +32 -17
  13. package/app/components/agents/AgentsContentPage.tsx +2 -1
  14. package/app/components/agents/AgentsOverviewSection.tsx +1 -14
  15. package/app/components/agents/agents-content-model.ts +16 -8
  16. package/app/components/ask/AskContent.tsx +137 -50
  17. package/app/components/ask/MentionPopover.tsx +16 -8
  18. package/app/components/ask/SlashCommandPopover.tsx +62 -0
  19. package/app/components/settings/KnowledgeTab.tsx +61 -0
  20. package/app/components/walkthrough/steps.ts +11 -6
  21. package/app/hooks/useMention.ts +14 -6
  22. package/app/hooks/useSlashCommand.ts +114 -0
  23. package/app/lib/actions.ts +79 -2
  24. package/app/lib/agent/index.ts +1 -1
  25. package/app/lib/agent/prompt.ts +2 -0
  26. package/app/lib/agent/tools.ts +106 -0
  27. package/app/lib/core/create-space.ts +11 -4
  28. package/app/lib/core/index.ts +1 -1
  29. package/app/lib/i18n-en.ts +51 -46
  30. package/app/lib/i18n-zh.ts +50 -45
  31. package/app/lib/mcp-agents.ts +8 -0
  32. package/app/lib/pdf-extract.ts +33 -0
  33. package/app/lib/pi-integration/extensions.ts +68 -0
  34. package/app/lib/pi-integration/mcporter.ts +219 -0
  35. package/app/lib/pi-integration/session-store.ts +62 -0
  36. package/app/lib/pi-integration/skills.ts +116 -0
  37. package/app/lib/settings.ts +1 -1
  38. package/app/next-env.d.ts +1 -1
  39. package/app/next.config.ts +1 -1
  40. package/app/package.json +2 -0
  41. package/mcp/src/index.ts +29 -0
  42. package/package.json +1 -1
@@ -1,22 +1,30 @@
1
1
  export const dynamic = 'force-dynamic';
2
- import { Agent, type AgentEvent, type BeforeToolCallContext, type BeforeToolCallResult, type AfterToolCallContext, type AfterToolCallResult } from '@mariozechner/pi-agent-core';
2
+ import type { AgentTool, AgentToolResult } from '@mariozechner/pi-agent-core';
3
+ import {
4
+ type AgentSessionEvent as AgentEvent,
5
+ AuthStorage,
6
+ convertToLlm,
7
+ createAgentSession,
8
+ DefaultResourceLoader,
9
+ ModelRegistry,
10
+ type ToolDefinition,
11
+ SessionManager,
12
+ SettingsManager,
13
+ } from '@mariozechner/pi-coding-agent';
3
14
  import { NextRequest, NextResponse } from 'next/server';
4
15
  import fs from 'fs';
5
16
  import path from 'path';
6
17
  import { getFileContent, getMindRoot } from '@/lib/fs';
7
18
  import { getModelConfig } from '@/lib/agent/model';
8
- import { knowledgeBaseTools, WRITE_TOOLS, truncate } from '@/lib/agent/tools';
19
+ import { getRequestScopedTools, WRITE_TOOLS, truncate } from '@/lib/agent/tools';
9
20
  import { AGENT_SYSTEM_PROMPT } from '@/lib/agent/prompt';
10
21
  import { toAgentMessages } from '@/lib/agent/to-agent-messages';
11
- import {
12
- estimateTokens, estimateStringTokens, getContextLimit,
13
- createTransformContext,
14
- } from '@/lib/agent/context';
15
22
  import { logAgentOp } from '@/lib/agent/log';
16
23
  import { readSettings } from '@/lib/settings';
17
24
  import { MindOSError, apiError, ErrorCodes } from '@/lib/errors';
18
25
  import { metrics } from '@/lib/metrics';
19
26
  import { assertNotProtected } from '@/lib/core';
27
+ import { scanExtensionPaths } from '@/lib/pi-integration/extensions';
20
28
  import type { Message as FrontendMessage } from '@/lib/types';
21
29
 
22
30
  // ---------------------------------------------------------------------------
@@ -147,6 +155,64 @@ function dirnameOf(filePath?: string): string | null {
147
155
  return normalized.slice(0, idx);
148
156
  }
149
157
 
158
+ function textToolResult(text: string): AgentToolResult<Record<string, never>> {
159
+ return { content: [{ type: 'text', text }], details: {} };
160
+ }
161
+
162
+ function getProtectedPaths(toolName: string, args: Record<string, unknown>): string[] {
163
+ const pathsToCheck: string[] = [];
164
+ if (toolName === 'batch_create_files' && Array.isArray((args as any).files)) {
165
+ (args as any).files.forEach((f: any) => { if (f.path) pathsToCheck.push(f.path); });
166
+ } else {
167
+ const singlePath = (args as any).path ?? (args as any).from_path;
168
+ if (typeof singlePath === 'string') pathsToCheck.push(singlePath);
169
+ }
170
+ return pathsToCheck;
171
+ }
172
+
173
+ function toPiCustomToolDefinitions(tools: AgentTool<any>[]): ToolDefinition[] {
174
+ return tools.map((tool) => ({
175
+ name: tool.name,
176
+ label: tool.label,
177
+ description: tool.description,
178
+ parameters: tool.parameters as any,
179
+ execute: async (toolCallId, params, signal, onUpdate) => {
180
+ const args = (params ?? {}) as Record<string, unknown>;
181
+
182
+ if (WRITE_TOOLS.has(tool.name)) {
183
+ for (const filePath of getProtectedPaths(tool.name, args)) {
184
+ try {
185
+ assertNotProtected(filePath, 'modified by AI agent');
186
+ } catch (error) {
187
+ const errorMsg = error instanceof Error ? error.message : String(error);
188
+ return textToolResult(`Write-protection error: ${errorMsg}. You CANNOT modify ${filePath} because it is system-protected. Please tell the user you don't have permission to do this.`);
189
+ }
190
+ }
191
+ }
192
+
193
+ const result = await tool.execute(toolCallId, params, signal, onUpdate as any);
194
+ const outputText = result?.content
195
+ ?.filter((p: any) => p.type === 'text')
196
+ .map((p: any) => p.text)
197
+ .join('') ?? '';
198
+
199
+ try {
200
+ logAgentOp({
201
+ ts: new Date().toISOString(),
202
+ tool: tool.name,
203
+ params: args,
204
+ result: outputText.startsWith('Error:') ? 'error' : 'ok',
205
+ message: outputText.slice(0, 200),
206
+ });
207
+ } catch {
208
+ // logging must never kill the stream
209
+ }
210
+
211
+ return result;
212
+ },
213
+ }));
214
+ }
215
+
150
216
  // ---------------------------------------------------------------------------
151
217
  // POST /api/ask
152
218
  // ---------------------------------------------------------------------------
@@ -328,91 +394,61 @@ export async function POST(req: NextRequest) {
328
394
  const historyMessages = agentMessages.slice(0, -1);
329
395
 
330
396
  // Capture API key for this request — safe since each POST creates a new Agent instance.
331
- // Even though JS closures are lexically scoped, being explicit guards against future refactors.
332
397
  const requestApiKey = apiKey;
398
+ const projectRoot = process.env.MINDOS_PROJECT_ROOT || path.resolve(process.cwd(), '..');
399
+ const requestTools = await getRequestScopedTools();
400
+ const customTools = toPiCustomToolDefinitions(requestTools);
401
+
402
+ const authStorage = AuthStorage.create();
403
+ authStorage.setRuntimeApiKey(provider, requestApiKey);
404
+ const modelRegistry = new ModelRegistry(authStorage);
405
+ const settingsManager = SettingsManager.inMemory({
406
+ enableSkillCommands: true,
407
+ ...(enableThinking && provider === 'anthropic' ? { thinkingBudgets: { medium: thinkingBudget } } : {}),
408
+ ...(contextStrategy === 'off' ? { compaction: { enabled: false } } : {}),
409
+ });
333
410
 
334
- // ── Loop detection state ──
335
- const stepHistory: Array<{ tool: string; input: string }> = [];
336
- let stepCount = 0;
337
- let loopCooldown = 0;
338
-
339
- // ── Create Agent (per-request lifecycle) ──
340
- const agent = new Agent({
341
- initialState: {
342
- systemPrompt,
343
- model,
344
- thinkingLevel: (enableThinking && provider === 'anthropic') ? 'medium' : 'off',
345
- tools: knowledgeBaseTools,
346
- messages: historyMessages,
347
- },
348
- getApiKey: async () => requestApiKey,
349
- toolExecution: 'parallel',
350
-
351
- // Context management: truncate compact prune
352
- transformContext: createTransformContext(
353
- systemPrompt,
354
- modelName,
355
- () => model,
356
- apiKey,
357
- contextStrategy,
358
- ),
359
-
360
- // Write-protection: block writes to protected files gracefully
361
- beforeToolCall: async (context: BeforeToolCallContext): Promise<BeforeToolCallResult | undefined> => {
362
- const { toolCall, args } = context;
363
- // toolCall is an object with type "toolCall" and contains the tool name and ID
364
- const toolName = (toolCall as any).toolName ?? (toolCall as any).name;
365
- if (toolName && WRITE_TOOLS.has(toolName)) {
366
- // Special handling for batch creations where we need to check multiple files
367
- const pathsToCheck: string[] = [];
368
- if (toolName === 'batch_create_files' && Array.isArray((args as any).files)) {
369
- (args as any).files.forEach((f: any) => { if (f.path) pathsToCheck.push(f.path); });
370
- } else {
371
- const singlePath = (args as any).path ?? (args as any).from_path;
372
- if (singlePath) pathsToCheck.push(singlePath);
373
- }
411
+ const resourceLoader = new DefaultResourceLoader({
412
+ cwd: projectRoot,
413
+ settingsManager,
414
+ systemPromptOverride: () => systemPrompt,
415
+ appendSystemPromptOverride: () => [],
416
+ additionalSkillPaths: [
417
+ path.join(projectRoot, 'app', 'data', 'skills'),
418
+ path.join(projectRoot, 'skills'),
419
+ path.join(getMindRoot(), '.skills'),
420
+ ],
421
+ additionalExtensionPaths: scanExtensionPaths(),
422
+ });
423
+ await resourceLoader.reload();
424
+
425
+ const { session } = await createAgentSession({
426
+ cwd: projectRoot,
427
+ model,
428
+ thinkingLevel: (enableThinking && provider === 'anthropic') ? 'medium' : 'off',
429
+ authStorage,
430
+ modelRegistry,
431
+ resourceLoader,
432
+ sessionManager: SessionManager.inMemory(),
433
+ settingsManager,
434
+ tools: [],
435
+ customTools,
436
+ });
374
437
 
375
- for (const filePath of pathsToCheck) {
376
- try {
377
- assertNotProtected(filePath, 'modified by AI agent');
378
- } catch (e) {
379
- const errorMsg = e instanceof Error ? e.message : String(e);
380
- return {
381
- block: true,
382
- reason: `Write-protection error: ${errorMsg}. You CANNOT modify ${filePath} because it is system-protected. Please tell the user you don't have permission to do this.`,
383
- };
384
- }
385
- }
438
+ const llmHistoryMessages = convertToLlm(historyMessages);
439
+ await session.newSession({
440
+ setup: async (sessionManager) => {
441
+ for (const message of llmHistoryMessages) {
442
+ sessionManager.appendMessage(message);
386
443
  }
387
- return undefined;
388
- },
389
-
390
- // Logging: record all tool executions
391
- afterToolCall: async (context: AfterToolCallContext): Promise<AfterToolCallResult | undefined> => {
392
- const ts = new Date().toISOString();
393
- const { toolCall, args, result, isError } = context;
394
- const toolName = (toolCall as any).toolName ?? (toolCall as any).name;
395
- const outputText = result?.content
396
- ?.filter((p: any) => p.type === 'text')
397
- .map((p: any) => p.text)
398
- .join('') ?? '';
399
- try {
400
- logAgentOp({
401
- ts,
402
- tool: toolName ?? 'unknown',
403
- params: args as Record<string, unknown>,
404
- result: isError ? 'error' : 'ok',
405
- message: outputText.slice(0, 200),
406
- });
407
- } catch { /* logging must never kill the stream */ }
408
- return undefined;
409
444
  },
410
-
411
- ...(enableThinking && provider === 'anthropic' ? {
412
- thinkingBudgets: { medium: thinkingBudget },
413
- } : {}),
414
445
  });
415
446
 
447
+ // ── Loop detection state ──
448
+ const stepHistory: Array<{ tool: string; input: string }> = [];
449
+ let stepCount = 0;
450
+ let loopCooldown = 0;
451
+
416
452
  // ── SSE Stream ──
417
453
  const encoder = new TextEncoder();
418
454
  const requestStartTime = Date.now();
@@ -424,7 +460,7 @@ export async function POST(req: NextRequest) {
424
460
  } catch { /* controller may be closed */ }
425
461
  }
426
462
 
427
- agent.subscribe((event: AgentEvent) => {
463
+ session.subscribe((event: AgentEvent) => {
428
464
  if (isTextDeltaEvent(event)) {
429
465
  send({ type: 'text_delta', delta: getTextDelta(event) });
430
466
  } else if (isThinkingDeltaEvent(event)) {
@@ -476,24 +512,20 @@ export async function POST(req: NextRequest) {
476
512
  if (lastN.every(s => s.tool === lastN[0].tool && s.input === lastN[0].input)) {
477
513
  loopCooldown = 3;
478
514
  // TODO (metrics): Track loop detection rate — metrics.increment('agent.loop_detected', { model: modelName })
479
- agent.steer({
480
- role: 'user',
481
- content: '[SYSTEM WARNING] You have called the same tool with identical arguments 3 times in a row. This appears to be a loop. Try a completely different approach or ask the user for clarification.',
482
- timestamp: Date.now(),
483
- } as any);
515
+ void session.steer('[SYSTEM WARNING] You have called the same tool with identical arguments 3 times in a row. This appears to be a loop. Try a completely different approach or ask the user for clarification.');
484
516
  }
485
517
  }
486
518
 
487
519
  // Step limit enforcement
488
520
  if (stepCount >= stepLimit) {
489
- agent.abort();
521
+ void session.abort();
490
522
  }
491
523
 
492
524
  console.log(`[ask] Step ${stepCount}/${stepLimit}`);
493
525
  }
494
526
  });
495
527
 
496
- agent.prompt(lastUserContent).then(() => {
528
+ session.prompt(lastUserContent).then(() => {
497
529
  metrics.recordRequest(Date.now() - requestStartTime);
498
530
  send({ type: 'done' });
499
531
  controller.close();
@@ -1,4 +1,7 @@
1
1
  export const dynamic = 'force-dynamic';
2
+ import os from 'os';
3
+ import fs from 'fs';
4
+ import path from 'path';
2
5
  import { NextResponse } from 'next/server';
3
6
  import {
4
7
  MCP_AGENTS,
@@ -9,6 +12,50 @@ import {
9
12
  detectAgentInstalledSkills,
10
13
  resolveSkillWorkspaceProfile,
11
14
  } from '@/lib/mcp-agents';
15
+ import { readSettings } from '@/lib/settings';
16
+ import { scanSkillDirs } from '@/lib/pi-integration/skills';
17
+ import { getMindRoot } from '@/lib/fs';
18
+
19
+ function enrichMindOsAgent(agent: Record<string, unknown>) {
20
+ agent.present = true;
21
+ agent.installed = true;
22
+ agent.scope = 'builtin';
23
+
24
+ try {
25
+ const settings = readSettings();
26
+ const port = Number(process.env.MINDOS_MCP_PORT) || settings.mcpPort || 8781;
27
+ agent.transport = `http :${port}`;
28
+ } catch {
29
+ agent.transport = 'http :8781';
30
+ }
31
+
32
+ try {
33
+ const projectRoot = process.env.MINDOS_PROJECT_ROOT || path.resolve(process.cwd(), '..');
34
+ const skills = scanSkillDirs({ projectRoot, mindRoot: getMindRoot() });
35
+ const enabledSkills = skills.filter(s => s.enabled);
36
+ agent.installedSkillNames = enabledSkills.map(s => s.name);
37
+ agent.installedSkillCount = enabledSkills.length;
38
+ agent.installedSkillSourcePath = path.join(projectRoot, 'skills');
39
+ agent.skillMode = 'universal';
40
+ agent.skillWorkspacePath = path.join(os.homedir(), '.agents', 'skills');
41
+ } catch { /* skill scan unavailable */ }
42
+
43
+ const mcpConfigPath = path.join(os.homedir(), '.mindos', 'mcp.json');
44
+ try {
45
+ if (fs.existsSync(mcpConfigPath)) {
46
+ const raw = JSON.parse(fs.readFileSync(mcpConfigPath, 'utf-8'));
47
+ const servers = Object.keys(raw.mcpServers ?? {});
48
+ agent.configuredMcpServers = servers;
49
+ agent.configuredMcpServerCount = servers.length;
50
+ agent.configuredMcpSources = servers.length > 0 ? [`local:${mcpConfigPath}`] : [];
51
+ }
52
+ } catch { /* ignore */ }
53
+
54
+ agent.runtimeConversationSignal = true;
55
+ agent.runtimeLastActivityAt = new Date().toISOString();
56
+ agent.hiddenRootPath = path.join(os.homedir(), '.mindos');
57
+ agent.hiddenRootPresent = true;
58
+ }
12
59
 
13
60
  export async function GET() {
14
61
  try {
@@ -30,7 +77,6 @@ export async function GET() {
30
77
  hasProjectScope: !!agent.project,
31
78
  hasGlobalScope: !!agent.global,
32
79
  preferredTransport: agent.preferredTransport,
33
- // Snippet generation fields
34
80
  format: agent.format ?? 'json',
35
81
  configKey: agent.key,
36
82
  globalNestedKey: agent.globalNestedKey,
@@ -53,8 +99,13 @@ export async function GET() {
53
99
  };
54
100
  });
55
101
 
56
- // Sort: installed first, then detected, then not found
102
+ const mindos = agents.find(a => a.key === 'mindos');
103
+ if (mindos) enrichMindOsAgent(mindos as unknown as Record<string, unknown>);
104
+
105
+ // Sort: mindos first, then installed, then detected, then not found
57
106
  agents.sort((a, b) => {
107
+ if (a.key === 'mindos') return -1;
108
+ if (b.key === 'mindos') return 1;
58
109
  const rank = (x: typeof a) => x.installed ? 0 : x.present ? 1 : 2;
59
110
  return rank(a) - rank(b);
60
111
  });
@@ -50,7 +50,7 @@ export async function GET(req: NextRequest) {
50
50
  transport: 'http',
51
51
  endpoint,
52
52
  port,
53
- toolCount: running ? 23 : 0,
53
+ toolCount: running ? 24 : 0,
54
54
  authConfigured,
55
55
  // Masked for display; full token only used server-side in snippet generation
56
56
  maskedToken: authConfigured ? maskToken(token) : undefined,
@@ -4,6 +4,7 @@ import fs from 'fs';
4
4
  import path from 'path';
5
5
  import os from 'os';
6
6
  import { readSettings, writeSettings } from '@/lib/settings';
7
+ import { parseSkillMd, readSkillContentByName, scanSkillDirs } from '@/lib/pi-integration/skills';
7
8
 
8
9
  const PROJECT_ROOT = process.env.MINDOS_PROJECT_ROOT || path.resolve(process.cwd(), '..');
9
10
 
@@ -12,113 +13,15 @@ function getMindRoot(): string {
12
13
  return s.mindRoot || process.env.MIND_ROOT || path.join(os.homedir(), 'MindOS', 'mind');
13
14
  }
14
15
 
15
- interface SkillInfo {
16
- name: string;
17
- description: string;
18
- path: string;
19
- source: 'builtin' | 'user';
20
- enabled: boolean;
21
- editable: boolean;
22
- }
23
-
24
- function parseSkillMd(content: string): { name: string; description: string } {
25
- const match = content.match(/^---\n([\s\S]*?)\n---/);
26
- if (!match) return { name: '', description: '' };
27
- const yaml = match[1];
28
- const nameMatch = yaml.match(/^name:\s*(.+)/m);
29
- const descMatch = yaml.match(/^description:\s*>?\s*\n?([\s\S]*?)(?=\n\w|\n---)/m);
30
- const name = nameMatch ? nameMatch[1].trim() : '';
31
- let description = '';
32
- if (descMatch) {
33
- description = descMatch[1].trim().split('\n').map(l => l.trim()).join(' ').slice(0, 200);
34
- } else {
35
- const simpleDesc = yaml.match(/^description:\s*(.+)/m);
36
- if (simpleDesc) description = simpleDesc[1].trim().slice(0, 200);
37
- }
38
- return { name, description };
39
- }
40
-
41
- function scanSkillDirs(disabledSkills: string[]): SkillInfo[] {
42
- const skills: SkillInfo[] = [];
43
- const seen = new Set<string>();
44
-
45
- // 1. app/data/skills/ — builtin
46
- const builtinDir = path.join(PROJECT_ROOT, 'app', 'data', 'skills');
47
- if (fs.existsSync(builtinDir)) {
48
- for (const entry of fs.readdirSync(builtinDir, { withFileTypes: true })) {
49
- if (!entry.isDirectory()) continue;
50
- const skillFile = path.join(builtinDir, entry.name, 'SKILL.md');
51
- if (!fs.existsSync(skillFile)) continue;
52
- const content = fs.readFileSync(skillFile, 'utf-8');
53
- const { name, description } = parseSkillMd(content);
54
- const skillName = name || entry.name;
55
- seen.add(skillName);
56
- skills.push({
57
- name: skillName,
58
- description,
59
- path: `app/data/skills/${entry.name}/SKILL.md`,
60
- source: 'builtin',
61
- enabled: !disabledSkills.includes(skillName),
62
- editable: false,
63
- });
64
- }
65
- }
66
-
67
- // 2. skills/ — project root builtin
68
- const skillsDir = path.join(PROJECT_ROOT, 'skills');
69
- if (fs.existsSync(skillsDir)) {
70
- for (const entry of fs.readdirSync(skillsDir, { withFileTypes: true })) {
71
- if (!entry.isDirectory()) continue;
72
- const skillFile = path.join(skillsDir, entry.name, 'SKILL.md');
73
- if (!fs.existsSync(skillFile)) continue;
74
- const content = fs.readFileSync(skillFile, 'utf-8');
75
- const { name, description } = parseSkillMd(content);
76
- const skillName = name || entry.name;
77
- if (seen.has(skillName)) continue; // already listed from app/data/skills/
78
- seen.add(skillName);
79
- skills.push({
80
- name: skillName,
81
- description,
82
- path: `skills/${entry.name}/SKILL.md`,
83
- source: 'builtin',
84
- enabled: !disabledSkills.includes(skillName),
85
- editable: false,
86
- });
87
- }
88
- }
89
-
90
- // 3. {mindRoot}/.skills/ — user custom
91
- const mindRoot = getMindRoot();
92
- const userSkillsDir = path.join(mindRoot, '.skills');
93
- if (fs.existsSync(userSkillsDir)) {
94
- for (const entry of fs.readdirSync(userSkillsDir, { withFileTypes: true })) {
95
- if (!entry.isDirectory()) continue;
96
- const skillFile = path.join(userSkillsDir, entry.name, 'SKILL.md');
97
- if (!fs.existsSync(skillFile)) continue;
98
- const content = fs.readFileSync(skillFile, 'utf-8');
99
- const { name, description } = parseSkillMd(content);
100
- const skillName = name || entry.name;
101
- if (seen.has(skillName)) continue;
102
- seen.add(skillName);
103
- skills.push({
104
- name: skillName,
105
- description,
106
- path: `{mindRoot}/.skills/${entry.name}/SKILL.md`,
107
- source: 'user',
108
- enabled: !disabledSkills.includes(skillName),
109
- editable: true,
110
- });
111
- }
112
- }
113
-
114
- return skills;
115
- }
116
-
117
16
  export async function GET() {
118
17
  try {
119
18
  const settings = readSettings();
120
19
  const disabledSkills = settings.disabledSkills ?? [];
121
- const skills = scanSkillDirs(disabledSkills);
20
+ const skills = scanSkillDirs({
21
+ projectRoot: PROJECT_ROOT,
22
+ mindRoot: getMindRoot(),
23
+ disabledSkills,
24
+ });
122
25
  return NextResponse.json({ skills });
123
26
  } catch (err) {
124
27
  return NextResponse.json({ error: String(err) }, { status: 500 });
@@ -205,18 +108,11 @@ export async function POST(req: NextRequest) {
205
108
 
206
109
  case 'read': {
207
110
  if (!name) return NextResponse.json({ error: 'name required' }, { status: 400 });
208
- const dirs = [
209
- path.join(PROJECT_ROOT, 'app', 'data', 'skills', name),
210
- path.join(PROJECT_ROOT, 'skills', name),
211
- path.join(userSkillsDir, name),
212
- ];
213
- for (const dir of dirs) {
214
- const file = path.join(dir, 'SKILL.md');
215
- if (fs.existsSync(file)) {
216
- return NextResponse.json({ content: fs.readFileSync(file, 'utf-8') });
217
- }
111
+ const content = readSkillContentByName(name, { projectRoot: PROJECT_ROOT, mindRoot });
112
+ if (!content) {
113
+ return NextResponse.json({ error: 'Skill not found' }, { status: 404 });
218
114
  }
219
- return NextResponse.json({ error: 'Skill not found' }, { status: 404 });
115
+ return NextResponse.json({ content });
220
116
  }
221
117
 
222
118
  case 'read-native': {
@@ -162,7 +162,6 @@ export default function ActivityBar({
162
162
  role="toolbar"
163
163
  aria-label="Navigation"
164
164
  aria-orientation="vertical"
165
- data-walkthrough="activity-bar"
166
165
  >
167
166
  {/* Content wrapper — overflow-hidden prevents text flash during width transitions */}
168
167
  <div className="flex flex-col h-full w-full overflow-hidden">
@@ -181,14 +180,15 @@ export default function ActivityBar({
181
180
  {/* ── Middle: Core panel toggles ── */}
182
181
  <div className={`flex flex-col ${expanded ? 'px-1.5' : 'items-center'} gap-1 py-2`}>
183
182
  <RailButton icon={<FolderTree size={18} />} label={t.sidebar.files} active={activePanel === 'files'} expanded={expanded} onClick={() => toggle('files')} walkthroughId="files-panel" />
184
- <RailButton icon={<Search size={18} />} label={t.sidebar.searchTitle} shortcut="⌘K" active={activePanel === 'search'} expanded={expanded} onClick={() => toggle('search')} walkthroughId="search-button" />
185
- <RailButton icon={<Radio size={18} />} label={t.sidebar.echo} active={activePanel === 'echo'} expanded={expanded} onClick={() => toggle('echo')} />
183
+ <RailButton icon={<Search size={18} />} label={t.sidebar.searchTitle} shortcut="⌘K" active={activePanel === 'search'} expanded={expanded} onClick={() => toggle('search')} />
184
+ <RailButton icon={<Radio size={18} />} label={t.sidebar.echo} active={activePanel === 'echo'} expanded={expanded} onClick={() => toggle('echo')} walkthroughId="echo-panel" />
186
185
  <RailButton
187
186
  icon={<Bot size={18} />}
188
187
  label={t.sidebar.agents}
189
188
  active={activePanel === 'agents'}
190
189
  expanded={expanded}
191
190
  onClick={() => onAgentsClick ? debounced(onAgentsClick) : toggle('agents')}
191
+ walkthroughId="agents-panel"
192
192
  />
193
193
  <RailButton icon={<Compass size={18} />} label={t.sidebar.discover} active={activePanel === 'discover'} expanded={expanded} onClick={() => toggle('discover')} />
194
194
  </div>
@@ -211,7 +211,6 @@ export default function ActivityBar({
211
211
  shortcut="⌘,"
212
212
  expanded={expanded}
213
213
  onClick={() => debounced(onSettingsClick)}
214
- walkthroughId="settings-button"
215
214
  badge={hasUpdate ? (
216
215
  <span className={`absolute ${expanded ? 'left-[26px] top-1.5' : 'top-1.5 right-1.5'} w-2 h-2 rounded-full bg-error`} />
217
216
  ) : undefined}
@@ -93,17 +93,42 @@ export default function CreateSpaceModal({ t, dirPaths }: { t: ReturnType<typeof
93
93
  if (useAi && aiAvailable) {
94
94
  const isZh = document.documentElement.lang === 'zh';
95
95
  const prompt = isZh
96
- ? `初始化新建的心智空间「${trimmed}」,路径为「${createdPath}/」。${description ? `描述:「${description}」。` : ''}请根据空间名称生成有意义的内容:\n1. README.md — 空间用途、结构概览、使用指南\n2. INSTRUCTION.md AI Agent 在此空间的行为规则\n\n使用可用工具直接写入文件,内容简洁实用。`
97
- : `Initialize this new Mind Space "${trimmed}" at "${createdPath}/". ${description ? `Description: "${description}". ` : ''}Generate meaningful content for:\n1. README.md — purpose, structure overview, usage guidelines\n2. INSTRUCTION.md — rules for AI agents operating in this space\n\nWrite directly to the files using available tools. Keep content concise and actionable.`;
98
- // Fire and forget — don't block navigation
99
- apiFetch('/api/ask', {
96
+ ? `初始化新建的心智空间「${trimmed}」,路径为「${createdPath}/」。${description ? `描述:「${description}」。` : ''}两个文件均已存在模板,用 write_file 覆盖:\n1. 「${createdPath}/README.md」— 写入空间用途、结构概览、使用指南\n2. 「${createdPath}/INSTRUCTION.md」— 写入 AI Agent 在此空间中的行为规则和操作约定\n\n内容简洁实用,直接使用工具写入。`
97
+ : `Initialize the new Mind Space "${trimmed}" at "${createdPath}/". ${description ? `Description: "${description}". ` : ''}Both files already exist with templates — use write_file to overwrite:\n1. "${createdPath}/README.md"write purpose, structure overview, usage guidelines\n2. "${createdPath}/INSTRUCTION.md"write rules for AI agents operating in this space\n\nKeep content concise and actionable. Write files directly using tools.`;
98
+
99
+ window.dispatchEvent(new CustomEvent('mindos:ai-init', {
100
+ detail: { spaceName: trimmed, spacePath: createdPath, description, state: 'working' },
101
+ }));
102
+
103
+ // /api/ask returns SSE — use raw fetch and consume the stream
104
+ // so the server-side agent runs to completion.
105
+ fetch('/api/ask', {
100
106
  method: 'POST',
101
107
  headers: { 'Content-Type': 'application/json' },
102
108
  body: JSON.stringify({
103
109
  messages: [{ role: 'user', content: prompt }],
104
- targetDir: createdPath,
110
+ currentFile: createdPath + '/INSTRUCTION.md',
105
111
  }),
106
- }).catch(() => { /* AI init is best-effort */ });
112
+ }).then(async (res) => {
113
+ if (!res.ok || !res.body) throw new Error(`HTTP ${res.status}`);
114
+ const reader = res.body.getReader();
115
+ try {
116
+ while (true) {
117
+ const { done } = await reader.read();
118
+ if (done) break;
119
+ }
120
+ } finally {
121
+ reader.releaseLock();
122
+ }
123
+ window.dispatchEvent(new CustomEvent('mindos:ai-init', {
124
+ detail: { spacePath: createdPath, state: 'done' },
125
+ }));
126
+ window.dispatchEvent(new Event('mindos:files-changed'));
127
+ }).catch(() => {
128
+ window.dispatchEvent(new CustomEvent('mindos:ai-init', {
129
+ detail: { spacePath: createdPath, state: 'error' },
130
+ }));
131
+ });
107
132
  }
108
133
 
109
134
  close();
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
 
3
- import { useState, useCallback, useRef, useTransition, useEffect } from 'react';
3
+ import { useState, useCallback, useRef, useTransition, useEffect, useSyncExternalStore } from 'react';
4
4
  import { useRouter, usePathname } from 'next/navigation';
5
5
  import { FileNode } from '@/lib/types';
6
6
  import { encodePath } from '@/lib/utils';
@@ -17,6 +17,33 @@ function notifyFilesChanged() {
17
17
 
18
18
  const SYSTEM_FILES = new Set(['INSTRUCTION.md', 'README.md']);
19
19
 
20
+ const HIDDEN_FILES_KEY = 'show-hidden-files';
21
+
22
+ function subscribeHiddenFiles(cb: () => void) {
23
+ const handler = (e: StorageEvent) => { if (e.key === HIDDEN_FILES_KEY) cb(); };
24
+ const custom = () => cb();
25
+ window.addEventListener('storage', handler);
26
+ window.addEventListener('mindos:hidden-files-changed', custom);
27
+ return () => {
28
+ window.removeEventListener('storage', handler);
29
+ window.removeEventListener('mindos:hidden-files-changed', custom);
30
+ };
31
+ }
32
+
33
+ function getShowHiddenFiles() {
34
+ if (typeof window === 'undefined') return false;
35
+ return localStorage.getItem(HIDDEN_FILES_KEY) === 'true';
36
+ }
37
+
38
+ export function setShowHiddenFiles(value: boolean) {
39
+ localStorage.setItem(HIDDEN_FILES_KEY, String(value));
40
+ window.dispatchEvent(new Event('mindos:hidden-files-changed'));
41
+ }
42
+
43
+ function useShowHiddenFiles() {
44
+ return useSyncExternalStore(subscribeHiddenFiles, getShowHiddenFiles, () => false);
45
+ }
46
+
20
47
  interface FileTreeProps {
21
48
  nodes: FileNode[];
22
49
  depth?: number;
@@ -618,9 +645,13 @@ function FileNodeItem({ node, depth, currentPath, onNavigate }: {
618
645
  export default function FileTree({ nodes, depth = 0, onNavigate, maxOpenDepth, parentIsSpace, onImport }: FileTreeProps) {
619
646
  const pathname = usePathname();
620
647
  const currentPath = getCurrentFilePath(pathname);
648
+ const showHidden = useShowHiddenFiles();
621
649
 
622
650
  const isInsideDir = depth > 0;
623
- const visibleNodes = isInsideDir ? filterVisibleNodes(nodes, !!parentIsSpace) : nodes;
651
+ let visibleNodes = isInsideDir ? filterVisibleNodes(nodes, !!parentIsSpace) : nodes;
652
+ if (!isInsideDir && !showHidden) {
653
+ visibleNodes = visibleNodes.filter(n => !n.name.startsWith('.'));
654
+ }
624
655
 
625
656
  useEffect(() => {
626
657
  if (!currentPath || depth !== 0) return;