@geminilight/mindos 0.5.70 → 0.6.0

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 (42) hide show
  1. package/app/app/api/ask/route.ts +122 -92
  2. package/app/app/api/mcp/agents/route.ts +53 -2
  3. package/app/app/api/mcp/status/route.ts +1 -1
  4. package/app/app/api/skills/route.ts +10 -114
  5. package/app/components/ActivityBar.tsx +3 -4
  6. package/app/components/CreateSpaceModal.tsx +31 -6
  7. package/app/components/FileTree.tsx +33 -2
  8. package/app/components/GuideCard.tsx +197 -131
  9. package/app/components/HomeContent.tsx +85 -18
  10. package/app/components/SidebarLayout.tsx +13 -0
  11. package/app/components/SpaceInitToast.tsx +173 -0
  12. package/app/components/agents/AgentDetailContent.tsx +32 -17
  13. package/app/components/agents/AgentsContentPage.tsx +2 -1
  14. package/app/components/agents/AgentsOverviewSection.tsx +1 -14
  15. package/app/components/agents/agents-content-model.ts +16 -8
  16. package/app/components/ask/AskContent.tsx +137 -50
  17. package/app/components/ask/MentionPopover.tsx +16 -8
  18. package/app/components/ask/SlashCommandPopover.tsx +62 -0
  19. package/app/components/settings/KnowledgeTab.tsx +61 -0
  20. package/app/components/walkthrough/steps.ts +11 -6
  21. package/app/hooks/useMention.ts +14 -6
  22. package/app/hooks/useSlashCommand.ts +114 -0
  23. package/app/lib/actions.ts +79 -2
  24. package/app/lib/agent/index.ts +1 -1
  25. package/app/lib/agent/prompt.ts +2 -0
  26. package/app/lib/agent/tools.ts +106 -0
  27. package/app/lib/core/create-space.ts +11 -4
  28. package/app/lib/core/index.ts +1 -1
  29. package/app/lib/i18n-en.ts +51 -46
  30. package/app/lib/i18n-zh.ts +50 -45
  31. package/app/lib/mcp-agents.ts +8 -0
  32. package/app/lib/pdf-extract.ts +33 -0
  33. package/app/lib/pi-integration/extensions.ts +45 -0
  34. package/app/lib/pi-integration/mcporter.ts +219 -0
  35. package/app/lib/pi-integration/session-store.ts +62 -0
  36. package/app/lib/pi-integration/skills.ts +116 -0
  37. package/app/lib/settings.ts +1 -1
  38. package/app/next-env.d.ts +1 -1
  39. package/app/next.config.ts +1 -1
  40. package/app/package.json +2 -0
  41. package/mcp/src/index.ts +29 -0
  42. package/package.json +1 -1
@@ -0,0 +1,173 @@
1
+ 'use client';
2
+
3
+ import { useState, useEffect, useCallback, useTransition } from 'react';
4
+ import { Sparkles, Check, Undo2, X } from 'lucide-react';
5
+ import { useRouter } from 'next/navigation';
6
+ import { useLocale } from '@/lib/LocaleContext';
7
+ import { encodePath } from '@/lib/utils';
8
+ import { revertSpaceInitAction } from '@/lib/actions';
9
+
10
+ type InitState = 'working' | 'done' | 'reverted' | 'error';
11
+
12
+ interface InitInfo {
13
+ spaceName: string;
14
+ spacePath: string;
15
+ description: string;
16
+ state: InitState;
17
+ }
18
+
19
+ /**
20
+ * Global toast that shows AI initialization progress when a new space is created.
21
+ * Mounted in SidebarLayout so it persists across route transitions.
22
+ *
23
+ * States:
24
+ * - working: ✨ progress animation
25
+ * - done: ✓ ready + [Review] [Discard] [×] — persists until user acts
26
+ * - reverted: ↩ "reverted to template" — auto-dismiss 2s
27
+ * - error: silent dismiss
28
+ */
29
+ export default function SpaceInitToast() {
30
+ const [info, setInfo] = useState<InitInfo | null>(null);
31
+ const [visible, setVisible] = useState(false);
32
+ const [reverting, startRevert] = useTransition();
33
+ const router = useRouter();
34
+ const { t } = useLocale();
35
+
36
+ const dismiss = useCallback(() => {
37
+ setVisible(false);
38
+ setTimeout(() => setInfo(null), 200);
39
+ }, []);
40
+
41
+ const handleEvent = useCallback((e: Event) => {
42
+ const detail = (e as CustomEvent).detail as Partial<InitInfo>;
43
+ if (detail.state === 'working') {
44
+ setInfo(detail as InitInfo);
45
+ requestAnimationFrame(() => setVisible(true));
46
+ } else if (detail.state === 'done') {
47
+ setInfo((prev) => prev ? { ...prev, state: 'done' } : null);
48
+ router.refresh();
49
+ } else {
50
+ dismiss();
51
+ }
52
+ }, [router, dismiss]);
53
+
54
+ useEffect(() => {
55
+ window.addEventListener('mindos:ai-init', handleEvent);
56
+ return () => window.removeEventListener('mindos:ai-init', handleEvent);
57
+ }, [handleEvent]);
58
+
59
+ const handleReview = useCallback(() => {
60
+ if (!info) return;
61
+ dismiss();
62
+ router.push(`/view/${encodePath(info.spacePath + '/')}`);
63
+ }, [info, dismiss, router]);
64
+
65
+ const handleDiscard = useCallback(() => {
66
+ if (!info) return;
67
+ startRevert(async () => {
68
+ await revertSpaceInitAction(info.spacePath, info.spaceName, info.description);
69
+ setInfo((prev) => prev ? { ...prev, state: 'reverted' } : null);
70
+ router.refresh();
71
+ window.dispatchEvent(new Event('mindos:files-changed'));
72
+ setTimeout(dismiss, 2000);
73
+ });
74
+ }, [info, router, dismiss]);
75
+
76
+ const handleAccept = useCallback(() => {
77
+ dismiss();
78
+ }, [dismiss]);
79
+
80
+ if (!info) return null;
81
+
82
+ const h = t.home;
83
+ const { state } = info;
84
+ const isDone = state === 'done';
85
+ const isReverted = state === 'reverted';
86
+ const isWorking = state === 'working';
87
+
88
+ return (
89
+ <div
90
+ className={`fixed bottom-6 left-1/2 z-50 -translate-x-1/2 transition-all duration-200 ease-out ${
91
+ visible
92
+ ? 'opacity-100 translate-y-0 scale-100'
93
+ : 'opacity-0 translate-y-3 scale-[0.96] pointer-events-none'
94
+ }`}
95
+ >
96
+ <div
97
+ className="flex items-center gap-2.5 rounded-full border bg-card/95 backdrop-blur py-2 shadow-lg"
98
+ style={{
99
+ borderColor: 'color-mix(in srgb, var(--amber) 40%, var(--border))',
100
+ boxShadow: '0 8px 24px color-mix(in srgb, var(--amber) 12%, rgba(0,0,0,.2))',
101
+ paddingLeft: '0.75rem',
102
+ paddingRight: isDone ? '0.5rem' : '1rem',
103
+ }}
104
+ >
105
+ {/* Icon */}
106
+ <span
107
+ className={`inline-flex h-6 w-6 items-center justify-center rounded-full shrink-0 ${
108
+ isReverted
109
+ ? 'text-muted-foreground bg-muted'
110
+ : 'text-[var(--amber)] bg-[var(--amber-subtle)]'
111
+ } ${isWorking ? 'animate-pulse' : ''}`}
112
+ >
113
+ {isReverted ? <Undo2 size={12} /> : isDone ? <Check size={13} strokeWidth={2.5} /> : <Sparkles size={13} />}
114
+ </span>
115
+
116
+ {/* Text */}
117
+ <span className="text-xs text-foreground whitespace-nowrap select-none font-medium">
118
+ {isReverted
119
+ ? h.aiInitReverted(info.spaceName)
120
+ : isDone
121
+ ? h.aiInitReady(info.spaceName)
122
+ : <>{h.aiInitGenerating(info.spaceName)}<DotPulse /></>
123
+ }
124
+ </span>
125
+
126
+ {/* Action buttons — only in done state */}
127
+ {isDone && (
128
+ <div className="flex items-center gap-1 ml-1">
129
+ <button
130
+ type="button"
131
+ onClick={handleReview}
132
+ className="px-2.5 py-1 rounded-full text-2xs font-medium bg-[var(--amber)] text-white hover:opacity-90 transition-opacity focus-visible:ring-2 focus-visible:ring-ring"
133
+ >
134
+ {h.aiInitReview}
135
+ </button>
136
+ <button
137
+ type="button"
138
+ onClick={handleDiscard}
139
+ disabled={reverting}
140
+ className="px-2.5 py-1 rounded-full text-2xs font-medium bg-muted text-muted-foreground hover:text-foreground transition-colors focus-visible:ring-2 focus-visible:ring-ring disabled:opacity-50"
141
+ >
142
+ {h.aiInitDiscard}
143
+ </button>
144
+ <button
145
+ type="button"
146
+ onClick={handleAccept}
147
+ aria-label="Accept"
148
+ className="ml-0.5 inline-flex h-5 w-5 items-center justify-center rounded-full text-muted-foreground hover:text-foreground hover:bg-muted/60 transition-colors focus-visible:ring-2 focus-visible:ring-ring"
149
+ >
150
+ <X size={12} />
151
+ </button>
152
+ </div>
153
+ )}
154
+ </div>
155
+ </div>
156
+ );
157
+ }
158
+
159
+ function DotPulse() {
160
+ return (
161
+ <span className="inline-flex gap-[2px] items-end h-[1em] ml-px">
162
+ <span className="w-[3px] h-[3px] rounded-full bg-muted-foreground animate-[dotBounce_1.2s_0ms_infinite]" />
163
+ <span className="w-[3px] h-[3px] rounded-full bg-muted-foreground animate-[dotBounce_1.2s_200ms_infinite]" />
164
+ <span className="w-[3px] h-[3px] rounded-full bg-muted-foreground animate-[dotBounce_1.2s_400ms_infinite]" />
165
+ <style>{`
166
+ @keyframes dotBounce {
167
+ 0%, 60%, 100% { opacity: 0.25; transform: translateY(0); }
168
+ 30% { opacity: 1; transform: translateY(-2px); }
169
+ }
170
+ `}</style>
171
+ </span>
172
+ );
173
+ }
@@ -73,6 +73,7 @@ export default function AgentDetailContent({ agentKey }: { agentKey: string }) {
73
73
  return map;
74
74
  }, [mcp.agents]);
75
75
 
76
+ const isMindOS = agentKey === 'mindos';
76
77
  const status = agent ? resolveAgentStatus(agent) : 'notFound';
77
78
  const currentScope = agent?.scope === 'project' ? 'project' : 'global';
78
79
  const currentTransport: 'stdio' | 'http' = agent?.transport === 'http' ? 'http' : 'stdio';
@@ -249,9 +250,19 @@ export default function AgentDetailContent({ agentKey }: { agentKey: string }) {
249
250
 
250
251
  {/* MCP status row */}
251
252
  <div className="grid grid-cols-1 md:grid-cols-3 gap-3 text-sm">
252
- <DetailLine label={a.detail.mcpInstalled} value={agent.installed ? a.detail.yes : a.detail.no} />
253
- <DetailLine label={a.detail.mcpScope} value={agent.scope ?? a.na} />
254
- <DetailLine label={a.detail.mcpConfigPath} value={agent.configPath ?? a.na} />
253
+ {isMindOS ? (
254
+ <>
255
+ <DetailLine label="Status" value={mcp.status?.running ? '● Running' : '○ Stopped'} />
256
+ <DetailLine label="Endpoint" value={mcp.status?.endpoint ?? a.na} />
257
+ <DetailLine label="Tools" value={mcp.status?.toolCount != null ? String(mcp.status.toolCount) : a.na} />
258
+ </>
259
+ ) : (
260
+ <>
261
+ <DetailLine label={a.detail.mcpInstalled} value={agent.installed ? a.detail.yes : a.detail.no} />
262
+ <DetailLine label={a.detail.mcpScope} value={agent.scope ?? a.na} />
263
+ <DetailLine label={a.detail.mcpConfigPath} value={agent.configPath ?? a.na} />
264
+ </>
265
+ )}
255
266
  </div>
256
267
 
257
268
  {/* Configured MCP servers with management */}
@@ -304,27 +315,31 @@ export default function AgentDetailContent({ agentKey }: { agentKey: string }) {
304
315
 
305
316
  {/* MCP actions */}
306
317
  <div className="flex flex-wrap items-center gap-2">
307
- <ActionButton
308
- onClick={() => void handleCopySnippet()}
309
- disabled={false}
310
- busy={false}
311
- label={snippetCopied ? a.detail.mcpCopied : a.detail.mcpCopySnippet}
312
- />
318
+ {!isMindOS && (
319
+ <ActionButton
320
+ onClick={() => void handleCopySnippet()}
321
+ disabled={false}
322
+ busy={false}
323
+ label={snippetCopied ? a.detail.mcpCopied : a.detail.mcpCopySnippet}
324
+ />
325
+ )}
313
326
  <ActionButton
314
327
  onClick={() => void mcp.refresh()}
315
328
  disabled={false}
316
329
  busy={false}
317
330
  label={a.detail.mcpRefresh}
318
331
  />
319
- <ActionButton
320
- onClick={() => void handleApplyMcpConfig(currentScope, currentTransport)}
321
- disabled={mcpBusy}
322
- busy={mcpBusy}
323
- label={a.detail.mcpReconnect}
324
- variant="primary"
325
- />
332
+ {!isMindOS && (
333
+ <ActionButton
334
+ onClick={() => void handleApplyMcpConfig(currentScope, currentTransport)}
335
+ disabled={mcpBusy}
336
+ busy={mcpBusy}
337
+ label={a.detail.mcpReconnect}
338
+ variant="primary"
339
+ />
340
+ )}
326
341
  </div>
327
- <p className="text-2xs text-muted-foreground truncate">{snippet.path}</p>
342
+ {!isMindOS && <p className="text-2xs text-muted-foreground truncate">{snippet.path}</p>}
328
343
  {mcpMessage && <p className="text-xs text-muted-foreground animate-in fade-in duration-200">{mcpMessage}</p>}
329
344
  </section>
330
345
 
@@ -46,8 +46,9 @@ export default function AgentsContentPage({ tab }: { tab: AgentsDashboardTab })
46
46
  detectedCount: buckets.detected.length,
47
47
  notFoundCount: buckets.notFound.length,
48
48
  allSkillsDisabled: mcp.skills.length > 0 && mcp.skills.every((s) => !s.enabled),
49
+ copy: a.overview,
49
50
  }),
50
- [mcp.skills, mcp.status?.running, buckets.detected.length, buckets.notFound.length],
51
+ [mcp.skills, mcp.status?.running, buckets.detected.length, buckets.notFound.length, a.overview],
51
52
  );
52
53
  const enabledSkillCount = useMemo(
53
54
  () => mcp.skills.filter((skill) => skill.enabled).length,
@@ -378,18 +378,10 @@ function AgentCard({
378
378
  ? 'bg-amber-500/10 text-amber-600 dark:text-amber-400'
379
379
  : 'bg-zinc-500/10 text-zinc-500';
380
380
 
381
- const accentBorder =
382
- status === 'connected'
383
- ? 'border-l-[var(--success)]'
384
- : status === 'detected'
385
- ? 'border-l-[var(--amber)]'
386
- : '';
387
-
388
381
  return (
389
382
  <Link
390
383
  href={`/agents/${encodeURIComponent(agent.key)}`}
391
- className={`group rounded-xl border bg-card p-3.5
392
- ${accentBorder ? `border-l-2 ${accentBorder} border-border` : 'border-border'}
384
+ className={`group rounded-xl border border-border bg-card p-3.5
393
385
  hover:border-[var(--amber)]/30 hover:shadow-[0_2px_8px_rgba(0,0,0,0.04)]
394
386
  active:scale-[0.98]
395
387
  transition-all duration-150 animate-in
@@ -416,11 +408,6 @@ function AgentCard({
416
408
  <div className="flex items-center gap-1 pt-2.5 border-t border-border/40">
417
409
  <MetricChip icon={<Server size={11} aria-hidden="true" />} value={mcpCount} label={copy.colMcp as string} />
418
410
  <MetricChip icon={<Zap size={11} aria-hidden="true" />} value={skillCount} label={copy.colSkills as string} />
419
- {agent.skillMode && (
420
- <span className="text-2xs px-1.5 py-0.5 rounded-md bg-muted/50 text-muted-foreground/70 truncate select-none">
421
- {agent.skillMode}
422
- </span>
423
- )}
424
411
  <span className="flex-1 min-w-[4px]" />
425
412
  {hasRuntime && (
426
413
  <span
@@ -56,11 +56,10 @@ 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;
59
+ export interface RiskCopy {
60
+ riskMcpStopped: string;
61
+ riskDetected: (n: number) => string;
62
+ riskSkillsDisabled: string;
64
63
  }
65
64
 
66
65
  export function buildRiskQueue(args: {
@@ -68,9 +67,12 @@ export function buildRiskQueue(args: {
68
67
  detectedCount: number;
69
68
  notFoundCount: number;
70
69
  allSkillsDisabled: boolean;
70
+ copy: RiskCopy;
71
71
  }): RiskItem[] {
72
- const items = buildBaseRiskItems(args);
73
- if (args.allSkillsDisabled) items.push({ id: 'skills-disabled', severity: 'warn', title: 'All skills are disabled' });
72
+ const items: RiskItem[] = [];
73
+ if (!args.mcpRunning) items.push({ id: 'mcp-stopped', severity: 'error', title: args.copy.riskMcpStopped });
74
+ if (args.detectedCount > 0) items.push({ id: 'detected-unconfigured', severity: 'warn', title: args.copy.riskDetected(args.detectedCount) });
75
+ if (args.allSkillsDisabled) items.push({ id: 'skills-disabled', severity: 'warn', title: args.copy.riskSkillsDisabled });
74
76
  return items;
75
77
  }
76
78
 
@@ -178,12 +180,18 @@ export function filterAgentsForMcpWorkspace(
178
180
  });
179
181
  }
180
182
 
183
+ const defaultRiskCopy: RiskCopy = {
184
+ riskMcpStopped: 'MCP server is not running.',
185
+ riskDetected: (n: number) => `${n} detected agent(s) need configuration.`,
186
+ riskSkillsDisabled: 'All skills are disabled.',
187
+ };
188
+
181
189
  export function buildMcpRiskQueue(args: {
182
190
  mcpRunning: boolean;
183
191
  detectedCount: number;
184
192
  notFoundCount: number;
185
193
  }): RiskItem[] {
186
- return buildBaseRiskItems(args);
194
+ return buildRiskQueue({ ...args, allSkillsDisabled: false, copy: defaultRiskCopy });
187
195
  }
188
196
 
189
197
  export interface McpBulkReconnectResult {