@geminilight/mindos 0.5.64 → 0.5.66
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -0
- package/README_zh.md +4 -0
- package/app/app/api/ask/route.ts +12 -0
- package/app/app/api/file/route.ts +9 -0
- package/app/app/api/mcp/agents/route.ts +27 -1
- package/app/app/api/skills/route.ts +18 -2
- package/app/app/api/tree-version/route.ts +8 -0
- package/app/components/ActivityBar.tsx +2 -2
- package/app/components/Backlinks.tsx +5 -5
- package/app/components/CreateSpaceModal.tsx +3 -2
- package/app/components/DirPicker.tsx +1 -1
- package/app/components/DirView.tsx +2 -3
- package/app/components/EditorWrapper.tsx +3 -3
- package/app/components/FileTree.tsx +25 -10
- package/app/components/GuideCard.tsx +4 -4
- package/app/components/HomeContent.tsx +6 -11
- package/app/components/MarkdownView.tsx +2 -2
- package/app/components/OnboardingView.tsx +1 -1
- package/app/components/Panel.tsx +1 -1
- package/app/components/RightAgentDetailPanel.tsx +1 -1
- package/app/components/RightAskPanel.tsx +1 -1
- package/app/components/SearchModal.tsx +10 -2
- package/app/components/SidebarLayout.tsx +35 -10
- package/app/components/ThemeToggle.tsx +1 -1
- package/app/components/agents/AgentDetailContent.tsx +454 -59
- package/app/components/agents/AgentsContentPage.tsx +70 -5
- package/app/components/agents/AgentsMcpSection.tsx +474 -159
- package/app/components/agents/AgentsOverviewSection.tsx +418 -59
- package/app/components/agents/AgentsPrimitives.tsx +335 -0
- package/app/components/agents/AgentsSkillsSection.tsx +739 -121
- package/app/components/agents/SkillDetailPopover.tsx +416 -0
- package/app/components/agents/agents-content-model.ts +292 -10
- package/app/components/ask/AskContent.tsx +34 -5
- package/app/components/ask/FileChip.tsx +1 -0
- package/app/components/ask/MentionPopover.tsx +13 -1
- package/app/components/ask/MessageList.tsx +5 -7
- package/app/components/ask/ToolCallBlock.tsx +4 -4
- package/app/components/changes/ChangesBanner.tsx +1 -2
- package/app/components/echo/EchoHero.tsx +10 -24
- package/app/components/echo/EchoInsightCollapsible.tsx +52 -43
- package/app/components/echo/EchoPageSections.tsx +13 -9
- package/app/components/echo/EchoSegmentNav.tsx +14 -11
- package/app/components/echo/EchoSegmentPageClient.tsx +64 -43
- package/app/components/explore/ExploreContent.tsx +3 -7
- package/app/components/explore/UseCaseCard.tsx +4 -15
- package/app/components/panels/AgentsPanel.tsx +12 -104
- package/app/components/panels/AgentsPanelAgentDetail.tsx +2 -2
- package/app/components/panels/AgentsPanelAgentGroups.tsx +3 -7
- package/app/components/panels/AgentsPanelAgentListRow.tsx +9 -11
- package/app/components/panels/EchoPanel.tsx +8 -10
- package/app/components/panels/PanelNavRow.tsx +9 -2
- package/app/components/panels/PluginsPanel.tsx +2 -2
- package/app/components/renderers/agent-inspector/AgentInspectorRenderer.tsx +30 -8
- package/app/components/renderers/agent-inspector/manifest.ts +3 -3
- package/app/components/renderers/todo/manifest.ts +1 -0
- package/app/components/settings/AiTab.tsx +3 -3
- package/app/components/settings/AppearanceTab.tsx +2 -2
- package/app/components/settings/KnowledgeTab.tsx +3 -3
- package/app/components/settings/McpAgentInstall.tsx +3 -6
- package/app/components/settings/McpSkillCreateForm.tsx +2 -3
- package/app/components/settings/McpSkillRow.tsx +2 -3
- package/app/components/settings/McpSkillsSection.tsx +2 -2
- package/app/components/settings/McpTab.tsx +12 -13
- package/app/components/settings/MonitoringTab.tsx +13 -13
- package/app/components/settings/PluginsTab.tsx +2 -2
- package/app/components/settings/Primitives.tsx +3 -4
- package/app/components/settings/SettingsContent.tsx +3 -3
- package/app/components/settings/SyncTab.tsx +11 -17
- package/app/components/settings/UpdateTab.tsx +18 -21
- package/app/components/settings/types.ts +14 -0
- package/app/components/setup/StepKB.tsx +1 -1
- package/app/hooks/useMcpData.tsx +4 -2
- package/app/hooks/useMention.ts +25 -8
- package/app/lib/agent/log.ts +15 -18
- package/app/lib/agent/prompt.ts +17 -29
- package/app/lib/agent/stream-consumer.ts +3 -0
- package/app/lib/agent/to-agent-messages.ts +6 -4
- package/app/lib/core/agent-audit-log.ts +280 -0
- package/app/lib/core/index.ts +11 -0
- package/app/lib/fs.ts +9 -0
- package/app/lib/i18n-en.ts +259 -33
- package/app/lib/i18n-zh.ts +258 -32
- package/app/lib/mcp-agents.ts +231 -2
- package/app/lib/types.ts +2 -0
- package/package.json +1 -1
- 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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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}
|
|
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(
|
|
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-
|
|
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
|
-
|
|
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
|
|
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}
|
|
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
|
|
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
|
|
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
|
|
68
|
+
{isDestructive && <AlertTriangle size={11} className="shrink-0 text-[var(--amber)]" />}
|
|
69
69
|
<span>{icon}</span>
|
|
70
|
-
<span className={`font-medium ${isDestructive ? 'text-amber
|
|
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
|
|
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
|
|
3
|
+
import type { ReactNode } from 'react';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
* Echo page hero: kicker,
|
|
7
|
-
*
|
|
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
|
|
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
|
|
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-
|
|
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
|
}
|