@miphamai/cli 0.30.0 → 0.30.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@miphamai/cli",
3
- "version": "0.30.0",
3
+ "version": "0.30.1",
4
4
  "description": "Mipham Code — Multi-model open-core intelligent coding terminal by MiphamAI",
5
5
  "keywords": [
6
6
  "ai",
@@ -2,97 +2,77 @@
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
14
  import { DEFAULT_PROVIDERS } 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 = getActiveModels(providerId)
49
+ const lines = [
50
+ '# Mipham Code Configuration',
51
+ `# Generated by Config Wizard — ${new Date().toISOString()}`,
52
+ '',
53
+ 'version: 1',
70
54
  `defaultProvider: ${providerId}`,
71
55
  `defaultModel: ${modelId}`,
72
- `permission: default`,
73
- ``,
74
- `providers:`,
56
+ 'permission: default',
57
+ '',
58
+ 'providers:',
75
59
  ` - id: ${providerId}`,
76
60
  ` name: ${provider?.name || providerId}`,
77
61
  ` protocol: ${provider?.protocol || 'openai-compatible'}`,
78
62
  ...(provider?.baseUrl ? [` baseUrl: ${provider.baseUrl}`] : []),
79
63
  ` apiKey: ${apiKey}`,
80
64
  ` models:`,
65
+ ...models.map((m) => ` - id: ${m.id}`),
81
66
  ]
82
67
 
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')
68
+ writeFileSync(join(configDir, 'config.yml'), lines.join('\n'), 'utf-8')
89
69
  }
90
70
 
91
- /** Check if Ollama is installed and running. */
92
71
  function checkOllama(): { installed: boolean; running: boolean; models: string[] } {
93
72
  try {
94
73
  execSync('ollama --version', { timeout: 5000, stdio: 'ignore' })
95
- const models = execSync('ollama list', { timeout: 5000, encoding: 'utf-8' })
74
+ const out = execSync('ollama list', { timeout: 5000, encoding: 'utf-8' })
75
+ const models = out
96
76
  .split('\n')
97
77
  .slice(1)
98
78
  .filter(Boolean)
@@ -111,166 +91,169 @@ function checkOllama(): { installed: boolean; running: boolean; models: string[]
111
91
  // ── Component ──
112
92
 
113
93
  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
- })
94
+ const [step, setStep] = useState<Step>('welcome')
95
+
96
+ // Selections
97
+ const [mode, setMode] = useState<'cloud' | 'local' | null>(null)
98
+ const [providerId, setProviderId] = useState<string | null>(null)
99
+ const [modelId, setModelId] = useState<string | null>(null)
100
+ const [apiKey, setApiKey] = useState('')
101
+ const [error, setError] = useState<string | null>(null)
124
102
 
125
- const [selectedIdx, setSelectedIdx] = useState(0)
126
- const [inputValue, setInputValue] = useState('')
103
+ // List navigation cursor
104
+ const [cursor, setCursor] = useState(0)
105
+
106
+ // Ollama
107
+ const [ollamaModel, setOllamaModel] = useState('')
127
108
  const [ollamaStatus, setOllamaStatus] = useState<ReturnType<typeof checkOllama> | null>(null)
128
109
 
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
- )
110
+ // ── Derived data ──
150
111
 
151
- const selectProvider = useCallback(
152
- (providerId: string) => {
153
- setState((s) => ({ ...s, providerId }))
154
- goTo('cloud-model')
155
- },
156
- [goTo],
157
- )
112
+ const providerList = CLOUD_PROVIDERS
113
+ const modelList = providerId ? getActiveModels(providerId) : []
158
114
 
159
- const selectModel = useCallback(
160
- (modelId: string) => {
161
- setState((s) => ({ ...s, modelId }))
162
- goTo('cloud-key')
163
- },
164
- [goTo],
165
- )
115
+ // Reset cursor when step changes
116
+ const goStep = (s: Step) => {
117
+ setCursor(0)
118
+ setError(null)
119
+ setStep(s)
120
+ }
121
+
122
+ // ── Completion handlers ──
166
123
 
167
- const confirmApiKey = useCallback(() => {
168
- const key = inputValue.trim()
169
- if (!key) {
170
- setState((s) => ({ ...s, error: '请输入 API Key' }))
124
+ const finishCloud = () => {
125
+ if (!providerId || !modelId || !apiKey.trim()) {
126
+ setError('请填写 API Key')
171
127
  return
172
128
  }
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
129
  try {
181
- writeConfig('ollama', model, 'ollama-local')
182
- onComplete({ providerId: 'ollama', modelId: model, apiKey: 'ollama-local' })
130
+ writeConfigFile(providerId, modelId, apiKey.trim())
131
+ onComplete({ providerId, modelId, apiKey: apiKey.trim() })
183
132
  } catch (err) {
184
- setState((s) => ({ ...s, error: `配置写入失败: ${String(err)}` }))
133
+ setError(`写入配置失败: ${String(err)}`)
185
134
  }
186
- }, [inputValue, state.ollamaModel, onComplete])
135
+ }
187
136
 
188
- const finishCloud = useCallback(() => {
189
- if (!state.providerId || !state.modelId || !state.apiKey) return
137
+ const finishLocal = () => {
138
+ const model = ollamaModel.trim() || 'llama3.2'
190
139
  try {
191
- writeConfig(state.providerId, state.modelId, state.apiKey)
192
- onComplete({ providerId: state.providerId, modelId: state.modelId, apiKey: state.apiKey })
140
+ writeConfigFile('ollama', model, 'ollama-local')
141
+ onComplete({ providerId: 'ollama', modelId: model, apiKey: 'ollama-local' })
193
142
  } catch (err) {
194
- setState((s) => ({ ...s, error: `配置写入失败: ${String(err)}` }))
143
+ setError(`写入配置失败: ${String(err)}`)
195
144
  }
196
- }, [state, onComplete])
145
+ }
197
146
 
198
- // ── Keyboard ──
147
+ // ── Ollama initialization ──
199
148
 
200
- useInput((_input, key) => {
201
- if (key.escape) {
202
- onSkip()
203
- return
149
+ useEffect(() => {
150
+ if (step === 'ollama' && !ollamaStatus) {
151
+ setOllamaStatus(checkOllama())
204
152
  }
153
+ }, [step, ollamaStatus])
205
154
 
206
- if (state.step === 'welcome') {
207
- if (key.return) goTo('mode')
208
- return
209
- }
155
+ // ── Keyboard: list navigation steps (no TextInput) ──
210
156
 
211
- if (state.step === 'mode') {
212
- if (key.upArrow || key.downArrow) {
213
- setSelectedIdx((i) => (i === 0 ? 1 : 0))
157
+ useInput((_input, key) => {
158
+ // Esc always skips
159
+ if (key.escape) {
160
+ if (step === 'welcome') {
161
+ onSkip()
162
+ return
214
163
  }
215
- if (key.return) {
216
- setMode(selectedIdx === 0 ? 'cloud' : 'local')
164
+ // Go back one step
165
+ if (step === 'mode') {
166
+ goStep('welcome')
167
+ return
168
+ }
169
+ if (step === 'provider') {
170
+ goStep('mode')
171
+ return
172
+ }
173
+ if (step === 'model') {
174
+ goStep('provider')
175
+ return
176
+ }
177
+ if (step === 'apikey') {
178
+ goStep('model')
179
+ return
180
+ }
181
+ if (step === 'ollama') {
182
+ goStep('mode')
183
+ return
184
+ }
185
+ if (step === 'confirm') {
186
+ if (mode === 'cloud') {
187
+ goStep('apikey')
188
+ return
189
+ }
190
+ if (mode === 'local') {
191
+ goStep('ollama')
192
+ return
193
+ }
217
194
  }
218
195
  return
219
196
  }
220
197
 
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
198
+ // ── Welcome ──
199
+ if (step === 'welcome') {
200
+ if (key.return) goStep('mode')
201
+ return
202
+ }
231
203
 
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
- }
204
+ // ── Mode selection ──
205
+ if (step === 'mode') {
206
+ if (key.upArrow) setCursor((c) => (c === 0 ? 1 : 0))
207
+ if (key.downArrow) setCursor((c) => (c === 0 ? 1 : 0))
238
208
  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
- }
209
+ if (cursor === 0) {
210
+ // Cloud
211
+ setMode('cloud')
212
+ goStep('provider')
213
+ } else {
214
+ // Local
215
+ setMode('local')
216
+ setOllamaStatus(checkOllama())
217
+ goStep('ollama')
246
218
  }
247
219
  }
248
220
  return
249
221
  }
250
222
 
251
- // API key input: Enter submits
252
- if (state.step === 'cloud-key') {
223
+ // ── Cloud provider selection ──
224
+ if (step === 'provider') {
225
+ if (key.upArrow) setCursor((c) => (c > 0 ? c - 1 : providerList.length - 1))
226
+ if (key.downArrow) setCursor((c) => (c < providerList.length - 1 ? c + 1 : 0))
253
227
  if (key.return) {
254
- confirmApiKey()
228
+ const p = providerList[cursor]
229
+ if (p) {
230
+ setProviderId(p.id)
231
+ goStep('model')
232
+ }
255
233
  }
256
234
  return
257
235
  }
258
236
 
259
- // Local Ollama input
260
- if (state.step === 'local-ollama') {
237
+ // ── Cloud model selection ──
238
+ if (step === 'model') {
239
+ if (modelList.length === 0) return
240
+ if (key.upArrow) setCursor((c) => (c > 0 ? c - 1 : modelList.length - 1))
241
+ if (key.downArrow) setCursor((c) => (c < modelList.length - 1 ? c + 1 : 0))
261
242
  if (key.return) {
262
- finishLocal()
243
+ const m = modelList[cursor]
244
+ if (m) {
245
+ setModelId(m.id)
246
+ goStep('apikey')
247
+ }
263
248
  }
264
249
  return
265
250
  }
266
251
 
267
- // Confirm
268
- if (state.step === 'confirm') {
252
+ // ── Confirm ──
253
+ if (step === 'confirm') {
269
254
  if (key.return) {
270
- finishCloud()
271
- }
272
- if (key.backspace) {
273
- goTo('cloud-key')
255
+ if (mode === 'cloud') finishCloud()
256
+ else finishLocal()
274
257
  }
275
258
  return
276
259
  }
@@ -278,8 +261,7 @@ export function ConfigWizard({ onComplete, onSkip }: Props) {
278
261
 
279
262
  // ── Render ──
280
263
 
281
- const selectedColor = 'cyan'
282
- const dimColor = 'gray'
264
+ const dim = 'gray'
283
265
 
284
266
  return (
285
267
  <Box flexDirection="column" padding={1}>
@@ -290,69 +272,66 @@ export function ConfigWizard({ onComplete, onSkip }: Props) {
290
272
  </Text>
291
273
  <Text dimColor> — 配置向导</Text>
292
274
  </Box>
293
-
294
275
  <Box marginBottom={1}>
295
- <Text dimColor>━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━</Text>
276
+ <Text dimColor>━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━</Text>
296
277
  </Box>
297
278
 
298
279
  {/* ── Welcome ── */}
299
- {state.step === 'welcome' && (
280
+ {step === 'welcome' && (
300
281
  <Box flexDirection="column">
301
282
  <Box marginBottom={1}>
302
283
  <Text bold color="cyan">
303
284
  欢迎使用 Mipham Code!
304
285
  </Text>
305
286
  </Box>
306
- <Box marginBottom={1}>
307
- <Text>在开始之前,需要先配置 AI 模型连接。</Text>
308
- </Box>
309
- <Box marginBottom={1}>
310
- <Text>此向导只需 1-2 分钟,需要您准备好 API Key。</Text>
287
+ <Text>在开始之前,需要先配置 AI 模型连接。</Text>
288
+ <Text>此向导只需 1 分钟,需要您准备好 API Key。</Text>
289
+ <Box marginTop={1}>
290
+ <Text color="yellow">按 Enter 开始 →</Text>
311
291
  </Box>
312
292
  <Box marginTop={1}>
313
- <Text color="yellow">按 Enter 开始配置 →</Text>
293
+ <Text dimColor>Esc 跳过配置</Text>
314
294
  </Box>
315
295
  </Box>
316
296
  )}
317
297
 
318
- {/* ── Mode Selection ── */}
319
- {state.step === 'mode' && (
298
+ {/* ── Mode ── */}
299
+ {step === 'mode' && (
320
300
  <Box flexDirection="column">
321
301
  <Box marginBottom={1}>
322
302
  <Text bold>选择模型连接方式:</Text>
323
303
  </Box>
304
+
324
305
  <Box marginBottom={1}>
325
- <Text color={selectedIdx === 0 ? selectedColor : dimColor}>
326
- {selectedIdx === 0 ? '▶' : ' '} 🌐 云端 AI 模型(推荐)
306
+ <Text color={cursor === 0 ? SELECTED_COLOR : undefined}>
307
+ {cursor === 0 ? '▶' : ' '} 🌐 云端 AI 模型(推荐)
327
308
  </Text>
328
309
  </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)
310
+ <Text dimColor> Claude · DeepSeek · Gemini · 豆包 · Qwen · 混元 · OpenAI</Text>
311
+
312
+ <Box marginTop={2} marginBottom={1}>
313
+ <Text color={cursor === 1 ? SELECTED_COLOR : undefined}>
314
+ {cursor === 1 ? '▶' : ' '} 🖥️ 本地模型(Ollama)
335
315
  </Text>
336
316
  </Box>
337
- <Box marginBottom={1}>
338
- <Text dimColor> 需要提前安装 Ollama 并下载模型</Text>
339
- </Box>
340
- <Box marginTop={1}>
341
- <Text dimColor>↑↓ 选择 · Enter 确认 · Esc 跳过</Text>
317
+ <Text dimColor> 需要提前安装 Ollama 并下载模型</Text>
318
+
319
+ <Box marginTop={2}>
320
+ <Text dimColor>↑↓ 选择 · Enter 确认 · Esc 返回</Text>
342
321
  </Box>
343
322
  </Box>
344
323
  )}
345
324
 
346
- {/* ── Cloud: Provider Selection ── */}
347
- {state.step === 'cloud-provider' && (
325
+ {/* ── Cloud Provider ── */}
326
+ {step === 'provider' && (
348
327
  <Box flexDirection="column">
349
328
  <Box marginBottom={1}>
350
329
  <Text bold>选择 AI 模型提供商:</Text>
351
330
  </Box>
352
- {CLOUD_PROVIDERS.map((p, i) => (
331
+ {providerList.map((p, i) => (
353
332
  <Box key={p.id} marginBottom={1}>
354
- <Text color={i === selectedIdx ? selectedColor : undefined}>
355
- {i === selectedIdx ? '▶' : ' '} {p.name}
333
+ <Text color={i === cursor ? SELECTED_COLOR : undefined}>
334
+ {i === cursor ? '▶' : ' '} {p.name}
356
335
  </Text>
357
336
  <Text dimColor> ({p.models.filter((m) => m.status === 'active').length} 个模型)</Text>
358
337
  </Box>
@@ -363,18 +342,18 @@ export function ConfigWizard({ onComplete, onSkip }: Props) {
363
342
  </Box>
364
343
  )}
365
344
 
366
- {/* ── Cloud: Model Selection ── */}
367
- {state.step === 'cloud-model' && state.providerId && (
345
+ {/* ── Cloud Model ── */}
346
+ {step === 'model' && (
368
347
  <Box flexDirection="column">
369
348
  <Box marginBottom={1}>
370
349
  <Text bold>
371
- 选择模型({DEFAULT_PROVIDERS.find((p) => p.id === state.providerId)?.name}):
350
+ 选择模型({DEFAULT_PROVIDERS.find((p) => p.id === providerId)?.name || providerId}):
372
351
  </Text>
373
352
  </Box>
374
- {getModels(state.providerId).map((m, i) => (
353
+ {modelList.map((m, i) => (
375
354
  <Box key={m.id} marginBottom={1}>
376
- <Text color={i === selectedIdx ? selectedColor : undefined}>
377
- {i === selectedIdx ? '▶' : ' '} {m.name}
355
+ <Text color={i === cursor ? SELECTED_COLOR : undefined}>
356
+ {i === cursor ? '▶' : ' '} {m.name}
378
357
  </Text>
379
358
  <Text dimColor> ({m.id})</Text>
380
359
  </Box>
@@ -385,44 +364,38 @@ export function ConfigWizard({ onComplete, onSkip }: Props) {
385
364
  </Box>
386
365
  )}
387
366
 
388
- {/* ── Cloud: API Key ── */}
389
- {state.step === 'cloud-key' && state.providerId && state.modelId && (
367
+ {/* ── API Key input ── */}
368
+ {step === 'apikey' && (
390
369
  <Box flexDirection="column">
391
370
  <Box marginBottom={1}>
392
371
  <Text bold>粘贴 API Key:</Text>
393
372
  </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 && (
373
+ <Text dimColor>提供商:{DEFAULT_PROVIDERS.find((p) => p.id === providerId)?.name}</Text>
374
+ <Text dimColor>
375
+ 模型:{getActiveModels(providerId!).find((m) => m.id === modelId)?.name}
376
+ </Text>
377
+ {error && (
405
378
  <Box marginBottom={1}>
406
- <Text color="red">⚠ {state.error}</Text>
379
+ <Text color="red">⚠ {error}</Text>
407
380
  </Box>
408
381
  )}
409
- <Box>
382
+ <Box marginTop={1}>
410
383
  <Text color="yellow">🔑 </Text>
411
384
  <TextInput
412
- value={inputValue}
413
- onChange={setInputValue}
414
- onSubmit={confirmApiKey}
385
+ value={apiKey}
386
+ onChange={setApiKey}
387
+ onSubmit={() => goStep('confirm')}
415
388
  placeholder="粘贴 API Key 后按 Enter..."
416
389
  />
417
390
  </Box>
418
391
  <Box marginTop={1}>
419
- <Text dimColor>Enter 确认 · Esc 返回</Text>
392
+ <Text dimColor>Enter 继续 · Esc 返回选模型</Text>
420
393
  </Box>
421
394
  </Box>
422
395
  )}
423
396
 
424
- {/* ── Local: Ollama ── */}
425
- {state.step === 'local-ollama' && (
397
+ {/* ── Ollama ── */}
398
+ {step === 'ollama' && (
426
399
  <Box flexDirection="column">
427
400
  <Box marginBottom={1}>
428
401
  <Text bold>Ollama 本地模型配置:</Text>
@@ -434,84 +407,75 @@ export function ConfigWizard({ onComplete, onSkip }: Props) {
434
407
  {ollamaStatus.running ? ' · 运行中' : ''}
435
408
  </Text>
436
409
  {ollamaStatus.models.length > 0 && (
437
- <Box marginTop={1}>
438
- <Text dimColor>已下载模型:{ollamaStatus.models.join(', ')}</Text>
439
- </Box>
410
+ <Text dimColor>已下载模型:{ollamaStatus.models.join(', ')}</Text>
440
411
  )}
441
412
  </Box>
442
413
  ) : (
443
- <Box marginBottom={1}>
444
- <Text dimColor>正在检测 Ollama...</Text>
445
- </Box>
414
+ <Text dimColor>正在检测 Ollama...</Text>
446
415
  )}
447
- <Box marginBottom={1}>
448
- <Text dimColor>请输入 Ollama 模型名称(如 llama3.2, qwen2.5, deepseek-r1):</Text>
416
+ <Box marginTop={1}>
417
+ <Text dimColor>输入 Ollama 模型名称(如 llama3.2, qwen2.5, deepseek-r1):</Text>
449
418
  </Box>
450
- <Box>
419
+ <Box marginTop={1}>
451
420
  <Text color="yellow">🖥️ </Text>
452
421
  <TextInput
453
- value={inputValue}
454
- onChange={setInputValue}
455
- onSubmit={finishLocal}
456
- placeholder="ollama 模型名,默认 llama3.2"
422
+ value={ollamaModel}
423
+ onChange={setOllamaModel}
424
+ onSubmit={() => goStep('confirm')}
425
+ placeholder="模型名,默认 llama3.2"
457
426
  />
458
427
  </Box>
459
428
  <Box marginTop={1}>
460
- <Text dimColor>Enter 确认 · Esc 返回</Text>
429
+ <Text dimColor>Enter 继续 · Esc 返回</Text>
461
430
  </Box>
462
431
  </Box>
463
432
  )}
464
433
 
465
434
  {/* ── Confirm ── */}
466
- {state.step === 'confirm' && (
435
+ {step === 'confirm' && (
467
436
  <Box flexDirection="column">
468
437
  <Box marginBottom={1}>
469
438
  <Text bold color="cyan">
470
439
  配置确认:
471
440
  </Text>
472
441
  </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}>
483
- <Text>
484
- API Key:{state.apiKey.slice(0, 8)}...{state.apiKey.slice(-4)}
485
- </Text>
486
- </Box>
487
- <Box marginBottom={1}>
442
+ <Text>连接方式:{mode === 'cloud' ? '🌐 云端' : '🖥️ 本地'}</Text>
443
+ {mode === 'cloud' && (
444
+ <>
445
+ <Text>提供商:{DEFAULT_PROVIDERS.find((p) => p.id === providerId)?.name}</Text>
446
+ <Text>模型:{modelId}</Text>
447
+ <Text>
448
+ API Key:{apiKey.slice(0, 8)}...{apiKey.slice(-4)}
449
+ </Text>
450
+ </>
451
+ )}
452
+ {mode === 'local' && <Text>Ollama 模型:{ollamaModel.trim() || 'llama3.2'}</Text>}
453
+ <Box marginTop={1}>
488
454
  <Text dimColor>配置文件将保存到 ~/.mipham/config.yml</Text>
489
455
  </Box>
490
- {state.error && (
456
+ {error && (
491
457
  <Box marginBottom={1}>
492
- <Text color="red">⚠ {state.error}</Text>
458
+ <Text color="red">⚠ {error}</Text>
493
459
  </Box>
494
460
  )}
495
461
  <Box marginTop={1}>
496
- <Text color="yellow">按 Enter 保存并开始 →</Text>
462
+ <Text color="yellow">按 Enter 保存并启动 →</Text>
497
463
  </Box>
498
464
  <Box marginTop={1}>
499
- <Text dimColor>Backspace 返回修改 · Esc 跳过配置</Text>
465
+ <Text dimColor>Esc 返回修改</Text>
500
466
  </Box>
501
467
  </Box>
502
468
  )}
503
469
 
504
470
  {/* ── Done ── */}
505
- {state.step === 'done' && (
471
+ {step === 'done' && (
506
472
  <Box flexDirection="column">
507
473
  <Box marginBottom={1}>
508
474
  <Text color="green" bold>
509
475
  ✅ 配置完成!
510
476
  </Text>
511
477
  </Box>
512
- <Box marginBottom={1}>
513
- <Text>Mipham Code 正在启动...</Text>
514
- </Box>
478
+ <Text>Mipham Code 正在启动...</Text>
515
479
  </Box>
516
480
  )}
517
481
  </Box>