@geminilight/mindos 0.6.23 → 0.6.27

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 (66) hide show
  1. package/README.md +19 -3
  2. package/README_zh.md +19 -3
  3. package/app/app/.well-known/agent-card.json/route.ts +34 -0
  4. package/app/app/api/a2a/discover/route.ts +23 -0
  5. package/app/app/api/a2a/route.ts +100 -0
  6. package/app/components/Backlinks.tsx +2 -2
  7. package/app/components/Breadcrumb.tsx +1 -1
  8. package/app/components/CreateSpaceModal.tsx +1 -0
  9. package/app/components/CsvView.tsx +41 -19
  10. package/app/components/DirView.tsx +2 -2
  11. package/app/components/GuideCard.tsx +6 -2
  12. package/app/components/HomeContent.tsx +1 -1
  13. package/app/components/ImportModal.tsx +3 -0
  14. package/app/components/OnboardingView.tsx +1 -0
  15. package/app/components/RightAskPanel.tsx +4 -2
  16. package/app/components/SearchModal.tsx +3 -3
  17. package/app/components/SidebarLayout.tsx +11 -2
  18. package/app/components/SyncStatusBar.tsx +2 -2
  19. package/app/components/agents/DiscoverAgentModal.tsx +149 -0
  20. package/app/components/ask/AskContent.tsx +22 -10
  21. package/app/components/ask/MentionPopover.tsx +2 -2
  22. package/app/components/ask/SessionTabBar.tsx +70 -0
  23. package/app/components/ask/SlashCommandPopover.tsx +1 -1
  24. package/app/components/echo/EchoInsightCollapsible.tsx +4 -0
  25. package/app/components/explore/UseCaseCard.tsx +2 -2
  26. package/app/components/help/HelpContent.tsx +6 -1
  27. package/app/components/panels/AgentsPanel.tsx +25 -2
  28. package/app/components/panels/AgentsPanelAgentDetail.tsx +2 -2
  29. package/app/components/panels/DiscoverPanel.tsx +3 -3
  30. package/app/components/panels/PanelNavRow.tsx +2 -2
  31. package/app/components/panels/PluginsPanel.tsx +1 -1
  32. package/app/components/panels/SearchPanel.tsx +3 -3
  33. package/app/components/renderers/summary/SummaryRenderer.tsx +1 -1
  34. package/app/components/renderers/workflow/WorkflowRenderer.tsx +5 -0
  35. package/app/components/settings/AiTab.tsx +5 -4
  36. package/app/components/settings/KnowledgeTab.tsx +3 -1
  37. package/app/components/settings/McpTab.tsx +22 -4
  38. package/app/components/settings/SyncTab.tsx +2 -0
  39. package/app/components/settings/UpdateTab.tsx +1 -1
  40. package/app/components/setup/StepDots.tsx +5 -1
  41. package/app/components/setup/index.tsx +9 -3
  42. package/app/components/walkthrough/WalkthroughProvider.tsx +2 -2
  43. package/app/data/skills/mindos/SKILL.md +186 -0
  44. package/app/data/skills/mindos-zh/SKILL.md +185 -0
  45. package/app/hooks/useA2aRegistry.ts +53 -0
  46. package/app/hooks/useAskSession.ts +44 -25
  47. package/app/lib/a2a/a2a-tools.ts +212 -0
  48. package/app/lib/a2a/agent-card.ts +107 -0
  49. package/app/lib/a2a/client.ts +207 -0
  50. package/app/lib/a2a/index.ts +31 -0
  51. package/app/lib/a2a/orchestrator.ts +255 -0
  52. package/app/lib/a2a/task-handler.ts +228 -0
  53. package/app/lib/a2a/types.ts +212 -0
  54. package/app/lib/agent/tools.ts +6 -4
  55. package/app/lib/i18n-en.ts +52 -0
  56. package/app/lib/i18n-zh.ts +52 -0
  57. package/app/next-env.d.ts +1 -1
  58. package/bin/cli.js +183 -164
  59. package/bin/commands/agent.js +110 -0
  60. package/bin/commands/api.js +60 -0
  61. package/bin/commands/ask.js +3 -3
  62. package/bin/commands/file.js +13 -13
  63. package/bin/commands/search.js +51 -0
  64. package/bin/commands/space.js +64 -10
  65. package/bin/lib/command.js +10 -0
  66. package/package.json +1 -1
@@ -0,0 +1,149 @@
1
+ 'use client';
2
+
3
+ import { useState } from 'react';
4
+ import { X, Loader2, Globe, AlertCircle, CheckCircle2 } from 'lucide-react';
5
+ import { useLocale } from '@/lib/LocaleContext';
6
+ import type { RemoteAgent } from '@/lib/a2a/types';
7
+
8
+ interface DiscoverAgentModalProps {
9
+ open: boolean;
10
+ onClose: () => void;
11
+ onDiscover: (url: string) => Promise<RemoteAgent | null>;
12
+ discovering: boolean;
13
+ error: string | null;
14
+ }
15
+
16
+ export default function DiscoverAgentModal({
17
+ open,
18
+ onClose,
19
+ onDiscover,
20
+ discovering,
21
+ error,
22
+ }: DiscoverAgentModalProps) {
23
+ const { t } = useLocale();
24
+ const p = t.panels.agents;
25
+ const [url, setUrl] = useState('');
26
+ const [result, setResult] = useState<RemoteAgent | null>(null);
27
+
28
+ if (!open) return null;
29
+
30
+ const handleDiscover = async () => {
31
+ if (!url.trim() || discovering) return;
32
+ setResult(null);
33
+ const agent = await onDiscover(url.trim());
34
+ if (agent) setResult(agent);
35
+ };
36
+
37
+ const handleKeyDown = (e: React.KeyboardEvent) => {
38
+ if (e.key === 'Enter') handleDiscover();
39
+ if (e.key === 'Escape') onClose();
40
+ };
41
+
42
+ const handleClose = () => {
43
+ setUrl('');
44
+ setResult(null);
45
+ onClose();
46
+ };
47
+
48
+ return (
49
+ <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40" onClick={handleClose}>
50
+ <div
51
+ className="bg-popover border border-border rounded-xl shadow-lg w-full max-w-md mx-4 p-5"
52
+ onClick={e => e.stopPropagation()}
53
+ role="dialog"
54
+ aria-modal="true"
55
+ aria-label={p.a2aDiscover}
56
+ >
57
+ <div className="flex items-center justify-between mb-4">
58
+ <h3 className="text-sm font-semibold text-foreground flex items-center gap-2">
59
+ <Globe size={15} className="text-muted-foreground" />
60
+ {p.a2aDiscover}
61
+ </h3>
62
+ <button
63
+ type="button"
64
+ onClick={handleClose}
65
+ className="p-1 rounded hover:bg-muted text-muted-foreground hover:text-foreground transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
66
+ aria-label="Close"
67
+ >
68
+ <X size={14} />
69
+ </button>
70
+ </div>
71
+
72
+ <p className="text-2xs text-muted-foreground mb-3">{p.a2aDiscoverHint}</p>
73
+
74
+ <div className="flex gap-2 mb-4">
75
+ <input
76
+ type="url"
77
+ value={url}
78
+ onChange={e => setUrl(e.target.value)}
79
+ onKeyDown={handleKeyDown}
80
+ placeholder={p.a2aDiscoverPlaceholder}
81
+ disabled={discovering}
82
+ className="flex-1 px-3 py-2 text-xs rounded-lg border border-border bg-background text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring disabled:opacity-50"
83
+ autoFocus
84
+ />
85
+ <button
86
+ type="button"
87
+ onClick={handleDiscover}
88
+ disabled={discovering || !url.trim()}
89
+ className="px-3 py-2 text-xs font-medium rounded-lg bg-foreground text-background hover:bg-foreground/90 disabled:opacity-50 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring flex items-center gap-1.5 shrink-0"
90
+ >
91
+ {discovering && <Loader2 size={12} className="animate-spin" />}
92
+ {discovering ? p.a2aDiscovering : p.a2aDiscover}
93
+ </button>
94
+ </div>
95
+
96
+ {error && !result && (
97
+ <div className="rounded-lg border border-error/30 bg-error/5 px-3 py-2.5 mb-3">
98
+ <div className="flex items-start gap-2">
99
+ <AlertCircle size={14} className="text-error mt-0.5 shrink-0" />
100
+ <div>
101
+ <p className="text-xs font-medium text-error">{p.a2aDiscoverFailed}</p>
102
+ <p className="text-2xs text-muted-foreground mt-0.5">{p.a2aDiscoverFailedHint}</p>
103
+ </div>
104
+ </div>
105
+ </div>
106
+ )}
107
+
108
+ {result && (
109
+ <div className="rounded-lg border border-success/30 bg-success/5 px-3 py-2.5">
110
+ <div className="flex items-start gap-2">
111
+ <CheckCircle2 size={14} className="text-success mt-0.5 shrink-0" />
112
+ <div className="min-w-0">
113
+ <p className="text-xs font-medium text-success mb-1.5">{p.a2aDiscoverSuccess}</p>
114
+ <div className="space-y-1">
115
+ <p className="text-xs font-medium text-foreground truncate" title={result.card.name}>
116
+ {result.card.name}
117
+ <span className="text-2xs text-muted-foreground ml-1.5">v{result.card.version}</span>
118
+ </p>
119
+ <p className="text-2xs text-muted-foreground truncate" title={result.card.description}>
120
+ {result.card.description}
121
+ </p>
122
+ {result.card.skills.length > 0 && (
123
+ <div className="mt-1.5">
124
+ <p className="text-2xs text-muted-foreground mb-1">{p.a2aSkills}:</p>
125
+ <div className="flex flex-wrap gap-1">
126
+ {result.card.skills.map(s => (
127
+ <span
128
+ key={s.id}
129
+ className="text-2xs px-1.5 py-0.5 rounded bg-muted/80 text-muted-foreground border border-border/50"
130
+ title={s.description}
131
+ >
132
+ {s.name}
133
+ </span>
134
+ ))}
135
+ </div>
136
+ </div>
137
+ )}
138
+ <p className="text-2xs text-muted-foreground mt-1 truncate" title={result.endpoint}>
139
+ {p.a2aEndpoint}: {result.endpoint}
140
+ </p>
141
+ </div>
142
+ </div>
143
+ </div>
144
+ </div>
145
+ )}
146
+ </div>
147
+ </div>
148
+ );
149
+ }
@@ -1,7 +1,7 @@
1
1
  'use client';
2
2
 
3
3
  import { useEffect, useLayoutEffect, useRef, useState, useCallback } from 'react';
4
- import { Sparkles, Send, Paperclip, StopCircle, RotateCcw, History, X, Zap, Maximize2, Minimize2, PanelRight, AppWindow } from 'lucide-react';
4
+ import { Sparkles, Send, Paperclip, StopCircle, SquarePen, History, X, Zap, Maximize2, Minimize2, PanelRight, AppWindow } from 'lucide-react';
5
5
  import { useLocale } from '@/lib/LocaleContext';
6
6
  import type { Message } from '@/lib/types';
7
7
  import { useAskSession } from '@/hooks/useAskSession';
@@ -13,6 +13,7 @@ import MessageList from '@/components/ask/MessageList';
13
13
  import MentionPopover from '@/components/ask/MentionPopover';
14
14
  import SlashCommandPopover from '@/components/ask/SlashCommandPopover';
15
15
  import SessionHistory from '@/components/ask/SessionHistory';
16
+ import SessionTabBar from '@/components/ask/SessionTabBar';
16
17
  import FileChip from '@/components/ask/FileChip';
17
18
  import { consumeUIMessageStream } from '@/lib/agent/stream-consumer';
18
19
  import { isRetryableError, retryDelay, sleep } from '@/lib/agent/reconnect';
@@ -442,7 +443,7 @@ export default function AskContent({ visible, currentFile, initialMessage, onFir
442
443
  } else if (typeof errBody?.message === 'string' && errBody.message.trim()) {
443
444
  errorMsg = errBody.message;
444
445
  }
445
- } catch {}
446
+ } catch (err) { console.warn("[AskContent] error body parse failed:", err); }
446
447
  const err = new Error(errorMsg);
447
448
  (err as Error & { httpStatus?: number }).httpStatus = res.status;
448
449
  throw err;
@@ -601,30 +602,41 @@ export default function AskContent({ visible, currentFile, initialMessage, onFir
601
602
  </span>
602
603
  </div>
603
604
  <div className="flex items-center gap-1">
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
+ <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={t.hints.sessionHistory}>
605
606
  <History size={iconSize} />
606
607
  </button>
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
- <RotateCcw size={iconSize} />
608
+ <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={t.hints.newSession}>
609
+ <SquarePen size={iconSize} />
609
610
  </button>
610
611
  {isPanel && onMaximize && (
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
+ <button type="button" onClick={onMaximize} className="p-1.5 rounded hover:bg-muted text-muted-foreground hover:text-foreground transition-colors" title={maximized ? t.hints.restorePanel : t.hints.maximizePanel}>
612
613
  {maximized ? <Minimize2 size={iconSize} /> : <Maximize2 size={iconSize} />}
613
614
  </button>
614
615
  )}
615
616
  {onModeSwitch && (
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
+ <button type="button" onClick={onModeSwitch} className="p-1.5 rounded hover:bg-muted text-muted-foreground hover:text-foreground transition-colors" title={askMode === 'popup' ? t.hints.dockToSide : t.hints.openAsPopup}>
617
618
  {askMode === 'popup' ? <PanelRight size={iconSize} /> : <AppWindow size={iconSize} />}
618
619
  </button>
619
620
  )}
620
621
  {onClose && (
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
+ <button type="button" onClick={onClose} className="p-1.5 rounded hover:bg-muted text-muted-foreground hover:text-foreground transition-colors" title={t.hints.closePanel} aria-label="Close">
622
623
  <X size={isPanel ? iconSize : 15} />
623
624
  </button>
624
625
  )}
625
626
  </div>
626
627
  </div>
627
628
 
629
+ {/* Session tabs — panel variant only */}
630
+ {isPanel && session.sessions.length > 0 && (
631
+ <SessionTabBar
632
+ sessions={session.sessions}
633
+ activeSessionId={session.activeSessionId}
634
+ onLoad={handleLoadSession}
635
+ onDelete={session.deleteSession}
636
+ onNew={handleResetSession}
637
+ />
638
+ )}
639
+
628
640
  {showHistory && (
629
641
  <SessionHistory
630
642
  sessions={session.sessions}
@@ -770,7 +782,7 @@ export default function AskContent({ visible, currentFile, initialMessage, onFir
770
782
  isPanel ? 'min-h-0 flex-1 items-end gap-1.5 px-2 py-2' : 'items-end gap-2 px-3 py-3',
771
783
  )}
772
784
  >
773
- <button type="button" onClick={() => upload.uploadInputRef.current?.click()} className="p-1.5 rounded-md text-muted-foreground hover:text-foreground hover:bg-muted transition-colors shrink-0" title="Attach local file">
785
+ <button type="button" onClick={() => upload.uploadInputRef.current?.click()} className="p-1.5 rounded-md text-muted-foreground hover:text-foreground hover:bg-muted transition-colors shrink-0" title={t.hints.attachFile}>
774
786
  <Paperclip size={inputIconSize} />
775
787
  </button>
776
788
 
@@ -807,7 +819,7 @@ export default function AskContent({ visible, currentFile, initialMessage, onFir
807
819
  {loadingPhase === 'reconnecting' ? <X size={inputIconSize} /> : <StopCircle size={inputIconSize} />}
808
820
  </button>
809
821
  ) : (
810
- <button type="submit" disabled={!input.trim() || mention.mentionQuery !== null || slash.slashQuery !== null} className="p-1.5 rounded-md disabled:opacity-40 disabled:cursor-not-allowed transition-opacity shrink-0 bg-[var(--amber)] text-[var(--amber-foreground)]">
822
+ <button type="submit" disabled={!input.trim() || mention.mentionQuery !== null || slash.slashQuery !== null} title={!input.trim() ? t.hints.typeMessage : mention.mentionQuery !== null || slash.slashQuery !== null ? t.hints.mentionInProgress : undefined} className="p-1.5 rounded-md disabled:opacity-40 disabled:cursor-not-allowed transition-opacity shrink-0 bg-[var(--amber)] text-[var(--amber-foreground)]">
811
823
  <Send size={isPanel ? 13 : 14} />
812
824
  </button>
813
825
  )}
@@ -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
  )}
@@ -0,0 +1,70 @@
1
+ 'use client';
2
+
3
+ import { Plus, X } from 'lucide-react';
4
+ import type { ChatSession } from '@/lib/types';
5
+ import { sessionTitle } from '@/hooks/useAskSession';
6
+ import { useLocale } from '@/lib/LocaleContext';
7
+
8
+ interface SessionTabBarProps {
9
+ sessions: ChatSession[];
10
+ activeSessionId: string | null;
11
+ onLoad: (id: string) => void;
12
+ onDelete: (id: string) => void;
13
+ onNew: () => void;
14
+ maxTabs?: number;
15
+ }
16
+
17
+ export default function SessionTabBar({
18
+ sessions, activeSessionId, onLoad, onDelete, onNew, maxTabs = 3,
19
+ }: SessionTabBarProps) {
20
+ const { t } = useLocale();
21
+ const visibleSessions = sessions.slice(0, maxTabs);
22
+
23
+ if (visibleSessions.length === 0) return null;
24
+
25
+ return (
26
+ <div className="flex items-center border-b border-border shrink-0 bg-background/50">
27
+ <div className="flex flex-1 min-w-0">
28
+ {visibleSessions.map((s) => {
29
+ const isActive = s.id === activeSessionId;
30
+ const title = sessionTitle(s);
31
+ return (
32
+ <button
33
+ key={s.id}
34
+ type="button"
35
+ onClick={() => onLoad(s.id)}
36
+ className={`group relative flex items-center gap-1 min-w-0 max-w-[160px] px-3 py-2 text-xs transition-colors
37
+ ${isActive
38
+ ? 'text-foreground border-b-2 border-[var(--amber)] bg-card'
39
+ : 'text-muted-foreground hover:text-foreground hover:bg-muted/50 border-b-2 border-transparent'
40
+ }`}
41
+ title={title}
42
+ >
43
+ <span className="truncate">{title === '(empty session)' ? t.hints.newChat : title}</span>
44
+ {visibleSessions.length > 1 && (
45
+ <span
46
+ role="button"
47
+ tabIndex={0}
48
+ onClick={(e) => { e.stopPropagation(); onDelete(s.id); }}
49
+ onKeyDown={(e) => { if (e.key === 'Enter') { e.stopPropagation(); onDelete(s.id); } }}
50
+ className="shrink-0 p-0.5 rounded opacity-0 group-hover:opacity-100 hover:bg-muted hover:text-error transition-opacity"
51
+ title={t.hints.closeSession}
52
+ >
53
+ <X size={10} />
54
+ </span>
55
+ )}
56
+ </button>
57
+ );
58
+ })}
59
+ </div>
60
+ <button
61
+ type="button"
62
+ onClick={onNew}
63
+ className="shrink-0 p-2 text-muted-foreground hover:text-foreground hover:bg-muted/50 transition-colors"
64
+ title={t.hints.newChat}
65
+ >
66
+ <Plus size={13} />
67
+ </button>
68
+ </div>
69
+ );
70
+ }
@@ -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
  ))}
@@ -7,6 +7,7 @@ import remarkGfm from 'remark-gfm';
7
7
  import { cn } from '@/lib/utils';
8
8
  import { consumeUIMessageStream } from '@/lib/agent/stream-consumer';
9
9
  import { useSettingsAiAvailable } from '@/hooks/useSettingsAiAvailable';
10
+ import { useLocale } from '@/lib/LocaleContext';
10
11
 
11
12
  const proseInsight =
12
13
  'prose prose-sm prose-panel dark:prose-invert max-w-none text-foreground ' +
@@ -50,6 +51,7 @@ export function EchoInsightCollapsible({
50
51
  const btnId = `${panelId}-btn`;
51
52
  const abortRef = useRef<AbortController | null>(null);
52
53
  const { ready: aiReady, loading: aiLoading } = useSettingsAiAvailable();
54
+ const { t } = useLocale();
53
55
 
54
56
  useEffect(() => () => abortRef.current?.abort(), []);
55
57
 
@@ -145,6 +147,7 @@ export function EchoInsightCollapsible({
145
147
  <button
146
148
  type="button"
147
149
  disabled={generateDisabled}
150
+ title={generateDisabled ? t.hints.aiNotConfigured : undefined}
148
151
  onClick={runGenerate}
149
152
  className="inline-flex items-center gap-2 rounded-lg bg-[var(--amber)] px-3 py-2 font-sans text-sm font-medium text-[var(--amber-foreground)] transition-opacity duration-150 hover:opacity-90 disabled:cursor-not-allowed disabled:opacity-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
150
153
  >
@@ -160,6 +163,7 @@ export function EchoInsightCollapsible({
160
163
  type="button"
161
164
  onClick={runGenerate}
162
165
  disabled={streaming || !aiReady}
166
+ title={streaming || !aiReady ? t.hints.generationInProgress : undefined}
163
167
  className="font-sans text-sm text-[var(--amber)] underline-offset-2 hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring rounded-sm"
164
168
  >
165
169
  {retryLabel}
@@ -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 (
@@ -2,12 +2,14 @@
2
2
 
3
3
  import { useState } from 'react';
4
4
  import { usePathname } from 'next/navigation';
5
- import { Loader2, RefreshCw, Settings } from 'lucide-react';
5
+ import { Globe, Loader2, RefreshCw, Settings } from 'lucide-react';
6
6
  import { useMcpData } from '@/hooks/useMcpData';
7
+ import { useA2aRegistry } from '@/hooks/useA2aRegistry';
7
8
  import { useLocale } from '@/lib/LocaleContext';
8
9
  import PanelHeader from './PanelHeader';
9
10
  import { AgentsPanelHubNav } from './AgentsPanelHubNav';
10
11
  import { AgentsPanelAgentGroups } from './AgentsPanelAgentGroups';
12
+ import DiscoverAgentModal from '../agents/DiscoverAgentModal';
11
13
 
12
14
  interface AgentsPanelProps {
13
15
  active: boolean;
@@ -28,6 +30,8 @@ export default function AgentsPanel({
28
30
  const pathname = usePathname();
29
31
  const [refreshing, setRefreshing] = useState(false);
30
32
  const [showNotDetected, setShowNotDetected] = useState(false);
33
+ const [showDiscoverModal, setShowDiscoverModal] = useState(false);
34
+ const a2a = useA2aRegistry();
31
35
 
32
36
  const handleRefresh = async () => {
33
37
  setRefreshing(true);
@@ -82,6 +86,9 @@ export default function AgentsPanel({
82
86
  {!mcp.loading && (
83
87
  <span className="text-2xs text-muted-foreground">
84
88
  {connected.length} {p.connected}
89
+ {a2a.agents.length > 0 && (
90
+ <> &middot; {p.a2aLabel} {a2a.agents.length}</>
91
+ )}
85
92
  </span>
86
93
  )}
87
94
  <button
@@ -146,7 +153,15 @@ export default function AgentsPanel({
146
153
  )}
147
154
  </div>
148
155
 
149
- <div className="px-3 py-2 border-t border-border shrink-0">
156
+ <div className="px-3 py-2 border-t border-border shrink-0 space-y-1">
157
+ <button
158
+ type="button"
159
+ onClick={() => setShowDiscoverModal(true)}
160
+ className="flex items-center gap-1.5 text-2xs text-muted-foreground hover:text-foreground transition-colors w-full focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring rounded-sm"
161
+ >
162
+ <Globe size={11} />
163
+ {p.a2aDiscover}
164
+ </button>
150
165
  <button
151
166
  type="button"
152
167
  onClick={openAdvancedConfig}
@@ -156,6 +171,14 @@ export default function AgentsPanel({
156
171
  {p.advancedConfig}
157
172
  </button>
158
173
  </div>
174
+
175
+ <DiscoverAgentModal
176
+ open={showDiscoverModal}
177
+ onClose={() => setShowDiscoverModal(false)}
178
+ onDiscover={a2a.discover}
179
+ discovering={a2a.discovering}
180
+ error={a2a.error}
181
+ />
159
182
  </div>
160
183
  );
161
184
  }
@@ -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() {
@@ -3,6 +3,7 @@
3
3
  import { useMemo, useState, useRef, useCallback } from 'react';
4
4
  import { Play, SkipForward, RotateCcw, CheckCircle2, Circle, Loader2, AlertCircle, ChevronDown, Sparkles } from 'lucide-react';
5
5
  import type { RendererContext } from '@/lib/renderers/registry';
6
+ import { useLocale } from '@/lib/LocaleContext';
6
7
 
7
8
  // ─── Types ────────────────────────────────────────────────────────────────────
8
9
 
@@ -171,6 +172,7 @@ function StepCard({
171
172
  onSkip: () => void;
172
173
  canRun: boolean;
173
174
  }) {
175
+ const { t } = useLocale();
174
176
  const [expanded, setExpanded] = useState(false);
175
177
  const hasBody = step.body.trim().length > 0;
176
178
  const hasOutput = step.output.length > 0;
@@ -201,6 +203,7 @@ function StepCard({
201
203
  <button
202
204
  onClick={onRun}
203
205
  disabled={!canRun}
206
+ title={!canRun ? t.hints.workflowStepRunning : undefined}
204
207
  style={{
205
208
  display: 'flex', alignItems: 'center', gap: 4,
206
209
  padding: '3px 10px', borderRadius: 6, fontSize: '0.72rem',
@@ -268,6 +271,7 @@ function StepCard({
268
271
  // ─── Main renderer ────────────────────────────────────────────────────────────
269
272
 
270
273
  export function WorkflowRenderer({ filePath, content }: RendererContext) {
274
+ const { t } = useLocale();
271
275
  const parsed = useMemo(() => parseWorkflow(content), [content]);
272
276
  const [steps, setSteps] = useState<WorkflowStep[]>(() => parsed.steps);
273
277
  const [running, setRunning] = useState(false);
@@ -357,6 +361,7 @@ export function WorkflowRenderer({ filePath, content }: RendererContext) {
357
361
  <button
358
362
  onClick={() => runStep(nextPendingIdx)}
359
363
  disabled={running}
364
+ title={running ? t.hints.workflowRunning : undefined}
360
365
  style={{
361
366
  display: 'flex', alignItems: 'center', gap: 5,
362
367
  padding: '4px 12px', borderRadius: 7, fontSize: '0.75rem',
@@ -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') => {
@@ -125,6 +125,7 @@ export function AiTab({ data, updateAi, updateAgent, t }: AiTabProps) {
125
125
  <button
126
126
  type="button"
127
127
  disabled={disabled}
128
+ title={disabled ? t.hints.testInProgressOrNoKey : undefined}
128
129
  onClick={() => handleTestKey(providerName)}
129
130
  className="inline-flex items-center gap-1.5 px-2.5 py-1 text-xs rounded-lg border border-border text-muted-foreground hover:text-foreground hover:border-foreground/20 transition-colors disabled:opacity-40 disabled:cursor-not-allowed"
130
131
  >
@@ -271,7 +272,7 @@ export function AiTab({ data, updateAi, updateAgent, t }: AiTabProps) {
271
272
  onChange={e => {
272
273
  const v = Number(e.target.value);
273
274
  updateAgent({ reconnectRetries: v });
274
- try { localStorage.setItem('mindos-reconnect-retries', String(v)); } catch {}
275
+ try { localStorage.setItem('mindos-reconnect-retries', String(v)); } catch (err) { console.warn("[AiTab] localStorage setItem reconnectRetries failed:", err); }
275
276
  }}
276
277
  >
277
278
  <option value="0">Off</option>
@@ -434,13 +435,13 @@ function AskDisplayMode() {
434
435
  try {
435
436
  const stored = localStorage.getItem('ask-mode');
436
437
  if (stored === 'popup') setMode('popup');
437
- } catch {}
438
+ } catch (err) { console.warn("[AiTab] localStorage getItem ask-mode failed:", err); }
438
439
  }, []);
439
440
 
440
441
  const handleChange = (value: string) => {
441
442
  const next = value as 'panel' | 'popup';
442
443
  setMode(next);
443
- try { localStorage.setItem('ask-mode', next); } catch {}
444
+ try { localStorage.setItem('ask-mode', next); } catch (err) { console.warn("[AiTab] localStorage setItem ask-mode failed:", err); }
444
445
  // Notify SidebarLayout to pick up the change
445
446
  window.dispatchEvent(new StorageEvent('storage', { key: 'ask-mode', newValue: next }));
446
447
  };