@miphamai/cli 0.29.0 → 0.30.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@miphamai/cli",
3
- "version": "0.29.0",
3
+ "version": "0.30.0",
4
4
  "description": "Mipham Code — Multi-model open-core intelligent coding terminal by MiphamAI",
5
5
  "keywords": [
6
6
  "ai",
package/src/index.tsx CHANGED
@@ -24,7 +24,7 @@ import { ContextManager } from './core/context'
24
24
  import { QueryEngine } from './core/engine'
25
25
  import { ExperienceRuleEngine } from './core/rule-engine.js'
26
26
  import { SessionStore } from './core/session-store'
27
- import type { PermissionLevel } from './shared/types'
27
+ import type { PermissionLevel, MiphamConfig } from './shared/types'
28
28
  import { SkillsLoader } from './skills/loader'
29
29
  import { PluginManager } from './plugin/plugin-manager'
30
30
  import { loadPlugins } from './plugin/plugin-loader'
@@ -42,6 +42,7 @@ import { AgentViewDashboard } from './agent-view/dashboard'
42
42
  import { createT } from './i18n-core/t'
43
43
  import { detectLocale } from './i18n-core/detect'
44
44
  import { I18nProvider } from './i18n-context'
45
+ import { ConfigWizard } from './ui/config-wizard'
45
46
  import enUS from './i18n-core/locales/en-US.json' with { type: 'json' }
46
47
  import zhCN from './i18n-core/locales/zh-CN.json' with { type: 'json' }
47
48
  import type { TranslationMap } from './i18n-core/types'
@@ -111,6 +112,70 @@ async function checkWorkspaceTrust(): Promise<void> {
111
112
  }
112
113
  }
113
114
 
115
+ // ── SetupGate: first-run wizard → App bridge ──
116
+
117
+ interface SetupGateProps {
118
+ needsSetup: boolean
119
+ engine: QueryEngine
120
+ config: MiphamConfig
121
+ defaultProvider: string
122
+ defaultModel: string
123
+ lang?: string
124
+ skillsLoader?: SkillsLoader
125
+ pluginManager?: PluginManager
126
+ version?: string
127
+ sessionId: string
128
+ agentViewManager?: AgentViewManager
129
+ locale: string
130
+ t: (key: string) => string
131
+ }
132
+
133
+ function SetupGate(props: SetupGateProps) {
134
+ const [showWizard, setShowWizard] = React.useState(props.needsSetup)
135
+ const [wizardProvider, setWizardProvider] = React.useState<string | null>(null)
136
+ const [wizardModel, setWizardModel] = React.useState<string | null>(null)
137
+
138
+ // When wizard completes: write config, switch provider, proceed to App
139
+ const handleWizardComplete = React.useCallback(
140
+ (wizardConfig: { providerId: string; modelId: string; apiKey: string }) => {
141
+ // Switch the engine to the newly configured provider
142
+ try {
143
+ props.engine.switchProvider(wizardConfig.providerId, wizardConfig.modelId)
144
+ } catch {
145
+ // If switch fails (e.g. provider not yet in registry), just use as-is
146
+ }
147
+ setWizardProvider(wizardConfig.providerId)
148
+ setWizardModel(wizardConfig.modelId)
149
+ setShowWizard(false)
150
+ },
151
+ [props.engine],
152
+ )
153
+
154
+ const handleWizardSkip = React.useCallback(() => {
155
+ setShowWizard(false)
156
+ }, [])
157
+
158
+ if (showWizard) {
159
+ return React.createElement(ConfigWizard, {
160
+ onComplete: handleWizardComplete,
161
+ onSkip: handleWizardSkip,
162
+ })
163
+ }
164
+
165
+ return React.createElement(App, {
166
+ engine: props.engine,
167
+ config: props.config,
168
+ initialProvider: wizardProvider || props.defaultProvider,
169
+ initialModel: wizardModel || props.defaultModel,
170
+ lang: props.lang,
171
+ skillsLoader: props.skillsLoader,
172
+ pluginManager: props.pluginManager,
173
+ version: props.version,
174
+ sessionId: props.sessionId,
175
+ agentViewManager: props.agentViewManager,
176
+ })
177
+ }
178
+
114
179
  export async function runApp(options: RunOptions): Promise<void> {
115
180
  // Metrics: count CLI invocation
116
181
  getMetrics().cliInvocations.inc()
@@ -481,21 +546,29 @@ export async function runApp(options: RunOptions): Promise<void> {
481
546
  }
482
547
  })
483
548
 
549
+ // ── First-run detection: show interactive ConfigWizard if no config exists ──
550
+ const hasUserConfig = existsSync(join(homedir(), '.mipham', 'config.yml'))
551
+ const hasProjectConfig = existsSync(join(process.cwd(), '.mipham', 'config.yml'))
552
+ const needsSetup = !hasUserConfig && !hasProjectConfig
553
+
484
554
  const { waitUntilExit } = render(
485
555
  React.createElement(I18nProvider, {
486
556
  locale,
487
557
  t,
488
- children: React.createElement(App, {
558
+ children: React.createElement(SetupGate, {
559
+ needsSetup,
489
560
  engine,
490
561
  config,
491
- initialProvider: defaultProvider,
492
- initialModel: defaultModel,
562
+ defaultProvider,
563
+ defaultModel,
493
564
  lang: options.lang,
494
565
  skillsLoader,
495
566
  pluginManager,
496
567
  version: options.version,
497
568
  sessionId: sessionName,
498
569
  agentViewManager,
570
+ locale,
571
+ t,
499
572
  }),
500
573
  }),
501
574
  )
@@ -35,6 +35,33 @@ export const DEFAULT_PROVIDERS: ProviderConfig[] = [
35
35
  vision: true,
36
36
  status: 'active',
37
37
  },
38
+ {
39
+ id: 'claude-fable-5',
40
+ name: 'Claude Fable 5',
41
+ providerId: 'anthropic',
42
+ contextWindow: 1_000_000,
43
+ maxOutput: 128_000,
44
+ vision: true,
45
+ status: 'active',
46
+ },
47
+ {
48
+ id: 'claude-sonnet-5',
49
+ name: 'Claude Sonnet 5',
50
+ providerId: 'anthropic',
51
+ contextWindow: 1_000_000,
52
+ maxOutput: 128_000,
53
+ vision: true,
54
+ status: 'active',
55
+ },
56
+ {
57
+ id: 'claude-opus-5',
58
+ name: 'Claude Opus 5',
59
+ providerId: 'anthropic',
60
+ contextWindow: 1_000_000,
61
+ maxOutput: 128_000,
62
+ vision: true,
63
+ status: 'active',
64
+ },
38
65
  ],
39
66
  },
40
67
  {
@@ -0,0 +1,519 @@
1
+ /**
2
+ * Interactive first-run Config Wizard
3
+ *
4
+ * Replaces manual config.yml editing for non-technical users.
5
+ * Flow: Welcome → Local/Cloud → Provider → Model → API Key → Done
6
+ *
7
+ * Local path: Ollama setup (endpoint + model pull)
8
+ * Cloud path: pick provider → pick model → paste API key → auto-configure
9
+ */
10
+
11
+ import React, { useState, useCallback } from 'react'
12
+ import { Box, Text, useInput } from 'ink'
13
+ import TextInput from 'ink-text-input'
14
+ import { DEFAULT_PROVIDERS } from '../shared/constants'
15
+ import type { ProviderConfig, ModelInfo } from '../shared/types'
16
+ import { mkdirSync, writeFileSync, existsSync } from 'node:fs'
17
+ import { join } from 'node:path'
18
+ import { homedir } from 'node:os'
19
+ import { execSync } from 'node:child_process'
20
+
21
+ // ── Types ──
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
+ }
41
+
42
+ interface Props {
43
+ onComplete: (config: { providerId: string; modelId: string; apiKey: string }) => void
44
+ onSkip: () => void
45
+ }
46
+
47
+ // ── Helpers ──
48
+
49
+ /** Filter to cloud-only providers (exclude ollama/mipham local-only). */
50
+ const CLOUD_PROVIDERS = DEFAULT_PROVIDERS.filter((p) => p.id !== 'ollama' && p.id !== 'mipham')
51
+
52
+ /** Get active models for a provider. */
53
+ function getModels(providerId: string): ModelInfo[] {
54
+ const provider = DEFAULT_PROVIDERS.find((p) => p.id === providerId)
55
+ return provider?.models.filter((m) => m.status === 'active') || []
56
+ }
57
+
58
+ /** Write config.yml to ~/.mipham/ */
59
+ function writeConfig(providerId: string, modelId: string, apiKey: string): void {
60
+ const configDir = join(homedir(), '.mipham')
61
+ mkdirSync(configDir, { recursive: true })
62
+
63
+ 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`,
70
+ `defaultProvider: ${providerId}`,
71
+ `defaultModel: ${modelId}`,
72
+ `permission: default`,
73
+ ``,
74
+ `providers:`,
75
+ ` - id: ${providerId}`,
76
+ ` name: ${provider?.name || providerId}`,
77
+ ` protocol: ${provider?.protocol || 'openai-compatible'}`,
78
+ ...(provider?.baseUrl ? [` baseUrl: ${provider.baseUrl}`] : []),
79
+ ` apiKey: ${apiKey}`,
80
+ ` models:`,
81
+ ]
82
+
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')
89
+ }
90
+
91
+ /** Check if Ollama is installed and running. */
92
+ function checkOllama(): { installed: boolean; running: boolean; models: string[] } {
93
+ try {
94
+ execSync('ollama --version', { timeout: 5000, stdio: 'ignore' })
95
+ const models = execSync('ollama list', { timeout: 5000, encoding: 'utf-8' })
96
+ .split('\n')
97
+ .slice(1)
98
+ .filter(Boolean)
99
+ .map((l) => l.split(/\s+/)[0]!)
100
+ return { installed: true, running: true, models }
101
+ } catch {
102
+ try {
103
+ execSync('ollama --version', { timeout: 5000, stdio: 'ignore' })
104
+ return { installed: true, running: false, models: [] }
105
+ } catch {
106
+ return { installed: false, running: false, models: [] }
107
+ }
108
+ }
109
+ }
110
+
111
+ // ── Component ──
112
+
113
+ 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
+ })
124
+
125
+ const [selectedIdx, setSelectedIdx] = useState(0)
126
+ const [inputValue, setInputValue] = useState('')
127
+ const [ollamaStatus, setOllamaStatus] = useState<ReturnType<typeof checkOllama> | null>(null)
128
+
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
+ )
150
+
151
+ const selectProvider = useCallback(
152
+ (providerId: string) => {
153
+ setState((s) => ({ ...s, providerId }))
154
+ goTo('cloud-model')
155
+ },
156
+ [goTo],
157
+ )
158
+
159
+ const selectModel = useCallback(
160
+ (modelId: string) => {
161
+ setState((s) => ({ ...s, modelId }))
162
+ goTo('cloud-key')
163
+ },
164
+ [goTo],
165
+ )
166
+
167
+ const confirmApiKey = useCallback(() => {
168
+ const key = inputValue.trim()
169
+ if (!key) {
170
+ setState((s) => ({ ...s, error: '请输入 API Key' }))
171
+ return
172
+ }
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
+ try {
181
+ writeConfig('ollama', model, 'ollama-local')
182
+ onComplete({ providerId: 'ollama', modelId: model, apiKey: 'ollama-local' })
183
+ } catch (err) {
184
+ setState((s) => ({ ...s, error: `配置写入失败: ${String(err)}` }))
185
+ }
186
+ }, [inputValue, state.ollamaModel, onComplete])
187
+
188
+ const finishCloud = useCallback(() => {
189
+ if (!state.providerId || !state.modelId || !state.apiKey) return
190
+ try {
191
+ writeConfig(state.providerId, state.modelId, state.apiKey)
192
+ onComplete({ providerId: state.providerId, modelId: state.modelId, apiKey: state.apiKey })
193
+ } catch (err) {
194
+ setState((s) => ({ ...s, error: `配置写入失败: ${String(err)}` }))
195
+ }
196
+ }, [state, onComplete])
197
+
198
+ // ── Keyboard ──
199
+
200
+ useInput((_input, key) => {
201
+ if (key.escape) {
202
+ onSkip()
203
+ return
204
+ }
205
+
206
+ if (state.step === 'welcome') {
207
+ if (key.return) goTo('mode')
208
+ return
209
+ }
210
+
211
+ if (state.step === 'mode') {
212
+ if (key.upArrow || key.downArrow) {
213
+ setSelectedIdx((i) => (i === 0 ? 1 : 0))
214
+ }
215
+ if (key.return) {
216
+ setMode(selectedIdx === 0 ? 'cloud' : 'local')
217
+ }
218
+ return
219
+ }
220
+
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
+ }
238
+ 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
+ }
246
+ }
247
+ }
248
+ return
249
+ }
250
+
251
+ // API key input: Enter submits
252
+ if (state.step === 'cloud-key') {
253
+ if (key.return) {
254
+ confirmApiKey()
255
+ }
256
+ return
257
+ }
258
+
259
+ // Local Ollama input
260
+ if (state.step === 'local-ollama') {
261
+ if (key.return) {
262
+ finishLocal()
263
+ }
264
+ return
265
+ }
266
+
267
+ // Confirm
268
+ if (state.step === 'confirm') {
269
+ if (key.return) {
270
+ finishCloud()
271
+ }
272
+ if (key.backspace) {
273
+ goTo('cloud-key')
274
+ }
275
+ return
276
+ }
277
+ })
278
+
279
+ // ── Render ──
280
+
281
+ const selectedColor = 'cyan'
282
+ const dimColor = 'gray'
283
+
284
+ return (
285
+ <Box flexDirection="column" padding={1}>
286
+ {/* Header */}
287
+ <Box marginBottom={1}>
288
+ <Text color="#FFD700" bold>
289
+ Mipham Code
290
+ </Text>
291
+ <Text dimColor> — 配置向导</Text>
292
+ </Box>
293
+
294
+ <Box marginBottom={1}>
295
+ <Text dimColor>━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━</Text>
296
+ </Box>
297
+
298
+ {/* ── Welcome ── */}
299
+ {state.step === 'welcome' && (
300
+ <Box flexDirection="column">
301
+ <Box marginBottom={1}>
302
+ <Text bold color="cyan">
303
+ 欢迎使用 Mipham Code!
304
+ </Text>
305
+ </Box>
306
+ <Box marginBottom={1}>
307
+ <Text>在开始之前,需要先配置 AI 模型连接。</Text>
308
+ </Box>
309
+ <Box marginBottom={1}>
310
+ <Text>此向导只需 1-2 分钟,需要您准备好 API Key。</Text>
311
+ </Box>
312
+ <Box marginTop={1}>
313
+ <Text color="yellow">按 Enter 开始配置 →</Text>
314
+ </Box>
315
+ </Box>
316
+ )}
317
+
318
+ {/* ── Mode Selection ── */}
319
+ {state.step === 'mode' && (
320
+ <Box flexDirection="column">
321
+ <Box marginBottom={1}>
322
+ <Text bold>选择模型连接方式:</Text>
323
+ </Box>
324
+ <Box marginBottom={1}>
325
+ <Text color={selectedIdx === 0 ? selectedColor : dimColor}>
326
+ {selectedIdx === 0 ? '▶' : ' '} 🌐 云端 AI 模型(推荐)
327
+ </Text>
328
+ </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)
335
+ </Text>
336
+ </Box>
337
+ <Box marginBottom={1}>
338
+ <Text dimColor> 需要提前安装 Ollama 并下载模型</Text>
339
+ </Box>
340
+ <Box marginTop={1}>
341
+ <Text dimColor>↑↓ 选择 · Enter 确认 · Esc 跳过</Text>
342
+ </Box>
343
+ </Box>
344
+ )}
345
+
346
+ {/* ── Cloud: Provider Selection ── */}
347
+ {state.step === 'cloud-provider' && (
348
+ <Box flexDirection="column">
349
+ <Box marginBottom={1}>
350
+ <Text bold>选择 AI 模型提供商:</Text>
351
+ </Box>
352
+ {CLOUD_PROVIDERS.map((p, i) => (
353
+ <Box key={p.id} marginBottom={1}>
354
+ <Text color={i === selectedIdx ? selectedColor : undefined}>
355
+ {i === selectedIdx ? '▶' : ' '} {p.name}
356
+ </Text>
357
+ <Text dimColor> ({p.models.filter((m) => m.status === 'active').length} 个模型)</Text>
358
+ </Box>
359
+ ))}
360
+ <Box marginTop={1}>
361
+ <Text dimColor>↑↓ 选择 · Enter 确认 · Esc 返回</Text>
362
+ </Box>
363
+ </Box>
364
+ )}
365
+
366
+ {/* ── Cloud: Model Selection ── */}
367
+ {state.step === 'cloud-model' && state.providerId && (
368
+ <Box flexDirection="column">
369
+ <Box marginBottom={1}>
370
+ <Text bold>
371
+ 选择模型({DEFAULT_PROVIDERS.find((p) => p.id === state.providerId)?.name}):
372
+ </Text>
373
+ </Box>
374
+ {getModels(state.providerId).map((m, i) => (
375
+ <Box key={m.id} marginBottom={1}>
376
+ <Text color={i === selectedIdx ? selectedColor : undefined}>
377
+ {i === selectedIdx ? '▶' : ' '} {m.name}
378
+ </Text>
379
+ <Text dimColor> ({m.id})</Text>
380
+ </Box>
381
+ ))}
382
+ <Box marginTop={1}>
383
+ <Text dimColor>↑↓ 选择 · Enter 确认 · Esc 返回</Text>
384
+ </Box>
385
+ </Box>
386
+ )}
387
+
388
+ {/* ── Cloud: API Key ── */}
389
+ {state.step === 'cloud-key' && state.providerId && state.modelId && (
390
+ <Box flexDirection="column">
391
+ <Box marginBottom={1}>
392
+ <Text bold>粘贴 API Key:</Text>
393
+ </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 && (
405
+ <Box marginBottom={1}>
406
+ <Text color="red">⚠ {state.error}</Text>
407
+ </Box>
408
+ )}
409
+ <Box>
410
+ <Text color="yellow">🔑 </Text>
411
+ <TextInput
412
+ value={inputValue}
413
+ onChange={setInputValue}
414
+ onSubmit={confirmApiKey}
415
+ placeholder="粘贴 API Key 后按 Enter..."
416
+ />
417
+ </Box>
418
+ <Box marginTop={1}>
419
+ <Text dimColor>Enter 确认 · Esc 返回</Text>
420
+ </Box>
421
+ </Box>
422
+ )}
423
+
424
+ {/* ── Local: Ollama ── */}
425
+ {state.step === 'local-ollama' && (
426
+ <Box flexDirection="column">
427
+ <Box marginBottom={1}>
428
+ <Text bold>Ollama 本地模型配置:</Text>
429
+ </Box>
430
+ {ollamaStatus ? (
431
+ <Box flexDirection="column" marginBottom={1}>
432
+ <Text>
433
+ 状态:{ollamaStatus.installed ? '✅ 已安装' : '❌ 未安装'}
434
+ {ollamaStatus.running ? ' · 运行中' : ''}
435
+ </Text>
436
+ {ollamaStatus.models.length > 0 && (
437
+ <Box marginTop={1}>
438
+ <Text dimColor>已下载模型:{ollamaStatus.models.join(', ')}</Text>
439
+ </Box>
440
+ )}
441
+ </Box>
442
+ ) : (
443
+ <Box marginBottom={1}>
444
+ <Text dimColor>正在检测 Ollama...</Text>
445
+ </Box>
446
+ )}
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
+ <Box marginTop={1}>
460
+ <Text dimColor>Enter 确认 · Esc 返回</Text>
461
+ </Box>
462
+ </Box>
463
+ )}
464
+
465
+ {/* ── Confirm ── */}
466
+ {state.step === 'confirm' && (
467
+ <Box flexDirection="column">
468
+ <Box marginBottom={1}>
469
+ <Text bold color="cyan">
470
+ 配置确认:
471
+ </Text>
472
+ </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}>
488
+ <Text dimColor>配置文件将保存到 ~/.mipham/config.yml</Text>
489
+ </Box>
490
+ {state.error && (
491
+ <Box marginBottom={1}>
492
+ <Text color="red">⚠ {state.error}</Text>
493
+ </Box>
494
+ )}
495
+ <Box marginTop={1}>
496
+ <Text color="yellow">按 Enter 保存并开始 →</Text>
497
+ </Box>
498
+ <Box marginTop={1}>
499
+ <Text dimColor>Backspace 返回修改 · Esc 跳过配置</Text>
500
+ </Box>
501
+ </Box>
502
+ )}
503
+
504
+ {/* ── Done ── */}
505
+ {state.step === 'done' && (
506
+ <Box flexDirection="column">
507
+ <Box marginBottom={1}>
508
+ <Text color="green" bold>
509
+ ✅ 配置完成!
510
+ </Text>
511
+ </Box>
512
+ <Box marginBottom={1}>
513
+ <Text>Mipham Code 正在启动...</Text>
514
+ </Box>
515
+ </Box>
516
+ )}
517
+ </Box>
518
+ )
519
+ }