@geminilight/mindos 0.5.63 → 0.5.65

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 (104) hide show
  1. package/README.md +4 -0
  2. package/README_zh.md +4 -0
  3. package/app/app/api/ask/route.ts +12 -0
  4. package/app/app/api/changes/route.ts +7 -1
  5. package/app/app/api/file/route.ts +9 -0
  6. package/app/app/api/mcp/agents/route.ts +27 -1
  7. package/app/app/api/mcp/install-skill/route.ts +9 -24
  8. package/app/app/api/skills/route.ts +18 -2
  9. package/app/app/api/tree-version/route.ts +8 -0
  10. package/app/app/layout.tsx +1 -0
  11. package/app/app/page.tsx +1 -2
  12. package/app/app/view/[...path]/ViewPageClient.tsx +0 -1
  13. package/app/components/ActivityBar.tsx +2 -2
  14. package/app/components/Backlinks.tsx +5 -5
  15. package/app/components/CreateSpaceModal.tsx +3 -2
  16. package/app/components/DirPicker.tsx +1 -1
  17. package/app/components/DirView.tsx +2 -3
  18. package/app/components/EditorWrapper.tsx +3 -3
  19. package/app/components/FileTree.tsx +25 -10
  20. package/app/components/GuideCard.tsx +4 -4
  21. package/app/components/HomeContent.tsx +44 -14
  22. package/app/components/MarkdownView.tsx +2 -2
  23. package/app/components/OnboardingView.tsx +1 -1
  24. package/app/components/Panel.tsx +1 -1
  25. package/app/components/RightAgentDetailPanel.tsx +2 -1
  26. package/app/components/RightAskPanel.tsx +1 -1
  27. package/app/components/SearchModal.tsx +10 -2
  28. package/app/components/SidebarLayout.tsx +36 -10
  29. package/app/components/ThemeToggle.tsx +1 -1
  30. package/app/components/agents/AgentDetailContent.tsx +454 -59
  31. package/app/components/agents/AgentsContentPage.tsx +89 -20
  32. package/app/components/agents/AgentsMcpSection.tsx +513 -85
  33. package/app/components/agents/AgentsOverviewSection.tsx +418 -59
  34. package/app/components/agents/AgentsPrimitives.tsx +335 -0
  35. package/app/components/agents/AgentsSkillsSection.tsx +746 -105
  36. package/app/components/agents/SkillDetailPopover.tsx +416 -0
  37. package/app/components/agents/agents-content-model.ts +308 -10
  38. package/app/components/ask/AskContent.tsx +34 -5
  39. package/app/components/ask/FileChip.tsx +1 -0
  40. package/app/components/ask/MentionPopover.tsx +13 -1
  41. package/app/components/ask/MessageList.tsx +5 -7
  42. package/app/components/ask/ToolCallBlock.tsx +4 -4
  43. package/app/components/changes/ChangesBanner.tsx +89 -13
  44. package/app/components/changes/ChangesContentPage.tsx +134 -51
  45. package/app/components/echo/EchoHero.tsx +10 -24
  46. package/app/components/echo/EchoInsightCollapsible.tsx +52 -43
  47. package/app/components/echo/EchoPageSections.tsx +13 -9
  48. package/app/components/echo/EchoSegmentNav.tsx +14 -11
  49. package/app/components/echo/EchoSegmentPageClient.tsx +64 -43
  50. package/app/components/explore/ExploreContent.tsx +3 -7
  51. package/app/components/explore/UseCaseCard.tsx +4 -15
  52. package/app/components/panels/AgentsPanel.tsx +22 -128
  53. package/app/components/panels/AgentsPanelAgentDetail.tsx +7 -6
  54. package/app/components/panels/AgentsPanelAgentGroups.tsx +8 -13
  55. package/app/components/panels/AgentsPanelAgentListRow.tsx +39 -16
  56. package/app/components/panels/AgentsPanelHubNav.tsx +12 -12
  57. package/app/components/panels/EchoPanel.tsx +8 -10
  58. package/app/components/panels/PanelNavRow.tsx +9 -2
  59. package/app/components/panels/PluginsPanel.tsx +5 -5
  60. package/app/components/renderers/agent-inspector/AgentInspectorRenderer.tsx +30 -8
  61. package/app/components/renderers/agent-inspector/manifest.ts +5 -3
  62. package/app/components/renderers/config/manifest.ts +1 -0
  63. package/app/components/renderers/csv/manifest.ts +1 -0
  64. package/app/components/renderers/todo/manifest.ts +1 -0
  65. package/app/components/settings/AiTab.tsx +3 -3
  66. package/app/components/settings/AppearanceTab.tsx +2 -2
  67. package/app/components/settings/KnowledgeTab.tsx +3 -3
  68. package/app/components/settings/McpAgentInstall.tsx +3 -6
  69. package/app/components/settings/McpSkillCreateForm.tsx +2 -3
  70. package/app/components/settings/McpSkillRow.tsx +2 -3
  71. package/app/components/settings/McpSkillsSection.tsx +2 -2
  72. package/app/components/settings/McpTab.tsx +12 -13
  73. package/app/components/settings/MonitoringTab.tsx +13 -13
  74. package/app/components/settings/PluginsTab.tsx +6 -5
  75. package/app/components/settings/Primitives.tsx +3 -4
  76. package/app/components/settings/SettingsContent.tsx +3 -3
  77. package/app/components/settings/SyncTab.tsx +11 -17
  78. package/app/components/settings/UpdateTab.tsx +18 -21
  79. package/app/components/settings/types.ts +14 -0
  80. package/app/components/setup/StepKB.tsx +1 -1
  81. package/app/hooks/useMcpData.tsx +7 -4
  82. package/app/hooks/useMention.ts +25 -8
  83. package/app/lib/agent/log.ts +15 -18
  84. package/app/lib/agent/stream-consumer.ts +3 -0
  85. package/app/lib/agent/to-agent-messages.ts +6 -4
  86. package/app/lib/core/agent-audit-log.ts +280 -0
  87. package/app/lib/core/content-changes.ts +148 -8
  88. package/app/lib/core/index.ts +11 -0
  89. package/app/lib/fs.ts +16 -1
  90. package/app/lib/i18n-en.ts +317 -36
  91. package/app/lib/i18n-zh.ts +316 -35
  92. package/app/lib/mcp-agents.ts +273 -2
  93. package/app/lib/renderers/index.ts +1 -2
  94. package/app/lib/renderers/registry.ts +10 -0
  95. package/app/lib/types.ts +2 -0
  96. package/app/next-env.d.ts +1 -1
  97. package/bin/lib/mcp-agents.js +38 -13
  98. package/package.json +1 -1
  99. package/scripts/migrate-agent-audit-log.js +170 -0
  100. package/scripts/migrate-agent-diff.js +146 -0
  101. package/scripts/setup.js +12 -17
  102. package/skills/plugin-core-builtin-migration/SKILL.md +178 -0
  103. package/app/components/renderers/diff/DiffRenderer.tsx +0 -311
  104. package/app/components/renderers/diff/manifest.ts +0 -14
package/README.md CHANGED
@@ -276,6 +276,10 @@ Join our WeChat group for early access, feedback, and AI workflow discussions:
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
278
 
279
+ ### 🙏 Acknowledgements
280
+
281
+ This project has been published on the [LINUX DO community](https://linux.do), and we deeply appreciate the community's support and feedback.
282
+
279
283
  ---
280
284
 
281
285
  ## 📄 License
package/README_zh.md CHANGED
@@ -276,6 +276,10 @@ MindOS/
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
278
 
279
+ ### 🙏 致谢
280
+
281
+ 本项目已在 [LINUX DO 社区](https://linux.do) 发布,感谢社区的支持与反馈。
282
+
279
283
  ---
280
284
 
281
285
  ## 📄 License
@@ -278,7 +278,19 @@ export async function POST(req: NextRequest) {
278
278
  }
279
279
  }
280
280
 
281
+ // Generate current time for the agent's context
282
+ const now = new Date();
283
+ const timeContext = `## Current Time Context
284
+ - Current UTC Time: ${now.toISOString()}
285
+ - System Local Time: ${new Intl.DateTimeFormat('en-US', { dateStyle: 'full', timeStyle: 'long' }).format(now)}
286
+ - Unix Timestamp: ${Math.floor(now.getTime() / 1000)}
287
+
288
+ *Note: The times listed above represent "NOW". The user may have sent messages hours or days ago in this same conversation thread. Each user message in the history contains its own specific timestamp which you should refer to when understanding historical context.*`;
289
+
281
290
  const promptParts: string[] = [AGENT_SYSTEM_PROMPT];
291
+
292
+ promptParts.push(`---\n\n${timeContext}`);
293
+
282
294
  promptParts.push(`---\n\nInitialization status (auto-loaded at request start):\n\n${initStatus}`);
283
295
 
284
296
  if (initContextBlocks.length > 0) {
@@ -22,10 +22,16 @@ export async function GET(req: NextRequest) {
22
22
 
23
23
  if (op === 'list') {
24
24
  const path = req.nextUrl.searchParams.get('path') ?? undefined;
25
+ const sourceParam = req.nextUrl.searchParams.get('source');
26
+ const source = sourceParam === 'user' || sourceParam === 'agent' || sourceParam === 'system'
27
+ ? sourceParam
28
+ : undefined;
29
+ const opFilter = req.nextUrl.searchParams.get('event_op') ?? undefined;
30
+ const q = req.nextUrl.searchParams.get('q') ?? undefined;
25
31
  const limitParam = req.nextUrl.searchParams.get('limit');
26
32
  const limit = limitParam ? Number(limitParam) : 50;
27
33
  if (!Number.isFinite(limit) || limit <= 0) return err('invalid limit');
28
- return NextResponse.json({ events: listContentChanges({ path, limit }) });
34
+ return NextResponse.json({ events: listContentChanges({ path, source, op: opFilter, q, limit }) });
29
35
  }
30
36
 
31
37
  return err(`unknown op: ${op}`);
@@ -22,6 +22,7 @@ import {
22
22
  appendContentChange,
23
23
  } from '@/lib/fs';
24
24
  import { createSpaceFilesystem } from '@/lib/core/create-space';
25
+ import { appendAgentAuditEvent, parseAgentAuditJsonLines } from '@/lib/core/agent-audit-log';
25
26
 
26
27
  function err(msg: string, status = 400) {
27
28
  return NextResponse.json({ error: msg }, { status });
@@ -123,6 +124,14 @@ export async function POST(req: NextRequest) {
123
124
  case 'append_to_file': {
124
125
  const { content } = params as { content: string };
125
126
  if (typeof content !== 'string') return err('missing content');
127
+ if (filePath === '.agent-log.json') {
128
+ const entries = parseAgentAuditJsonLines(content);
129
+ for (const entry of entries) {
130
+ appendAgentAuditEvent(getMindRoot(), entry);
131
+ }
132
+ resp = NextResponse.json({ ok: true, migratedEntries: entries.length });
133
+ break;
134
+ }
126
135
  const before = safeRead(filePath);
127
136
  appendToFile(filePath, content);
128
137
  changeEvent = {
@@ -1,12 +1,24 @@
1
1
  export const dynamic = 'force-dynamic';
2
2
  import { NextResponse } from 'next/server';
3
- import { MCP_AGENTS, detectInstalled, detectAgentPresence } from '@/lib/mcp-agents';
3
+ import {
4
+ MCP_AGENTS,
5
+ detectInstalled,
6
+ detectAgentPresence,
7
+ detectAgentRuntimeSignals,
8
+ detectAgentConfiguredMcpServers,
9
+ detectAgentInstalledSkills,
10
+ resolveSkillWorkspaceProfile,
11
+ } from '@/lib/mcp-agents';
4
12
 
5
13
  export async function GET() {
6
14
  try {
7
15
  const agents = Object.entries(MCP_AGENTS).map(([key, agent]) => {
8
16
  const status = detectInstalled(key);
9
17
  const present = detectAgentPresence(key);
18
+ const skillProfile = resolveSkillWorkspaceProfile(key);
19
+ const runtime = detectAgentRuntimeSignals(key);
20
+ const configuredMcp = detectAgentConfiguredMcpServers(key);
21
+ const installedSkills = detectAgentInstalledSkills(key);
10
22
  return {
11
23
  key,
12
24
  name: agent.name,
@@ -24,6 +36,20 @@ export async function GET() {
24
36
  globalNestedKey: agent.globalNestedKey,
25
37
  globalPath: agent.global,
26
38
  projectPath: agent.project,
39
+ skillMode: skillProfile.mode,
40
+ skillAgentName: skillProfile.skillAgentName,
41
+ skillWorkspacePath: skillProfile.workspacePath,
42
+ hiddenRootPath: runtime.hiddenRootPath,
43
+ hiddenRootPresent: runtime.hiddenRootPresent,
44
+ runtimeConversationSignal: runtime.conversationSignal,
45
+ runtimeUsageSignal: runtime.usageSignal,
46
+ runtimeLastActivityAt: runtime.lastActivityAt,
47
+ configuredMcpServers: configuredMcp.servers,
48
+ configuredMcpServerCount: configuredMcp.servers.length,
49
+ configuredMcpSources: configuredMcp.sources,
50
+ installedSkillNames: installedSkills.skills,
51
+ installedSkillCount: installedSkills.skills.length,
52
+ installedSkillSourcePath: installedSkills.sourcePath,
27
53
  };
28
54
  });
29
55
 
@@ -3,35 +3,15 @@ import { NextRequest, NextResponse } from 'next/server';
3
3
  import { execSync } from 'child_process';
4
4
  import path from 'path';
5
5
  import fs from 'fs';
6
+ import { SKILL_AGENT_REGISTRY } from '@/lib/mcp-agents';
6
7
 
7
8
  /* ── Constants ────────────────────────────────────────────────── */
8
9
 
9
10
  const GITHUB_SOURCE = 'GeminiLight/MindOS';
10
11
 
11
- // Universal agents read directly from ~/.agents/skills/ — no symlink needed.
12
- const UNIVERSAL_AGENTS = new Set([
13
- 'amp', 'cline', 'codex', 'cursor', 'gemini-cli',
14
- 'github-copilot', 'kimi-cli', 'opencode', 'warp',
15
- ]);
16
-
17
12
  // Agents that do NOT support Skills at all
18
13
  const SKILL_UNSUPPORTED = new Set<string>([]);
19
14
 
20
- // MCP agent key → npx skills agent name (for non-universal agents)
21
- const AGENT_NAME_MAP: Record<string, string> = {
22
- 'claude-code': 'claude-code',
23
- 'windsurf': 'windsurf',
24
- 'trae': 'trae',
25
- 'openclaw': 'openclaw',
26
- 'codebuddy': 'codebuddy',
27
- 'iflow-cli': 'iflow-cli',
28
- 'pi': 'pi',
29
- 'augment': 'augment',
30
- 'qwen-code': 'qwen-code',
31
- 'trae-cn': 'trae-cn',
32
- 'roo': 'roo',
33
- };
34
-
35
15
  /* ── Helpers ──────────────────────────────────────────────────── */
36
16
 
37
17
  /** Fallback: find local skills directory for offline installs */
@@ -78,9 +58,14 @@ export async function POST(req: NextRequest) {
78
58
  return NextResponse.json({ error: 'Invalid skill name' }, { status: 400 });
79
59
  }
80
60
 
81
- const additionalAgents = (agents || [])
82
- .filter(key => !UNIVERSAL_AGENTS.has(key) && !SKILL_UNSUPPORTED.has(key))
83
- .map(key => AGENT_NAME_MAP[key] || key);
61
+ const additionalAgents = (agents || []).flatMap((key) => {
62
+ if (SKILL_UNSUPPORTED.has(key)) return [];
63
+ const reg = SKILL_AGENT_REGISTRY[key];
64
+ if (!reg) return [key]; // Forward-compatible fallback for unknown keys.
65
+ if (reg.mode === 'unsupported') return [];
66
+ if (reg.mode === 'universal') return [];
67
+ return [reg.skillAgentName || key];
68
+ });
84
69
 
85
70
  // Try GitHub source first, fall back to local path
86
71
  const sources = [GITHUB_SOURCE];
@@ -128,12 +128,13 @@ export async function GET() {
128
128
  export async function POST(req: NextRequest) {
129
129
  try {
130
130
  const body = await req.json();
131
- const { action, name, description, content, enabled } = body as {
132
- action: 'create' | 'update' | 'delete' | 'toggle' | 'read';
131
+ const { action, name, description, content, enabled, sourcePath } = body as {
132
+ action: 'create' | 'update' | 'delete' | 'toggle' | 'read' | 'read-native';
133
133
  name?: string;
134
134
  description?: string;
135
135
  content?: string;
136
136
  enabled?: boolean;
137
+ sourcePath?: string;
137
138
  };
138
139
 
139
140
  const settings = readSettings();
@@ -218,6 +219,21 @@ export async function POST(req: NextRequest) {
218
219
  return NextResponse.json({ error: 'Skill not found' }, { status: 404 });
219
220
  }
220
221
 
222
+ case 'read-native': {
223
+ if (!name || !sourcePath) return NextResponse.json({ error: 'name and sourcePath required' }, { status: 400 });
224
+ const nativeBase = path.resolve(sourcePath);
225
+ const nativeSkillFile = path.join(nativeBase, name, 'SKILL.md');
226
+ if (!nativeSkillFile.startsWith(nativeBase)) {
227
+ return NextResponse.json({ error: 'Invalid path' }, { status: 400 });
228
+ }
229
+ if (!fs.existsSync(nativeSkillFile)) {
230
+ return NextResponse.json({ error: 'Skill not found' }, { status: 404 });
231
+ }
232
+ const nativeContent = fs.readFileSync(nativeSkillFile, 'utf-8');
233
+ const { description: nativeDesc } = parseSkillMd(nativeContent);
234
+ return NextResponse.json({ content: nativeContent, description: nativeDesc });
235
+ }
236
+
221
237
  default:
222
238
  return NextResponse.json({ error: `Unknown action: ${action}` }, { status: 400 });
223
239
  }
@@ -0,0 +1,8 @@
1
+ import { NextResponse } from 'next/server';
2
+ import { getTreeVersion } from '@/lib/fs';
3
+
4
+ export const dynamic = 'force-dynamic';
5
+
6
+ export function GET() {
7
+ return NextResponse.json({ v: getTreeVersion() });
8
+ }
@@ -11,6 +11,7 @@ import UpdateBanner from '@/components/UpdateBanner';
11
11
  import UpdateOverlay from '@/components/UpdateOverlay';
12
12
  import { cookies } from 'next/headers';
13
13
  import type { Locale } from '@/lib/i18n';
14
+ import '@/lib/renderers/index'; // globally register built-in renderers once
14
15
 
15
16
  const geistSans = Inter({
16
17
  variable: '--font-geist-sans',
package/app/app/page.tsx CHANGED
@@ -2,7 +2,6 @@ import { redirect } from 'next/navigation';
2
2
  import { readSettings } from '@/lib/settings';
3
3
  import { getRecentlyModified, getFileContent, getFileTree } from '@/lib/fs';
4
4
  import { getAllRenderers } from '@/lib/renderers/registry';
5
- import '@/lib/renderers/index'; // registers all renderers
6
5
  import HomeContent from '@/components/HomeContent';
7
6
  import type { FileNode } from '@/lib/core/types';
8
7
 
@@ -83,7 +82,7 @@ export default function HomePage() {
83
82
  console.error('[HomePage] Failed to load recent files:', err);
84
83
  }
85
84
 
86
- // Derive plugin entry paths from registry — no hardcoded list needed
85
+ // Derive renderer entry paths from registry — used by plugin and app-builtin sections on home.
87
86
  const entryPaths = getAllRenderers()
88
87
  .map(r => r.entryPath)
89
88
  .filter((p): p is string => !!p);
@@ -17,7 +17,6 @@ import { resolveRenderer, isRendererEnabled } from '@/lib/renderers/registry';
17
17
  import { encodePath } from '@/lib/utils';
18
18
  import { useLocale } from '@/lib/LocaleContext';
19
19
  import DirPicker from '@/components/DirPicker';
20
- import '@/lib/renderers/index'; // registers all renderers
21
20
 
22
21
  interface ViewPageClientProps {
23
22
  filePath: string;
@@ -58,7 +58,7 @@ function RailButton({ icon, label, shortcut, active = false, expanded, onClick,
58
58
  `}
59
59
  >
60
60
  {active && (
61
- <span className="absolute left-0 top-1/2 -translate-y-1/2 w-[2px] h-[18px] rounded-r-full" style={{ background: 'var(--amber)' }} />
61
+ <span className="absolute left-0 top-1/2 -translate-y-1/2 w-[2px] h-[18px] rounded-r-full bg-[var(--amber)]" />
62
62
  )}
63
63
  <span className="shrink-0 flex items-center justify-center w-[18px]">{icon}</span>
64
64
  {badge}
@@ -147,7 +147,7 @@ export default function ActivityBar({
147
147
  {/* ── Top: Logo ── */}
148
148
  <Link
149
149
  href="/"
150
- className={`flex items-center ${expanded ? 'px-3 gap-2' : 'justify-center'} w-full py-3 hover:opacity-80 transition-opacity`}
150
+ className={`flex items-center ${expanded ? 'px-3 gap-2' : 'justify-center'} w-full py-[13px] hover:opacity-80 transition-opacity`}
151
151
  aria-label="MindOS Home"
152
152
  >
153
153
  <Logo id="rail" className="w-7 h-3.5 shrink-0" />
@@ -30,7 +30,7 @@ export default function Backlinks({ filePath }: { filePath: string }) {
30
30
  return (
31
31
  <div className="mt-12 pt-8 border-t border-border">
32
32
  <div className="flex items-center gap-2 mb-6 text-muted-foreground">
33
- <LinkIcon size={16} className="text-amber-500/70" />
33
+ <LinkIcon size={16} className="text-[var(--amber)]/70" />
34
34
  <h3 className="text-sm font-semibold tracking-wider uppercase font-display">
35
35
  {t.common?.relatedFiles || 'Related Files'}
36
36
  </h3>
@@ -51,14 +51,14 @@ export default function Backlinks({ filePath }: { filePath: string }) {
51
51
  <Link
52
52
  key={link.filePath}
53
53
  href={`/view/${link.filePath.split('/').map(encodeURIComponent).join('/')}`}
54
- className="group block p-4 rounded-xl border border-border/50 bg-card/30 hover:bg-muted/30 hover:border-amber-500/30 transition-all duration-200"
54
+ className="group block p-4 rounded-xl border border-border/50 bg-card/30 hover:bg-muted/30 hover:border-[var(--amber)]/30 transition-all duration-200"
55
55
  >
56
56
  <div className="flex items-start gap-3">
57
- <div className="mt-1 p-1.5 rounded-md bg-muted group-hover:bg-amber-500/10 transition-colors">
58
- <FileText size={14} className="text-muted-foreground group-hover:text-amber-500" />
57
+ <div className="mt-1 p-1.5 rounded-md bg-muted group-hover:bg-[var(--amber)]/10 transition-colors">
58
+ <FileText size={14} className="text-muted-foreground group-hover:text-[var(--amber)]" />
59
59
  </div>
60
60
  <div className="min-w-0 flex-1">
61
- <div className="font-medium text-sm text-foreground group-hover:text-amber-500 transition-colors truncate mb-1">
61
+ <div className="font-medium text-sm text-foreground group-hover:text-[var(--amber)] transition-colors truncate mb-1">
62
62
  {link.filePath}
63
63
  </div>
64
64
  <div className="text-xs text-muted-foreground line-clamp-2 leading-relaxed italic opacity-80 group-hover:opacity-100 transition-opacity">
@@ -108,6 +108,7 @@ export default function CreateSpaceModal({ t, dirPaths }: { t: ReturnType<typeof
108
108
 
109
109
  close();
110
110
  router.refresh();
111
+ window.dispatchEvent(new Event('mindos:files-changed'));
111
112
  router.push(`/view/${encodePath(createdPath + '/')}`);
112
113
  } else {
113
114
  const msg = result.error ?? '';
@@ -200,7 +201,7 @@ export default function CreateSpaceModal({ t, dirPaths }: { t: ReturnType<typeof
200
201
  disabled={!aiAvailable}
201
202
  onClick={() => setUseAi(v => !v)}
202
203
  className={`relative mt-0.5 inline-flex shrink-0 h-4 w-7 cursor-pointer rounded-full border-2 border-transparent transition-colors focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 ${
203
- useAi ? 'bg-amber-500' : 'bg-muted'
204
+ useAi ? 'bg-[var(--amber)]' : 'bg-muted'
204
205
  }`}
205
206
  >
206
207
  <span className={`pointer-events-none inline-block h-3 w-3 rounded-full bg-white shadow-sm transition-transform ${useAi ? 'translate-x-3' : 'translate-x-0'}`} />
@@ -212,7 +213,7 @@ export default function CreateSpaceModal({ t, dirPaths }: { t: ReturnType<typeof
212
213
  </div>
213
214
  {aiAvailable === false && (
214
215
  <p className="text-2xs text-muted-foreground mt-0.5 flex items-center gap-1">
215
- <AlertTriangle size={10} className="text-amber-500 shrink-0" />
216
+ <AlertTriangle size={10} className="text-[var(--amber)] shrink-0" />
216
217
  {h.aiInitNoKey ?? 'Configure an API key in Settings → AI to enable'}
217
218
  </p>
218
219
  )}
@@ -57,7 +57,7 @@ export default function DirPicker({ dirPaths, value, onChange, rootLabel = 'Root
57
57
  <button
58
58
  type="button"
59
59
  onClick={() => setExpanded(true)}
60
- className="w-full flex items-center gap-2 px-3 py-2 text-sm rounded-lg border border-border bg-background text-foreground hover:border-amber-500/40 transition-colors text-left"
60
+ className="w-full flex items-center gap-2 px-3 py-2 text-sm rounded-lg border border-border bg-background text-foreground hover:border-[var(--amber)]/40 transition-colors text-left"
61
61
  >
62
62
  <Folder size={14} className="shrink-0 text-[var(--amber)]" />
63
63
  <span className="flex-1 truncate">{displayLabel}</span>
@@ -84,8 +84,7 @@ function SpacePreviewCard({ icon, title, lines, viewAllHref, viewAllLabel }: {
84
84
  <div className="flex justify-end mt-2">
85
85
  <Link
86
86
  href={viewAllHref}
87
- className="text-xs hover:underline transition-colors"
88
- style={{ color: 'var(--amber)' }}
87
+ className="text-xs hover:underline transition-colors text-[var(--amber)]"
89
88
  >
90
89
  {viewAllLabel}
91
90
  </Link>
@@ -150,7 +149,7 @@ export default function DirView({ dirPath, entries, spacePreview }: DirViewProps
150
149
  return (
151
150
  <div className="flex flex-col min-h-screen">
152
151
  {/* Topbar */}
153
- <div className="sticky top-[52px] md:top-0 z-20 border-b border-border px-4 md:px-6 py-2.5" style={{ background: 'var(--background)' }}>
152
+ <div className="sticky top-[52px] md:top-0 z-20 border-b border-border px-4 md:px-6 py-2.5 bg-background">
154
153
  <div className="max-w-[860px] mx-auto flex items-center justify-between gap-2">
155
154
  <div className="min-w-0 flex-1">
156
155
  <Breadcrumb filePath={dirPath} />
@@ -5,9 +5,9 @@ import dynamic from 'next/dynamic';
5
5
  const Editor = dynamic(() => import('./Editor'), {
6
6
  ssr: false,
7
7
  loading: () => (
8
- <div className="h-full w-full min-h-[400px] rounded-lg border border-zinc-800 bg-zinc-900 flex items-center justify-center">
9
- <div className="flex items-center gap-2 text-zinc-500 text-sm">
10
- <div className="w-4 h-4 border-2 border-zinc-600 border-t-zinc-300 rounded-full animate-spin" />
8
+ <div className="h-full w-full min-h-[400px] rounded-lg border border-border bg-background flex items-center justify-center">
9
+ <div className="flex items-center gap-2 text-muted-foreground text-sm">
10
+ <div className="w-4 h-4 border-2 border-border border-t-foreground rounded-full animate-spin" />
11
11
  <span>Loading editor...</span>
12
12
  </div>
13
13
  </div>
@@ -11,6 +11,10 @@ import {
11
11
  import { createFileAction, deleteFileAction, renameFileAction, renameSpaceAction, deleteSpaceAction, convertToSpaceAction, deleteFolderAction } from '@/lib/actions';
12
12
  import { useLocale } from '@/lib/LocaleContext';
13
13
 
14
+ function notifyFilesChanged() {
15
+ window.dispatchEvent(new Event('mindos:files-changed'));
16
+ }
17
+
14
18
  const SYSTEM_FILES = new Set(['INSTRUCTION.md', 'README.md']);
15
19
 
16
20
  interface FileTreeProps {
@@ -112,7 +116,7 @@ function SpaceContextMenu({ x, y, node, onClose, onRename }: {
112
116
  if (!confirm(t.fileTree.confirmDeleteSpace(node.name))) return;
113
117
  startTransition(async () => {
114
118
  const result = await deleteSpaceAction(node.path);
115
- if (result.success) { router.push('/'); router.refresh(); }
119
+ if (result.success) { router.push('/'); router.refresh(); notifyFilesChanged(); }
116
120
  onClose();
117
121
  });
118
122
  }}>
@@ -137,11 +141,11 @@ function FolderContextMenu({ x, y, node, onClose, onRename }: {
137
141
  <button className={MENU_ITEM} disabled={isPending} onClick={() => {
138
142
  startTransition(async () => {
139
143
  const result = await convertToSpaceAction(node.path);
140
- if (result.success) router.refresh();
144
+ if (result.success) { router.refresh(); notifyFilesChanged(); }
141
145
  onClose();
142
146
  });
143
147
  }}>
144
- <Layers size={14} className="shrink-0" style={{ color: 'var(--amber)' }} /> {t.fileTree.convertToSpace}
148
+ <Layers size={14} className="shrink-0 text-[var(--amber)]" /> {t.fileTree.convertToSpace}
145
149
  </button>
146
150
  <button className={MENU_ITEM} onClick={() => { onRename(); onClose(); }}>
147
151
  <Pencil size={14} className="shrink-0" /> {t.fileTree.rename}
@@ -151,7 +155,7 @@ function FolderContextMenu({ x, y, node, onClose, onRename }: {
151
155
  if (!confirm(t.fileTree.confirmDeleteFolder(node.name))) return;
152
156
  startTransition(async () => {
153
157
  const result = await deleteFolderAction(node.path);
154
- if (result.success) { router.push('/'); router.refresh(); }
158
+ if (result.success) { router.push('/'); router.refresh(); notifyFilesChanged(); }
155
159
  onClose();
156
160
  });
157
161
  }}>
@@ -181,6 +185,7 @@ function NewFileInline({ dirPath, depth, onDone }: { dirPath: string; depth: num
181
185
  onDone();
182
186
  router.push(`/view/${encodePath(result.filePath)}`);
183
187
  router.refresh();
188
+ notifyFilesChanged();
184
189
  } else {
185
190
  setError(result.error || t.fileTree.failed);
186
191
  }
@@ -217,11 +222,11 @@ function NewFileInline({ dirPath, depth, onDone }: { dirPath: string; depth: num
217
222
  "
218
223
  />
219
224
  {isPending
220
- ? <Loader2 size={13} className="text-zinc-500 animate-spin shrink-0" />
225
+ ? <Loader2 size={13} className="text-muted-foreground animate-spin shrink-0" />
221
226
  : (
222
227
  <button
223
228
  onClick={handleSubmit}
224
- className="text-xs text-blue-400 hover:text-blue-300 shrink-0 px-1"
229
+ className="text-xs text-[var(--amber)] hover:text-foreground shrink-0 px-1"
225
230
  >
226
231
  {t.fileTree.create}
227
232
  </button>
@@ -294,6 +299,7 @@ function DirectoryNode({ node, depth, currentPath, onNavigate, maxOpenDepth }: {
294
299
  setRenaming(false);
295
300
  router.push(`/view/${encodePath(result.newPath)}`);
296
301
  router.refresh();
302
+ notifyFilesChanged();
297
303
  } else {
298
304
  setRenaming(false);
299
305
  }
@@ -338,7 +344,7 @@ function DirectoryNode({ node, depth, currentPath, onNavigate, maxOpenDepth }: {
338
344
  onBlur={commitRename}
339
345
  className="w-full bg-muted border border-border rounded px-2 py-0.5 text-xs text-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
340
346
  />
341
- {isPending && <Loader2 size={12} className="absolute right-3 top-1/2 -translate-y-1/2 animate-spin text-zinc-500" />}
347
+ {isPending && <Loader2 size={12} className="absolute right-3 top-1/2 -translate-y-1/2 animate-spin text-muted-foreground" />}
342
348
  </div>
343
349
  );
344
350
  }
@@ -353,7 +359,7 @@ function DirectoryNode({ node, depth, currentPath, onNavigate, maxOpenDepth }: {
353
359
  >
354
360
  <button
355
361
  onClick={toggle}
356
- className="shrink-0 p-1 rounded hover:bg-muted text-zinc-500 transition-colors"
362
+ className="shrink-0 p-1 rounded hover:bg-muted text-muted-foreground transition-colors"
357
363
  style={{ marginLeft: `${depth * 12 + 4}px` }}
358
364
  aria-label={open ? 'Collapse' : 'Expand'}
359
365
  >
@@ -373,7 +379,7 @@ function DirectoryNode({ node, depth, currentPath, onNavigate, maxOpenDepth }: {
373
379
  `}
374
380
  >
375
381
  {isSpace
376
- ? <Layers size={14} className="shrink-0" style={{ color: 'var(--amber)' }} />
382
+ ? <Layers size={14} className="shrink-0 text-[var(--amber)]" />
377
383
  : open
378
384
  ? <FolderOpen size={14} className="text-yellow-400 shrink-0" />
379
385
  : <Folder size={14} className="text-yellow-400 shrink-0" />
@@ -505,6 +511,7 @@ function FileNodeItem({ node, depth, currentPath, onNavigate }: {
505
511
  setRenaming(false);
506
512
  router.push(`/view/${encodePath(result.newPath)}`);
507
513
  router.refresh();
514
+ notifyFilesChanged();
508
515
  } else {
509
516
  setRenaming(false);
510
517
  }
@@ -518,9 +525,15 @@ function FileNodeItem({ node, depth, currentPath, onNavigate }: {
518
525
  await deleteFileAction(node.path);
519
526
  if (currentPath === node.path) router.push('/');
520
527
  router.refresh();
528
+ notifyFilesChanged();
521
529
  });
522
530
  }, [node.name, node.path, currentPath, router, t]);
523
531
 
532
+ const handleDragStart = useCallback((e: React.DragEvent) => {
533
+ e.dataTransfer.setData('text/mindos-path', node.path);
534
+ e.dataTransfer.effectAllowed = 'copy';
535
+ }, [node.path]);
536
+
524
537
  if (renaming) {
525
538
  return (
526
539
  <div className="relative px-2 py-0.5" style={{ paddingLeft: `${depth * 12 + 8}px` }}>
@@ -536,7 +549,7 @@ function FileNodeItem({ node, depth, currentPath, onNavigate }: {
536
549
  onBlur={commitRename}
537
550
  className="w-full bg-muted border border-border rounded px-2 py-0.5 text-xs text-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
538
551
  />
539
- {isPending && <Loader2 size={12} className="absolute right-3 top-1/2 -translate-y-1/2 animate-spin text-zinc-500" />}
552
+ {isPending && <Loader2 size={12} className="absolute right-3 top-1/2 -translate-y-1/2 animate-spin text-muted-foreground" />}
540
553
  </div>
541
554
  );
542
555
  }
@@ -546,6 +559,8 @@ function FileNodeItem({ node, depth, currentPath, onNavigate }: {
546
559
  <button
547
560
  onClick={handleClick}
548
561
  onDoubleClick={startRename}
562
+ draggable
563
+ onDragStart={handleDragStart}
549
564
  data-filepath={node.path}
550
565
  className={`
551
566
  w-full flex items-center gap-1.5 px-2 py-1 rounded text-left
@@ -238,7 +238,7 @@ export default function GuideCard({ onNavigate, spaces = [], recentFiles = [] }:
238
238
  const fileName = file.path.split('/').pop() || file.path;
239
239
  return (
240
240
  <button key={file.path} onClick={() => handleFileOpen(file.path)}
241
- className="text-left text-xs px-3 py-2 rounded-lg border border-border text-foreground transition-colors hover:border-amber-500/30 hover:bg-muted/50 truncate">
241
+ className="text-left text-xs px-3 py-2 rounded-lg border border-border text-foreground transition-colors hover:border-[var(--amber)]/30 hover:bg-muted/50 truncate">
242
242
  📄 {fileName}
243
243
  </button>
244
244
  );
@@ -255,7 +255,7 @@ export default function GuideCard({ onNavigate, spaces = [], recentFiles = [] }:
255
255
  const label = stripEmoji(s.name);
256
256
  return (
257
257
  <button key={s.name} onClick={() => handleFileOpen(s.path)}
258
- className="text-left text-xs px-3 py-2 rounded-lg border border-border text-foreground transition-colors hover:border-amber-500/30 hover:bg-muted/50">
258
+ className="text-left text-xs px-3 py-2 rounded-lg border border-border text-foreground transition-colors hover:border-[var(--amber)]/30 hover:bg-muted/50">
259
259
  <span className="mr-1.5">{emoji || '📁'}</span>
260
260
  <span>{label}</span>
261
261
  <span className="block text-2xs mt-0.5 text-muted-foreground">
@@ -305,8 +305,8 @@ function TaskCard({ icon, title, cta, done, active, optional, onClick }: {
305
305
  className={`
306
306
  flex flex-col items-center gap-1.5 px-3 py-3 rounded-lg border text-center
307
307
  transition-all duration-150
308
- ${done ? 'opacity-60' : 'hover:border-amber-500/30 hover:bg-muted/50 cursor-pointer'}
309
- ${active ? 'border-amber-500/40 bg-muted/50' : ''}
308
+ ${done ? 'opacity-60' : 'hover:border-[var(--amber)]/30 hover:bg-muted/50 cursor-pointer'}
309
+ ${active ? 'border-[var(--amber)]/40 bg-muted/50' : ''}
310
310
  ${done || active ? 'border-[var(--amber)]' : 'border-border'}
311
311
  `}
312
312
  >