@miphamai/cli 0.20.0 → 0.20.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 +1 -1
- package/src/core/engine.ts +7 -7
- package/src/i18n-context.tsx +2 -6
- package/src/i18n-core/detect.ts +54 -0
- package/src/i18n-core/locales/en-US.json +781 -0
- package/src/i18n-core/locales/zh-CN.json +781 -0
- package/src/i18n-core/t.ts +19 -0
- package/src/i18n-core/types.ts +10 -0
- package/src/index.tsx +9 -6
- package/src/mcp/client.ts +4 -4
- package/src/mcp/oauth.ts +4 -4
- package/src/mcp/registry.ts +9 -5
- package/src/tools/index.ts +9 -5
- package/src/ui/app.tsx +55 -10
- package/src/ui/chat.tsx +18 -12
- package/src/ui/command-picker.tsx +3 -1
- package/src/ui/commands.ts +87 -28
- package/src/ui/picker.tsx +3 -1
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { TranslationMap } from './types'
|
|
2
|
+
|
|
3
|
+
function getNested(obj: TranslationMap, key: string): unknown {
|
|
4
|
+
return key.split('.').reduce((o, k) => (o as any)?.[k], obj as any)
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function createT(
|
|
8
|
+
current: TranslationMap,
|
|
9
|
+
fallback: TranslationMap,
|
|
10
|
+
): (key: string, params?: Record<string, string>) => string {
|
|
11
|
+
return function t(key: string, params?: Record<string, string>): string {
|
|
12
|
+
const val = getNested(current, key) ?? getNested(fallback, key) ?? key
|
|
13
|
+
if (typeof val !== 'string') return key
|
|
14
|
+
if (params) {
|
|
15
|
+
return val.replace(/\{(\w+)\}/g, (_, k: string) => params[k] ?? '')
|
|
16
|
+
}
|
|
17
|
+
return val
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export type Locale = 'en-US' | 'zh-CN'
|
|
2
|
+
|
|
3
|
+
export const SUPPORTED_LOCALES: Locale[] = ['en-US', 'zh-CN']
|
|
4
|
+
|
|
5
|
+
export const FALLBACK_LOCALE: Locale = 'en-US'
|
|
6
|
+
|
|
7
|
+
/** A TranslationMap is a nested object where leaves are template strings. */
|
|
8
|
+
export interface TranslationMap {
|
|
9
|
+
[key: string]: string | TranslationMap
|
|
10
|
+
}
|
package/src/index.tsx
CHANGED
|
@@ -24,12 +24,12 @@ import { getMetrics } from './core/metrics'
|
|
|
24
24
|
import { ARTIFACTS_DIR, ARTIFACT_PORT, MIPHAM_DIR } from './shared/constants'
|
|
25
25
|
import { AgentViewManager } from './agent-view/agent-view-manager'
|
|
26
26
|
import { AgentViewDashboard } from './agent-view/dashboard'
|
|
27
|
-
import { createT } from '
|
|
28
|
-
import { detectLocale } from '
|
|
27
|
+
import { createT } from './i18n-core/t'
|
|
28
|
+
import { detectLocale } from './i18n-core/detect'
|
|
29
29
|
import { I18nProvider } from './i18n-context'
|
|
30
|
-
import enUS from '
|
|
31
|
-
import zhCN from '
|
|
32
|
-
import type { TranslationMap } from '
|
|
30
|
+
import enUS from './i18n-core/locales/en-US.json' with { type: 'json' }
|
|
31
|
+
import zhCN from './i18n-core/locales/zh-CN.json' with { type: 'json' }
|
|
32
|
+
import type { TranslationMap } from './i18n-core/types'
|
|
33
33
|
|
|
34
34
|
interface RunOptions {
|
|
35
35
|
model?: string
|
|
@@ -69,7 +69,10 @@ export async function runApp(options: RunOptions): Promise<void> {
|
|
|
69
69
|
|
|
70
70
|
// Detect locale and create translation function
|
|
71
71
|
const locale = detectLocale({ lang: options.lang })
|
|
72
|
-
const localeBundles: Record<string, TranslationMap> = {
|
|
72
|
+
const localeBundles: Record<string, TranslationMap> = {
|
|
73
|
+
'en-US': enUS as TranslationMap,
|
|
74
|
+
'zh-CN': zhCN as TranslationMap,
|
|
75
|
+
}
|
|
73
76
|
const t = createT(localeBundles[locale] || enUS, enUS)
|
|
74
77
|
|
|
75
78
|
// Bootstrap providers
|
package/src/mcp/client.ts
CHANGED
|
@@ -10,10 +10,10 @@ 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 '
|
|
14
|
-
import enUS from '
|
|
15
|
-
import zhCN from '
|
|
16
|
-
import type { TranslationMap } from '
|
|
13
|
+
import { createT } from '../i18n-core/t'
|
|
14
|
+
import enUS from '../i18n-core/locales/en-US.json'
|
|
15
|
+
import zhCN from '../i18n-core/locales/zh-CN.json'
|
|
16
|
+
import type { TranslationMap } from '../i18n-core/types'
|
|
17
17
|
|
|
18
18
|
const bundles: Record<string, TranslationMap> = {
|
|
19
19
|
'en-US': enUS as TranslationMap,
|
package/src/mcp/oauth.ts
CHANGED
|
@@ -4,10 +4,10 @@ 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 '
|
|
8
|
-
import enUS from '
|
|
9
|
-
import zhCN from '
|
|
10
|
-
import type { TranslationMap } from '
|
|
7
|
+
import { createT } from '../i18n-core/t'
|
|
8
|
+
import enUS from '../i18n-core/locales/en-US.json'
|
|
9
|
+
import zhCN from '../i18n-core/locales/zh-CN.json'
|
|
10
|
+
import type { TranslationMap } from '../i18n-core/types'
|
|
11
11
|
|
|
12
12
|
const bundles: Record<string, TranslationMap> = {
|
|
13
13
|
'en-US': enUS as TranslationMap,
|
package/src/mcp/registry.ts
CHANGED
|
@@ -9,10 +9,10 @@
|
|
|
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 '
|
|
13
|
-
import enUS from '
|
|
14
|
-
import zhCN from '
|
|
15
|
-
import type { TranslationMap } from '
|
|
12
|
+
import { createT } from '../i18n-core/t'
|
|
13
|
+
import enUS from '../i18n-core/locales/en-US.json'
|
|
14
|
+
import zhCN from '../i18n-core/locales/zh-CN.json'
|
|
15
|
+
import type { TranslationMap } from '../i18n-core/types'
|
|
16
16
|
|
|
17
17
|
const bundles: Record<string, TranslationMap> = {
|
|
18
18
|
'en-US': enUS as TranslationMap,
|
|
@@ -127,7 +127,11 @@ export function registerMcpServerTools(
|
|
|
127
127
|
registered++
|
|
128
128
|
} catch (err) {
|
|
129
129
|
process.stderr.write(
|
|
130
|
-
t('errors.mcp_register_failed', {
|
|
130
|
+
t('errors.mcp_register_failed', {
|
|
131
|
+
tool: mcpTool.name,
|
|
132
|
+
server: serverName,
|
|
133
|
+
error: String(err),
|
|
134
|
+
}) + '\n',
|
|
131
135
|
)
|
|
132
136
|
}
|
|
133
137
|
}
|
package/src/tools/index.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import type { ToolDefinition, ToolResult } from '../shared/index.ts'
|
|
2
2
|
import { sanitizeParams } from '../shared/sanitize'
|
|
3
|
-
import { createT } from '
|
|
4
|
-
import enUS from '
|
|
5
|
-
import zhCN from '
|
|
6
|
-
import type { TranslationMap } from '
|
|
3
|
+
import { createT } from '../i18n-core/t'
|
|
4
|
+
import enUS from '../i18n-core/locales/en-US.json'
|
|
5
|
+
import zhCN from '../i18n-core/locales/zh-CN.json'
|
|
6
|
+
import type { TranslationMap } from '../i18n-core/types'
|
|
7
7
|
import { readTool } from './file/read'
|
|
8
8
|
import { writeTool } from './file/write'
|
|
9
9
|
import { editTool } from './file/edit'
|
|
@@ -110,7 +110,11 @@ function withValidation(tool: ToolDefinition): ToolDefinition {
|
|
|
110
110
|
const cleanParams = sanitizeParams(params)
|
|
111
111
|
const errors = validateParams(schema, cleanParams)
|
|
112
112
|
if (errors.length > 0) {
|
|
113
|
-
return {
|
|
113
|
+
return {
|
|
114
|
+
success: false,
|
|
115
|
+
content: '',
|
|
116
|
+
error: t('errors.invalid_params', { errors: errors.join('; ') }),
|
|
117
|
+
}
|
|
114
118
|
}
|
|
115
119
|
return tool.execute(cleanParams, ctx)
|
|
116
120
|
},
|
package/src/ui/app.tsx
CHANGED
|
@@ -146,6 +146,15 @@ export function App({
|
|
|
146
146
|
const [goalText, setGoalText] = useState('')
|
|
147
147
|
const [permissionMode, setPermissionMode] = useState<PermissionMode>('default')
|
|
148
148
|
const abortRef = useRef<AbortController | null>(null)
|
|
149
|
+
// Stream buffer: accumulate text chunks and throttle state updates to ~16fps.
|
|
150
|
+
// Without this, every SSE chunk triggers setMessages → copies full array →
|
|
151
|
+
// re-renders ChatPanel → re-runs compactToolGroups O(n). At 20-50 chunks/sec
|
|
152
|
+
// with 200+ messages, this saturates the event loop and freezes the UI.
|
|
153
|
+
const streamBufferRef = useRef<{
|
|
154
|
+
turnContent: string
|
|
155
|
+
isFirst: boolean
|
|
156
|
+
timer: ReturnType<typeof setTimeout> | null
|
|
157
|
+
}>({ turnContent: '', isFirst: true, timer: null })
|
|
149
158
|
const [agentProgress, setAgentProgress] = useState<AgentProgress | null>(null)
|
|
150
159
|
// Multi-agent tracking: keyed by agent/task ID, shows all running + recently completed agents
|
|
151
160
|
const [runningAgents, setRunningAgents] = useState<Record<string, AgentEntry>>({})
|
|
@@ -364,6 +373,25 @@ export function App({
|
|
|
364
373
|
let turnContent = ''
|
|
365
374
|
let isNewTurn = true
|
|
366
375
|
|
|
376
|
+
// Flush any pending stream buffer to state
|
|
377
|
+
const flushStreamBuffer = () => {
|
|
378
|
+
if (streamBufferRef.current.timer) {
|
|
379
|
+
clearTimeout(streamBufferRef.current.timer)
|
|
380
|
+
streamBufferRef.current.timer = null
|
|
381
|
+
}
|
|
382
|
+
const latest = streamBufferRef.current.turnContent
|
|
383
|
+
if (latest && !streamBufferRef.current.isFirst) {
|
|
384
|
+
setMessages((prev) => {
|
|
385
|
+
const updated = [...prev]
|
|
386
|
+
const last = updated[updated.length - 1]
|
|
387
|
+
if (last?.role === 'assistant') {
|
|
388
|
+
last.content = latest
|
|
389
|
+
}
|
|
390
|
+
return updated
|
|
391
|
+
})
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
367
395
|
try {
|
|
368
396
|
for await (const chunk of engine.process(input, controller.signal)) {
|
|
369
397
|
// Reasoning content (DeepSeek V4 thinking mode) — silently consumed,
|
|
@@ -373,21 +401,34 @@ export function App({
|
|
|
373
401
|
}
|
|
374
402
|
|
|
375
403
|
if (chunk.type === 'text' && chunk.content) {
|
|
376
|
-
// New turn:
|
|
404
|
+
// New turn: push fresh assistant message, reset stream buffer
|
|
377
405
|
if (isNewTurn) {
|
|
378
406
|
turnContent = chunk.content
|
|
379
407
|
isNewTurn = false
|
|
380
|
-
|
|
408
|
+
streamBufferRef.current = { turnContent: chunk.content, isFirst: false, timer: null }
|
|
409
|
+
setMessages((prev) => [
|
|
410
|
+
...prev,
|
|
411
|
+
{ role: 'assistant' as const, content: chunk.content || '' },
|
|
412
|
+
])
|
|
381
413
|
} else {
|
|
382
414
|
turnContent += chunk.content
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
415
|
+
streamBufferRef.current.turnContent = turnContent
|
|
416
|
+
// Throttle: flush to state at most every 60ms (~16 fps for text).
|
|
417
|
+
// The ref holds the latest text; state update copies the ref value.
|
|
418
|
+
if (!streamBufferRef.current.timer) {
|
|
419
|
+
streamBufferRef.current.timer = setTimeout(() => {
|
|
420
|
+
streamBufferRef.current.timer = null
|
|
421
|
+
const latest = streamBufferRef.current.turnContent
|
|
422
|
+
setMessages((prev) => {
|
|
423
|
+
const updated = [...prev]
|
|
424
|
+
const last = updated[updated.length - 1]
|
|
425
|
+
if (last?.role === 'assistant') {
|
|
426
|
+
last.content = latest
|
|
427
|
+
}
|
|
428
|
+
return updated
|
|
429
|
+
})
|
|
430
|
+
}, 60)
|
|
431
|
+
}
|
|
391
432
|
}
|
|
392
433
|
assistantContent += chunk.content
|
|
393
434
|
}
|
|
@@ -417,6 +458,8 @@ export function App({
|
|
|
417
458
|
toolMeta: { name: toolName, input: detail, collapsed: true },
|
|
418
459
|
},
|
|
419
460
|
])
|
|
461
|
+
// Flush stream buffer before showing tool card
|
|
462
|
+
flushStreamBuffer()
|
|
420
463
|
// Mark that next text chunk starts a new turn
|
|
421
464
|
isNewTurn = true
|
|
422
465
|
}
|
|
@@ -487,6 +530,8 @@ export function App({
|
|
|
487
530
|
} catch (err) {
|
|
488
531
|
setMessages((prev) => [...prev, { role: 'system', content: `Error: ${String(err)}` }])
|
|
489
532
|
} finally {
|
|
533
|
+
// Flush any remaining stream buffer before finishing
|
|
534
|
+
flushStreamBuffer()
|
|
490
535
|
setIsLoading(false)
|
|
491
536
|
abortRef.current = null
|
|
492
537
|
// Auto-save checkpoint after each AI response
|
package/src/ui/chat.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React from 'react'
|
|
1
|
+
import React, { useMemo } from 'react'
|
|
2
2
|
import { Box, Text } from 'ink'
|
|
3
3
|
import { useI18n } from '../i18n-context'
|
|
4
4
|
import type { ChatMessage } from './app'
|
|
@@ -96,16 +96,22 @@ const MessageRow = React.memo(
|
|
|
96
96
|
|
|
97
97
|
export function ChatPanel({ messages, focusMode }: ChatPanelProps) {
|
|
98
98
|
const { t } = useI18n()
|
|
99
|
-
//
|
|
100
|
-
//
|
|
101
|
-
|
|
99
|
+
// Memoize display message computation to avoid O(n) compact on every render.
|
|
100
|
+
// During streaming, messages changes on every chunk; without memoization,
|
|
101
|
+
// compactToolGroups/compactForFocus re-iterates the full message list on each
|
|
102
|
+
// chunk, saturating the event loop at 20-50 chunks/sec.
|
|
103
|
+
const displayMessages = useMemo(
|
|
104
|
+
() => (focusMode ? compactForFocus(messages) : compactToolGroups(messages)),
|
|
105
|
+
[messages, focusMode],
|
|
106
|
+
)
|
|
102
107
|
|
|
103
108
|
return (
|
|
104
109
|
<Box flexDirection="column" marginY={1} flexGrow={1}>
|
|
105
110
|
{focusMode && messages.length > 0 && (
|
|
106
111
|
<Box marginBottom={1}>
|
|
107
112
|
<Text dimColor>
|
|
108
|
-
🔍 {t('ui.chat.focus_hint')} — {countHidden(messages, displayMessages, t)} ·
|
|
113
|
+
🔍 {t('ui.chat.focus_hint')} — {countHidden(messages, displayMessages, t)} ·{' '}
|
|
114
|
+
{t('ui.chat.toggle_focus_hint')} · {t('ui.chat.expand_hint')}
|
|
109
115
|
</Text>
|
|
110
116
|
</Box>
|
|
111
117
|
)}
|
|
@@ -124,14 +130,10 @@ export function ChatPanel({ messages, focusMode }: ChatPanelProps) {
|
|
|
124
130
|
</Text>
|
|
125
131
|
</Box>
|
|
126
132
|
<Box marginTop={1}>
|
|
127
|
-
<Text dimColor>
|
|
128
|
-
{t('ui.banner.controls_hint')}
|
|
129
|
-
</Text>
|
|
133
|
+
<Text dimColor>{t('ui.banner.controls_hint')}</Text>
|
|
130
134
|
</Box>
|
|
131
135
|
<Box marginTop={1}>
|
|
132
|
-
<Text dimColor>
|
|
133
|
-
{t('ui.banner.tip_clear')}
|
|
134
|
-
</Text>
|
|
136
|
+
<Text dimColor>{t('ui.banner.tip_clear')}</Text>
|
|
135
137
|
</Box>
|
|
136
138
|
</Box>
|
|
137
139
|
)}
|
|
@@ -243,7 +245,11 @@ function summarizeToolGroup(tools: ChatMessage[]): ChatMessage {
|
|
|
243
245
|
}
|
|
244
246
|
|
|
245
247
|
/** Count how many messages are hidden in focus mode. */
|
|
246
|
-
function countHidden(
|
|
248
|
+
function countHidden(
|
|
249
|
+
all: ChatMessage[],
|
|
250
|
+
compacted: ChatMessage[],
|
|
251
|
+
t: (key: string, params?: Record<string, string>) => string,
|
|
252
|
+
): string {
|
|
247
253
|
const toolCount = all.filter((m) => m.toolMeta).length
|
|
248
254
|
const hidden = all.length - compacted.length
|
|
249
255
|
if (hidden <= 0) return t('ui.chat.showing_all', { count: String(all.length) })
|
|
@@ -101,7 +101,9 @@ export function CommandPicker({
|
|
|
101
101
|
|
|
102
102
|
{/* Command list */}
|
|
103
103
|
<Box flexDirection="column" marginBottom={1}>
|
|
104
|
-
{visible.length === 0 &&
|
|
104
|
+
{visible.length === 0 && (
|
|
105
|
+
<Text dimColor> {t('ui.command_picker.no_matches', { filter })}</Text>
|
|
106
|
+
)}
|
|
105
107
|
{visible.map((cmd, i) => {
|
|
106
108
|
const globalIdx = i + scrollStart
|
|
107
109
|
const isCursor = globalIdx === cursorIdx
|
package/src/ui/commands.ts
CHANGED
|
@@ -13,10 +13,10 @@ import { McpClient } from '../mcp/client'
|
|
|
13
13
|
import { NPM_UPDATE_COMMAND, PACKAGE_VERSION } from '../shared/index.ts'
|
|
14
14
|
import { getPreference } from '../config/preferences'
|
|
15
15
|
import { stripIndent } from './strip-indent.js'
|
|
16
|
-
import { createT } from '
|
|
17
|
-
import type { TranslationMap } from '
|
|
18
|
-
import enUS from '
|
|
19
|
-
import zhCN from '
|
|
16
|
+
import { createT } from '../i18n-core/t'
|
|
17
|
+
import type { TranslationMap } from '../i18n-core/types'
|
|
18
|
+
import enUS from '../i18n-core/locales/en-US.json' with { type: 'json' }
|
|
19
|
+
import zhCN from '../i18n-core/locales/zh-CN.json' with { type: 'json' }
|
|
20
20
|
|
|
21
21
|
// Default t() fallback for plain-function contexts (tests, bootstrap).
|
|
22
22
|
// When called from the React tree, ctx.t is populated from useI18n().
|
|
@@ -351,7 +351,9 @@ const modelsCmd: CommandHandler = (ctx) => {
|
|
|
351
351
|
|
|
352
352
|
const providerCmd: CommandHandler = (ctx) => {
|
|
353
353
|
const t = resolveT(ctx)
|
|
354
|
-
return {
|
|
354
|
+
return {
|
|
355
|
+
content: t('commands.provider.current', { provider: ctx.providerId, model: ctx.modelId }),
|
|
356
|
+
}
|
|
355
357
|
}
|
|
356
358
|
|
|
357
359
|
const providersCmd: CommandHandler = (ctx) => {
|
|
@@ -417,7 +419,9 @@ const toolsCmd: CommandHandler = (ctx) => {
|
|
|
417
419
|
.filter(([, v]) => v!.length > 0)
|
|
418
420
|
.map(([cat, items]) => `── ${cat.toUpperCase()} ──\n${items!.join('\n')}`)
|
|
419
421
|
|
|
420
|
-
return {
|
|
422
|
+
return {
|
|
423
|
+
content: `${t('commands.tools.title', { count: String(tools.size) })}\n\n${sections.join('\n\n')}`,
|
|
424
|
+
}
|
|
421
425
|
}
|
|
422
426
|
|
|
423
427
|
const skillsCmd: CommandHandler = (ctx) => {
|
|
@@ -548,10 +552,17 @@ const goalCmd: CommandHandler = (ctx, args) => {
|
|
|
548
552
|
if (!state.goal) {
|
|
549
553
|
return { content: t('commands.goal.usage') }
|
|
550
554
|
}
|
|
551
|
-
const lines = [
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
+
const lines = [
|
|
556
|
+
t('commands.goal.status_title'),
|
|
557
|
+
'',
|
|
558
|
+
t('commands.goal.status_goal', { goal: state.goal }),
|
|
559
|
+
]
|
|
560
|
+
if (state.verifyScript)
|
|
561
|
+
lines.push(t('commands.goal.status_verify_script', { script: state.verifyScript }))
|
|
562
|
+
if (state.verifySkill)
|
|
563
|
+
lines.push(t('commands.goal.status_verify_skill', { skill: state.verifySkill }))
|
|
564
|
+
if (state.decompose)
|
|
565
|
+
lines.push(t('commands.goal.status_decompose', { count: String(state.subtasks.length) }))
|
|
555
566
|
lines.push('', t('commands.goal.status_clear_hint'))
|
|
556
567
|
return { content: lines.join('\n') }
|
|
557
568
|
}
|
|
@@ -670,10 +681,15 @@ const usageCmd: CommandHandler = (ctx) => {
|
|
|
670
681
|
const apiTotal = summary.apiInputTokens + summary.apiOutputTokens
|
|
671
682
|
const apiLine =
|
|
672
683
|
summary.apiInputTokens > 0 || summary.apiOutputTokens > 0
|
|
673
|
-
? t('commands.usage.api_tokens', {
|
|
684
|
+
? t('commands.usage.api_tokens', {
|
|
685
|
+
in: summary.apiInputTokens.toLocaleString(),
|
|
686
|
+
out: summary.apiOutputTokens.toLocaleString(),
|
|
687
|
+
total: apiTotal.toLocaleString(),
|
|
688
|
+
})
|
|
674
689
|
: t('commands.usage.no_api')
|
|
675
690
|
|
|
676
|
-
const toolSection =
|
|
691
|
+
const toolSection =
|
|
692
|
+
toolLines.length > 0 ? `\n${t('commands.usage.tool_section')}\n${toolLines.join('\n')}` : ''
|
|
677
693
|
|
|
678
694
|
return {
|
|
679
695
|
content: stripIndent`
|
|
@@ -976,7 +992,11 @@ const rewindCmd: CommandHandler = (ctx) => {
|
|
|
976
992
|
}
|
|
977
993
|
|
|
978
994
|
return {
|
|
979
|
-
content: t('commands.rewind.confirmed', {
|
|
995
|
+
content: t('commands.rewind.confirmed', {
|
|
996
|
+
label: result.label,
|
|
997
|
+
count: String(result.messageCount),
|
|
998
|
+
remaining: String(c.getCheckpoints().length),
|
|
999
|
+
}),
|
|
980
1000
|
clearMessages: true,
|
|
981
1001
|
}
|
|
982
1002
|
}
|
|
@@ -1164,7 +1184,11 @@ const branchCmd: CommandHandler = (ctx, args) => {
|
|
|
1164
1184
|
|
|
1165
1185
|
const checkpointId = c.saveCheckpoint(name)
|
|
1166
1186
|
return {
|
|
1167
|
-
content: t('commands.branch.details', {
|
|
1187
|
+
content: t('commands.branch.details', {
|
|
1188
|
+
name,
|
|
1189
|
+
checkpoint: String(checkpointId),
|
|
1190
|
+
messages: String(msgs.length),
|
|
1191
|
+
}),
|
|
1168
1192
|
}
|
|
1169
1193
|
}
|
|
1170
1194
|
|
|
@@ -1575,9 +1599,7 @@ const lintCmd: CommandHandler = async (ctx) => {
|
|
|
1575
1599
|
'',
|
|
1576
1600
|
truncated || '(no issues found)',
|
|
1577
1601
|
'',
|
|
1578
|
-
hasPackageJson
|
|
1579
|
-
? t('commands.lint.fix_hint')
|
|
1580
|
-
: t('commands.lint.setup_hint'),
|
|
1602
|
+
hasPackageJson ? t('commands.lint.fix_hint') : t('commands.lint.setup_hint'),
|
|
1581
1603
|
].join('\n'),
|
|
1582
1604
|
}
|
|
1583
1605
|
} catch {
|
|
@@ -1618,7 +1640,9 @@ const filesCmd: CommandHandler = async (ctx) => {
|
|
|
1618
1640
|
t('commands.files.cwd', { path: cwd }),
|
|
1619
1641
|
'',
|
|
1620
1642
|
...items,
|
|
1621
|
-
entries.length > 40
|
|
1643
|
+
entries.length > 40
|
|
1644
|
+
? t('commands.files.more_files', { count: String(entries.length - 40) })
|
|
1645
|
+
: '',
|
|
1622
1646
|
'',
|
|
1623
1647
|
t('commands.files.hint'),
|
|
1624
1648
|
].join('\n'),
|
|
@@ -1643,7 +1667,12 @@ const statsCmd: CommandHandler = (ctx) => {
|
|
|
1643
1667
|
content: [
|
|
1644
1668
|
t('commands.stats.title'),
|
|
1645
1669
|
'',
|
|
1646
|
-
t('commands.stats.messages', {
|
|
1670
|
+
t('commands.stats.messages', {
|
|
1671
|
+
total: String(msgs.length),
|
|
1672
|
+
user: String(userMsgs),
|
|
1673
|
+
assistant: String(assistantMsgs),
|
|
1674
|
+
system: String(systemMsgs),
|
|
1675
|
+
}),
|
|
1647
1676
|
t('commands.stats.tokens', { tokens: tokens.toLocaleString() }),
|
|
1648
1677
|
t('commands.stats.tools', { count: String(tools.size) }),
|
|
1649
1678
|
t('commands.stats.provider', { provider: ctx.providerId }),
|
|
@@ -1738,7 +1767,12 @@ const hooksCmd: CommandHandler = async (ctx) => {
|
|
|
1738
1767
|
|
|
1739
1768
|
try {
|
|
1740
1769
|
const files = readdirSync(hooksDir).filter((f) => f.endsWith('.sh'))
|
|
1741
|
-
const lines: string[] = [
|
|
1770
|
+
const lines: string[] = [
|
|
1771
|
+
t('commands.hooks.title'),
|
|
1772
|
+
'',
|
|
1773
|
+
t('commands.hooks.location', { path: hooksDir }),
|
|
1774
|
+
'',
|
|
1775
|
+
]
|
|
1742
1776
|
|
|
1743
1777
|
for (const f of files) {
|
|
1744
1778
|
try {
|
|
@@ -1801,7 +1835,11 @@ const exportCmd: CommandHandler = async (ctx) => {
|
|
|
1801
1835
|
|
|
1802
1836
|
writeFileSync(filepath, lines.join('\n'), 'utf-8')
|
|
1803
1837
|
return {
|
|
1804
|
-
content: t('commands.export.confirmed', {
|
|
1838
|
+
content: t('commands.export.confirmed', {
|
|
1839
|
+
path: filepath,
|
|
1840
|
+
count: String(msgs.length),
|
|
1841
|
+
lines: String(lines.length),
|
|
1842
|
+
}),
|
|
1805
1843
|
copyContent: filepath,
|
|
1806
1844
|
}
|
|
1807
1845
|
}
|
|
@@ -1886,7 +1924,9 @@ const resumeLastCmd: CommandHandler = async (ctx) => {
|
|
|
1886
1924
|
|
|
1887
1925
|
const latest = SessionStore.getLatest()
|
|
1888
1926
|
if (!latest) {
|
|
1889
|
-
return {
|
|
1927
|
+
return {
|
|
1928
|
+
content: `${t('commands.resume.restored')}\n\n${t('commands.resume.no_sessions')}\n\n${t('commands.resume.empty_footer')}`,
|
|
1929
|
+
}
|
|
1890
1930
|
}
|
|
1891
1931
|
|
|
1892
1932
|
const session = SessionStore.load(latest.name)
|
|
@@ -1907,10 +1947,20 @@ const resumeLastCmd: CommandHandler = async (ctx) => {
|
|
|
1907
1947
|
content: [
|
|
1908
1948
|
t('commands.resume.restored'),
|
|
1909
1949
|
'',
|
|
1910
|
-
t('commands.resume.restored_content', {
|
|
1950
|
+
t('commands.resume.restored_content', {
|
|
1951
|
+
name: latest.name,
|
|
1952
|
+
total: String(session.messages.length),
|
|
1953
|
+
truncated: truncatedStr,
|
|
1954
|
+
provider: latest.provider,
|
|
1955
|
+
model: latest.model,
|
|
1956
|
+
date,
|
|
1957
|
+
}),
|
|
1911
1958
|
'',
|
|
1912
1959
|
truncated
|
|
1913
|
-
? t('commands.resume.restored_footer', {
|
|
1960
|
+
? t('commands.resume.restored_footer', {
|
|
1961
|
+
loaded: String(messages.length),
|
|
1962
|
+
total: String(session.messages.length),
|
|
1963
|
+
})
|
|
1914
1964
|
: t('commands.resume.restored_full_footer', { loaded: String(messages.length) }),
|
|
1915
1965
|
].join('\n'),
|
|
1916
1966
|
forwardedMessages: messages,
|
|
@@ -1954,13 +2004,17 @@ const resumeCmd: CommandHandler = async (ctx, args) => {
|
|
|
1954
2004
|
content: `${t('commands.resume.found_title')}\n\n${t('commands.resume.found_content', { name: session.metadata.name, messages: String(session.metadata.messageCount), provider: session.metadata.provider, model: session.metadata.model, updated: session.metadata.updatedAt, target: targetName })}`,
|
|
1955
2005
|
}
|
|
1956
2006
|
}
|
|
1957
|
-
return {
|
|
2007
|
+
return {
|
|
2008
|
+
content: `${t('commands.resume.not_found_title')}\n\n${t('commands.resume.not_found_content', { name: targetName })}`,
|
|
2009
|
+
}
|
|
1958
2010
|
}
|
|
1959
2011
|
|
|
1960
2012
|
const sessions = SessionStore.list()
|
|
1961
2013
|
|
|
1962
2014
|
if (sessions.length === 0) {
|
|
1963
|
-
return {
|
|
2015
|
+
return {
|
|
2016
|
+
content: `${t('commands.resume.restored')}\n\n${t('commands.resume.no_sessions')}\n\n${t('commands.resume.empty_footer')}`,
|
|
2017
|
+
}
|
|
1964
2018
|
}
|
|
1965
2019
|
|
|
1966
2020
|
const recent = sessions.slice(0, 10)
|
|
@@ -2029,7 +2083,10 @@ const memoryCmd: CommandHandler = async (ctx) => {
|
|
|
2029
2083
|
t('commands.memories.title'),
|
|
2030
2084
|
'',
|
|
2031
2085
|
t('commands.memories.location', { path: memoryDir }),
|
|
2032
|
-
t('commands.memories.total', {
|
|
2086
|
+
t('commands.memories.total', {
|
|
2087
|
+
count: String(memories.length),
|
|
2088
|
+
plural: memories.length === 1 ? 'y' : 'ies',
|
|
2089
|
+
}),
|
|
2033
2090
|
'',
|
|
2034
2091
|
...memories.map(
|
|
2035
2092
|
(m, i) =>
|
|
@@ -2095,7 +2152,9 @@ const upgradeCmd: CommandHandler = async (ctx) => {
|
|
|
2095
2152
|
lines.push(t('commands.upgrade.old_version_warning'))
|
|
2096
2153
|
} else {
|
|
2097
2154
|
lines.push('')
|
|
2098
|
-
lines.push(
|
|
2155
|
+
lines.push(
|
|
2156
|
+
t('commands.upgrade.update_failed', { command: NPM_UPDATE_COMMAND, path: backupPath || '' }),
|
|
2157
|
+
)
|
|
2099
2158
|
}
|
|
2100
2159
|
|
|
2101
2160
|
return { content: lines.join('\n') }
|
package/src/ui/picker.tsx
CHANGED
|
@@ -152,7 +152,9 @@ export function ModelPicker({
|
|
|
152
152
|
{isCurrent ? '✓' : ' '}
|
|
153
153
|
{p.name.padEnd(16)}
|
|
154
154
|
</Text>
|
|
155
|
-
<Text dimColor>
|
|
155
|
+
<Text dimColor>
|
|
156
|
+
{isUpcoming ? t('ui.picker.coming_soon') : `${activeModels(p).length}m`}
|
|
157
|
+
</Text>
|
|
156
158
|
</Box>
|
|
157
159
|
)
|
|
158
160
|
})}
|