@geminilight/mindos 0.6.8 → 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.
- package/README.md +2 -0
- package/README_zh.md +2 -0
- package/app/app/api/mcp/install/route.ts +4 -1
- package/app/app/api/setup/check-path/route.ts +2 -7
- package/app/app/api/setup/ls/route.ts +3 -9
- package/app/app/api/setup/path-utils.ts +8 -0
- package/app/app/api/setup/route.ts +2 -7
- package/app/app/api/uninstall/route.ts +47 -0
- package/app/app/globals.css +11 -0
- package/app/components/ActivityBar.tsx +10 -3
- package/app/components/AskFab.tsx +7 -3
- package/app/components/CreateSpaceModal.tsx +1 -1
- package/app/components/DirView.tsx +1 -1
- package/app/components/FileTree.tsx +30 -23
- package/app/components/GuideCard.tsx +1 -1
- package/app/components/HomeContent.tsx +137 -109
- package/app/components/ImportModal.tsx +16 -477
- package/app/components/MarkdownView.tsx +3 -0
- package/app/components/OnboardingView.tsx +1 -1
- package/app/components/OrganizeToast.tsx +386 -0
- package/app/components/Panel.tsx +23 -2
- package/app/components/Sidebar.tsx +1 -1
- package/app/components/SidebarLayout.tsx +44 -1
- package/app/components/agents/AgentDetailContent.tsx +33 -12
- package/app/components/agents/AgentsMcpSection.tsx +1 -1
- package/app/components/agents/AgentsOverviewSection.tsx +3 -4
- package/app/components/agents/AgentsPrimitives.tsx +2 -2
- package/app/components/agents/AgentsSkillsSection.tsx +2 -2
- package/app/components/agents/SkillDetailPopover.tsx +24 -8
- package/app/components/ask/AskContent.tsx +124 -75
- package/app/components/ask/HighlightMatch.tsx +14 -0
- package/app/components/ask/MentionPopover.tsx +5 -3
- package/app/components/ask/MessageList.tsx +39 -11
- package/app/components/ask/SlashCommandPopover.tsx +4 -2
- package/app/components/changes/ChangesBanner.tsx +20 -2
- package/app/components/changes/ChangesContentPage.tsx +10 -2
- package/app/components/echo/EchoHero.tsx +1 -1
- package/app/components/echo/EchoInsightCollapsible.tsx +1 -1
- package/app/components/echo/EchoPageSections.tsx +1 -1
- package/app/components/explore/UseCaseCard.tsx +1 -1
- package/app/components/panels/DiscoverPanel.tsx +29 -25
- package/app/components/panels/ImportHistoryPanel.tsx +195 -0
- package/app/components/panels/PluginsPanel.tsx +2 -2
- package/app/components/settings/AiTab.tsx +24 -0
- package/app/components/settings/KnowledgeTab.tsx +1 -1
- package/app/components/settings/McpSkillCreateForm.tsx +1 -1
- package/app/components/settings/McpSkillRow.tsx +1 -1
- package/app/components/settings/McpSkillsSection.tsx +2 -2
- package/app/components/settings/McpTab.tsx +2 -2
- package/app/components/settings/PluginsTab.tsx +1 -1
- package/app/components/settings/Primitives.tsx +118 -6
- package/app/components/settings/SettingsContent.tsx +5 -2
- package/app/components/settings/UninstallTab.tsx +179 -0
- package/app/components/settings/UpdateTab.tsx +17 -5
- package/app/components/settings/types.ts +2 -1
- package/app/components/ui/dialog.tsx +1 -1
- package/app/hooks/useAiOrganize.ts +122 -10
- package/app/hooks/useMention.ts +21 -3
- package/app/hooks/useSlashCommand.ts +18 -4
- package/app/lib/agent/reconnect.ts +40 -0
- package/app/lib/core/backlinks.ts +2 -2
- package/app/lib/core/git.ts +14 -10
- package/app/lib/fs.ts +2 -1
- package/app/lib/i18n-en.ts +46 -2
- package/app/lib/i18n-zh.ts +46 -2
- package/app/lib/organize-history.ts +74 -0
- package/app/lib/settings.ts +2 -0
- package/app/lib/types.ts +2 -0
- package/app/next.config.ts +23 -5
- package/bin/cli.js +6 -9
- package/bin/lib/mcp-build.js +74 -0
- package/bin/lib/mcp-spawn.js +8 -5
- package/bin/lib/port.js +17 -2
- package/bin/lib/stop.js +12 -2
- package/mcp/dist/index.cjs +43 -43
- package/mcp/src/index.ts +58 -12
- package/package.json +1 -1
- package/scripts/setup.js +2 -2
|
@@ -158,7 +158,7 @@ export default function AgentsOverviewSection({
|
|
|
158
158
|
riskOpen ? 'grid-rows-[1fr]' : 'grid-rows-[0fr]',
|
|
159
159
|
)}
|
|
160
160
|
>
|
|
161
|
-
<div className="overflow-hidden">
|
|
161
|
+
<div className="overflow-hidden" {...(!riskOpen && { inert: true } as React.HTMLAttributes<HTMLDivElement>)}>
|
|
162
162
|
<ul className="mt-3 space-y-2" role="list">
|
|
163
163
|
{riskQueue.map((risk, i) => (
|
|
164
164
|
<li
|
|
@@ -181,7 +181,7 @@ export default function AgentsOverviewSection({
|
|
|
181
181
|
className={`text-2xs px-1.5 py-0.5 rounded font-medium shrink-0 select-none ${
|
|
182
182
|
risk.severity === 'error'
|
|
183
183
|
? 'bg-destructive/10 text-destructive'
|
|
184
|
-
: 'bg-[var(--amber-dim)] text-[var(--amber)]'
|
|
184
|
+
: 'bg-[var(--amber-dim)] text-[var(--amber-text)]'
|
|
185
185
|
}`}
|
|
186
186
|
>
|
|
187
187
|
{risk.severity === 'error' ? copy.riskLevelError : copy.riskLevelWarn}
|
|
@@ -384,9 +384,8 @@ function AgentCard({
|
|
|
384
384
|
className={`group rounded-xl border border-border bg-card p-3.5
|
|
385
385
|
hover:border-[var(--amber)]/30 hover:shadow-[0_2px_8px_rgba(0,0,0,0.04)]
|
|
386
386
|
active:scale-[0.98]
|
|
387
|
-
transition-all duration-150
|
|
387
|
+
transition-all duration-150
|
|
388
388
|
focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring`}
|
|
389
|
-
style={{ animationDelay: `${Math.min(index * 30, 300)}ms` }}
|
|
390
389
|
>
|
|
391
390
|
{/* Top row: avatar + name + status */}
|
|
392
391
|
<div className="flex items-center gap-2.5 mb-3">
|
|
@@ -13,7 +13,7 @@ export function PillButton({ active, label, onClick }: { active: boolean; label:
|
|
|
13
13
|
aria-pressed={active}
|
|
14
14
|
className={`relative px-2.5 min-h-[28px] rounded text-xs cursor-pointer transition-all duration-150 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring ${
|
|
15
15
|
active
|
|
16
|
-
? 'bg-[var(--amber-dim)] text-[var(--amber)] font-medium shadow-[0_1px_2px_rgba(200,135,58,0.08)]'
|
|
16
|
+
? 'bg-[var(--amber-dim)] text-[var(--amber-text)] font-medium shadow-[0_1px_2px_rgba(200,135,58,0.08)]'
|
|
17
17
|
: 'text-muted-foreground hover:text-foreground hover:bg-muted/60'
|
|
18
18
|
}`}
|
|
19
19
|
>
|
|
@@ -261,7 +261,7 @@ export function ConfirmDialog({
|
|
|
261
261
|
|
|
262
262
|
return (
|
|
263
263
|
<div className="fixed inset-0 z-50 flex items-center justify-center" role="alertdialog" aria-modal="true" aria-label={title}>
|
|
264
|
-
<div className="absolute inset-0
|
|
264
|
+
<div className="absolute inset-0 overlay-backdrop" onClick={onCancel} aria-hidden="true" />
|
|
265
265
|
<div className="relative bg-card border border-border rounded-lg shadow-xl p-5 max-w-sm w-full mx-4 animate-in fade-in zoom-in-95 duration-200">
|
|
266
266
|
<h3 className="text-sm font-medium text-foreground mb-1.5">{title}</h3>
|
|
267
267
|
<p className="text-sm text-muted-foreground mb-4 leading-relaxed">{message}</p>
|
|
@@ -483,7 +483,7 @@ function BySkillView({
|
|
|
483
483
|
? 'bg-muted text-muted-foreground'
|
|
484
484
|
: skill.source === 'builtin'
|
|
485
485
|
? 'bg-muted text-muted-foreground'
|
|
486
|
-
: 'bg-[var(--amber-dim)] text-[var(--amber)]'
|
|
486
|
+
: 'bg-[var(--amber-dim)] text-[var(--amber-text)]'
|
|
487
487
|
}`}>
|
|
488
488
|
{skill.kind === 'native' ? copy.sourceNative : skill.source === 'builtin' ? copy.sourceBuiltin : copy.sourceUser}
|
|
489
489
|
</span>
|
|
@@ -697,7 +697,7 @@ function AgentCard({
|
|
|
697
697
|
{skillMode && (
|
|
698
698
|
<span className={`text-2xs px-1.5 py-0.5 rounded shrink-0 ${
|
|
699
699
|
skillMode === 'universal' ? 'bg-success/10 text-success'
|
|
700
|
-
: skillMode === 'additional' ? 'bg-[var(--amber-dim)] text-[var(--amber)]'
|
|
700
|
+
: skillMode === 'additional' ? 'bg-[var(--amber-dim)] text-[var(--amber-text)]'
|
|
701
701
|
: 'bg-muted text-muted-foreground'
|
|
702
702
|
}`}>
|
|
703
703
|
{skillMode}
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
ToggleLeft,
|
|
15
15
|
Trash2,
|
|
16
16
|
X,
|
|
17
|
+
ChevronDown,
|
|
17
18
|
} from 'lucide-react';
|
|
18
19
|
import { apiFetch } from '@/lib/api';
|
|
19
20
|
import { copyToClipboard } from '@/lib/clipboard';
|
|
@@ -100,6 +101,7 @@ export default function SkillDetailPopover({
|
|
|
100
101
|
const [deleting, setDeleting] = useState(false);
|
|
101
102
|
const [deleteMsg, setDeleteMsg] = useState<string | null>(null);
|
|
102
103
|
const [toggleBusy, setToggleBusy] = useState(false);
|
|
104
|
+
const [descriptionExpanded, setDescriptionExpanded] = useState(false);
|
|
103
105
|
|
|
104
106
|
const fetchContent = useCallback(async () => {
|
|
105
107
|
if (!skillName) return;
|
|
@@ -204,7 +206,7 @@ export default function SkillDetailPopover({
|
|
|
204
206
|
<>
|
|
205
207
|
{/* Backdrop */}
|
|
206
208
|
<div
|
|
207
|
-
className="fixed inset-0 z-40
|
|
209
|
+
className="fixed inset-0 z-40 overlay-backdrop"
|
|
208
210
|
onClick={onClose}
|
|
209
211
|
aria-hidden="true"
|
|
210
212
|
/>
|
|
@@ -229,7 +231,7 @@ export default function SkillDetailPopover({
|
|
|
229
231
|
? 'bg-muted text-muted-foreground'
|
|
230
232
|
: skill?.source === 'builtin'
|
|
231
233
|
? 'bg-muted text-muted-foreground'
|
|
232
|
-
: 'bg-[var(--amber-dim)] text-[var(--amber)]'
|
|
234
|
+
: 'bg-[var(--amber-dim)] text-[var(--amber-text)]'
|
|
233
235
|
}`}>
|
|
234
236
|
{sourceLabel}
|
|
235
237
|
</span>
|
|
@@ -248,12 +250,26 @@ export default function SkillDetailPopover({
|
|
|
248
250
|
|
|
249
251
|
{/* ─── Body (scrollable) ─── */}
|
|
250
252
|
<div className="flex-1 overflow-y-auto px-5 py-4 space-y-5">
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
253
|
+
{/* Description */}
|
|
254
|
+
{description ? (
|
|
255
|
+
<div className="space-y-2">
|
|
256
|
+
<p className={`text-sm text-foreground leading-relaxed whitespace-pre-wrap ${!descriptionExpanded ? 'line-clamp-3' : ''}`}>
|
|
257
|
+
{description}
|
|
258
|
+
</p>
|
|
259
|
+
{description.split('\n').length > 3 && (
|
|
260
|
+
<button
|
|
261
|
+
type="button"
|
|
262
|
+
onClick={() => setDescriptionExpanded(!descriptionExpanded)}
|
|
263
|
+
className="inline-flex items-center gap-1 text-2xs font-medium text-[var(--amber)] hover:opacity-80 transition-opacity cursor-pointer"
|
|
264
|
+
>
|
|
265
|
+
<ChevronDown size={12} className={`transition-transform duration-200 ${descriptionExpanded ? 'rotate-180' : ''}`} />
|
|
266
|
+
<span>{descriptionExpanded ? '收起' : '查看全部'}</span>
|
|
267
|
+
</button>
|
|
268
|
+
)}
|
|
269
|
+
</div>
|
|
270
|
+
) : !isNative ? (
|
|
271
|
+
<p className="text-sm text-muted-foreground italic">{copy.noDescription}</p>
|
|
272
|
+
) : null}
|
|
257
273
|
|
|
258
274
|
{/* Quick meta */}
|
|
259
275
|
<div className="grid grid-cols-2 gap-3">
|
|
@@ -15,6 +15,7 @@ import SlashCommandPopover from '@/components/ask/SlashCommandPopover';
|
|
|
15
15
|
import SessionHistory from '@/components/ask/SessionHistory';
|
|
16
16
|
import FileChip from '@/components/ask/FileChip';
|
|
17
17
|
import { consumeUIMessageStream } from '@/lib/agent/stream-consumer';
|
|
18
|
+
import { isRetryableError, retryDelay, sleep } from '@/lib/agent/reconnect';
|
|
18
19
|
import { cn } from '@/lib/utils';
|
|
19
20
|
import { useComposerVerticalResize } from '@/hooks/useComposerVerticalResize';
|
|
20
21
|
|
|
@@ -27,7 +28,7 @@ const PANEL_COMPOSER_KEY_STEP = 24;
|
|
|
27
28
|
/** 输入框随内容增高,超过此行数后在框内滚动(与常见 IM 一致) */
|
|
28
29
|
const PANEL_TEXTAREA_MAX_VISIBLE_LINES = 8;
|
|
29
30
|
|
|
30
|
-
function
|
|
31
|
+
function syncTextareaToContent(el: HTMLTextAreaElement, maxVisibleLines: number, availableHeight?: number): void {
|
|
31
32
|
const style = getComputedStyle(el);
|
|
32
33
|
const parsedLh = parseFloat(style.lineHeight);
|
|
33
34
|
const parsedFs = parseFloat(style.fontSize);
|
|
@@ -86,7 +87,7 @@ interface AskContentProps {
|
|
|
86
87
|
export default function AskContent({ visible, currentFile, initialMessage, onFirstMessage, variant, onClose, maximized, onMaximize, askMode, onModeSwitch }: AskContentProps) {
|
|
87
88
|
const isPanel = variant === 'panel';
|
|
88
89
|
|
|
89
|
-
const inputRef = useRef<
|
|
90
|
+
const inputRef = useRef<HTMLTextAreaElement>(null);
|
|
90
91
|
const abortRef = useRef<AbortController | null>(null);
|
|
91
92
|
const firstMessageFired = useRef(false);
|
|
92
93
|
const { t } = useLocale();
|
|
@@ -171,7 +172,9 @@ export default function AskContent({ visible, currentFile, initialMessage, onFir
|
|
|
171
172
|
|
|
172
173
|
const [input, setInput] = useState('');
|
|
173
174
|
const [isLoading, setIsLoading] = useState(false);
|
|
174
|
-
const [loadingPhase, setLoadingPhase] = useState<'connecting' | 'thinking' | 'streaming'>('connecting');
|
|
175
|
+
const [loadingPhase, setLoadingPhase] = useState<'connecting' | 'thinking' | 'streaming' | 'reconnecting'>('connecting');
|
|
176
|
+
const [reconnectAttempt, setReconnectAttempt] = useState(0);
|
|
177
|
+
const reconnectMaxRef = useRef(3);
|
|
175
178
|
const [attachedFiles, setAttachedFiles] = useState<string[]>([]);
|
|
176
179
|
const [showHistory, setShowHistory] = useState(false);
|
|
177
180
|
const [isDragOver, setIsDragOver] = useState(false);
|
|
@@ -220,13 +223,18 @@ export default function AskContent({ visible, currentFile, initialMessage, onFir
|
|
|
220
223
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
221
224
|
}, [visible, currentFile]);
|
|
222
225
|
|
|
223
|
-
// Persist session on message changes
|
|
226
|
+
// Persist session on message changes (skip if last msg is empty assistant placeholder during loading)
|
|
224
227
|
useEffect(() => {
|
|
225
228
|
if (!visible || !session.activeSessionId) return;
|
|
226
|
-
session.
|
|
229
|
+
const msgs = session.messages;
|
|
230
|
+
if (isLoading && msgs.length > 0) {
|
|
231
|
+
const last = msgs[msgs.length - 1];
|
|
232
|
+
if (last.role === 'assistant' && !last.content.trim() && (!last.parts || last.parts.length === 0)) return;
|
|
233
|
+
}
|
|
234
|
+
session.persistSession(msgs, session.activeSessionId);
|
|
227
235
|
return () => session.clearPersistTimer();
|
|
228
236
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
229
|
-
}, [visible, session.messages, session.activeSessionId]);
|
|
237
|
+
}, [visible, session.messages, session.activeSessionId, isLoading]);
|
|
230
238
|
|
|
231
239
|
// Esc to close — modal only
|
|
232
240
|
useEffect(() => {
|
|
@@ -252,12 +260,13 @@ export default function AskContent({ visible, currentFile, initialMessage, onFir
|
|
|
252
260
|
const formRef = useRef<HTMLFormElement>(null);
|
|
253
261
|
|
|
254
262
|
useLayoutEffect(() => {
|
|
255
|
-
if (!
|
|
263
|
+
if (!visible) return;
|
|
256
264
|
const el = inputRef.current;
|
|
257
265
|
if (!el || !(el instanceof HTMLTextAreaElement)) return;
|
|
258
266
|
const form = formRef.current;
|
|
259
|
-
const
|
|
260
|
-
|
|
267
|
+
const maxLines = isPanel ? PANEL_TEXTAREA_MAX_VISIBLE_LINES : 6;
|
|
268
|
+
const availableH = isPanel && form ? form.clientHeight - 40 : undefined;
|
|
269
|
+
syncTextareaToContent(el, maxLines, availableH);
|
|
261
270
|
}, [input, isPanel, isLoading, visible, panelComposerHeight]);
|
|
262
271
|
|
|
263
272
|
const mentionTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
@@ -280,9 +289,7 @@ export default function AskContent({ visible, currentFile, initialMessage, onFir
|
|
|
280
289
|
|
|
281
290
|
const selectMention = useCallback((filePath: string) => {
|
|
282
291
|
const el = inputRef.current;
|
|
283
|
-
const cursorPos = el
|
|
284
|
-
? (el instanceof HTMLTextAreaElement || el instanceof HTMLInputElement ? el.selectionStart ?? input.length : input.length)
|
|
285
|
-
: input.length;
|
|
292
|
+
const cursorPos = el?.selectionStart ?? input.length;
|
|
286
293
|
const before = input.slice(0, cursorPos);
|
|
287
294
|
const atIdx = before.lastIndexOf('@');
|
|
288
295
|
const newVal = input.slice(0, atIdx) + input.slice(cursorPos);
|
|
@@ -299,9 +306,7 @@ export default function AskContent({ visible, currentFile, initialMessage, onFir
|
|
|
299
306
|
|
|
300
307
|
const selectSlashCommand = useCallback((item: SlashItem) => {
|
|
301
308
|
const el = inputRef.current;
|
|
302
|
-
const cursorPos = el
|
|
303
|
-
? (el instanceof HTMLTextAreaElement || el instanceof HTMLInputElement ? el.selectionStart ?? input.length : input.length)
|
|
304
|
-
: input.length;
|
|
309
|
+
const cursorPos = el?.selectionStart ?? input.length;
|
|
305
310
|
const before = input.slice(0, cursorPos);
|
|
306
311
|
const slashIdx = before.lastIndexOf('/');
|
|
307
312
|
const newVal = input.slice(0, slashIdx) + input.slice(cursorPos);
|
|
@@ -315,7 +320,7 @@ export default function AskContent({ visible, currentFile, initialMessage, onFir
|
|
|
315
320
|
}, [input, slash]);
|
|
316
321
|
|
|
317
322
|
const handleInputKeyDown = useCallback(
|
|
318
|
-
(e: React.KeyboardEvent<
|
|
323
|
+
(e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
|
319
324
|
if (mention.mentionQuery !== null) {
|
|
320
325
|
if (e.key === 'Escape') {
|
|
321
326
|
e.preventDefault();
|
|
@@ -329,7 +334,7 @@ export default function AskContent({ visible, currentFile, initialMessage, onFir
|
|
|
329
334
|
e.preventDefault();
|
|
330
335
|
mention.navigateMention('up');
|
|
331
336
|
} else if (e.key === 'Enter' || e.key === 'Tab') {
|
|
332
|
-
if (e.key === 'Enter' && e.shiftKey) return;
|
|
337
|
+
if (e.key === 'Enter' && (e.shiftKey || e.nativeEvent.isComposing)) return;
|
|
333
338
|
if (mention.mentionResults.length > 0) {
|
|
334
339
|
e.preventDefault();
|
|
335
340
|
selectMention(mention.mentionResults[mention.mentionIndex]);
|
|
@@ -350,7 +355,7 @@ export default function AskContent({ visible, currentFile, initialMessage, onFir
|
|
|
350
355
|
e.preventDefault();
|
|
351
356
|
slash.navigateSlash('up');
|
|
352
357
|
} else if (e.key === 'Enter' || e.key === 'Tab') {
|
|
353
|
-
if (e.key === 'Enter' && e.shiftKey) return;
|
|
358
|
+
if (e.key === 'Enter' && (e.shiftKey || e.nativeEvent.isComposing)) return;
|
|
354
359
|
if (slash.slashResults.length > 0) {
|
|
355
360
|
e.preventDefault();
|
|
356
361
|
selectSlashCommand(slash.slashResults[slash.slashIndex]);
|
|
@@ -358,12 +363,12 @@ export default function AskContent({ visible, currentFile, initialMessage, onFir
|
|
|
358
363
|
}
|
|
359
364
|
return;
|
|
360
365
|
}
|
|
361
|
-
if (
|
|
366
|
+
if (e.key === 'Enter' && !e.shiftKey && !e.nativeEvent.isComposing && !isLoading && input.trim()) {
|
|
362
367
|
e.preventDefault();
|
|
363
368
|
(e.currentTarget as HTMLTextAreaElement).form?.requestSubmit();
|
|
364
369
|
}
|
|
365
370
|
},
|
|
366
|
-
[mention, selectMention, slash, selectSlashCommand,
|
|
371
|
+
[mention, selectMention, slash, selectSlashCommand, isLoading, input],
|
|
367
372
|
);
|
|
368
373
|
|
|
369
374
|
const handleStop = useCallback(() => { abortRef.current?.abort(); }, []);
|
|
@@ -377,7 +382,12 @@ export default function AskContent({ visible, currentFile, initialMessage, onFir
|
|
|
377
382
|
const content = selectedSkill
|
|
378
383
|
? `Use the skill ${selectedSkill.name}: ${text}`
|
|
379
384
|
: text;
|
|
380
|
-
const userMsg: Message = {
|
|
385
|
+
const userMsg: Message = {
|
|
386
|
+
role: 'user',
|
|
387
|
+
content,
|
|
388
|
+
timestamp: Date.now(),
|
|
389
|
+
...(selectedSkill && { skillName: selectedSkill.name }),
|
|
390
|
+
};
|
|
381
391
|
const requestMessages = [...session.messages, userMsg];
|
|
382
392
|
session.setMessages([...requestMessages, { role: 'assistant', content: '', timestamp: Date.now() }]);
|
|
383
393
|
setInput('');
|
|
@@ -389,25 +399,35 @@ export default function AskContent({ visible, currentFile, initialMessage, onFir
|
|
|
389
399
|
setAttachedFiles(currentFile ? [currentFile] : []);
|
|
390
400
|
setIsLoading(true);
|
|
391
401
|
setLoadingPhase('connecting');
|
|
402
|
+
setReconnectAttempt(0);
|
|
392
403
|
|
|
393
404
|
const controller = new AbortController();
|
|
394
405
|
abortRef.current = controller;
|
|
395
406
|
|
|
407
|
+
let maxRetries = 3;
|
|
396
408
|
try {
|
|
409
|
+
const stored = localStorage.getItem('mindos-reconnect-retries');
|
|
410
|
+
if (stored !== null) { const n = parseInt(stored, 10); if (Number.isFinite(n)) maxRetries = Math.max(0, Math.min(10, n)); }
|
|
411
|
+
} catch { /* localStorage unavailable */ }
|
|
412
|
+
reconnectMaxRef.current = maxRetries;
|
|
413
|
+
|
|
414
|
+
const requestBody = JSON.stringify({
|
|
415
|
+
messages: requestMessages,
|
|
416
|
+
currentFile,
|
|
417
|
+
attachedFiles,
|
|
418
|
+
uploadedFiles: upload.localAttachments.map(f => ({
|
|
419
|
+
name: f.name,
|
|
420
|
+
content: f.content.length > 20_000
|
|
421
|
+
? f.content.slice(0, 20_000) + '\n\n[...truncated to first ~20000 chars]'
|
|
422
|
+
: f.content,
|
|
423
|
+
})),
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
const doFetch = async (): Promise<{ finalMessage: Message }> => {
|
|
397
427
|
const res = await fetch('/api/ask', {
|
|
398
428
|
method: 'POST',
|
|
399
429
|
headers: { 'Content-Type': 'application/json' },
|
|
400
|
-
body:
|
|
401
|
-
messages: requestMessages,
|
|
402
|
-
currentFile,
|
|
403
|
-
attachedFiles,
|
|
404
|
-
uploadedFiles: upload.localAttachments.map(f => ({
|
|
405
|
-
name: f.name,
|
|
406
|
-
content: f.content.length > 20_000
|
|
407
|
-
? f.content.slice(0, 20_000) + '\n\n[...truncated to first ~20000 chars]'
|
|
408
|
-
: f.content,
|
|
409
|
-
})),
|
|
410
|
-
}),
|
|
430
|
+
body: requestBody,
|
|
411
431
|
signal: controller.signal,
|
|
412
432
|
});
|
|
413
433
|
|
|
@@ -423,7 +443,9 @@ export default function AskContent({ visible, currentFile, initialMessage, onFir
|
|
|
423
443
|
errorMsg = errBody.message;
|
|
424
444
|
}
|
|
425
445
|
} catch {}
|
|
426
|
-
|
|
446
|
+
const err = new Error(errorMsg);
|
|
447
|
+
(err as Error & { httpStatus?: number }).httpStatus = res.status;
|
|
448
|
+
throw err;
|
|
427
449
|
}
|
|
428
450
|
|
|
429
451
|
if (!res.body) throw new Error('No response body');
|
|
@@ -442,14 +464,45 @@ export default function AskContent({ visible, currentFile, initialMessage, onFir
|
|
|
442
464
|
},
|
|
443
465
|
controller.signal,
|
|
444
466
|
);
|
|
467
|
+
return { finalMessage };
|
|
468
|
+
};
|
|
445
469
|
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
470
|
+
try {
|
|
471
|
+
let lastError: Error | null = null;
|
|
472
|
+
|
|
473
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
474
|
+
if (controller.signal.aborted) break;
|
|
475
|
+
|
|
476
|
+
if (attempt > 0) {
|
|
477
|
+
setReconnectAttempt(attempt);
|
|
478
|
+
setLoadingPhase('reconnecting');
|
|
479
|
+
session.setMessages(prev => {
|
|
480
|
+
const updated = [...prev];
|
|
481
|
+
updated[updated.length - 1] = { role: 'assistant', content: '', timestamp: Date.now() };
|
|
482
|
+
return updated;
|
|
483
|
+
});
|
|
484
|
+
await sleep(retryDelay(attempt - 1), controller.signal);
|
|
485
|
+
setLoadingPhase('connecting');
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
try {
|
|
489
|
+
const { finalMessage } = await doFetch();
|
|
490
|
+
if (!finalMessage.content.trim() && (!finalMessage.parts || finalMessage.parts.length === 0)) {
|
|
491
|
+
session.setMessages(prev => {
|
|
492
|
+
const updated = [...prev];
|
|
493
|
+
updated[updated.length - 1] = { role: 'assistant', content: `__error__${t.ask.errorNoResponse}` };
|
|
494
|
+
return updated;
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
return;
|
|
498
|
+
} catch (err) {
|
|
499
|
+
lastError = err instanceof Error ? err : new Error(String(err));
|
|
500
|
+
const httpStatus = (err as Error & { httpStatus?: number }).httpStatus;
|
|
501
|
+
if (!isRetryableError(err, httpStatus) || attempt >= maxRetries) break;
|
|
502
|
+
}
|
|
452
503
|
}
|
|
504
|
+
|
|
505
|
+
if (lastError) throw lastError;
|
|
453
506
|
} catch (err) {
|
|
454
507
|
if ((err as Error).name === 'AbortError') {
|
|
455
508
|
session.setMessages(prev => {
|
|
@@ -482,6 +535,7 @@ export default function AskContent({ visible, currentFile, initialMessage, onFir
|
|
|
482
535
|
}
|
|
483
536
|
} finally {
|
|
484
537
|
setIsLoading(false);
|
|
538
|
+
setReconnectAttempt(0);
|
|
485
539
|
abortRef.current = null;
|
|
486
540
|
}
|
|
487
541
|
}, [input, session, isLoading, currentFile, attachedFiles, upload.localAttachments, mention.mentionQuery, slash.slashQuery, selectedSkill, t.ask.errorNoResponse, t.ask.stopped, onFirstMessage]);
|
|
@@ -595,15 +649,21 @@ export default function AskContent({ visible, currentFile, initialMessage, onFir
|
|
|
595
649
|
emptyPrompt={t.ask.emptyPrompt}
|
|
596
650
|
suggestions={t.ask.suggestions}
|
|
597
651
|
onSuggestionClick={setInput}
|
|
598
|
-
labels={{
|
|
652
|
+
labels={{
|
|
653
|
+
connecting: t.ask.connecting,
|
|
654
|
+
thinking: t.ask.thinking,
|
|
655
|
+
generating: t.ask.generating,
|
|
656
|
+
reconnecting: reconnectAttempt > 0 ? t.ask.reconnecting(reconnectAttempt, reconnectMaxRef.current) : undefined,
|
|
657
|
+
}}
|
|
599
658
|
/>
|
|
600
659
|
|
|
601
|
-
{/* Popovers —
|
|
660
|
+
{/* Popovers — flex children so they stay within overflow boundary (absolute positioning would be clipped by RightAskPanel's overflow-hidden) */}
|
|
602
661
|
{mention.mentionQuery !== null && mention.mentionResults.length > 0 && (
|
|
603
662
|
<div className="shrink-0 px-2 pb-1">
|
|
604
663
|
<MentionPopover
|
|
605
664
|
results={mention.mentionResults}
|
|
606
665
|
selectedIndex={mention.mentionIndex}
|
|
666
|
+
query={mention.mentionQuery ?? undefined}
|
|
607
667
|
onSelect={selectMention}
|
|
608
668
|
/>
|
|
609
669
|
</div>
|
|
@@ -614,6 +674,7 @@ export default function AskContent({ visible, currentFile, initialMessage, onFir
|
|
|
614
674
|
<SlashCommandPopover
|
|
615
675
|
results={slash.slashResults}
|
|
616
676
|
selectedIndex={slash.slashIndex}
|
|
677
|
+
query={slash.slashQuery ?? undefined}
|
|
617
678
|
onSelect={selectSlashCommand}
|
|
618
679
|
/>
|
|
619
680
|
</div>
|
|
@@ -706,7 +767,7 @@ export default function AskContent({ visible, currentFile, initialMessage, onFir
|
|
|
706
767
|
onSubmit={handleSubmit}
|
|
707
768
|
className={cn(
|
|
708
769
|
'flex',
|
|
709
|
-
isPanel ? 'min-h-0 flex-1 items-end gap-1.5 px-2 py-2' : 'items-
|
|
770
|
+
isPanel ? 'min-h-0 flex-1 items-end gap-1.5 px-2 py-2' : 'items-end gap-2 px-3 py-3',
|
|
710
771
|
)}
|
|
711
772
|
>
|
|
712
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">
|
|
@@ -726,34 +787,24 @@ export default function AskContent({ visible, currentFile, initialMessage, onFir
|
|
|
726
787
|
}}
|
|
727
788
|
/>
|
|
728
789
|
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
ref={(el) => {
|
|
744
|
-
inputRef.current = el;
|
|
745
|
-
}}
|
|
746
|
-
value={input}
|
|
747
|
-
onChange={e => handleInputChange(e.target.value, e.target.selectionStart ?? undefined)}
|
|
748
|
-
onKeyDown={handleInputKeyDown}
|
|
749
|
-
placeholder={t.ask.placeholder}
|
|
750
|
-
className="flex-1 bg-transparent text-sm text-foreground placeholder:text-muted-foreground outline-none min-w-0"
|
|
751
|
-
/>
|
|
752
|
-
)}
|
|
790
|
+
<textarea
|
|
791
|
+
ref={(el) => {
|
|
792
|
+
inputRef.current = el;
|
|
793
|
+
}}
|
|
794
|
+
value={input}
|
|
795
|
+
onChange={e => handleInputChange(e.target.value, e.target.selectionStart ?? undefined)}
|
|
796
|
+
onKeyDown={handleInputKeyDown}
|
|
797
|
+
placeholder={t.ask.placeholder}
|
|
798
|
+
rows={1}
|
|
799
|
+
className={cn(
|
|
800
|
+
'min-w-0 flex-1 resize-none overflow-y-auto bg-transparent text-sm leading-snug text-foreground placeholder:text-muted-foreground outline-none focus-visible:ring-0',
|
|
801
|
+
isPanel ? 'py-2' : 'py-1.5',
|
|
802
|
+
)}
|
|
803
|
+
/>
|
|
753
804
|
|
|
754
805
|
{isLoading ? (
|
|
755
|
-
<button type="button" onClick={handleStop} className="p-1.5 rounded-md transition-colors shrink-0 text-muted-foreground hover:text-foreground hover:bg-muted" title={t.ask.stopTitle}>
|
|
756
|
-
<StopCircle size={inputIconSize} />
|
|
806
|
+
<button type="button" onClick={handleStop} className="p-1.5 rounded-md transition-colors shrink-0 text-muted-foreground hover:text-foreground hover:bg-muted" title={loadingPhase === 'reconnecting' ? t.ask.cancelReconnect : t.ask.stopTitle}>
|
|
807
|
+
{loadingPhase === 'reconnecting' ? <X size={inputIconSize} /> : <StopCircle size={inputIconSize} />}
|
|
757
808
|
</button>
|
|
758
809
|
) : (
|
|
759
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)]">
|
|
@@ -770,18 +821,16 @@ export default function AskContent({ visible, currentFile, initialMessage, onFir
|
|
|
770
821
|
'flex shrink-0 items-center',
|
|
771
822
|
isPanel
|
|
772
823
|
? 'flex-wrap gap-x-2 gap-y-1 px-3 pb-1.5 text-[10px] text-muted-foreground/40'
|
|
773
|
-
: '
|
|
824
|
+
: 'flex-wrap gap-x-3 gap-y-1 px-4 pb-2 text-[10px] md:text-xs text-muted-foreground/50',
|
|
774
825
|
)}
|
|
775
826
|
>
|
|
776
827
|
<span suppressHydrationWarning>
|
|
777
828
|
<kbd className="font-mono">↵</kbd> {t.ask.send}
|
|
778
829
|
</span>
|
|
779
|
-
|
|
780
|
-
<
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
</span>
|
|
784
|
-
) : null}
|
|
830
|
+
<span suppressHydrationWarning>
|
|
831
|
+
<kbd className="font-mono">⇧</kbd>
|
|
832
|
+
<kbd className="font-mono ml-0.5">↵</kbd> {t.ask.newlineHint}
|
|
833
|
+
</span>
|
|
785
834
|
<span suppressHydrationWarning>
|
|
786
835
|
<kbd className="font-mono">@</kbd> {t.ask.attachFile}
|
|
787
836
|
</span>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
export default function HighlightMatch({ text, query }: { text: string; query?: string }) {
|
|
4
|
+
if (!query) return <>{text}</>;
|
|
5
|
+
const idx = text.toLowerCase().indexOf(query.toLowerCase());
|
|
6
|
+
if (idx === -1) return <>{text}</>;
|
|
7
|
+
return (
|
|
8
|
+
<>
|
|
9
|
+
{text.slice(0, idx)}
|
|
10
|
+
<span className="text-[var(--amber)] font-semibold">{text.slice(idx, idx + query.length)}</span>
|
|
11
|
+
{text.slice(idx + query.length)}
|
|
12
|
+
</>
|
|
13
|
+
);
|
|
14
|
+
}
|
|
@@ -2,14 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
import { useEffect, useRef } from 'react';
|
|
4
4
|
import { FileText, Table, FolderOpen } from 'lucide-react';
|
|
5
|
+
import HighlightMatch from './HighlightMatch';
|
|
5
6
|
|
|
6
7
|
interface MentionPopoverProps {
|
|
7
8
|
results: string[];
|
|
8
9
|
selectedIndex: number;
|
|
10
|
+
query?: string;
|
|
9
11
|
onSelect: (filePath: string) => void;
|
|
10
12
|
}
|
|
11
13
|
|
|
12
|
-
export default function MentionPopover({ results, selectedIndex, onSelect }: MentionPopoverProps) {
|
|
14
|
+
export default function MentionPopover({ results, selectedIndex, query, onSelect }: MentionPopoverProps) {
|
|
13
15
|
const listRef = useRef<HTMLDivElement>(null);
|
|
14
16
|
|
|
15
17
|
useEffect(() => {
|
|
@@ -52,10 +54,10 @@ export default function MentionPopover({ results, selectedIndex, onSelect }: Men
|
|
|
52
54
|
) : (
|
|
53
55
|
<FileText size={13} className="text-muted-foreground shrink-0" />
|
|
54
56
|
)}
|
|
55
|
-
<span className="truncate font-medium flex-1"
|
|
57
|
+
<span className="truncate font-medium flex-1"><HighlightMatch text={name} query={query} /></span>
|
|
56
58
|
{dir && (
|
|
57
59
|
<span className="text-2xs text-muted-foreground/40 truncate max-w-[140px] shrink-0">
|
|
58
|
-
{dir}
|
|
60
|
+
<HighlightMatch text={dir} query={query} />
|
|
59
61
|
</span>
|
|
60
62
|
)}
|
|
61
63
|
</button>
|