@miphamai/cli 0.15.0 → 0.16.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/dist/mipham +0 -0
- package/package.json +2 -4
- package/src/agent/message-bus.ts +19 -1
- package/src/agent/sub-agent.ts +25 -2
- package/src/commands/environment.ts +270 -0
- package/src/commands/git.ts +152 -0
- package/src/commands/project.ts +1011 -0
- package/src/config/defaults.ts +4 -0
- package/src/config/preferences.ts +45 -0
- package/src/core/context.ts +9 -0
- package/src/core/engine.ts +17 -5
- package/src/core/permission-config.ts +70 -6
- package/src/core/permission.ts +50 -9
- package/src/core/session-store.ts +7 -2
- package/src/index.tsx +51 -2
- package/src/providers/registry.ts +8 -0
- package/src/shared/sanitize.ts +38 -0
- package/src/shared/types.ts +18 -0
- package/src/skills/community-registry.json +72 -0
- package/src/skills/registry.ts +71 -77
- package/src/tools/exec/bash.ts +40 -8
- package/src/tools/exec/enter-worktree.ts +23 -1
- package/src/tools/exec/exit-worktree.ts +13 -0
- package/src/tools/index.ts +5 -2
- package/src/ui/agent-footer.tsx +89 -0
- package/src/ui/app.tsx +169 -35
- package/src/ui/chat.tsx +80 -30
- package/src/ui/commands.ts +176 -1477
- package/src/ui/strip-indent.ts +19 -0
- package/src/workflow/runtime.ts +29 -31
- package/src/workflow/sandbox.ts +22 -10
package/src/ui/app.tsx
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
|
-
import React, { useState, useCallback, useRef, useMemo } from 'react'
|
|
1
|
+
import React, { useState, useCallback, useEffect, useRef, useMemo } from 'react'
|
|
2
2
|
import { Box, Text, useInput } from 'ink'
|
|
3
|
+
import { readFileSync } from 'node:fs'
|
|
3
4
|
import type { QueryEngine } from '../core/engine'
|
|
4
5
|
import type { MiphamConfig } from '../shared/index.ts'
|
|
5
6
|
import type { SkillsLoader } from '../skills/loader'
|
|
6
7
|
import type { PluginManager } from '../plugin/plugin-manager'
|
|
8
|
+
import { getPreference, setPreference } from '../config/preferences'
|
|
7
9
|
import { AgentRegistry } from '../agent/agent-registry'
|
|
10
|
+
import { getBackgroundAgentRegistry } from '../agent/background-registry'
|
|
8
11
|
import { ChatPanel } from './chat'
|
|
9
12
|
import { InputBar } from './input'
|
|
10
13
|
import { ModelPicker } from './picker'
|
|
14
|
+
import { AgentFooter, type AgentEntry } from './agent-footer'
|
|
11
15
|
import {
|
|
12
16
|
getCommand,
|
|
13
17
|
looksLikeSlashCommand,
|
|
@@ -26,6 +30,7 @@ interface AppProps {
|
|
|
26
30
|
skillsLoader?: SkillsLoader
|
|
27
31
|
pluginManager?: PluginManager
|
|
28
32
|
version?: string
|
|
33
|
+
sessionId?: string
|
|
29
34
|
}
|
|
30
35
|
|
|
31
36
|
export interface ToolMeta {
|
|
@@ -45,6 +50,10 @@ interface AgentProgress {
|
|
|
45
50
|
name: string
|
|
46
51
|
description: string
|
|
47
52
|
startTime: number
|
|
53
|
+
tokensUsed?: number
|
|
54
|
+
isTask?: boolean
|
|
55
|
+
/** Unique identifier for multi-agent tracking. */
|
|
56
|
+
id?: string
|
|
48
57
|
}
|
|
49
58
|
|
|
50
59
|
// Version is read fresh from package.json at startup via runApp prop
|
|
@@ -75,6 +84,34 @@ const PERMISSION_COLORS: Record<PermissionMode, string> = {
|
|
|
75
84
|
bypassPermissions: 'red',
|
|
76
85
|
}
|
|
77
86
|
|
|
87
|
+
/** Format a tool's input parameters into a compact one-line detail string. */
|
|
88
|
+
function formatToolDetail(name: string, input: Record<string, unknown>): string {
|
|
89
|
+
switch (name) {
|
|
90
|
+
case 'Bash':
|
|
91
|
+
return (input.command as string) || ''
|
|
92
|
+
case 'Read':
|
|
93
|
+
return (input.file_path as string) || ''
|
|
94
|
+
case 'Write':
|
|
95
|
+
return (input.file_path as string) || ''
|
|
96
|
+
case 'Edit':
|
|
97
|
+
return (input.file_path as string) || ''
|
|
98
|
+
case 'Grep':
|
|
99
|
+
return (input.pattern as string) || ''
|
|
100
|
+
case 'Glob':
|
|
101
|
+
return (input.pattern as string) || ''
|
|
102
|
+
case 'Agent':
|
|
103
|
+
return `${(input.subagent_type as string) || 'general'}, "${((input.description as string) || (input.prompt as string) || '').slice(0, 80)}"`
|
|
104
|
+
case 'WebSearch':
|
|
105
|
+
return (input.query as string) || ''
|
|
106
|
+
case 'WebFetch':
|
|
107
|
+
return (input.url as string) || ''
|
|
108
|
+
case 'Task':
|
|
109
|
+
return `"${(input.subject as string) || ''}"`
|
|
110
|
+
default:
|
|
111
|
+
return JSON.stringify(input).slice(0, 80)
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
78
115
|
export function App({
|
|
79
116
|
engine,
|
|
80
117
|
config,
|
|
@@ -84,6 +121,7 @@ export function App({
|
|
|
84
121
|
skillsLoader,
|
|
85
122
|
pluginManager,
|
|
86
123
|
version,
|
|
124
|
+
sessionId,
|
|
87
125
|
}: AppProps) {
|
|
88
126
|
const [messages, setMessages] = useState<ChatMessage[]>([])
|
|
89
127
|
const [isLoading, setIsLoading] = useState(false)
|
|
@@ -94,23 +132,81 @@ export function App({
|
|
|
94
132
|
const [fastMode, setFastMode] = useState(false)
|
|
95
133
|
const [effort, setEffort] = useState('high')
|
|
96
134
|
const [focusMode, setFocusMode] = useState(false)
|
|
135
|
+
const [_ultracodeMode, setUltracodeMode] = useState(false)
|
|
97
136
|
const [goalText, setGoalText] = useState('')
|
|
98
137
|
const [permissionMode, setPermissionMode] = useState<PermissionMode>('default')
|
|
99
138
|
const abortRef = useRef<AbortController | null>(null)
|
|
100
139
|
const [agentProgress, setAgentProgress] = useState<AgentProgress | null>(null)
|
|
101
|
-
|
|
140
|
+
// Multi-agent tracking: keyed by agent/task ID, shows all running + recently completed agents
|
|
141
|
+
const [runningAgents, setRunningAgents] = useState<Record<string, AgentEntry>>({})
|
|
142
|
+
const [gitBranch, setGitBranch] = useState('')
|
|
143
|
+
const [agentTick, setAgentTick] = useState(0)
|
|
144
|
+
|
|
145
|
+
// Detect git branch on mount
|
|
146
|
+
useEffect(() => {
|
|
147
|
+
try {
|
|
148
|
+
const head = readFileSync('.git/HEAD', 'utf8').trim()
|
|
149
|
+
const m = head.match(/^ref: refs\/heads\/(.+)$/)
|
|
150
|
+
setGitBranch(m ? m[1]! : head.slice(0, 7))
|
|
151
|
+
} catch {
|
|
152
|
+
// Not a git repo — leave empty
|
|
153
|
+
}
|
|
154
|
+
}, [])
|
|
102
155
|
|
|
103
|
-
//
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
156
|
+
// Tick timer for agent elapsed displays (re-renders every second while agents are running)
|
|
157
|
+
useEffect(() => {
|
|
158
|
+
const hasRunning =
|
|
159
|
+
Object.values(runningAgents).some((a) => a.status === 'running') || agentProgress !== null
|
|
160
|
+
if (!hasRunning) return
|
|
161
|
+
const i = setInterval(() => setAgentTick((t) => t + 1), 1000)
|
|
162
|
+
return () => clearInterval(i)
|
|
163
|
+
}, [runningAgents, agentProgress])
|
|
164
|
+
|
|
165
|
+
// Sync running agents from BackgroundAgentRegistry into React state.
|
|
166
|
+
// Called after Agent tool results and task notifications to keep the footer current.
|
|
167
|
+
const syncBgAgents = useCallback(() => {
|
|
168
|
+
const bgRegistry = getBackgroundAgentRegistry()
|
|
169
|
+
const all = bgRegistry.list()
|
|
170
|
+
setRunningAgents((prev) => {
|
|
171
|
+
const next: Record<string, AgentEntry> = {}
|
|
172
|
+
for (const task of all) {
|
|
173
|
+
// Preserve existing entry if it has more data (e.g. tokens accumulated during streaming)
|
|
174
|
+
const existing = prev[task.id]
|
|
175
|
+
next[task.id] = {
|
|
176
|
+
id: task.id,
|
|
177
|
+
name: existing?.name || task.agentType.charAt(0).toUpperCase() + task.agentType.slice(1),
|
|
178
|
+
description: existing?.description || task.description,
|
|
179
|
+
startTime: existing?.startTime || task.startedAt.getTime(),
|
|
180
|
+
tokensUsed: existing?.tokensUsed || 0,
|
|
181
|
+
status: task.status === 'running' ? 'running' : 'completed',
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return next
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
// Register onComplete for running tasks to auto-dismiss them
|
|
188
|
+
for (const task of all) {
|
|
189
|
+
if (task.status === 'running') {
|
|
190
|
+
bgRegistry.onComplete(task.id, () => {
|
|
191
|
+
setRunningAgents((prev) => {
|
|
192
|
+
const next = { ...prev }
|
|
193
|
+
if (next[task.id]) {
|
|
194
|
+
next[task.id] = { ...next[task.id], status: 'completed' } as AgentEntry
|
|
195
|
+
}
|
|
196
|
+
return next
|
|
197
|
+
})
|
|
198
|
+
// Auto-dismiss after 5 seconds
|
|
199
|
+
setTimeout(() => {
|
|
200
|
+
setRunningAgents((prev) => {
|
|
201
|
+
const next = { ...prev }
|
|
202
|
+
delete next[task.id]
|
|
203
|
+
return next
|
|
204
|
+
})
|
|
205
|
+
}, 5000)
|
|
206
|
+
})
|
|
207
|
+
}
|
|
108
208
|
}
|
|
109
|
-
|
|
110
|
-
setAgentElapsed(Math.floor((Date.now() - agentProgress.startTime) / 1000))
|
|
111
|
-
}, 1000)
|
|
112
|
-
return () => clearInterval(interval)
|
|
113
|
-
}, [agentProgress])
|
|
209
|
+
}, [])
|
|
114
210
|
|
|
115
211
|
// Initialize agent registry (one-time, on mount)
|
|
116
212
|
useMemo(() => {
|
|
@@ -131,15 +227,20 @@ export function App({
|
|
|
131
227
|
providerId,
|
|
132
228
|
modelId,
|
|
133
229
|
version: version || '0.0.0',
|
|
230
|
+
sessionId: sessionId || '',
|
|
134
231
|
setSessionTitle: (title: string) => setSessionTitle(title),
|
|
135
232
|
setFastMode: (on: boolean) => setFastMode(on),
|
|
136
|
-
setEffort: (level: string) =>
|
|
233
|
+
setEffort: (level: string) => {
|
|
234
|
+
setEffort(level)
|
|
235
|
+
setPreference('lastCodeReviewEffort', level)
|
|
236
|
+
},
|
|
137
237
|
setFocusMode: (on: boolean) => setFocusMode(on),
|
|
138
238
|
setGoal: (text: string) => setGoalText(text),
|
|
239
|
+
setUltracodeMode: (on: boolean) => setUltracodeMode(on),
|
|
139
240
|
skillsLoader,
|
|
140
241
|
pluginManager,
|
|
141
242
|
}),
|
|
142
|
-
[engine, config, providerId, modelId, skillsLoader, pluginManager],
|
|
243
|
+
[engine, config, providerId, modelId, skillsLoader, pluginManager, sessionId],
|
|
143
244
|
)
|
|
144
245
|
|
|
145
246
|
const handleSubmit = useCallback(
|
|
@@ -294,10 +395,11 @@ export function App({
|
|
|
294
395
|
if (chunk.type === 'tool_use' && chunk.toolUse) {
|
|
295
396
|
const toolName = chunk.toolUse.name
|
|
296
397
|
const isAgent = toolName === 'Agent' || toolName === 'Task'
|
|
398
|
+
const detail = formatToolDetail(toolName, chunk.toolUse.input)
|
|
297
399
|
|
|
298
400
|
if (isAgent) {
|
|
299
401
|
setAgentProgress({
|
|
300
|
-
name: (chunk.toolUse.input.subagent_type as string) || 'General-purpose',
|
|
402
|
+
name: detail || (chunk.toolUse.input.subagent_type as string) || 'General-purpose',
|
|
301
403
|
description:
|
|
302
404
|
(chunk.toolUse.input.description as string) ||
|
|
303
405
|
(chunk.toolUse.input.prompt as string) ||
|
|
@@ -305,14 +407,47 @@ export function App({
|
|
|
305
407
|
startTime: Date.now(),
|
|
306
408
|
})
|
|
307
409
|
}
|
|
308
|
-
|
|
410
|
+
|
|
411
|
+
// Show tool call as a visible, collapsed message
|
|
412
|
+
setMessages((prev) => [
|
|
413
|
+
...prev,
|
|
414
|
+
{
|
|
415
|
+
role: 'system' as const,
|
|
416
|
+
content: detail,
|
|
417
|
+
toolMeta: { name: toolName, input: detail, collapsed: true },
|
|
418
|
+
},
|
|
419
|
+
])
|
|
309
420
|
// Mark that next text chunk starts a new turn
|
|
310
421
|
isNewTurn = true
|
|
311
422
|
}
|
|
312
423
|
|
|
313
424
|
if (chunk.type === 'tool_result') {
|
|
314
425
|
setAgentProgress(null)
|
|
315
|
-
//
|
|
426
|
+
// Sync background agents (e.g. Agent tool may have spawned them)
|
|
427
|
+
syncBgAgents()
|
|
428
|
+
// Show tool result as a compact indented line
|
|
429
|
+
const output = chunk.content ? String(chunk.content).trim() : ''
|
|
430
|
+
if (output) {
|
|
431
|
+
const firstLine = output.split('\n')[0]!.slice(0, 200)
|
|
432
|
+
const preview = firstLine.length < output.length ? `${firstLine}...` : firstLine
|
|
433
|
+
setMessages((prev) => [
|
|
434
|
+
...prev,
|
|
435
|
+
{
|
|
436
|
+
role: 'system' as const,
|
|
437
|
+
content: ` ⎿ ${preview}`,
|
|
438
|
+
toolMeta: { name: '', input: '', output: preview, collapsed: true },
|
|
439
|
+
},
|
|
440
|
+
])
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
if (chunk.type === 'usage' && agentProgress) {
|
|
445
|
+
const totalTokens = (chunk.inputTokens || 0) + (chunk.outputTokens || 0)
|
|
446
|
+
if (totalTokens > 0) {
|
|
447
|
+
setAgentProgress((prev) =>
|
|
448
|
+
prev ? { ...prev, tokensUsed: (prev.tokensUsed || 0) + totalTokens } : null,
|
|
449
|
+
)
|
|
450
|
+
}
|
|
316
451
|
}
|
|
317
452
|
|
|
318
453
|
if (chunk.type === 'error') {
|
|
@@ -324,8 +459,8 @@ export function App({
|
|
|
324
459
|
|
|
325
460
|
if (chunk.type === 'task_notification' && chunk.taskNotification) {
|
|
326
461
|
const tn = chunk.taskNotification
|
|
327
|
-
const
|
|
328
|
-
const
|
|
462
|
+
const isDone = tn.status === 'completed'
|
|
463
|
+
const symbol = isDone ? '◼' : '✳'
|
|
329
464
|
const preview = tn.content
|
|
330
465
|
? tn.content.slice(0, 120) + (tn.content.length > 120 ? '...' : '')
|
|
331
466
|
: tn.error
|
|
@@ -335,9 +470,17 @@ export function App({
|
|
|
335
470
|
...prev,
|
|
336
471
|
{
|
|
337
472
|
role: 'system',
|
|
338
|
-
content: `${
|
|
473
|
+
content: `${symbol} ${tn.description}${isDone ? ` · finished` : ''}`,
|
|
474
|
+
toolMeta: {
|
|
475
|
+
name: isDone ? '' : 'task',
|
|
476
|
+
input: tn.description || '',
|
|
477
|
+
output: preview,
|
|
478
|
+
collapsed: isDone,
|
|
479
|
+
},
|
|
339
480
|
},
|
|
340
481
|
])
|
|
482
|
+
// Sync agent footer — mark completed background agents
|
|
483
|
+
syncBgAgents()
|
|
341
484
|
}
|
|
342
485
|
}
|
|
343
486
|
} catch (err) {
|
|
@@ -349,9 +492,11 @@ export function App({
|
|
|
349
492
|
if (assistantContent) {
|
|
350
493
|
engine.getContext().saveCheckpoint('post-turn')
|
|
351
494
|
}
|
|
495
|
+
// Final sync of background agents after the turn completes
|
|
496
|
+
syncBgAgents()
|
|
352
497
|
}
|
|
353
498
|
},
|
|
354
|
-
[engine, mkCtx],
|
|
499
|
+
[engine, mkCtx, syncBgAgents],
|
|
355
500
|
)
|
|
356
501
|
|
|
357
502
|
useInput((_input, key) => {
|
|
@@ -420,20 +565,6 @@ export function App({
|
|
|
420
565
|
|
|
421
566
|
return (
|
|
422
567
|
<Box flexDirection="column" padding={1} height="100%">
|
|
423
|
-
{/* Agent progress banner */}
|
|
424
|
-
{agentProgress && (
|
|
425
|
-
<Box flexDirection="column" marginY={1}>
|
|
426
|
-
<Text color="cyan" bold>
|
|
427
|
-
⏺ {agentProgress.name} <Text color="yellow">{agentElapsed}s</Text>
|
|
428
|
-
</Text>
|
|
429
|
-
<Text dimColor>
|
|
430
|
-
{agentProgress.description.length > 100
|
|
431
|
-
? `"${agentProgress.description.slice(0, 100)}..."`
|
|
432
|
-
: `"${agentProgress.description}"`}
|
|
433
|
-
</Text>
|
|
434
|
-
</Box>
|
|
435
|
-
)}
|
|
436
|
-
|
|
437
568
|
{/* Header — left-aligned */}
|
|
438
569
|
<Box flexDirection="column" marginBottom={1}>
|
|
439
570
|
<Text color="#FFD700" bold>
|
|
@@ -473,6 +604,9 @@ export function App({
|
|
|
473
604
|
</Box>
|
|
474
605
|
)}
|
|
475
606
|
|
|
607
|
+
{/* Agent status footer — shows running background agents */}
|
|
608
|
+
<AgentFooter agents={Object.values(runningAgents)} gitBranch={gitBranch} tick={agentTick} />
|
|
609
|
+
|
|
476
610
|
{/* Status line */}
|
|
477
611
|
<Box marginTop={1} flexDirection="column">
|
|
478
612
|
{goalText && (
|
package/src/ui/chat.tsx
CHANGED
|
@@ -17,6 +17,81 @@ function displayCwd(): string {
|
|
|
17
17
|
return cwd
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
/** Map tool names to dot colors — Claude Code parity */
|
|
21
|
+
function toolColor(name: string): string {
|
|
22
|
+
switch (name) {
|
|
23
|
+
case 'Bash':
|
|
24
|
+
case 'Read':
|
|
25
|
+
case 'Glob':
|
|
26
|
+
case 'Grep':
|
|
27
|
+
return 'white'
|
|
28
|
+
case 'Write':
|
|
29
|
+
case 'Edit':
|
|
30
|
+
return 'red'
|
|
31
|
+
case 'Agent':
|
|
32
|
+
case 'Task':
|
|
33
|
+
case 'Skill':
|
|
34
|
+
case 'Workflow':
|
|
35
|
+
return 'magenta'
|
|
36
|
+
case 'WebSearch':
|
|
37
|
+
case 'WebFetch':
|
|
38
|
+
return 'blue'
|
|
39
|
+
default:
|
|
40
|
+
return 'white'
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Memoized single message row — skips re-render when content hasn't changed.
|
|
46
|
+
* This prevents O(n) re-renders of the entire chat history on every streaming chunk.
|
|
47
|
+
* The custom comparator returns true (skip render) when content, role, and collapse
|
|
48
|
+
* state are identical to the previous render.
|
|
49
|
+
*/
|
|
50
|
+
const MessageRow = React.memo(
|
|
51
|
+
function MessageRow({ msg }: { msg: ChatMessage; isLast: boolean }) {
|
|
52
|
+
return (
|
|
53
|
+
<Box
|
|
54
|
+
flexDirection="column"
|
|
55
|
+
marginTop={msg.toolMeta ? 0 : 1}
|
|
56
|
+
marginBottom={msg.toolMeta ? 0 : 1}
|
|
57
|
+
>
|
|
58
|
+
{msg.toolMeta ? (
|
|
59
|
+
<Box flexDirection="column">
|
|
60
|
+
<Text color={toolColor(msg.toolMeta.name)}>
|
|
61
|
+
{msg.toolMeta.collapsed ? '⏺' : '⏺ ▼'} {msg.toolMeta.name}
|
|
62
|
+
{msg.toolMeta.input ? `(${msg.toolMeta.input.slice(0, 120)})` : ''}
|
|
63
|
+
</Text>
|
|
64
|
+
{msg.toolMeta.output ? (
|
|
65
|
+
<Text dimColor> ⎿ {msg.toolMeta.output.slice(0, 300)}</Text>
|
|
66
|
+
) : msg.content && !msg.toolMeta.collapsed ? (
|
|
67
|
+
<Text dimColor>{msg.content}</Text>
|
|
68
|
+
) : null}
|
|
69
|
+
</Box>
|
|
70
|
+
) : (
|
|
71
|
+
<>
|
|
72
|
+
<Text
|
|
73
|
+
bold
|
|
74
|
+
color={msg.role === 'user' ? 'green' : msg.role === 'system' ? 'yellow' : 'blue'}
|
|
75
|
+
>
|
|
76
|
+
{msg.role === 'user'
|
|
77
|
+
? `▸ ${displayCwd()}`
|
|
78
|
+
: msg.role === 'assistant'
|
|
79
|
+
? 'Mipham Code'
|
|
80
|
+
: '⚠ System'}
|
|
81
|
+
:
|
|
82
|
+
</Text>
|
|
83
|
+
<Text>{msg.content}</Text>
|
|
84
|
+
</>
|
|
85
|
+
)}
|
|
86
|
+
</Box>
|
|
87
|
+
)
|
|
88
|
+
},
|
|
89
|
+
(prev, next) =>
|
|
90
|
+
prev.msg.content === next.msg.content &&
|
|
91
|
+
prev.msg.role === next.msg.role &&
|
|
92
|
+
prev.msg.toolMeta?.collapsed === next.msg.toolMeta?.collapsed,
|
|
93
|
+
)
|
|
94
|
+
|
|
20
95
|
export function ChatPanel({ messages, focusMode }: ChatPanelProps) {
|
|
21
96
|
// In focus mode, compact tool activity into summary lines
|
|
22
97
|
const displayMessages = focusMode ? compactForFocus(messages) : messages
|
|
@@ -56,36 +131,11 @@ export function ChatPanel({ messages, focusMode }: ChatPanelProps) {
|
|
|
56
131
|
</Box>
|
|
57
132
|
)}
|
|
58
133
|
{displayMessages.map((msg, i) => (
|
|
59
|
-
<
|
|
60
|
-
key={i}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
>
|
|
65
|
-
{msg.toolMeta ? (
|
|
66
|
-
<Box flexDirection="column">
|
|
67
|
-
<Text color="yellow">
|
|
68
|
-
{msg.toolMeta.collapsed ? '⏺' : '⏺ ▼'} {msg.toolMeta.name}
|
|
69
|
-
</Text>
|
|
70
|
-
<Text dimColor>{msg.content}</Text>
|
|
71
|
-
</Box>
|
|
72
|
-
) : (
|
|
73
|
-
<>
|
|
74
|
-
<Text
|
|
75
|
-
bold
|
|
76
|
-
color={msg.role === 'user' ? 'green' : msg.role === 'system' ? 'yellow' : 'blue'}
|
|
77
|
-
>
|
|
78
|
-
{msg.role === 'user'
|
|
79
|
-
? `▸ ${displayCwd()}`
|
|
80
|
-
: msg.role === 'assistant'
|
|
81
|
-
? 'Mipham Code'
|
|
82
|
-
: '⚠ System'}
|
|
83
|
-
:
|
|
84
|
-
</Text>
|
|
85
|
-
<Text>{msg.content}</Text>
|
|
86
|
-
</>
|
|
87
|
-
)}
|
|
88
|
-
</Box>
|
|
134
|
+
<MessageRow
|
|
135
|
+
key={`${i}-${msg.role}-${typeof msg.content === 'string' ? msg.content.length : 0}`}
|
|
136
|
+
msg={msg}
|
|
137
|
+
isLast={i === displayMessages.length - 1}
|
|
138
|
+
/>
|
|
89
139
|
))}
|
|
90
140
|
</Box>
|
|
91
141
|
)
|