@miphamai/cli 0.65.0 → 0.66.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.
- package/package.json +1 -1
- package/src/agent/background-registry.ts +12 -0
- package/src/agent/sub-agent.ts +12 -2
- package/src/agent/types.ts +2 -0
- package/src/shared/package-info.ts +1 -1
- package/src/tools/exec/git.ts +55 -1
- package/src/ui/agent-footer.tsx +14 -3
- package/src/ui/app.tsx +19 -3
package/package.json
CHANGED
|
@@ -20,6 +20,8 @@ export interface BackgroundTask {
|
|
|
20
20
|
status: BackgroundTaskStatus
|
|
21
21
|
kind: BackgroundTaskKind
|
|
22
22
|
startedAt: Date
|
|
23
|
+
/** Cumulative token usage (input + output) reported by the running task. */
|
|
24
|
+
tokensUsed: number
|
|
23
25
|
completedAt?: Date
|
|
24
26
|
result?: string
|
|
25
27
|
error?: string
|
|
@@ -99,6 +101,7 @@ export class BackgroundAgentRegistry {
|
|
|
99
101
|
status: 'running',
|
|
100
102
|
kind,
|
|
101
103
|
startedAt: now,
|
|
104
|
+
tokensUsed: 0,
|
|
102
105
|
abortController: new AbortController(),
|
|
103
106
|
}
|
|
104
107
|
|
|
@@ -177,6 +180,15 @@ export class BackgroundAgentRegistry {
|
|
|
177
180
|
return this.list().filter((t) => t.status === 'running')
|
|
178
181
|
}
|
|
179
182
|
|
|
183
|
+
/**
|
|
184
|
+
* Update the cumulative token usage reported by a running task.
|
|
185
|
+
* No-op if the task no longer exists.
|
|
186
|
+
*/
|
|
187
|
+
updateTokenUsage(id: string, tokensUsed: number): void {
|
|
188
|
+
const task = this.tasks.get(id)
|
|
189
|
+
if (task) task.tokensUsed = tokensUsed
|
|
190
|
+
}
|
|
191
|
+
|
|
180
192
|
/**
|
|
181
193
|
* Stop a running task by aborting its controller.
|
|
182
194
|
* Returns true if the task was running and was stopped.
|
package/src/agent/sub-agent.ts
CHANGED
|
@@ -73,8 +73,13 @@ export class SubAgent {
|
|
|
73
73
|
const bgRegistry = getBackgroundAgentRegistry()
|
|
74
74
|
|
|
75
75
|
const taskId = bgRegistry.spawn(description, agentType, async (signal) => {
|
|
76
|
-
// Run the synchronous execution inside the background executor
|
|
77
|
-
|
|
76
|
+
// Run the synchronous execution inside the background executor, reporting
|
|
77
|
+
// cumulative token usage back to the registry for live footer display.
|
|
78
|
+
const opts: SubAgentOptions = {
|
|
79
|
+
...options,
|
|
80
|
+
onTokenUsage: (total) => bgRegistry.updateTokenUsage(taskId, total),
|
|
81
|
+
}
|
|
82
|
+
return this.runExecution(prompt, opts, signal)
|
|
78
83
|
})
|
|
79
84
|
|
|
80
85
|
// Register completion callback for hook firing
|
|
@@ -323,6 +328,7 @@ export class SubAgent {
|
|
|
323
328
|
|
|
324
329
|
let currentMessages = messages
|
|
325
330
|
let currentSystemPrompt = systemPrompt
|
|
331
|
+
let totalTokens = 0
|
|
326
332
|
|
|
327
333
|
try {
|
|
328
334
|
for (let turn = 0; turn < MAX_TOOL_TURNS; turn++) {
|
|
@@ -366,6 +372,10 @@ export class SubAgent {
|
|
|
366
372
|
if (chunk.type === 'stop') {
|
|
367
373
|
break
|
|
368
374
|
}
|
|
375
|
+
if (chunk.type === 'usage') {
|
|
376
|
+
totalTokens += (chunk.inputTokens ?? 0) + (chunk.outputTokens ?? 0)
|
|
377
|
+
if (options.onTokenUsage) options.onTokenUsage(totalTokens)
|
|
378
|
+
}
|
|
369
379
|
}
|
|
370
380
|
|
|
371
381
|
chunks.push(turnText)
|
package/src/agent/types.ts
CHANGED
|
@@ -46,6 +46,8 @@ export interface SubAgentOptions {
|
|
|
46
46
|
runInBackground?: boolean
|
|
47
47
|
/** Optional callback for streaming progress chunks during background execution. */
|
|
48
48
|
onProgress?: (chunk: string) => void
|
|
49
|
+
/** Optional callback reporting cumulative token usage (input + output) from the API stream. */
|
|
50
|
+
onTokenUsage?: (totalTokens: number) => void
|
|
49
51
|
/** When set, tool executions use this path as cwd (git worktree isolation). */
|
|
50
52
|
worktreePath?: string
|
|
51
53
|
/** Seed the sub-agent with a parent conversation prefix (e.g., fork inheritance). */
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
export const PACKAGE_NAME = '@miphamai/cli' as const
|
|
10
10
|
|
|
11
11
|
/** 当前发布版本 */
|
|
12
|
-
export const PACKAGE_VERSION = '0.
|
|
12
|
+
export const PACKAGE_VERSION = '0.66.0' as const
|
|
13
13
|
|
|
14
14
|
/** npm install 全局安装命令 */
|
|
15
15
|
export const NPM_INSTALL_COMMAND = `npm install -g ${PACKAGE_NAME}` as const
|
package/src/tools/exec/git.ts
CHANGED
|
@@ -75,6 +75,60 @@ function isOutsideWorktree(command: string, cwd: string): string | null {
|
|
|
75
75
|
return null
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
+
/**
|
|
79
|
+
* Split a git command string into argv tokens, honoring shell-style quoting
|
|
80
|
+
* (single quotes, double quotes, and backslash escapes). Unlike a naive
|
|
81
|
+
* `split(/\s+/)`, this keeps `-m "multi word message"` as a single argument
|
|
82
|
+
* instead of splitting it into separate tokens.
|
|
83
|
+
*/
|
|
84
|
+
export function splitCommand(command: string): string[] {
|
|
85
|
+
const tokens: string[] = []
|
|
86
|
+
let current = ''
|
|
87
|
+
let inSingle = false
|
|
88
|
+
let inDouble = false
|
|
89
|
+
|
|
90
|
+
for (let i = 0; i < command.length; i++) {
|
|
91
|
+
const ch = command[i]!
|
|
92
|
+
|
|
93
|
+
if (inSingle) {
|
|
94
|
+
if (ch === "'") inSingle = false
|
|
95
|
+
else current += ch
|
|
96
|
+
continue
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (inDouble) {
|
|
100
|
+
if (ch === '"') inDouble = false
|
|
101
|
+
else if (ch === '\\' && i + 1 < command.length && '$`"\\'.includes(command[i + 1]!)) {
|
|
102
|
+
current += command[++i]!
|
|
103
|
+
} else {
|
|
104
|
+
current += ch
|
|
105
|
+
}
|
|
106
|
+
continue
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (ch === ' ' || ch === '\t' || ch === '\n' || ch === '\r') {
|
|
110
|
+
if (current !== '') {
|
|
111
|
+
tokens.push(current)
|
|
112
|
+
current = ''
|
|
113
|
+
}
|
|
114
|
+
continue
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (ch === "'") {
|
|
118
|
+
inSingle = true
|
|
119
|
+
} else if (ch === '"') {
|
|
120
|
+
inDouble = true
|
|
121
|
+
} else if (ch === '\\' && i + 1 < command.length) {
|
|
122
|
+
current += command[++i]!
|
|
123
|
+
} else {
|
|
124
|
+
current += ch
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (current !== '') tokens.push(current)
|
|
129
|
+
return tokens
|
|
130
|
+
}
|
|
131
|
+
|
|
78
132
|
export const gitTool: ToolDefinition = {
|
|
79
133
|
name: 'Git',
|
|
80
134
|
description: 'Execute git commands. Dangerous operations (force push, hard reset) are blocked.',
|
|
@@ -115,7 +169,7 @@ export const gitTool: ToolDefinition = {
|
|
|
115
169
|
}
|
|
116
170
|
|
|
117
171
|
try {
|
|
118
|
-
const proc = Bun.spawn(['git', ...command
|
|
172
|
+
const proc = Bun.spawn(['git', ...splitCommand(command)], {
|
|
119
173
|
cwd: ctx.cwd,
|
|
120
174
|
stdout: 'pipe',
|
|
121
175
|
stderr: 'pipe',
|
package/src/ui/agent-footer.tsx
CHANGED
|
@@ -101,6 +101,9 @@ function rotatingGerund(tick: number): string {
|
|
|
101
101
|
return GERUNDS[Math.floor(tick / 2) % GERUNDS.length]!
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
+
/** Max agent lines to render before collapsing the rest into "↓ N more". */
|
|
105
|
+
const MAX_AGENT_LINES = 5
|
|
106
|
+
|
|
104
107
|
export function AgentFooter({ agents, tick, activeTool, agentProgress }: AgentFooterProps) {
|
|
105
108
|
const { t } = useI18n()
|
|
106
109
|
const hasAgents = agents.length > 0
|
|
@@ -118,6 +121,7 @@ export function AgentFooter({ agents, tick, activeTool, agentProgress }: AgentFo
|
|
|
118
121
|
{' '}[{activeTool!.name} {activeTool!.detail.slice(0, 60)}
|
|
119
122
|
{animatedDots(tick)}]
|
|
120
123
|
</Text>
|
|
124
|
+
<Box flexGrow={1} />
|
|
121
125
|
<Text dimColor>
|
|
122
126
|
{' '}
|
|
123
127
|
{formatElapsed(Math.floor((Date.now() - activeTool!.startTime) / 1000))}
|
|
@@ -131,6 +135,7 @@ export function AgentFooter({ agents, tick, activeTool, agentProgress }: AgentFo
|
|
|
131
135
|
<Text color="#FFA500">
|
|
132
136
|
{' '}✻ {rotatingGerund(tick)}…
|
|
133
137
|
</Text>
|
|
138
|
+
<Box flexGrow={1} />
|
|
134
139
|
<Text dimColor>
|
|
135
140
|
{' ('}
|
|
136
141
|
{formatElapsed(Math.floor((Date.now() - agentProgress!.startTime) / 1000))}
|
|
@@ -142,8 +147,8 @@ export function AgentFooter({ agents, tick, activeTool, agentProgress }: AgentFo
|
|
|
142
147
|
</Box>
|
|
143
148
|
)}
|
|
144
149
|
|
|
145
|
-
{/* Agent status lines */}
|
|
146
|
-
{agents.map((agent) => {
|
|
150
|
+
{/* Agent status lines — collapse to MAX_AGENT_LINES + "↓ N more" */}
|
|
151
|
+
{agents.slice(0, MAX_AGENT_LINES).map((agent) => {
|
|
147
152
|
const elapsed = Math.floor((Date.now() - agent.startTime) / 1000)
|
|
148
153
|
const isRunning = agent.status === 'running'
|
|
149
154
|
const symbol = isRunning ? '◯' : '◼'
|
|
@@ -163,7 +168,8 @@ export function AgentFooter({ agents, tick, activeTool, agentProgress }: AgentFo
|
|
|
163
168
|
{agent.name}
|
|
164
169
|
</Text>
|
|
165
170
|
<Text dimColor> {desc}</Text>
|
|
166
|
-
{/* Spacer pushes
|
|
171
|
+
{/* Spacer pushes elapsed + tokens to the right edge */}
|
|
172
|
+
<Box flexGrow={1} />
|
|
167
173
|
<Text dimColor>
|
|
168
174
|
{' '}
|
|
169
175
|
{isRunning ? formatElapsed(elapsed) : t('ui.agent.finished')}
|
|
@@ -177,6 +183,11 @@ export function AgentFooter({ agents, tick, activeTool, agentProgress }: AgentFo
|
|
|
177
183
|
</Box>
|
|
178
184
|
)
|
|
179
185
|
})}
|
|
186
|
+
{agents.length > MAX_AGENT_LINES && (
|
|
187
|
+
<Text dimColor>
|
|
188
|
+
{' '}↓ {agents.length - MAX_AGENT_LINES} more
|
|
189
|
+
</Text>
|
|
190
|
+
)}
|
|
180
191
|
</Box>
|
|
181
192
|
)
|
|
182
193
|
}
|
package/src/ui/app.tsx
CHANGED
|
@@ -270,7 +270,23 @@ export function App({
|
|
|
270
270
|
agentProgressRef.current !== null ||
|
|
271
271
|
activeToolRef.current !== null
|
|
272
272
|
if (!hasRunning) return
|
|
273
|
-
const i = setInterval(() =>
|
|
273
|
+
const i = setInterval(() => {
|
|
274
|
+
setAgentTick((t) => t + 1)
|
|
275
|
+
// Refresh live token counts for running background agents.
|
|
276
|
+
setRunningAgents((prev) => {
|
|
277
|
+
const bg = getBackgroundAgentRegistry()
|
|
278
|
+
let changed = false
|
|
279
|
+
const next = { ...prev }
|
|
280
|
+
for (const task of bg.list()) {
|
|
281
|
+
const cur = next[task.id]
|
|
282
|
+
if (cur && cur.status === 'running' && cur.tokensUsed !== task.tokensUsed) {
|
|
283
|
+
next[task.id] = { ...cur, tokensUsed: task.tokensUsed }
|
|
284
|
+
changed = true
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
return changed ? next : prev
|
|
288
|
+
})
|
|
289
|
+
}, 1000)
|
|
274
290
|
return () => clearInterval(i)
|
|
275
291
|
}, [runningAgents, agentProgress, activeTool])
|
|
276
292
|
|
|
@@ -282,14 +298,14 @@ export function App({
|
|
|
282
298
|
setRunningAgents((prev) => {
|
|
283
299
|
const next: Record<string, AgentEntry> = {}
|
|
284
300
|
for (const task of all) {
|
|
285
|
-
// Preserve existing entry
|
|
301
|
+
// Preserve existing entry for metadata; tokens come from the registry task.
|
|
286
302
|
const existing = prev[task.id]
|
|
287
303
|
next[task.id] = {
|
|
288
304
|
id: task.id,
|
|
289
305
|
name: existing?.name || task.agentType.charAt(0).toUpperCase() + task.agentType.slice(1),
|
|
290
306
|
description: existing?.description || task.description,
|
|
291
307
|
startTime: existing?.startTime || task.startedAt.getTime(),
|
|
292
|
-
tokensUsed:
|
|
308
|
+
tokensUsed: task.tokensUsed ?? 0,
|
|
293
309
|
status: task.status === 'running' ? 'running' : 'completed',
|
|
294
310
|
}
|
|
295
311
|
}
|