@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
@@ -4,7 +4,12 @@ export type AgentsDashboardTab = 'overview' | 'mcp' | 'skills';
4
4
  export type AgentResolvedStatus = 'connected' | 'detected' | 'notFound';
5
5
  export type SkillCapability = 'research' | 'coding' | 'docs' | 'ops' | 'memory';
6
6
  export type SkillSourceFilter = 'all' | 'builtin' | 'user';
7
+ export type UnifiedSourceFilter = 'all' | 'builtin' | 'user' | 'native';
7
8
  export type AgentStatusFilter = 'all' | 'connected' | 'detected' | 'notFound';
9
+ export type AgentTransportFilter = 'all' | 'stdio' | 'http' | 'other';
10
+ export type SkillWorkspaceStatusFilter = 'all' | 'enabled' | 'disabled' | 'attention';
11
+ export type SkillCapabilityFilter = SkillCapability | 'all';
12
+ export type AgentDetailSkillSourceFilter = 'all' | 'builtin' | 'user';
8
13
 
9
14
  export interface RiskItem {
10
15
  id: string;
@@ -38,12 +43,7 @@ export function resolveAgentStatus(agent: AgentInfo): AgentResolvedStatus {
38
43
  }
39
44
 
40
45
  export function capabilityForSkill(skill: SkillInfo): SkillCapability {
41
- const text = `${skill.name} ${skill.description}`.toLowerCase();
42
- if (text.includes('search') || text.includes('research')) return 'research';
43
- if (text.includes('doc') || text.includes('write')) return 'docs';
44
- if (text.includes('deploy') || text.includes('ops') || text.includes('ci')) return 'ops';
45
- if (text.includes('memory') || text.includes('mind')) return 'memory';
46
- return 'coding';
46
+ return capabilityFromText(`${skill.name} ${skill.description}`);
47
47
  }
48
48
 
49
49
  export function groupSkillsByCapability(skills: SkillInfo[]): Record<SkillCapability, SkillInfo[]> {
@@ -56,16 +56,20 @@ export function groupSkillsByCapability(skills: SkillInfo[]): Record<SkillCapabi
56
56
  };
57
57
  }
58
58
 
59
+ function buildBaseRiskItems(args: { mcpRunning: boolean; detectedCount: number; notFoundCount: number }): RiskItem[] {
60
+ const items: RiskItem[] = [];
61
+ if (!args.mcpRunning) items.push({ id: 'mcp-stopped', severity: 'error', title: 'MCP server is not running' });
62
+ if (args.detectedCount > 0) items.push({ id: 'detected-unconfigured', severity: 'warn', title: `${args.detectedCount} detected agent(s) need configuration` });
63
+ return items;
64
+ }
65
+
59
66
  export function buildRiskQueue(args: {
60
67
  mcpRunning: boolean;
61
68
  detectedCount: number;
62
69
  notFoundCount: number;
63
70
  allSkillsDisabled: boolean;
64
71
  }): RiskItem[] {
65
- const items: RiskItem[] = [];
66
- if (!args.mcpRunning) items.push({ id: 'mcp-stopped', severity: 'error', title: 'MCP server is not running' });
67
- if (args.detectedCount > 0) items.push({ id: 'detected-unconfigured', severity: 'warn', title: `${args.detectedCount} detected agent(s) need configuration` });
68
- if (args.notFoundCount > 0) items.push({ id: 'not-found', severity: 'warn', title: `${args.notFoundCount} agent(s) not detected on this machine` });
72
+ const items = buildBaseRiskItems(args);
69
73
  if (args.allSkillsDisabled) items.push({ id: 'skills-disabled', severity: 'warn', title: 'All skills are disabled' });
70
74
  return items;
71
75
  }
@@ -80,6 +84,65 @@ export function filterSkills(skills: SkillInfo[], query: string, source: SkillSo
80
84
  });
81
85
  }
82
86
 
87
+ export function buildSkillAttentionSet(skills: SkillInfo[]): Set<string> {
88
+ return new Set(
89
+ skills
90
+ .filter((skill) => (!skill.enabled && skill.source === 'user') || skill.description.trim().length === 0)
91
+ .map((skill) => skill.name),
92
+ );
93
+ }
94
+
95
+ export function filterSkillsForWorkspace(
96
+ skills: SkillInfo[],
97
+ filters: {
98
+ query: string;
99
+ source: SkillSourceFilter;
100
+ status: SkillWorkspaceStatusFilter;
101
+ capability: SkillCapabilityFilter;
102
+ },
103
+ ): SkillInfo[] {
104
+ const attention = buildSkillAttentionSet(skills);
105
+ const byQueryAndSource = filterSkills(skills, filters.query, filters.source);
106
+ return byQueryAndSource.filter((skill) => {
107
+ if (filters.status === 'enabled' && !skill.enabled) return false;
108
+ if (filters.status === 'disabled' && skill.enabled) return false;
109
+ if (filters.status === 'attention' && !attention.has(skill.name)) return false;
110
+ if (filters.capability !== 'all' && capabilityForSkill(skill) !== filters.capability) return false;
111
+ return true;
112
+ });
113
+ }
114
+
115
+ export function resolveMatrixAgents(agents: AgentInfo[], focusKey: string): AgentInfo[] {
116
+ if (focusKey === 'all') return agents;
117
+ const focused = agents.find((agent) => agent.key === focusKey);
118
+ return focused ? [focused] : [];
119
+ }
120
+
121
+ export function createBulkSkillTogglePlan(skills: SkillInfo[], targetEnabled: boolean): string[] {
122
+ return skills.filter((skill) => skill.enabled !== targetEnabled).map((skill) => skill.name);
123
+ }
124
+
125
+ export interface BulkSkillToggleResult {
126
+ skillName: string;
127
+ ok: boolean;
128
+ reason?: string;
129
+ }
130
+
131
+ export function summarizeBulkSkillToggleResults(results: BulkSkillToggleResult[]): {
132
+ total: number;
133
+ succeeded: number;
134
+ failed: number;
135
+ failedSkills: string[];
136
+ } {
137
+ const failedSkills = results.filter((item) => !item.ok).map((item) => item.skillName);
138
+ return {
139
+ total: results.length,
140
+ succeeded: results.length - failedSkills.length,
141
+ failed: failedSkills.length,
142
+ failedSkills,
143
+ };
144
+ }
145
+
83
146
  export function filterAgentsByStatus(agents: AgentInfo[], status: AgentStatusFilter): AgentInfo[] {
84
147
  if (status === 'all') return agents;
85
148
  return agents.filter((agent) => resolveAgentStatus(agent) === status);
@@ -94,3 +157,222 @@ export function filterAgentsForMcpTable(agents: AgentInfo[], query: string, stat
94
157
  return haystack.includes(q);
95
158
  });
96
159
  }
160
+
161
+ export function resolveAgentTransport(agent: AgentInfo): AgentTransportFilter {
162
+ const transport = (agent.transport ?? agent.preferredTransport ?? '').toLowerCase();
163
+ if (transport === 'stdio' || transport === 'http') return transport;
164
+ return 'other';
165
+ }
166
+
167
+ export function filterAgentsForMcpWorkspace(
168
+ agents: AgentInfo[],
169
+ filters: { query: string; status: AgentStatusFilter; transport: AgentTransportFilter },
170
+ ): AgentInfo[] {
171
+ const q = filters.query.trim().toLowerCase();
172
+ return agents.filter((agent) => {
173
+ if (filters.status !== 'all' && resolveAgentStatus(agent) !== filters.status) return false;
174
+ if (filters.transport !== 'all' && resolveAgentTransport(agent) !== filters.transport) return false;
175
+ if (!q) return true;
176
+ const haystack = `${agent.name} ${agent.key} ${agent.configPath ?? ''}`.toLowerCase();
177
+ return haystack.includes(q);
178
+ });
179
+ }
180
+
181
+ export function buildMcpRiskQueue(args: {
182
+ mcpRunning: boolean;
183
+ detectedCount: number;
184
+ notFoundCount: number;
185
+ }): RiskItem[] {
186
+ return buildBaseRiskItems(args);
187
+ }
188
+
189
+ export interface McpBulkReconnectResult {
190
+ agentKey: string;
191
+ ok: boolean;
192
+ }
193
+
194
+ export function summarizeMcpBulkReconnectResults(results: McpBulkReconnectResult[]): {
195
+ total: number;
196
+ succeeded: number;
197
+ failed: number;
198
+ } {
199
+ const failed = results.filter((item) => !item.ok).length;
200
+ return {
201
+ total: results.length,
202
+ succeeded: results.length - failed,
203
+ failed,
204
+ };
205
+ }
206
+
207
+ export interface CrossAgentMcpServer {
208
+ serverName: string;
209
+ agents: string[];
210
+ }
211
+
212
+ export function aggregateCrossAgentMcpServers(agents: AgentInfo[]): CrossAgentMcpServer[] {
213
+ const map = new Map<string, string[]>();
214
+ for (const agent of agents) {
215
+ for (const server of agent.configuredMcpServers ?? []) {
216
+ const existing = map.get(server);
217
+ if (existing) existing.push(agent.name);
218
+ else map.set(server, [agent.name]);
219
+ }
220
+ }
221
+ return [...map.entries()]
222
+ .map(([serverName, agentNames]) => ({ serverName, agents: agentNames }))
223
+ .sort((a, b) => b.agents.length - a.agents.length || a.serverName.localeCompare(b.serverName));
224
+ }
225
+
226
+ export interface CrossAgentSkill {
227
+ skillName: string;
228
+ agents: string[];
229
+ }
230
+
231
+ export function aggregateCrossAgentSkills(agents: AgentInfo[]): CrossAgentSkill[] {
232
+ const map = new Map<string, string[]>();
233
+ for (const agent of agents) {
234
+ for (const skill of agent.installedSkillNames ?? []) {
235
+ const existing = map.get(skill);
236
+ if (existing) existing.push(agent.name);
237
+ else map.set(skill, [agent.name]);
238
+ }
239
+ }
240
+ return [...map.entries()]
241
+ .map(([skillName, agentNames]) => ({ skillName, agents: agentNames }))
242
+ .sort((a, b) => b.agents.length - a.agents.length || a.skillName.localeCompare(b.skillName));
243
+ }
244
+
245
+ const STATUS_ORDER: Record<AgentResolvedStatus, number> = { connected: 0, detected: 1, notFound: 2 };
246
+
247
+ export function sortAgentsByStatus(agents: AgentInfo[]): AgentInfo[] {
248
+ return [...agents].sort((a, b) => {
249
+ const sa = STATUS_ORDER[resolveAgentStatus(a)];
250
+ const sb = STATUS_ORDER[resolveAgentStatus(b)];
251
+ if (sa !== sb) return sa - sb;
252
+ return a.name.localeCompare(b.name);
253
+ });
254
+ }
255
+
256
+ export function filterSkillsForAgentDetail(
257
+ skills: SkillInfo[],
258
+ filters: { query: string; source: AgentDetailSkillSourceFilter },
259
+ ): SkillInfo[] {
260
+ const q = filters.query.trim().toLowerCase();
261
+ return skills.filter((skill) => {
262
+ if (filters.source !== 'all' && skill.source !== filters.source) return false;
263
+ if (!q) return true;
264
+ const haystack = `${skill.name} ${skill.description} ${skill.path}`.toLowerCase();
265
+ return haystack.includes(q);
266
+ });
267
+ }
268
+
269
+ /* ────────── Unified Skill List (MindOS + Native) ────────── */
270
+
271
+ export interface UnifiedSkillItem {
272
+ name: string;
273
+ kind: 'mindos' | 'native';
274
+ mindosSkill?: SkillInfo;
275
+ agents: string[];
276
+ capability: SkillCapability;
277
+ enabled: boolean;
278
+ source: 'builtin' | 'user' | 'native';
279
+ description: string;
280
+ }
281
+
282
+ export function capabilityFromText(text: string): SkillCapability {
283
+ const lower = text.toLowerCase();
284
+ if (lower.includes('search') || lower.includes('research') || lower.includes('arxiv') || lower.includes('paper')) return 'research';
285
+ if (lower.includes('doc') || lower.includes('write') || lower.includes('readme') || lower.includes('copy') || lower.includes('slide') || lower.includes('ppt')) return 'docs';
286
+ if (lower.includes('deploy') || lower.includes('ops') || lower.includes('ci') || lower.includes('ship') || lower.includes('qa') || lower.includes('review') || lower.includes('test')) return 'ops';
287
+ if (lower.includes('memory') || lower.includes('mind') || lower.includes('handoff') || lower.includes('session')) return 'memory';
288
+ return 'coding';
289
+ }
290
+
291
+ export function buildUnifiedSkillList(
292
+ mindosSkills: SkillInfo[],
293
+ crossAgentSkills: CrossAgentSkill[],
294
+ ): UnifiedSkillItem[] {
295
+ const mindosNames = new Set(mindosSkills.map((s) => s.name));
296
+ const crossMap = new Map<string, string[]>();
297
+ for (const cs of crossAgentSkills) crossMap.set(cs.skillName, cs.agents);
298
+
299
+ const result: UnifiedSkillItem[] = [];
300
+
301
+ for (const skill of mindosSkills) {
302
+ result.push({
303
+ name: skill.name,
304
+ kind: 'mindos',
305
+ mindosSkill: skill,
306
+ agents: crossMap.get(skill.name) ?? [],
307
+ capability: capabilityForSkill(skill),
308
+ enabled: skill.enabled,
309
+ source: skill.source,
310
+ description: skill.description,
311
+ });
312
+ }
313
+
314
+ for (const cs of crossAgentSkills) {
315
+ if (mindosNames.has(cs.skillName)) continue;
316
+ result.push({
317
+ name: cs.skillName,
318
+ kind: 'native',
319
+ agents: cs.agents,
320
+ capability: capabilityFromText(cs.skillName),
321
+ enabled: true,
322
+ source: 'native',
323
+ description: '',
324
+ });
325
+ }
326
+
327
+ return result;
328
+ }
329
+
330
+ export function groupUnifiedSkills(skills: UnifiedSkillItem[]): Record<SkillCapability, UnifiedSkillItem[]> {
331
+ return {
332
+ research: skills.filter((s) => s.capability === 'research'),
333
+ coding: skills.filter((s) => s.capability === 'coding'),
334
+ docs: skills.filter((s) => s.capability === 'docs'),
335
+ ops: skills.filter((s) => s.capability === 'ops'),
336
+ memory: skills.filter((s) => s.capability === 'memory'),
337
+ };
338
+ }
339
+
340
+ export function filterUnifiedSkills(
341
+ skills: UnifiedSkillItem[],
342
+ filters: {
343
+ query: string;
344
+ source: UnifiedSourceFilter;
345
+ status: SkillWorkspaceStatusFilter;
346
+ capability: SkillCapabilityFilter;
347
+ },
348
+ ): UnifiedSkillItem[] {
349
+ const q = filters.query.trim().toLowerCase();
350
+ const attentionSet = new Set(
351
+ skills
352
+ .filter((s) => s.kind === 'mindos' && ((!s.enabled && s.source === 'user') || s.description.trim().length === 0))
353
+ .map((s) => s.name),
354
+ );
355
+
356
+ return skills.filter((skill) => {
357
+ if (filters.source !== 'all') {
358
+ if (filters.source === 'native' && skill.kind !== 'native') return false;
359
+ if (filters.source === 'builtin' && skill.source !== 'builtin') return false;
360
+ if (filters.source === 'user' && skill.source !== 'user') return false;
361
+ }
362
+ if (filters.status === 'enabled' && !skill.enabled) return false;
363
+ if (filters.status === 'disabled' && skill.enabled) return false;
364
+ if (filters.status === 'attention' && !attentionSet.has(skill.name)) return false;
365
+ if (filters.capability !== 'all' && skill.capability !== filters.capability) return false;
366
+ if (q) {
367
+ const haystack = `${skill.name} ${skill.description}`.toLowerCase();
368
+ if (!haystack.includes(q)) return false;
369
+ }
370
+ return true;
371
+ });
372
+ }
373
+
374
+ export function createBulkUnifiedTogglePlan(skills: UnifiedSkillItem[], targetEnabled: boolean): string[] {
375
+ return skills
376
+ .filter((s) => s.kind === 'mindos' && s.enabled !== targetEnabled)
377
+ .map((s) => s.name);
378
+ }
@@ -171,6 +171,7 @@ export default function AskContent({ visible, currentFile, initialMessage, onFir
171
171
  const [loadingPhase, setLoadingPhase] = useState<'connecting' | 'thinking' | 'streaming'>('connecting');
172
172
  const [attachedFiles, setAttachedFiles] = useState<string[]>([]);
173
173
  const [showHistory, setShowHistory] = useState(false);
174
+ const [isDragOver, setIsDragOver] = useState(false);
174
175
 
175
176
  const session = useAskSession(currentFile);
176
177
  const upload = useFileUpload();
@@ -295,9 +296,11 @@ export default function AskContent({ visible, currentFile, initialMessage, onFir
295
296
  const text = input.trim();
296
297
  if (!text || isLoading) return;
297
298
 
298
- const userMsg: Message = { role: 'user', content: text };
299
+ // Attach current timestamp so backend knows EXACTLY when the user typed this message
300
+ const userMsg: Message = { role: 'user', content: text, timestamp: Date.now() };
299
301
  const requestMessages = [...session.messages, userMsg];
300
- session.setMessages([...requestMessages, { role: 'assistant', content: '' }]);
302
+ // And for the incoming assistant response, give it an initial timestamp
303
+ session.setMessages([...requestMessages, { role: 'assistant', content: '', timestamp: Date.now() }]);
301
304
  setInput('');
302
305
  if (onFirstMessage && !firstMessageFired.current) {
303
306
  firstMessageFired.current = true;
@@ -409,6 +412,25 @@ export default function AskContent({ visible, currentFile, initialMessage, onFir
409
412
  setTimeout(() => inputRef.current?.focus(), 0);
410
413
  }, [isLoading, currentFile, session, upload, mention]);
411
414
 
415
+ const handleDragOver = useCallback((e: React.DragEvent) => {
416
+ if (e.dataTransfer.types.includes('text/mindos-path')) {
417
+ e.preventDefault();
418
+ e.dataTransfer.dropEffect = 'copy';
419
+ setIsDragOver(true);
420
+ }
421
+ }, []);
422
+
423
+ const handleDragLeave = useCallback(() => setIsDragOver(false), []);
424
+
425
+ const handleDrop = useCallback((e: React.DragEvent) => {
426
+ e.preventDefault();
427
+ setIsDragOver(false);
428
+ const filePath = e.dataTransfer.getData('text/mindos-path');
429
+ if (filePath && !attachedFiles.includes(filePath)) {
430
+ setAttachedFiles(prev => [...prev, filePath]);
431
+ }
432
+ }, [attachedFiles]);
433
+
412
434
  const handleLoadSession = useCallback((id: string) => {
413
435
  session.loadSession(id);
414
436
  setShowHistory(false);
@@ -430,7 +452,7 @@ export default function AskContent({ visible, currentFile, initialMessage, onFir
430
452
  <div className="absolute top-2 left-1/2 -translate-x-1/2 w-8 h-1 rounded-full bg-muted-foreground/20 md:hidden" />
431
453
  )}
432
454
  <div className="flex items-center gap-2 text-sm font-medium text-foreground">
433
- <Sparkles size={isPanel ? 14 : 15} style={{ color: 'var(--amber)' }} />
455
+ <Sparkles size={isPanel ? 14 : 15} className="text-[var(--amber)]" />
434
456
  <span className={isPanel ? 'font-display text-xs uppercase tracking-wider text-muted-foreground' : 'font-display'}>
435
457
  {isPanel ? 'MindOS Agent' : t.ask.title}
436
458
  </span>
@@ -489,8 +511,15 @@ export default function AskContent({ visible, currentFile, initialMessage, onFir
489
511
 
490
512
  {/* Input area — panel: fixed-height shell + top drag handle (persisted); modal: simple block */}
491
513
  <div
492
- className={cn('shrink-0 border-t border-border', isPanel && 'flex flex-col overflow-hidden bg-card')}
514
+ className={cn(
515
+ 'shrink-0 border-t border-border',
516
+ isPanel && 'flex flex-col overflow-hidden bg-card',
517
+ isDragOver && 'ring-2 ring-[var(--amber)] ring-inset bg-[var(--amber-dim)]',
518
+ )}
493
519
  style={isPanel ? { height: panelComposerHeight } : undefined}
520
+ onDragOver={handleDragOver}
521
+ onDragLeave={handleDragLeave}
522
+ onDrop={handleDrop}
494
523
  >
495
524
  {isPanel ? (
496
525
  <div
@@ -514,7 +543,7 @@ export default function AskContent({ visible, currentFile, initialMessage, onFir
514
543
  </div>
515
544
  ) : null}
516
545
 
517
- <div className={cn(isPanel && 'flex min-h-0 flex-1 flex-col overflow-hidden')}>
546
+ <div className={cn(isPanel && 'flex min-h-0 flex-1 flex-col overflow-y-auto')}>
518
547
  {attachedFiles.length > 0 && (
519
548
  <div className={cn('shrink-0', isPanel ? 'px-3 pt-2 pb-1' : 'px-4 pt-2.5 pb-1')}>
520
549
  <div className={`text-muted-foreground/70 mb-1 ${isPanel ? 'text-[10px]' : 'text-xs'}`}>
@@ -21,6 +21,7 @@ export default function FileChip({ path, onRemove, variant = 'kb' }: FileChipPro
21
21
  <button
22
22
  type="button"
23
23
  onClick={onRemove}
24
+ aria-label={`Remove ${name}`}
24
25
  className="text-muted-foreground hover:text-foreground ml-0.5 shrink-0"
25
26
  >
26
27
  <X size={10} />
@@ -1,5 +1,6 @@
1
1
  'use client';
2
2
 
3
+ import { useEffect, useRef } from 'react';
3
4
  import { FileText, Table } from 'lucide-react';
4
5
 
5
6
  interface MentionPopoverProps {
@@ -9,10 +10,20 @@ interface MentionPopoverProps {
9
10
  }
10
11
 
11
12
  export default function MentionPopover({ results, selectedIndex, onSelect }: MentionPopoverProps) {
13
+ const listRef = useRef<HTMLDivElement>(null);
14
+
15
+ useEffect(() => {
16
+ const container = listRef.current;
17
+ if (!container) return;
18
+ const selected = container.children[selectedIndex] as HTMLElement | undefined;
19
+ selected?.scrollIntoView({ block: 'nearest' });
20
+ }, [selectedIndex]);
21
+
12
22
  if (results.length === 0) return null;
13
23
 
14
24
  return (
15
25
  <div className="mx-4 mb-1 border border-border rounded-lg bg-card shadow-lg overflow-hidden">
26
+ <div ref={listRef} className="max-h-[240px] overflow-y-auto">
16
27
  {results.map((f, idx) => {
17
28
  const name = f.split('/').pop() ?? f;
18
29
  const isCsv = name.endsWith('.csv');
@@ -42,7 +53,8 @@ export default function MentionPopover({ results, selectedIndex, onSelect }: Men
42
53
  </button>
43
54
  );
44
55
  })}
45
- <div className="px-3 py-1.5 border-t border-border flex gap-3 text-2xs text-muted-foreground/50">
56
+ </div>
57
+ <div className="px-3 py-1.5 border-t border-border flex gap-3 text-2xs text-muted-foreground/50 shrink-0">
46
58
  <span>↑↓ navigate</span>
47
59
  <span>↵ select</span>
48
60
  <span>ESC dismiss</span>
@@ -63,7 +63,7 @@ function AssistantMessageWithParts({ message, isStreaming }: { message: Message;
63
63
  })}
64
64
  {showTrailingSpinner && (
65
65
  <div className="flex items-center gap-2 py-1 mt-1">
66
- <Loader2 size={12} className="animate-spin" style={{ color: 'var(--amber)' }} />
66
+ <Loader2 size={12} className="animate-spin text-[var(--amber)]" />
67
67
  <span className="text-xs text-muted-foreground animate-pulse">Executing tool…</span>
68
68
  </div>
69
69
  )}
@@ -137,16 +137,14 @@ export default function MessageList({
137
137
  <div key={i} className={`flex gap-3 ${m.role === 'user' ? 'justify-end' : 'justify-start'}`}>
138
138
  {m.role === 'assistant' && (
139
139
  <div
140
- className="w-6 h-6 rounded-full flex items-center justify-center shrink-0 mt-0.5"
141
- style={{ background: 'var(--amber-dim)' }}
140
+ className="w-6 h-6 rounded-full flex items-center justify-center shrink-0 mt-0.5 bg-[var(--amber-dim)]"
142
141
  >
143
- <Sparkles size={12} style={{ color: 'var(--amber)' }} />
142
+ <Sparkles size={12} className="text-[var(--amber)]" />
144
143
  </div>
145
144
  )}
146
145
  {m.role === 'user' ? (
147
146
  <div
148
- className="max-w-[85%] px-3 py-2 rounded-xl rounded-br-sm text-sm leading-relaxed whitespace-pre-wrap"
149
- style={{ background: 'var(--amber)', color: 'var(--amber-foreground)' }}
147
+ className="max-w-[85%] px-3 py-2 rounded-xl rounded-br-sm text-sm leading-relaxed whitespace-pre-wrap bg-[var(--amber)] text-[var(--amber-foreground)]"
150
148
  >
151
149
  {m.content}
152
150
  </div>
@@ -168,7 +166,7 @@ export default function MessageList({
168
166
  </>
169
167
  ) : isLoading && i === messages.length - 1 ? (
170
168
  <div className="flex items-center gap-2 py-1">
171
- <Loader2 size={14} className="animate-spin" style={{ color: 'var(--amber)' }} />
169
+ <Loader2 size={14} className="animate-spin text-[var(--amber)]" />
172
170
  <span className="text-xs text-muted-foreground animate-pulse">
173
171
  {loadingPhase === 'connecting'
174
172
  ? labels.connecting
@@ -56,7 +56,7 @@ export default function ToolCallBlock({ part }: { part: ToolCallPart }) {
56
56
  return (
57
57
  <div className={`my-1 rounded-md border text-xs font-mono ${
58
58
  isDestructive
59
- ? 'border-amber-500/30 bg-amber-500/5'
59
+ ? 'border-[var(--amber)]/30 bg-[var(--amber)]/5'
60
60
  : 'border-border/50 bg-muted/30'
61
61
  }`}>
62
62
  <button
@@ -65,13 +65,13 @@ export default function ToolCallBlock({ part }: { part: ToolCallPart }) {
65
65
  className="w-full flex items-center gap-1.5 px-2 py-1.5 text-left hover:bg-muted/50 transition-colors rounded-md"
66
66
  >
67
67
  {expanded ? <ChevronDown size={12} className="shrink-0 text-muted-foreground" /> : <ChevronRight size={12} className="shrink-0 text-muted-foreground" />}
68
- {isDestructive && <AlertTriangle size={11} className="shrink-0 text-amber-500" />}
68
+ {isDestructive && <AlertTriangle size={11} className="shrink-0 text-[var(--amber)]" />}
69
69
  <span>{icon}</span>
70
- <span className={`font-medium ${isDestructive ? 'text-amber-600 dark:text-amber-400' : 'text-foreground'}`}>{part.toolName}</span>
70
+ <span className={`font-medium ${isDestructive ? 'text-[var(--amber)]' : 'text-foreground'}`}>{part.toolName}</span>
71
71
  <span className="text-muted-foreground truncate flex-1">({inputSummary})</span>
72
72
  <span className="shrink-0 ml-auto">
73
73
  {part.state === 'pending' || part.state === 'running' ? (
74
- <Loader2 size={12} className="animate-spin text-amber-500" />
74
+ <Loader2 size={12} className="animate-spin text-[var(--amber)]" />
75
75
  ) : part.state === 'done' ? (
76
76
  <CheckCircle2 size={12} className="text-success" />
77
77
  ) : (
@@ -88,8 +88,7 @@ export default function ChangesBanner() {
88
88
  >
89
89
  <div className="flex items-start gap-2.5">
90
90
  <span
91
- className="inline-flex h-7 w-7 items-center justify-center rounded-full text-[var(--amber)]"
92
- style={{ background: 'color-mix(in srgb, var(--amber) 18%, transparent)' }}
91
+ className="inline-flex h-7 w-7 items-center justify-center rounded-full text-[var(--amber)] bg-[var(--amber-subtle)]"
93
92
  >
94
93
  <History size={14} />
95
94
  </span>
@@ -1,55 +1,41 @@
1
1
  'use client';
2
2
 
3
- import Link from 'next/link';
3
+ import type { ReactNode } from 'react';
4
4
 
5
5
  /**
6
- * Echo page hero: kicker, minimal breadcrumb (parent only h1 holds the section title),
7
- * lead. Avoids repeating the current segment name in both breadcrumb and h1.
6
+ * Echo page hero: kicker, h1, lead, and optional embedded children (e.g. segment nav).
7
+ * The accent bar highlights the text zone; children sit below it inside the card.
8
8
  */
9
9
  export function EchoHero({
10
- breadcrumbNav,
11
- parentHref,
12
- parent,
13
10
  heroKicker,
14
11
  pageTitle,
15
12
  lead,
16
13
  titleId,
14
+ children,
17
15
  }: {
18
- breadcrumbNav: string;
19
- parentHref: string;
20
- parent: string;
21
16
  heroKicker: string;
22
17
  pageTitle: string;
23
18
  lead: string;
24
19
  titleId: string;
20
+ children?: ReactNode;
25
21
  }) {
26
22
  return (
27
- <header className="relative overflow-hidden rounded-xl border border-border bg-card px-5 py-6 shadow-sm sm:px-8 sm:py-8">
23
+ <header className="relative overflow-hidden rounded-xl border border-border bg-card px-5 pb-5 pt-6 shadow-sm sm:px-8 sm:pb-6 sm:pt-8">
28
24
  <div
29
- className="absolute bottom-5 left-0 top-5 w-0.5 rounded-full bg-[var(--amber)] sm:bottom-6 sm:top-6"
25
+ className="absolute left-0 top-5 w-[3px] rounded-full bg-[var(--amber)] sm:top-6"
26
+ style={{ bottom: children ? '40%' : '1.25rem' }}
30
27
  aria-hidden
31
28
  />
32
29
  <div className="relative pl-4 sm:pl-5">
33
- <p className="mb-3 font-sans text-2xs font-semibold uppercase tracking-[0.2em] text-[var(--amber)]">
30
+ <p className="mb-4 font-sans text-2xs font-semibold uppercase tracking-[0.2em] text-[var(--amber)]">
34
31
  {heroKicker}
35
32
  </p>
36
- <nav aria-label={breadcrumbNav} className="mb-5 font-sans text-sm">
37
- <ol className="m-0 list-none p-0">
38
- <li>
39
- <Link
40
- href={parentHref}
41
- className="text-muted-foreground transition-colors duration-150 hover:text-[var(--amber)] focus-visible:rounded-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
42
- >
43
- {parent}
44
- </Link>
45
- </li>
46
- </ol>
47
- </nav>
48
33
  <h1 id={titleId} className="font-display text-2xl font-semibold tracking-tight text-foreground md:text-3xl">
49
34
  {pageTitle}
50
35
  </h1>
51
36
  <p className="mt-3 max-w-prose font-sans text-base leading-relaxed text-muted-foreground">{lead}</p>
52
37
  </div>
38
+ {children}
53
39
  </header>
54
40
  );
55
41
  }