@geminilight/mindos 0.6.17 → 0.6.19
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/app/app/api/ask/route.ts +220 -2
- package/app/app/api/settings/list-models/route.ts +96 -0
- package/app/app/api/settings/test-key/route.ts +79 -35
- package/app/components/ActivityBar.tsx +8 -5
- package/app/components/JsonView.tsx +2 -5
- package/app/components/OrganizeToast.tsx +237 -82
- package/app/components/Panel.tsx +6 -6
- package/app/components/SidebarLayout.tsx +10 -0
- package/app/components/UpdateOverlay.tsx +6 -6
- package/app/components/agents/AgentDetailContent.tsx +2 -2
- package/app/components/agents/AgentsMcpSection.tsx +2 -2
- package/app/components/agents/AgentsOverviewSection.tsx +11 -11
- package/app/components/agents/AgentsPrimitives.tsx +14 -14
- package/app/components/agents/AgentsSkillsSection.tsx +1 -1
- package/app/components/agents/SkillDetailPopover.tsx +1 -1
- package/app/components/ask/MessageList.tsx +1 -1
- package/app/components/panels/EchoPanel.tsx +7 -7
- package/app/components/renderers/summary/SummaryRenderer.tsx +1 -1
- package/app/components/settings/AiTab.tsx +133 -9
- package/app/components/settings/types.ts +1 -0
- package/app/lib/i18n-en.ts +10 -1
- package/app/lib/i18n-zh.ts +10 -1
- package/app/lib/settings.ts +2 -0
- package/app/next-env.d.ts +1 -1
- package/package.json +1 -1
|
@@ -207,7 +207,7 @@ Be specific. Reference actual content from the files. Keep the total response un
|
|
|
207
207
|
|
|
208
208
|
{/* error */}
|
|
209
209
|
{error && (
|
|
210
|
-
<div className="font-display" style={{ padding: '10px 14px', borderRadius: 8, background: '
|
|
210
|
+
<div className="font-display" style={{ padding: '10px 14px', borderRadius: 8, background: 'color-mix(in srgb, var(--error) 10%, transparent)', border: '1px solid color-mix(in srgb, var(--error) 30%, transparent)', color: 'var(--error)', fontSize: 12, marginBottom: '1rem' }}>
|
|
211
211
|
{error}
|
|
212
212
|
</div>
|
|
213
213
|
)}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { useState, useRef, useCallback, useEffect } from 'react';
|
|
4
|
-
import { AlertCircle, Loader2 } from 'lucide-react';
|
|
4
|
+
import { AlertCircle, ChevronDown, Loader2 } from 'lucide-react';
|
|
5
5
|
import type { AiSettings, AgentSettings, ProviderConfig, SettingsData, AiTabProps } from './types';
|
|
6
6
|
import { Field, Select, Input, EnvBadge, ApiKeyInput, Toggle, SectionLabel } from './Primitives';
|
|
7
7
|
import { useLocale } from '@/lib/LocaleContext';
|
|
@@ -14,6 +14,7 @@ interface TestResult {
|
|
|
14
14
|
latency?: number;
|
|
15
15
|
error?: string;
|
|
16
16
|
code?: ErrorCode;
|
|
17
|
+
streamingSupported?: boolean;
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
function errorMessage(t: AiTabProps['t'], code?: ErrorCode): string {
|
|
@@ -72,12 +73,16 @@ export function AiTab({ data, updateAi, updateAgent, t }: AiTabProps) {
|
|
|
72
73
|
const json = await res.json();
|
|
73
74
|
|
|
74
75
|
if (json.ok) {
|
|
75
|
-
|
|
76
|
-
|
|
76
|
+
const streamingSupported = json.streamingSupported !== false;
|
|
77
|
+
setTestResult(prev => ({ ...prev, [providerName]: { state: 'ok', latency: json.latency, streamingSupported } }));
|
|
78
|
+
// Auto-persist streaming capability so /api/ask uses the right path
|
|
79
|
+
if (providerName === data.ai.provider) {
|
|
80
|
+
updateAgent({ useStreaming: streamingSupported });
|
|
81
|
+
}
|
|
77
82
|
if (okTimerRef.current) clearTimeout(okTimerRef.current);
|
|
78
83
|
okTimerRef.current = setTimeout(() => {
|
|
79
84
|
setTestResult(prev => ({ ...prev, [providerName]: { state: 'idle' } }));
|
|
80
|
-
},
|
|
85
|
+
}, 8000);
|
|
81
86
|
} else {
|
|
82
87
|
setTestResult(prev => ({
|
|
83
88
|
...prev,
|
|
@@ -139,7 +144,12 @@ export function AiTab({ data, updateAi, updateAgent, t }: AiTabProps) {
|
|
|
139
144
|
)}
|
|
140
145
|
</button>
|
|
141
146
|
{result.state === 'ok' && result.latency != null && (
|
|
142
|
-
<span className="text-xs text-success">
|
|
147
|
+
<span className="text-xs text-success">
|
|
148
|
+
{t.settings.ai.testKeyOk(result.latency)}
|
|
149
|
+
{result.streamingSupported === false && (
|
|
150
|
+
<span className="text-muted-foreground ml-1.5">{t.settings.ai.streamingFallback}</span>
|
|
151
|
+
)}
|
|
152
|
+
</span>
|
|
143
153
|
)}
|
|
144
154
|
{result.state === 'error' && (
|
|
145
155
|
<span className="text-xs text-error">✗ {errorMessage(t, result.code)}</span>
|
|
@@ -163,10 +173,14 @@ export function AiTab({ data, updateAi, updateAgent, t }: AiTabProps) {
|
|
|
163
173
|
{provider === 'anthropic' ? (
|
|
164
174
|
<>
|
|
165
175
|
<Field label={<>{t.settings.ai.model} <EnvBadge overridden={env.ANTHROPIC_MODEL} /></>}>
|
|
166
|
-
<
|
|
176
|
+
<ModelInput
|
|
167
177
|
value={anthropic.model}
|
|
168
|
-
onChange={
|
|
178
|
+
onChange={v => patchProvider('anthropic', { model: v })}
|
|
169
179
|
placeholder={envVal.ANTHROPIC_MODEL || 'claude-sonnet-4-6'}
|
|
180
|
+
provider="anthropic"
|
|
181
|
+
apiKey={anthropic.apiKey}
|
|
182
|
+
envKey={env.ANTHROPIC_API_KEY}
|
|
183
|
+
t={t}
|
|
170
184
|
/>
|
|
171
185
|
</Field>
|
|
172
186
|
<Field
|
|
@@ -183,10 +197,15 @@ export function AiTab({ data, updateAi, updateAgent, t }: AiTabProps) {
|
|
|
183
197
|
) : (
|
|
184
198
|
<>
|
|
185
199
|
<Field label={<>{t.settings.ai.model} <EnvBadge overridden={env.OPENAI_MODEL} /></>}>
|
|
186
|
-
<
|
|
200
|
+
<ModelInput
|
|
187
201
|
value={openai.model}
|
|
188
|
-
onChange={
|
|
202
|
+
onChange={v => patchProvider('openai', { model: v })}
|
|
189
203
|
placeholder={envVal.OPENAI_MODEL || 'gpt-5.4'}
|
|
204
|
+
provider="openai"
|
|
205
|
+
apiKey={openai.apiKey}
|
|
206
|
+
envKey={env.OPENAI_API_KEY}
|
|
207
|
+
baseUrl={openai.baseUrl}
|
|
208
|
+
t={t}
|
|
190
209
|
/>
|
|
191
210
|
</Field>
|
|
192
211
|
<Field
|
|
@@ -309,6 +328,111 @@ export function AiTab({ data, updateAi, updateAgent, t }: AiTabProps) {
|
|
|
309
328
|
);
|
|
310
329
|
}
|
|
311
330
|
|
|
331
|
+
/* ── Model Input with "List models" picker ── */
|
|
332
|
+
|
|
333
|
+
function ModelInput({
|
|
334
|
+
value, onChange, placeholder, provider, apiKey, envKey, baseUrl, t,
|
|
335
|
+
}: {
|
|
336
|
+
value: string;
|
|
337
|
+
onChange: (v: string) => void;
|
|
338
|
+
placeholder: string;
|
|
339
|
+
provider: 'anthropic' | 'openai';
|
|
340
|
+
apiKey: string;
|
|
341
|
+
envKey?: boolean;
|
|
342
|
+
baseUrl?: string;
|
|
343
|
+
t: AiTabProps['t'];
|
|
344
|
+
}) {
|
|
345
|
+
const [models, setModels] = useState<string[] | null>(null);
|
|
346
|
+
const [loading, setLoading] = useState(false);
|
|
347
|
+
const [open, setOpen] = useState(false);
|
|
348
|
+
const [error, setError] = useState('');
|
|
349
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
350
|
+
|
|
351
|
+
const hasKey = !!apiKey || !!envKey;
|
|
352
|
+
|
|
353
|
+
const fetchModels = useCallback(async () => {
|
|
354
|
+
if (loading) return;
|
|
355
|
+
setLoading(true);
|
|
356
|
+
setError('');
|
|
357
|
+
try {
|
|
358
|
+
const body: Record<string, string> = { provider };
|
|
359
|
+
if (apiKey) body.apiKey = apiKey;
|
|
360
|
+
if (baseUrl) body.baseUrl = baseUrl;
|
|
361
|
+
|
|
362
|
+
const res = await fetch('/api/settings/list-models', {
|
|
363
|
+
method: 'POST',
|
|
364
|
+
headers: { 'Content-Type': 'application/json' },
|
|
365
|
+
body: JSON.stringify(body),
|
|
366
|
+
});
|
|
367
|
+
const json = await res.json();
|
|
368
|
+
if (json.ok && Array.isArray(json.models)) {
|
|
369
|
+
setModels(json.models);
|
|
370
|
+
setOpen(true);
|
|
371
|
+
} else {
|
|
372
|
+
setError(json.error || 'Failed to fetch models');
|
|
373
|
+
}
|
|
374
|
+
} catch {
|
|
375
|
+
setError('Network error');
|
|
376
|
+
} finally {
|
|
377
|
+
setLoading(false);
|
|
378
|
+
}
|
|
379
|
+
}, [provider, apiKey, baseUrl, loading]);
|
|
380
|
+
|
|
381
|
+
useEffect(() => {
|
|
382
|
+
if (!open) return;
|
|
383
|
+
function handleClickOutside(e: MouseEvent) {
|
|
384
|
+
if (containerRef.current && !containerRef.current.contains(e.target as Node)) {
|
|
385
|
+
setOpen(false);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
document.addEventListener('mousedown', handleClickOutside);
|
|
389
|
+
return () => document.removeEventListener('mousedown', handleClickOutside);
|
|
390
|
+
}, [open]);
|
|
391
|
+
|
|
392
|
+
return (
|
|
393
|
+
<div ref={containerRef} className="relative">
|
|
394
|
+
<div className="flex gap-1.5">
|
|
395
|
+
<Input
|
|
396
|
+
value={value}
|
|
397
|
+
onChange={e => onChange(e.target.value)}
|
|
398
|
+
placeholder={placeholder}
|
|
399
|
+
className="flex-1"
|
|
400
|
+
/>
|
|
401
|
+
<button
|
|
402
|
+
type="button"
|
|
403
|
+
disabled={!hasKey || loading}
|
|
404
|
+
onClick={fetchModels}
|
|
405
|
+
title={t.settings.ai.listModels}
|
|
406
|
+
className="inline-flex items-center gap-1 px-2 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 shrink-0"
|
|
407
|
+
>
|
|
408
|
+
{loading ? <Loader2 size={12} className="animate-spin" /> : <ChevronDown size={12} />}
|
|
409
|
+
{t.settings.ai.listModels}
|
|
410
|
+
</button>
|
|
411
|
+
</div>
|
|
412
|
+
{error && <p className="text-xs text-error mt-1">{error}</p>}
|
|
413
|
+
{open && models && models.length > 0 && (
|
|
414
|
+
<div className="absolute z-50 mt-1 w-full max-h-48 overflow-y-auto rounded-lg border border-border bg-popover shadow-lg">
|
|
415
|
+
{models.map(m => (
|
|
416
|
+
<button
|
|
417
|
+
key={m}
|
|
418
|
+
type="button"
|
|
419
|
+
className={`w-full text-left px-3 py-1.5 text-xs hover:bg-accent transition-colors ${m === value ? 'bg-accent/60 font-medium' : ''}`}
|
|
420
|
+
onClick={() => { onChange(m); setOpen(false); }}
|
|
421
|
+
>
|
|
422
|
+
{m}
|
|
423
|
+
</button>
|
|
424
|
+
))}
|
|
425
|
+
</div>
|
|
426
|
+
)}
|
|
427
|
+
{open && models && models.length === 0 && (
|
|
428
|
+
<div className="absolute z-50 mt-1 w-full rounded-lg border border-border bg-popover shadow-lg px-3 py-2 text-xs text-muted-foreground">
|
|
429
|
+
{t.settings.ai.noModelsFound}
|
|
430
|
+
</div>
|
|
431
|
+
)}
|
|
432
|
+
</div>
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
|
|
312
436
|
/* ── Ask AI Display Mode (localStorage-based, no server roundtrip) ── */
|
|
313
437
|
|
|
314
438
|
function AskDisplayMode() {
|
package/app/lib/i18n-en.ts
CHANGED
|
@@ -144,7 +144,7 @@ export const en = {
|
|
|
144
144
|
searching: 'Searching knowledge base...',
|
|
145
145
|
generating: 'Generating response...',
|
|
146
146
|
stopped: 'Generation stopped.',
|
|
147
|
-
errorNoResponse: '
|
|
147
|
+
errorNoResponse: 'AI returned no content. Possible causes: model does not support streaming, proxy compatibility issue, or request exceeds context limit.',
|
|
148
148
|
reconnecting: (attempt: number, max: number) => `Connection lost. Reconnecting (${attempt}/${max})...`,
|
|
149
149
|
reconnectFailed: 'Connection failed after multiple attempts.',
|
|
150
150
|
retry: 'Retry',
|
|
@@ -789,6 +789,12 @@ export const en = {
|
|
|
789
789
|
organizeUndone: 'Undone',
|
|
790
790
|
organizeViewFile: 'View file',
|
|
791
791
|
organizeUndoSuccess: (n: number) => `Reverted ${n} file${n !== 1 ? 's' : ''}`,
|
|
792
|
+
organizeDetailTitle: 'AI Organize Details',
|
|
793
|
+
organizeSourceFiles: 'Source Files',
|
|
794
|
+
organizeSummaryLabel: 'AI Summary',
|
|
795
|
+
organizeChangesLabel: (n: number) => `Changes (${n})`,
|
|
796
|
+
organizeNoSummary: 'AI is working...',
|
|
797
|
+
organizeMinimizeModal: 'Minimize',
|
|
792
798
|
},
|
|
793
799
|
importHistory: {
|
|
794
800
|
title: 'Import History',
|
|
@@ -836,6 +842,9 @@ export const en = {
|
|
|
836
842
|
testKeyNetworkError: 'Network error',
|
|
837
843
|
testKeyNoKey: 'No API key configured',
|
|
838
844
|
testKeyUnknown: 'Test failed',
|
|
845
|
+
listModels: 'Browse',
|
|
846
|
+
noModelsFound: 'No models found',
|
|
847
|
+
streamingFallback: '(will use standard mode)',
|
|
839
848
|
},
|
|
840
849
|
agent: {
|
|
841
850
|
title: 'Agent Behavior',
|
package/app/lib/i18n-zh.ts
CHANGED
|
@@ -169,7 +169,7 @@ export const zh = {
|
|
|
169
169
|
searching: '正在搜索知识库...',
|
|
170
170
|
generating: '正在生成回复...',
|
|
171
171
|
stopped: '已停止生成。',
|
|
172
|
-
errorNoResponse: 'AI
|
|
172
|
+
errorNoResponse: 'AI 未返回有效内容。可能原因:模型不支持流式输出、中转站兼容性问题、或请求超出上下文限制。',
|
|
173
173
|
reconnecting: (attempt: number, max: number) => `连接中断,正在重连 (${attempt}/${max})...`,
|
|
174
174
|
reconnectFailed: '多次重连失败,请检查网络后重试。',
|
|
175
175
|
retry: '重试',
|
|
@@ -813,6 +813,12 @@ export const zh = {
|
|
|
813
813
|
organizeUndone: '已撤销',
|
|
814
814
|
organizeViewFile: '查看文件',
|
|
815
815
|
organizeUndoSuccess: (n: number) => `已撤销 ${n} 个文件`,
|
|
816
|
+
organizeDetailTitle: 'AI 整理详情',
|
|
817
|
+
organizeSourceFiles: '源文件',
|
|
818
|
+
organizeSummaryLabel: 'AI 总结',
|
|
819
|
+
organizeChangesLabel: (n: number) => `变更列表 (${n})`,
|
|
820
|
+
organizeNoSummary: 'AI 正在处理中...',
|
|
821
|
+
organizeMinimizeModal: '最小化',
|
|
816
822
|
},
|
|
817
823
|
importHistory: {
|
|
818
824
|
title: '导入历史',
|
|
@@ -860,6 +866,9 @@ export const zh = {
|
|
|
860
866
|
testKeyNetworkError: '网络错误',
|
|
861
867
|
testKeyNoKey: '未配置 API Key',
|
|
862
868
|
testKeyUnknown: '测试失败',
|
|
869
|
+
listModels: '选择模型',
|
|
870
|
+
noModelsFound: '未找到可用模型',
|
|
871
|
+
streamingFallback: '(将使用标准模式)',
|
|
863
872
|
},
|
|
864
873
|
agent: {
|
|
865
874
|
title: 'Agent 行为',
|
package/app/lib/settings.ts
CHANGED
|
@@ -24,6 +24,7 @@ export interface AgentConfig {
|
|
|
24
24
|
thinkingBudget?: number; // default 5000
|
|
25
25
|
contextStrategy?: 'auto' | 'off'; // default 'auto'
|
|
26
26
|
reconnectRetries?: number; // default 3, range 0-10 (0 = disabled)
|
|
27
|
+
useStreaming?: boolean; // default true; false = non-streaming fallback for proxy compat
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
export interface GuideState {
|
|
@@ -130,6 +131,7 @@ function parseAgent(raw: unknown): AgentConfig | undefined {
|
|
|
130
131
|
if (typeof obj.thinkingBudget === 'number') result.thinkingBudget = Math.min(50000, Math.max(1000, obj.thinkingBudget));
|
|
131
132
|
if (obj.contextStrategy === 'auto' || obj.contextStrategy === 'off') result.contextStrategy = obj.contextStrategy;
|
|
132
133
|
if (typeof obj.reconnectRetries === 'number') result.reconnectRetries = Math.min(10, Math.max(0, obj.reconnectRetries));
|
|
134
|
+
if (typeof obj.useStreaming === 'boolean') result.useStreaming = obj.useStreaming;
|
|
133
135
|
return Object.keys(result).length > 0 ? result : undefined;
|
|
134
136
|
}
|
|
135
137
|
|
package/app/next-env.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/// <reference types="next" />
|
|
2
2
|
/// <reference types="next/image-types/global" />
|
|
3
|
-
import "./.next/types/routes.d.ts";
|
|
3
|
+
import "./.next/dev/types/routes.d.ts";
|
|
4
4
|
|
|
5
5
|
// NOTE: This file should not be edited
|
|
6
6
|
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
package/package.json
CHANGED