@geminilight/mindos 0.6.7 → 0.6.12

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 (85) hide show
  1. package/README.md +2 -0
  2. package/README_zh.md +2 -0
  3. package/app/app/api/ask/route.ts +35 -2
  4. package/app/app/api/file/route.ts +27 -0
  5. package/app/app/api/mcp/install/route.ts +4 -1
  6. package/app/app/api/setup/check-path/route.ts +2 -7
  7. package/app/app/api/setup/check-port/route.ts +18 -13
  8. package/app/app/api/setup/ls/route.ts +3 -9
  9. package/app/app/api/setup/path-utils.ts +8 -0
  10. package/app/app/api/setup/route.ts +2 -7
  11. package/app/app/api/uninstall/route.ts +47 -0
  12. package/app/app/globals.css +11 -0
  13. package/app/components/ActivityBar.tsx +10 -3
  14. package/app/components/AskFab.tsx +7 -3
  15. package/app/components/CreateSpaceModal.tsx +1 -1
  16. package/app/components/DirView.tsx +1 -1
  17. package/app/components/FileTree.tsx +30 -23
  18. package/app/components/GuideCard.tsx +1 -1
  19. package/app/components/HomeContent.tsx +137 -109
  20. package/app/components/ImportModal.tsx +104 -60
  21. package/app/components/MarkdownView.tsx +3 -0
  22. package/app/components/OnboardingView.tsx +1 -1
  23. package/app/components/OrganizeToast.tsx +386 -0
  24. package/app/components/Panel.tsx +23 -2
  25. package/app/components/Sidebar.tsx +1 -1
  26. package/app/components/SidebarLayout.tsx +44 -1
  27. package/app/components/agents/AgentDetailContent.tsx +33 -12
  28. package/app/components/agents/AgentsMcpSection.tsx +1 -1
  29. package/app/components/agents/AgentsOverviewSection.tsx +3 -4
  30. package/app/components/agents/AgentsPrimitives.tsx +2 -2
  31. package/app/components/agents/AgentsSkillsSection.tsx +2 -2
  32. package/app/components/agents/SkillDetailPopover.tsx +24 -8
  33. package/app/components/ask/AskContent.tsx +124 -70
  34. package/app/components/ask/HighlightMatch.tsx +14 -0
  35. package/app/components/ask/MentionPopover.tsx +5 -3
  36. package/app/components/ask/MessageList.tsx +39 -11
  37. package/app/components/ask/SlashCommandPopover.tsx +4 -2
  38. package/app/components/changes/ChangesBanner.tsx +20 -2
  39. package/app/components/changes/ChangesContentPage.tsx +10 -2
  40. package/app/components/echo/EchoHero.tsx +1 -1
  41. package/app/components/echo/EchoInsightCollapsible.tsx +1 -1
  42. package/app/components/echo/EchoPageSections.tsx +1 -1
  43. package/app/components/explore/UseCaseCard.tsx +1 -1
  44. package/app/components/panels/DiscoverPanel.tsx +29 -25
  45. package/app/components/panels/ImportHistoryPanel.tsx +195 -0
  46. package/app/components/panels/PluginsPanel.tsx +2 -2
  47. package/app/components/settings/AiTab.tsx +24 -0
  48. package/app/components/settings/KnowledgeTab.tsx +1 -1
  49. package/app/components/settings/McpSkillCreateForm.tsx +1 -1
  50. package/app/components/settings/McpSkillRow.tsx +1 -1
  51. package/app/components/settings/McpSkillsSection.tsx +2 -2
  52. package/app/components/settings/McpTab.tsx +2 -2
  53. package/app/components/settings/PluginsTab.tsx +1 -1
  54. package/app/components/settings/Primitives.tsx +118 -6
  55. package/app/components/settings/SettingsContent.tsx +5 -2
  56. package/app/components/settings/UninstallTab.tsx +179 -0
  57. package/app/components/settings/UpdateTab.tsx +17 -5
  58. package/app/components/settings/types.ts +2 -1
  59. package/app/components/ui/dialog.tsx +1 -1
  60. package/app/hooks/useAiOrganize.ts +450 -0
  61. package/app/hooks/useFileImport.ts +39 -2
  62. package/app/hooks/useMention.ts +21 -3
  63. package/app/hooks/useSlashCommand.ts +18 -4
  64. package/app/lib/agent/reconnect.ts +40 -0
  65. package/app/lib/core/backlinks.ts +2 -2
  66. package/app/lib/core/git.ts +14 -10
  67. package/app/lib/fs.ts +2 -1
  68. package/app/lib/i18n-en.ts +85 -4
  69. package/app/lib/i18n-zh.ts +85 -4
  70. package/app/lib/organize-history.ts +74 -0
  71. package/app/lib/settings.ts +2 -0
  72. package/app/lib/types.ts +2 -0
  73. package/app/next-env.d.ts +1 -1
  74. package/app/next.config.ts +23 -5
  75. package/app/package.json +1 -1
  76. package/bin/cli.js +21 -18
  77. package/bin/lib/mcp-build.js +74 -0
  78. package/bin/lib/mcp-spawn.js +8 -5
  79. package/bin/lib/port.js +17 -2
  80. package/bin/lib/stop.js +12 -2
  81. package/mcp/dist/index.cjs +43 -43
  82. package/mcp/src/index.ts +58 -12
  83. package/package.json +1 -1
  84. package/scripts/release.sh +1 -1
  85. 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]}>{content}</ReactMarkdown>
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
- <Loader2 size={14} className="animate-spin text-[var(--amber)]" />
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 === 'connecting'
172
- ? labels.connecting
173
- : loadingPhase === 'thinking'
174
- ? labels.thinking
175
- : labels.generating}
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">/{item.name}</span>
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 && 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" />
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
- </div>
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&apos;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>