@geminilight/mindos 0.5.63 → 0.5.65
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -0
- package/README_zh.md +4 -0
- package/app/app/api/ask/route.ts +12 -0
- package/app/app/api/changes/route.ts +7 -1
- package/app/app/api/file/route.ts +9 -0
- package/app/app/api/mcp/agents/route.ts +27 -1
- package/app/app/api/mcp/install-skill/route.ts +9 -24
- package/app/app/api/skills/route.ts +18 -2
- package/app/app/api/tree-version/route.ts +8 -0
- package/app/app/layout.tsx +1 -0
- package/app/app/page.tsx +1 -2
- package/app/app/view/[...path]/ViewPageClient.tsx +0 -1
- 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 +44 -14
- 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 +2 -1
- package/app/components/RightAskPanel.tsx +1 -1
- package/app/components/SearchModal.tsx +10 -2
- package/app/components/SidebarLayout.tsx +36 -10
- package/app/components/ThemeToggle.tsx +1 -1
- package/app/components/agents/AgentDetailContent.tsx +454 -59
- package/app/components/agents/AgentsContentPage.tsx +89 -20
- package/app/components/agents/AgentsMcpSection.tsx +513 -85
- package/app/components/agents/AgentsOverviewSection.tsx +418 -59
- package/app/components/agents/AgentsPrimitives.tsx +335 -0
- package/app/components/agents/AgentsSkillsSection.tsx +746 -105
- package/app/components/agents/SkillDetailPopover.tsx +416 -0
- package/app/components/agents/agents-content-model.ts +308 -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 +89 -13
- package/app/components/changes/ChangesContentPage.tsx +134 -51
- 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 +22 -128
- package/app/components/panels/AgentsPanelAgentDetail.tsx +7 -6
- package/app/components/panels/AgentsPanelAgentGroups.tsx +8 -13
- package/app/components/panels/AgentsPanelAgentListRow.tsx +39 -16
- package/app/components/panels/AgentsPanelHubNav.tsx +12 -12
- package/app/components/panels/EchoPanel.tsx +8 -10
- package/app/components/panels/PanelNavRow.tsx +9 -2
- package/app/components/panels/PluginsPanel.tsx +5 -5
- package/app/components/renderers/agent-inspector/AgentInspectorRenderer.tsx +30 -8
- package/app/components/renderers/agent-inspector/manifest.ts +5 -3
- package/app/components/renderers/config/manifest.ts +1 -0
- package/app/components/renderers/csv/manifest.ts +1 -0
- 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 +6 -5
- 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 +7 -4
- package/app/hooks/useMention.ts +25 -8
- package/app/lib/agent/log.ts +15 -18
- 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/content-changes.ts +148 -8
- package/app/lib/core/index.ts +11 -0
- package/app/lib/fs.ts +16 -1
- package/app/lib/i18n-en.ts +317 -36
- package/app/lib/i18n-zh.ts +316 -35
- package/app/lib/mcp-agents.ts +273 -2
- package/app/lib/renderers/index.ts +1 -2
- package/app/lib/renderers/registry.ts +10 -0
- package/app/lib/types.ts +2 -0
- package/app/next-env.d.ts +1 -1
- package/bin/lib/mcp-agents.js +38 -13
- package/package.json +1 -1
- package/scripts/migrate-agent-audit-log.js +170 -0
- package/scripts/migrate-agent-diff.js +146 -0
- package/scripts/setup.js +12 -17
- package/skills/plugin-core-builtin-migration/SKILL.md +178 -0
- package/app/components/renderers/diff/DiffRenderer.tsx +0 -311
- package/app/components/renderers/diff/manifest.ts +0 -14
|
@@ -4,6 +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';
|
|
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';
|
|
7
13
|
|
|
8
14
|
export interface RiskItem {
|
|
9
15
|
id: string;
|
|
@@ -37,12 +43,7 @@ export function resolveAgentStatus(agent: AgentInfo): AgentResolvedStatus {
|
|
|
37
43
|
}
|
|
38
44
|
|
|
39
45
|
export function capabilityForSkill(skill: SkillInfo): SkillCapability {
|
|
40
|
-
|
|
41
|
-
if (text.includes('search') || text.includes('research')) return 'research';
|
|
42
|
-
if (text.includes('doc') || text.includes('write')) return 'docs';
|
|
43
|
-
if (text.includes('deploy') || text.includes('ops') || text.includes('ci')) return 'ops';
|
|
44
|
-
if (text.includes('memory') || text.includes('mind')) return 'memory';
|
|
45
|
-
return 'coding';
|
|
46
|
+
return capabilityFromText(`${skill.name} ${skill.description}`);
|
|
46
47
|
}
|
|
47
48
|
|
|
48
49
|
export function groupSkillsByCapability(skills: SkillInfo[]): Record<SkillCapability, SkillInfo[]> {
|
|
@@ -55,16 +56,20 @@ export function groupSkillsByCapability(skills: SkillInfo[]): Record<SkillCapabi
|
|
|
55
56
|
};
|
|
56
57
|
}
|
|
57
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
|
+
|
|
58
66
|
export function buildRiskQueue(args: {
|
|
59
67
|
mcpRunning: boolean;
|
|
60
68
|
detectedCount: number;
|
|
61
69
|
notFoundCount: number;
|
|
62
70
|
allSkillsDisabled: boolean;
|
|
63
71
|
}): RiskItem[] {
|
|
64
|
-
const items
|
|
65
|
-
if (!args.mcpRunning) items.push({ id: 'mcp-stopped', severity: 'error', title: 'MCP server is not running' });
|
|
66
|
-
if (args.detectedCount > 0) items.push({ id: 'detected-unconfigured', severity: 'warn', title: `${args.detectedCount} detected agent(s) need configuration` });
|
|
67
|
-
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);
|
|
68
73
|
if (args.allSkillsDisabled) items.push({ id: 'skills-disabled', severity: 'warn', title: 'All skills are disabled' });
|
|
69
74
|
return items;
|
|
70
75
|
}
|
|
@@ -78,3 +83,296 @@ export function filterSkills(skills: SkillInfo[], query: string, source: SkillSo
|
|
|
78
83
|
return haystack.includes(q);
|
|
79
84
|
});
|
|
80
85
|
}
|
|
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
|
+
|
|
146
|
+
export function filterAgentsByStatus(agents: AgentInfo[], status: AgentStatusFilter): AgentInfo[] {
|
|
147
|
+
if (status === 'all') return agents;
|
|
148
|
+
return agents.filter((agent) => resolveAgentStatus(agent) === status);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function filterAgentsForMcpTable(agents: AgentInfo[], query: string, status: AgentStatusFilter): AgentInfo[] {
|
|
152
|
+
const q = query.trim().toLowerCase();
|
|
153
|
+
const byStatus = filterAgentsByStatus(agents, status);
|
|
154
|
+
if (!q) return byStatus;
|
|
155
|
+
return byStatus.filter((agent) => {
|
|
156
|
+
const haystack = `${agent.name} ${agent.key} ${agent.configPath ?? ''}`.toLowerCase();
|
|
157
|
+
return haystack.includes(q);
|
|
158
|
+
});
|
|
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
|
) : (
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import Link from 'next/link';
|
|
4
|
-
import { useEffect, useState } from 'react';
|
|
4
|
+
import { useEffect, useMemo, useState } from 'react';
|
|
5
|
+
import { usePathname } from 'next/navigation';
|
|
6
|
+
import { History, X } from 'lucide-react';
|
|
5
7
|
import { apiFetch } from '@/lib/api';
|
|
8
|
+
import { useLocale } from '@/lib/LocaleContext';
|
|
6
9
|
|
|
7
10
|
interface ChangeSummaryPayload {
|
|
8
11
|
unreadCount: number;
|
|
@@ -10,6 +13,11 @@ interface ChangeSummaryPayload {
|
|
|
10
13
|
|
|
11
14
|
export default function ChangesBanner() {
|
|
12
15
|
const [unreadCount, setUnreadCount] = useState(0);
|
|
16
|
+
const [dismissedAtCount, setDismissedAtCount] = useState<number | null>(null);
|
|
17
|
+
const [isRendered, setIsRendered] = useState(false);
|
|
18
|
+
const [isVisible, setIsVisible] = useState(false);
|
|
19
|
+
const pathname = usePathname();
|
|
20
|
+
const { t } = useLocale();
|
|
13
21
|
|
|
14
22
|
useEffect(() => {
|
|
15
23
|
let active = true;
|
|
@@ -29,21 +37,89 @@ export default function ChangesBanner() {
|
|
|
29
37
|
};
|
|
30
38
|
}, []);
|
|
31
39
|
|
|
32
|
-
|
|
40
|
+
const shouldShow = useMemo(() => {
|
|
41
|
+
if (unreadCount <= 0) return false;
|
|
42
|
+
if (pathname?.startsWith('/changes')) return false;
|
|
43
|
+
if (dismissedAtCount !== null && unreadCount <= dismissedAtCount) return false;
|
|
44
|
+
return true;
|
|
45
|
+
}, [dismissedAtCount, pathname, unreadCount]);
|
|
46
|
+
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
const durationMs = 160;
|
|
49
|
+
if (shouldShow) {
|
|
50
|
+
setIsRendered(true);
|
|
51
|
+
const raf = requestAnimationFrame(() => setIsVisible(true));
|
|
52
|
+
return () => cancelAnimationFrame(raf);
|
|
53
|
+
}
|
|
54
|
+
setIsVisible(false);
|
|
55
|
+
const timer = setTimeout(() => setIsRendered(false), durationMs);
|
|
56
|
+
return () => clearTimeout(timer);
|
|
57
|
+
}, [shouldShow]);
|
|
58
|
+
|
|
59
|
+
async function handleMarkAllRead() {
|
|
60
|
+
try {
|
|
61
|
+
await apiFetch('/api/changes', {
|
|
62
|
+
method: 'POST',
|
|
63
|
+
headers: { 'Content-Type': 'application/json' },
|
|
64
|
+
body: JSON.stringify({ op: 'mark_seen' }),
|
|
65
|
+
});
|
|
66
|
+
} catch {
|
|
67
|
+
// Keep UI resilient; polling will recover server state.
|
|
68
|
+
} finally {
|
|
69
|
+
setUnreadCount(0);
|
|
70
|
+
setDismissedAtCount(0);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (!isRendered) return null;
|
|
33
75
|
|
|
34
76
|
return (
|
|
35
|
-
<div
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
77
|
+
<div
|
|
78
|
+
className={`fixed right-3 top-[60px] md:right-6 md:top-4 z-30 transition-all duration-150 ease-out ${
|
|
79
|
+
isVisible ? 'opacity-100 translate-y-0 scale-100' : 'opacity-0 -translate-y-2 scale-[0.98] pointer-events-none'
|
|
80
|
+
}`}
|
|
81
|
+
>
|
|
82
|
+
<div
|
|
83
|
+
className="rounded-2xl border bg-card/95 backdrop-blur px-3 py-2.5 shadow-lg min-w-[260px] max-w-[350px]"
|
|
84
|
+
style={{
|
|
85
|
+
borderColor: 'color-mix(in srgb, var(--amber) 45%, var(--border))',
|
|
86
|
+
boxShadow: '0 12px 28px color-mix(in srgb, var(--amber) 14%, rgba(0,0,0,.24))',
|
|
87
|
+
}}
|
|
88
|
+
>
|
|
89
|
+
<div className="flex items-start gap-2.5">
|
|
90
|
+
<span
|
|
91
|
+
className="inline-flex h-7 w-7 items-center justify-center rounded-full text-[var(--amber)] bg-[var(--amber-subtle)]"
|
|
92
|
+
>
|
|
93
|
+
<History size={14} />
|
|
94
|
+
</span>
|
|
95
|
+
<div className="min-w-0 flex-1">
|
|
96
|
+
<p className="text-xs text-foreground font-display whitespace-nowrap">
|
|
97
|
+
{t.changes.unreadBanner(unreadCount)}
|
|
98
|
+
</p>
|
|
99
|
+
<div className="mt-1 flex items-center gap-1.5">
|
|
100
|
+
<Link
|
|
101
|
+
href="/changes"
|
|
102
|
+
className="inline-flex items-center rounded-md px-2.5 py-1 text-xs font-medium bg-[var(--amber)] text-white focus-visible:ring-2 focus-visible:ring-ring hover:opacity-90"
|
|
103
|
+
>
|
|
104
|
+
{t.changes.reviewNow}
|
|
105
|
+
</Link>
|
|
106
|
+
<button
|
|
107
|
+
type="button"
|
|
108
|
+
onClick={() => void handleMarkAllRead()}
|
|
109
|
+
className="inline-flex items-center rounded-md px-2 py-1 text-xs font-medium bg-muted text-muted-foreground hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring"
|
|
110
|
+
>
|
|
111
|
+
{t.changes.markAllRead}
|
|
112
|
+
</button>
|
|
113
|
+
</div>
|
|
114
|
+
</div>
|
|
115
|
+
<button
|
|
116
|
+
type="button"
|
|
117
|
+
onClick={() => setDismissedAtCount(unreadCount)}
|
|
118
|
+
aria-label={t.changes.dismiss}
|
|
119
|
+
className="inline-flex h-6 w-6 items-center justify-center rounded-md text-muted-foreground hover:text-foreground hover:bg-muted/60 focus-visible:ring-2 focus-visible:ring-ring"
|
|
44
120
|
>
|
|
45
|
-
|
|
46
|
-
</
|
|
121
|
+
<X size={14} />
|
|
122
|
+
</button>
|
|
47
123
|
</div>
|
|
48
124
|
</div>
|
|
49
125
|
</div>
|