@miphamai/cli 0.30.0 → 0.31.0

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.
@@ -2,97 +2,123 @@
2
2
  * Interactive first-run Config Wizard
3
3
  *
4
4
  * Replaces manual config.yml editing for non-technical users.
5
- * Flow: Welcome → Local/Cloud → Provider → Model → API Key → Done
5
+ * Flow: Welcome → Mode → Provider → Model → API Key → Confirm → Done
6
6
  *
7
- * Local path: Ollama setup (endpoint + model pull)
7
+ * Local path: Ollama setup
8
8
  * Cloud path: pick provider → pick model → paste API key → auto-configure
9
9
  */
10
10
 
11
- import React, { useState, useCallback } from 'react'
11
+ import React, { useState, useEffect } from 'react'
12
12
  import { Box, Text, useInput } from 'ink'
13
13
  import TextInput from 'ink-text-input'
14
- import { DEFAULT_PROVIDERS } from '../shared/constants'
14
+ import { DEFAULT_PROVIDERS, OLLAMA_PRESET_MODELS } from '../shared/constants'
15
15
  import type { ProviderConfig, ModelInfo } from '../shared/types'
16
- import { mkdirSync, writeFileSync, existsSync } from 'node:fs'
16
+ import { mkdirSync, writeFileSync } from 'node:fs'
17
17
  import { join } from 'node:path'
18
18
  import { homedir } from 'node:os'
19
19
  import { execSync } from 'node:child_process'
20
20
 
21
21
  // ── Types ──
22
22
 
23
- interface WizardState {
24
- step:
25
- | 'welcome'
26
- | 'mode'
27
- | 'cloud-provider'
28
- | 'cloud-model'
29
- | 'cloud-key'
30
- | 'local-ollama'
31
- | 'confirm'
32
- | 'done'
33
- mode: 'local' | 'cloud' | null
34
- providerId: string | null
35
- modelId: string | null
36
- apiKey: string
37
- ollamaEndpoint: string
38
- ollamaModel: string
39
- error: string | null
40
- }
23
+ type Step = 'welcome' | 'mode' | 'provider' | 'model' | 'apikey' | 'ollama' | 'confirm' | 'done'
41
24
 
42
25
  interface Props {
43
26
  onComplete: (config: { providerId: string; modelId: string; apiKey: string }) => void
44
27
  onSkip: () => void
45
28
  }
46
29
 
47
- // ── Helpers ──
30
+ // ── Constants ──
48
31
 
49
- /** Filter to cloud-only providers (exclude ollama/mipham local-only). */
50
32
  const CLOUD_PROVIDERS = DEFAULT_PROVIDERS.filter((p) => p.id !== 'ollama' && p.id !== 'mipham')
51
33
 
52
- /** Get active models for a provider. */
53
- function getModels(providerId: string): ModelInfo[] {
34
+ const SELECTED_COLOR = 'cyan'
35
+
36
+ // ── Helpers ──
37
+
38
+ function getActiveModels(providerId: string): ModelInfo[] {
54
39
  const provider = DEFAULT_PROVIDERS.find((p) => p.id === providerId)
55
40
  return provider?.models.filter((m) => m.status === 'active') || []
56
41
  }
57
42
 
58
- /** Write config.yml to ~/.mipham/ */
59
- function writeConfig(providerId: string, modelId: string, apiKey: string): void {
43
+ function writeConfigFile(providerId: string, modelId: string, apiKey: string): void {
60
44
  const configDir = join(homedir(), '.mipham')
61
45
  mkdirSync(configDir, { recursive: true })
62
46
 
63
47
  const provider = DEFAULT_PROVIDERS.find((p) => p.id === providerId)
64
- const configLines = [
65
- `# Mipham Code Configuration`,
66
- `# Generated by Config Wizard`,
67
- `# ${new Date().toISOString()}`,
68
- ``,
69
- `version: 1`,
48
+ const models =
49
+ providerId === 'ollama' ? getOllamaModelListForConfig() : getActiveModels(providerId)
50
+
51
+ function getOllamaModelListForConfig(): ModelInfo[] {
52
+ // 与 getOllamaModelList 逻辑一致,但用于配置写入
53
+ const seen = new Set<string>()
54
+ const result: ModelInfo[] = []
55
+
56
+ // ollama list
57
+ try {
58
+ const out = execSync('ollama list', { timeout: 5000, encoding: 'utf-8' })
59
+ const lines = out.split('\n').slice(1).filter(Boolean)
60
+ for (const line of lines) {
61
+ const name = line.split(/\s+/)[0]!
62
+ if (!seen.has(name)) {
63
+ seen.add(name)
64
+ result.push({
65
+ id: name,
66
+ name,
67
+ providerId: 'ollama',
68
+ contextWindow: 128_000,
69
+ maxOutput: 32_000,
70
+ vision: false,
71
+ status: 'active',
72
+ })
73
+ }
74
+ }
75
+ } catch {
76
+ /* ollama not available */
77
+ }
78
+
79
+ for (const p of OLLAMA_PRESET_MODELS) {
80
+ if (!seen.has(p.id)) {
81
+ seen.add(p.id)
82
+ result.push({
83
+ id: p.id,
84
+ name: p.id,
85
+ providerId: 'ollama',
86
+ contextWindow: 128_000,
87
+ maxOutput: 32_000,
88
+ vision: false,
89
+ status: 'active',
90
+ })
91
+ }
92
+ }
93
+ return result
94
+ }
95
+ const lines = [
96
+ '# Mipham Code Configuration',
97
+ `# Generated by Config Wizard — ${new Date().toISOString()}`,
98
+ '',
99
+ 'version: 1',
70
100
  `defaultProvider: ${providerId}`,
71
101
  `defaultModel: ${modelId}`,
72
- `permission: default`,
73
- ``,
74
- `providers:`,
102
+ 'permission: default',
103
+ '',
104
+ 'providers:',
75
105
  ` - id: ${providerId}`,
76
106
  ` name: ${provider?.name || providerId}`,
77
107
  ` protocol: ${provider?.protocol || 'openai-compatible'}`,
78
108
  ...(provider?.baseUrl ? [` baseUrl: ${provider.baseUrl}`] : []),
79
109
  ` apiKey: ${apiKey}`,
80
110
  ` models:`,
111
+ ...models.map((m) => ` - id: ${m.id}`),
81
112
  ]
82
113
 
83
- const models = getModels(providerId)
84
- for (const m of models) {
85
- configLines.push(` - id: ${m.id}`)
86
- }
87
-
88
- writeFileSync(join(configDir, 'config.yml'), configLines.join('\n'), 'utf-8')
114
+ writeFileSync(join(configDir, 'config.yml'), lines.join('\n'), 'utf-8')
89
115
  }
90
116
 
91
- /** Check if Ollama is installed and running. */
92
117
  function checkOllama(): { installed: boolean; running: boolean; models: string[] } {
93
118
  try {
94
119
  execSync('ollama --version', { timeout: 5000, stdio: 'ignore' })
95
- const models = execSync('ollama list', { timeout: 5000, encoding: 'utf-8' })
120
+ const out = execSync('ollama list', { timeout: 5000, encoding: 'utf-8' })
121
+ const models = out
96
122
  .split('\n')
97
123
  .slice(1)
98
124
  .filter(Boolean)
@@ -108,169 +134,224 @@ function checkOllama(): { installed: boolean; running: boolean; models: string[]
108
134
  }
109
135
  }
110
136
 
137
+ // ── Ollama model list helpers ──
138
+
139
+ interface OllamaModelItem {
140
+ id: string
141
+ source: 'local' | 'MiphamAI' | '热门'
142
+ }
143
+
144
+ function getOllamaModelList(installedModels: string[]): OllamaModelItem[] {
145
+ const seen = new Set<string>()
146
+ const result: OllamaModelItem[] = []
147
+
148
+ // ollama list 返回的本地模型
149
+ for (const name of installedModels) {
150
+ if (!seen.has(name)) {
151
+ seen.add(name)
152
+ result.push({ id: name, source: 'local' })
153
+ }
154
+ }
155
+
156
+ // 预置模型(去重)
157
+ for (const preset of OLLAMA_PRESET_MODELS) {
158
+ if (!seen.has(preset.id)) {
159
+ seen.add(preset.id)
160
+ result.push({ id: preset.id, source: preset.source as 'MiphamAI' | '热门' })
161
+ }
162
+ }
163
+
164
+ return result
165
+ }
166
+
111
167
  // ── Component ──
112
168
 
113
169
  export function ConfigWizard({ onComplete, onSkip }: Props) {
114
- const [state, setState] = useState<WizardState>({
115
- step: 'welcome',
116
- mode: null,
117
- providerId: null,
118
- modelId: null,
119
- apiKey: '',
120
- ollamaEndpoint: 'http://localhost:11434',
121
- ollamaModel: '',
122
- error: null,
123
- })
170
+ const [step, setStep] = useState<Step>('welcome')
124
171
 
125
- const [selectedIdx, setSelectedIdx] = useState(0)
126
- const [inputValue, setInputValue] = useState('')
172
+ // Selections
173
+ const [mode, setMode] = useState<'cloud' | 'local' | null>(null)
174
+ const [providerId, setProviderId] = useState<string | null>(null)
175
+ const [modelId, setModelId] = useState<string | null>(null)
176
+ const [apiKey, setApiKey] = useState('')
177
+ const [error, setError] = useState<string | null>(null)
178
+
179
+ // List navigation cursor
180
+ const [cursor, setCursor] = useState(0)
181
+
182
+ // Ollama
183
+ const [ollamaModel, _setOllamaModel] = useState('')
127
184
  const [ollamaStatus, setOllamaStatus] = useState<ReturnType<typeof checkOllama> | null>(null)
185
+ const [ollamaCursor, setOllamaCursor] = useState(0)
186
+ const ollamaModelList = ollamaStatus ? getOllamaModelList(ollamaStatus.models) : []
128
187
 
129
- // ── Navigation helpers ──
130
-
131
- const goTo = useCallback((step: WizardState['step']) => {
132
- setSelectedIdx(0)
133
- setInputValue('')
134
- setState((s) => ({ ...s, step, error: null }))
135
- }, [])
136
-
137
- const setMode = useCallback(
138
- (mode: 'local' | 'cloud') => {
139
- setState((s) => ({ ...s, mode }))
140
- if (mode === 'cloud') {
141
- goTo('cloud-provider')
142
- } else {
143
- const status = checkOllama()
144
- setOllamaStatus(status)
145
- goTo('local-ollama')
146
- }
147
- },
148
- [goTo],
149
- )
188
+ // ── Derived data ──
150
189
 
151
- const selectProvider = useCallback(
152
- (providerId: string) => {
153
- setState((s) => ({ ...s, providerId }))
154
- goTo('cloud-model')
155
- },
156
- [goTo],
157
- )
190
+ const providerList = CLOUD_PROVIDERS
191
+ const modelList = providerId ? getActiveModels(providerId) : []
158
192
 
159
- const selectModel = useCallback(
160
- (modelId: string) => {
161
- setState((s) => ({ ...s, modelId }))
162
- goTo('cloud-key')
163
- },
164
- [goTo],
165
- )
193
+ // Reset cursor when step changes
194
+ const goStep = (s: Step) => {
195
+ setCursor(0)
196
+ setOllamaCursor(0)
197
+ setError(null)
198
+ setStep(s)
199
+ }
200
+
201
+ // ── Completion handlers ──
166
202
 
167
- const confirmApiKey = useCallback(() => {
168
- const key = inputValue.trim()
169
- if (!key) {
170
- setState((s) => ({ ...s, error: '请输入 API Key' }))
203
+ const finishCloud = () => {
204
+ if (!providerId || !modelId || !apiKey.trim()) {
205
+ setError('请填写 API Key')
171
206
  return
172
207
  }
173
- setState((s) => ({ ...s, apiKey: key, error: null }))
174
- goTo('confirm')
175
- }, [inputValue, goTo])
176
-
177
- const finishLocal = useCallback(() => {
178
- const endpoint = inputValue.trim() || 'http://localhost:11434'
179
- const model = state.ollamaModel || 'llama3.2'
180
208
  try {
181
- writeConfig('ollama', model, 'ollama-local')
182
- onComplete({ providerId: 'ollama', modelId: model, apiKey: 'ollama-local' })
209
+ writeConfigFile(providerId, modelId, apiKey.trim())
210
+ onComplete({ providerId, modelId, apiKey: apiKey.trim() })
183
211
  } catch (err) {
184
- setState((s) => ({ ...s, error: `配置写入失败: ${String(err)}` }))
212
+ setError(`写入配置失败: ${String(err)}`)
185
213
  }
186
- }, [inputValue, state.ollamaModel, onComplete])
214
+ }
187
215
 
188
- const finishCloud = useCallback(() => {
189
- if (!state.providerId || !state.modelId || !state.apiKey) return
216
+ const finishLocal = () => {
217
+ const selectedModel = ollamaModelList[ollamaCursor]
218
+ const model = selectedModel?.id || ollamaModel.trim() || 'llama3.2'
190
219
  try {
191
- writeConfig(state.providerId, state.modelId, state.apiKey)
192
- onComplete({ providerId: state.providerId, modelId: state.modelId, apiKey: state.apiKey })
220
+ writeConfigFile('ollama', model, 'ollama-local')
221
+ onComplete({ providerId: 'ollama', modelId: model, apiKey: 'ollama-local' })
193
222
  } catch (err) {
194
- setState((s) => ({ ...s, error: `配置写入失败: ${String(err)}` }))
223
+ setError(`写入配置失败: ${String(err)}`)
224
+ }
225
+ }
226
+
227
+ // ── Ollama initialization ──
228
+
229
+ useEffect(() => {
230
+ if (step === 'ollama' && !ollamaStatus) {
231
+ setOllamaStatus(checkOllama())
195
232
  }
196
- }, [state, onComplete])
233
+ }, [step, ollamaStatus])
197
234
 
198
- // ── Keyboard ──
235
+ // ── Keyboard: list navigation steps (no TextInput) ──
199
236
 
200
237
  useInput((_input, key) => {
238
+ // Esc always skips
201
239
  if (key.escape) {
202
- onSkip()
240
+ if (step === 'welcome') {
241
+ onSkip()
242
+ return
243
+ }
244
+ // Go back one step
245
+ if (step === 'mode') {
246
+ goStep('welcome')
247
+ return
248
+ }
249
+ if (step === 'provider') {
250
+ goStep('mode')
251
+ return
252
+ }
253
+ if (step === 'model') {
254
+ goStep('provider')
255
+ return
256
+ }
257
+ if (step === 'apikey') {
258
+ goStep('model')
259
+ return
260
+ }
261
+ if (step === 'ollama') {
262
+ goStep('mode')
263
+ return
264
+ }
265
+ if (step === 'confirm') {
266
+ if (mode === 'cloud') {
267
+ goStep('apikey')
268
+ return
269
+ }
270
+ if (mode === 'local') {
271
+ goStep('ollama')
272
+ return
273
+ }
274
+ }
203
275
  return
204
276
  }
205
277
 
206
- if (state.step === 'welcome') {
207
- if (key.return) goTo('mode')
278
+ // ── Welcome ──
279
+ if (step === 'welcome') {
280
+ if (key.return) goStep('mode')
208
281
  return
209
282
  }
210
283
 
211
- if (state.step === 'mode') {
212
- if (key.upArrow || key.downArrow) {
213
- setSelectedIdx((i) => (i === 0 ? 1 : 0))
214
- }
284
+ // ── Mode selection ──
285
+ if (step === 'mode') {
286
+ if (key.upArrow) setCursor((c) => (c === 0 ? 1 : 0))
287
+ if (key.downArrow) setCursor((c) => (c === 0 ? 1 : 0))
215
288
  if (key.return) {
216
- setMode(selectedIdx === 0 ? 'cloud' : 'local')
289
+ if (cursor === 0) {
290
+ // Cloud
291
+ setMode('cloud')
292
+ goStep('provider')
293
+ } else {
294
+ // Local
295
+ setMode('local')
296
+ setOllamaStatus(checkOllama())
297
+ goStep('ollama')
298
+ }
217
299
  }
218
300
  return
219
301
  }
220
302
 
221
- // Cloud provider / model selection (list navigation)
222
- if (state.step === 'cloud-provider' || state.step === 'cloud-model') {
223
- const items =
224
- state.step === 'cloud-provider'
225
- ? CLOUD_PROVIDERS
226
- : state.providerId
227
- ? getModels(state.providerId)
228
- : []
229
-
230
- if (items.length === 0) return
231
-
232
- if (key.upArrow) {
233
- setSelectedIdx((i) => (i > 0 ? i - 1 : items.length - 1))
234
- }
235
- if (key.downArrow) {
236
- setSelectedIdx((i) => (i < items.length - 1 ? i + 1 : 0))
237
- }
303
+ // ── Cloud provider selection ──
304
+ if (step === 'provider') {
305
+ if (key.upArrow) setCursor((c) => (c > 0 ? c - 1 : providerList.length - 1))
306
+ if (key.downArrow) setCursor((c) => (c < providerList.length - 1 ? c + 1 : 0))
238
307
  if (key.return) {
239
- const item = items[selectedIdx]
240
- if (item) {
241
- if (state.step === 'cloud-provider') {
242
- selectProvider((item as ProviderConfig).id)
243
- } else {
244
- selectModel((item as ModelInfo).id)
245
- }
308
+ const p = providerList[cursor]
309
+ if (p) {
310
+ setProviderId(p.id)
311
+ goStep('model')
246
312
  }
247
313
  }
248
314
  return
249
315
  }
250
316
 
251
- // API key input: Enter submits
252
- if (state.step === 'cloud-key') {
317
+ // ── Cloud model selection ──
318
+ if (step === 'model') {
319
+ if (modelList.length === 0) return
320
+ if (key.upArrow) setCursor((c) => (c > 0 ? c - 1 : modelList.length - 1))
321
+ if (key.downArrow) setCursor((c) => (c < modelList.length - 1 ? c + 1 : 0))
253
322
  if (key.return) {
254
- confirmApiKey()
323
+ const m = modelList[cursor]
324
+ if (m) {
325
+ setModelId(m.id)
326
+ goStep('apikey')
327
+ }
255
328
  }
256
329
  return
257
330
  }
258
331
 
259
- // Local Ollama input
260
- if (state.step === 'local-ollama') {
332
+ // ── Ollama model selection ──
333
+ if (step === 'ollama') {
334
+ if (ollamaModelList.length === 0) return
335
+ if (key.upArrow) {
336
+ setOllamaCursor((c) => (c > 0 ? c - 1 : ollamaModelList.length - 1))
337
+ return
338
+ }
339
+ if (key.downArrow) {
340
+ setOllamaCursor((c) => (c < ollamaModelList.length - 1 ? c + 1 : 0))
341
+ return
342
+ }
261
343
  if (key.return) {
262
- finishLocal()
344
+ goStep('confirm')
345
+ return
263
346
  }
264
347
  return
265
348
  }
266
349
 
267
- // Confirm
268
- if (state.step === 'confirm') {
350
+ // ── Confirm ──
351
+ if (step === 'confirm') {
269
352
  if (key.return) {
270
- finishCloud()
271
- }
272
- if (key.backspace) {
273
- goTo('cloud-key')
353
+ if (mode === 'cloud') finishCloud()
354
+ else finishLocal()
274
355
  }
275
356
  return
276
357
  }
@@ -278,9 +359,6 @@ export function ConfigWizard({ onComplete, onSkip }: Props) {
278
359
 
279
360
  // ── Render ──
280
361
 
281
- const selectedColor = 'cyan'
282
- const dimColor = 'gray'
283
-
284
362
  return (
285
363
  <Box flexDirection="column" padding={1}>
286
364
  {/* Header */}
@@ -290,69 +368,66 @@ export function ConfigWizard({ onComplete, onSkip }: Props) {
290
368
  </Text>
291
369
  <Text dimColor> — 配置向导</Text>
292
370
  </Box>
293
-
294
371
  <Box marginBottom={1}>
295
- <Text dimColor>━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━</Text>
372
+ <Text dimColor>━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━</Text>
296
373
  </Box>
297
374
 
298
375
  {/* ── Welcome ── */}
299
- {state.step === 'welcome' && (
376
+ {step === 'welcome' && (
300
377
  <Box flexDirection="column">
301
378
  <Box marginBottom={1}>
302
379
  <Text bold color="cyan">
303
380
  欢迎使用 Mipham Code!
304
381
  </Text>
305
382
  </Box>
306
- <Box marginBottom={1}>
307
- <Text>在开始之前,需要先配置 AI 模型连接。</Text>
308
- </Box>
309
- <Box marginBottom={1}>
310
- <Text>此向导只需 1-2 分钟,需要您准备好 API Key。</Text>
383
+ <Text>在开始之前,需要先配置 AI 模型连接。</Text>
384
+ <Text>此向导只需 1 分钟,需要您准备好 API Key。</Text>
385
+ <Box marginTop={1}>
386
+ <Text color="yellow">按 Enter 开始 →</Text>
311
387
  </Box>
312
388
  <Box marginTop={1}>
313
- <Text color="yellow">按 Enter 开始配置 →</Text>
389
+ <Text dimColor>Esc 跳过配置</Text>
314
390
  </Box>
315
391
  </Box>
316
392
  )}
317
393
 
318
- {/* ── Mode Selection ── */}
319
- {state.step === 'mode' && (
394
+ {/* ── Mode ── */}
395
+ {step === 'mode' && (
320
396
  <Box flexDirection="column">
321
397
  <Box marginBottom={1}>
322
398
  <Text bold>选择模型连接方式:</Text>
323
399
  </Box>
400
+
324
401
  <Box marginBottom={1}>
325
- <Text color={selectedIdx === 0 ? selectedColor : dimColor}>
326
- {selectedIdx === 0 ? '▶' : ' '} 🌐 云端 AI 模型(推荐)
402
+ <Text color={cursor === 0 ? SELECTED_COLOR : undefined}>
403
+ {cursor === 0 ? '▶' : ' '} 🌐 云端 AI 模型(推荐)
327
404
  </Text>
328
405
  </Box>
329
- <Box marginBottom={1}>
330
- <Text dimColor> Claude · DeepSeek · Gemini · 豆包 · Qwen · 混元 · OpenAI</Text>
331
- </Box>
332
- <Box marginBottom={1}>
333
- <Text color={selectedIdx === 1 ? selectedColor : dimColor}>
334
- {selectedIdx === 1 ? '▶' : ' '} 🖥️ 本地模型(Ollama)
406
+ <Text dimColor> Claude · DeepSeek · Gemini · 豆包 · Qwen · 混元 · OpenAI</Text>
407
+
408
+ <Box marginTop={2} marginBottom={1}>
409
+ <Text color={cursor === 1 ? SELECTED_COLOR : undefined}>
410
+ {cursor === 1 ? '▶' : ' '} 🖥️ 本地模型(Ollama)
335
411
  </Text>
336
412
  </Box>
337
- <Box marginBottom={1}>
338
- <Text dimColor> 需要提前安装 Ollama 并下载模型</Text>
339
- </Box>
340
- <Box marginTop={1}>
341
- <Text dimColor>↑↓ 选择 · Enter 确认 · Esc 跳过</Text>
413
+ <Text dimColor> 需要提前安装 Ollama 并下载模型</Text>
414
+
415
+ <Box marginTop={2}>
416
+ <Text dimColor>↑↓ 选择 · Enter 确认 · Esc 返回</Text>
342
417
  </Box>
343
418
  </Box>
344
419
  )}
345
420
 
346
- {/* ── Cloud: Provider Selection ── */}
347
- {state.step === 'cloud-provider' && (
421
+ {/* ── Cloud Provider ── */}
422
+ {step === 'provider' && (
348
423
  <Box flexDirection="column">
349
424
  <Box marginBottom={1}>
350
425
  <Text bold>选择 AI 模型提供商:</Text>
351
426
  </Box>
352
- {CLOUD_PROVIDERS.map((p, i) => (
427
+ {providerList.map((p, i) => (
353
428
  <Box key={p.id} marginBottom={1}>
354
- <Text color={i === selectedIdx ? selectedColor : undefined}>
355
- {i === selectedIdx ? '▶' : ' '} {p.name}
429
+ <Text color={i === cursor ? SELECTED_COLOR : undefined}>
430
+ {i === cursor ? '▶' : ' '} {p.name}
356
431
  </Text>
357
432
  <Text dimColor> ({p.models.filter((m) => m.status === 'active').length} 个模型)</Text>
358
433
  </Box>
@@ -363,18 +438,18 @@ export function ConfigWizard({ onComplete, onSkip }: Props) {
363
438
  </Box>
364
439
  )}
365
440
 
366
- {/* ── Cloud: Model Selection ── */}
367
- {state.step === 'cloud-model' && state.providerId && (
441
+ {/* ── Cloud Model ── */}
442
+ {step === 'model' && (
368
443
  <Box flexDirection="column">
369
444
  <Box marginBottom={1}>
370
445
  <Text bold>
371
- 选择模型({DEFAULT_PROVIDERS.find((p) => p.id === state.providerId)?.name}):
446
+ 选择模型({DEFAULT_PROVIDERS.find((p) => p.id === providerId)?.name || providerId}):
372
447
  </Text>
373
448
  </Box>
374
- {getModels(state.providerId).map((m, i) => (
449
+ {modelList.map((m, i) => (
375
450
  <Box key={m.id} marginBottom={1}>
376
- <Text color={i === selectedIdx ? selectedColor : undefined}>
377
- {i === selectedIdx ? '▶' : ' '} {m.name}
451
+ <Text color={i === cursor ? SELECTED_COLOR : undefined}>
452
+ {i === cursor ? '▶' : ' '} {m.name}
378
453
  </Text>
379
454
  <Text dimColor> ({m.id})</Text>
380
455
  </Box>
@@ -385,44 +460,38 @@ export function ConfigWizard({ onComplete, onSkip }: Props) {
385
460
  </Box>
386
461
  )}
387
462
 
388
- {/* ── Cloud: API Key ── */}
389
- {state.step === 'cloud-key' && state.providerId && state.modelId && (
463
+ {/* ── API Key input ── */}
464
+ {step === 'apikey' && (
390
465
  <Box flexDirection="column">
391
466
  <Box marginBottom={1}>
392
467
  <Text bold>粘贴 API Key:</Text>
393
468
  </Box>
394
- <Box marginBottom={1}>
395
- <Text dimColor>
396
- 提供商:{DEFAULT_PROVIDERS.find((p) => p.id === state.providerId)?.name}
397
- </Text>
398
- </Box>
399
- <Box marginBottom={1}>
400
- <Text dimColor>
401
- 模型:{getModels(state.providerId).find((m) => m.id === state.modelId)?.name}
402
- </Text>
403
- </Box>
404
- {state.error && (
469
+ <Text dimColor>提供商:{DEFAULT_PROVIDERS.find((p) => p.id === providerId)?.name}</Text>
470
+ <Text dimColor>
471
+ 模型:{getActiveModels(providerId!).find((m) => m.id === modelId)?.name}
472
+ </Text>
473
+ {error && (
405
474
  <Box marginBottom={1}>
406
- <Text color="red">⚠ {state.error}</Text>
475
+ <Text color="red">⚠ {error}</Text>
407
476
  </Box>
408
477
  )}
409
- <Box>
478
+ <Box marginTop={1}>
410
479
  <Text color="yellow">🔑 </Text>
411
480
  <TextInput
412
- value={inputValue}
413
- onChange={setInputValue}
414
- onSubmit={confirmApiKey}
481
+ value={apiKey}
482
+ onChange={setApiKey}
483
+ onSubmit={() => goStep('confirm')}
415
484
  placeholder="粘贴 API Key 后按 Enter..."
416
485
  />
417
486
  </Box>
418
487
  <Box marginTop={1}>
419
- <Text dimColor>Enter 确认 · Esc 返回</Text>
488
+ <Text dimColor>Enter 继续 · Esc 返回选模型</Text>
420
489
  </Box>
421
490
  </Box>
422
491
  )}
423
492
 
424
- {/* ── Local: Ollama ── */}
425
- {state.step === 'local-ollama' && (
493
+ {/* ── Ollama ── */}
494
+ {step === 'ollama' && (
426
495
  <Box flexDirection="column">
427
496
  <Box marginBottom={1}>
428
497
  <Text bold>Ollama 本地模型配置:</Text>
@@ -431,87 +500,96 @@ export function ConfigWizard({ onComplete, onSkip }: Props) {
431
500
  <Box flexDirection="column" marginBottom={1}>
432
501
  <Text>
433
502
  状态:{ollamaStatus.installed ? '✅ 已安装' : '❌ 未安装'}
434
- {ollamaStatus.running ? ' · 运行中' : ''}
503
+ {ollamaStatus.running
504
+ ? ' · 运行中'
505
+ : ollamaStatus.installed
506
+ ? ' · 未运行(请执行 ollama serve)'
507
+ : ''}
435
508
  </Text>
436
- {ollamaStatus.models.length > 0 && (
437
- <Box marginTop={1}>
438
- <Text dimColor>已下载模型:{ollamaStatus.models.join(', ')}</Text>
439
- </Box>
440
- )}
441
509
  </Box>
442
510
  ) : (
511
+ <Text dimColor>正在检测 Ollama...</Text>
512
+ )}
513
+ {ollamaModelList.length > 0 && (
514
+ <Box flexDirection="column" marginBottom={1}>
515
+ <Box marginBottom={1}>
516
+ <Text dimColor>已下载模型(↑↓ 选择 · Enter 确认):</Text>
517
+ </Box>
518
+ {ollamaModelList.map((m, i) => (
519
+ <Box key={m.id}>
520
+ <Text color={i === ollamaCursor ? 'cyan' : undefined}>
521
+ {i === ollamaCursor ? '▶' : ' '} {m.id}
522
+ </Text>
523
+ {m.source !== 'local' && (
524
+ <Text dimColor> [{m.source === 'MiphamAI' ? 'MiphamAI' : '热门'}]</Text>
525
+ )}
526
+ </Box>
527
+ ))}
528
+ </Box>
529
+ )}
530
+ {ollamaStatus && ollamaModelList.length === 0 && (
443
531
  <Box marginBottom={1}>
444
- <Text dimColor>正在检测 Ollama...</Text>
532
+ <Text dimColor>
533
+ 未检测到已下载模型。请先运行 ollama pull &lt;model&gt; 下载模型。
534
+ </Text>
535
+ <Text dimColor>以下为预置模型列表,选择后可在对话中通过 /models 切换。</Text>
445
536
  </Box>
446
537
  )}
447
- <Box marginBottom={1}>
448
- <Text dimColor>请输入 Ollama 模型名称(如 llama3.2, qwen2.5, deepseek-r1):</Text>
449
- </Box>
450
- <Box>
451
- <Text color="yellow">🖥️ </Text>
452
- <TextInput
453
- value={inputValue}
454
- onChange={setInputValue}
455
- onSubmit={finishLocal}
456
- placeholder="ollama 模型名,默认 llama3.2"
457
- />
458
- </Box>
459
538
  <Box marginTop={1}>
460
- <Text dimColor>Enter 确认 · Esc 返回</Text>
539
+ <Text dimColor>↑↓ 选择 · Enter 确认 · Esc 返回</Text>
461
540
  </Box>
462
541
  </Box>
463
542
  )}
464
543
 
465
544
  {/* ── Confirm ── */}
466
- {state.step === 'confirm' && (
545
+ {step === 'confirm' && (
467
546
  <Box flexDirection="column">
468
547
  <Box marginBottom={1}>
469
548
  <Text bold color="cyan">
470
549
  配置确认:
471
550
  </Text>
472
551
  </Box>
473
- <Box marginBottom={1}>
474
- <Text>连接方式:{state.mode === 'cloud' ? '🌐 云端' : '🖥️ 本地'}</Text>
475
- </Box>
476
- <Box marginBottom={1}>
477
- <Text>提供商:{DEFAULT_PROVIDERS.find((p) => p.id === state.providerId)?.name}</Text>
478
- </Box>
479
- <Box marginBottom={1}>
480
- <Text>模型:{state.modelId}</Text>
481
- </Box>
482
- <Box marginBottom={1}>
552
+ <Text>连接方式:{mode === 'cloud' ? '🌐 云端' : '🖥️ 本地'}</Text>
553
+ {mode === 'cloud' && (
554
+ <>
555
+ <Text>提供商:{DEFAULT_PROVIDERS.find((p) => p.id === providerId)?.name}</Text>
556
+ <Text>模型:{modelId}</Text>
557
+ <Text>
558
+ API Key:{apiKey.slice(0, 8)}...{apiKey.slice(-4)}
559
+ </Text>
560
+ </>
561
+ )}
562
+ {mode === 'local' && (
483
563
  <Text>
484
- API Key:{state.apiKey.slice(0, 8)}...{state.apiKey.slice(-4)}
564
+ Ollama 模型:{ollamaModelList[ollamaCursor]?.id || ollamaModel.trim() || 'llama3.2'}
485
565
  </Text>
486
- </Box>
487
- <Box marginBottom={1}>
566
+ )}
567
+ <Box marginTop={1}>
488
568
  <Text dimColor>配置文件将保存到 ~/.mipham/config.yml</Text>
489
569
  </Box>
490
- {state.error && (
570
+ {error && (
491
571
  <Box marginBottom={1}>
492
- <Text color="red">⚠ {state.error}</Text>
572
+ <Text color="red">⚠ {error}</Text>
493
573
  </Box>
494
574
  )}
495
575
  <Box marginTop={1}>
496
- <Text color="yellow">按 Enter 保存并开始 →</Text>
576
+ <Text color="yellow">按 Enter 保存并启动 →</Text>
497
577
  </Box>
498
578
  <Box marginTop={1}>
499
- <Text dimColor>Backspace 返回修改 · Esc 跳过配置</Text>
579
+ <Text dimColor>Esc 返回修改</Text>
500
580
  </Box>
501
581
  </Box>
502
582
  )}
503
583
 
504
584
  {/* ── Done ── */}
505
- {state.step === 'done' && (
585
+ {step === 'done' && (
506
586
  <Box flexDirection="column">
507
587
  <Box marginBottom={1}>
508
588
  <Text color="green" bold>
509
589
  ✅ 配置完成!
510
590
  </Text>
511
591
  </Box>
512
- <Box marginBottom={1}>
513
- <Text>Mipham Code 正在启动...</Text>
514
- </Box>
592
+ <Text>Mipham Code 正在启动...</Text>
515
593
  </Box>
516
594
  )}
517
595
  </Box>