@miphamai/cli 0.3.0 → 0.4.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.
@@ -1,10 +1,4 @@
1
- import type {
2
- ProviderConfig,
3
- ModelInfo,
4
- Message,
5
- StreamChunk,
6
- ContentBlock,
7
- } from '../shared/index.ts'
1
+ import type { ProviderConfig, ModelInfo, Message, StreamChunk } from '../shared/index.ts'
8
2
  import type { ProviderInstance, ChatRequest } from './registry'
9
3
  import { fetchWithRetry } from './fetch-utils'
10
4
 
@@ -49,10 +43,7 @@ export class OpenAICompatProvider implements ProviderInstance {
49
43
  let buffer = ''
50
44
 
51
45
  // Track incremental tool calls across streaming deltas
52
- const pendingToolCalls = new Map<
53
- number,
54
- { id: string; name: string; arguments: string }
55
- >()
46
+ const pendingToolCalls = new Map<number, { id: string; name: string; arguments: string }>()
56
47
 
57
48
  while (true) {
58
49
  const { done, value } = await reader.read()
@@ -361,8 +361,15 @@ export const PROTOCOL_LABELS: Record<string, string> = {
361
361
  custom: 'Custom Protocol',
362
362
  }
363
363
 
364
- export const TOOL_CATEGORIES = ['file', 'exec', 'agent', 'network', 'system'] as const
364
+ export const TOOL_CATEGORIES = ['file', 'exec', 'agent', 'network', 'system', 'artifact'] as const
365
365
  export const CONFIG_FILE_NAME = 'config.yml'
366
366
  export const MIPHAM_DIR = '.mipham'
367
367
  export const USER_CONFIG_DIR = '.mipham'
368
368
  export const MEMORY_DIR = 'memory'
369
+
370
+ // ── Artifact Constants ──
371
+ export const ARTIFACTS_DIR = 'artifacts'
372
+ export const ARTIFACT_PORT = 9876
373
+ export const ARTIFACT_MAX_PORT_TRIES = 10
374
+ export const ARTIFACT_MAX_SIZE = 5 * 1024 * 1024 // 5MB
375
+ export const ARTIFACT_ALLOWED_EXTENSIONS = ['.html', '.svg']
@@ -50,7 +50,7 @@ export interface Message {
50
50
 
51
51
  // ── Tool Types ──
52
52
  export type ToolPermission = 'auto' | 'ask' | 'bypass'
53
- export type ToolCategory = 'file' | 'exec' | 'agent' | 'network' | 'system'
53
+ export type ToolCategory = 'file' | 'exec' | 'agent' | 'network' | 'system' | 'artifact'
54
54
 
55
55
  export interface ToolContext {
56
56
  cwd: string
@@ -59,6 +59,26 @@ export interface ToolContext {
59
59
  model: string
60
60
  skillsLoader?: import('../skills/loader').SkillsLoader
61
61
  registry?: import('../providers/registry').ProviderRegistry
62
+ artifactServer?: import('../artifacts/server').ArtifactServer
63
+ }
64
+
65
+ // ── Artifact Types ──
66
+ export interface ArtifactEntry {
67
+ name: string
68
+ path: string
69
+ url: string
70
+ size: number
71
+ type: 'html' | 'svg'
72
+ createdAt: string
73
+ sessionId: string
74
+ versions?: string[] // version tags e.g. ['v1', 'v2']
75
+ versionCount?: number
76
+ }
77
+
78
+ export interface ArtifactManifest {
79
+ version: 1
80
+ artifacts: ArtifactEntry[]
81
+ port?: number
62
82
  }
63
83
 
64
84
  export interface ToolResult {
@@ -1,5 +1,5 @@
1
1
  import { readFileSync, existsSync, readdirSync, statSync } from 'node:fs'
2
- import { join, extname } from 'node:path'
2
+ import { join } from 'node:path'
3
3
  import { parse as parseYaml } from 'yaml'
4
4
  import type { SkillDefinition } from '../shared/index.ts'
5
5
 
@@ -84,7 +84,7 @@ export class SkillsLoader {
84
84
  private tryLoad(path: string, type: 'standard' | 'mipham'): void {
85
85
  try {
86
86
  const raw = readFileSync(path, 'utf-8')
87
- const { data, content } = parseFrontmatter(raw)
87
+ const { data } = parseFrontmatter(raw)
88
88
 
89
89
  const skill: SkillDefinition = {
90
90
  name: (data.name as string) || this.nameFromPath(path),
@@ -0,0 +1,144 @@
1
+ import { writeFileSync, mkdirSync, existsSync } from 'node:fs'
2
+ import { join } from 'node:path'
3
+ import type { ToolDefinition } from '../../shared/index.ts'
4
+ import { ARTIFACTS_DIR, ARTIFACT_MAX_SIZE } from '../../shared/constants'
5
+ import { addToManifest, readManifest, archiveVersion } from '../../artifacts/manifest'
6
+
7
+ const NAME_PATTERN = /^[a-z0-9][a-z0-9-]*[a-z0-9]$/
8
+
9
+ export const artifactTool: ToolDefinition = {
10
+ name: 'Artifact',
11
+ description:
12
+ 'Create an interactive HTML or SVG artifact that opens in the browser. Use for dashboards, visualizations, reports, and interactive demos. Content must be self-contained — no external CDN references.',
13
+ category: 'artifact',
14
+ permission: 'ask',
15
+ parameters: {
16
+ type: 'object',
17
+ properties: {
18
+ name: {
19
+ type: 'string',
20
+ description: 'Short kebab-case name (e.g. "user-dashboard", "pipeline-diagram")',
21
+ },
22
+ type: {
23
+ type: 'string',
24
+ enum: ['html', 'svg'],
25
+ description: 'Artifact type',
26
+ },
27
+ content: {
28
+ type: 'string',
29
+ description: 'Full HTML or SVG content (self-contained, inline CSS/JS only)',
30
+ },
31
+ },
32
+ required: ['name', 'type', 'content'],
33
+ },
34
+ async execute(params, ctx) {
35
+ const name = params.name as string
36
+ const type = params.type as string
37
+ const content = params.content as string
38
+
39
+ // Validate name
40
+ if (!NAME_PATTERN.test(name) || name.length < 3) {
41
+ return {
42
+ success: false,
43
+ content: '',
44
+ error:
45
+ `Invalid artifact name "${name}". Use kebab-case (letters, numbers, hyphens), ` +
46
+ `at least 3 characters, e.g. "my-dashboard".`,
47
+ }
48
+ }
49
+
50
+ // Validate size
51
+ const size = Buffer.byteLength(content, 'utf-8')
52
+ if (size > ARTIFACT_MAX_SIZE) {
53
+ return {
54
+ success: false,
55
+ content: '',
56
+ error: `Content size ${(size / 1_048_576).toFixed(1)}MB exceeds ${ARTIFACT_MAX_SIZE / 1_048_576}MB limit.`,
57
+ }
58
+ }
59
+
60
+ // Determine output paths
61
+ const baseDir = join(ctx.cwd, ARTIFACTS_DIR)
62
+ const sessionDir = join(baseDir, ctx.sessionId)
63
+ mkdirSync(sessionDir, { recursive: true })
64
+
65
+ const ext = type === 'svg' ? '.svg' : '.html'
66
+ const filename = `${name}${ext}`
67
+ const filepath = join(sessionDir, filename)
68
+
69
+ // Start server if not running
70
+ let port: number | undefined
71
+ if (ctx.artifactServer && !ctx.artifactServer.isRunning()) {
72
+ port = await ctx.artifactServer.start()
73
+ } else if (ctx.artifactServer) {
74
+ port = ctx.artifactServer.getPort()
75
+ }
76
+
77
+ // Archive previous version if this artifact already exists
78
+ let archivedVersion: string | undefined
79
+ const isUpdate = existsSync(filepath)
80
+ if (isUpdate) {
81
+ const manifest = readManifest(baseDir)
82
+ const existing = manifest.artifacts.find(
83
+ (a) => a.name === name && a.sessionId === ctx.sessionId,
84
+ )
85
+ if (existing) {
86
+ archivedVersion = archiveVersion(baseDir, existing)
87
+ }
88
+ }
89
+
90
+ // Write artifact
91
+ writeFileSync(filepath, content, 'utf-8')
92
+
93
+ // Build URL and manifest entry
94
+ const url = port
95
+ ? `http://localhost:${port}/${ctx.sessionId}/${filename}`
96
+ : `file://${filepath}`
97
+
98
+ // Preserve existing version count
99
+ const manifestPre = readManifest(baseDir)
100
+ const prev = manifestPre.artifacts.find((a) => a.name === name && a.sessionId === ctx.sessionId)
101
+ const versionCount = prev?.versionCount || (isUpdate ? 1 : undefined)
102
+
103
+ addToManifest(
104
+ baseDir,
105
+ {
106
+ name,
107
+ path: filepath,
108
+ url,
109
+ size,
110
+ type: type as 'html' | 'svg',
111
+ createdAt: new Date().toISOString(),
112
+ sessionId: ctx.sessionId,
113
+ versions: prev?.versions,
114
+ versionCount,
115
+ },
116
+ port,
117
+ )
118
+
119
+ // Notify SSE clients to refresh Gallery
120
+ if (ctx.artifactServer) {
121
+ ctx.artifactServer.notifyReload()
122
+ }
123
+
124
+ const galleryUrl = port ? `http://localhost:${port}` : undefined
125
+ const versionLine = archivedVersion ? ` Prev archived as: ${archivedVersion}` : ''
126
+ const galleryLine = galleryUrl ? `Gallery: ${galleryUrl}` : ''
127
+
128
+ return {
129
+ success: true,
130
+ content: [
131
+ `✓ Artifact "${name}" saved${isUpdate ? ' (updated)' : ''}.`,
132
+ ` URL: ${url}`,
133
+ ` Size: ${size.toLocaleString()} bytes`,
134
+ versionLine,
135
+ galleryLine,
136
+ '',
137
+ `Open in browser: /artifact open ${name}`,
138
+ `List all: /artifact list`,
139
+ ]
140
+ .filter(Boolean)
141
+ .join('\n'),
142
+ }
143
+ },
144
+ }
@@ -15,6 +15,7 @@ import { webFetchTool } from './network/web-fetch'
15
15
  import { webSearchTool } from './network/web-search'
16
16
  import { configTool } from './system/config'
17
17
  import { mcpTool } from './system/mcp'
18
+ import { artifactTool } from './artifact/artifact'
18
19
 
19
20
  /**
20
21
  * Validate tool parameters against the tool's JSON Schema definition.
@@ -116,6 +117,8 @@ export function createToolRegistry(): Map<string, ToolDefinition> {
116
117
  // System tools
117
118
  withValidation(configTool),
118
119
  withValidation(mcpTool),
120
+ // Artifact tools
121
+ withValidation(artifactTool),
119
122
  ]
120
123
 
121
124
  const map = new Map<string, ToolDefinition>()
package/src/ui/app.tsx CHANGED
@@ -58,7 +58,7 @@ export function App({
58
58
  config,
59
59
  initialProvider,
60
60
  initialModel,
61
- lang,
61
+ lang: _lang,
62
62
  skillsLoader,
63
63
  }: AppProps) {
64
64
  const [messages, setMessages] = useState<ChatMessage[]>([])
@@ -78,7 +78,10 @@ export function App({
78
78
 
79
79
  // Agent elapsed timer
80
80
  React.useEffect(() => {
81
- if (!agentProgress) { setAgentElapsed(0); return }
81
+ if (!agentProgress) {
82
+ setAgentElapsed(0)
83
+ return
84
+ }
82
85
  const interval = setInterval(() => {
83
86
  setAgentElapsed(Math.floor((Date.now() - agentProgress.startTime) / 1000))
84
87
  }, 1000)
@@ -216,8 +219,10 @@ export function App({
216
219
  if (isAgent) {
217
220
  setAgentProgress({
218
221
  name: (chunk.toolUse.input.subagent_type as string) || 'General-purpose',
219
- description: (chunk.toolUse.input.description as string) ||
220
- (chunk.toolUse.input.prompt as string) || '',
222
+ description:
223
+ (chunk.toolUse.input.description as string) ||
224
+ (chunk.toolUse.input.prompt as string) ||
225
+ '',
221
226
  startTime: Date.now(),
222
227
  })
223
228
  }
@@ -379,8 +384,7 @@ export function App({
379
384
  {agentProgress && (
380
385
  <Box flexDirection="column" marginY={1}>
381
386
  <Text color="cyan" bold>
382
- ⏺ {agentProgress.name}{' '}
383
- <Text color="yellow">{agentElapsed}s</Text>
387
+ ⏺ {agentProgress.name} <Text color="yellow">{agentElapsed}s</Text>
384
388
  </Text>
385
389
  <Text dimColor>
386
390
  {agentProgress.description.length > 100
package/src/ui/chat.tsx CHANGED
@@ -39,8 +39,8 @@ export function ChatPanel({ messages, focusMode }: ChatPanelProps) {
39
39
  </Box>
40
40
  <Box marginTop={1}>
41
41
  <Text dimColor>
42
- Tip: Use <Text color="yellow">/clear</Text> to start fresh when switching topics
43
- and free up context
42
+ Tip: Use <Text color="yellow">/clear</Text> to start fresh when switching topics and
43
+ free up context
44
44
  </Text>
45
45
  </Box>
46
46
  </Box>
@@ -50,8 +50,7 @@ export function ChatPanel({ messages, focusMode }: ChatPanelProps) {
50
50
  {msg.toolMeta ? (
51
51
  <Box flexDirection="column">
52
52
  <Text color="yellow">
53
- {msg.toolMeta.collapsed ? '⏺' : '⏺ ▼'}{' '}
54
- {msg.toolMeta.name}
53
+ {msg.toolMeta.collapsed ? '⏺' : '⏺ ▼'} {msg.toolMeta.name}
55
54
  </Text>
56
55
  <Text dimColor>{msg.content}</Text>
57
56
  </Box>
@@ -61,7 +60,12 @@ export function ChatPanel({ messages, focusMode }: ChatPanelProps) {
61
60
  bold
62
61
  color={msg.role === 'user' ? 'green' : msg.role === 'system' ? 'yellow' : 'blue'}
63
62
  >
64
- {msg.role === 'user' ? '▸ You' : msg.role === 'assistant' ? 'Mipham Code' : '⚠ System'}:
63
+ {msg.role === 'user'
64
+ ? '▸ You'
65
+ : msg.role === 'assistant'
66
+ ? 'Mipham Code'
67
+ : '⚠ System'}
68
+ :
65
69
  </Text>
66
70
  <Text>{msg.content}</Text>
67
71
  </>
@@ -8,14 +8,7 @@ import type { QueryEngine } from '../core/engine'
8
8
  import type { MiphamConfig } from '../shared/index.ts'
9
9
  import type { SkillsLoader } from '../skills/loader'
10
10
  import { McpClient } from '../mcp/client'
11
- import {
12
- PACKAGE_NAME,
13
- NPM_INSTALL_COMMAND,
14
- NPM_UPDATE_COMMAND,
15
- NPM_URL,
16
- PRODUCT_URL_INTERNATIONAL,
17
- PRODUCT_URL_CHINA,
18
- } from '@mipham/shared'
11
+ import { NPM_INSTALL_COMMAND, NPM_UPDATE_COMMAND } from '@mipham/shared'
19
12
 
20
13
  export interface CommandContext {
21
14
  engine: QueryEngine
@@ -421,7 +414,6 @@ const renameCmd: CommandHandler = (ctx, args) => {
421
414
  const goalCmd: CommandHandler = (ctx, args) => {
422
415
  const goal = args.join(' ')
423
416
  if (!goal.trim()) {
424
- const c = ctx.engine.getContext()
425
417
  return {
426
418
  content: `Usage: /goal <statement>\n\nSet a session-level completion condition. Mipham Code will track progress toward this goal.\n\nExample: /goal Fix all TypeScript errors and make tests pass`,
427
419
  }
@@ -867,7 +859,7 @@ const doctorCmd: CommandHandler = async (ctx) => {
867
859
  // ═══════════════════════════════════════════════════════════════
868
860
 
869
861
  const exportCmd: CommandHandler = async (ctx) => {
870
- const { writeFileSync, existsSync } = await import('node:fs')
862
+ const { writeFileSync } = await import('node:fs')
871
863
  const { join } = await import('node:path')
872
864
 
873
865
  const cwd = process.cwd()
@@ -2128,6 +2120,87 @@ Mipham Code includes 4 agent-type tools for structured workflows:
2128
2120
  Use /tools to see all 16 tools, including the 4 agent tools.`,
2129
2121
  })
2130
2122
 
2123
+ // ═══════════════════════════════════════════════════════════════
2124
+ // Artifacts
2125
+ // ═══════════════════════════════════════════════════════════════
2126
+
2127
+ async function openBrowser(url: string): Promise<void> {
2128
+ const cmd =
2129
+ process.platform === 'darwin'
2130
+ ? `open "${url}"`
2131
+ : process.platform === 'win32'
2132
+ ? `start "" "${url}"`
2133
+ : `xdg-open "${url}"`
2134
+ const { exec } = await import('node:child_process')
2135
+ exec(cmd, () => {
2136
+ /* fire-and-forget */
2137
+ })
2138
+ }
2139
+
2140
+ const artifactOpenCmd: CommandHandler = async (ctx, args) => {
2141
+ const name = args[0]
2142
+ if (!name) {
2143
+ return { content: 'Usage: /artifact open <name>\nExample: /artifact open dashboard' }
2144
+ }
2145
+
2146
+ const sessionId = 'session-1' // default session
2147
+ const port = 9876
2148
+ const ext = name.endsWith('.svg') ? '' : '.html'
2149
+ const url = `http://localhost:${port}/${sessionId}/${name}${ext}`
2150
+
2151
+ try {
2152
+ await openBrowser(url)
2153
+ return { content: `✓ Opening artifact "${name}" in browser...\n ${url}` }
2154
+ } catch (err) {
2155
+ return { content: `✗ Failed to open browser: ${String(err)}\n URL: ${url}` }
2156
+ }
2157
+ }
2158
+
2159
+ const artifactListCmd: CommandHandler = async (_ctx, _args) => {
2160
+ const { getSessionArtifacts } = await import('../artifacts/manifest')
2161
+ const { join } = await import('node:path')
2162
+ const { ARTIFACTS_DIR } = await import('../shared/constants')
2163
+
2164
+ const dir = join(process.cwd(), ARTIFACTS_DIR)
2165
+ const entries = getSessionArtifacts(dir, 'session-1')
2166
+
2167
+ if (entries.length === 0) {
2168
+ return {
2169
+ content: 'No artifacts created yet. Ask the AI to generate one with the Artifact tool.',
2170
+ }
2171
+ }
2172
+
2173
+ const lines = ['── Artifacts ──', '']
2174
+ for (const e of entries) {
2175
+ const ver = e.versions && e.versions.length > 1 ? ` v${e.versions.length}` : ''
2176
+ lines.push(
2177
+ ` ${e.name.padEnd(24)} ${(e.type + ver).padEnd(8)} ${(e.size / 1024).toFixed(1).padStart(8)}KB ${e.createdAt.slice(0, 16).replace('T', ' ')}`,
2178
+ )
2179
+ }
2180
+ lines.push('', ` ${entries.length} artifact(s) — /artifact open <name> to view`)
2181
+ lines.push(` Gallery: http://localhost:9876`)
2182
+
2183
+ return { content: lines.join('\n') }
2184
+ }
2185
+
2186
+ const artifactServerCmd: CommandHandler = async (_ctx, _args) => {
2187
+ const url = 'http://localhost:9876'
2188
+ return {
2189
+ content: `── Artifact Server ──\n\n URL: ${url}\n Port: 9876\n\nArtifacts are served locally. No external network access.`,
2190
+ }
2191
+ }
2192
+
2193
+ const artifactBaseCmd: CommandHandler = (ctx, args) => {
2194
+ const sub = args[0]
2195
+ if (sub === 'open') return artifactOpenCmd(ctx, args.slice(1))
2196
+ if (sub === 'list') return artifactListCmd(ctx, [])
2197
+ if (sub === 'server') return artifactServerCmd(ctx, [])
2198
+ return {
2199
+ content:
2200
+ 'Usage: /artifact <open|list|server> [name]\n\n /artifact open <name> Open artifact in browser\n /artifact list List all artifacts\n /artifact server Show server status',
2201
+ }
2202
+ }
2203
+
2131
2204
  // ═══════════════════════════════════════════════════════════════
2132
2205
  // Remaining low-priority stubs (backend not yet available)
2133
2206
  // ═══════════════════════════════════════════════════════════════
@@ -2214,6 +2287,9 @@ registry.set('/logout', logoutCmd)
2214
2287
  registry.set('/feedback', feedbackCmd)
2215
2288
  registry.set('/agents', agentsCmd)
2216
2289
 
2290
+ // Artifacts
2291
+ registry.set('/artifact', artifactBaseCmd)
2292
+
2217
2293
  // Lower-priority semi-stubs (WIP, backend pending)
2218
2294
  registry.set('/branch', branchCmd)
2219
2295
  registry.set('/schedule', scheduleCmd)
package/src/ui/input.tsx CHANGED
@@ -10,34 +10,95 @@ interface InputBarProps {
10
10
  // ── Status verbs — English (Claude Code-aligned) + Chinese (Mipham originals) ──
11
11
 
12
12
  const STATUS_EN = [
13
- 'Doodling', 'Forging', 'Germinating', 'Mosmosing', 'Cerebrating',
14
- 'Boogieing', 'Finagling', 'Misting', 'Recombobulating', 'Slithering',
15
- 'Effecting', 'Roosting', 'Crystallizing', 'Canoodling', 'Billowing',
16
- 'Warping', 'Improvising', 'Metamorphing', 'Gusting', 'Whiring',
17
- 'Topsy-turvying', 'Flibbertigibetting', 'Wibbling', 'Thundering',
18
- 'Moseying', 'Julienning', 'Evaporating', 'Ruminating', 'Whisking',
19
- 'Scampering', 'Meandering', 'Concoting', 'Gesticulating', 'Flamebeing',
20
- 'Quantumizing', 'Waddling', 'Fluttering', 'Sprouting', 'Elucidating',
21
- 'Embellishing', 'Razzmatizing', 'Pondering', 'Cogitating', 'Garnishing',
22
- 'Actualizing', 'Zigzagging', 'Mustering', 'Frolicking', 'Hullaballooing',
23
- 'Doing', 'Fermenting', 'Considering', 'Skedaddling', 'Actioning',
24
- 'Orbiting', 'Perambulating', 'Drizzling', 'Schlepping', 'Ionizing',
13
+ 'Doodling',
14
+ 'Forging',
15
+ 'Germinating',
16
+ 'Mosmosing',
17
+ 'Cerebrating',
18
+ 'Boogieing',
19
+ 'Finagling',
20
+ 'Misting',
21
+ 'Recombobulating',
22
+ 'Slithering',
23
+ 'Effecting',
24
+ 'Roosting',
25
+ 'Crystallizing',
26
+ 'Canoodling',
27
+ 'Billowing',
28
+ 'Warping',
29
+ 'Improvising',
30
+ 'Metamorphing',
31
+ 'Gusting',
32
+ 'Whiring',
33
+ 'Topsy-turvying',
34
+ 'Flibbertigibetting',
35
+ 'Wibbling',
36
+ 'Thundering',
37
+ 'Moseying',
38
+ 'Julienning',
39
+ 'Evaporating',
40
+ 'Ruminating',
41
+ 'Whisking',
42
+ 'Scampering',
43
+ 'Meandering',
44
+ 'Concoting',
45
+ 'Gesticulating',
46
+ 'Flamebeing',
47
+ 'Quantumizing',
48
+ 'Waddling',
49
+ 'Fluttering',
50
+ 'Sprouting',
51
+ 'Elucidating',
52
+ 'Embellishing',
53
+ 'Razzmatizing',
54
+ 'Pondering',
55
+ 'Cogitating',
56
+ 'Garnishing',
57
+ 'Actualizing',
58
+ 'Zigzagging',
59
+ 'Mustering',
60
+ 'Frolicking',
61
+ 'Hullaballooing',
62
+ 'Doing',
63
+ 'Fermenting',
64
+ 'Considering',
65
+ 'Skedaddling',
66
+ 'Actioning',
67
+ 'Orbiting',
68
+ 'Perambulating',
69
+ 'Drizzling',
70
+ 'Schlepping',
71
+ 'Ionizing',
25
72
  'Scurrying',
26
73
  ]
27
74
 
28
75
  const STATUS_CN = [
29
- '思忖中', '推演中', '凝炼中', '熬煮中', '锻造中',
30
- '研磨中', '解构中', '冥思中', '编织中', '萃取中',
31
- '烹制中', '淬火中', '雕琢中', '融汇中', '觉照中',
32
- '运转中', '参详中', '化合中', '浸润中', '焙烤中',
76
+ '思忖中',
77
+ '推演中',
78
+ '凝炼中',
79
+ '熬煮中',
80
+ '锻造中',
81
+ '研磨中',
82
+ '解构中',
83
+ '冥思中',
84
+ '编织中',
85
+ '萃取中',
86
+ '烹制中',
87
+ '淬火中',
88
+ '雕琢中',
89
+ '融汇中',
90
+ '觉照中',
91
+ '运转中',
92
+ '参详中',
93
+ '化合中',
94
+ '浸润中',
95
+ '焙烤中',
33
96
  ]
34
97
 
35
98
  // Merge both — bilingual status verbs
36
99
  const STATUS_GERUNDS = [...STATUS_EN, ...STATUS_CN]
37
100
 
38
- const STATUS_PAST = [
39
- 'Brewed', 'Churned', 'Cooked', 'Sautéed', 'Cogitated', 'Crunched',
40
- ]
101
+ const STATUS_PAST = ['Brewed', 'Churned', 'Cooked', 'Sautéed', 'Cogitated', 'Crunched']
41
102
 
42
103
  function pick<T>(arr: T[]): T {
43
104
  return arr[Math.floor(Math.random() * arr.length)]!
package/src/ui/picker.tsx CHANGED
@@ -1,6 +1,6 @@
1
- import React, { useState, useCallback, useEffect } from 'react'
1
+ import React, { useState, useCallback } from 'react'
2
2
  import { Box, Text, useInput } from 'ink'
3
- import type { MiphamConfig, ProviderConfig, ModelInfo } from '../shared/index.ts'
3
+ import type { MiphamConfig, ProviderConfig } from '../shared/index.ts'
4
4
 
5
5
  interface PickerProps {
6
6
  config: MiphamConfig
@@ -113,9 +113,6 @@ export function ModelPicker({
113
113
  // Provider labels
114
114
  const providerColor = (p: ProviderConfig) => (p.id === currentProvider ? 'green' : 'white')
115
115
 
116
- const modelColor = (m: ModelInfo) =>
117
- selectedProvider?.id === currentProvider && m.id === currentModel ? 'green' : 'white'
118
-
119
116
  return (
120
117
  <Box flexDirection="column" borderStyle="round" borderColor="cyan" padding={1}>
121
118
  {/* Title */}