@miphamai/cli 0.19.5 → 0.20.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 +2 -1
- package/src/core/engine.ts +42 -18
- package/src/i18n-context.tsx +37 -0
- package/src/index.tsx +27 -11
- package/src/mcp/client.ts +13 -3
- package/src/mcp/oauth.ts +14 -4
- package/src/mcp/registry.ts +12 -2
- package/src/tools/index.ts +18 -8
- package/src/ui/agent-footer.tsx +3 -1
- package/src/ui/app.tsx +18 -12
- package/src/ui/chat.tsx +19 -15
- package/src/ui/command-picker.tsx +11 -6
- package/src/ui/commands.ts +309 -433
- package/src/ui/input.tsx +53 -101
- package/src/ui/picker.tsx +10 -8
package/src/ui/input.tsx
CHANGED
|
@@ -4,6 +4,7 @@ import TextInput from 'ink-text-input'
|
|
|
4
4
|
import { VimMotionEngine, type VimMode } from './vim-motions.js'
|
|
5
5
|
import { getCommandList } from './commands.js'
|
|
6
6
|
import { CommandPicker } from './command-picker.js'
|
|
7
|
+
import { useI18n } from '../i18n-context'
|
|
7
8
|
|
|
8
9
|
interface InputBarProps {
|
|
9
10
|
onSubmit: (input: string) => void
|
|
@@ -20,99 +21,46 @@ interface InputBarProps {
|
|
|
20
21
|
onCancel?: () => void
|
|
21
22
|
}
|
|
22
23
|
|
|
23
|
-
// ──
|
|
24
|
-
|
|
25
|
-
const
|
|
26
|
-
'
|
|
27
|
-
'
|
|
28
|
-
'
|
|
29
|
-
'
|
|
30
|
-
'
|
|
31
|
-
'
|
|
32
|
-
'
|
|
33
|
-
'
|
|
34
|
-
'
|
|
35
|
-
'
|
|
36
|
-
'
|
|
37
|
-
'
|
|
38
|
-
'
|
|
39
|
-
'
|
|
40
|
-
'
|
|
41
|
-
'
|
|
42
|
-
'
|
|
43
|
-
'
|
|
44
|
-
'
|
|
45
|
-
'
|
|
46
|
-
'
|
|
47
|
-
'
|
|
48
|
-
'
|
|
49
|
-
'
|
|
50
|
-
'
|
|
51
|
-
'
|
|
52
|
-
'Evaporating',
|
|
53
|
-
'Ruminating',
|
|
54
|
-
'Whisking',
|
|
55
|
-
'Scampering',
|
|
56
|
-
'Meandering',
|
|
57
|
-
'Concoting',
|
|
58
|
-
'Gesticulating',
|
|
59
|
-
'Flamebeing',
|
|
60
|
-
'Quantumizing',
|
|
61
|
-
'Waddling',
|
|
62
|
-
'Fluttering',
|
|
63
|
-
'Sprouting',
|
|
64
|
-
'Elucidating',
|
|
65
|
-
'Embellishing',
|
|
66
|
-
'Razzmatizing',
|
|
67
|
-
'Pondering',
|
|
68
|
-
'Cogitating',
|
|
69
|
-
'Garnishing',
|
|
70
|
-
'Actualizing',
|
|
71
|
-
'Zigzagging',
|
|
72
|
-
'Mustering',
|
|
73
|
-
'Frolicking',
|
|
74
|
-
'Hullaballooing',
|
|
75
|
-
'Doing',
|
|
76
|
-
'Fermenting',
|
|
77
|
-
'Considering',
|
|
78
|
-
'Skedaddling',
|
|
79
|
-
'Actioning',
|
|
80
|
-
'Orbiting',
|
|
81
|
-
'Perambulating',
|
|
82
|
-
'Drizzling',
|
|
83
|
-
'Schlepping',
|
|
84
|
-
'Ionizing',
|
|
85
|
-
'Scurrying',
|
|
24
|
+
// ── Loading verb keys (i18n) ──
|
|
25
|
+
|
|
26
|
+
const LOADING_KEYS = [
|
|
27
|
+
'ui.loading.doodling',
|
|
28
|
+
'ui.loading.forging',
|
|
29
|
+
'ui.loading.cerebrating',
|
|
30
|
+
'ui.loading.recombobulating',
|
|
31
|
+
'ui.loading.thinking',
|
|
32
|
+
'ui.loading.computing',
|
|
33
|
+
'ui.loading.processing',
|
|
34
|
+
'ui.loading.analyzing',
|
|
35
|
+
'ui.loading.generating',
|
|
36
|
+
'ui.loading.dreaming',
|
|
37
|
+
'ui.loading.pondering',
|
|
38
|
+
'ui.loading.ruminating',
|
|
39
|
+
'ui.loading.deliberating',
|
|
40
|
+
'ui.loading.contemplating',
|
|
41
|
+
'ui.loading.synthesizing',
|
|
42
|
+
'ui.loading.calculating',
|
|
43
|
+
'ui.loading.inferring',
|
|
44
|
+
'ui.loading.optimizing',
|
|
45
|
+
'ui.loading.compiling',
|
|
46
|
+
'ui.loading.orchestrating',
|
|
47
|
+
'ui.loading.harmonizing',
|
|
48
|
+
'ui.loading.galvanizing',
|
|
49
|
+
'ui.loading.illuminating',
|
|
50
|
+
'ui.loading.manifesting',
|
|
51
|
+
'ui.loading.transmogrifying',
|
|
52
|
+
'ui.loading.actualizing',
|
|
86
53
|
]
|
|
87
54
|
|
|
88
|
-
const
|
|
89
|
-
'
|
|
90
|
-
'
|
|
91
|
-
'
|
|
92
|
-
'
|
|
93
|
-
'
|
|
94
|
-
'
|
|
95
|
-
'解构中',
|
|
96
|
-
'冥思中',
|
|
97
|
-
'编织中',
|
|
98
|
-
'萃取中',
|
|
99
|
-
'烹制中',
|
|
100
|
-
'淬火中',
|
|
101
|
-
'雕琢中',
|
|
102
|
-
'融汇中',
|
|
103
|
-
'觉照中',
|
|
104
|
-
'运转中',
|
|
105
|
-
'参详中',
|
|
106
|
-
'化合中',
|
|
107
|
-
'浸润中',
|
|
108
|
-
'焙烤中',
|
|
55
|
+
const COMPLETED_KEYS = [
|
|
56
|
+
'ui.loading.completed_brewed',
|
|
57
|
+
'ui.loading.completed_churned',
|
|
58
|
+
'ui.loading.completed_cooked',
|
|
59
|
+
'ui.loading.completed_sauteed',
|
|
60
|
+
'ui.loading.completed_cogitated',
|
|
61
|
+
'ui.loading.completed_crunched',
|
|
109
62
|
]
|
|
110
63
|
|
|
111
|
-
// Merge both — bilingual status verbs
|
|
112
|
-
const STATUS_GERUNDS = [...STATUS_EN, ...STATUS_CN]
|
|
113
|
-
|
|
114
|
-
const STATUS_PAST = ['Brewed', 'Churned', 'Cooked', 'Sautéed', 'Cogitated', 'Crunched']
|
|
115
|
-
|
|
116
64
|
function pick<T>(arr: T[]): T {
|
|
117
65
|
return arr[Math.floor(Math.random() * arr.length)]!
|
|
118
66
|
}
|
|
@@ -126,8 +74,9 @@ export function InputBar({
|
|
|
126
74
|
onCyclePermission,
|
|
127
75
|
onCancel,
|
|
128
76
|
}: InputBarProps) {
|
|
77
|
+
const { t } = useI18n()
|
|
129
78
|
const [value, setValue] = useState('')
|
|
130
|
-
const [verb, setVerb] = useState(() => pick(
|
|
79
|
+
const [verb, setVerb] = useState(() => t(pick(LOADING_KEYS)))
|
|
131
80
|
const [completionVerb, setCompletionVerb] = useState<string | null>(null)
|
|
132
81
|
const prevLoading = useRef(isLoading)
|
|
133
82
|
|
|
@@ -144,28 +93,28 @@ export function InputBar({
|
|
|
144
93
|
useEffect(() => {
|
|
145
94
|
if (!isLoading) return
|
|
146
95
|
const interval = setInterval(() => {
|
|
147
|
-
setVerb(pick(
|
|
96
|
+
setVerb(t(pick(LOADING_KEYS)))
|
|
148
97
|
}, 1200)
|
|
149
98
|
return () => clearInterval(interval)
|
|
150
|
-
}, [isLoading])
|
|
99
|
+
}, [isLoading, t])
|
|
151
100
|
|
|
152
101
|
// Pick a fresh gerund when loading starts
|
|
153
102
|
useEffect(() => {
|
|
154
103
|
if (isLoading) {
|
|
155
|
-
setVerb(pick(
|
|
104
|
+
setVerb(t(pick(LOADING_KEYS)))
|
|
156
105
|
setCompletionVerb(null)
|
|
157
106
|
}
|
|
158
|
-
}, [isLoading])
|
|
107
|
+
}, [isLoading, t])
|
|
159
108
|
|
|
160
109
|
// Flash a past participle when loading stops
|
|
161
110
|
useEffect(() => {
|
|
162
111
|
if (prevLoading.current === true && isLoading === false) {
|
|
163
|
-
setCompletionVerb(pick(
|
|
112
|
+
setCompletionVerb(t(pick(COMPLETED_KEYS)))
|
|
164
113
|
const timer = setTimeout(() => setCompletionVerb(null), 1500)
|
|
165
114
|
return () => clearTimeout(timer)
|
|
166
115
|
}
|
|
167
116
|
prevLoading.current = isLoading
|
|
168
|
-
}, [isLoading])
|
|
117
|
+
}, [isLoading, t])
|
|
169
118
|
|
|
170
119
|
const [vimMode, setVimMode] = useState<VimMode>('insert')
|
|
171
120
|
// NOTE: Dual mode state — React state (vimMode) drives UI re-renders (prompt color,
|
|
@@ -376,19 +325,19 @@ export function InputBar({
|
|
|
376
325
|
searchMode
|
|
377
326
|
? `/${searchQuery}`
|
|
378
327
|
: vimMode === 'normal'
|
|
379
|
-
? '
|
|
328
|
+
? t('ui.input.vim_help')
|
|
380
329
|
: isLoading
|
|
381
330
|
? `${verb}...`
|
|
382
331
|
: completionVerb
|
|
383
332
|
? completionVerb
|
|
384
|
-
: '
|
|
333
|
+
: t('ui.input.placeholder')
|
|
385
334
|
}
|
|
386
335
|
/>
|
|
387
336
|
</Box>
|
|
388
337
|
{/* Slash command hints — shown when typing / in INSERT mode (only when picker is NOT active) */}
|
|
389
338
|
{slashHints.length > 0 && vimMode === 'insert' && !pickerActive && (
|
|
390
339
|
<Box marginTop={1} flexDirection="column" gap={1}>
|
|
391
|
-
<Text dimColor>
|
|
340
|
+
<Text dimColor>{t('ui.slash_hints.label')} </Text>
|
|
392
341
|
{slashHints.map((cmd, _i) => (
|
|
393
342
|
<Text key={cmd.name} color="cyan">
|
|
394
343
|
{cmd.name}
|
|
@@ -397,8 +346,11 @@ export function InputBar({
|
|
|
397
346
|
<Text dimColor>
|
|
398
347
|
(
|
|
399
348
|
{slashHints.length === allCommands.length
|
|
400
|
-
? 'all'
|
|
401
|
-
:
|
|
349
|
+
? t('ui.slash_hints.all')
|
|
350
|
+
: t('ui.slash_hints.count', {
|
|
351
|
+
shown: String(slashHints.length),
|
|
352
|
+
total: String(allCommands.length),
|
|
353
|
+
})}
|
|
402
354
|
)
|
|
403
355
|
</Text>
|
|
404
356
|
</Box>
|
package/src/ui/picker.tsx
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import React, { useState, useCallback } from 'react'
|
|
2
2
|
import { Box, Text, useInput } from 'ink'
|
|
3
|
+
import { useI18n } from '../i18n-context'
|
|
3
4
|
import type { MiphamConfig, ProviderConfig } from '../shared/index.ts'
|
|
4
5
|
|
|
5
6
|
interface PickerProps {
|
|
@@ -19,6 +20,7 @@ export function ModelPicker({
|
|
|
19
20
|
onSelect,
|
|
20
21
|
onClose,
|
|
21
22
|
}: PickerProps) {
|
|
23
|
+
const { t } = useI18n()
|
|
22
24
|
// Get active providers only
|
|
23
25
|
const providers = config.providers.filter((p) => p.status !== 'upcoming')
|
|
24
26
|
const activeModels = (p: ProviderConfig) => p.models.filter((m) => m.status === 'active')
|
|
@@ -118,9 +120,9 @@ export function ModelPicker({
|
|
|
118
120
|
{/* Title */}
|
|
119
121
|
<Box marginBottom={1}>
|
|
120
122
|
<Text bold color="cyan">
|
|
121
|
-
|
|
123
|
+
{t('ui.picker.title')}
|
|
122
124
|
</Text>
|
|
123
|
-
<Text dimColor>
|
|
125
|
+
<Text dimColor> {t('ui.picker.nav_help')}</Text>
|
|
124
126
|
</Box>
|
|
125
127
|
|
|
126
128
|
{/* Two-column layout */}
|
|
@@ -134,7 +136,7 @@ export function ModelPicker({
|
|
|
134
136
|
padding={1}
|
|
135
137
|
>
|
|
136
138
|
<Text bold underline dimColor>
|
|
137
|
-
|
|
139
|
+
{t('ui.picker.provider_label')} {activePanel === 'provider' ? '◀' : ''}
|
|
138
140
|
</Text>
|
|
139
141
|
{providers.map((p, i) => {
|
|
140
142
|
const isCurrent = p.id === currentProvider
|
|
@@ -150,7 +152,7 @@ export function ModelPicker({
|
|
|
150
152
|
{isCurrent ? '✓' : ' '}
|
|
151
153
|
{p.name.padEnd(16)}
|
|
152
154
|
</Text>
|
|
153
|
-
<Text dimColor>{isUpcoming ? '
|
|
155
|
+
<Text dimColor>{isUpcoming ? t('ui.picker.coming_soon') : `${activeModels(p).length}m`}</Text>
|
|
154
156
|
</Box>
|
|
155
157
|
)
|
|
156
158
|
})}
|
|
@@ -165,10 +167,10 @@ export function ModelPicker({
|
|
|
165
167
|
padding={1}
|
|
166
168
|
>
|
|
167
169
|
<Text bold underline dimColor>
|
|
168
|
-
|
|
170
|
+
{t('ui.picker.models_label')} {activePanel === 'model' ? '◀' : ''}
|
|
169
171
|
{selectedProvider ? ` — ${selectedProvider.name}` : ''}
|
|
170
172
|
</Text>
|
|
171
|
-
{models.length === 0 && <Text dimColor> (
|
|
173
|
+
{models.length === 0 && <Text dimColor> {t('ui.picker.no_active_models')}</Text>}
|
|
172
174
|
{models.map((m, i) => {
|
|
173
175
|
const isCurrentModel = selectedProvider?.id === currentProvider && m.id === currentModel
|
|
174
176
|
const isSelected = i === modelIdx
|
|
@@ -197,8 +199,8 @@ export function ModelPicker({
|
|
|
197
199
|
<Box marginTop={1}>
|
|
198
200
|
<Text dimColor>
|
|
199
201
|
{activePanel === 'provider'
|
|
200
|
-
? '
|
|
201
|
-
: '
|
|
202
|
+
? t('ui.picker.select_provider_hint')
|
|
203
|
+
: t('ui.picker.select_model_hint')}
|
|
202
204
|
</Text>
|
|
203
205
|
</Box>
|
|
204
206
|
</Box>
|