@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@miphamai/cli",
3
- "version": "0.45.0",
3
+ "version": "0.46.0",
4
4
  "description": "Mipham Code — Multi-model open-core intelligent coding terminal by MiphamAI",
5
5
  "keywords": [
6
6
  "ai",
@@ -9,7 +9,7 @@
9
9
  export const PACKAGE_NAME = '@miphamai/cli' as const
10
10
 
11
11
  /** 当前发布版本 */
12
- export const PACKAGE_VERSION = '0.45.0' as const
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
@@ -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(',')}]` : ''
@@ -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 args = ['-n', '--heading', '--color=never', '-M', '500', pattern]
34
- if (include) args.push('--glob', include)
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: true,
49
- content: content.slice(0, 50000) || '(no matches)',
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
- } catch {
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: 'grep failed. Install ripgrep: brew install ripgrep',
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
+ }