@geminilight/mindos 0.5.68 → 0.5.70

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 (33) hide show
  1. package/app/app/api/ask/route.ts +12 -4
  2. package/app/app/api/file/import/route.ts +197 -0
  3. package/app/app/api/mcp/install/route.ts +99 -12
  4. package/app/app/api/mcp/status/route.ts +1 -1
  5. package/app/components/ActivityBar.tsx +3 -4
  6. package/app/components/FileTree.tsx +35 -9
  7. package/app/components/ImportModal.tsx +415 -0
  8. package/app/components/OnboardingView.tsx +9 -0
  9. package/app/components/Panel.tsx +4 -2
  10. package/app/components/SidebarLayout.tsx +83 -8
  11. package/app/components/TableOfContents.tsx +1 -0
  12. package/app/components/agents/AgentDetailContent.tsx +37 -28
  13. package/app/components/agents/AgentsMcpSection.tsx +16 -12
  14. package/app/components/agents/AgentsOverviewSection.tsx +48 -34
  15. package/app/components/agents/AgentsPrimitives.tsx +41 -20
  16. package/app/components/agents/AgentsSkillsSection.tsx +16 -7
  17. package/app/components/agents/SkillDetailPopover.tsx +13 -11
  18. package/app/components/ask/AskContent.tsx +11 -0
  19. package/app/components/panels/AgentsPanelAgentGroups.tsx +8 -6
  20. package/app/components/panels/AgentsPanelHubNav.tsx +3 -3
  21. package/app/components/panels/DiscoverPanel.tsx +88 -2
  22. package/app/hooks/useFileImport.ts +191 -0
  23. package/app/hooks/useFileUpload.ts +11 -0
  24. package/app/lib/agent/context.ts +7 -2
  25. package/app/lib/agent/tools.ts +245 -6
  26. package/app/lib/core/backlinks.ts +12 -4
  27. package/app/lib/core/file-convert.ts +97 -0
  28. package/app/lib/core/organize.ts +105 -0
  29. package/app/lib/core/search.ts +17 -3
  30. package/app/lib/fs.ts +5 -3
  31. package/app/lib/i18n-en.ts +51 -0
  32. package/app/lib/i18n-zh.ts +51 -0
  33. package/package.json +1 -1
@@ -177,6 +177,17 @@ export default function AskContent({ visible, currentFile, initialMessage, onFir
177
177
  const upload = useFileUpload();
178
178
  const mention = useMention();
179
179
 
180
+ useEffect(() => {
181
+ const handler = (e: Event) => {
182
+ const files = (e as CustomEvent).detail?.files;
183
+ if (Array.isArray(files) && files.length > 0) {
184
+ upload.injectFiles(files);
185
+ }
186
+ };
187
+ window.addEventListener('mindos:inject-ask-files', handler);
188
+ return () => window.removeEventListener('mindos:inject-ask-files', handler);
189
+ }, [upload]);
190
+
180
191
  // Focus and init session when becoming visible (edge-triggered for panel, level-triggered for modal)
181
192
  const prevVisibleRef = useRef(false);
182
193
  useEffect(() => {
@@ -34,13 +34,14 @@ export function AgentsPanelAgentGroups({
34
34
  }) {
35
35
  return (
36
36
  <div>
37
- <div className="px-0 py-1 mb-0.5">
38
- <span className="text-2xs font-semibold text-muted-foreground uppercase tracking-wider">{p.rosterLabel}</span>
37
+ <div className="px-0 py-1.5 mb-1">
38
+ <span className="text-2xs font-semibold text-muted-foreground/70 uppercase tracking-wider">{p.rosterLabel}</span>
39
39
  </div>
40
40
  {connected.length > 0 && (
41
41
  <section className="mb-3">
42
- <h3 className="text-[11px] font-medium text-muted-foreground/90 uppercase tracking-wider mb-2 pl-0.5">
43
- {p.sectionConnected} ({connected.length})
42
+ <h3 className="flex items-center gap-1.5 text-[11px] font-medium text-muted-foreground/80 uppercase tracking-wider mb-2 pl-0.5">
43
+ <span className="w-1 h-3 rounded-full bg-[var(--success)]/50" aria-hidden="true" />
44
+ {p.sectionConnected} <span className="text-muted-foreground/50 tabular-nums">({connected.length})</span>
44
45
  </h3>
45
46
  <div className="space-y-1.5">
46
47
  {connected.map(agent => (
@@ -60,8 +61,9 @@ export function AgentsPanelAgentGroups({
60
61
 
61
62
  {detected.length > 0 && (
62
63
  <section className="mb-3">
63
- <h3 className="text-[11px] font-medium text-muted-foreground/90 uppercase tracking-wider mb-2 pl-0.5">
64
- {p.sectionDetected} ({detected.length})
64
+ <h3 className="flex items-center gap-1.5 text-[11px] font-medium text-muted-foreground/80 uppercase tracking-wider mb-2 pl-0.5">
65
+ <span className="w-1 h-3 rounded-full bg-[var(--amber)]/50" aria-hidden="true" />
66
+ {p.sectionDetected} <span className="text-muted-foreground/50 tabular-nums">({detected.length})</span>
65
67
  </h3>
66
68
  <div className="space-y-1.5">
67
69
  {detected.map(agent => (
@@ -27,18 +27,18 @@ export function AgentsPanelHubNav({
27
27
  <PanelNavRow
28
28
  icon={<LayoutDashboard size={14} className="text-[var(--amber)]" />}
29
29
  title={copy.navOverview}
30
- badge={<span className="text-2xs tabular-nums text-muted-foreground">{connectedCount}</span>}
30
+ badge={<span className="text-2xs tabular-nums text-muted-foreground/60 px-1.5 py-0.5 rounded bg-muted/40 font-medium">{connectedCount}</span>}
31
31
  href="/agents"
32
32
  active={inAgentsRoute && (tab === null || tab === 'overview')}
33
33
  />
34
34
  <PanelNavRow
35
- icon={<Server size={14} className="text-muted-foreground" />}
35
+ icon={<Server size={14} className={inAgentsRoute && tab === 'mcp' ? 'text-[var(--amber)]' : 'text-muted-foreground'} />}
36
36
  title={copy.navMcp}
37
37
  href="/agents?tab=mcp"
38
38
  active={inAgentsRoute && tab === 'mcp'}
39
39
  />
40
40
  <PanelNavRow
41
- icon={<Zap size={14} className="text-muted-foreground" />}
41
+ icon={<Zap size={14} className={inAgentsRoute && tab === 'skills' ? 'text-[var(--amber)]' : 'text-muted-foreground'} />}
42
42
  title={copy.navSkills}
43
43
  href="/agents?tab=skills"
44
44
  active={inAgentsRoute && tab === 'skills'}
@@ -1,11 +1,15 @@
1
1
  'use client';
2
2
 
3
- import { Lightbulb, Blocks, Zap, LayoutTemplate, User, Download, RefreshCw, Repeat, Rocket, Search, Handshake, ShieldCheck } from 'lucide-react';
3
+ import { useState, useEffect, useCallback, useRef } from 'react';
4
+ import { useRouter } from 'next/navigation';
5
+ import { Lightbulb, Blocks, Zap, LayoutTemplate, User, Download, RefreshCw, Repeat, Rocket, Search, Handshake, ShieldCheck, ChevronDown } from 'lucide-react';
4
6
  import PanelHeader from './PanelHeader';
5
7
  import { PanelNavRow, ComingSoonBadge } from './PanelNavRow';
6
8
  import { useLocale } from '@/lib/LocaleContext';
7
9
  import { useCases } from '@/components/explore/use-cases';
8
10
  import { openAskModal } from '@/hooks/useAskModal';
11
+ import { getPluginRenderers, isRendererEnabled, setRendererEnabled, loadDisabledState } from '@/lib/renderers/registry';
12
+ import { Toggle } from '../settings/Primitives';
9
13
 
10
14
  interface DiscoverPanelProps {
11
15
  active: boolean;
@@ -56,8 +60,44 @@ export default function DiscoverPanel({ active, maximized, onMaximize }: Discove
56
60
  const { t } = useLocale();
57
61
  const d = t.panels.discover;
58
62
  const e = t.explore;
63
+ const p = t.panels.plugins;
64
+ const router = useRouter();
65
+
66
+ const [pluginsMounted, setPluginsMounted] = useState(false);
67
+ const [showPlugins, setShowPlugins] = useState(false);
68
+ const [, forceUpdate] = useState(0);
69
+ const [existingFiles, setExistingFiles] = useState<Set<string>>(new Set());
70
+ const fetchedRef = useRef(false);
71
+
72
+ useEffect(() => {
73
+ loadDisabledState();
74
+ setPluginsMounted(true);
75
+ }, []);
76
+
77
+ useEffect(() => {
78
+ if (!pluginsMounted || fetchedRef.current) return;
79
+ fetchedRef.current = true;
80
+ const entryPaths = getPluginRenderers().map(r => r.entryPath).filter((ep): ep is string => !!ep);
81
+ if (entryPaths.length === 0) return;
82
+ fetch('/api/files')
83
+ .then(r => r.ok ? r.json() : [])
84
+ .then((allPaths: string[]) => {
85
+ const pathSet = new Set(allPaths);
86
+ setExistingFiles(new Set(entryPaths.filter(ep => pathSet.has(ep))));
87
+ })
88
+ .catch(() => {});
89
+ }, [pluginsMounted]);
90
+
91
+ const handleToggle = useCallback((id: string, enabled: boolean) => {
92
+ setRendererEnabled(id, enabled);
93
+ forceUpdate(n => n + 1);
94
+ window.dispatchEvent(new Event('renderer-state-changed'));
95
+ }, []);
96
+
97
+ const handleOpenPlugin = useCallback((entryPath: string) => {
98
+ router.push(`/view/${entryPath.split('/').map(encodeURIComponent).join('/')}`);
99
+ }, [router]);
59
100
 
60
- /** Type-safe lookup for use case i18n */
61
101
  const getUseCaseText = (id: string): { title: string; prompt: string } | undefined => {
62
102
  const map: Record<string, { title: string; desc: string; prompt: string }> = {
63
103
  c1: e.c1, c2: e.c2, c3: e.c3, c4: e.c4, c5: e.c5,
@@ -66,6 +106,9 @@ export default function DiscoverPanel({ active, maximized, onMaximize }: Discove
66
106
  return map[id];
67
107
  };
68
108
 
109
+ const renderers = pluginsMounted ? getPluginRenderers() : [];
110
+ const enabledCount = pluginsMounted ? renderers.filter(r => isRendererEnabled(r.id)).length : 0;
111
+
69
112
  return (
70
113
  <div className={`flex flex-col h-full ${active ? '' : 'hidden'}`}>
71
114
  <PanelHeader title={d.title} maximized={maximized} onMaximize={onMaximize} />
@@ -97,6 +140,49 @@ export default function DiscoverPanel({ active, maximized, onMaximize }: Discove
97
140
 
98
141
  <div className="mx-4 border-t border-border" />
99
142
 
143
+ {/* Installed extensions (merged from Plugins panel) */}
144
+ <div className="py-2">
145
+ <button
146
+ type="button"
147
+ onClick={() => setShowPlugins(v => !v)}
148
+ className="w-full flex items-center gap-1.5 px-4 py-1.5 text-left"
149
+ >
150
+ <ChevronDown size={11} className={`text-muted-foreground transition-transform duration-150 ${showPlugins ? '' : '-rotate-90'}`} />
151
+ <Blocks size={13} className="text-muted-foreground shrink-0" />
152
+ <span className="text-2xs font-medium text-muted-foreground uppercase tracking-wider flex-1">
153
+ {p.title}
154
+ </span>
155
+ <span className="text-2xs text-muted-foreground tabular-nums">{enabledCount}/{renderers.length}</span>
156
+ </button>
157
+ {showPlugins && renderers.map(r => {
158
+ const enabled = isRendererEnabled(r.id);
159
+ const fileExists = r.entryPath ? existingFiles.has(r.entryPath) : false;
160
+ const canOpen = enabled && r.entryPath && fileExists;
161
+ return (
162
+ <div
163
+ key={r.id}
164
+ className={`flex items-center gap-2 px-4 py-1.5 mx-1 rounded-sm transition-colors ${canOpen ? 'cursor-pointer hover:bg-muted/50' : ''} ${!enabled ? 'opacity-50' : ''}`}
165
+ onClick={canOpen ? () => handleOpenPlugin(r.entryPath!) : undefined}
166
+ role={canOpen ? 'link' : undefined}
167
+ tabIndex={canOpen ? 0 : undefined}
168
+ onKeyDown={canOpen ? (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); handleOpenPlugin(r.entryPath!); } } : undefined}
169
+ >
170
+ <span className="text-sm shrink-0" suppressHydrationWarning>{r.icon}</span>
171
+ <span className="text-xs text-foreground truncate flex-1">{r.name}</span>
172
+ {r.core ? (
173
+ <span className="text-2xs px-1.5 py-0.5 rounded bg-muted text-muted-foreground shrink-0">{p.core}</span>
174
+ ) : (
175
+ <div onClick={e => e.stopPropagation()}>
176
+ <Toggle checked={enabled} onChange={v => handleToggle(r.id, v)} size="sm" />
177
+ </div>
178
+ )}
179
+ </div>
180
+ );
181
+ })}
182
+ </div>
183
+
184
+ <div className="mx-4 border-t border-border" />
185
+
100
186
  {/* Quick try — use case list */}
101
187
  <div className="py-2">
102
188
  <div className="px-4 py-1.5">
@@ -0,0 +1,191 @@
1
+ 'use client';
2
+
3
+ import { useState, useCallback, useRef } from 'react';
4
+ import { ALLOWED_IMPORT_EXTENSIONS } from '@/lib/core/file-convert';
5
+
6
+ export type ImportIntent = 'archive' | 'digest';
7
+ export type ImportStep = 'select' | 'archive_config' | 'importing' | 'done';
8
+ export type ConflictMode = 'skip' | 'rename' | 'overwrite';
9
+
10
+ export interface ImportFile {
11
+ file: File;
12
+ name: string;
13
+ size: number;
14
+ content: string | null;
15
+ loading: boolean;
16
+ error: string | null;
17
+ }
18
+
19
+ const MAX_FILE_SIZE = 5 * 1024 * 1024;
20
+ const MAX_PDF_SIZE = 12 * 1024 * 1024;
21
+ const MAX_FILES = 20;
22
+
23
+ function getExt(name: string): string {
24
+ const idx = name.lastIndexOf('.');
25
+ return idx >= 0 ? name.slice(idx).toLowerCase() : '';
26
+ }
27
+
28
+ function formatSize(bytes: number): string {
29
+ if (bytes < 1024) return `${bytes} B`;
30
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
31
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
32
+ }
33
+
34
+ export function useFileImport() {
35
+ const [files, setFiles] = useState<ImportFile[]>([]);
36
+ const [step, setStep] = useState<ImportStep>('select');
37
+ const [intent, setIntent] = useState<ImportIntent>('archive');
38
+ const [targetSpace, setTargetSpace] = useState('');
39
+ const [conflict, setConflict] = useState<ConflictMode>('rename');
40
+ const [importing, setImporting] = useState(false);
41
+ const [result, setResult] = useState<{
42
+ created: Array<{ original: string; path: string }>;
43
+ skipped: Array<{ name: string; reason: string }>;
44
+ errors: Array<{ name: string; error: string }>;
45
+ updatedFiles: string[];
46
+ } | null>(null);
47
+ const inputRef = useRef<HTMLInputElement>(null);
48
+
49
+ const addFiles = useCallback(async (fileList: FileList | File[]) => {
50
+ const incoming = Array.from(fileList).slice(0, MAX_FILES);
51
+ const newFiles: ImportFile[] = [];
52
+
53
+ for (const file of incoming) {
54
+ const ext = getExt(file.name);
55
+ const maxSize = ext === '.pdf' ? MAX_PDF_SIZE : MAX_FILE_SIZE;
56
+
57
+ let error: string | null = null;
58
+ if (!ALLOWED_IMPORT_EXTENSIONS.has(ext)) {
59
+ error = 'unsupported';
60
+ } else if (file.size > maxSize) {
61
+ error = 'tooLarge';
62
+ }
63
+
64
+ newFiles.push({
65
+ file,
66
+ name: file.name,
67
+ size: file.size,
68
+ content: null,
69
+ loading: !error,
70
+ error,
71
+ });
72
+ }
73
+
74
+ setFiles(prev => {
75
+ const merged = [...prev];
76
+ for (const f of newFiles) {
77
+ const isDup = merged.some(m =>
78
+ m.name === f.name && m.size === f.size
79
+ );
80
+ if (!isDup && merged.length < MAX_FILES) merged.push(f);
81
+ }
82
+ return merged;
83
+ });
84
+
85
+ for (const f of newFiles) {
86
+ if (f.error) continue;
87
+ try {
88
+ const text = await f.file.text();
89
+ setFiles(prev => prev.map(p =>
90
+ p.name === f.name && p.size === f.size
91
+ ? { ...p, content: text, loading: false }
92
+ : p
93
+ ));
94
+ } catch {
95
+ setFiles(prev => prev.map(p =>
96
+ p.name === f.name && p.size === f.size
97
+ ? { ...p, loading: false, error: 'readFailed' }
98
+ : p
99
+ ));
100
+ }
101
+ }
102
+ }, []);
103
+
104
+ const removeFile = useCallback((index: number) => {
105
+ setFiles(prev => prev.filter((_, i) => i !== index));
106
+ }, []);
107
+
108
+ const clearFiles = useCallback(() => {
109
+ setFiles([]);
110
+ setStep('select');
111
+ setResult(null);
112
+ }, []);
113
+
114
+ const validFiles = files.filter(f => !f.error && f.content !== null);
115
+ const allReady = files.length > 0 && files.every(f => !f.loading);
116
+ const hasErrors = files.some(f => f.error);
117
+
118
+ const doArchive = useCallback(async () => {
119
+ if (validFiles.length === 0) return;
120
+ setImporting(true);
121
+ setStep('importing');
122
+
123
+ try {
124
+ const payload = {
125
+ files: validFiles.map(f => ({
126
+ name: f.name,
127
+ content: f.content!,
128
+ })),
129
+ targetSpace: targetSpace || undefined,
130
+ conflict,
131
+ organize: true,
132
+ };
133
+
134
+ const res = await fetch('/api/file/import', {
135
+ method: 'POST',
136
+ headers: { 'Content-Type': 'application/json' },
137
+ body: JSON.stringify(payload),
138
+ });
139
+
140
+ const data = await res.json();
141
+ if (!res.ok) throw new Error(data.error || `HTTP ${res.status}`);
142
+
143
+ setResult(data);
144
+ setStep('done');
145
+ } catch (err) {
146
+ setResult({
147
+ created: [],
148
+ skipped: [],
149
+ errors: [{ name: '*', error: (err as Error).message }],
150
+ updatedFiles: [],
151
+ });
152
+ setStep('done');
153
+ } finally {
154
+ setImporting(false);
155
+ }
156
+ }, [validFiles, targetSpace, conflict]);
157
+
158
+ const reset = useCallback(() => {
159
+ setFiles([]);
160
+ setStep('select');
161
+ setIntent('archive');
162
+ setTargetSpace('');
163
+ setConflict('rename');
164
+ setImporting(false);
165
+ setResult(null);
166
+ }, []);
167
+
168
+ return {
169
+ files,
170
+ step,
171
+ intent,
172
+ targetSpace,
173
+ conflict,
174
+ importing,
175
+ result,
176
+ inputRef,
177
+ validFiles,
178
+ allReady,
179
+ hasErrors,
180
+ addFiles,
181
+ removeFile,
182
+ clearFiles,
183
+ setStep,
184
+ setIntent,
185
+ setTargetSpace,
186
+ setConflict,
187
+ doArchive,
188
+ reset,
189
+ formatSize,
190
+ };
191
+ }
@@ -115,6 +115,16 @@ export function useFileUpload() {
115
115
  setUploadError('');
116
116
  }, []);
117
117
 
118
+ const injectFiles = useCallback((files: LocalAttachment[]) => {
119
+ setLocalAttachments(prev => {
120
+ const merged = [...prev];
121
+ for (const item of files) {
122
+ if (!merged.some(m => m.name === item.name)) merged.push(item);
123
+ }
124
+ return merged;
125
+ });
126
+ }, []);
127
+
118
128
  return {
119
129
  localAttachments,
120
130
  uploadError,
@@ -122,5 +132,6 @@ export function useFileUpload() {
122
132
  pickFiles,
123
133
  removeAttachment,
124
134
  clearAttachments,
135
+ injectFiles,
125
136
  };
126
137
  }
@@ -227,7 +227,7 @@ export async function compactMessages(
227
227
 
228
228
  console.log(`[ask] Compacted ${earlyMessages.length} early messages into summary (${summaryText.length} chars)`);
229
229
 
230
- const summaryContent = `[Summary of earlier conversation]\n\n${summaryText}`;
230
+ const summaryContent = `[System Note: Older conversation history has been truncated due to context length limits, but here is an AI-generated summary of what was discussed so far.]\n\n${summaryText}`;
231
231
 
232
232
  // If first recent message is also 'user', merge summary into it to avoid
233
233
  // consecutive user messages (Anthropic rejects user→user sequences).
@@ -316,10 +316,15 @@ export function hardPrune(
316
316
  console.log(`[ask] Hard pruned ${cutIdx} messages, injecting synthetic user message (${messages.length} → ${pruned.length + 1})`);
317
317
  const syntheticUser: UserMessage = {
318
318
  role: 'user',
319
- content: '[Conversation context was pruned due to length. Continuing from here.]',
319
+ content: '[System Note: Older conversation history has been truncated due to context length limits. The user may refer to things you can no longer see. If so, kindly ask them to repeat the context.]',
320
320
  timestamp: Date.now(),
321
321
  };
322
322
  return [syntheticUser as AgentMessage, ...pruned];
323
+ } else if (cutIdx > 0 && pruned.length > 0 && (pruned[0] as any).role === 'user') {
324
+ // If we pruned and the first message IS a user message, prepend the warning to it
325
+ const firstMsg = { ...pruned[0] } as UserMessage;
326
+ firstMsg.content = `[System Note: Older conversation history has been truncated due to context length limits. The user may refer to things you can no longer see. If so, kindly ask them to repeat the context.]\n\n` + firstMsg.content;
327
+ pruned[0] = firstMsg as AgentMessage;
323
328
  }
324
329
 
325
330
  if (cutIdx > 0) {