@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.
@@ -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: 'rgba(200,60,60,0.1)', border: '1px solid rgba(200,60,60,0.3)', color: '#c83c3c', fontSize: 12, marginBottom: '1rem' }}>
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
- setTestResult(prev => ({ ...prev, [providerName]: { state: 'ok', latency: json.latency } }));
76
- // Auto-clear after 5s
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
- }, 5000);
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">{t.settings.ai.testKeyOk(result.latency)}</span>
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
- <Input
176
+ <ModelInput
167
177
  value={anthropic.model}
168
- onChange={e => patchProvider('anthropic', { model: e.target.value })}
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
- <Input
200
+ <ModelInput
187
201
  value={openai.model}
188
- onChange={e => patchProvider('openai', { model: e.target.value })}
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() {
@@ -21,6 +21,7 @@ export interface AgentSettings {
21
21
  thinkingBudget?: number;
22
22
  contextStrategy?: 'auto' | 'off';
23
23
  reconnectRetries?: number;
24
+ useStreaming?: boolean;
24
25
  }
25
26
 
26
27
  export interface SettingsData {
@@ -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: 'No response from AI. Please check your API key and provider settings.',
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',
@@ -169,7 +169,7 @@ export const zh = {
169
169
  searching: '正在搜索知识库...',
170
170
  generating: '正在生成回复...',
171
171
  stopped: '已停止生成。',
172
- errorNoResponse: 'AI 未返回响应,请检查 API Key 和服务商设置。',
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 行为',
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geminilight/mindos",
3
- "version": "0.6.17",
3
+ "version": "0.6.19",
4
4
  "description": "MindOS — Human-Agent Collaborative Mind System. Local-first knowledge base that syncs your mind to all AI Agents via MCP.",
5
5
  "keywords": [
6
6
  "mindos",