@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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@miphamai/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.20.0",
|
|
4
4
|
"description": "Mipham Code — Multi-model open-core intelligent coding terminal by MiphamAI",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
@@ -55,6 +55,7 @@
|
|
|
55
55
|
"@types/bun": "^1.3.14",
|
|
56
56
|
"@types/node": "^22.19.19",
|
|
57
57
|
"@types/react": "^18.3.29",
|
|
58
|
+
"ink-testing-library": "^4.0.0",
|
|
58
59
|
"vitest": "^4.1.7"
|
|
59
60
|
}
|
|
60
61
|
}
|
package/src/core/engine.ts
CHANGED
|
@@ -18,6 +18,16 @@ import { getBackgroundAgentRegistry } from '../agent/background-registry'
|
|
|
18
18
|
import { RulesLoader } from './rules-loader'
|
|
19
19
|
import { UsageTracker } from './usage-tracker'
|
|
20
20
|
import { buildRequest, sendInferenceCheck, isInferenceHookEnabled } from './inference-hook'
|
|
21
|
+
import { createT } from '@mipham/shared/i18n/t'
|
|
22
|
+
import enUS from '@mipham/shared/i18n/locales/en-US.json'
|
|
23
|
+
import zhCN from '@mipham/shared/i18n/locales/zh-CN.json'
|
|
24
|
+
import type { TranslationMap } from '@mipham/shared/i18n/types'
|
|
25
|
+
|
|
26
|
+
const bundles: Record<string, TranslationMap> = {
|
|
27
|
+
'en-US': enUS as TranslationMap,
|
|
28
|
+
'zh-CN': zhCN as TranslationMap,
|
|
29
|
+
}
|
|
30
|
+
const t = createT(bundles['en-US'] || (enUS as TranslationMap), enUS as TranslationMap)
|
|
21
31
|
|
|
22
32
|
export class QueryEngine {
|
|
23
33
|
private hookEngine?: HookEngine
|
|
@@ -232,7 +242,7 @@ export class QueryEngine {
|
|
|
232
242
|
// Return a minimal summary on failure
|
|
233
243
|
}
|
|
234
244
|
|
|
235
|
-
return summary.slice(0, 8000) || '
|
|
245
|
+
return summary.slice(0, 8000) || t('system.context.omitted')
|
|
236
246
|
})
|
|
237
247
|
}
|
|
238
248
|
|
|
@@ -257,7 +267,7 @@ export class QueryEngine {
|
|
|
257
267
|
if (!submitResult.allowed) {
|
|
258
268
|
yield {
|
|
259
269
|
type: 'error',
|
|
260
|
-
error: submitResult.reason || '
|
|
270
|
+
error: submitResult.reason || t('errors.user_input_blocked'),
|
|
261
271
|
}
|
|
262
272
|
return
|
|
263
273
|
}
|
|
@@ -291,7 +301,7 @@ export class QueryEngine {
|
|
|
291
301
|
if (!verdict.allowed) {
|
|
292
302
|
yield {
|
|
293
303
|
type: 'error',
|
|
294
|
-
error: verdict.reason || '
|
|
304
|
+
error: verdict.reason || t('errors.dlp_blocked'),
|
|
295
305
|
}
|
|
296
306
|
return
|
|
297
307
|
}
|
|
@@ -316,7 +326,10 @@ export class QueryEngine {
|
|
|
316
326
|
yield chunk
|
|
317
327
|
|
|
318
328
|
if (chunk.type === 'error') {
|
|
319
|
-
this.context.addMessage({
|
|
329
|
+
this.context.addMessage({
|
|
330
|
+
role: 'assistant',
|
|
331
|
+
content: t('errors.model_error', { error: chunk.error ?? 'Unknown error' }),
|
|
332
|
+
})
|
|
320
333
|
return
|
|
321
334
|
}
|
|
322
335
|
|
|
@@ -495,17 +508,26 @@ export class QueryEngine {
|
|
|
495
508
|
if (this.goalVerifyScript) {
|
|
496
509
|
const passed = await this.runScriptVerification()
|
|
497
510
|
if (passed) {
|
|
498
|
-
yield {
|
|
511
|
+
yield {
|
|
512
|
+
type: 'text',
|
|
513
|
+
content: t('system.goal.verified', { script: this.goalVerifyScript }),
|
|
514
|
+
}
|
|
499
515
|
break
|
|
500
516
|
}
|
|
501
517
|
// Script failed — continue looping
|
|
502
|
-
yield {
|
|
518
|
+
yield {
|
|
519
|
+
type: 'text',
|
|
520
|
+
content: t('system.goal.failed_continuing', { loop: String(loop) }),
|
|
521
|
+
}
|
|
503
522
|
continue
|
|
504
523
|
}
|
|
505
524
|
|
|
506
525
|
// If using skill verification, delegate to the skill
|
|
507
526
|
if (this.goalVerifySkill) {
|
|
508
|
-
yield {
|
|
527
|
+
yield {
|
|
528
|
+
type: 'text',
|
|
529
|
+
content: t('system.goal.running_skill', { skill: this.goalVerifySkill }),
|
|
530
|
+
}
|
|
509
531
|
yield* this.process(checkMsg, signal)
|
|
510
532
|
if (this.lastAssistantContent?.includes('VERIFIED')) break
|
|
511
533
|
continue
|
|
@@ -519,7 +541,7 @@ export class QueryEngine {
|
|
|
519
541
|
if (loop >= this.maxGoalLoops) {
|
|
520
542
|
yield {
|
|
521
543
|
type: 'text',
|
|
522
|
-
content:
|
|
544
|
+
content: t('system.goal.max_loops', { max: String(this.maxGoalLoops) }),
|
|
523
545
|
}
|
|
524
546
|
}
|
|
525
547
|
}
|
|
@@ -571,7 +593,7 @@ export class QueryEngine {
|
|
|
571
593
|
if (!verdict.allowed) {
|
|
572
594
|
yield {
|
|
573
595
|
type: 'error',
|
|
574
|
-
error: verdict.reason || '
|
|
596
|
+
error: verdict.reason || t('errors.dlp_blocked'),
|
|
575
597
|
}
|
|
576
598
|
return
|
|
577
599
|
}
|
|
@@ -655,10 +677,10 @@ export class QueryEngine {
|
|
|
655
677
|
if (turn === MAX_TURNS - 1 && toolUses.length > 0) {
|
|
656
678
|
this.context.addMessage({
|
|
657
679
|
role: 'user',
|
|
658
|
-
content:
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
680
|
+
content: t('errors.max_tool_turns_warning', {
|
|
681
|
+
max: String(MAX_TURNS),
|
|
682
|
+
pending: String(toolUses.length),
|
|
683
|
+
}),
|
|
662
684
|
})
|
|
663
685
|
// Give model one final chance to respond with a summary
|
|
664
686
|
try {
|
|
@@ -677,7 +699,7 @@ export class QueryEngine {
|
|
|
677
699
|
} catch {
|
|
678
700
|
yield {
|
|
679
701
|
type: 'error',
|
|
680
|
-
error:
|
|
702
|
+
error: t('errors.max_tool_turns', { max: String(MAX_TURNS) }),
|
|
681
703
|
}
|
|
682
704
|
}
|
|
683
705
|
return
|
|
@@ -730,7 +752,7 @@ export class QueryEngine {
|
|
|
730
752
|
return {
|
|
731
753
|
success: false,
|
|
732
754
|
content: '',
|
|
733
|
-
error:
|
|
755
|
+
error: t('errors.tool_not_allowed', { name }),
|
|
734
756
|
}
|
|
735
757
|
}
|
|
736
758
|
|
|
@@ -742,7 +764,7 @@ export class QueryEngine {
|
|
|
742
764
|
return {
|
|
743
765
|
success: false,
|
|
744
766
|
content: '',
|
|
745
|
-
error: preResult.reason ||
|
|
767
|
+
error: preResult.reason || t('errors.tool_blocked', { name }),
|
|
746
768
|
}
|
|
747
769
|
}
|
|
748
770
|
if (preResult.modifiedInput) {
|
|
@@ -803,7 +825,7 @@ export class QueryEngine {
|
|
|
803
825
|
/** Register a tool dynamically (used by MCP auto-registration). */
|
|
804
826
|
registerTool(tool: ToolDefinition): void {
|
|
805
827
|
if (this.tools.has(tool.name)) {
|
|
806
|
-
process.stderr.write(
|
|
828
|
+
process.stderr.write(t('errors.tool_collision', { name: tool.name }) + '\n')
|
|
807
829
|
return
|
|
808
830
|
}
|
|
809
831
|
this.tools.set(tool.name, tool)
|
|
@@ -869,7 +891,9 @@ export class QueryEngine {
|
|
|
869
891
|
// Feed the block reason back to the AI and continue
|
|
870
892
|
this.context.addMessage({
|
|
871
893
|
role: 'user',
|
|
872
|
-
content:
|
|
894
|
+
content: t('system.context.stop_blocked', {
|
|
895
|
+
reason: stopResult.reason || 'Continue working.',
|
|
896
|
+
}),
|
|
873
897
|
})
|
|
874
898
|
yield* this.continueWithTools(signal)
|
|
875
899
|
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import React, { createContext, useContext } from 'react'
|
|
2
|
+
import type { Locale } from '@mipham/shared/i18n/types'
|
|
3
|
+
|
|
4
|
+
export interface I18nContextValue {
|
|
5
|
+
locale: Locale
|
|
6
|
+
t: (key: string, params?: Record<string, string>) => string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const I18nContext = createContext<I18nContextValue | null>(null)
|
|
10
|
+
|
|
11
|
+
export function I18nProvider({
|
|
12
|
+
locale,
|
|
13
|
+
t,
|
|
14
|
+
children,
|
|
15
|
+
}: {
|
|
16
|
+
locale: Locale
|
|
17
|
+
t: (key: string, params?: Record<string, string>) => string
|
|
18
|
+
children: React.ReactNode
|
|
19
|
+
}) {
|
|
20
|
+
return React.createElement(
|
|
21
|
+
I18nContext.Provider,
|
|
22
|
+
{ value: { locale, t } },
|
|
23
|
+
children,
|
|
24
|
+
)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function useI18n(): I18nContextValue {
|
|
28
|
+
const ctx = useContext(I18nContext)
|
|
29
|
+
if (!ctx) {
|
|
30
|
+
// If no Provider is mounted (e.g., tests, bare scripts), return en-US no-op.
|
|
31
|
+
return {
|
|
32
|
+
locale: 'en-US',
|
|
33
|
+
t: (key: string) => key,
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return ctx
|
|
37
|
+
}
|
package/src/index.tsx
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import React from 'react'
|
|
1
2
|
import { join } from 'node:path'
|
|
2
3
|
import { existsSync } from 'node:fs'
|
|
3
4
|
import { render } from 'ink'
|
|
@@ -23,6 +24,12 @@ import { getMetrics } from './core/metrics'
|
|
|
23
24
|
import { ARTIFACTS_DIR, ARTIFACT_PORT, MIPHAM_DIR } from './shared/constants'
|
|
24
25
|
import { AgentViewManager } from './agent-view/agent-view-manager'
|
|
25
26
|
import { AgentViewDashboard } from './agent-view/dashboard'
|
|
27
|
+
import { createT } from '@mipham/shared/i18n/t'
|
|
28
|
+
import { detectLocale } from '@mipham/shared/i18n/detect'
|
|
29
|
+
import { I18nProvider } from './i18n-context'
|
|
30
|
+
import enUS from '@mipham/shared/i18n/locales/en-US.json' with { type: 'json' }
|
|
31
|
+
import zhCN from '@mipham/shared/i18n/locales/zh-CN.json' with { type: 'json' }
|
|
32
|
+
import type { TranslationMap } from '@mipham/shared/i18n/types'
|
|
26
33
|
|
|
27
34
|
interface RunOptions {
|
|
28
35
|
model?: string
|
|
@@ -60,6 +67,11 @@ export async function runApp(options: RunOptions): Promise<void> {
|
|
|
60
67
|
// Load configuration
|
|
61
68
|
const config = loadConfig()
|
|
62
69
|
|
|
70
|
+
// Detect locale and create translation function
|
|
71
|
+
const locale = detectLocale({ lang: options.lang })
|
|
72
|
+
const localeBundles: Record<string, TranslationMap> = { 'en-US': enUS as TranslationMap, 'zh-CN': zhCN as TranslationMap }
|
|
73
|
+
const t = createT(localeBundles[locale] || enUS, enUS)
|
|
74
|
+
|
|
63
75
|
// Bootstrap providers
|
|
64
76
|
const defaultProvider = options.provider || config.defaultProvider
|
|
65
77
|
const defaultModel = options.model || config.defaultModel
|
|
@@ -297,17 +309,21 @@ export async function runApp(options: RunOptions): Promise<void> {
|
|
|
297
309
|
})
|
|
298
310
|
|
|
299
311
|
const { waitUntilExit } = render(
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
312
|
+
React.createElement(I18nProvider, {
|
|
313
|
+
locale,
|
|
314
|
+
t,
|
|
315
|
+
children: React.createElement(App, {
|
|
316
|
+
engine,
|
|
317
|
+
config,
|
|
318
|
+
initialProvider: defaultProvider,
|
|
319
|
+
initialModel: defaultModel,
|
|
320
|
+
lang: options.lang,
|
|
321
|
+
skillsLoader,
|
|
322
|
+
pluginManager,
|
|
323
|
+
version: options.version,
|
|
324
|
+
sessionId: sessionName,
|
|
325
|
+
}),
|
|
326
|
+
}),
|
|
311
327
|
)
|
|
312
328
|
await waitUntilExit()
|
|
313
329
|
saveAndExit()
|
package/src/mcp/client.ts
CHANGED
|
@@ -10,6 +10,16 @@ import { StdioTransport } from './transport'
|
|
|
10
10
|
import { McpProtocol } from './protocol'
|
|
11
11
|
import { OAuthClient } from './oauth'
|
|
12
12
|
import { TokenStore } from './token-store'
|
|
13
|
+
import { createT } from '@mipham/shared/i18n/t'
|
|
14
|
+
import enUS from '@mipham/shared/i18n/locales/en-US.json'
|
|
15
|
+
import zhCN from '@mipham/shared/i18n/locales/zh-CN.json'
|
|
16
|
+
import type { TranslationMap } from '@mipham/shared/i18n/types'
|
|
17
|
+
|
|
18
|
+
const bundles: Record<string, TranslationMap> = {
|
|
19
|
+
'en-US': enUS as TranslationMap,
|
|
20
|
+
'zh-CN': zhCN as TranslationMap,
|
|
21
|
+
}
|
|
22
|
+
const t = createT(bundles['en-US'] || (enUS as TranslationMap), enUS as TranslationMap)
|
|
13
23
|
|
|
14
24
|
interface ActiveConnection {
|
|
15
25
|
config: McpServerConfig
|
|
@@ -98,7 +108,7 @@ export class McpClient {
|
|
|
98
108
|
/** Reconnect with exponential backoff (1s→2s→4s→…max 60s, 10 attempts). */
|
|
99
109
|
async reconnect(name: string): Promise<void> {
|
|
100
110
|
const connection = this.connections.get(name)
|
|
101
|
-
if (!connection) throw new Error(
|
|
111
|
+
if (!connection) throw new Error(t('errors.mcp_no_connection', { name }))
|
|
102
112
|
|
|
103
113
|
const config = connection.config
|
|
104
114
|
let delay = 1000
|
|
@@ -262,7 +272,7 @@ export class McpClient {
|
|
|
262
272
|
const conn = this.connections.get(serverName)
|
|
263
273
|
if (!conn || conn.status !== 'connected') {
|
|
264
274
|
return {
|
|
265
|
-
content: [{ type: 'text', text:
|
|
275
|
+
content: [{ type: 'text', text: t('errors.mcp_not_connected', { server: serverName }) }],
|
|
266
276
|
isError: true,
|
|
267
277
|
}
|
|
268
278
|
}
|
|
@@ -271,7 +281,7 @@ export class McpClient {
|
|
|
271
281
|
return await conn.protocol.callTool(toolName, params)
|
|
272
282
|
} catch (err) {
|
|
273
283
|
return {
|
|
274
|
-
content: [{ type: 'text', text:
|
|
284
|
+
content: [{ type: 'text', text: t('errors.mcp_tool_error', { error: String(err) }) }],
|
|
275
285
|
isError: true,
|
|
276
286
|
}
|
|
277
287
|
}
|
package/src/mcp/oauth.ts
CHANGED
|
@@ -4,6 +4,16 @@ import { createServer, Server } from 'node:http'
|
|
|
4
4
|
import type { IncomingMessage, ServerResponse } from 'node:http'
|
|
5
5
|
import type { McpServerConfig } from '../shared/types'
|
|
6
6
|
import { TokenStore } from './token-store'
|
|
7
|
+
import { createT } from '@mipham/shared/i18n/t'
|
|
8
|
+
import enUS from '@mipham/shared/i18n/locales/en-US.json'
|
|
9
|
+
import zhCN from '@mipham/shared/i18n/locales/zh-CN.json'
|
|
10
|
+
import type { TranslationMap } from '@mipham/shared/i18n/types'
|
|
11
|
+
|
|
12
|
+
const bundles: Record<string, TranslationMap> = {
|
|
13
|
+
'en-US': enUS as TranslationMap,
|
|
14
|
+
'zh-CN': zhCN as TranslationMap,
|
|
15
|
+
}
|
|
16
|
+
const t = createT(bundles['en-US'] || (enUS as TranslationMap), enUS as TranslationMap)
|
|
7
17
|
|
|
8
18
|
interface TokenResponse {
|
|
9
19
|
accessToken: string
|
|
@@ -41,18 +51,18 @@ export class OAuthClient {
|
|
|
41
51
|
if (receivedState !== state) {
|
|
42
52
|
res.writeHead(400)
|
|
43
53
|
res.end('State mismatch')
|
|
44
|
-
reject(new Error('
|
|
54
|
+
reject(new Error(t('errors.oauth_state_mismatch')))
|
|
45
55
|
return
|
|
46
56
|
}
|
|
47
57
|
if (!receivedCode) {
|
|
48
58
|
res.writeHead(400)
|
|
49
59
|
res.end('No code received')
|
|
50
|
-
reject(new Error('
|
|
60
|
+
reject(new Error(t('errors.oauth_no_code')))
|
|
51
61
|
return
|
|
52
62
|
}
|
|
53
63
|
res.writeHead(200, { 'Content-Type': 'text/html' })
|
|
54
64
|
res.end(
|
|
55
|
-
|
|
65
|
+
`<html><body><h1>${t('system.oauth.authenticated')}</h1><p>${t('system.oauth.close_window')}</p></body></html>`,
|
|
56
66
|
)
|
|
57
67
|
server.close()
|
|
58
68
|
resolve(receivedCode)
|
|
@@ -81,7 +91,7 @@ export class OAuthClient {
|
|
|
81
91
|
})
|
|
82
92
|
setTimeout(() => {
|
|
83
93
|
server.close()
|
|
84
|
-
reject(new Error(
|
|
94
|
+
reject(new Error(t('errors.oauth_timeout')))
|
|
85
95
|
}, 300_000)
|
|
86
96
|
})
|
|
87
97
|
|
package/src/mcp/registry.ts
CHANGED
|
@@ -9,6 +9,16 @@
|
|
|
9
9
|
import type { ToolDefinition as McpToolDefinition, ToolCallResult } from './types'
|
|
10
10
|
import type { ToolDefinition, ToolResult, ToolContext } from '../shared/types'
|
|
11
11
|
import { McpClient } from './client'
|
|
12
|
+
import { createT } from '@mipham/shared/i18n/t'
|
|
13
|
+
import enUS from '@mipham/shared/i18n/locales/en-US.json'
|
|
14
|
+
import zhCN from '@mipham/shared/i18n/locales/zh-CN.json'
|
|
15
|
+
import type { TranslationMap } from '@mipham/shared/i18n/types'
|
|
16
|
+
|
|
17
|
+
const bundles: Record<string, TranslationMap> = {
|
|
18
|
+
'en-US': enUS as TranslationMap,
|
|
19
|
+
'zh-CN': zhCN as TranslationMap,
|
|
20
|
+
}
|
|
21
|
+
const t = createT(bundles['en-US'] || (enUS as TranslationMap), enUS as TranslationMap)
|
|
12
22
|
|
|
13
23
|
const MCP_TOOL_PREFIX = 'mcp__'
|
|
14
24
|
const MAX_NAME_LENGTH = 128
|
|
@@ -108,7 +118,7 @@ export function registerMcpServerTools(
|
|
|
108
118
|
|
|
109
119
|
if (toolsMap.has(tool.name)) {
|
|
110
120
|
process.stderr.write(
|
|
111
|
-
|
|
121
|
+
t('errors.mcp_register_collision', { name: tool.name, server: serverName }) + '\n',
|
|
112
122
|
)
|
|
113
123
|
continue
|
|
114
124
|
}
|
|
@@ -117,7 +127,7 @@ export function registerMcpServerTools(
|
|
|
117
127
|
registered++
|
|
118
128
|
} catch (err) {
|
|
119
129
|
process.stderr.write(
|
|
120
|
-
|
|
130
|
+
t('errors.mcp_register_failed', { tool: mcpTool.name, server: serverName, error: String(err) }) + '\n',
|
|
121
131
|
)
|
|
122
132
|
}
|
|
123
133
|
}
|
package/src/tools/index.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import type { ToolDefinition, ToolResult } from '../shared/index.ts'
|
|
2
2
|
import { sanitizeParams } from '../shared/sanitize'
|
|
3
|
+
import { createT } from '@mipham/shared/i18n/t'
|
|
4
|
+
import enUS from '@mipham/shared/i18n/locales/en-US.json'
|
|
5
|
+
import zhCN from '@mipham/shared/i18n/locales/zh-CN.json'
|
|
6
|
+
import type { TranslationMap } from '@mipham/shared/i18n/types'
|
|
3
7
|
import { readTool } from './file/read'
|
|
4
8
|
import { writeTool } from './file/write'
|
|
5
9
|
import { editTool } from './file/edit'
|
|
@@ -29,6 +33,12 @@ import { computerUseTool } from './computer/computer-use'
|
|
|
29
33
|
import { scheduleWakeupTool } from './scheduling/schedule-wakeup.js'
|
|
30
34
|
import { cronCreateTool, cronDeleteTool, cronListTool } from './scheduling/cron.js'
|
|
31
35
|
|
|
36
|
+
const bundles: Record<string, TranslationMap> = {
|
|
37
|
+
'en-US': enUS as TranslationMap,
|
|
38
|
+
'zh-CN': zhCN as TranslationMap,
|
|
39
|
+
}
|
|
40
|
+
const t = createT(bundles['en-US'] || (enUS as TranslationMap), enUS as TranslationMap)
|
|
41
|
+
|
|
32
42
|
/**
|
|
33
43
|
* Validate tool parameters against the tool's JSON Schema definition.
|
|
34
44
|
* Returns an array of error messages (empty = valid).
|
|
@@ -44,7 +54,7 @@ function validateParams(
|
|
|
44
54
|
if (required) {
|
|
45
55
|
for (const field of required) {
|
|
46
56
|
if (params[field] === undefined || params[field] === null) {
|
|
47
|
-
errors.push(
|
|
57
|
+
errors.push(t('errors.missing_param', { param: field }))
|
|
48
58
|
}
|
|
49
59
|
}
|
|
50
60
|
}
|
|
@@ -59,25 +69,25 @@ function validateParams(
|
|
|
59
69
|
|
|
60
70
|
switch (def.type) {
|
|
61
71
|
case 'string':
|
|
62
|
-
if (typeof value !== 'string') errors.push(
|
|
72
|
+
if (typeof value !== 'string') errors.push(t('errors.type_string', { key }))
|
|
63
73
|
else if (def.enum && !def.enum.includes(value)) {
|
|
64
|
-
errors.push(
|
|
74
|
+
errors.push(t('errors.type_enum', { key, values: def.enum.join(', ') }))
|
|
65
75
|
}
|
|
66
76
|
break
|
|
67
77
|
case 'integer':
|
|
68
78
|
case 'number':
|
|
69
|
-
if (typeof value !== 'number') errors.push(
|
|
79
|
+
if (typeof value !== 'number') errors.push(t('errors.type_number', { key }))
|
|
70
80
|
break
|
|
71
81
|
case 'boolean':
|
|
72
|
-
if (typeof value !== 'boolean') errors.push(
|
|
82
|
+
if (typeof value !== 'boolean') errors.push(t('errors.type_boolean', { key }))
|
|
73
83
|
break
|
|
74
84
|
case 'object':
|
|
75
85
|
if (typeof value !== 'object' || Array.isArray(value)) {
|
|
76
|
-
errors.push(
|
|
86
|
+
errors.push(t('errors.type_object', { key }))
|
|
77
87
|
}
|
|
78
88
|
break
|
|
79
89
|
case 'array':
|
|
80
|
-
if (!Array.isArray(value)) errors.push(
|
|
90
|
+
if (!Array.isArray(value)) errors.push(t('errors.type_array', { key }))
|
|
81
91
|
break
|
|
82
92
|
}
|
|
83
93
|
}
|
|
@@ -100,7 +110,7 @@ function withValidation(tool: ToolDefinition): ToolDefinition {
|
|
|
100
110
|
const cleanParams = sanitizeParams(params)
|
|
101
111
|
const errors = validateParams(schema, cleanParams)
|
|
102
112
|
if (errors.length > 0) {
|
|
103
|
-
return { success: false, content: '', error:
|
|
113
|
+
return { success: false, content: '', error: t('errors.invalid_params', { errors: errors.join('; ') }) }
|
|
104
114
|
}
|
|
105
115
|
return tool.execute(cleanParams, ctx)
|
|
106
116
|
},
|
package/src/ui/agent-footer.tsx
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
2
|
import { Box, Text } from 'ink'
|
|
3
|
+
import { useI18n } from '../i18n-context'
|
|
3
4
|
|
|
4
5
|
export interface AgentEntry {
|
|
5
6
|
id: string
|
|
@@ -33,6 +34,7 @@ function formatTokens(n: number): string {
|
|
|
33
34
|
}
|
|
34
35
|
|
|
35
36
|
export function AgentFooter({ agents, gitBranch, tick: _tick }: AgentFooterProps) {
|
|
37
|
+
const { t } = useI18n()
|
|
36
38
|
const hasAgents = agents.length > 0
|
|
37
39
|
const hasBranch = gitBranch.length > 0
|
|
38
40
|
|
|
@@ -73,7 +75,7 @@ export function AgentFooter({ agents, gitBranch, tick: _tick }: AgentFooterProps
|
|
|
73
75
|
{/* Spacer pushes stats to the right — Ink handles this naturally */}
|
|
74
76
|
<Text dimColor>
|
|
75
77
|
{' '}
|
|
76
|
-
{isRunning ? formatElapsed(elapsed) : 'finished'}
|
|
78
|
+
{isRunning ? formatElapsed(elapsed) : t('ui.agent.finished')}
|
|
77
79
|
</Text>
|
|
78
80
|
{isRunning && agent.tokensUsed > 0 && (
|
|
79
81
|
<Text dimColor>
|
package/src/ui/app.tsx
CHANGED
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
handleSwitch,
|
|
20
20
|
type CommandContext,
|
|
21
21
|
} from './commands'
|
|
22
|
+
import { useI18n } from '../i18n-context'
|
|
22
23
|
import type { PermissionMode } from '../shared/index.ts'
|
|
23
24
|
|
|
24
25
|
interface AppProps {
|
|
@@ -72,14 +73,6 @@ const PERMISSION_MODES: PermissionMode[] = [
|
|
|
72
73
|
// Labels aligned with Claude Code terminology: describe behavior, not capability.
|
|
73
74
|
// Claude Code modes: manual mode → accept edits on → bypass
|
|
74
75
|
// Mipham extends with 3 extra modes (plan, auto, dontAsk) for finer control.
|
|
75
|
-
const PERMISSION_LABELS: Record<PermissionMode, string> = {
|
|
76
|
-
default: 'manual mode',
|
|
77
|
-
acceptEdits: 'accept edits on',
|
|
78
|
-
plan: 'plan mode · read-only',
|
|
79
|
-
auto: 'auto mode',
|
|
80
|
-
dontAsk: "don't ask",
|
|
81
|
-
bypassPermissions: 'bypass',
|
|
82
|
-
}
|
|
83
76
|
const PERMISSION_COLORS: Record<PermissionMode, string> = {
|
|
84
77
|
default: 'white',
|
|
85
78
|
acceptEdits: 'blue',
|
|
@@ -128,6 +121,18 @@ export function App({
|
|
|
128
121
|
version,
|
|
129
122
|
sessionId,
|
|
130
123
|
}: AppProps) {
|
|
124
|
+
const { t } = useI18n()
|
|
125
|
+
const PERMISSION_LABELS = useMemo<Record<PermissionMode, string>>(
|
|
126
|
+
() => ({
|
|
127
|
+
default: t('ui.permission.manual'),
|
|
128
|
+
acceptEdits: t('ui.permission.accept_edits'),
|
|
129
|
+
plan: t('ui.permission.plan_mode'),
|
|
130
|
+
auto: t('ui.permission.auto'),
|
|
131
|
+
dontAsk: t('ui.permission.dont_ask'),
|
|
132
|
+
bypassPermissions: t('ui.permission.bypass'),
|
|
133
|
+
}),
|
|
134
|
+
[t],
|
|
135
|
+
)
|
|
131
136
|
const [messages, setMessages] = useState<ChatMessage[]>([])
|
|
132
137
|
const [isLoading, setIsLoading] = useState(false)
|
|
133
138
|
const [providerId, setProviderId] = useState(initialProvider || config.defaultProvider)
|
|
@@ -244,8 +249,9 @@ export function App({
|
|
|
244
249
|
setUltracodeMode: (on: boolean) => setUltracodeMode(on),
|
|
245
250
|
skillsLoader,
|
|
246
251
|
pluginManager,
|
|
252
|
+
t,
|
|
247
253
|
}),
|
|
248
|
-
[engine, config, providerId, modelId, skillsLoader, pluginManager, sessionId],
|
|
254
|
+
[engine, config, providerId, modelId, skillsLoader, pluginManager, sessionId, t],
|
|
249
255
|
)
|
|
250
256
|
|
|
251
257
|
const handleSubmit = useCallback(
|
|
@@ -612,10 +618,10 @@ export function App({
|
|
|
612
618
|
)}
|
|
613
619
|
<Box flexDirection="row">
|
|
614
620
|
<Text color={PERMISSION_COLORS[permissionMode]}>
|
|
615
|
-
⏵⏵ {PERMISSION_LABELS[permissionMode]} (
|
|
621
|
+
⏵⏵ {PERMISSION_LABELS[permissionMode]} ({t('ui.status.shift_tab_cycle')})
|
|
616
622
|
</Text>
|
|
617
|
-
<Text dimColor> ·
|
|
618
|
-
<Text dimColor> ·
|
|
623
|
+
<Text dimColor> · {t('ui.status.esc_to_interrupt')}</Text>
|
|
624
|
+
<Text dimColor> · {t('ui.status.left_for_agents')}</Text>
|
|
619
625
|
</Box>
|
|
620
626
|
</Box>
|
|
621
627
|
</Box>
|
package/src/ui/chat.tsx
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
2
|
import { Box, Text } from 'ink'
|
|
3
|
+
import { useI18n } from '../i18n-context'
|
|
3
4
|
import type { ChatMessage } from './app'
|
|
4
5
|
|
|
5
6
|
interface ChatPanelProps {
|
|
@@ -49,6 +50,7 @@ function toolColor(name: string): string {
|
|
|
49
50
|
*/
|
|
50
51
|
const MessageRow = React.memo(
|
|
51
52
|
function MessageRow({ msg }: { msg: ChatMessage; isLast: boolean }) {
|
|
53
|
+
const { t } = useI18n()
|
|
52
54
|
return (
|
|
53
55
|
<Box
|
|
54
56
|
flexDirection="column"
|
|
@@ -76,8 +78,8 @@ const MessageRow = React.memo(
|
|
|
76
78
|
{msg.role === 'user'
|
|
77
79
|
? `▸ ${displayCwd()}`
|
|
78
80
|
: msg.role === 'assistant'
|
|
79
|
-
? '
|
|
80
|
-
:
|
|
81
|
+
? t('ui.assistant.role_label')
|
|
82
|
+
: `⚠ ${t('ui.system.role_label')}`}
|
|
81
83
|
:
|
|
82
84
|
</Text>
|
|
83
85
|
<Text>{msg.content}</Text>
|
|
@@ -93,6 +95,7 @@ const MessageRow = React.memo(
|
|
|
93
95
|
)
|
|
94
96
|
|
|
95
97
|
export function ChatPanel({ messages, focusMode }: ChatPanelProps) {
|
|
98
|
+
const { t } = useI18n()
|
|
96
99
|
// Group consecutive tool calls into compact summaries to reduce visual noise.
|
|
97
100
|
// In focus mode, use the more aggressive compactForFocus.
|
|
98
101
|
const displayMessages = focusMode ? compactForFocus(messages) : compactToolGroups(messages)
|
|
@@ -102,8 +105,7 @@ export function ChatPanel({ messages, focusMode }: ChatPanelProps) {
|
|
|
102
105
|
{focusMode && messages.length > 0 && (
|
|
103
106
|
<Box marginBottom={1}>
|
|
104
107
|
<Text dimColor>
|
|
105
|
-
🔍
|
|
106
|
-
to expand
|
|
108
|
+
🔍 {t('ui.chat.focus_hint')} — {countHidden(messages, displayMessages, t)} · {t('ui.chat.toggle_focus_hint')} · {t('ui.chat.expand_hint')}
|
|
107
109
|
</Text>
|
|
108
110
|
</Box>
|
|
109
111
|
)}
|
|
@@ -111,22 +113,24 @@ export function ChatPanel({ messages, focusMode }: ChatPanelProps) {
|
|
|
111
113
|
<Box flexDirection="column">
|
|
112
114
|
<Box marginBottom={1}>
|
|
113
115
|
<Text color="cyan" bold>
|
|
114
|
-
|
|
116
|
+
{t('ui.banner.title')}
|
|
115
117
|
</Text>
|
|
116
|
-
<Text dimColor> —
|
|
118
|
+
<Text dimColor> — {t('ui.banner.subtitle')}</Text>
|
|
117
119
|
</Box>
|
|
118
|
-
<Text dimColor>
|
|
120
|
+
<Text dimColor>{t('ui.banner.tagline')}</Text>
|
|
119
121
|
<Box marginTop={1}>
|
|
120
122
|
<Text dimColor>
|
|
121
|
-
|
|
122
|
-
<Text color="yellow">Ctrl+P</Text> pick model · <Text color="yellow">Esc</Text> to
|
|
123
|
-
exit
|
|
123
|
+
{t('ui.banner.start_message')} <Text color="yellow">/help</Text>
|
|
124
124
|
</Text>
|
|
125
125
|
</Box>
|
|
126
126
|
<Box marginTop={1}>
|
|
127
127
|
<Text dimColor>
|
|
128
|
-
|
|
129
|
-
|
|
128
|
+
{t('ui.banner.controls_hint')}
|
|
129
|
+
</Text>
|
|
130
|
+
</Box>
|
|
131
|
+
<Box marginTop={1}>
|
|
132
|
+
<Text dimColor>
|
|
133
|
+
{t('ui.banner.tip_clear')}
|
|
130
134
|
</Text>
|
|
131
135
|
</Box>
|
|
132
136
|
</Box>
|
|
@@ -239,9 +243,9 @@ function summarizeToolGroup(tools: ChatMessage[]): ChatMessage {
|
|
|
239
243
|
}
|
|
240
244
|
|
|
241
245
|
/** Count how many messages are hidden in focus mode. */
|
|
242
|
-
function countHidden(all: ChatMessage[], compacted: ChatMessage[]): string {
|
|
246
|
+
function countHidden(all: ChatMessage[], compacted: ChatMessage[], t: (key: string, params?: Record<string, string>) => string): string {
|
|
243
247
|
const toolCount = all.filter((m) => m.toolMeta).length
|
|
244
248
|
const hidden = all.length - compacted.length
|
|
245
|
-
if (hidden <= 0) return
|
|
246
|
-
return
|
|
249
|
+
if (hidden <= 0) return t('ui.chat.showing_all', { count: String(all.length) })
|
|
250
|
+
return t('ui.chat.tool_outputs_folded', { hidden: String(hidden), toolCount: String(toolCount) })
|
|
247
251
|
}
|