@miphamai/cli 0.2.4 → 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.
Files changed (37) hide show
  1. package/bin/mipham.ts +2 -0
  2. package/dist/mipham +0 -0
  3. package/package.json +10 -9
  4. package/skills/mipham/om-artifact.mipham-skill.md +69 -0
  5. package/skills/mipham/om-model-optimize.mipham-skill.md +58 -9
  6. package/skills/mipham/om-security.mipham-skill.md +85 -10
  7. package/skills/standard/doc-generator.SKILL.md +72 -11
  8. package/skills/standard/github-ops.SKILL.md +94 -13
  9. package/skills/standard/memory.SKILL.md +72 -11
  10. package/skills/standard/self-review.SKILL.md +50 -10
  11. package/skills/standard/superpower.SKILL.md +46 -8
  12. package/skills/standard/tdd.SKILL.md +85 -12
  13. package/skills/standard/web-search.SKILL.md +65 -13
  14. package/src/agent/sub-agent.ts +0 -1
  15. package/src/artifacts/manifest.ts +101 -0
  16. package/src/artifacts/server.ts +343 -0
  17. package/src/core/context.ts +73 -13
  18. package/src/core/engine.ts +194 -71
  19. package/src/core/instructions.ts +1 -1
  20. package/src/core/permission.ts +35 -4
  21. package/src/core/session-store.ts +5 -1
  22. package/src/index.tsx +35 -2
  23. package/src/mcp/transport.ts +42 -5
  24. package/src/providers/anthropic.ts +2 -1
  25. package/src/providers/fetch-utils.ts +101 -0
  26. package/src/providers/openai-compat.ts +53 -15
  27. package/src/providers/registry.ts +1 -0
  28. package/src/shared/constants.ts +8 -1
  29. package/src/shared/types.ts +21 -1
  30. package/src/skills/loader.ts +2 -2
  31. package/src/tools/artifact/artifact.ts +144 -0
  32. package/src/tools/index.ts +3 -0
  33. package/src/ui/app.tsx +122 -15
  34. package/src/ui/chat.tsx +29 -7
  35. package/src/ui/commands.ts +117 -19
  36. package/src/ui/input.tsx +117 -35
  37. package/src/ui/picker.tsx +2 -5
@@ -1,11 +1,6 @@
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'
3
+ import { fetchWithRetry } from './fetch-utils'
9
4
 
10
5
  export class OpenAICompatProvider implements ProviderInstance {
11
6
  constructor(public config: ProviderConfig) {}
@@ -23,7 +18,7 @@ export class OpenAICompatProvider implements ProviderInstance {
23
18
  tools: req.tools?.map((t) => ({ type: 'function', function: t })),
24
19
  }
25
20
 
26
- const response = await fetch(`${baseUrl}/chat/completions`, {
21
+ const response = await fetchWithRetry(`${baseUrl}/chat/completions`, {
27
22
  method: 'POST',
28
23
  headers: {
29
24
  'Content-Type': 'application/json',
@@ -47,6 +42,9 @@ export class OpenAICompatProvider implements ProviderInstance {
47
42
  const decoder = new TextDecoder()
48
43
  let buffer = ''
49
44
 
45
+ // Track incremental tool calls across streaming deltas
46
+ const pendingToolCalls = new Map<number, { id: string; name: string; arguments: string }>()
47
+
50
48
  while (true) {
51
49
  const { done, value } = await reader.read()
52
50
  if (done) break
@@ -60,6 +58,18 @@ export class OpenAICompatProvider implements ProviderInstance {
60
58
  if (!trimmed || !trimmed.startsWith('data: ')) continue
61
59
  const data = trimmed.slice(6)
62
60
  if (data === '[DONE]') {
61
+ // Emit any pending tool calls before stopping
62
+ for (const [, tc] of pendingToolCalls) {
63
+ yield {
64
+ type: 'tool_use',
65
+ toolUse: {
66
+ type: 'tool_use',
67
+ id: tc.id,
68
+ name: tc.name,
69
+ input: this.safeParseJson(tc.arguments),
70
+ },
71
+ }
72
+ }
63
73
  yield { type: 'stop' }
64
74
  return
65
75
  }
@@ -73,21 +83,39 @@ export class OpenAICompatProvider implements ProviderInstance {
73
83
 
74
84
  if (delta?.tool_calls) {
75
85
  for (const tc of delta.tool_calls) {
86
+ const idx = tc.index ?? 0
87
+ const pending = pendingToolCalls.get(idx) || {
88
+ id: '',
89
+ name: '',
90
+ arguments: '',
91
+ }
92
+
93
+ if (tc.id) pending.id = tc.id
94
+ if (tc.function?.name) pending.name = tc.function.name
95
+ if (tc.function?.arguments) pending.arguments += tc.function.arguments
96
+
97
+ pendingToolCalls.set(idx, pending)
98
+ }
99
+ }
100
+
101
+ if (delta?.content) {
102
+ yield { type: 'text', content: delta.content }
103
+ }
104
+
105
+ if (choice.finish_reason === 'tool_calls') {
106
+ // Emit fully accumulated tool calls
107
+ for (const [, tc] of pendingToolCalls) {
76
108
  yield {
77
109
  type: 'tool_use',
78
110
  toolUse: {
79
111
  type: 'tool_use',
80
112
  id: tc.id || `call_${Date.now()}`,
81
- name: tc.function?.name || '',
82
- input: tc.function?.arguments ? JSON.parse(tc.function.arguments) : {},
113
+ name: tc.name,
114
+ input: this.safeParseJson(tc.arguments),
83
115
  },
84
116
  }
85
117
  }
86
- continue
87
- }
88
-
89
- if (delta?.content) {
90
- yield { type: 'text', content: delta.content }
118
+ pendingToolCalls.clear()
91
119
  }
92
120
 
93
121
  if (choice.finish_reason === 'stop') {
@@ -148,6 +176,16 @@ export class OpenAICompatProvider implements ProviderInstance {
148
176
  return result
149
177
  }
150
178
 
179
+ /** Safely parse JSON, returning an empty object on failure. */
180
+ private safeParseJson(raw: string): Record<string, unknown> {
181
+ if (!raw) return {}
182
+ try {
183
+ return JSON.parse(raw)
184
+ } catch {
185
+ return { _raw: raw }
186
+ }
187
+ }
188
+
151
189
  private resolveApiKey(keyTemplate: string): string {
152
190
  const match = keyTemplate.match(/^\$\{(.+)\}$/)
153
191
  if (match?.[1]) {
@@ -14,6 +14,7 @@ export interface ChatRequest {
14
14
  tools?: Record<string, unknown>[]
15
15
  maxTokens?: number
16
16
  temperature?: number
17
+ signal?: AbortSignal
17
18
  }
18
19
 
19
20
  export class ProviderRegistry {
@@ -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
@@ -1,4 +1,4 @@
1
- import React, { useState, useCallback } from 'react'
1
+ import React, { useState, useCallback, useRef } from 'react'
2
2
  import { Box, Text, useInput } from 'ink'
3
3
  import type { QueryEngine } from '../core/engine'
4
4
  import type { MiphamConfig } from '../shared/index.ts'
@@ -23,12 +23,26 @@ interface AppProps {
23
23
  skillsLoader?: SkillsLoader
24
24
  }
25
25
 
26
+ export interface ToolMeta {
27
+ name: string
28
+ input: string
29
+ output?: string
30
+ collapsed: boolean
31
+ }
32
+
26
33
  export interface ChatMessage {
27
34
  role: 'user' | 'assistant' | 'system'
28
35
  content: string
36
+ toolMeta?: ToolMeta
29
37
  }
30
38
 
31
- const VERSION = '0.2.0'
39
+ interface AgentProgress {
40
+ name: string
41
+ description: string
42
+ startTime: number
43
+ }
44
+
45
+ const VERSION = '0.3.0'
32
46
 
33
47
  type PermissionMode = 'auto' | 'ask' | 'bypass'
34
48
 
@@ -44,7 +58,7 @@ export function App({
44
58
  config,
45
59
  initialProvider,
46
60
  initialModel,
47
- lang,
61
+ lang: _lang,
48
62
  skillsLoader,
49
63
  }: AppProps) {
50
64
  const [messages, setMessages] = useState<ChatMessage[]>([])
@@ -58,6 +72,21 @@ export function App({
58
72
  const [focusMode, setFocusMode] = useState(false)
59
73
  const [goalText, setGoalText] = useState('')
60
74
  const [permissionMode, setPermissionMode] = useState<PermissionMode>('auto')
75
+ const abortRef = useRef<AbortController | null>(null)
76
+ const [agentProgress, setAgentProgress] = useState<AgentProgress | null>(null)
77
+ const [agentElapsed, setAgentElapsed] = useState(0)
78
+
79
+ // Agent elapsed timer
80
+ React.useEffect(() => {
81
+ if (!agentProgress) {
82
+ setAgentElapsed(0)
83
+ return
84
+ }
85
+ const interval = setInterval(() => {
86
+ setAgentElapsed(Math.floor((Date.now() - agentProgress.startTime) / 1000))
87
+ }, 1000)
88
+ return () => clearInterval(interval)
89
+ }, [agentProgress])
61
90
 
62
91
  const mkCtx = useCallback(
63
92
  (): CommandContext => ({
@@ -161,10 +190,13 @@ export function App({
161
190
  setMessages((prev) => [...prev, { role: 'user', content: input }])
162
191
  setIsLoading(true)
163
192
 
193
+ const controller = new AbortController()
194
+ abortRef.current = controller
195
+
164
196
  let assistantContent = ''
165
197
 
166
198
  try {
167
- for await (const chunk of engine.process(input)) {
199
+ for await (const chunk of engine.process(input, controller.signal)) {
168
200
  if (chunk.type === 'text' && chunk.content) {
169
201
  assistantContent += chunk.content
170
202
  setMessages((prev) => {
@@ -180,24 +212,52 @@ export function App({
180
212
  }
181
213
 
182
214
  if (chunk.type === 'tool_use' && chunk.toolUse) {
215
+ const toolName = chunk.toolUse.name
216
+ const inputStr = JSON.stringify(chunk.toolUse.input)
217
+ const isAgent = toolName === 'Agent' || toolName === 'Task'
218
+
219
+ if (isAgent) {
220
+ setAgentProgress({
221
+ name: (chunk.toolUse.input.subagent_type as string) || 'General-purpose',
222
+ description:
223
+ (chunk.toolUse.input.description as string) ||
224
+ (chunk.toolUse.input.prompt as string) ||
225
+ '',
226
+ startTime: Date.now(),
227
+ })
228
+ }
229
+
230
+ const collapsed = toolName !== 'Agent' // agents always expanded
183
231
  setMessages((prev) => [
184
232
  ...prev,
185
233
  {
186
234
  role: 'system',
187
- content: `šŸ”§ Using tool: ${chunk.toolUse!.name}(${JSON.stringify(chunk.toolUse!.input)})`,
235
+ content: collapsed
236
+ ? `āŗ ${toolName} Ā· ${inputStr.slice(0, 50)}${inputStr.length > 50 ? '...' : ''} (Ctrl+O to expand)`
237
+ : `šŸ”§ ${toolName}: ${inputStr.slice(0, 200)}`,
238
+ toolMeta: { name: toolName, input: inputStr, collapsed },
188
239
  },
189
240
  ])
190
241
  }
191
242
 
192
243
  if (chunk.type === 'tool_result') {
193
- const resultPreview =
194
- chunk.content && chunk.content.length > 200
195
- ? chunk.content.slice(0, 200) + '...'
196
- : chunk.content || '(empty)'
197
- setMessages((prev) => [
198
- ...prev,
199
- { role: 'system', content: `šŸ“‹ Tool result: ${resultPreview}` },
200
- ])
244
+ setAgentProgress(null)
245
+ setMessages((prev) => {
246
+ const updated = [...prev]
247
+ for (let i = updated.length - 1; i >= 0; i--) {
248
+ if (updated[i]?.toolMeta && !updated[i]!.toolMeta!.output) {
249
+ updated[i] = {
250
+ ...updated[i]!,
251
+ content: updated[i]!.toolMeta!.collapsed
252
+ ? updated[i]!.content
253
+ : `šŸ“‹ ${updated[i]!.toolMeta!.name} result: ${(chunk.content || '(empty)').slice(0, 200)}`,
254
+ toolMeta: { ...updated[i]!.toolMeta!, output: chunk.content || '' },
255
+ }
256
+ break
257
+ }
258
+ }
259
+ return updated
260
+ })
201
261
  }
202
262
 
203
263
  if (chunk.type === 'error') {
@@ -211,6 +271,7 @@ export function App({
211
271
  setMessages((prev) => [...prev, { role: 'system', content: `Error: ${String(err)}` }])
212
272
  } finally {
213
273
  setIsLoading(false)
274
+ abortRef.current = null
214
275
  // Auto-save checkpoint after each AI response
215
276
  if (assistantContent) {
216
277
  engine.getContext().saveCheckpoint('post-turn')
@@ -227,6 +288,10 @@ export function App({
227
288
  setPickerOpen(false)
228
289
  return
229
290
  }
291
+ if (isLoading && abortRef.current) {
292
+ abortRef.current.abort()
293
+ return
294
+ }
230
295
  process.exit(0)
231
296
  }
232
297
  // Ctrl+P → open model picker
@@ -234,6 +299,34 @@ export function App({
234
299
  setPickerOpen((prev) => !prev)
235
300
  return
236
301
  }
302
+ // Ctrl+O → toggle last tool call expand/collapse
303
+ if (_input === '\x0f') {
304
+ setMessages((prev) => {
305
+ const msgs = [...prev]
306
+ for (let i = msgs.length - 1; i >= 0; i--) {
307
+ if (msgs[i]?.toolMeta) {
308
+ const meta = msgs[i]!.toolMeta!
309
+ if (meta.collapsed) {
310
+ msgs[i] = {
311
+ ...msgs[i]!,
312
+ content: `šŸ”§ ${meta.name}: ${meta.input}\nšŸ“‹ Result: ${meta.output || '(pending)'}`,
313
+ toolMeta: { ...meta, collapsed: false },
314
+ }
315
+ } else {
316
+ const short = meta.input.length > 50 ? meta.input.slice(0, 50) + '...' : meta.input
317
+ msgs[i] = {
318
+ ...msgs[i]!,
319
+ content: `āŗ ${meta.name} Ā· ${short} (Ctrl+O to expand)`,
320
+ toolMeta: { ...meta, collapsed: true },
321
+ }
322
+ }
323
+ break
324
+ }
325
+ }
326
+ return msgs
327
+ })
328
+ return
329
+ }
237
330
  // Shift+Tab → cycle permission mode (auto → ask → bypass → auto)
238
331
  if (key.shift && key.tab) {
239
332
  setPermissionMode((prev) => {
@@ -255,7 +348,7 @@ export function App({
255
348
  <Text bold color="cyan">
256
349
  Mipham Code
257
350
  </Text>
258
- <Text dimColor> v0.2.0</Text>
351
+ <Text dimColor> v0.3.0</Text>
259
352
  {sessionTitle ? (
260
353
  <Text color="yellow"> — {sessionTitle}</Text>
261
354
  ) : (
@@ -278,7 +371,7 @@ export function App({
278
371
  ā— {PERMISSION_LABELS[permissionMode]}
279
372
  </Text>
280
373
  <Text dimColor> (Shift+Tab to cycle)</Text>
281
- <Text dimColor> Ā· Ctrl+P pick Ā· /help Ā· Esc exit</Text>
374
+ <Text dimColor> Ā· Ctrl+P pick Ā· /help Ā· Esc cancel</Text>
282
375
  </Box>
283
376
  {goalText && (
284
377
  <Box>
@@ -287,6 +380,20 @@ export function App({
287
380
  )}
288
381
  </Box>
289
382
 
383
+ {/* Agent progress banner */}
384
+ {agentProgress && (
385
+ <Box flexDirection="column" marginY={1}>
386
+ <Text color="cyan" bold>
387
+ āŗ {agentProgress.name} <Text color="yellow">{agentElapsed}s</Text>
388
+ </Text>
389
+ <Text dimColor>
390
+ {agentProgress.description.length > 100
391
+ ? `"${agentProgress.description.slice(0, 100)}..."`
392
+ : `"${agentProgress.description}"`}
393
+ </Text>
394
+ </Box>
395
+ )}
396
+
290
397
  {/* Chat panel */}
291
398
  <ChatPanel messages={messages} focusMode={focusMode} />
292
399
 
package/src/ui/chat.tsx CHANGED
@@ -37,17 +37,39 @@ export function ChatPanel({ messages, focusMode }: ChatPanelProps) {
37
37
  exit
38
38
  </Text>
39
39
  </Box>
40
+ <Box marginTop={1}>
41
+ <Text dimColor>
42
+ Tip: Use <Text color="yellow">/clear</Text> to start fresh when switching topics and
43
+ free up context
44
+ </Text>
45
+ </Box>
40
46
  </Box>
41
47
  )}
42
48
  {displayMessages.map((msg, i) => (
43
49
  <Box key={i} flexDirection="column" marginY={1}>
44
- <Text
45
- bold
46
- color={msg.role === 'user' ? 'green' : msg.role === 'system' ? 'yellow' : 'blue'}
47
- >
48
- {msg.role === 'user' ? 'ā–ø You' : msg.role === 'assistant' ? 'Mipham Code' : '⚠ System'}:
49
- </Text>
50
- <Text>{msg.content}</Text>
50
+ {msg.toolMeta ? (
51
+ <Box flexDirection="column">
52
+ <Text color="yellow">
53
+ {msg.toolMeta.collapsed ? 'āŗ' : 'āŗ ā–¼'} {msg.toolMeta.name}
54
+ </Text>
55
+ <Text dimColor>{msg.content}</Text>
56
+ </Box>
57
+ ) : (
58
+ <>
59
+ <Text
60
+ bold
61
+ color={msg.role === 'user' ? 'green' : msg.role === 'system' ? 'yellow' : 'blue'}
62
+ >
63
+ {msg.role === 'user'
64
+ ? 'ā–ø You'
65
+ : msg.role === 'assistant'
66
+ ? 'Mipham Code'
67
+ : '⚠ System'}
68
+ :
69
+ </Text>
70
+ <Text>{msg.content}</Text>
71
+ </>
72
+ )}
51
73
  </Box>
52
74
  ))}
53
75
  </Box>