@geminilight/mindos 0.5.64 → 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 (85) 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/file/route.ts +9 -0
  5. package/app/app/api/mcp/agents/route.ts +27 -1
  6. package/app/app/api/skills/route.ts +18 -2
  7. package/app/app/api/tree-version/route.ts +8 -0
  8. package/app/components/ActivityBar.tsx +2 -2
  9. package/app/components/Backlinks.tsx +5 -5
  10. package/app/components/CreateSpaceModal.tsx +3 -2
  11. package/app/components/DirPicker.tsx +1 -1
  12. package/app/components/DirView.tsx +2 -3
  13. package/app/components/EditorWrapper.tsx +3 -3
  14. package/app/components/FileTree.tsx +25 -10
  15. package/app/components/GuideCard.tsx +4 -4
  16. package/app/components/HomeContent.tsx +6 -11
  17. package/app/components/MarkdownView.tsx +2 -2
  18. package/app/components/OnboardingView.tsx +1 -1
  19. package/app/components/Panel.tsx +1 -1
  20. package/app/components/RightAgentDetailPanel.tsx +1 -1
  21. package/app/components/RightAskPanel.tsx +1 -1
  22. package/app/components/SearchModal.tsx +10 -2
  23. package/app/components/SidebarLayout.tsx +35 -10
  24. package/app/components/ThemeToggle.tsx +1 -1
  25. package/app/components/agents/AgentDetailContent.tsx +454 -59
  26. package/app/components/agents/AgentsContentPage.tsx +70 -5
  27. package/app/components/agents/AgentsMcpSection.tsx +474 -159
  28. package/app/components/agents/AgentsOverviewSection.tsx +418 -59
  29. package/app/components/agents/AgentsPrimitives.tsx +335 -0
  30. package/app/components/agents/AgentsSkillsSection.tsx +739 -121
  31. package/app/components/agents/SkillDetailPopover.tsx +416 -0
  32. package/app/components/agents/agents-content-model.ts +292 -10
  33. package/app/components/ask/AskContent.tsx +34 -5
  34. package/app/components/ask/FileChip.tsx +1 -0
  35. package/app/components/ask/MentionPopover.tsx +13 -1
  36. package/app/components/ask/MessageList.tsx +5 -7
  37. package/app/components/ask/ToolCallBlock.tsx +4 -4
  38. package/app/components/changes/ChangesBanner.tsx +1 -2
  39. package/app/components/echo/EchoHero.tsx +10 -24
  40. package/app/components/echo/EchoInsightCollapsible.tsx +52 -43
  41. package/app/components/echo/EchoPageSections.tsx +13 -9
  42. package/app/components/echo/EchoSegmentNav.tsx +14 -11
  43. package/app/components/echo/EchoSegmentPageClient.tsx +64 -43
  44. package/app/components/explore/ExploreContent.tsx +3 -7
  45. package/app/components/explore/UseCaseCard.tsx +4 -15
  46. package/app/components/panels/AgentsPanel.tsx +12 -104
  47. package/app/components/panels/AgentsPanelAgentDetail.tsx +2 -2
  48. package/app/components/panels/AgentsPanelAgentGroups.tsx +3 -7
  49. package/app/components/panels/AgentsPanelAgentListRow.tsx +9 -11
  50. package/app/components/panels/EchoPanel.tsx +8 -10
  51. package/app/components/panels/PanelNavRow.tsx +9 -2
  52. package/app/components/panels/PluginsPanel.tsx +2 -2
  53. package/app/components/renderers/agent-inspector/AgentInspectorRenderer.tsx +30 -8
  54. package/app/components/renderers/agent-inspector/manifest.ts +3 -3
  55. package/app/components/renderers/todo/manifest.ts +1 -0
  56. package/app/components/settings/AiTab.tsx +3 -3
  57. package/app/components/settings/AppearanceTab.tsx +2 -2
  58. package/app/components/settings/KnowledgeTab.tsx +3 -3
  59. package/app/components/settings/McpAgentInstall.tsx +3 -6
  60. package/app/components/settings/McpSkillCreateForm.tsx +2 -3
  61. package/app/components/settings/McpSkillRow.tsx +2 -3
  62. package/app/components/settings/McpSkillsSection.tsx +2 -2
  63. package/app/components/settings/McpTab.tsx +12 -13
  64. package/app/components/settings/MonitoringTab.tsx +13 -13
  65. package/app/components/settings/PluginsTab.tsx +2 -2
  66. package/app/components/settings/Primitives.tsx +3 -4
  67. package/app/components/settings/SettingsContent.tsx +3 -3
  68. package/app/components/settings/SyncTab.tsx +11 -17
  69. package/app/components/settings/UpdateTab.tsx +18 -21
  70. package/app/components/settings/types.ts +14 -0
  71. package/app/components/setup/StepKB.tsx +1 -1
  72. package/app/hooks/useMcpData.tsx +4 -2
  73. package/app/hooks/useMention.ts +25 -8
  74. package/app/lib/agent/log.ts +15 -18
  75. package/app/lib/agent/stream-consumer.ts +3 -0
  76. package/app/lib/agent/to-agent-messages.ts +6 -4
  77. package/app/lib/core/agent-audit-log.ts +280 -0
  78. package/app/lib/core/index.ts +11 -0
  79. package/app/lib/fs.ts +9 -0
  80. package/app/lib/i18n-en.ts +259 -33
  81. package/app/lib/i18n-zh.ts +258 -32
  82. package/app/lib/mcp-agents.ts +231 -2
  83. package/app/lib/types.ts +2 -0
  84. package/package.json +1 -1
  85. package/scripts/migrate-agent-audit-log.js +170 -0
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,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
 
@@ -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
+ }
@@ -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
  >
@@ -115,25 +115,20 @@ export default function HomeContent({ recent, existingFiles, spaces, dirPaths }:
115
115
  }, [suggestions.length]);
116
116
 
117
117
  const existingSet = new Set(existingFiles ?? []);
118
+ const spaceList = spaces ?? [];
119
+ const { groups, rootFiles } = useMemo(() => groupBySpace(recent, spaceList), [recent, spaceList]);
118
120
 
119
- // Empty knowledge base → show onboarding
120
121
  if (recent.length === 0) {
121
122
  return <OnboardingView />;
122
123
  }
123
124
 
124
125
  const formatTime = (mtime: number) => relativeTime(mtime, t.home.relativeTime);
125
126
 
126
- // User-manageable plugins: only show available entry files
127
127
  const availablePlugins = getPluginRenderers().filter(r => r.entryPath && existingSet.has(r.entryPath));
128
- // App-builtin features: always visible, with active/inactive state
129
128
  const builtinFeatures = getAllRenderers().filter((r) => r.appBuiltinFeature && r.id !== 'csv');
130
129
 
131
130
  const lastFile = recent[0];
132
131
 
133
- // Group recent files by Space
134
- const spaceList = spaces ?? [];
135
- const { groups, rootFiles } = useMemo(() => groupBySpace(recent, spaceList), [recent, spaceList]);
136
-
137
132
  return (
138
133
  <div className="content-width px-4 md:px-6 py-8 md:py-12">
139
134
  <GuideCard
@@ -160,7 +155,7 @@ export default function HomeContent({ recent, existingFiles, spaces, dirPaths }:
160
155
  onClick={triggerAsk}
161
156
  title="⌘/"
162
157
  data-walkthrough="ask-button"
163
- className="flex-1 flex items-center gap-3 px-4 py-3 rounded-xl border border-border bg-card transition-all duration-150 hover:border-amber-500/50 hover:bg-amber-500/8"
158
+ className="flex-1 flex items-center gap-3 px-4 py-3 rounded-xl border border-border bg-card transition-all duration-150 hover:border-[var(--amber)]/50 hover:bg-[var(--amber)]/8"
164
159
  >
165
160
  <Sparkles size={15} className="shrink-0 text-[var(--amber)]" />
166
161
  <span className="text-sm flex-1 text-left text-foreground">
@@ -237,7 +232,7 @@ export default function HomeContent({ recent, existingFiles, spaces, dirPaths }:
237
232
  className={`flex items-start gap-3 px-3.5 py-3 rounded-xl border transition-all duration-150 hover:translate-x-0.5 ${
238
233
  isEmpty
239
234
  ? 'border-dashed border-border/50 opacity-50 hover:opacity-70'
240
- : 'border-border hover:border-amber-500/30 hover:bg-muted/40'
235
+ : 'border-border hover:border-[var(--amber)]/30 hover:bg-muted/40'
241
236
  }`}
242
237
  >
243
238
  {emoji ? (
@@ -288,7 +283,7 @@ export default function HomeContent({ recent, existingFiles, spaces, dirPaths }:
288
283
  if (active && r.entryPath) {
289
284
  return (
290
285
  <Link key={r.id} href={`/view/${encodePath(r.entryPath)}`}>
291
- <span className="inline-flex items-center gap-2 px-3 py-2 rounded-lg border border-border text-xs transition-all duration-150 hover:border-amber-500/30 hover:bg-muted/60">
286
+ <span className="inline-flex items-center gap-2 px-3 py-2 rounded-lg border border-border text-xs transition-all duration-150 hover:border-[var(--amber)]/30 hover:bg-muted/60">
292
287
  <span className="text-sm leading-none" suppressHydrationWarning>{r.icon}</span>
293
288
  <span className="font-medium text-foreground">{r.name}</span>
294
289
  </span>
@@ -335,7 +330,7 @@ export default function HomeContent({ recent, existingFiles, spaces, dirPaths }:
335
330
  <Link
336
331
  key={r.id}
337
332
  href={`/view/${encodePath(r.entryPath!)}`}
338
- className="inline-flex items-center gap-2 px-3 py-2 rounded-lg border border-border text-xs transition-all duration-150 hover:border-amber-500/30 hover:bg-muted/60"
333
+ className="inline-flex items-center gap-2 px-3 py-2 rounded-lg border border-border text-xs transition-all duration-150 hover:border-[var(--amber)]/30 hover:bg-muted/60"
339
334
  >
340
335
  <span className="text-sm leading-none" suppressHydrationWarning>{r.icon}</span>
341
336
  <span className="font-medium text-foreground">{r.name}</span>
@@ -31,8 +31,8 @@ function CopyButton({ code }: { code: string }) {
31
31
  className="
32
32
  absolute top-2.5 right-2.5
33
33
  p-1.5 rounded-md
34
- bg-zinc-700 hover:bg-zinc-600
35
- text-zinc-400 hover:text-zinc-200
34
+ bg-muted hover:bg-accent
35
+ text-muted-foreground hover:text-foreground
36
36
  transition-colors duration-100
37
37
  opacity-0 group-hover:opacity-100
38
38
  "
@@ -100,7 +100,7 @@ export default function OnboardingView() {
100
100
  key={tpl.id}
101
101
  disabled={isDisabled}
102
102
  onClick={() => handleSelect(tpl.id)}
103
- className="group relative flex flex-col items-start gap-3 p-5 rounded-xl border border-border bg-card text-left transition-all duration-150 hover:border-amber-500/50 hover:bg-amber-500/5 disabled:opacity-60 disabled:cursor-not-allowed"
103
+ className="group relative flex flex-col items-start gap-3 p-5 rounded-xl border border-border bg-card text-left transition-all duration-150 hover:border-[var(--amber)]/50 hover:bg-[var(--amber)]/5 disabled:opacity-60 disabled:cursor-not-allowed"
104
104
  >
105
105
  {/* Icon + title */}
106
106
  <div className="flex items-center gap-2.5 w-full">
@@ -149,7 +149,7 @@ export default function Panel({
149
149
  className="absolute top-0 -right-[3px] w-[6px] h-full cursor-col-resize z-40 group hidden md:block"
150
150
  onMouseDown={handleMouseDown}
151
151
  >
152
- <div className="absolute right-[2px] top-0 w-[2px] h-full opacity-0 group-hover:opacity-100 bg-amber-500/60 transition-opacity" />
152
+ <div className="absolute right-[2px] top-0 w-[2px] h-full opacity-0 group-hover:opacity-100 bg-[var(--amber)]/60 transition-opacity" />
153
153
  </div>
154
154
  )}
155
155
  </aside>
@@ -96,7 +96,7 @@ export default function RightAgentDetailPanel({
96
96
  aria-hidden={!open || !resolved}
97
97
  >
98
98
  {resolved && (
99
- <div className="flex flex-col flex-1 min-h-0 overflow-hidden">
99
+ <div className="flex flex-col flex-1 min-h-0 overflow-y-auto">
100
100
  <AgentsPanelAgentDetail
101
101
  agent={resolved.agent}
102
102
  agentStatus={resolved.status}
@@ -81,7 +81,7 @@ export default function RightAskPanel({
81
81
  className="absolute top-0 -left-[3px] w-[6px] h-full cursor-col-resize z-40 group hidden md:block"
82
82
  onMouseDown={handleMouseDown}
83
83
  >
84
- <div className="absolute left-[2px] top-0 w-[2px] h-full opacity-0 group-hover:opacity-100 bg-amber-500/60 transition-opacity" />
84
+ <div className="absolute left-[2px] top-0 w-[2px] h-full opacity-0 group-hover:opacity-100 bg-[var(--amber)]/60 transition-opacity" />
85
85
  </div>
86
86
  </aside>
87
87
  );
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
 
3
- import { useState, useEffect, useCallback, useRef } from 'react';
3
+ import { useState, useEffect, useCallback, useRef, useLayoutEffect } from 'react';
4
4
  import { useRouter } from 'next/navigation';
5
5
  import { Search, X, FileText, Table } from 'lucide-react';
6
6
  import { SearchResult } from '@/lib/types';
@@ -32,6 +32,7 @@ export default function SearchModal({ open, onClose }: SearchModalProps) {
32
32
  const [loading, setLoading] = useState(false);
33
33
  const [selectedIndex, setSelectedIndex] = useState(0);
34
34
  const inputRef = useRef<HTMLInputElement>(null);
35
+ const resultsRef = useRef<HTMLDivElement>(null);
35
36
  const router = useRouter();
36
37
  const debounceTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
37
38
  const { t } = useLocale();
@@ -99,6 +100,13 @@ export default function SearchModal({ open, onClose }: SearchModalProps) {
99
100
  return () => window.removeEventListener('keydown', handler);
100
101
  }, [open, onClose, results, selectedIndex, navigate]);
101
102
 
103
+ useLayoutEffect(() => {
104
+ const container = resultsRef.current;
105
+ if (!container) return;
106
+ const selected = container.children[selectedIndex] as HTMLElement | undefined;
107
+ selected?.scrollIntoView({ block: 'nearest' });
108
+ }, [selectedIndex]);
109
+
102
110
  if (!open) return null;
103
111
 
104
112
  return (
@@ -134,7 +142,7 @@ export default function SearchModal({ open, onClose }: SearchModalProps) {
134
142
  </div>
135
143
 
136
144
  {/* Results */}
137
- <div className="max-h-[50vh] md:max-h-80 overflow-y-auto flex-1">
145
+ <div ref={resultsRef} className="max-h-[50vh] md:max-h-80 overflow-y-auto flex-1">
138
146
  {results.length === 0 && query && !loading && (
139
147
  <div className="px-4 py-8 text-center text-sm text-muted-foreground">{t.search.noResults}</div>
140
148
  )}