@geminilight/mindos 0.6.7 → 0.6.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/README_zh.md +2 -0
- package/app/app/api/ask/route.ts +35 -2
- package/app/app/api/file/route.ts +27 -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/check-port/route.ts +18 -13
- 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 +104 -60
- 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 -70
- 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 +450 -0
- package/app/hooks/useFileImport.ts +39 -2
- 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 +85 -4
- package/app/lib/i18n-zh.ts +85 -4
- 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-env.d.ts +1 -1
- package/app/next.config.ts +23 -5
- package/app/package.json +1 -1
- package/bin/cli.js +21 -18
- 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/release.sh +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,20 +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,
|
|
405
|
-
}),
|
|
430
|
+
body: requestBody,
|
|
406
431
|
signal: controller.signal,
|
|
407
432
|
});
|
|
408
433
|
|
|
@@ -418,7 +443,9 @@ export default function AskContent({ visible, currentFile, initialMessage, onFir
|
|
|
418
443
|
errorMsg = errBody.message;
|
|
419
444
|
}
|
|
420
445
|
} catch {}
|
|
421
|
-
|
|
446
|
+
const err = new Error(errorMsg);
|
|
447
|
+
(err as Error & { httpStatus?: number }).httpStatus = res.status;
|
|
448
|
+
throw err;
|
|
422
449
|
}
|
|
423
450
|
|
|
424
451
|
if (!res.body) throw new Error('No response body');
|
|
@@ -437,14 +464,45 @@ export default function AskContent({ visible, currentFile, initialMessage, onFir
|
|
|
437
464
|
},
|
|
438
465
|
controller.signal,
|
|
439
466
|
);
|
|
467
|
+
return { finalMessage };
|
|
468
|
+
};
|
|
440
469
|
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
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
|
+
}
|
|
447
503
|
}
|
|
504
|
+
|
|
505
|
+
if (lastError) throw lastError;
|
|
448
506
|
} catch (err) {
|
|
449
507
|
if ((err as Error).name === 'AbortError') {
|
|
450
508
|
session.setMessages(prev => {
|
|
@@ -477,6 +535,7 @@ export default function AskContent({ visible, currentFile, initialMessage, onFir
|
|
|
477
535
|
}
|
|
478
536
|
} finally {
|
|
479
537
|
setIsLoading(false);
|
|
538
|
+
setReconnectAttempt(0);
|
|
480
539
|
abortRef.current = null;
|
|
481
540
|
}
|
|
482
541
|
}, [input, session, isLoading, currentFile, attachedFiles, upload.localAttachments, mention.mentionQuery, slash.slashQuery, selectedSkill, t.ask.errorNoResponse, t.ask.stopped, onFirstMessage]);
|
|
@@ -590,15 +649,21 @@ export default function AskContent({ visible, currentFile, initialMessage, onFir
|
|
|
590
649
|
emptyPrompt={t.ask.emptyPrompt}
|
|
591
650
|
suggestions={t.ask.suggestions}
|
|
592
651
|
onSuggestionClick={setInput}
|
|
593
|
-
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
|
+
}}
|
|
594
658
|
/>
|
|
595
659
|
|
|
596
|
-
{/* Popovers —
|
|
660
|
+
{/* Popovers — flex children so they stay within overflow boundary (absolute positioning would be clipped by RightAskPanel's overflow-hidden) */}
|
|
597
661
|
{mention.mentionQuery !== null && mention.mentionResults.length > 0 && (
|
|
598
662
|
<div className="shrink-0 px-2 pb-1">
|
|
599
663
|
<MentionPopover
|
|
600
664
|
results={mention.mentionResults}
|
|
601
665
|
selectedIndex={mention.mentionIndex}
|
|
666
|
+
query={mention.mentionQuery ?? undefined}
|
|
602
667
|
onSelect={selectMention}
|
|
603
668
|
/>
|
|
604
669
|
</div>
|
|
@@ -609,6 +674,7 @@ export default function AskContent({ visible, currentFile, initialMessage, onFir
|
|
|
609
674
|
<SlashCommandPopover
|
|
610
675
|
results={slash.slashResults}
|
|
611
676
|
selectedIndex={slash.slashIndex}
|
|
677
|
+
query={slash.slashQuery ?? undefined}
|
|
612
678
|
onSelect={selectSlashCommand}
|
|
613
679
|
/>
|
|
614
680
|
</div>
|
|
@@ -701,7 +767,7 @@ export default function AskContent({ visible, currentFile, initialMessage, onFir
|
|
|
701
767
|
onSubmit={handleSubmit}
|
|
702
768
|
className={cn(
|
|
703
769
|
'flex',
|
|
704
|
-
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',
|
|
705
771
|
)}
|
|
706
772
|
>
|
|
707
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">
|
|
@@ -721,34 +787,24 @@ export default function AskContent({ visible, currentFile, initialMessage, onFir
|
|
|
721
787
|
}}
|
|
722
788
|
/>
|
|
723
789
|
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
ref={(el) => {
|
|
739
|
-
inputRef.current = el;
|
|
740
|
-
}}
|
|
741
|
-
value={input}
|
|
742
|
-
onChange={e => handleInputChange(e.target.value, e.target.selectionStart ?? undefined)}
|
|
743
|
-
onKeyDown={handleInputKeyDown}
|
|
744
|
-
placeholder={t.ask.placeholder}
|
|
745
|
-
className="flex-1 bg-transparent text-sm text-foreground placeholder:text-muted-foreground outline-none min-w-0"
|
|
746
|
-
/>
|
|
747
|
-
)}
|
|
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
|
+
/>
|
|
748
804
|
|
|
749
805
|
{isLoading ? (
|
|
750
|
-
<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}>
|
|
751
|
-
<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} />}
|
|
752
808
|
</button>
|
|
753
809
|
) : (
|
|
754
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)]">
|
|
@@ -765,18 +821,16 @@ export default function AskContent({ visible, currentFile, initialMessage, onFir
|
|
|
765
821
|
'flex shrink-0 items-center',
|
|
766
822
|
isPanel
|
|
767
823
|
? 'flex-wrap gap-x-2 gap-y-1 px-3 pb-1.5 text-[10px] text-muted-foreground/40'
|
|
768
|
-
: '
|
|
824
|
+
: 'flex-wrap gap-x-3 gap-y-1 px-4 pb-2 text-[10px] md:text-xs text-muted-foreground/50',
|
|
769
825
|
)}
|
|
770
826
|
>
|
|
771
827
|
<span suppressHydrationWarning>
|
|
772
828
|
<kbd className="font-mono">↵</kbd> {t.ask.send}
|
|
773
829
|
</span>
|
|
774
|
-
|
|
775
|
-
<
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
</span>
|
|
779
|
-
) : null}
|
|
830
|
+
<span suppressHydrationWarning>
|
|
831
|
+
<kbd className="font-mono">⇧</kbd>
|
|
832
|
+
<kbd className="font-mono ml-0.5">↵</kbd> {t.ask.newlineHint}
|
|
833
|
+
</span>
|
|
780
834
|
<span suppressHydrationWarning>
|
|
781
835
|
<kbd className="font-mono">@</kbd> {t.ask.attachFile}
|
|
782
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>
|