@geminilight/mindos 0.6.8 → 0.6.13
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 +2 -0
- package/README_zh.md +2 -0
- package/app/app/api/mcp/install/route.ts +4 -1
- package/app/app/api/setup/check-path/route.ts +2 -7
- package/app/app/api/setup/ls/route.ts +3 -9
- package/app/app/api/setup/path-utils.ts +8 -0
- package/app/app/api/setup/route.ts +2 -7
- package/app/app/api/uninstall/route.ts +47 -0
- package/app/app/globals.css +11 -0
- package/app/components/ActivityBar.tsx +10 -3
- package/app/components/AskFab.tsx +7 -3
- package/app/components/CreateSpaceModal.tsx +1 -1
- package/app/components/DirView.tsx +1 -1
- package/app/components/FileTree.tsx +30 -23
- package/app/components/GuideCard.tsx +1 -1
- package/app/components/HomeContent.tsx +137 -109
- package/app/components/ImportModal.tsx +16 -477
- package/app/components/MarkdownView.tsx +3 -0
- package/app/components/OnboardingView.tsx +1 -1
- package/app/components/OrganizeToast.tsx +386 -0
- package/app/components/Panel.tsx +23 -2
- package/app/components/Sidebar.tsx +1 -1
- package/app/components/SidebarLayout.tsx +44 -1
- package/app/components/agents/AgentDetailContent.tsx +33 -12
- package/app/components/agents/AgentsMcpSection.tsx +1 -1
- package/app/components/agents/AgentsOverviewSection.tsx +3 -4
- package/app/components/agents/AgentsPrimitives.tsx +2 -2
- package/app/components/agents/AgentsSkillsSection.tsx +2 -2
- package/app/components/agents/SkillDetailPopover.tsx +24 -8
- package/app/components/ask/AskContent.tsx +124 -75
- package/app/components/ask/HighlightMatch.tsx +14 -0
- package/app/components/ask/MentionPopover.tsx +5 -3
- package/app/components/ask/MessageList.tsx +39 -11
- package/app/components/ask/SlashCommandPopover.tsx +4 -2
- package/app/components/changes/ChangesBanner.tsx +20 -2
- package/app/components/changes/ChangesContentPage.tsx +10 -2
- package/app/components/echo/EchoHero.tsx +1 -1
- package/app/components/echo/EchoInsightCollapsible.tsx +1 -1
- package/app/components/echo/EchoPageSections.tsx +1 -1
- package/app/components/explore/UseCaseCard.tsx +1 -1
- package/app/components/panels/DiscoverPanel.tsx +29 -25
- package/app/components/panels/ImportHistoryPanel.tsx +195 -0
- package/app/components/panels/PluginsPanel.tsx +2 -2
- package/app/components/settings/AiTab.tsx +24 -0
- package/app/components/settings/KnowledgeTab.tsx +1 -1
- package/app/components/settings/McpSkillCreateForm.tsx +1 -1
- package/app/components/settings/McpSkillRow.tsx +1 -1
- package/app/components/settings/McpSkillsSection.tsx +2 -2
- package/app/components/settings/McpTab.tsx +2 -2
- package/app/components/settings/PluginsTab.tsx +1 -1
- package/app/components/settings/Primitives.tsx +118 -6
- package/app/components/settings/SettingsContent.tsx +5 -2
- package/app/components/settings/UninstallTab.tsx +179 -0
- package/app/components/settings/UpdateTab.tsx +17 -5
- package/app/components/settings/types.ts +2 -1
- package/app/components/setup/StepDots.tsx +2 -2
- package/app/components/ui/dialog.tsx +1 -1
- package/app/hooks/useAiOrganize.ts +122 -10
- package/app/hooks/useMention.ts +21 -3
- package/app/hooks/useSlashCommand.ts +18 -4
- package/app/lib/agent/reconnect.ts +40 -0
- package/app/lib/core/backlinks.ts +2 -2
- package/app/lib/core/git.ts +14 -10
- package/app/lib/fs.ts +2 -1
- package/app/lib/i18n-en.ts +46 -2
- package/app/lib/i18n-zh.ts +46 -2
- package/app/lib/organize-history.ts +74 -0
- package/app/lib/settings.ts +2 -0
- package/app/lib/types.ts +2 -0
- package/app/next.config.ts +23 -5
- package/bin/cli.js +6 -9
- package/bin/lib/mcp-build.js +74 -0
- package/bin/lib/mcp-spawn.js +8 -5
- package/bin/lib/port.js +17 -2
- package/bin/lib/stop.js +12 -2
- package/mcp/dist/index.cjs +43 -43
- package/mcp/src/index.ts +58 -12
- package/package.json +1 -1
- package/scripts/setup.js +2 -2
|
@@ -1,14 +1,35 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { useRef, useEffect } from 'react';
|
|
4
|
-
import { Sparkles, Loader2, AlertCircle, Wrench } from 'lucide-react';
|
|
4
|
+
import { Sparkles, Loader2, AlertCircle, Wrench, WifiOff, Zap } from 'lucide-react';
|
|
5
5
|
import ReactMarkdown from 'react-markdown';
|
|
6
6
|
import remarkGfm from 'remark-gfm';
|
|
7
7
|
import type { Message } from '@/lib/types';
|
|
8
|
+
import { stripThinkingTags } from '@/hooks/useAiOrganize';
|
|
8
9
|
import ToolCallBlock from './ToolCallBlock';
|
|
9
10
|
import ThinkingBlock from './ThinkingBlock';
|
|
10
11
|
|
|
12
|
+
const SKILL_PREFIX_RE = /^Use the skill ([^:]+):\s*/;
|
|
13
|
+
|
|
14
|
+
function UserMessageContent({ content, skillName }: { content: string; skillName?: string }) {
|
|
15
|
+
const resolved = skillName ?? content.match(SKILL_PREFIX_RE)?.[1];
|
|
16
|
+
if (!resolved) return <>{content}</>;
|
|
17
|
+
const prefixMatch = content.match(SKILL_PREFIX_RE);
|
|
18
|
+
const rest = prefixMatch ? content.slice(prefixMatch[0].length) : content;
|
|
19
|
+
return (
|
|
20
|
+
<>
|
|
21
|
+
<span className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded-full text-[11px] font-medium bg-[var(--amber-foreground)]/15 text-[var(--amber-foreground)]/90 mr-1 align-middle">
|
|
22
|
+
<Zap size={10} className="shrink-0" />
|
|
23
|
+
{resolved}
|
|
24
|
+
</span>
|
|
25
|
+
{rest}
|
|
26
|
+
</>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
11
30
|
function AssistantMessage({ content, isStreaming }: { content: string; isStreaming: boolean }) {
|
|
31
|
+
const cleaned = stripThinkingTags(content);
|
|
32
|
+
if (!cleaned && !isStreaming) return null;
|
|
12
33
|
return (
|
|
13
34
|
<div className="prose prose-sm prose-panel dark:prose-invert max-w-none text-foreground
|
|
14
35
|
prose-p:my-1 prose-p:leading-relaxed
|
|
@@ -22,7 +43,7 @@ function AssistantMessage({ content, isStreaming }: { content: string; isStreami
|
|
|
22
43
|
prose-strong:text-foreground prose-strong:font-semibold
|
|
23
44
|
prose-table:text-xs prose-th:py-1 prose-td:py-1
|
|
24
45
|
">
|
|
25
|
-
<ReactMarkdown remarkPlugins={[remarkGfm]}>{
|
|
46
|
+
<ReactMarkdown remarkPlugins={[remarkGfm]}>{cleaned}</ReactMarkdown>
|
|
26
47
|
{isStreaming && (
|
|
27
48
|
<span className="inline-block w-1.5 h-3.5 bg-amber-400 ml-0.5 align-middle animate-pulse rounded-sm" />
|
|
28
49
|
)}
|
|
@@ -88,7 +109,7 @@ function StepCounter({ parts }: { parts: Message['parts'] }) {
|
|
|
88
109
|
interface MessageListProps {
|
|
89
110
|
messages: Message[];
|
|
90
111
|
isLoading: boolean;
|
|
91
|
-
loadingPhase: 'connecting' | 'thinking' | 'streaming';
|
|
112
|
+
loadingPhase: 'connecting' | 'thinking' | 'streaming' | 'reconnecting';
|
|
92
113
|
emptyPrompt: string;
|
|
93
114
|
suggestions: readonly string[];
|
|
94
115
|
onSuggestionClick: (text: string) => void;
|
|
@@ -96,6 +117,7 @@ interface MessageListProps {
|
|
|
96
117
|
connecting: string;
|
|
97
118
|
thinking: string;
|
|
98
119
|
generating: string;
|
|
120
|
+
reconnecting?: string;
|
|
99
121
|
};
|
|
100
122
|
}
|
|
101
123
|
|
|
@@ -146,7 +168,7 @@ export default function MessageList({
|
|
|
146
168
|
<div
|
|
147
169
|
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)]"
|
|
148
170
|
>
|
|
149
|
-
{m.content}
|
|
171
|
+
<UserMessageContent content={m.content} skillName={m.skillName} />
|
|
150
172
|
</div>
|
|
151
173
|
) : m.content.startsWith('__error__') ? (
|
|
152
174
|
<div className="max-w-[85%] px-3 py-2.5 rounded-xl rounded-bl-sm border border-error/20 bg-error/8 text-sm">
|
|
@@ -157,7 +179,7 @@ export default function MessageList({
|
|
|
157
179
|
</div>
|
|
158
180
|
) : (
|
|
159
181
|
<div className="max-w-[85%] px-3 py-2 rounded-xl rounded-bl-sm bg-muted text-foreground text-sm">
|
|
160
|
-
{(m.parts && m.parts.length > 0) || m.content ? (
|
|
182
|
+
{(m.parts && m.parts.length > 0) || stripThinkingTags(m.content) ? (
|
|
161
183
|
<>
|
|
162
184
|
<AssistantMessageWithParts message={m} isStreaming={isLoading && i === messages.length - 1} />
|
|
163
185
|
{isLoading && i === messages.length - 1 && (
|
|
@@ -166,13 +188,19 @@ export default function MessageList({
|
|
|
166
188
|
</>
|
|
167
189
|
) : isLoading && i === messages.length - 1 ? (
|
|
168
190
|
<div className="flex items-center gap-2 py-1">
|
|
169
|
-
|
|
191
|
+
{loadingPhase === 'reconnecting' ? (
|
|
192
|
+
<WifiOff size={14} className="text-[var(--amber)] animate-pulse" />
|
|
193
|
+
) : (
|
|
194
|
+
<Loader2 size={14} className="animate-spin text-[var(--amber)]" />
|
|
195
|
+
)}
|
|
170
196
|
<span className="text-xs text-muted-foreground animate-pulse">
|
|
171
|
-
{loadingPhase === '
|
|
172
|
-
? labels.
|
|
173
|
-
: loadingPhase === '
|
|
174
|
-
? labels.
|
|
175
|
-
:
|
|
197
|
+
{loadingPhase === 'reconnecting'
|
|
198
|
+
? (labels.reconnecting ?? 'Reconnecting...')
|
|
199
|
+
: loadingPhase === 'connecting'
|
|
200
|
+
? labels.connecting
|
|
201
|
+
: loadingPhase === 'thinking'
|
|
202
|
+
? labels.thinking
|
|
203
|
+
: labels.generating}
|
|
176
204
|
</span>
|
|
177
205
|
</div>
|
|
178
206
|
) : null}
|
|
@@ -3,14 +3,16 @@
|
|
|
3
3
|
import { useEffect, useRef } from 'react';
|
|
4
4
|
import { Zap } from 'lucide-react';
|
|
5
5
|
import type { SlashItem } from '@/hooks/useSlashCommand';
|
|
6
|
+
import HighlightMatch from './HighlightMatch';
|
|
6
7
|
|
|
7
8
|
interface SlashCommandPopoverProps {
|
|
8
9
|
results: SlashItem[];
|
|
9
10
|
selectedIndex: number;
|
|
11
|
+
query?: string;
|
|
10
12
|
onSelect: (item: SlashItem) => void;
|
|
11
13
|
}
|
|
12
14
|
|
|
13
|
-
export default function SlashCommandPopover({ results, selectedIndex, onSelect }: SlashCommandPopoverProps) {
|
|
15
|
+
export default function SlashCommandPopover({ results, selectedIndex, query, onSelect }: SlashCommandPopoverProps) {
|
|
14
16
|
const listRef = useRef<HTMLDivElement>(null);
|
|
15
17
|
|
|
16
18
|
useEffect(() => {
|
|
@@ -45,7 +47,7 @@ export default function SlashCommandPopover({ results, selectedIndex, onSelect }
|
|
|
45
47
|
}`}
|
|
46
48
|
>
|
|
47
49
|
<Zap size={13} className="text-[var(--amber)] shrink-0" />
|
|
48
|
-
<span className="text-sm font-medium shrink-0"
|
|
50
|
+
<span className="text-sm font-medium shrink-0">/<HighlightMatch text={item.name} query={query} /></span>
|
|
49
51
|
{item.description && (
|
|
50
52
|
<span className="text-2xs text-muted-foreground/50 truncate min-w-0 flex-1">{item.description}</span>
|
|
51
53
|
)}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import Link from 'next/link';
|
|
4
|
-
import { useEffect, useMemo, useState } from 'react';
|
|
4
|
+
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
5
5
|
import { usePathname } from 'next/navigation';
|
|
6
6
|
import { History, X } from 'lucide-react';
|
|
7
7
|
import { apiFetch } from '@/lib/api';
|
|
@@ -14,6 +14,8 @@ interface ChangeSummaryPayload {
|
|
|
14
14
|
export default function ChangesBanner() {
|
|
15
15
|
const [unreadCount, setUnreadCount] = useState(0);
|
|
16
16
|
const [dismissedAtCount, setDismissedAtCount] = useState<number | null>(null);
|
|
17
|
+
const [autoDismissed, setAutoDismissed] = useState(false);
|
|
18
|
+
const prevUnreadRef = useRef(0);
|
|
17
19
|
const [isRendered, setIsRendered] = useState(false);
|
|
18
20
|
const [isVisible, setIsVisible] = useState(false);
|
|
19
21
|
const pathname = usePathname();
|
|
@@ -37,12 +39,28 @@ export default function ChangesBanner() {
|
|
|
37
39
|
};
|
|
38
40
|
}, []);
|
|
39
41
|
|
|
42
|
+
// Re-show banner when new changes arrive after auto-dismiss
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
if (unreadCount > prevUnreadRef.current && autoDismissed) {
|
|
45
|
+
setAutoDismissed(false);
|
|
46
|
+
}
|
|
47
|
+
prevUnreadRef.current = unreadCount;
|
|
48
|
+
}, [unreadCount, autoDismissed]);
|
|
49
|
+
|
|
40
50
|
const shouldShow = useMemo(() => {
|
|
41
51
|
if (unreadCount <= 0) return false;
|
|
42
52
|
if (pathname?.startsWith('/changes')) return false;
|
|
43
53
|
if (dismissedAtCount !== null && unreadCount <= dismissedAtCount) return false;
|
|
54
|
+
if (autoDismissed) return false;
|
|
44
55
|
return true;
|
|
45
|
-
}, [dismissedAtCount, pathname, unreadCount]);
|
|
56
|
+
}, [dismissedAtCount, pathname, unreadCount, autoDismissed]);
|
|
57
|
+
|
|
58
|
+
// Auto-dismiss after 10 seconds
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
if (!shouldShow) return;
|
|
61
|
+
const timer = setTimeout(() => setAutoDismissed(true), 10_000);
|
|
62
|
+
return () => clearTimeout(timer);
|
|
63
|
+
}, [shouldShow]);
|
|
46
64
|
|
|
47
65
|
useEffect(() => {
|
|
48
66
|
const durationMs = 160;
|
|
@@ -7,6 +7,14 @@ import { apiFetch } from '@/lib/api';
|
|
|
7
7
|
import { useLocale } from '@/lib/LocaleContext';
|
|
8
8
|
import { collapseDiffContext, buildLineDiff } from './line-diff';
|
|
9
9
|
|
|
10
|
+
/** Semantic color for operation type badges */
|
|
11
|
+
function opColorClass(op: string): string {
|
|
12
|
+
if (op.startsWith('create') || op === 'import_file') return 'text-success';
|
|
13
|
+
if (op.startsWith('delete')) return 'text-error';
|
|
14
|
+
if (op.startsWith('rename') || op.startsWith('move')) return 'text-muted-foreground';
|
|
15
|
+
return ''; // update_lines, update_section — default foreground
|
|
16
|
+
}
|
|
17
|
+
|
|
10
18
|
interface ChangeEvent {
|
|
11
19
|
id: string;
|
|
12
20
|
ts: string;
|
|
@@ -225,7 +233,7 @@ export default function ChangesContentPage({ initialPath = '' }: { initialPath?:
|
|
|
225
233
|
>
|
|
226
234
|
{event.path}
|
|
227
235
|
</span>
|
|
228
|
-
<span>{event.op}</span>
|
|
236
|
+
<span className={opColorClass(event.op)}>{event.op}</span>
|
|
229
237
|
<span>·</span>
|
|
230
238
|
<span>{sourceLabel(event.source)}</span>
|
|
231
239
|
<span>·</span>
|
|
@@ -234,7 +242,7 @@ export default function ChangesContentPage({ initialPath = '' }: { initialPath?:
|
|
|
234
242
|
</div>
|
|
235
243
|
<Link
|
|
236
244
|
href={`/view/${event.path.split('/').map(encodeURIComponent).join('/')}`}
|
|
237
|
-
className="inline-flex items-center rounded-md px-2 py-0.5 text-xs font-medium bg-[var(--amber-dim)] text-[var(--amber)] focus-visible:ring-2 focus-visible:ring-ring hover:opacity-90"
|
|
245
|
+
className="inline-flex items-center rounded-md px-2 py-0.5 text-xs font-medium bg-[var(--amber-dim)] text-[var(--amber-text)] focus-visible:ring-2 focus-visible:ring-ring hover:opacity-90"
|
|
238
246
|
onClick={(e) => e.stopPropagation()}
|
|
239
247
|
>
|
|
240
248
|
{t.changes.open}
|
|
@@ -27,7 +27,7 @@ export function EchoHero({
|
|
|
27
27
|
aria-hidden
|
|
28
28
|
/>
|
|
29
29
|
<div className="relative pl-4 sm:pl-5">
|
|
30
|
-
<p className="mb-4 font-sans text-2xs font-semibold uppercase tracking-[0.2em] text-[var(--amber)]">
|
|
30
|
+
<p className="mb-4 font-sans text-2xs font-semibold uppercase tracking-[0.2em] text-[var(--amber-text)]">
|
|
31
31
|
{heroKicker}
|
|
32
32
|
</p>
|
|
33
33
|
<h1 id={titleId} className="font-display text-2xl font-semibold tracking-tight text-foreground md:text-3xl">
|
|
@@ -138,7 +138,7 @@ export function EchoInsightCollapsible({
|
|
|
138
138
|
open ? 'grid-rows-[1fr]' : 'grid-rows-[0fr]',
|
|
139
139
|
)}
|
|
140
140
|
>
|
|
141
|
-
<div className="overflow-hidden">
|
|
141
|
+
<div className="overflow-hidden" {...(!open && { inert: true } as React.HTMLAttributes<HTMLDivElement>)}>
|
|
142
142
|
<div className="border-t border-border/60 px-5 pb-5 pt-4">
|
|
143
143
|
<p className="font-sans text-sm leading-relaxed text-muted-foreground">{hint}</p>
|
|
144
144
|
<div className="mt-4 flex flex-wrap items-center gap-2">
|
|
@@ -43,7 +43,7 @@ export function EchoFactSnapshot({
|
|
|
43
43
|
<p className="mt-2 font-sans font-medium text-foreground">{emptyTitle}</p>
|
|
44
44
|
</div>
|
|
45
45
|
</div>
|
|
46
|
-
<span className="font-sans text-2xs font-medium uppercase tracking-wide text-[var(--amber)] sm:mt-0.5 sm:shrink-0 rounded-md bg-[var(--amber-dim)] px-2 py-1">
|
|
46
|
+
<span className="font-sans text-2xs font-medium uppercase tracking-wide text-[var(--amber-text)] sm:mt-0.5 sm:shrink-0 rounded-md bg-[var(--amber-dim)] px-2 py-1">
|
|
47
47
|
{snapshotBadge}
|
|
48
48
|
</span>
|
|
49
49
|
</div>
|
|
@@ -30,7 +30,7 @@ export default function UseCaseCard({ icon, title, description, prompt, tryItLab
|
|
|
30
30
|
</div>
|
|
31
31
|
<button
|
|
32
32
|
onClick={() => openAskModal(prompt, 'user')}
|
|
33
|
-
className="self-start inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium transition-all duration-150 hover:opacity-80 cursor-pointer bg-[var(--amber-dim)] text-[var(--amber)]"
|
|
33
|
+
className="self-start inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium transition-all duration-150 hover:opacity-80 cursor-pointer bg-[var(--amber-dim)] text-[var(--amber-text)]"
|
|
34
34
|
>
|
|
35
35
|
{tryItLabel} →
|
|
36
36
|
</button>
|
|
@@ -35,7 +35,7 @@ function UseCaseRow({
|
|
|
35
35
|
<span className="text-xs text-foreground truncate flex-1">{title}</span>
|
|
36
36
|
<button
|
|
37
37
|
onClick={() => openAskModal(prompt, 'user')}
|
|
38
|
-
className="opacity-0 group-hover:opacity-100 text-2xs px-2 py-0.5 rounded text-[var(--amber)] bg-[var(--amber-dim)] hover:opacity-80 transition-all duration-150 shrink-0"
|
|
38
|
+
className="opacity-0 group-hover:opacity-100 text-2xs px-2 py-0.5 rounded text-[var(--amber-text)] bg-[var(--amber-dim)] hover:opacity-80 transition-all duration-150 shrink-0"
|
|
39
39
|
>
|
|
40
40
|
{tryLabel}
|
|
41
41
|
</button>
|
|
@@ -154,31 +154,35 @@ export default function DiscoverPanel({ active, maximized, onMaximize }: Discove
|
|
|
154
154
|
</span>
|
|
155
155
|
<span className="text-2xs text-muted-foreground tabular-nums">{enabledCount}/{renderers.length}</span>
|
|
156
156
|
</button>
|
|
157
|
-
{showPlugins
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
157
|
+
<div className={`grid transition-[grid-template-rows] duration-200 ease-out ${showPlugins ? 'grid-rows-[1fr]' : 'grid-rows-[0fr]'}`}>
|
|
158
|
+
<div className="overflow-hidden" {...(!showPlugins && { inert: true } as React.HTMLAttributes<HTMLDivElement>)}>
|
|
159
|
+
{renderers.map(r => {
|
|
160
|
+
const enabled = isRendererEnabled(r.id);
|
|
161
|
+
const fileExists = r.entryPath ? existingFiles.has(r.entryPath) : false;
|
|
162
|
+
const canOpen = enabled && r.entryPath && fileExists;
|
|
163
|
+
return (
|
|
164
|
+
<div
|
|
165
|
+
key={r.id}
|
|
166
|
+
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' : ''}`}
|
|
167
|
+
onClick={canOpen ? () => handleOpenPlugin(r.entryPath!) : undefined}
|
|
168
|
+
role={canOpen ? 'link' : undefined}
|
|
169
|
+
tabIndex={canOpen ? 0 : undefined}
|
|
170
|
+
onKeyDown={canOpen ? (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); handleOpenPlugin(r.entryPath!); } } : undefined}
|
|
171
|
+
>
|
|
172
|
+
<span className="text-sm shrink-0" suppressHydrationWarning>{r.icon}</span>
|
|
173
|
+
<span className="text-xs text-foreground truncate flex-1">{r.name}</span>
|
|
174
|
+
{r.core ? (
|
|
175
|
+
<span className="text-2xs px-1.5 py-0.5 rounded bg-muted text-muted-foreground shrink-0">{p.core}</span>
|
|
176
|
+
) : (
|
|
177
|
+
<div onClick={e => e.stopPropagation()}>
|
|
178
|
+
<Toggle checked={enabled} onChange={v => handleToggle(r.id, v)} size="sm" />
|
|
179
|
+
</div>
|
|
180
|
+
)}
|
|
177
181
|
</div>
|
|
178
|
-
)
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
+
);
|
|
183
|
+
})}
|
|
184
|
+
</div>
|
|
185
|
+
</div>
|
|
182
186
|
</div>
|
|
183
187
|
|
|
184
188
|
<div className="mx-4 border-t border-border" />
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect, useCallback } from 'react';
|
|
4
|
+
import { useRouter } from 'next/navigation';
|
|
5
|
+
import {
|
|
6
|
+
ChevronDown, FilePlus, FileEdit, ExternalLink, Trash2, FileInput,
|
|
7
|
+
} from 'lucide-react';
|
|
8
|
+
import { useLocale } from '@/lib/LocaleContext';
|
|
9
|
+
import { encodePath } from '@/lib/utils';
|
|
10
|
+
import PanelHeader from './PanelHeader';
|
|
11
|
+
import {
|
|
12
|
+
loadHistory, clearHistory,
|
|
13
|
+
type OrganizeHistoryEntry,
|
|
14
|
+
} from '@/lib/organize-history';
|
|
15
|
+
|
|
16
|
+
interface ImportHistoryPanelProps {
|
|
17
|
+
active: boolean;
|
|
18
|
+
maximized?: boolean;
|
|
19
|
+
onMaximize?: () => void;
|
|
20
|
+
/** Incremented externally to trigger a refresh */
|
|
21
|
+
refreshToken?: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function formatTime(ts: number): string {
|
|
25
|
+
return new Date(ts).toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' });
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function groupByDate(entries: OrganizeHistoryEntry[]): Map<string, OrganizeHistoryEntry[]> {
|
|
29
|
+
const groups = new Map<string, OrganizeHistoryEntry[]>();
|
|
30
|
+
const today = new Date();
|
|
31
|
+
const todayStr = today.toDateString();
|
|
32
|
+
const yesterday = new Date(today);
|
|
33
|
+
yesterday.setDate(yesterday.getDate() - 1);
|
|
34
|
+
const yesterdayStr = yesterday.toDateString();
|
|
35
|
+
|
|
36
|
+
for (const entry of entries) {
|
|
37
|
+
const d = new Date(entry.timestamp).toDateString();
|
|
38
|
+
let label: string;
|
|
39
|
+
if (d === todayStr) label = 'Today';
|
|
40
|
+
else if (d === yesterdayStr) label = 'Yesterday';
|
|
41
|
+
else label = new Date(entry.timestamp).toLocaleDateString(undefined, { month: 'short', day: 'numeric' });
|
|
42
|
+
|
|
43
|
+
const list = groups.get(label) ?? [];
|
|
44
|
+
list.push(entry);
|
|
45
|
+
groups.set(label, list);
|
|
46
|
+
}
|
|
47
|
+
return groups;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export default function ImportHistoryPanel({ active, maximized, onMaximize, refreshToken }: ImportHistoryPanelProps) {
|
|
51
|
+
const { t } = useLocale();
|
|
52
|
+
const router = useRouter();
|
|
53
|
+
const [entries, setEntries] = useState<OrganizeHistoryEntry[]>([]);
|
|
54
|
+
const [expandedId, setExpandedId] = useState<string | null>(null);
|
|
55
|
+
const hi = (t as unknown as Record<string, Record<string, unknown>>).importHistory ?? {};
|
|
56
|
+
|
|
57
|
+
const refresh = useCallback(() => {
|
|
58
|
+
setEntries(loadHistory());
|
|
59
|
+
}, []);
|
|
60
|
+
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
if (active) refresh();
|
|
63
|
+
}, [active, refresh, refreshToken]);
|
|
64
|
+
|
|
65
|
+
useEffect(() => {
|
|
66
|
+
const handler = () => refresh();
|
|
67
|
+
window.addEventListener('mindos:organize-history-update', handler);
|
|
68
|
+
return () => window.removeEventListener('mindos:organize-history-update', handler);
|
|
69
|
+
}, [refresh]);
|
|
70
|
+
|
|
71
|
+
const handleClearAll = useCallback(() => {
|
|
72
|
+
clearHistory();
|
|
73
|
+
setEntries([]);
|
|
74
|
+
}, []);
|
|
75
|
+
|
|
76
|
+
const handleViewFile = useCallback((path: string) => {
|
|
77
|
+
router.push(`/view/${encodePath(path)}`);
|
|
78
|
+
}, [router]);
|
|
79
|
+
|
|
80
|
+
const groups = groupByDate(entries);
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
<div className={`flex flex-col h-full ${active ? '' : 'hidden'}`}>
|
|
84
|
+
<PanelHeader
|
|
85
|
+
title={hi.title as string ?? 'Import History'}
|
|
86
|
+
maximized={maximized}
|
|
87
|
+
onMaximize={onMaximize}
|
|
88
|
+
>
|
|
89
|
+
{entries.length > 0 && (
|
|
90
|
+
<button
|
|
91
|
+
onClick={handleClearAll}
|
|
92
|
+
className="p-1 rounded hover:bg-muted text-muted-foreground hover:text-foreground transition-colors"
|
|
93
|
+
title={hi.clearAll as string ?? 'Clear history'}
|
|
94
|
+
>
|
|
95
|
+
<Trash2 size={13} />
|
|
96
|
+
</button>
|
|
97
|
+
)}
|
|
98
|
+
</PanelHeader>
|
|
99
|
+
|
|
100
|
+
<div className="flex-1 overflow-y-auto min-h-0 px-2 py-2">
|
|
101
|
+
{entries.length === 0 ? (
|
|
102
|
+
<div className="flex flex-col items-center justify-center gap-2 py-12 text-center">
|
|
103
|
+
<FileInput size={28} className="text-muted-foreground/30" />
|
|
104
|
+
<p className="text-xs text-muted-foreground">
|
|
105
|
+
{hi.emptyTitle as string ?? 'No import history yet'}
|
|
106
|
+
</p>
|
|
107
|
+
<p className="text-2xs text-muted-foreground/60 max-w-[200px]">
|
|
108
|
+
{hi.emptyDesc as string ?? 'AI organize results will appear here'}
|
|
109
|
+
</p>
|
|
110
|
+
</div>
|
|
111
|
+
) : (
|
|
112
|
+
<div className="space-y-4">
|
|
113
|
+
{Array.from(groups.entries()).map(([label, items]) => (
|
|
114
|
+
<div key={label}>
|
|
115
|
+
<p className="text-2xs font-medium text-muted-foreground/60 uppercase tracking-wider px-2 mb-1.5">{label}</p>
|
|
116
|
+
<div className="space-y-1">
|
|
117
|
+
{items.map(entry => {
|
|
118
|
+
const isExpanded = expandedId === entry.id;
|
|
119
|
+
const createdCount = entry.files.filter(f => f.action === 'create' && f.ok && !f.undone).length;
|
|
120
|
+
const updatedCount = entry.files.filter(f => f.action === 'update' && f.ok && !f.undone).length;
|
|
121
|
+
const undoneCount = entry.files.filter(f => f.undone).length;
|
|
122
|
+
const sourceLabel = entry.sourceFiles.length === 1
|
|
123
|
+
? entry.sourceFiles[0]
|
|
124
|
+
: `${entry.sourceFiles.length} files`;
|
|
125
|
+
|
|
126
|
+
return (
|
|
127
|
+
<div key={entry.id} className="rounded-lg border border-border/50 overflow-hidden">
|
|
128
|
+
<button
|
|
129
|
+
type="button"
|
|
130
|
+
onClick={() => setExpandedId(isExpanded ? null : entry.id)}
|
|
131
|
+
className="w-full flex items-center gap-2 px-3 py-2.5 text-left hover:bg-muted/30 transition-colors"
|
|
132
|
+
>
|
|
133
|
+
<FileInput size={14} className="text-[var(--amber)] shrink-0" />
|
|
134
|
+
<div className="flex-1 min-w-0">
|
|
135
|
+
<p className="text-xs text-foreground truncate">{sourceLabel}</p>
|
|
136
|
+
<p className="text-2xs text-muted-foreground/60">
|
|
137
|
+
{formatTime(entry.timestamp)}
|
|
138
|
+
{createdCount > 0 && ` · ${createdCount} created`}
|
|
139
|
+
{updatedCount > 0 && ` · ${updatedCount} updated`}
|
|
140
|
+
{undoneCount > 0 && ` · ${undoneCount} undone`}
|
|
141
|
+
</p>
|
|
142
|
+
</div>
|
|
143
|
+
<ChevronDown
|
|
144
|
+
size={12}
|
|
145
|
+
className={`text-muted-foreground/40 shrink-0 transition-transform duration-150 ${isExpanded ? 'rotate-180' : ''}`}
|
|
146
|
+
/>
|
|
147
|
+
</button>
|
|
148
|
+
|
|
149
|
+
{isExpanded && (
|
|
150
|
+
<div className="border-t border-border/30 px-2 py-1.5 space-y-0.5">
|
|
151
|
+
{entry.files.map((f, idx) => {
|
|
152
|
+
const fileName = f.path.split('/').pop() ?? f.path;
|
|
153
|
+
return (
|
|
154
|
+
<div
|
|
155
|
+
key={`${f.path}-${idx}`}
|
|
156
|
+
className={`flex items-center gap-2 px-2 py-1.5 rounded text-xs ${f.undone ? 'opacity-40' : ''}`}
|
|
157
|
+
>
|
|
158
|
+
{f.action === 'create' ? (
|
|
159
|
+
<FilePlus size={12} className="text-success shrink-0" />
|
|
160
|
+
) : (
|
|
161
|
+
<FileEdit size={12} className="text-[var(--amber)] shrink-0" />
|
|
162
|
+
)}
|
|
163
|
+
<span className={`truncate flex-1 ${f.undone ? 'line-through text-muted-foreground' : 'text-foreground'}`}>
|
|
164
|
+
{fileName}
|
|
165
|
+
</span>
|
|
166
|
+
{f.undone && (
|
|
167
|
+
<span className="text-2xs text-muted-foreground shrink-0">undone</span>
|
|
168
|
+
)}
|
|
169
|
+
{f.ok && !f.undone && (
|
|
170
|
+
<button
|
|
171
|
+
type="button"
|
|
172
|
+
onClick={(e) => { e.stopPropagation(); handleViewFile(f.path); }}
|
|
173
|
+
className="text-muted-foreground/40 hover:text-[var(--amber)] transition-colors shrink-0"
|
|
174
|
+
title="View file"
|
|
175
|
+
>
|
|
176
|
+
<ExternalLink size={11} />
|
|
177
|
+
</button>
|
|
178
|
+
)}
|
|
179
|
+
</div>
|
|
180
|
+
);
|
|
181
|
+
})}
|
|
182
|
+
</div>
|
|
183
|
+
)}
|
|
184
|
+
</div>
|
|
185
|
+
);
|
|
186
|
+
})}
|
|
187
|
+
</div>
|
|
188
|
+
</div>
|
|
189
|
+
))}
|
|
190
|
+
</div>
|
|
191
|
+
)}
|
|
192
|
+
</div>
|
|
193
|
+
</div>
|
|
194
|
+
);
|
|
195
|
+
}
|
|
@@ -142,12 +142,12 @@ export default function PluginsPanel({ active, maximized, onMaximize }: PluginsP
|
|
|
142
142
|
</span>
|
|
143
143
|
))}
|
|
144
144
|
{r.entryPath && enabled && !fileExists && (
|
|
145
|
-
<span className="text-2xs text-[var(--amber)]">
|
|
145
|
+
<span className="text-2xs text-[var(--amber-text)]">
|
|
146
146
|
{(p.createFile ?? 'Create {file}').replace('{file}', r.entryPath)}
|
|
147
147
|
</span>
|
|
148
148
|
)}
|
|
149
149
|
{canOpen && (
|
|
150
|
-
<span className="text-2xs text-[var(--amber)]">
|
|
150
|
+
<span className="text-2xs text-[var(--amber-text)]">
|
|
151
151
|
→ {r.entryPath}
|
|
152
152
|
</span>
|
|
153
153
|
)}
|
|
@@ -48,6 +48,12 @@ export function AiTab({ data, updateAi, updateAgent, t }: AiTabProps) {
|
|
|
48
48
|
// Cleanup ok timer
|
|
49
49
|
useEffect(() => () => { if (okTimerRef.current) clearTimeout(okTimerRef.current); }, []);
|
|
50
50
|
|
|
51
|
+
// Sync reconnectRetries to localStorage so AskContent can read it without fetching settings
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
const v = data.agent?.reconnectRetries ?? 3;
|
|
54
|
+
try { localStorage.setItem('mindos-reconnect-retries', String(v)); } catch {}
|
|
55
|
+
}, [data.agent?.reconnectRetries]);
|
|
56
|
+
|
|
51
57
|
const handleTestKey = useCallback(async (providerName: 'anthropic' | 'openai') => {
|
|
52
58
|
const prov = data.ai.providers?.[providerName] ?? {} as ProviderConfig;
|
|
53
59
|
setTestResult(prev => ({ ...prev, [providerName]: { state: 'testing' } }));
|
|
@@ -249,6 +255,24 @@ export function AiTab({ data, updateAi, updateAgent, t }: AiTabProps) {
|
|
|
249
255
|
</Select>
|
|
250
256
|
</Field>
|
|
251
257
|
|
|
258
|
+
<Field label={t.settings.agent.reconnectRetries} hint={t.settings.agent.reconnectRetriesHint}>
|
|
259
|
+
<Select
|
|
260
|
+
value={String(data.agent?.reconnectRetries ?? 3)}
|
|
261
|
+
onChange={e => {
|
|
262
|
+
const v = Number(e.target.value);
|
|
263
|
+
updateAgent({ reconnectRetries: v });
|
|
264
|
+
try { localStorage.setItem('mindos-reconnect-retries', String(v)); } catch {}
|
|
265
|
+
}}
|
|
266
|
+
>
|
|
267
|
+
<option value="0">Off</option>
|
|
268
|
+
<option value="1">1</option>
|
|
269
|
+
<option value="2">2</option>
|
|
270
|
+
<option value="3">3</option>
|
|
271
|
+
<option value="5">5</option>
|
|
272
|
+
<option value="10">10</option>
|
|
273
|
+
</Select>
|
|
274
|
+
</Field>
|
|
275
|
+
|
|
252
276
|
{provider === 'anthropic' && (
|
|
253
277
|
<>
|
|
254
278
|
<div className="flex items-center justify-between">
|
|
@@ -269,7 +269,7 @@ export function KnowledgeTab({ data, setData, t }: KnowledgeTabProps) {
|
|
|
269
269
|
)}
|
|
270
270
|
</div>
|
|
271
271
|
{revealedToken && (
|
|
272
|
-
<p className="text-xs text-[var(--amber)]">
|
|
272
|
+
<p className="text-xs text-[var(--amber-text)]">
|
|
273
273
|
New token generated. Copy it now — it won't be shown in full again.
|
|
274
274
|
</p>
|
|
275
275
|
)}
|
|
@@ -129,7 +129,7 @@ export default function SkillCreateForm({ onSave, onCancel, saving, error, m }:
|
|
|
129
129
|
onClick={() => handleTemplateChange(tmpl)}
|
|
130
130
|
className={`px-2.5 py-1 text-xs transition-colors ${i > 0 ? 'border-l border-border' : ''} ${
|
|
131
131
|
selectedTemplate === tmpl
|
|
132
|
-
? 'bg-[var(--amber-subtle)] text-[var(--amber)] font-medium'
|
|
132
|
+
? 'bg-[var(--amber-subtle)] text-[var(--amber-text)] font-medium'
|
|
133
133
|
: 'text-muted-foreground hover:bg-muted'
|
|
134
134
|
}`}
|
|
135
135
|
>
|
|
@@ -51,7 +51,7 @@ export default function SkillRow({
|
|
|
51
51
|
{expanded ? <ChevronDown size={12} /> : <ChevronRight size={12} />}
|
|
52
52
|
<span className="text-xs font-medium flex-1">{skill.name}</span>
|
|
53
53
|
<span className={`text-2xs px-1.5 py-0.5 rounded ${
|
|
54
|
-
skill.source === 'builtin' ? 'bg-muted text-muted-foreground' : 'bg-[var(--amber-subtle)] text-[var(--amber)]'
|
|
54
|
+
skill.source === 'builtin' ? 'bg-muted text-muted-foreground' : 'bg-[var(--amber-subtle)] text-[var(--amber-text)]'
|
|
55
55
|
}`}>
|
|
56
56
|
{skill.source === 'builtin' ? (m?.skillBuiltin ?? 'Built-in') : (m?.skillUser ?? 'Custom')}
|
|
57
57
|
</span>
|
|
@@ -239,7 +239,7 @@ export default function SkillsSection({ t }: McpSkillsSectionProps) {
|
|
|
239
239
|
onClick={() => handleLangSwitch('en')}
|
|
240
240
|
disabled={switchingLang}
|
|
241
241
|
className={`px-2.5 py-1 text-xs transition-colors disabled:opacity-50 disabled:cursor-not-allowed ${
|
|
242
|
-
currentLang === 'en' ? 'bg-[var(--amber-subtle)] text-[var(--amber)] font-medium' : 'text-muted-foreground hover:bg-muted'
|
|
242
|
+
currentLang === 'en' ? 'bg-[var(--amber-subtle)] text-[var(--amber-text)] font-medium' : 'text-muted-foreground hover:bg-muted'
|
|
243
243
|
}`}
|
|
244
244
|
>
|
|
245
245
|
{m?.skillLangEn ?? 'English'}
|
|
@@ -248,7 +248,7 @@ export default function SkillsSection({ t }: McpSkillsSectionProps) {
|
|
|
248
248
|
onClick={() => handleLangSwitch('zh')}
|
|
249
249
|
disabled={switchingLang}
|
|
250
250
|
className={`px-2.5 py-1 text-xs transition-colors disabled:opacity-50 disabled:cursor-not-allowed border-l border-border ${
|
|
251
|
-
currentLang === 'zh' ? 'bg-[var(--amber-subtle)] text-[var(--amber)] font-medium' : 'text-muted-foreground hover:bg-muted'
|
|
251
|
+
currentLang === 'zh' ? 'bg-[var(--amber-subtle)] text-[var(--amber-text)] font-medium' : 'text-muted-foreground hover:bg-muted'
|
|
252
252
|
}`}
|
|
253
253
|
>
|
|
254
254
|
{m?.skillLangZh ?? '中文'}
|
|
@@ -226,7 +226,7 @@ function AgentConfigViewer({ connectedAgents, detectedAgents, notFoundAgents, cu
|
|
|
226
226
|
{m?.tagConnected ?? 'Connected'}
|
|
227
227
|
</span>
|
|
228
228
|
) : currentAgent.present && !currentAgent.installed ? (
|
|
229
|
-
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-2xs font-medium bg-[var(--amber-subtle)] text-[var(--amber)]">
|
|
229
|
+
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-2xs font-medium bg-[var(--amber-subtle)] text-[var(--amber-text)]">
|
|
230
230
|
<span className="w-1.5 h-1.5 rounded-full bg-[var(--amber)] inline-block" />
|
|
231
231
|
{m?.tagDetected ?? 'Detected — not configured'}
|
|
232
232
|
</span>
|
|
@@ -266,7 +266,7 @@ function AgentConfigViewer({ connectedAgents, detectedAgents, notFoundAgents, cu
|
|
|
266
266
|
|
|
267
267
|
{/* Auth warning */}
|
|
268
268
|
{transport === 'http' && mcpStatus && !mcpStatus.authConfigured && (
|
|
269
|
-
<p className="flex items-center gap-1.5 text-xs text-[var(--amber)]">
|
|
269
|
+
<p className="flex items-center gap-1.5 text-xs text-[var(--amber-text)]">
|
|
270
270
|
<AlertCircle size={12} />
|
|
271
271
|
{m?.noAuthWarning ?? 'Auth not configured. Run `mindos token` to set up.'}
|
|
272
272
|
</p>
|