@miphamai/cli 0.9.0 → 0.11.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.
@@ -4,13 +4,17 @@ interface Task {
4
4
  id: string
5
5
  subject: string
6
6
  description: string
7
- status: 'pending' | 'in_progress' | 'completed' | 'deleted'
7
+ status: 'pending' | 'in_progress' | 'completed' | 'deleted' | 'failed'
8
8
  activeForm?: string
9
9
  metadata?: Record<string, unknown>
10
10
  blocks: string[]
11
11
  blockedBy: string[]
12
12
  owner?: string
13
13
  createdAt: string
14
+ /** Background task output content (set when a background agent completes). */
15
+ output?: string
16
+ /** Background task error message (set when a background agent fails). */
17
+ outputError?: string
14
18
  }
15
19
 
16
20
  const tasks = new Map<string, Task>()
@@ -20,14 +24,33 @@ function formatTask(t: Task): string {
20
24
  const meta = t.metadata ? ` ${JSON.stringify(t.metadata)}` : ''
21
25
  const blocks = t.blocks.length ? ` blocks:[${t.blocks.join(',')}]` : ''
22
26
  const blocked = t.blockedBy.length ? ` waits:[${t.blockedBy.join(',')}]` : ''
23
- return `[${t.status}] #${t.id}: ${t.subject}${blocks}${blocked}${meta}`
27
+ const owner = t.owner ? ` @${t.owner}` : ''
28
+ return `[${t.status}] #${t.id}: ${t.subject}${owner}${blocks}${blocked}${meta}`
29
+ }
30
+
31
+ /** Check if a task is blocked — has unresolved dependencies. */
32
+ function isBlocked(task: Task): boolean {
33
+ if (task.blockedBy.length === 0) return false
34
+ return task.blockedBy.some((depId) => {
35
+ const dep = tasks.get(depId)
36
+ return dep && dep.status !== 'completed' && dep.status !== 'deleted'
37
+ })
38
+ }
39
+
40
+ /** Get the list of blocking task IDs (unresolved dependencies). */
41
+ function getBlockingIds(task: Task): string[] {
42
+ return task.blockedBy.filter((depId) => {
43
+ const dep = tasks.get(depId)
44
+ return dep && dep.status !== 'completed' && dep.status !== 'deleted'
45
+ })
24
46
  }
25
47
 
26
48
  export const taskTool: ToolDefinition = {
27
49
  name: 'Task',
28
50
  description:
29
51
  'Create and manage structured task lists for tracking progress. ' +
30
- 'Supports CRUD, dependencies (blocks/blockedBy), metadata, owner assignment, and status workflow: pending → in_progress → completed. ' +
52
+ 'Supports CRUD, dependencies (blocks/blockedBy), metadata, owner assignment, ' +
53
+ 'background task output/stop, and status workflow: pending → in_progress → completed. ' +
31
54
  'Use for complex multi-step tasks, session tracking, and organizing work.',
32
55
  category: 'exec',
33
56
  permission: 'auto',
@@ -36,9 +59,9 @@ export const taskTool: ToolDefinition = {
36
59
  properties: {
37
60
  action: {
38
61
  type: 'string',
39
- enum: ['create', 'list', 'update', 'get', 'delete'],
62
+ enum: ['create', 'list', 'update', 'get', 'delete', 'output', 'stop'],
40
63
  description:
41
- 'Action: create a task, list all, update status/fields, get one by ID, or delete.',
64
+ 'Action: create a task, list all, update status/fields, get one by ID, delete, view output of background task, or stop a running task.',
42
65
  },
43
66
  subject: { type: 'string', description: 'A brief, actionable title (for create/update).' },
44
67
  description: { type: 'string', description: 'What needs to be done (for create/update).' },
@@ -46,21 +69,24 @@ export const taskTool: ToolDefinition = {
46
69
  type: 'string',
47
70
  description: 'Present continuous form shown during work (for create/update).',
48
71
  },
49
- taskId: { type: 'string', description: 'Task ID (for update/get/delete).' },
72
+ taskId: { type: 'string', description: 'Task ID (for update/get/delete/output/stop).' },
50
73
  status: {
51
74
  type: 'string',
52
- enum: ['pending', 'in_progress', 'completed', 'deleted'],
53
- description: 'New status. "deleted" permanently removes the task.',
75
+ enum: ['pending', 'in_progress', 'completed', 'deleted', 'failed'],
76
+ description:
77
+ 'New status. "deleted" permanently removes the task, "failed" marks as failed.',
54
78
  },
55
79
  addBlocks: {
56
80
  type: 'array',
57
81
  items: { type: 'string' },
58
- description: 'Task IDs that this task blocks (they depend on this one).',
82
+ description:
83
+ 'Task IDs that this task blocks (they depend on this one). Use for create or update.',
59
84
  },
60
85
  addBlockedBy: {
61
86
  type: 'array',
62
87
  items: { type: 'string' },
63
- description: 'Task IDs that block this task (this one depends on them).',
88
+ description:
89
+ 'Task IDs that block this task (this one depends on them). Use for create or update.',
64
90
  },
65
91
  owner: { type: 'string', description: 'Agent or user name assigned to this task.' },
66
92
  metadata: {
@@ -90,8 +116,24 @@ export const taskTool: ToolDefinition = {
90
116
  owner: (params.owner as string) || undefined,
91
117
  createdAt: new Date().toISOString(),
92
118
  }
119
+
120
+ // Accept initial dependencies at creation time
121
+ if (Array.isArray(params.addBlocks)) {
122
+ for (const bid of params.addBlocks as string[]) {
123
+ if (!task.blocks.includes(bid)) task.blocks.push(bid)
124
+ }
125
+ }
126
+ if (Array.isArray(params.addBlockedBy)) {
127
+ for (const bid of params.addBlockedBy as string[]) {
128
+ if (!task.blockedBy.includes(bid)) task.blockedBy.push(bid)
129
+ }
130
+ }
131
+
93
132
  tasks.set(id, task)
94
- return { success: true, content: `Task #${id} created: ${subject}` }
133
+ const blockedNote = isBlocked(task)
134
+ ? ` (blocked — waiting on: ${getBlockingIds(task).join(', ')})`
135
+ : ''
136
+ return { success: true, content: `Task #${id} created: ${subject}${blockedNote}` }
95
137
  }
96
138
 
97
139
  // ── LIST ──
@@ -99,18 +141,63 @@ export const taskTool: ToolDefinition = {
99
141
  const all = Array.from(tasks.values()).filter((t) => t.status !== 'deleted')
100
142
  if (all.length === 0) return { success: true, content: '(no tasks)' }
101
143
 
102
- // Group by status
103
- const groups: Record<string, Task[]> = { pending: [], in_progress: [], completed: [] }
104
- for (const t of all) groups[t.status]?.push(t)
144
+ // Sort by availability: in_progress → pending(available) → pending(blocked) → completed
145
+ const sortOrder: Record<string, number> = {
146
+ in_progress: 0,
147
+ pending: 1,
148
+ completed: 2,
149
+ failed: 3,
150
+ }
151
+
152
+ const sorted = [...all].sort((a, b) => {
153
+ const orderA = sortOrder[a.status] ?? 3
154
+ const orderB = sortOrder[b.status] ?? 3
155
+ if (orderA !== orderB) return orderA - orderB
156
+ // Within same status: available before blocked
157
+ if (a.status === 'pending' && b.status === 'pending') {
158
+ const aBlocked = isBlocked(a)
159
+ const bBlocked = isBlocked(b)
160
+ if (aBlocked !== bBlocked) return aBlocked ? 1 : -1
161
+ }
162
+ return 0
163
+ })
105
164
 
106
165
  const lines: string[] = []
107
- for (const status of ['in_progress', 'pending', 'completed'] as const) {
108
- const group = groups[status]!
109
- if (group.length === 0) continue
110
- const emoji = status === 'in_progress' ? '🔄' : status === 'pending' ? '📋' : '✅'
111
- lines.push(`${emoji} ${status.replace('_', ' ')} (${group.length}):`)
112
- for (const t of group) lines.push(` ${formatTask(t)}`)
166
+ const statusLabels: Record<string, { emoji: string; label: string }> = {
167
+ in_progress: { emoji: '🔄', label: 'In Progress' },
168
+ pending: { emoji: '📋', label: 'Pending' },
169
+ completed: { emoji: '✅', label: 'Completed' },
170
+ failed: { emoji: '❌', label: 'Failed' },
171
+ }
172
+
173
+ let currentStatus = ''
174
+ for (const t of sorted) {
175
+ const statusInfo = statusLabels[t.status]
176
+ if (!statusInfo) continue
177
+
178
+ if (statusInfo.label !== currentStatus) {
179
+ currentStatus = statusInfo.label
180
+ // Count tasks in this status group
181
+ const count = sorted.filter((s) => statusLabels[s.status]?.label === currentStatus).length
182
+ lines.push(`${statusInfo.emoji} ${currentStatus} (${count}):`)
183
+ }
184
+
185
+ const blocked = isBlocked(t)
186
+ const prefix = blocked && t.status === 'pending' ? '🚫 ' : ' '
187
+ const blockingNote = blocked ? ` 🚫 阻塞中 (等待: ${getBlockingIds(t).join(', ')})` : ''
188
+ lines.push(`${prefix}${formatTask(t)}${blockingNote}`)
113
189
  }
190
+
191
+ // Summary footer
192
+ const available = all.filter((t) => t.status === 'pending' && !isBlocked(t)).length
193
+ const blockedCount = all.filter((t) => t.status === 'pending' && isBlocked(t)).length
194
+ if (blockedCount > 0) {
195
+ lines.push('')
196
+ lines.push(
197
+ `📊 ${available} available · ${blockedCount} blocked · ${all.filter((t) => t.status === 'completed').length} done`,
198
+ )
199
+ }
200
+
114
201
  return { success: true, content: lines.join('\n') }
115
202
  }
116
203
 
@@ -129,8 +216,27 @@ export const taskTool: ToolDefinition = {
129
216
  if (task.activeForm) lines.push(`Active form: ${task.activeForm}`)
130
217
  if (task.owner) lines.push(`Owner: ${task.owner}`)
131
218
  if (task.blocks.length) lines.push(`Blocks: ${task.blocks.join(', ')}`)
132
- if (task.blockedBy.length) lines.push(`Blocked by: ${task.blockedBy.join(', ')}`)
219
+ if (task.blockedBy.length) {
220
+ const blocking = getBlockingIds(task)
221
+ if (blocking.length > 0) {
222
+ lines.push(
223
+ `Blocked by: ${task.blockedBy.join(', ')} (active blockers: ${blocking.join(', ')})`,
224
+ )
225
+ } else {
226
+ lines.push(`Blocked by: ${task.blockedBy.join(', ')} (all resolved ✓)`)
227
+ }
228
+ }
133
229
  if (task.metadata) lines.push(`Metadata: ${JSON.stringify(task.metadata)}`)
230
+ if (task.output) {
231
+ lines.push('')
232
+ lines.push('── Output ──')
233
+ lines.push(task.output.slice(0, 2000))
234
+ }
235
+ if (task.outputError) {
236
+ lines.push('')
237
+ lines.push(`── Error ──`)
238
+ lines.push(task.outputError)
239
+ }
134
240
 
135
241
  return { success: true, content: lines.join('\n') }
136
242
  }
@@ -167,7 +273,14 @@ export const taskTool: ToolDefinition = {
167
273
  }
168
274
  }
169
275
 
170
- return { success: true, content: `Task #${taskId} updated.\n${formatTask(task)}` }
276
+ const blockedNote = isBlocked(task)
277
+ ? ` (blocked — waiting on: ${getBlockingIds(task).join(', ')})`
278
+ : ''
279
+
280
+ return {
281
+ success: true,
282
+ content: `Task #${taskId} updated.\n${formatTask(task)}${blockedNote}`,
283
+ }
171
284
  }
172
285
 
173
286
  // ── DELETE ──
@@ -179,6 +292,86 @@ export const taskTool: ToolDefinition = {
179
292
  return { success: true, content: `Task #${taskId} deleted.` }
180
293
  }
181
294
 
295
+ // ── OUTPUT ──
296
+ if (action === 'output') {
297
+ const taskId = params.taskId as string
298
+ const task = tasks.get(taskId)
299
+ if (!task) return { success: false, content: '', error: `Task #${taskId} not found` }
300
+
301
+ if (task.status === 'pending') {
302
+ return {
303
+ success: true,
304
+ content: `Task #${taskId} is still pending — no output yet.\nStatus: ${task.status}\nSubject: ${task.subject}`,
305
+ }
306
+ }
307
+
308
+ if (task.status === 'in_progress') {
309
+ return {
310
+ success: true,
311
+ content: `Task #${taskId} is still running — output not yet available.\nStatus: in_progress\nSubject: ${task.subject}\n\nUse Task output again once the task completes.`,
312
+ }
313
+ }
314
+
315
+ if (task.status === 'completed' && task.output) {
316
+ return {
317
+ success: true,
318
+ content: `── Task #${taskId} Output ──\nStatus: completed\nSubject: ${task.subject}\n\n${task.output.slice(0, 5000)}`,
319
+ }
320
+ }
321
+
322
+ if (task.status === 'failed' && task.outputError) {
323
+ return {
324
+ success: false,
325
+ content: '',
326
+ error: `Task #${taskId} failed: ${task.outputError}`,
327
+ }
328
+ }
329
+
330
+ return {
331
+ success: true,
332
+ content: `Task #${taskId} — Status: ${task.status}\nSubject: ${task.subject}\n\n(no output recorded)`,
333
+ }
334
+ }
335
+
336
+ // ── STOP ──
337
+ if (action === 'stop') {
338
+ const taskId = params.taskId as string
339
+ const task = tasks.get(taskId)
340
+ if (!task) return { success: false, content: '', error: `Task #${taskId} not found` }
341
+
342
+ if (task.status === 'completed' || task.status === 'deleted') {
343
+ return {
344
+ success: true,
345
+ content: `Task #${taskId} is already ${task.status}. Nothing to stop.`,
346
+ }
347
+ }
348
+
349
+ if (task.status === 'pending') {
350
+ // Cancel a pending task
351
+ task.status = 'deleted'
352
+ return { success: true, content: `Task #${taskId} cancelled (was pending).` }
353
+ }
354
+
355
+ // For in_progress tasks: mark as failed — actual process termination is handled
356
+ // by BackgroundAgentRegistry.stop() when wired via the engine context
357
+ task.status = 'failed'
358
+ task.outputError = 'Task stopped by user.'
359
+
360
+ // If BackgroundAgentRegistry is available in context, try to abort
361
+ if (_ctx.registry) {
362
+ // Signal to any background registry that this task should stop
363
+ // The background registry will be injected via the engine context
364
+ const bgRegistry = (_ctx as unknown as Record<string, unknown>).backgroundAgentRegistry as
365
+ | { stop(id: string): boolean }
366
+ | undefined
367
+ if (bgRegistry) {
368
+ bgRegistry.stop(taskId)
369
+ }
370
+ }
371
+
372
+ return { success: true, content: `Task #${taskId} stopped.` }
373
+ }
374
+
182
375
  return { success: false, content: '', error: `Unknown action: ${action}` }
183
376
  },
184
377
  }
@@ -7,16 +7,23 @@ import { grepTool } from './file/grep'
7
7
  import { bashTool } from './exec/bash'
8
8
  import { gitTool } from './exec/git'
9
9
  import { taskTool } from './exec/task'
10
+ import { enterWorktreeTool } from './exec/enter-worktree'
11
+ import { exitWorktreeTool } from './exec/exit-worktree'
10
12
  import { agentTool } from './agent/agent'
11
13
  import { skillTool } from './agent/skill'
12
14
  import { planTool } from './agent/plan'
15
+ import { enterPlanModeTool } from './agent/enter-plan'
16
+ import { exitPlanModeTool } from './agent/exit-plan'
13
17
  import { memoryTool } from './agent/memory'
14
18
  import { workflowTool } from './agent/workflow'
15
19
  import { webFetchTool } from './network/web-fetch'
16
20
  import { webSearchTool } from './network/web-search'
17
21
  import { configTool } from './system/config'
18
22
  import { mcpTool } from './system/mcp'
23
+ import { toolSearchTool } from './system/tool-search'
19
24
  import { artifactTool } from './artifact/artifact'
25
+ import { reportFindingsTool } from './agent/report-findings'
26
+ import { sendMessageTool } from './agent/send-message'
20
27
  import { computerUseTool } from './computer/computer-use'
21
28
  import { scheduleWakeupTool } from './scheduling/schedule-wakeup.js'
22
29
  import { cronCreateTool, cronDeleteTool, cronListTool } from './scheduling/cron.js'
@@ -110,18 +117,25 @@ export function createToolRegistry(): Map<string, ToolDefinition> {
110
117
  withValidation(bashTool),
111
118
  withValidation(gitTool),
112
119
  withValidation(taskTool),
120
+ withValidation(enterWorktreeTool),
121
+ withValidation(exitWorktreeTool),
113
122
  // Agent tools
114
123
  withValidation(agentTool),
115
124
  withValidation(skillTool),
116
125
  withValidation(planTool),
126
+ withValidation(enterPlanModeTool),
127
+ withValidation(exitPlanModeTool),
117
128
  withValidation(memoryTool),
118
129
  withValidation(workflowTool),
130
+ withValidation(reportFindingsTool),
131
+ withValidation(sendMessageTool),
119
132
  // Network tools
120
133
  withValidation(webFetchTool),
121
134
  withValidation(webSearchTool),
122
135
  // System tools
123
136
  withValidation(configTool),
124
137
  withValidation(mcpTool),
138
+ withValidation(toolSearchTool),
125
139
  // Artifact tools
126
140
  withValidation(artifactTool),
127
141
  // Computer Use tools
@@ -0,0 +1,143 @@
1
+ /**
2
+ * ToolSearch — search MCP server tools by name or description.
3
+ *
4
+ * Instead of loading all MCP tool definitions into the context upfront,
5
+ * the AI can call ToolSearch to discover relevant tools on demand.
6
+ * This saves ~85% of context tokens for large MCP servers (50-100 tools).
7
+ */
8
+
9
+ import type { ToolDefinition } from '../../shared/index.ts'
10
+ import { McpClient } from '../../mcp/client'
11
+
12
+ export const toolSearchTool: ToolDefinition = {
13
+ name: 'ToolSearch',
14
+ description:
15
+ 'Search available MCP server tools by name or description. ' +
16
+ 'Use this to discover tools on demand instead of loading all tool definitions into context. ' +
17
+ 'Returns matching tool names with their server and description.',
18
+ category: 'system',
19
+ permission: 'auto',
20
+ parameters: {
21
+ type: 'object',
22
+ properties: {
23
+ query: {
24
+ type: 'string',
25
+ description:
26
+ 'Search query — matches against tool names and descriptions. Leave empty to list all available MCP tools.',
27
+ },
28
+ server: {
29
+ type: 'string',
30
+ description: 'Optional: limit search to a specific MCP server name.',
31
+ },
32
+ limit: {
33
+ type: 'integer',
34
+ description: 'Max results to return (default: 20).',
35
+ },
36
+ },
37
+ required: [],
38
+ },
39
+ async execute(params, _ctx) {
40
+ const query = ((params.query as string) || '').toLowerCase()
41
+ const serverFilter = (params.server as string) || ''
42
+ const limit = (params.limit as number) || 20
43
+
44
+ const client = McpClient.getInstance()
45
+ const connectedServers = client.getConnectedServers()
46
+
47
+ if (connectedServers.length === 0) {
48
+ return {
49
+ success: true,
50
+ content:
51
+ '── MCP Tool Search ──\n\n' +
52
+ 'No MCP servers connected.\n\n' +
53
+ 'Connect MCP servers in .mcp.json or via /mcp add <name> <url>.',
54
+ }
55
+ }
56
+
57
+ // Filter by server if specified
58
+ const servers = serverFilter
59
+ ? connectedServers.filter((s) => s.toLowerCase().includes(serverFilter))
60
+ : connectedServers
61
+
62
+ if (serverFilter && servers.length === 0) {
63
+ return {
64
+ success: true,
65
+ content:
66
+ `── MCP Tool Search ──\n\n` +
67
+ `No connected server matching "${serverFilter}".\n\n` +
68
+ `Connected servers: ${connectedServers.join(', ')}`,
69
+ }
70
+ }
71
+
72
+ // Collect matching tools
73
+ interface Match {
74
+ server: string
75
+ tool: string
76
+ description: string
77
+ }
78
+
79
+ const matches: Match[] = []
80
+
81
+ for (const serverName of servers) {
82
+ try {
83
+ const tools = client.getTools(serverName)
84
+ for (const tool of tools) {
85
+ const nameMatch = !query || tool.name.toLowerCase().includes(query)
86
+ const descMatch = !query || (tool.description || '').toLowerCase().includes(query)
87
+ if (nameMatch || descMatch) {
88
+ matches.push({
89
+ server: serverName,
90
+ tool: tool.name,
91
+ description: (tool.description || '').slice(0, 120),
92
+ })
93
+ }
94
+ }
95
+ } catch {
96
+ // Server disconnected mid-search — skip
97
+ }
98
+ }
99
+
100
+ if (matches.length === 0) {
101
+ return {
102
+ success: true,
103
+ content:
104
+ `── MCP Tool Search ──\n\n` +
105
+ `No tools matching "${query}" across ${servers.length} server(s).\n\n` +
106
+ `Try a different query, or omit the query to list all tools.`,
107
+ }
108
+ }
109
+
110
+ // Truncate to limit
111
+ const limited = matches.slice(0, limit)
112
+
113
+ const lines: string[] = [
114
+ `── MCP Tool Search ──`,
115
+ '',
116
+ `${limited.length} of ${matches.length} matching tool(s) across ${servers.length} server(s):`,
117
+ '',
118
+ ]
119
+
120
+ // Group by server
121
+ const byServer = new Map<string, Match[]>()
122
+ for (const m of limited) {
123
+ const list = byServer.get(m.server) || []
124
+ list.push(m)
125
+ byServer.set(m.server, list)
126
+ }
127
+
128
+ for (const [server, tools] of byServer) {
129
+ lines.push(` ${server} (${tools.length}):`)
130
+ for (const t of tools) {
131
+ const desc = t.description ? ` — ${t.description}` : ''
132
+ lines.push(` mcp__${server.replace(/[^a-z0-9-]/g, '_')}__${t.tool}${desc}`)
133
+ }
134
+ lines.push('')
135
+ }
136
+
137
+ if (matches.length > limit) {
138
+ lines.push(`... and ${matches.length - limit} more. Use "query" to narrow results.`)
139
+ }
140
+
141
+ return { success: true, content: lines.join('\n') }
142
+ },
143
+ }
package/src/ui/app.tsx CHANGED
@@ -15,6 +15,7 @@ import {
15
15
  handleSwitch,
16
16
  type CommandContext,
17
17
  } from './commands'
18
+ import type { PermissionMode } from '../shared/index.ts'
18
19
 
19
20
  interface AppProps {
20
21
  engine: QueryEngine
@@ -49,13 +50,29 @@ interface AgentProgress {
49
50
  // Version is read fresh from package.json at startup via runApp prop
50
51
  // (bypasses Bun module caching after npm update)
51
52
 
52
- type PermissionMode = 'auto' | 'ask' | 'bypass'
53
-
54
- const PERMISSION_MODES: PermissionMode[] = ['auto', 'ask', 'bypass']
53
+ const PERMISSION_MODES: PermissionMode[] = [
54
+ 'default',
55
+ 'acceptEdits',
56
+ 'plan',
57
+ 'auto',
58
+ 'dontAsk',
59
+ 'bypassPermissions',
60
+ ]
55
61
  const PERMISSION_LABELS: Record<PermissionMode, string> = {
56
- auto: 'auto mode on',
57
- ask: 'ask mode (confirm each action)',
58
- bypass: 'bypass mode (skip all checks)',
62
+ default: 'default · reads free',
63
+ acceptEdits: 'accept edits · reads + edits free',
64
+ plan: 'plan mode · read-only',
65
+ auto: 'auto mode · hooks gate',
66
+ dontAsk: "don't ask · pre-approved only",
67
+ bypassPermissions: 'bypass · skip all checks',
68
+ }
69
+ const PERMISSION_COLORS: Record<PermissionMode, string> = {
70
+ default: 'white',
71
+ acceptEdits: 'blue',
72
+ plan: 'yellow',
73
+ auto: 'green',
74
+ dontAsk: 'cyan',
75
+ bypassPermissions: 'red',
59
76
  }
60
77
 
61
78
  export function App({
@@ -78,7 +95,7 @@ export function App({
78
95
  const [effort, setEffort] = useState('high')
79
96
  const [focusMode, setFocusMode] = useState(false)
80
97
  const [goalText, setGoalText] = useState('')
81
- const [permissionMode, setPermissionMode] = useState<PermissionMode>('auto')
98
+ const [permissionMode, setPermissionMode] = useState<PermissionMode>('default')
82
99
  const abortRef = useRef<AbortController | null>(null)
83
100
  const [agentProgress, setAgentProgress] = useState<AgentProgress | null>(null)
84
101
  const [agentElapsed, setAgentElapsed] = useState(0)
@@ -304,6 +321,24 @@ export function App({
304
321
  { role: 'system', content: `❌ Error: ${chunk.error}` },
305
322
  ])
306
323
  }
324
+
325
+ if (chunk.type === 'task_notification' && chunk.taskNotification) {
326
+ const tn = chunk.taskNotification
327
+ const emoji = tn.status === 'completed' ? '✅' : '❌'
328
+ const label = tn.status === 'completed' ? 'completed' : 'failed'
329
+ const preview = tn.content
330
+ ? tn.content.slice(0, 120) + (tn.content.length > 120 ? '...' : '')
331
+ : tn.error
332
+ ? `Error: ${tn.error.slice(0, 120)}`
333
+ : '(no output)'
334
+ setMessages((prev) => [
335
+ ...prev,
336
+ {
337
+ role: 'system',
338
+ content: `${emoji} Background Agent ${label}: ${tn.description}\n ID: ${tn.taskId}\n ${preview}`,
339
+ },
340
+ ])
341
+ }
307
342
  }
308
343
  } catch (err) {
309
344
  setMessages((prev) => [...prev, { role: 'system', content: `Error: ${String(err)}` }])
@@ -365,13 +400,13 @@ export function App({
365
400
  })
366
401
  return
367
402
  }
368
- // Shift+Tab → cycle permission mode (auto → ask → bypass → auto)
403
+ // Shift+Tab → cycle through all 6 permission modes
369
404
  if (key.shift && key.tab) {
370
405
  setPermissionMode((prev) => {
371
406
  const idx = PERMISSION_MODES.indexOf(prev)
372
407
  const next = PERMISSION_MODES[(idx + 1) % PERMISSION_MODES.length]!
373
- // Sync to engine
374
- engine.getPermission().setDefaultLevel(next)
408
+ // Sync to engine — use setMode for proper mode semantics
409
+ engine.getPermission().setMode(next)
375
410
  return next
376
411
  })
377
412
  return
@@ -441,17 +476,12 @@ export function App({
441
476
  </Box>
442
477
  )}
443
478
  <Box flexDirection="row">
444
- <Text
445
- color={
446
- permissionMode === 'auto' ? 'green' : permissionMode === 'ask' ? 'yellow' : 'red'
447
- }
448
- >
479
+ <Text color={PERMISSION_COLORS[permissionMode]}>
449
480
  ● {PERMISSION_LABELS[permissionMode]}
450
481
  </Text>
451
- <Text dimColor> · ⏵⏵ accept edits on</Text>
452
- <Text dimColor> (Shift+Tab to cycle)</Text>
453
- <Text dimColor> · Ctrl+P pick · /help /commands · Esc cancel</Text>
454
- <Text dimColor> · ← agents</Text>
482
+ <Text dimColor> · Shift+Tab mode</Text>
483
+ <Text dimColor> · Ctrl+P model · Ctrl+O expand</Text>
484
+ <Text dimColor> · Esc cancel · /help · ← agents</Text>
455
485
  </Box>
456
486
  </Box>
457
487
  </Box>