@miphamai/cli 0.45.0 → 0.46.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/shared/package-info.ts +1 -1
- package/src/tools/exec/task.ts +6 -1
- package/src/tools/file/grep.ts +60 -19
- package/src/ui/app.tsx +12 -5
- package/src/ui/chat.tsx +4 -0
- package/src/ui/goal-progress.tsx +137 -0
package/package.json
CHANGED
|
@@ -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.46.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/task.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ToolDefinition } from '../../shared/index.ts'
|
|
2
2
|
|
|
3
|
-
interface Task {
|
|
3
|
+
export interface Task {
|
|
4
4
|
id: string
|
|
5
5
|
subject: string
|
|
6
6
|
description: string
|
|
@@ -20,6 +20,11 @@ interface Task {
|
|
|
20
20
|
const tasks = new Map<string, Task>()
|
|
21
21
|
let taskCounter = 0
|
|
22
22
|
|
|
23
|
+
/** Read accessor for the session task store — returns all non-deleted tasks. */
|
|
24
|
+
export function getTasks(): Task[] {
|
|
25
|
+
return Array.from(tasks.values()).filter((t) => t.status !== 'deleted')
|
|
26
|
+
}
|
|
27
|
+
|
|
23
28
|
function formatTask(t: Task): string {
|
|
24
29
|
const meta = t.metadata ? ` ${JSON.stringify(t.metadata)}` : ''
|
|
25
30
|
const blocks = t.blocks.length ? ` blocks:[${t.blocks.join(',')}]` : ''
|
package/src/tools/file/grep.ts
CHANGED
|
@@ -1,7 +1,28 @@
|
|
|
1
|
-
import { $ } from 'bun'
|
|
2
1
|
import type { ToolDefinition } from '../../shared/index.ts'
|
|
3
2
|
import { resolveSafe } from '../../security/path'
|
|
4
3
|
|
|
4
|
+
/** Grep stall guard: a search over a huge tree (or a pathological regex) must
|
|
5
|
+
* not hang the session for minutes. Matches the Bash tool's 120s ceiling. */
|
|
6
|
+
const GREP_TIMEOUT_MS = 120_000
|
|
7
|
+
|
|
8
|
+
/** Run a search command with a kill-timer; report whether the timeout fired. */
|
|
9
|
+
export async function runSearch(
|
|
10
|
+
cmd: string[],
|
|
11
|
+
cwd: string,
|
|
12
|
+
timeoutMs: number,
|
|
13
|
+
): Promise<{ stdout: string; timedOut: boolean; exitCode: number | null }> {
|
|
14
|
+
const proc = Bun.spawn(cmd, { cwd, stdout: 'pipe', stderr: 'pipe' })
|
|
15
|
+
let timedOut = false
|
|
16
|
+
const timer = setTimeout(() => {
|
|
17
|
+
timedOut = true
|
|
18
|
+
proc.kill()
|
|
19
|
+
}, timeoutMs)
|
|
20
|
+
const stdout = await new Response(proc.stdout).text()
|
|
21
|
+
await proc.exited
|
|
22
|
+
clearTimeout(timer)
|
|
23
|
+
return { stdout, timedOut, exitCode: proc.exitCode }
|
|
24
|
+
}
|
|
25
|
+
|
|
5
26
|
export const grepTool: ToolDefinition = {
|
|
6
27
|
name: 'Grep',
|
|
7
28
|
description:
|
|
@@ -29,32 +50,52 @@ export const grepTool: ToolDefinition = {
|
|
|
29
50
|
const searchPath = resolveSafe(ctx.cwd, (params.path as string) || '.')
|
|
30
51
|
const include = params.include as string | undefined
|
|
31
52
|
|
|
53
|
+
// 1. ripgrep (fast path)
|
|
54
|
+
const rgArgs = ['rg', '-n', '--heading', '--color=never', '-M', '500', pattern]
|
|
55
|
+
if (include) rgArgs.push('--glob', include)
|
|
56
|
+
rgArgs.push(searchPath)
|
|
32
57
|
try {
|
|
33
|
-
const
|
|
34
|
-
if (
|
|
35
|
-
args.push(searchPath)
|
|
36
|
-
|
|
37
|
-
const result = await $`rg ${args}`.cwd(ctx.cwd).quiet().text()
|
|
38
|
-
return { success: true, content: result || '(no matches)' }
|
|
39
|
-
} catch (err: unknown) {
|
|
40
|
-
const exitErr = err as { exitCode?: number }
|
|
41
|
-
if (exitErr.exitCode === 1) {
|
|
42
|
-
return { success: true, content: '(no matches)' }
|
|
43
|
-
}
|
|
44
|
-
// Fallback to grep
|
|
45
|
-
try {
|
|
46
|
-
const content = await $`grep -rn ${pattern} ${searchPath}`.cwd(ctx.cwd).quiet().text()
|
|
58
|
+
const { stdout, timedOut, exitCode } = await runSearch(rgArgs, ctx.cwd, GREP_TIMEOUT_MS)
|
|
59
|
+
if (timedOut) {
|
|
47
60
|
return {
|
|
48
|
-
success:
|
|
49
|
-
content:
|
|
61
|
+
success: false,
|
|
62
|
+
content: '',
|
|
63
|
+
error: `Grep timed out after ${GREP_TIMEOUT_MS / 1000}s — narrow scope with "path" and "include".`,
|
|
50
64
|
}
|
|
51
|
-
}
|
|
65
|
+
}
|
|
66
|
+
if (exitCode === 1) return { success: true, content: '(no matches)' }
|
|
67
|
+
if (exitCode === 0) return { success: true, content: stdout || '(no matches)' }
|
|
68
|
+
// rg exit 2 (error) or null (killed) → fall through to grep
|
|
69
|
+
} catch {
|
|
70
|
+
// rg not installed → fall through to grep
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// 2. fallback to plain grep
|
|
74
|
+
const grepArgs = ['grep', '-rn', pattern, searchPath]
|
|
75
|
+
try {
|
|
76
|
+
const { stdout, timedOut, exitCode } = await runSearch(grepArgs, ctx.cwd, GREP_TIMEOUT_MS)
|
|
77
|
+
if (timedOut) {
|
|
52
78
|
return {
|
|
53
79
|
success: false,
|
|
54
80
|
content: '',
|
|
55
|
-
error:
|
|
81
|
+
error: `Grep timed out after ${GREP_TIMEOUT_MS / 1000}s — narrow scope with "path" and "include".`,
|
|
56
82
|
}
|
|
57
83
|
}
|
|
84
|
+
if (exitCode === 1) return { success: true, content: '(no matches)' }
|
|
85
|
+
if (exitCode === 0) {
|
|
86
|
+
return { success: true, content: stdout.slice(0, 50000) || '(no matches)' }
|
|
87
|
+
}
|
|
88
|
+
return {
|
|
89
|
+
success: false,
|
|
90
|
+
content: '',
|
|
91
|
+
error: 'grep failed. Install ripgrep: brew install ripgrep',
|
|
92
|
+
}
|
|
93
|
+
} catch {
|
|
94
|
+
return {
|
|
95
|
+
success: false,
|
|
96
|
+
content: '',
|
|
97
|
+
error: 'grep failed. Install ripgrep: brew install ripgrep',
|
|
98
|
+
}
|
|
58
99
|
}
|
|
59
100
|
},
|
|
60
101
|
}
|
package/src/ui/app.tsx
CHANGED
|
@@ -20,6 +20,7 @@ import { AgentFooter, type AgentEntry } from './agent-footer'
|
|
|
20
20
|
import { AgentViewDashboard } from '../agent-view/dashboard'
|
|
21
21
|
import type { AgentViewManager } from '../agent-view/agent-view-manager'
|
|
22
22
|
import { WorkflowProgress } from './workflow-progress.js'
|
|
23
|
+
import { GoalProgress } from './goal-progress.js'
|
|
23
24
|
import {
|
|
24
25
|
getCommand,
|
|
25
26
|
looksLikeSlashCommand,
|
|
@@ -828,6 +829,17 @@ export function App({
|
|
|
828
829
|
{/* Workflow progress — auto-detects active workflows, renders nothing when idle */}
|
|
829
830
|
<WorkflowProgress />
|
|
830
831
|
|
|
832
|
+
{/* Goal progress — live goal + subtasks + done/total · elapsed · tokens (auto-hides when no goal) */}
|
|
833
|
+
<GoalProgress
|
|
834
|
+
goal={goalText}
|
|
835
|
+
getTokens={() => {
|
|
836
|
+
const tracker = engine.getUsageTracker()
|
|
837
|
+
if (!tracker) return 0
|
|
838
|
+
if ('totalApiTokens' in tracker) return tracker.totalApiTokens
|
|
839
|
+
return Object.values(tracker.getStats()).reduce((a, b) => a + b, 0)
|
|
840
|
+
}}
|
|
841
|
+
/>
|
|
842
|
+
|
|
831
843
|
{/* Agent View Dashboard — Ctrl+G overlay (replaces chat + input) */}
|
|
832
844
|
{agentViewOpen && agentViewManager ? (
|
|
833
845
|
<AgentViewDashboard
|
|
@@ -935,11 +947,6 @@ export function App({
|
|
|
935
947
|
|
|
936
948
|
{/* Status line — Claude Code style */}
|
|
937
949
|
<Box marginTop={1} flexDirection="column">
|
|
938
|
-
{goalText && (
|
|
939
|
-
<Box>
|
|
940
|
-
<Text color="green">🎯 Goal: {goalText}</Text>
|
|
941
|
-
</Box>
|
|
942
|
-
)}
|
|
943
950
|
<Box flexDirection="row">
|
|
944
951
|
<Text color={PERMISSION_COLORS[permissionMode]}>
|
|
945
952
|
⏵⏵ {PERMISSION_LABELS[permissionMode]}
|
package/src/ui/chat.tsx
CHANGED
|
@@ -83,6 +83,10 @@ const MessageRow = React.memo(
|
|
|
83
83
|
<Text bold color="green">
|
|
84
84
|
▸ {displayCwd()}:
|
|
85
85
|
</Text>
|
|
86
|
+
) : msg.role === 'assistant' ? (
|
|
87
|
+
<Text bold color="magenta">
|
|
88
|
+
◆ {t('ui.assistant.role_label')}:
|
|
89
|
+
</Text>
|
|
86
90
|
) : msg.role === 'system' ? (
|
|
87
91
|
<Text bold color="yellow">
|
|
88
92
|
⚠ {t('ui.system.role_label')}:
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { useState, useEffect, useRef } from 'react'
|
|
2
|
+
import { Box, Text } from 'ink'
|
|
3
|
+
import { getTasks } from '../tools/exec/task.js'
|
|
4
|
+
import type { Task } from '../tools/exec/task.js'
|
|
5
|
+
|
|
6
|
+
export interface GoalProgressProps {
|
|
7
|
+
goal: string
|
|
8
|
+
/** Read current session token total; defaults to 0. */
|
|
9
|
+
getTokens?: () => number
|
|
10
|
+
/** Read current tasks; defaults to the real task store. */
|
|
11
|
+
listTasks?: () => Task[]
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface TaskSummary {
|
|
15
|
+
total: number
|
|
16
|
+
done: number
|
|
17
|
+
inProgress: number
|
|
18
|
+
pending: number
|
|
19
|
+
failed: number
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Count tasks by status, ignoring deleted. */
|
|
23
|
+
export function summarizeTasks(tasks: Task[]): TaskSummary {
|
|
24
|
+
const summary: TaskSummary = { total: 0, done: 0, inProgress: 0, pending: 0, failed: 0 }
|
|
25
|
+
for (const t of tasks) {
|
|
26
|
+
if (t.status === 'deleted') continue
|
|
27
|
+
summary.total++
|
|
28
|
+
if (t.status === 'completed') summary.done++
|
|
29
|
+
else if (t.status === 'in_progress') summary.inProgress++
|
|
30
|
+
else if (t.status === 'pending') summary.pending++
|
|
31
|
+
else if (t.status === 'failed') summary.failed++
|
|
32
|
+
}
|
|
33
|
+
return summary
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Format a token count: raw under 1000, `1.2K` style above. */
|
|
37
|
+
export function formatTokens(n: number): string {
|
|
38
|
+
if (n >= 1000) return `${(n / 1000).toFixed(1)}K`
|
|
39
|
+
return String(n)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function formatElapsed(ms: number): string {
|
|
43
|
+
const s = Math.floor(ms / 1000)
|
|
44
|
+
if (s < 60) return `${s}s`
|
|
45
|
+
const m = Math.floor(s / 60)
|
|
46
|
+
return `${m}m ${s % 60}s`
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function taskIcon(status: Task['status']): { icon: string; color?: string; dim?: boolean } {
|
|
50
|
+
switch (status) {
|
|
51
|
+
case 'completed':
|
|
52
|
+
return { icon: '✓', color: 'green' }
|
|
53
|
+
case 'in_progress':
|
|
54
|
+
return { icon: '⏳', color: 'yellow' }
|
|
55
|
+
case 'failed':
|
|
56
|
+
return { icon: '✗', color: 'red' }
|
|
57
|
+
default:
|
|
58
|
+
return { icon: '📋', dim: true }
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Live goal progress panel — mirrors ZCode's goal view: title + subtasks + done/total · elapsed · tokens. */
|
|
63
|
+
export function GoalProgress({ goal, getTokens, listTasks }: GoalProgressProps) {
|
|
64
|
+
const [tasks, setTasks] = useState<Task[]>([])
|
|
65
|
+
const [tokens, setTokens] = useState(0)
|
|
66
|
+
const [elapsedMs, setElapsedMs] = useState(0)
|
|
67
|
+
|
|
68
|
+
// Hold latest callbacks in refs so the interval doesn't churn on re-render.
|
|
69
|
+
const listTasksRef = useRef(listTasks ?? getTasks)
|
|
70
|
+
const getTokensRef = useRef(getTokens ?? (() => 0))
|
|
71
|
+
listTasksRef.current = listTasks ?? getTasks
|
|
72
|
+
getTokensRef.current = getTokens ?? (() => 0)
|
|
73
|
+
|
|
74
|
+
useEffect(() => {
|
|
75
|
+
if (!goal) {
|
|
76
|
+
setTasks([])
|
|
77
|
+
setTokens(0)
|
|
78
|
+
setElapsedMs(0)
|
|
79
|
+
return
|
|
80
|
+
}
|
|
81
|
+
const start = Date.now()
|
|
82
|
+
const tick = () => {
|
|
83
|
+
setTasks(listTasksRef.current())
|
|
84
|
+
setTokens(getTokensRef.current())
|
|
85
|
+
setElapsedMs(Date.now() - start)
|
|
86
|
+
}
|
|
87
|
+
tick()
|
|
88
|
+
const timer = setInterval(tick, 1000)
|
|
89
|
+
return () => clearInterval(timer)
|
|
90
|
+
}, [goal])
|
|
91
|
+
|
|
92
|
+
if (!goal) return null
|
|
93
|
+
|
|
94
|
+
const summary = summarizeTasks(tasks)
|
|
95
|
+
const visibleTasks = tasks.filter((t) => t.status !== 'deleted')
|
|
96
|
+
|
|
97
|
+
return (
|
|
98
|
+
<Box
|
|
99
|
+
flexDirection="column"
|
|
100
|
+
borderStyle="round"
|
|
101
|
+
borderColor="cyan"
|
|
102
|
+
paddingX={1}
|
|
103
|
+
marginBottom={1}
|
|
104
|
+
>
|
|
105
|
+
<Text bold color="green">
|
|
106
|
+
🎯 Goal: {goal}
|
|
107
|
+
</Text>
|
|
108
|
+
|
|
109
|
+
{visibleTasks.length === 0 ? (
|
|
110
|
+
<Text dimColor> (no subtasks yet)</Text>
|
|
111
|
+
) : (
|
|
112
|
+
visibleTasks.map((t) => {
|
|
113
|
+
const meta = taskIcon(t.status)
|
|
114
|
+
return (
|
|
115
|
+
<Box key={t.id} marginLeft={1}>
|
|
116
|
+
<Text color={meta.color} dimColor={meta.dim}>
|
|
117
|
+
{meta.icon} {t.subject}
|
|
118
|
+
</Text>
|
|
119
|
+
</Box>
|
|
120
|
+
)
|
|
121
|
+
})
|
|
122
|
+
)}
|
|
123
|
+
|
|
124
|
+
<Box marginTop={1}>
|
|
125
|
+
<Text dimColor>
|
|
126
|
+
═══ {summary.done}/{summary.total} done
|
|
127
|
+
{summary.inProgress > 0 && ` · ${summary.inProgress} in progress`}
|
|
128
|
+
{summary.failed > 0 && ` · ${summary.failed} failed`}
|
|
129
|
+
{' · '}
|
|
130
|
+
{formatElapsed(elapsedMs)}
|
|
131
|
+
{tokens > 0 && ` · ${formatTokens(tokens)} tokens`}
|
|
132
|
+
{' ═══'}
|
|
133
|
+
</Text>
|
|
134
|
+
</Box>
|
|
135
|
+
</Box>
|
|
136
|
+
)
|
|
137
|
+
}
|