@miphamai/cli 0.29.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.29.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",
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,483 @@
1
+ /**
2
+ * Interactive first-run Config Wizard
3
+ *
4
+ * Replaces manual config.yml editing for non-technical users.
5
+ * Flow: Welcome → Mode → Provider → Model → API Key → Confirm → Done
6
+ *
7
+ * Local path: Ollama setup
8
+ * Cloud path: pick provider → pick model → paste API key → auto-configure
9
+ */
10
+
11
+ import React, { useState, useEffect } 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 } 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
+ type Step = 'welcome' | 'mode' | 'provider' | 'model' | 'apikey' | 'ollama' | 'confirm' | 'done'
24
+
25
+ interface Props {
26
+ onComplete: (config: { providerId: string; modelId: string; apiKey: string }) => void
27
+ onSkip: () => void
28
+ }
29
+
30
+ // ── Constants ──
31
+
32
+ const CLOUD_PROVIDERS = DEFAULT_PROVIDERS.filter((p) => p.id !== 'ollama' && p.id !== 'mipham')
33
+
34
+ const SELECTED_COLOR = 'cyan'
35
+
36
+ // ── Helpers ──
37
+
38
+ function getActiveModels(providerId: string): ModelInfo[] {
39
+ const provider = DEFAULT_PROVIDERS.find((p) => p.id === providerId)
40
+ return provider?.models.filter((m) => m.status === 'active') || []
41
+ }
42
+
43
+ function writeConfigFile(providerId: string, modelId: string, apiKey: string): void {
44
+ const configDir = join(homedir(), '.mipham')
45
+ mkdirSync(configDir, { recursive: true })
46
+
47
+ const provider = DEFAULT_PROVIDERS.find((p) => p.id === providerId)
48
+ const models = getActiveModels(providerId)
49
+ const lines = [
50
+ '# Mipham Code Configuration',
51
+ `# Generated by Config Wizard — ${new Date().toISOString()}`,
52
+ '',
53
+ 'version: 1',
54
+ `defaultProvider: ${providerId}`,
55
+ `defaultModel: ${modelId}`,
56
+ 'permission: default',
57
+ '',
58
+ 'providers:',
59
+ ` - id: ${providerId}`,
60
+ ` name: ${provider?.name || providerId}`,
61
+ ` protocol: ${provider?.protocol || 'openai-compatible'}`,
62
+ ...(provider?.baseUrl ? [` baseUrl: ${provider.baseUrl}`] : []),
63
+ ` apiKey: ${apiKey}`,
64
+ ` models:`,
65
+ ...models.map((m) => ` - id: ${m.id}`),
66
+ ]
67
+
68
+ writeFileSync(join(configDir, 'config.yml'), lines.join('\n'), 'utf-8')
69
+ }
70
+
71
+ function checkOllama(): { installed: boolean; running: boolean; models: string[] } {
72
+ try {
73
+ execSync('ollama --version', { timeout: 5000, stdio: 'ignore' })
74
+ const out = execSync('ollama list', { timeout: 5000, encoding: 'utf-8' })
75
+ const models = out
76
+ .split('\n')
77
+ .slice(1)
78
+ .filter(Boolean)
79
+ .map((l) => l.split(/\s+/)[0]!)
80
+ return { installed: true, running: true, models }
81
+ } catch {
82
+ try {
83
+ execSync('ollama --version', { timeout: 5000, stdio: 'ignore' })
84
+ return { installed: true, running: false, models: [] }
85
+ } catch {
86
+ return { installed: false, running: false, models: [] }
87
+ }
88
+ }
89
+ }
90
+
91
+ // ── Component ──
92
+
93
+ export function ConfigWizard({ onComplete, onSkip }: Props) {
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)
102
+
103
+ // List navigation cursor
104
+ const [cursor, setCursor] = useState(0)
105
+
106
+ // Ollama
107
+ const [ollamaModel, setOllamaModel] = useState('')
108
+ const [ollamaStatus, setOllamaStatus] = useState<ReturnType<typeof checkOllama> | null>(null)
109
+
110
+ // ── Derived data ──
111
+
112
+ const providerList = CLOUD_PROVIDERS
113
+ const modelList = providerId ? getActiveModels(providerId) : []
114
+
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 ──
123
+
124
+ const finishCloud = () => {
125
+ if (!providerId || !modelId || !apiKey.trim()) {
126
+ setError('请填写 API Key')
127
+ return
128
+ }
129
+ try {
130
+ writeConfigFile(providerId, modelId, apiKey.trim())
131
+ onComplete({ providerId, modelId, apiKey: apiKey.trim() })
132
+ } catch (err) {
133
+ setError(`写入配置失败: ${String(err)}`)
134
+ }
135
+ }
136
+
137
+ const finishLocal = () => {
138
+ const model = ollamaModel.trim() || 'llama3.2'
139
+ try {
140
+ writeConfigFile('ollama', model, 'ollama-local')
141
+ onComplete({ providerId: 'ollama', modelId: model, apiKey: 'ollama-local' })
142
+ } catch (err) {
143
+ setError(`写入配置失败: ${String(err)}`)
144
+ }
145
+ }
146
+
147
+ // ── Ollama initialization ──
148
+
149
+ useEffect(() => {
150
+ if (step === 'ollama' && !ollamaStatus) {
151
+ setOllamaStatus(checkOllama())
152
+ }
153
+ }, [step, ollamaStatus])
154
+
155
+ // ── Keyboard: list navigation steps (no TextInput) ──
156
+
157
+ useInput((_input, key) => {
158
+ // Esc always skips
159
+ if (key.escape) {
160
+ if (step === 'welcome') {
161
+ onSkip()
162
+ return
163
+ }
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
+ }
194
+ }
195
+ return
196
+ }
197
+
198
+ // ── Welcome ──
199
+ if (step === 'welcome') {
200
+ if (key.return) goStep('mode')
201
+ return
202
+ }
203
+
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))
208
+ if (key.return) {
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')
218
+ }
219
+ }
220
+ return
221
+ }
222
+
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))
227
+ if (key.return) {
228
+ const p = providerList[cursor]
229
+ if (p) {
230
+ setProviderId(p.id)
231
+ goStep('model')
232
+ }
233
+ }
234
+ return
235
+ }
236
+
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))
242
+ if (key.return) {
243
+ const m = modelList[cursor]
244
+ if (m) {
245
+ setModelId(m.id)
246
+ goStep('apikey')
247
+ }
248
+ }
249
+ return
250
+ }
251
+
252
+ // ── Confirm ──
253
+ if (step === 'confirm') {
254
+ if (key.return) {
255
+ if (mode === 'cloud') finishCloud()
256
+ else finishLocal()
257
+ }
258
+ return
259
+ }
260
+ })
261
+
262
+ // ── Render ──
263
+
264
+ const dim = 'gray'
265
+
266
+ return (
267
+ <Box flexDirection="column" padding={1}>
268
+ {/* Header */}
269
+ <Box marginBottom={1}>
270
+ <Text color="#FFD700" bold>
271
+ Mipham Code
272
+ </Text>
273
+ <Text dimColor> — 配置向导</Text>
274
+ </Box>
275
+ <Box marginBottom={1}>
276
+ <Text dimColor>━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━</Text>
277
+ </Box>
278
+
279
+ {/* ── Welcome ── */}
280
+ {step === 'welcome' && (
281
+ <Box flexDirection="column">
282
+ <Box marginBottom={1}>
283
+ <Text bold color="cyan">
284
+ 欢迎使用 Mipham Code!
285
+ </Text>
286
+ </Box>
287
+ <Text>在开始之前,需要先配置 AI 模型连接。</Text>
288
+ <Text>此向导只需 1 分钟,需要您准备好 API Key。</Text>
289
+ <Box marginTop={1}>
290
+ <Text color="yellow">按 Enter 开始 →</Text>
291
+ </Box>
292
+ <Box marginTop={1}>
293
+ <Text dimColor>Esc 跳过配置</Text>
294
+ </Box>
295
+ </Box>
296
+ )}
297
+
298
+ {/* ── Mode ── */}
299
+ {step === 'mode' && (
300
+ <Box flexDirection="column">
301
+ <Box marginBottom={1}>
302
+ <Text bold>选择模型连接方式:</Text>
303
+ </Box>
304
+
305
+ <Box marginBottom={1}>
306
+ <Text color={cursor === 0 ? SELECTED_COLOR : undefined}>
307
+ {cursor === 0 ? '▶' : ' '} 🌐 云端 AI 模型(推荐)
308
+ </Text>
309
+ </Box>
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)
315
+ </Text>
316
+ </Box>
317
+ <Text dimColor> 需要提前安装 Ollama 并下载模型</Text>
318
+
319
+ <Box marginTop={2}>
320
+ <Text dimColor>↑↓ 选择 · Enter 确认 · Esc 返回</Text>
321
+ </Box>
322
+ </Box>
323
+ )}
324
+
325
+ {/* ── Cloud Provider ── */}
326
+ {step === 'provider' && (
327
+ <Box flexDirection="column">
328
+ <Box marginBottom={1}>
329
+ <Text bold>选择 AI 模型提供商:</Text>
330
+ </Box>
331
+ {providerList.map((p, i) => (
332
+ <Box key={p.id} marginBottom={1}>
333
+ <Text color={i === cursor ? SELECTED_COLOR : undefined}>
334
+ {i === cursor ? '▶' : ' '} {p.name}
335
+ </Text>
336
+ <Text dimColor> ({p.models.filter((m) => m.status === 'active').length} 个模型)</Text>
337
+ </Box>
338
+ ))}
339
+ <Box marginTop={1}>
340
+ <Text dimColor>↑↓ 选择 · Enter 确认 · Esc 返回</Text>
341
+ </Box>
342
+ </Box>
343
+ )}
344
+
345
+ {/* ── Cloud Model ── */}
346
+ {step === 'model' && (
347
+ <Box flexDirection="column">
348
+ <Box marginBottom={1}>
349
+ <Text bold>
350
+ 选择模型({DEFAULT_PROVIDERS.find((p) => p.id === providerId)?.name || providerId}):
351
+ </Text>
352
+ </Box>
353
+ {modelList.map((m, i) => (
354
+ <Box key={m.id} marginBottom={1}>
355
+ <Text color={i === cursor ? SELECTED_COLOR : undefined}>
356
+ {i === cursor ? '▶' : ' '} {m.name}
357
+ </Text>
358
+ <Text dimColor> ({m.id})</Text>
359
+ </Box>
360
+ ))}
361
+ <Box marginTop={1}>
362
+ <Text dimColor>↑↓ 选择 · Enter 确认 · Esc 返回</Text>
363
+ </Box>
364
+ </Box>
365
+ )}
366
+
367
+ {/* ── API Key input ── */}
368
+ {step === 'apikey' && (
369
+ <Box flexDirection="column">
370
+ <Box marginBottom={1}>
371
+ <Text bold>粘贴 API Key:</Text>
372
+ </Box>
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 && (
378
+ <Box marginBottom={1}>
379
+ <Text color="red">⚠ {error}</Text>
380
+ </Box>
381
+ )}
382
+ <Box marginTop={1}>
383
+ <Text color="yellow">🔑 </Text>
384
+ <TextInput
385
+ value={apiKey}
386
+ onChange={setApiKey}
387
+ onSubmit={() => goStep('confirm')}
388
+ placeholder="粘贴 API Key 后按 Enter..."
389
+ />
390
+ </Box>
391
+ <Box marginTop={1}>
392
+ <Text dimColor>Enter 继续 · Esc 返回选模型</Text>
393
+ </Box>
394
+ </Box>
395
+ )}
396
+
397
+ {/* ── Ollama ── */}
398
+ {step === 'ollama' && (
399
+ <Box flexDirection="column">
400
+ <Box marginBottom={1}>
401
+ <Text bold>Ollama 本地模型配置:</Text>
402
+ </Box>
403
+ {ollamaStatus ? (
404
+ <Box flexDirection="column" marginBottom={1}>
405
+ <Text>
406
+ 状态:{ollamaStatus.installed ? '✅ 已安装' : '❌ 未安装'}
407
+ {ollamaStatus.running ? ' · 运行中' : ''}
408
+ </Text>
409
+ {ollamaStatus.models.length > 0 && (
410
+ <Text dimColor>已下载模型:{ollamaStatus.models.join(', ')}</Text>
411
+ )}
412
+ </Box>
413
+ ) : (
414
+ <Text dimColor>正在检测 Ollama...</Text>
415
+ )}
416
+ <Box marginTop={1}>
417
+ <Text dimColor>输入 Ollama 模型名称(如 llama3.2, qwen2.5, deepseek-r1):</Text>
418
+ </Box>
419
+ <Box marginTop={1}>
420
+ <Text color="yellow">🖥️ </Text>
421
+ <TextInput
422
+ value={ollamaModel}
423
+ onChange={setOllamaModel}
424
+ onSubmit={() => goStep('confirm')}
425
+ placeholder="模型名,默认 llama3.2"
426
+ />
427
+ </Box>
428
+ <Box marginTop={1}>
429
+ <Text dimColor>Enter 继续 · Esc 返回</Text>
430
+ </Box>
431
+ </Box>
432
+ )}
433
+
434
+ {/* ── Confirm ── */}
435
+ {step === 'confirm' && (
436
+ <Box flexDirection="column">
437
+ <Box marginBottom={1}>
438
+ <Text bold color="cyan">
439
+ 配置确认:
440
+ </Text>
441
+ </Box>
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}>
454
+ <Text dimColor>配置文件将保存到 ~/.mipham/config.yml</Text>
455
+ </Box>
456
+ {error && (
457
+ <Box marginBottom={1}>
458
+ <Text color="red">⚠ {error}</Text>
459
+ </Box>
460
+ )}
461
+ <Box marginTop={1}>
462
+ <Text color="yellow">按 Enter 保存并启动 →</Text>
463
+ </Box>
464
+ <Box marginTop={1}>
465
+ <Text dimColor>Esc 返回修改</Text>
466
+ </Box>
467
+ </Box>
468
+ )}
469
+
470
+ {/* ── Done ── */}
471
+ {step === 'done' && (
472
+ <Box flexDirection="column">
473
+ <Box marginBottom={1}>
474
+ <Text color="green" bold>
475
+ ✅ 配置完成!
476
+ </Text>
477
+ </Box>
478
+ <Text>Mipham Code 正在启动...</Text>
479
+ </Box>
480
+ )}
481
+ </Box>
482
+ )
483
+ }