@miphamai/cli 0.32.0 → 0.32.2

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@miphamai/cli",
3
- "version": "0.32.0",
3
+ "version": "0.32.2",
4
4
  "description": "Mipham Code — Multi-model open-core intelligent coding terminal by MiphamAI",
5
5
  "keywords": [
6
6
  "ai",
@@ -11,11 +11,23 @@ export interface AgentEntry {
11
11
  status: 'running' | 'completed'
12
12
  }
13
13
 
14
+ export interface AgentProgress {
15
+ name: string
16
+ description: string
17
+ startTime: number
18
+ tokensUsed?: number
19
+ isTask?: boolean
20
+ id?: string
21
+ }
22
+
14
23
  interface AgentFooterProps {
15
24
  agents: AgentEntry[]
16
- gitBranch: string
17
25
  /** Tick counter for live elapsed-time re-renders. */
18
26
  tick: number
27
+ /** Active foreground tool: shown as [ToolName detail...] before agent lines. */
28
+ activeTool?: { name: string; detail: string } | null
29
+ /** Inline agent progress from tool_use streaming: ✻ Gerund… (elapsed · tokens). */
30
+ agentProgress?: AgentProgress | null
19
31
  }
20
32
 
21
33
  /** Format elapsed seconds into human-readable string (e.g. "2m 51s", "38s"). */
@@ -33,20 +45,68 @@ function formatTokens(n: number): string {
33
45
  return String(n)
34
46
  }
35
47
 
36
- export function AgentFooter({ agents, gitBranch, tick: _tick }: AgentFooterProps) {
48
+ /** Map tool name to Ink color for the [Tool ...] indicator. */
49
+ function toolIndicatorColor(name: string): string {
50
+ switch (name) {
51
+ case 'Bash': case 'Read': case 'Glob': case 'Grep': return 'white'
52
+ case 'Write': case 'Edit': case 'Update': return 'red'
53
+ case 'Agent': case 'Task': case 'Skill': case 'Workflow': return 'magenta'
54
+ case 'WebSearch': case 'WebFetch': return 'blue'
55
+ default: return 'white'
56
+ }
57
+ }
58
+
59
+ /** Animate dots for the active tool indicator: cycles 1→2→3→1 dots. */
60
+ function animatedDots(tick: number): string {
61
+ const n = (tick % 3) + 1
62
+ return '.'.repeat(n)
63
+ }
64
+
65
+ /** Gerund verbs for agent progress — rotated every 2 seconds for visual liveliness. */
66
+ const GERUNDS = [
67
+ 'Pondering', 'Contemplating', 'Cerebrating', 'Ruminating',
68
+ 'Deliberating', 'Cogitating', 'Perambulating', 'Computing',
69
+ 'Processing', 'Analyzing', 'Synthesizing', 'Orchestrating',
70
+ 'Forging', 'Illuminating', 'Manifesting', 'Transmogrifying',
71
+ ]
72
+
73
+ /** Pick a gerund that changes every 2 seconds (driven by tick). */
74
+ function rotatingGerund(tick: number): string {
75
+ return GERUNDS[Math.floor(tick / 2) % GERUNDS.length]!
76
+ }
77
+
78
+ export function AgentFooter({ agents, tick, activeTool, agentProgress }: AgentFooterProps) {
37
79
  const { t } = useI18n()
38
80
  const hasAgents = agents.length > 0
39
- const hasBranch = gitBranch.length > 0
81
+ const hasTool = !!activeTool
82
+ const hasProgress = !!agentProgress
40
83
 
41
- if (!hasAgents && !hasBranch) return null
84
+ if (!hasAgents && !hasTool && !hasProgress) return null
42
85
 
43
86
  return (
44
87
  <Box flexDirection="column" marginTop={1}>
45
- {/* Git branch indicator */}
46
- {hasBranch && (
88
+ {/* Active foreground tool indicator — Claude Code style [Bash command...] */}
89
+ {hasTool && (
47
90
  <Box>
91
+ <Text color={toolIndicatorColor(activeTool!.name)}>
92
+ {' '}[{activeTool!.name} {activeTool!.detail.slice(0, 60)}{animatedDots(tick)}]
93
+ </Text>
94
+ </Box>
95
+ )}
96
+
97
+ {/* Inline agent progress — ✻ Gerund… (elapsed · ↓ tokens) in orange */}
98
+ {hasProgress && (
99
+ <Box>
100
+ <Text color="#FFA500">
101
+ {' '}✻ {rotatingGerund(tick)}…
102
+ </Text>
48
103
  <Text dimColor>
49
- {' '}⏺ {gitBranch}
104
+ {' ('}
105
+ {formatElapsed(Math.floor((Date.now() - agentProgress!.startTime) / 1000))}
106
+ {agentProgress!.tokensUsed && agentProgress!.tokensUsed > 0
107
+ ? ` · ↓ ${formatTokens(agentProgress!.tokensUsed)} tokens`
108
+ : ''}
109
+ {')'}
50
110
  </Text>
51
111
  </Box>
52
112
  )}
package/src/ui/app.tsx CHANGED
@@ -1,7 +1,6 @@
1
1
  import React, { useState, useCallback, useEffect, useRef, useMemo } from 'react'
2
2
  import { Box, Text, useInput } from 'ink'
3
3
  import TextInput from 'ink-text-input'
4
- import { readFileSync } from 'node:fs'
5
4
  import type { QueryEngine } from '../core/engine'
6
5
  import type { RemoteEngine } from '../daemon/remote-engine'
7
6
  import type { MiphamConfig } from '../shared/index.ts'
@@ -183,28 +182,18 @@ export function App({
183
182
  const [agentProgress, setAgentProgress] = useState<AgentProgress | null>(null)
184
183
  // Multi-agent tracking: keyed by agent/task ID, shows all running + recently completed agents
185
184
  const [runningAgents, setRunningAgents] = useState<Record<string, AgentEntry>>({})
186
- const [gitBranch, setGitBranch] = useState('')
187
185
  const [agentTick, setAgentTick] = useState(0)
188
-
189
- // Detect git branch on mount
190
- useEffect(() => {
191
- try {
192
- const head = readFileSync('.git/HEAD', 'utf8').trim()
193
- const m = head.match(/^ref: refs\/heads\/(.+)$/)
194
- setGitBranch(m ? m[1]! : head.slice(0, 7))
195
- } catch {
196
- // Not a git repo — leave empty
197
- }
198
- }, [])
186
+ /** Active foreground tool indicator: [Bash command...], [Update file.ts...], etc. */
187
+ const [activeTool, setActiveTool] = useState<{ name: string; detail: string } | null>(null)
199
188
 
200
189
  // Tick timer for agent elapsed displays (re-renders every second while agents are running)
201
190
  useEffect(() => {
202
191
  const hasRunning =
203
- Object.values(runningAgents).some((a) => a.status === 'running') || agentProgress !== null
192
+ Object.values(runningAgents).some((a) => a.status === 'running') || agentProgress !== null || activeTool !== null
204
193
  if (!hasRunning) return
205
194
  const i = setInterval(() => setAgentTick((t) => t + 1), 1000)
206
195
  return () => clearInterval(i)
207
- }, [runningAgents, agentProgress])
196
+ }, [runningAgents, agentProgress, activeTool])
208
197
 
209
198
  // Sync running agents from BackgroundAgentRegistry into React state.
210
199
  // Called after Agent tool results and task notifications to keep the footer current.
@@ -520,6 +509,9 @@ export function App({
520
509
  const isAgent = toolName === 'Agent' || toolName === 'Task'
521
510
  const detail = formatToolDetail(toolName, chunk.toolUse.input)
522
511
 
512
+ // Show [ToolName detail...] activity indicator for ALL tools
513
+ setActiveTool({ name: toolDisplayName(toolName), detail })
514
+
523
515
  if (isAgent) {
524
516
  setAgentProgress({
525
517
  name: detail || (chunk.toolUse.input.subagent_type as string) || 'General-purpose',
@@ -553,6 +545,7 @@ export function App({
553
545
 
554
546
  if (chunk.type === 'tool_result') {
555
547
  setAgentProgress(null)
548
+ setActiveTool(null)
556
549
  // Sync background agents (e.g. Agent tool may have spawned them)
557
550
  syncBgAgents()
558
551
  const output = chunk.content ? String(chunk.content).trim() : ''
@@ -834,11 +827,12 @@ export function App({
834
827
  </Box>
835
828
  )}
836
829
 
837
- {/* Agent status footer — shows running background agents */}
830
+ {/* Agent status footer — shows active tool indicator + inline agent progress + running background agents */}
838
831
  <AgentFooter
839
832
  agents={Object.values(runningAgents)}
840
- gitBranch={gitBranch}
841
833
  tick={agentTick}
834
+ activeTool={activeTool}
835
+ agentProgress={agentProgress}
842
836
  />
843
837
 
844
838
  {/* Status line — Claude Code style */}
package/src/ui/chat.tsx CHANGED
@@ -157,52 +157,58 @@ export function ChatPanel({ messages, focusMode }: ChatPanelProps) {
157
157
  }
158
158
 
159
159
  /**
160
- * Group consecutive tool calls into compact one-liners for the normal view.
161
- * Single tools show normally; 2+ consecutive tools get folded:
162
- * ⏺ Read · Glob · Bash (3 tools)
160
+ * Pair each tool_use message with its following tool_result message(s),
161
+ * showing every tool individually with its name, detail, and output.
162
+ *
163
+ * Claude Code parity: each tool renders as:
164
+ * ⏺ ToolName (detail) ← colored per tool type
165
+ * ⎿ output summary ← dimmed below
166
+ *
167
+ * Only true duplicates (same tool name + same detail) are collapsed
168
+ * into a single entry with a count.
163
169
  */
164
170
  function compactToolGroups(messages: ChatMessage[]): ChatMessage[] {
165
171
  const result: ChatMessage[] = []
166
- let toolGroup: ChatMessage[] = []
172
+ let current: ChatMessage | null = null
167
173
 
168
- const flush = () => {
169
- if (toolGroup.length === 0) return
170
- if (toolGroup.length === 1) {
171
- result.push(toolGroup[0]!)
172
- } else {
173
- const names = toolGroup
174
- .filter((t) => t.toolMeta?.name && !t.toolMeta.name.startsWith('tools ('))
175
- .map((t) => t.toolMeta!.name)
176
- const uniqueNames = [...new Set(names)]
177
- result.push({
178
- role: 'system',
179
- content: `⏺ ${uniqueNames.join(' · ')} (${names.length} tools)`,
180
- toolMeta: {
181
- name: `tools (${names.length})`,
182
- input: uniqueNames.join(', '),
183
- collapsed: true,
184
- },
185
- })
174
+ for (const msg of messages) {
175
+ // Tool call with a name → flush previous, start a new one
176
+ if (msg.toolMeta?.name && !msg.toolMeta.name.startsWith('tools (')) {
177
+ if (current) result.push(current)
178
+ current = { ...msg }
179
+ continue
186
180
  }
187
- toolGroup = []
188
- }
189
181
 
190
- for (const msg of messages) {
191
- // Tool results (no name, has output) get folded into the preceding tool group
182
+ // Tool result (no name, has output) → merge output into current tool
192
183
  if (msg.toolMeta && !msg.toolMeta.name && msg.toolMeta.output) {
193
- toolGroup.push(msg)
184
+ if (current) {
185
+ current = {
186
+ ...current,
187
+ content: msg.content,
188
+ toolMeta: {
189
+ ...current.toolMeta!,
190
+ output: msg.toolMeta.output,
191
+ },
192
+ }
193
+ }
194
+ // Orphan tool result (no preceding tool_use) → skip
194
195
  continue
195
196
  }
196
- // Tool calls with a name
197
- if (msg.toolMeta?.name && !msg.toolMeta.name.startsWith('tools (')) {
198
- toolGroup.push(msg)
197
+
198
+ // Already-grouped message (from a previous compact cycle) or non-tool
199
+ if (current) {
200
+ result.push(current)
201
+ current = null
202
+ }
203
+ // Skip orphan tool-result messages without a preceding tool_use
204
+ if (msg.toolMeta && !msg.toolMeta.name && msg.toolMeta.output) {
199
205
  continue
200
206
  }
201
- // Already-grouped or non-tool message
202
- flush()
203
207
  result.push(msg)
204
208
  }
205
- flush()
209
+
210
+ // Flush any remaining tool
211
+ if (current) result.push(current)
206
212
  return result
207
213
  }
208
214