@geminilight/mindos 0.6.22 → 0.6.25

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 (62) hide show
  1. package/README.md +58 -46
  2. package/README_zh.md +58 -46
  3. package/app/app/.well-known/agent-card.json/route.ts +34 -0
  4. package/app/app/api/a2a/route.ts +100 -0
  5. package/app/app/api/file/import/route.ts +0 -2
  6. package/app/app/api/setup/route.ts +2 -0
  7. package/app/components/Backlinks.tsx +2 -2
  8. package/app/components/Breadcrumb.tsx +1 -1
  9. package/app/components/CsvView.tsx +41 -19
  10. package/app/components/DirView.tsx +2 -2
  11. package/app/components/FileTree.tsx +14 -1
  12. package/app/components/GuideCard.tsx +6 -2
  13. package/app/components/HomeContent.tsx +2 -2
  14. package/app/components/RightAskPanel.tsx +17 -10
  15. package/app/components/SearchModal.tsx +3 -3
  16. package/app/components/SidebarLayout.tsx +4 -2
  17. package/app/components/SyncStatusBar.tsx +2 -2
  18. package/app/components/ask/AskContent.tsx +6 -6
  19. package/app/components/ask/FileChip.tsx +1 -1
  20. package/app/components/ask/MentionPopover.tsx +2 -2
  21. package/app/components/ask/MessageList.tsx +1 -1
  22. package/app/components/ask/SlashCommandPopover.tsx +1 -1
  23. package/app/components/explore/UseCaseCard.tsx +2 -2
  24. package/app/components/help/HelpContent.tsx +6 -1
  25. package/app/components/panels/AgentsPanelAgentDetail.tsx +2 -2
  26. package/app/components/panels/DiscoverPanel.tsx +3 -3
  27. package/app/components/panels/PanelNavRow.tsx +2 -2
  28. package/app/components/panels/PluginsPanel.tsx +1 -1
  29. package/app/components/panels/SearchPanel.tsx +3 -3
  30. package/app/components/renderers/summary/SummaryRenderer.tsx +1 -1
  31. package/app/components/settings/AiTab.tsx +4 -4
  32. package/app/components/settings/KnowledgeTab.tsx +1 -1
  33. package/app/components/settings/McpTab.tsx +22 -4
  34. package/app/components/settings/UpdateTab.tsx +1 -1
  35. package/app/components/setup/index.tsx +9 -3
  36. package/app/components/walkthrough/WalkthroughProvider.tsx +2 -2
  37. package/app/hooks/useAskPanel.ts +7 -3
  38. package/app/hooks/useFileImport.ts +1 -1
  39. package/app/lib/a2a/agent-card.ts +107 -0
  40. package/app/lib/a2a/index.ts +23 -0
  41. package/app/lib/a2a/task-handler.ts +228 -0
  42. package/app/lib/a2a/types.ts +158 -0
  43. package/app/lib/agent/tools.ts +1 -1
  44. package/app/lib/core/fs-ops.ts +3 -2
  45. package/app/lib/fs.ts +28 -11
  46. package/app/lib/i18n-en.ts +2 -0
  47. package/app/lib/i18n-zh.ts +2 -0
  48. package/app/lib/settings.ts +1 -1
  49. package/bin/cli.js +48 -20
  50. package/bin/commands/agent.js +18 -0
  51. package/bin/commands/api.js +58 -0
  52. package/bin/commands/ask.js +101 -0
  53. package/bin/commands/file.js +286 -0
  54. package/bin/commands/search.js +51 -0
  55. package/bin/commands/space.js +167 -0
  56. package/bin/commands/status.js +69 -0
  57. package/bin/lib/command.js +156 -0
  58. package/mcp/dist/index.cjs +1 -1
  59. package/mcp/src/index.ts +1 -1
  60. package/package.json +1 -1
  61. package/skills/mindos/SKILL.md +2 -2
  62. package/skills/mindos-zh/SKILL.md +2 -2
@@ -6,7 +6,7 @@ import { FileNode } from '@/lib/types';
6
6
  import { encodePath } from '@/lib/utils';
7
7
  import {
8
8
  ChevronDown, FileText, Table, Folder, FolderOpen, Plus, Loader2,
9
- Trash2, Pencil, Layers, ScrollText, FolderInput,
9
+ Trash2, Pencil, Layers, ScrollText, FolderInput, Copy,
10
10
  } from 'lucide-react';
11
11
  import { createFileAction, deleteFileAction, renameFileAction, renameSpaceAction, deleteSpaceAction, convertToSpaceAction, deleteFolderAction } from '@/lib/actions';
12
12
  import { useLocale } from '@/lib/LocaleContext';
@@ -16,6 +16,10 @@ function notifyFilesChanged() {
16
16
  window.dispatchEvent(new Event('mindos:files-changed'));
17
17
  }
18
18
 
19
+ async function copyPathToClipboard(path: string) {
20
+ try { await navigator.clipboard.writeText(path); } catch { /* noop */ }
21
+ }
22
+
19
23
  const SYSTEM_FILES = new Set(['INSTRUCTION.md', 'README.md']);
20
24
 
21
25
  const HIDDEN_FILES_KEY = 'show-hidden-files';
@@ -141,6 +145,9 @@ function SpaceContextMenu({ x, y, node, onClose, onRename, onImport, onDelete }:
141
145
  <FolderInput size={14} className="shrink-0" /> {t.fileTree.importFile}
142
146
  </button>
143
147
  )}
148
+ <button className={MENU_ITEM} onClick={() => { copyPathToClipboard(node.path); onClose(); }}>
149
+ <Copy size={14} className="shrink-0" /> {t.fileTree.copyPath}
150
+ </button>
144
151
  <button className={MENU_ITEM} onClick={() => { onRename(); onClose(); }}>
145
152
  <Pencil size={14} className="shrink-0" /> {t.fileTree.renameSpace}
146
153
  </button>
@@ -173,6 +180,9 @@ function FolderContextMenu({ x, y, node, onClose, onRename, onDelete }: {
173
180
  }}>
174
181
  <Layers size={14} className="shrink-0 text-[var(--amber)]" /> {t.fileTree.convertToSpace}
175
182
  </button>
183
+ <button className={MENU_ITEM} onClick={() => { copyPathToClipboard(node.path); onClose(); }}>
184
+ <Copy size={14} className="shrink-0" /> {t.fileTree.copyPath}
185
+ </button>
176
186
  <button className={MENU_ITEM} onClick={() => { onRename(); onClose(); }}>
177
187
  <Pencil size={14} className="shrink-0" /> {t.fileTree.rename}
178
188
  </button>
@@ -643,6 +653,9 @@ function FileNodeItem({ node, depth, currentPath, onNavigate }: {
643
653
  <span className="truncate leading-5" suppressHydrationWarning>{node.name}</span>
644
654
  </button>
645
655
  <div className="absolute right-1 top-1/2 -translate-y-1/2 hidden group-hover/file:flex items-center gap-0.5">
656
+ <button onClick={() => copyPathToClipboard(node.path)} className="p-0.5 rounded text-muted-foreground hover:text-foreground hover:bg-muted transition-colors" title={t.fileTree.copyPath}>
657
+ <Copy size={12} />
658
+ </button>
646
659
  <button onClick={startRename} className="p-0.5 rounded text-muted-foreground hover:text-foreground hover:bg-muted transition-colors" title={t.fileTree.rename}>
647
660
  <Pencil size={12} />
648
661
  </button>
@@ -29,7 +29,9 @@ export default function GuideCard() {
29
29
  setGuideState(null);
30
30
  }
31
31
  })
32
- .catch(() => {});
32
+ .catch((err) => {
33
+ console.warn('[GuideCard] Fetch guide state failed:', err);
34
+ });
33
35
  }, []);
34
36
 
35
37
  useEffect(() => {
@@ -54,7 +56,9 @@ export default function GuideCard() {
54
56
  method: 'PATCH',
55
57
  headers: { 'Content-Type': 'application/json' },
56
58
  body: JSON.stringify({ guideState: patch }),
57
- }).catch(() => {});
59
+ }).catch((err) => {
60
+ console.warn('[GuideCard] PATCH guide state failed:', err);
61
+ });
58
62
  }, []);
59
63
 
60
64
  const handleDismiss = useCallback(() => {
@@ -159,7 +159,7 @@ function FeatureChip({ id, icon, name, entryPath, active, inactiveTitle }: {
159
159
  );
160
160
 
161
161
  if (active && entryPath) {
162
- return <Link key={id} href={`/view/${encodePath(entryPath)}`}><span className={cls}>{inner}</span></Link>;
162
+ return <Link key={id} href={`/view/${encodePath(entryPath)}`} className={cls}>{inner}</Link>;
163
163
  }
164
164
  return <span key={id} className={cls} title={inactiveTitle}>{inner}</span>;
165
165
  }
@@ -618,7 +618,7 @@ function ExampleCleanupBanner() {
618
618
  useEffect(() => {
619
619
  scanExampleFilesAction().then(r => {
620
620
  if (r.files.length > 0) setCount(r.files.length);
621
- }).catch(() => {});
621
+ }).catch((err) => { console.warn("[HomeContent] scanExampleFilesAction failed:", err); });
622
622
  }, []);
623
623
 
624
624
  const handleCleanup = useCallback(async () => {
@@ -21,11 +21,14 @@ interface RightAskPanelProps {
21
21
  onWidthCommit: (w: number) => void;
22
22
  askMode?: 'panel' | 'popup';
23
23
  onModeSwitch?: () => void;
24
+ maximized?: boolean;
25
+ onMaximize?: () => void;
24
26
  }
25
27
 
26
28
  export default function RightAskPanel({
27
29
  open, onClose, currentFile, initialMessage, onFirstMessage,
28
30
  width, onWidthChange, onWidthCommit, askMode, onModeSwitch,
31
+ maximized = false, onMaximize,
29
32
  }: RightAskPanelProps) {
30
33
  const handleMouseDown = useResizeDrag({
31
34
  width,
@@ -42,10 +45,11 @@ export default function RightAskPanel({
42
45
  className={`
43
46
  hidden md:flex fixed top-0 right-0 h-screen z-40
44
47
  flex-col bg-card border-l border-border
45
- transition-transform duration-200 ease-out
48
+ transition-all duration-200 ease-out
46
49
  ${open ? 'translate-x-0' : 'translate-x-full pointer-events-none'}
50
+ ${maximized ? 'border-l-0' : ''}
47
51
  `}
48
- style={{ width: `${width}px` }}
52
+ style={{ width: maximized ? '100vw' : `${width}px` }}
49
53
  role="complementary"
50
54
  aria-label="MindOS Agent panel"
51
55
  >
@@ -61,7 +65,6 @@ export default function RightAskPanel({
61
65
  </button>
62
66
  </div>
63
67
  }>
64
- {/* Flex column + min-h-0 so MessageList flex-1 gets a bounded height (fragment children are direct flex items) */}
65
68
  <div className="flex min-h-0 w-full flex-1 flex-col overflow-hidden">
66
69
  <AskContent
67
70
  visible={open}
@@ -72,17 +75,21 @@ export default function RightAskPanel({
72
75
  onClose={onClose}
73
76
  askMode={askMode}
74
77
  onModeSwitch={onModeSwitch}
78
+ maximized={maximized}
79
+ onMaximize={onMaximize}
75
80
  />
76
81
  </div>
77
82
  </ErrorBoundary>
78
83
 
79
- {/* Drag resize handle — LEFT edge */}
80
- <div
81
- className="absolute top-0 -left-[3px] w-[6px] h-full cursor-col-resize z-40 group hidden md:block"
82
- onMouseDown={handleMouseDown}
83
- >
84
- <div className="absolute left-[2px] top-0 w-[2px] h-full opacity-0 group-hover:opacity-100 bg-[var(--amber)]/60 transition-opacity" />
85
- </div>
84
+ {/* Drag resize handle — LEFT edge (hidden when maximized) */}
85
+ {!maximized && (
86
+ <div
87
+ className="absolute top-0 -left-[3px] w-[6px] h-full cursor-col-resize z-40 group hidden md:block"
88
+ onMouseDown={handleMouseDown}
89
+ >
90
+ <div className="absolute left-[2px] top-0 w-[2px] h-full opacity-0 group-hover:opacity-100 bg-[var(--amber)]/60 transition-opacity" />
91
+ </div>
92
+ )}
86
93
  </aside>
87
94
  );
88
95
  }
@@ -171,13 +171,13 @@ export default function SearchModal({ open, onClose }: SearchModalProps) {
171
171
  }
172
172
  <div className="min-w-0 flex-1">
173
173
  <div className="flex items-baseline gap-2 flex-wrap">
174
- <span className="text-sm text-foreground font-medium truncate">{fileName}</span>
174
+ <span className="text-sm text-foreground font-medium truncate" title={fileName}>{fileName}</span>
175
175
  {dirPath && (
176
- <span className="text-xs text-muted-foreground truncate">{dirPath}</span>
176
+ <span className="text-xs text-muted-foreground truncate" title={dirPath}>{dirPath}</span>
177
177
  )}
178
178
  </div>
179
179
  {result.snippet && (
180
- <p className="text-xs text-muted-foreground mt-0.5 line-clamp-2 leading-relaxed">
180
+ <p className="text-xs text-muted-foreground mt-0.5 line-clamp-2 leading-relaxed" title={result.snippet}>
181
181
  {highlightSnippet(result.snippet, query)}
182
182
  </p>
183
183
  )}
@@ -413,6 +413,8 @@ export default function SidebarLayout({ fileTree, children }: SidebarLayoutProps
413
413
  onWidthCommit={ap.handleAskWidthCommit}
414
414
  askMode={ap.askMode}
415
415
  onModeSwitch={ap.handleAskModeSwitch}
416
+ maximized={ap.askMaximized}
417
+ onMaximize={ap.toggleAskMaximized}
416
418
  />
417
419
 
418
420
  <RightAgentDetailPanel
@@ -493,7 +495,7 @@ export default function SidebarLayout({ fileTree, children }: SidebarLayoutProps
493
495
 
494
496
  <main
495
497
  id="main-content"
496
- className="min-h-screen transition-all duration-200 pt-[52px] md:pt-0"
498
+ className={`min-h-screen transition-all duration-200 pt-[52px] md:pt-0 ${ap.askMaximized ? 'hidden' : ''}`}
497
499
  onDragEnter={(e) => {
498
500
  if (!e.dataTransfer.types.includes('Files')) return;
499
501
  e.preventDefault();
@@ -560,7 +562,7 @@ export default function SidebarLayout({ fileTree, children }: SidebarLayoutProps
560
562
  <style>{`
561
563
  @media (min-width: 768px) {
562
564
  :root {
563
- --right-panel-width: ${ap.askPanelOpen ? ap.askPanelWidth : 0}px;
565
+ --right-panel-width: ${ap.askMaximized ? '100vw' : `${ap.askPanelOpen ? ap.askPanelWidth : 0}px`};
564
566
  --right-agent-detail-width: ${agentDockOpen ? agentDetailWidth : 0}px;
565
567
  }
566
568
  #main-content {
@@ -168,7 +168,7 @@ export default function SyncStatusBar({ collapsed, onOpenSyncSettings }: SyncSta
168
168
  const prevLevelRef = useRef<StatusLevel>('off');
169
169
  const [hintDismissed, setHintDismissed] = useState(() => {
170
170
  if (typeof window !== 'undefined') {
171
- try { return !!localStorage.getItem('sync-hint-dismissed'); } catch {}
171
+ try { return !!localStorage.getItem('sync-hint-dismissed'); } catch (err) { console.warn("[SyncStatusBar] localStorage read failed:", err); }
172
172
  }
173
173
  return false;
174
174
  });
@@ -219,7 +219,7 @@ export default function SyncStatusBar({ collapsed, onOpenSyncSettings }: SyncSta
219
219
  <button
220
220
  onClick={(e) => {
221
221
  e.stopPropagation();
222
- try { localStorage.setItem('sync-hint-dismissed', '1'); } catch {}
222
+ try { localStorage.setItem('sync-hint-dismissed', '1'); } catch (err) { console.warn("[SyncStatusBar] localStorage write dismissed:", err); }
223
223
  setHintDismissed(true);
224
224
  }}
225
225
  className="p-1 rounded hover:bg-muted hover:text-foreground transition-colors shrink-0 ml-2 text-muted-foreground/50 hover:text-muted-foreground"
@@ -442,7 +442,7 @@ export default function AskContent({ visible, currentFile, initialMessage, onFir
442
442
  } else if (typeof errBody?.message === 'string' && errBody.message.trim()) {
443
443
  errorMsg = errBody.message;
444
444
  }
445
- } catch {}
445
+ } catch (err) { console.warn("[AskContent] error body parse failed:", err); }
446
446
  const err = new Error(errorMsg);
447
447
  (err as Error & { httpStatus?: number }).httpStatus = res.status;
448
448
  throw err;
@@ -601,24 +601,24 @@ export default function AskContent({ visible, currentFile, initialMessage, onFir
601
601
  </span>
602
602
  </div>
603
603
  <div className="flex items-center gap-1">
604
- <button type="button" onClick={() => setShowHistory(v => !v)} aria-pressed={showHistory} className={`p-1 rounded transition-colors ${showHistory ? 'bg-muted text-foreground' : 'text-muted-foreground hover:text-foreground hover:bg-muted'}`} title="Session history">
604
+ <button type="button" onClick={() => setShowHistory(v => !v)} aria-pressed={showHistory} className={`p-1.5 rounded transition-colors ${showHistory ? 'bg-muted text-foreground' : 'text-muted-foreground hover:text-foreground hover:bg-muted'}`} title="Session history">
605
605
  <History size={iconSize} />
606
606
  </button>
607
- <button type="button" onClick={handleResetSession} disabled={isLoading} className="p-1 rounded hover:bg-muted text-muted-foreground hover:text-foreground transition-colors disabled:opacity-40" title="New session">
607
+ <button type="button" onClick={handleResetSession} disabled={isLoading} className="p-1.5 rounded hover:bg-muted text-muted-foreground hover:text-foreground transition-colors disabled:opacity-40" title="New session">
608
608
  <RotateCcw size={iconSize} />
609
609
  </button>
610
610
  {isPanel && onMaximize && (
611
- <button type="button" onClick={onMaximize} className="p-1 rounded hover:bg-muted text-muted-foreground hover:text-foreground transition-colors" title={maximized ? 'Restore panel' : 'Maximize panel'}>
611
+ <button type="button" onClick={onMaximize} className="p-1.5 rounded hover:bg-muted text-muted-foreground hover:text-foreground transition-colors" title={maximized ? 'Restore panel' : 'Maximize panel'}>
612
612
  {maximized ? <Minimize2 size={iconSize} /> : <Maximize2 size={iconSize} />}
613
613
  </button>
614
614
  )}
615
615
  {onModeSwitch && (
616
- <button type="button" onClick={onModeSwitch} className="p-1 rounded hover:bg-muted text-muted-foreground hover:text-foreground transition-colors" title={askMode === 'popup' ? 'Dock to side panel' : 'Open as popup'}>
616
+ <button type="button" onClick={onModeSwitch} className="p-1.5 rounded hover:bg-muted text-muted-foreground hover:text-foreground transition-colors" title={askMode === 'popup' ? 'Dock to side panel' : 'Open as popup'}>
617
617
  {askMode === 'popup' ? <PanelRight size={iconSize} /> : <AppWindow size={iconSize} />}
618
618
  </button>
619
619
  )}
620
620
  {onClose && (
621
- <button type="button" onClick={onClose} className="p-1 rounded hover:bg-muted text-muted-foreground hover:text-foreground transition-colors" aria-label="Close">
621
+ <button type="button" onClick={onClose} className="p-1.5 rounded hover:bg-muted text-muted-foreground hover:text-foreground transition-colors" aria-label="Close">
622
622
  <X size={isPanel ? iconSize : 15} />
623
623
  </button>
624
624
  )}
@@ -22,7 +22,7 @@ export default function FileChip({ path, onRemove, variant = 'kb' }: FileChipPro
22
22
  type="button"
23
23
  onClick={onRemove}
24
24
  aria-label={`Remove ${name}`}
25
- className="text-muted-foreground hover:text-foreground ml-0.5 shrink-0"
25
+ className="p-1 -mr-1 rounded hover:bg-muted hover:text-foreground transition-colors shrink-0"
26
26
  >
27
27
  <X size={10} />
28
28
  </button>
@@ -54,9 +54,9 @@ export default function MentionPopover({ results, selectedIndex, query, onSelect
54
54
  ) : (
55
55
  <FileText size={13} className="text-muted-foreground shrink-0" />
56
56
  )}
57
- <span className="truncate font-medium flex-1"><HighlightMatch text={name} query={query} /></span>
57
+ <span className="truncate font-medium flex-1" title={name}><HighlightMatch text={name} query={query} /></span>
58
58
  {dir && (
59
- <span className="text-2xs text-muted-foreground/40 truncate max-w-[140px] shrink-0">
59
+ <span className="text-2xs text-muted-foreground/40 truncate max-w-[140px] shrink-0" title={dir}>
60
60
  <HighlightMatch text={dir} query={query} />
61
61
  </span>
62
62
  )}
@@ -137,7 +137,7 @@ export default function MessageList({
137
137
  }, [messages]);
138
138
 
139
139
  return (
140
- <div className="flex-1 overflow-y-auto px-4 py-4 space-y-4 min-h-0">
140
+ <div className="flex-1 overflow-y-auto overflow-x-hidden px-4 py-4 space-y-4 min-h-0">
141
141
  {messages.length === 0 && (
142
142
  <div className="mt-6 space-y-3">
143
143
  <p className="text-center text-sm text-muted-foreground/60">{emptyPrompt}</p>
@@ -49,7 +49,7 @@ export default function SlashCommandPopover({ results, selectedIndex, query, onS
49
49
  <Zap size={13} className="text-[var(--amber)] shrink-0" />
50
50
  <span className="text-sm font-medium shrink-0">/<HighlightMatch text={item.name} query={query} /></span>
51
51
  {item.description && (
52
- <span className="text-2xs text-muted-foreground/50 truncate min-w-0 flex-1">{item.description}</span>
52
+ <span className="text-2xs text-muted-foreground/50 truncate min-w-0 flex-1" title={item.description}>{item.description}</span>
53
53
  )}
54
54
  </button>
55
55
  ))}
@@ -20,10 +20,10 @@ export default function UseCaseCard({ icon, title, description, prompt, tryItLab
20
20
  {icon}
21
21
  </span>
22
22
  <div className="flex-1 min-w-0">
23
- <h3 className="text-sm font-semibold font-display truncate text-foreground">
23
+ <h3 className="text-sm font-semibold font-display truncate text-foreground" title={title}>
24
24
  {title}
25
25
  </h3>
26
- <p className="text-xs leading-relaxed mt-1 line-clamp-2 text-muted-foreground">
26
+ <p className="text-xs leading-relaxed mt-1 line-clamp-2 text-muted-foreground" title={description}>
27
27
  {description}
28
28
  </p>
29
29
  </div>
@@ -59,7 +59,12 @@ function PromptBlock({ text, copyLabel }: { text: string; copyLabel: string }) {
59
59
  navigator.clipboard.writeText(clean).then(() => {
60
60
  setCopied(true);
61
61
  setTimeout(() => setCopied(false), 1500);
62
- }).catch(() => {});
62
+ }).catch((err) => {
63
+ console.error('[HelpContent] Clipboard copy failed:', err);
64
+ // Show error feedback in UI
65
+ setCopied(true); // Reuse copied state to show error
66
+ setTimeout(() => setCopied(false), 2000);
67
+ });
63
68
  }, [text]);
64
69
 
65
70
  return (
@@ -85,7 +85,7 @@ export default function AgentsPanelAgentDetail({
85
85
  <header className="shrink-0 flex items-center justify-between gap-3 border-b border-border px-4 py-3 bg-card">
86
86
  <div className="flex items-center gap-2.5 min-w-0">
87
87
  <span className={`w-2 h-2 rounded-full shrink-0 ${dot}`} />
88
- <h2 className="text-sm font-semibold text-foreground truncate font-display">{agent.name}</h2>
88
+ <h2 className="text-sm font-semibold text-foreground truncate font-display" title={agent.name}>{agent.name}</h2>
89
89
  </div>
90
90
  <button
91
91
  type="button"
@@ -107,7 +107,7 @@ export default function AgentsPanelAgentDetail({
107
107
  {copy.backToList}
108
108
  </button>
109
109
  <span className={`w-1.5 h-1.5 rounded-full shrink-0 ${dot}`} />
110
- <span className="text-sm font-medium text-foreground truncate">{agent.name}</span>
110
+ <span className="text-sm font-medium text-foreground truncate" title={agent.name}>{agent.name}</span>
111
111
  </div>
112
112
  )}
113
113
 
@@ -32,7 +32,7 @@ function UseCaseRow({
32
32
  return (
33
33
  <div className="group flex items-center gap-2.5 px-4 py-1.5 hover:bg-muted/50 transition-colors rounded-sm mx-1">
34
34
  <span className="text-muted-foreground shrink-0">{icon}</span>
35
- <span className="text-xs text-foreground truncate flex-1">{title}</span>
35
+ <span className="text-xs text-foreground truncate flex-1" title={title}>{title}</span>
36
36
  <button
37
37
  onClick={() => openAskModal(prompt, 'user')}
38
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"
@@ -85,7 +85,7 @@ export default function DiscoverPanel({ active, maximized, onMaximize }: Discove
85
85
  const pathSet = new Set(allPaths);
86
86
  setExistingFiles(new Set(entryPaths.filter(ep => pathSet.has(ep))));
87
87
  })
88
- .catch(() => {});
88
+ .catch((err) => { console.warn("[DiscoverPanel] fetch /api/files failed:", err); });
89
89
  }, [pluginsMounted]);
90
90
 
91
91
  const handleToggle = useCallback((id: string, enabled: boolean) => {
@@ -170,7 +170,7 @@ export default function DiscoverPanel({ active, maximized, onMaximize }: Discove
170
170
  onKeyDown={canOpen ? (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); handleOpenPlugin(r.entryPath!); } } : undefined}
171
171
  >
172
172
  <span className="text-sm shrink-0" suppressHydrationWarning>{r.icon}</span>
173
- <span className="text-xs text-foreground truncate flex-1">{r.name}</span>
173
+ <span className="text-xs text-foreground truncate flex-1" title={r.name}>{r.name}</span>
174
174
  {r.core ? (
175
175
  <span className="text-2xs px-1.5 py-0.5 rounded bg-muted text-muted-foreground shrink-0">{p.core}</span>
176
176
  ) : (
@@ -28,9 +28,9 @@ export function PanelNavRow({
28
28
  <>
29
29
  <span className="flex h-7 w-7 shrink-0 items-center justify-center rounded-md bg-muted">{icon}</span>
30
30
  <span className="flex-1 min-w-0">
31
- <span className="block text-left text-sm font-medium text-foreground truncate">{title}</span>
31
+ <span className="block text-left text-sm font-medium text-foreground truncate" title={title}>{title}</span>
32
32
  {subtitle ? (
33
- <span className="block text-left text-2xs text-muted-foreground truncate">{subtitle}</span>
33
+ <span className="block text-left text-2xs text-muted-foreground truncate" title={subtitle}>{subtitle}</span>
34
34
  ) : null}
35
35
  </span>
36
36
  {badge}
@@ -44,7 +44,7 @@ export default function PluginsPanel({ active, maximized, onMaximize }: PluginsP
44
44
  const pathSet = new Set(allPaths);
45
45
  setExistingFiles(new Set(entryPaths.filter(p => pathSet.has(p))));
46
46
  })
47
- .catch(() => {});
47
+ .catch((err) => { console.warn("[PluginsPanel] fetch /api/files failed:", err); });
48
48
  }, [mounted]);
49
49
 
50
50
  const renderers = mounted ? getPluginRenderers() : [];
@@ -150,13 +150,13 @@ export default function SearchPanel({ active, onNavigate, maximized, onMaximize
150
150
  }
151
151
  <div className="min-w-0 flex-1">
152
152
  <div className="flex items-baseline gap-2 flex-wrap">
153
- <span className="text-sm text-foreground font-medium truncate">{fileName}</span>
153
+ <span className="text-sm text-foreground font-medium truncate" title={fileName}>{fileName}</span>
154
154
  {dirPath && (
155
- <span className="text-xs text-muted-foreground truncate">{dirPath}</span>
155
+ <span className="text-xs text-muted-foreground truncate" title={dirPath}>{dirPath}</span>
156
156
  )}
157
157
  </div>
158
158
  {result.snippet && (
159
- <p className="text-xs text-muted-foreground mt-0.5 line-clamp-2 leading-relaxed">
159
+ <p className="text-xs text-muted-foreground mt-0.5 line-clamp-2 leading-relaxed" title={result.snippet}>
160
160
  {highlightSnippet(result.snippet, query)}
161
161
  </p>
162
162
  )}
@@ -55,7 +55,7 @@ export function SummaryRenderer({ filePath }: RendererContext) {
55
55
  useEffect(() => {
56
56
  apiFetch<RecentFile[]>(`/api/recent-files?limit=${LIMIT}`)
57
57
  .then((data) => setRecentFiles(data.filter(f => f.path.endsWith('.md'))))
58
- .catch(() => {});
58
+ .catch((err) => { console.warn("[SummaryRenderer] fetch recent-files failed:", err); });
59
59
  }, [filePath]);
60
60
 
61
61
  async function generate() {
@@ -51,7 +51,7 @@ export function AiTab({ data, updateAi, updateAgent, t }: AiTabProps) {
51
51
  // Sync reconnectRetries to localStorage so AskContent can read it without fetching settings
52
52
  useEffect(() => {
53
53
  const v = data.agent?.reconnectRetries ?? 3;
54
- try { localStorage.setItem('mindos-reconnect-retries', String(v)); } catch {}
54
+ try { localStorage.setItem('mindos-reconnect-retries', String(v)); } catch (err) { console.warn("[AiTab] localStorage setItem reconnectRetries failed:", err); }
55
55
  }, [data.agent?.reconnectRetries]);
56
56
 
57
57
  const handleTestKey = useCallback(async (providerName: 'anthropic' | 'openai') => {
@@ -271,7 +271,7 @@ export function AiTab({ data, updateAi, updateAgent, t }: AiTabProps) {
271
271
  onChange={e => {
272
272
  const v = Number(e.target.value);
273
273
  updateAgent({ reconnectRetries: v });
274
- try { localStorage.setItem('mindos-reconnect-retries', String(v)); } catch {}
274
+ try { localStorage.setItem('mindos-reconnect-retries', String(v)); } catch (err) { console.warn("[AiTab] localStorage setItem reconnectRetries failed:", err); }
275
275
  }}
276
276
  >
277
277
  <option value="0">Off</option>
@@ -434,13 +434,13 @@ function AskDisplayMode() {
434
434
  try {
435
435
  const stored = localStorage.getItem('ask-mode');
436
436
  if (stored === 'popup') setMode('popup');
437
- } catch {}
437
+ } catch (err) { console.warn("[AiTab] localStorage getItem ask-mode failed:", err); }
438
438
  }, []);
439
439
 
440
440
  const handleChange = (value: string) => {
441
441
  const next = value as 'panel' | 'popup';
442
442
  setMode(next);
443
- try { localStorage.setItem('ask-mode', next); } catch {}
443
+ try { localStorage.setItem('ask-mode', next); } catch (err) { console.warn("[AiTab] localStorage setItem ask-mode failed:", err); }
444
444
  // Notify SidebarLayout to pick up the change
445
445
  window.dispatchEvent(new StorageEvent('storage', { key: 'ask-mode', newValue: next }));
446
446
  };
@@ -26,7 +26,7 @@ export function KnowledgeTab({ data, setData, t }: KnowledgeTabProps) {
26
26
  const [cleanupResult, setCleanupResult] = useState<number | null>(null);
27
27
 
28
28
  useEffect(() => {
29
- scanExampleFilesAction().then(r => setExampleCount(r.files.length)).catch(() => {});
29
+ scanExampleFilesAction().then(r => setExampleCount(r.files.length)).catch((err) => { console.warn("[KnowledgeTab] scanExampleFilesAction failed:", err); });
30
30
  }, []);
31
31
 
32
32
  // Guide state toggle
@@ -52,15 +52,33 @@ export function McpTab({ t }: McpTabProps) {
52
52
  restarting={restarting}
53
53
  onRestart={async () => {
54
54
  setRestarting(true);
55
- try { await apiFetch('/api/mcp/restart', { method: 'POST' }); } catch {}
55
+ try {
56
+ await apiFetch('/api/mcp/restart', { method: 'POST' });
57
+ } catch (err) {
58
+ console.error('[McpTab] Restart request failed:', err);
59
+ setRestarting(false);
60
+ return; // Exit early, don't start polling if restart request fails
61
+ }
56
62
  const deadline = Date.now() + 60_000;
57
63
  clearInterval(restartPollRef.current);
58
64
  restartPollRef.current = setInterval(async () => {
59
- if (Date.now() > deadline) { clearInterval(restartPollRef.current); setRestarting(false); return; }
65
+ if (Date.now() > deadline) {
66
+ clearInterval(restartPollRef.current);
67
+ setRestarting(false);
68
+ console.warn('[McpTab] MCP restart timed out after 60s');
69
+ return;
70
+ }
60
71
  try {
61
72
  const s = await apiFetch<McpStatus>('/api/mcp/status', { timeout: 3000 });
62
- if (s.running) { clearInterval(restartPollRef.current); setRestarting(false); mcp.refresh(); }
63
- } catch {}
73
+ if (s.running) {
74
+ clearInterval(restartPollRef.current);
75
+ setRestarting(false);
76
+ mcp.refresh();
77
+ }
78
+ } catch (err) {
79
+ console.warn('[McpTab] Status poll attempt failed:', err);
80
+ // Continue polling on individual failures
81
+ }
64
82
  }, 3000);
65
83
  }}
66
84
  onRefresh={mcp.refresh}
@@ -80,7 +80,7 @@ function DesktopUpdateTab() {
80
80
  useEffect(() => {
81
81
  bridge.getAppInfo?.().then((info) => {
82
82
  if (info?.version) setAppVersion(info.version);
83
- }).catch(() => {});
83
+ }).catch((err) => { console.warn("[UpdateTab] getAppInfo failed:", err); });
84
84
  handleCheck();
85
85
  const cleanups: Array<() => void> = [];
86
86
  if (bridge.onUpdateProgress) {
@@ -230,9 +230,15 @@ export default function SetupWizard() {
230
230
  }, []);
231
231
 
232
232
  const copyToken = useCallback(() => {
233
- copyToClipboard(state.authToken).catch(() => {});
234
- setTokenCopied(true);
235
- setTimeout(() => setTokenCopied(false), 2000);
233
+ copyToClipboard(state.authToken)
234
+ .then(() => {
235
+ setTokenCopied(true);
236
+ setTimeout(() => setTokenCopied(false), 2000);
237
+ })
238
+ .catch((err) => {
239
+ console.error('[Setup] Token copy failed:', err);
240
+ // Show error toast instead of success
241
+ });
236
242
  }, [state.authToken]);
237
243
 
238
244
  const checkPort = useCallback(async (port: number, which: 'web' | 'mcp') => {
@@ -42,7 +42,7 @@ export default function WalkthroughProvider({ children }: WalkthroughProviderPro
42
42
  walkthroughDismissed: dismissed,
43
43
  },
44
44
  }),
45
- }).catch(() => {});
45
+ }).catch((err) => { console.warn("[WalkthroughProvider] localStorage setItem failed:", err); });
46
46
  }, []);
47
47
 
48
48
  // Check for auto-start via ?welcome=1 or guideState
@@ -81,7 +81,7 @@ export default function WalkthroughProvider({ children }: WalkthroughProviderPro
81
81
  setCurrentStep(gs.walkthroughStep);
82
82
  }
83
83
  })
84
- .catch(() => {});
84
+ .catch((err) => { console.warn("[WalkthroughProvider] guideState read failed:", err); });
85
85
  }, [totalSteps]);
86
86
 
87
87
  const start = useCallback(() => {
@@ -7,6 +7,7 @@ import { useAskModal } from './useAskModal';
7
7
  export interface AskPanelState {
8
8
  askPanelOpen: boolean;
9
9
  askPanelWidth: number;
10
+ askMaximized: boolean;
10
11
  askMode: 'panel' | 'popup';
11
12
  desktopAskPopupOpen: boolean;
12
13
  askInitialMessage: string;
@@ -17,6 +18,7 @@ export interface AskPanelState {
17
18
  handleAskWidthChange: (w: number) => void;
18
19
  handleAskWidthCommit: (w: number) => void;
19
20
  handleAskModeSwitch: () => void;
21
+ toggleAskMaximized: () => void;
20
22
  }
21
23
 
22
24
  /**
@@ -29,6 +31,7 @@ export function useAskPanel(): AskPanelState {
29
31
  const [askMode, setAskMode] = useState<'panel' | 'popup'>('panel');
30
32
  const [desktopAskPopupOpen, setDesktopAskPopupOpen] = useState(false);
31
33
  const [askInitialMessage, setAskInitialMessage] = useState('');
34
+ const [askMaximized, setAskMaximized] = useState(false);
32
35
  const [askOpenSource, setAskOpenSource] = useState<'user' | 'guide' | 'guide-next'>('user');
33
36
 
34
37
  const askModal = useAskModal();
@@ -82,7 +85,8 @@ export function useAskPanel(): AskPanelState {
82
85
  }
83
86
  }, [askMode]);
84
87
 
85
- const closeAskPanel = useCallback(() => setAskPanelOpen(false), []);
88
+ const closeAskPanel = useCallback(() => { setAskPanelOpen(false); setAskMaximized(false); }, []);
89
+ const toggleAskMaximized = useCallback(() => setAskMaximized(v => !v), []);
86
90
  const closeDesktopAskPopup = useCallback(() => setDesktopAskPopupOpen(false), []);
87
91
 
88
92
  const handleAskWidthChange = useCallback((w: number) => setAskPanelWidth(w), []);
@@ -109,9 +113,9 @@ export function useAskPanel(): AskPanelState {
109
113
  }, []);
110
114
 
111
115
  return {
112
- askPanelOpen, askPanelWidth, askMode, desktopAskPopupOpen,
116
+ askPanelOpen, askPanelWidth, askMaximized, askMode, desktopAskPopupOpen,
113
117
  askInitialMessage, askOpenSource,
114
118
  toggleAskPanel, closeAskPanel, closeDesktopAskPopup,
115
- handleAskWidthChange, handleAskWidthCommit, handleAskModeSwitch,
119
+ handleAskWidthChange, handleAskWidthCommit, handleAskModeSwitch, toggleAskMaximized,
116
120
  };
117
121
  }
@@ -106,7 +106,7 @@ export function useFileImport() {
106
106
  const merged = [...prev];
107
107
  for (const f of newFiles) {
108
108
  const isDup = merged.some(m =>
109
- m.name === f.name && m.size === f.size
109
+ m.name === f.name && m.size === f.size && m.file.lastModified === f.file.lastModified
110
110
  );
111
111
  if (!isDup && merged.length < MAX_FILES) merged.push(f);
112
112
  }