pi-code 0.1.0 → 0.2.1
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/README.md +24 -7
- package/extensions/claude-rules.ts +7 -3
- package/extensions/context-imports.ts +56 -29
- package/extensions/git-checkpoint.ts +16 -16
- package/extensions/hooks.ts +5 -2
- package/extensions/mcp.ts +29 -14
- package/extensions/memory.ts +4 -3
- package/extensions/notify.ts +6 -4
- package/extensions/output-guard.ts +23 -0
- package/extensions/output-styles.ts +1 -1
- package/extensions/plan-mode/index.ts +83 -58
- package/extensions/plan-mode/utils.ts +118 -16
- package/extensions/project-approval.ts +73 -0
- package/extensions/question.ts +63 -49
- package/extensions/subagent/agents.ts +12 -11
- package/extensions/subagent/index.ts +612 -465
- package/extensions/todo.ts +48 -31
- package/extensions/web.ts +49 -27
- package/package.json +1 -1
|
@@ -19,9 +19,10 @@ import * as path from 'node:path'
|
|
|
19
19
|
import type { AgentToolResult } from '@earendil-works/pi-agent-core'
|
|
20
20
|
import type { Message } from '@earendil-works/pi-ai'
|
|
21
21
|
import { StringEnum } from '@earendil-works/pi-ai'
|
|
22
|
-
import { type ExtensionAPI, getMarkdownTheme, type Theme, withFileMutationQueue } from '@earendil-works/pi-coding-agent'
|
|
22
|
+
import { type ExtensionAPI, type ExtensionContext, getMarkdownTheme, type Theme, withFileMutationQueue } from '@earendil-works/pi-coding-agent'
|
|
23
23
|
import { Container, Markdown, Spacer, Text } from '@earendil-works/pi-tui'
|
|
24
|
-
import { Type } from 'typebox'
|
|
24
|
+
import { type Static, Type } from 'typebox'
|
|
25
|
+
import { isProjectApproved } from '../project-approval.js'
|
|
25
26
|
import { type AgentConfig, type AgentScope, discoverAgents } from './agents.js'
|
|
26
27
|
import { backgroundStatusText, startBackgroundRun } from './background.js'
|
|
27
28
|
|
|
@@ -29,14 +30,14 @@ const MAX_PARALLEL_TASKS = 8
|
|
|
29
30
|
const MAX_CONCURRENCY = 4
|
|
30
31
|
const COLLAPSED_ITEM_COUNT = 10
|
|
31
32
|
|
|
32
|
-
function formatTokens(count: number): string {
|
|
33
|
+
export function formatTokens(count: number): string {
|
|
33
34
|
if (count < 1000) return count.toString()
|
|
34
35
|
if (count < 10000) return `${(count / 1000).toFixed(1)}k`
|
|
35
36
|
if (count < 1000000) return `${Math.round(count / 1000)}k`
|
|
36
37
|
return `${(count / 1000000).toFixed(1)}M`
|
|
37
38
|
}
|
|
38
39
|
|
|
39
|
-
function formatUsageStats(
|
|
40
|
+
export function formatUsageStats(
|
|
40
41
|
usage: {
|
|
41
42
|
input: number
|
|
42
43
|
output: number
|
|
@@ -62,7 +63,7 @@ function formatUsageStats(
|
|
|
62
63
|
return parts.join(' ')
|
|
63
64
|
}
|
|
64
65
|
|
|
65
|
-
function formatToolCall(toolName: string, args: Record<string, unknown>, themeFg: Theme['fg']): string {
|
|
66
|
+
export function formatToolCall(toolName: string, args: Record<string, unknown>, themeFg: Theme['fg']): string {
|
|
66
67
|
const shortenPath = (p: string) => {
|
|
67
68
|
const home = os.homedir()
|
|
68
69
|
return p.startsWith(home) ? `~${p.slice(home.length)}` : p
|
|
@@ -83,7 +84,8 @@ function formatToolCall(toolName: string, args: Record<string, unknown>, themeFg
|
|
|
83
84
|
if (offset !== undefined || limit !== undefined) {
|
|
84
85
|
const startLine = offset ?? 1
|
|
85
86
|
const endLine = limit !== undefined ? startLine + limit - 1 : ''
|
|
86
|
-
|
|
87
|
+
const rangeSuffix = endLine ? `-${endLine}` : ''
|
|
88
|
+
text += themeFg('warning', `:${startLine}${rangeSuffix}`)
|
|
87
89
|
}
|
|
88
90
|
return themeFg('muted', 'read ') + text
|
|
89
91
|
}
|
|
@@ -153,7 +155,7 @@ interface SubagentDetails {
|
|
|
153
155
|
results: SingleResult[]
|
|
154
156
|
}
|
|
155
157
|
|
|
156
|
-
function getFinalOutput(messages: Message[]): string {
|
|
158
|
+
export function getFinalOutput(messages: Message[]): string {
|
|
157
159
|
for (let i = messages.length - 1; i >= 0; i--) {
|
|
158
160
|
const msg = messages[i]
|
|
159
161
|
if (msg.role === 'assistant') {
|
|
@@ -167,7 +169,7 @@ function getFinalOutput(messages: Message[]): string {
|
|
|
167
169
|
|
|
168
170
|
type DisplayItem = { type: 'text'; text: string } | { type: 'toolCall'; name: string; args: Record<string, unknown> }
|
|
169
171
|
|
|
170
|
-
function getDisplayItems(messages: Message[]): DisplayItem[] {
|
|
172
|
+
export function getDisplayItems(messages: Message[]): DisplayItem[] {
|
|
171
173
|
const items: DisplayItem[] = []
|
|
172
174
|
for (const msg of messages) {
|
|
173
175
|
if (msg.role === 'assistant') {
|
|
@@ -180,7 +182,7 @@ function getDisplayItems(messages: Message[]): DisplayItem[] {
|
|
|
180
182
|
return items
|
|
181
183
|
}
|
|
182
184
|
|
|
183
|
-
async function mapWithConcurrencyLimit<TIn, TOut>(items: TIn[], concurrency: number, fn: (item: TIn, index: number) => Promise<TOut>): Promise<TOut[]> {
|
|
185
|
+
export async function mapWithConcurrencyLimit<TIn, TOut>(items: TIn[], concurrency: number, fn: (item: TIn, index: number) => Promise<TOut>): Promise<TOut[]> {
|
|
184
186
|
if (items.length === 0) return []
|
|
185
187
|
const limit = Math.max(1, Math.min(concurrency, items.length))
|
|
186
188
|
const results: TOut[] = new Array(items.length)
|
|
@@ -224,7 +226,38 @@ function getPiInvocation(args: string[]): { command: string; args: string[] } {
|
|
|
224
226
|
|
|
225
227
|
type OnUpdateCallback = (partial: AgentToolResult<SubagentDetails>) => void
|
|
226
228
|
|
|
227
|
-
|
|
229
|
+
type AssistantMessage = Extract<Message, { role: 'assistant' }>
|
|
230
|
+
|
|
231
|
+
function accumulateAssistantMessage(result: SingleResult, msg: AssistantMessage): void {
|
|
232
|
+
result.usage.turns++
|
|
233
|
+
const usage = msg.usage
|
|
234
|
+
if (usage) {
|
|
235
|
+
result.usage.input += usage.input || 0
|
|
236
|
+
result.usage.output += usage.output || 0
|
|
237
|
+
result.usage.cacheRead += usage.cacheRead || 0
|
|
238
|
+
result.usage.cacheWrite += usage.cacheWrite || 0
|
|
239
|
+
result.usage.cost += usage.cost?.total || 0
|
|
240
|
+
result.usage.contextTokens = usage.totalTokens || 0
|
|
241
|
+
}
|
|
242
|
+
if (!result.model && msg.model) result.model = msg.model
|
|
243
|
+
if (msg.stopReason) result.stopReason = msg.stopReason
|
|
244
|
+
if (msg.errorMessage) result.errorMessage = msg.errorMessage
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
interface RunAgentOptions {
|
|
248
|
+
defaultCwd: string
|
|
249
|
+
agents: AgentConfig[]
|
|
250
|
+
agentName: string
|
|
251
|
+
task: string
|
|
252
|
+
cwd?: string
|
|
253
|
+
step?: number
|
|
254
|
+
signal?: AbortSignal
|
|
255
|
+
onUpdate?: OnUpdateCallback
|
|
256
|
+
makeDetails: (results: SingleResult[]) => SubagentDetails
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
async function runSingleAgent(options: RunAgentOptions): Promise<SingleResult> {
|
|
260
|
+
const { defaultCwd, agents, agentName, task, cwd, step, signal, onUpdate, makeDetails } = options
|
|
228
261
|
const agent = agents.find((a) => a.name === agentName)
|
|
229
262
|
|
|
230
263
|
if (!agent) {
|
|
@@ -298,29 +331,14 @@ async function runSingleAgent(defaultCwd: string, agents: AgentConfig[], agentNa
|
|
|
298
331
|
return
|
|
299
332
|
}
|
|
300
333
|
|
|
301
|
-
if (event.
|
|
334
|
+
if (!event.message) return
|
|
335
|
+
|
|
336
|
+
if (event.type === 'message_end') {
|
|
302
337
|
const msg = event.message as Message
|
|
303
338
|
currentResult.messages.push(msg)
|
|
304
|
-
|
|
305
|
-
if (msg.role === 'assistant') {
|
|
306
|
-
currentResult.usage.turns++
|
|
307
|
-
const usage = msg.usage
|
|
308
|
-
if (usage) {
|
|
309
|
-
currentResult.usage.input += usage.input || 0
|
|
310
|
-
currentResult.usage.output += usage.output || 0
|
|
311
|
-
currentResult.usage.cacheRead += usage.cacheRead || 0
|
|
312
|
-
currentResult.usage.cacheWrite += usage.cacheWrite || 0
|
|
313
|
-
currentResult.usage.cost += usage.cost?.total || 0
|
|
314
|
-
currentResult.usage.contextTokens = usage.totalTokens || 0
|
|
315
|
-
}
|
|
316
|
-
if (!currentResult.model && msg.model) currentResult.model = msg.model
|
|
317
|
-
if (msg.stopReason) currentResult.stopReason = msg.stopReason
|
|
318
|
-
if (msg.errorMessage) currentResult.errorMessage = msg.errorMessage
|
|
319
|
-
}
|
|
339
|
+
if (msg.role === 'assistant') accumulateAssistantMessage(currentResult, msg)
|
|
320
340
|
emitUpdate()
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
if (event.type === 'tool_result_end' && event.message) {
|
|
341
|
+
} else if (event.type === 'tool_result_end') {
|
|
324
342
|
currentResult.messages.push(event.message as Message)
|
|
325
343
|
emitUpdate()
|
|
326
344
|
}
|
|
@@ -437,7 +455,554 @@ export function projectAgentGate(projectAgentCount: number, trusted: boolean, ha
|
|
|
437
455
|
return trusted ? 'allow' : 'refuse'
|
|
438
456
|
}
|
|
439
457
|
|
|
440
|
-
|
|
458
|
+
/**
|
|
459
|
+
* pi only sets a tool result's error flag when execute() throws; a returned isError is
|
|
460
|
+
* ignored (docs/extensions.md, "Signaling errors"). Throwing here would be worse: the
|
|
461
|
+
* agent loop replaces the result with createErrorToolResult(message), discarding the
|
|
462
|
+
* details renderResult needs to show the failed agent's transcript. The failure is
|
|
463
|
+
* carried in the content text instead, which is what reaches the model.
|
|
464
|
+
*/
|
|
465
|
+
type ToolResult = AgentToolResult<SubagentDetails>
|
|
466
|
+
type SubagentMode = 'single' | 'parallel' | 'chain'
|
|
467
|
+
type MakeDetails = (mode: SubagentMode) => (results: SingleResult[]) => SubagentDetails
|
|
468
|
+
type SubagentParamsStatic = Static<typeof SubagentParams>
|
|
469
|
+
type ChainStepParam = Static<typeof ChainItem>
|
|
470
|
+
type TaskItemParam = Static<typeof TaskItem>
|
|
471
|
+
|
|
472
|
+
/** Everything a mode handler needs from the surrounding execute() call. */
|
|
473
|
+
interface ModeContext {
|
|
474
|
+
agents: AgentConfig[]
|
|
475
|
+
defaultCwd: string
|
|
476
|
+
signal: AbortSignal | undefined
|
|
477
|
+
onUpdate: OnUpdateCallback | undefined
|
|
478
|
+
makeDetails: MakeDetails
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
async function checkProjectAgentGate(params: SubagentParamsStatic, agents: AgentConfig[], ctx: ExtensionContext, projectAgentsDir: string | null, gateMode: SubagentMode, makeDetails: MakeDetails): Promise<ToolResult | null> {
|
|
482
|
+
const requestedAgentNames = new Set<string>()
|
|
483
|
+
if (params.agent) requestedAgentNames.add(params.agent)
|
|
484
|
+
for (const step of params.chain ?? []) requestedAgentNames.add(step.agent)
|
|
485
|
+
for (const t of params.tasks ?? []) requestedAgentNames.add(t.agent)
|
|
486
|
+
const requestedProjectAgents = [...requestedAgentNames].map((name) => agents.find((a) => a.name === name)).filter((a): a is AgentConfig => a?.source === 'project')
|
|
487
|
+
|
|
488
|
+
// isProjectTrusted alone is true for a repo pi never asked about; see project-approval.
|
|
489
|
+
const approved = await isProjectApproved(ctx)
|
|
490
|
+
const gate = projectAgentGate(requestedProjectAgents.length, approved, ctx.hasUI, params.confirmProjectAgents ?? true)
|
|
491
|
+
const names = requestedProjectAgents.map((a) => a.name).join(', ')
|
|
492
|
+
if (gate === 'refuse') {
|
|
493
|
+
return { content: [{ type: 'text', text: `Project-local agents (${names}) require a trusted project; refusing in non-interactive mode.` }], details: makeDetails(gateMode)([]) }
|
|
494
|
+
}
|
|
495
|
+
if (gate === 'confirm') {
|
|
496
|
+
const dir = projectAgentsDir ?? '(unknown)'
|
|
497
|
+
const ok = await ctx.ui.confirm('Run project-local agents?', `Agents: ${names}\nSource: ${dir}\n\nProject agents are repo-controlled. Only continue for trusted repositories.`)
|
|
498
|
+
if (!ok) return { content: [{ type: 'text', text: 'Canceled: project-local agents not approved.' }], details: makeDetails(gateMode)([]) }
|
|
499
|
+
}
|
|
500
|
+
return null
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
async function runBackgroundMode(params: SubagentParamsStatic, agents: AgentConfig[], defaultCwd: string, pi: ExtensionAPI, makeDetails: MakeDetails): Promise<ToolResult> {
|
|
504
|
+
const task = params.task
|
|
505
|
+
const agentName = params.agent
|
|
506
|
+
if (!task || !agentName) {
|
|
507
|
+
return {
|
|
508
|
+
content: [{ type: 'text', text: 'background: true requires single mode (agent + task).' }],
|
|
509
|
+
details: makeDetails('single')([]),
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
const agent = agents.find((a) => a.name === agentName)
|
|
513
|
+
if (!agent) {
|
|
514
|
+
const available = agents.map((a) => `"${a.name}"`).join(', ') || 'none'
|
|
515
|
+
return {
|
|
516
|
+
content: [{ type: 'text', text: `Unknown agent: "${agentName}". Available agents: ${available}.` }],
|
|
517
|
+
details: makeDetails('single')([]),
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
const args: string[] = ['--mode', 'json', '-p', '--no-session']
|
|
521
|
+
if (agent.model) args.push('--model', agent.model)
|
|
522
|
+
if (agent.tools && agent.tools.length > 0) args.push('--tools', agent.tools.join(','))
|
|
523
|
+
let tmpPrompt: { dir: string; filePath: string } | undefined
|
|
524
|
+
if (agent.systemPrompt.trim()) {
|
|
525
|
+
tmpPrompt = await writePromptToTempFile(agent.name, agent.systemPrompt)
|
|
526
|
+
args.push('--append-system-prompt', tmpPrompt.filePath)
|
|
527
|
+
}
|
|
528
|
+
args.push(`Task: ${task}`)
|
|
529
|
+
const invocation = getPiInvocation(args)
|
|
530
|
+
const id = startBackgroundRun(agent.name, task, { command: invocation.command, args: invocation.args, cwd: params.cwd ?? defaultCwd }, (run) => {
|
|
531
|
+
if (tmpPrompt) {
|
|
532
|
+
try {
|
|
533
|
+
fs.unlinkSync(tmpPrompt.filePath)
|
|
534
|
+
} catch {
|
|
535
|
+
/* ignore */
|
|
536
|
+
}
|
|
537
|
+
try {
|
|
538
|
+
fs.rmdirSync(tmpPrompt.dir)
|
|
539
|
+
} catch {
|
|
540
|
+
/* ignore */
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
const output = run.output || '(no output)'
|
|
544
|
+
pi.sendMessage(
|
|
545
|
+
{
|
|
546
|
+
customType: 'subagent-background',
|
|
547
|
+
content: `Background subagent run ${run.id} (${run.agent}) ${run.state} after ${run.turns} turns.\n\n${output}`,
|
|
548
|
+
display: true,
|
|
549
|
+
},
|
|
550
|
+
{ triggerTurn: true },
|
|
551
|
+
)
|
|
552
|
+
})
|
|
553
|
+
return {
|
|
554
|
+
content: [{ type: 'text', text: `Started background run ${id} (${agent.name}). A notification will arrive on completion; check progress with {status: true}.` }],
|
|
555
|
+
details: makeDetails('single')([]),
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
async function runChainMode(chain: ChainStepParam[], mode: ModeContext): Promise<ToolResult> {
|
|
560
|
+
const { agents, defaultCwd, signal, onUpdate, makeDetails } = mode
|
|
561
|
+
const results: SingleResult[] = []
|
|
562
|
+
let previousOutput = ''
|
|
563
|
+
|
|
564
|
+
for (let i = 0; i < chain.length; i++) {
|
|
565
|
+
const step = chain[i]
|
|
566
|
+
const taskWithContext = step.task.replaceAll('{previous}', previousOutput)
|
|
567
|
+
|
|
568
|
+
// Create update callback that includes all previous results
|
|
569
|
+
const chainUpdate: OnUpdateCallback | undefined = onUpdate
|
|
570
|
+
? (partial) => {
|
|
571
|
+
// Combine completed results with current streaming result
|
|
572
|
+
const currentResult = partial.details?.results[0]
|
|
573
|
+
if (currentResult) {
|
|
574
|
+
const allResults = [...results, currentResult]
|
|
575
|
+
onUpdate({
|
|
576
|
+
content: partial.content,
|
|
577
|
+
details: makeDetails('chain')(allResults),
|
|
578
|
+
})
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
: undefined
|
|
582
|
+
|
|
583
|
+
const result = await runSingleAgent({
|
|
584
|
+
defaultCwd,
|
|
585
|
+
agents,
|
|
586
|
+
agentName: step.agent,
|
|
587
|
+
task: taskWithContext,
|
|
588
|
+
cwd: step.cwd,
|
|
589
|
+
step: i + 1,
|
|
590
|
+
signal,
|
|
591
|
+
onUpdate: chainUpdate,
|
|
592
|
+
makeDetails: makeDetails('chain'),
|
|
593
|
+
})
|
|
594
|
+
results.push(result)
|
|
595
|
+
|
|
596
|
+
const isError = result.exitCode !== 0 || result.stopReason === 'error' || result.stopReason === 'aborted'
|
|
597
|
+
if (isError) {
|
|
598
|
+
const errorMsg = result.errorMessage || result.stderr || getFinalOutput(result.messages) || '(no output)'
|
|
599
|
+
return {
|
|
600
|
+
content: [{ type: 'text', text: `Chain stopped at step ${i + 1} (${step.agent}): ${errorMsg}` }],
|
|
601
|
+
details: makeDetails('chain')(results),
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
previousOutput = getFinalOutput(result.messages)
|
|
605
|
+
}
|
|
606
|
+
return {
|
|
607
|
+
content: [{ type: 'text', text: getFinalOutput(results.at(-1)?.messages ?? []) || '(no output)' }],
|
|
608
|
+
details: makeDetails('chain')(results),
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
async function runParallelMode(tasks: TaskItemParam[], mode: ModeContext): Promise<ToolResult> {
|
|
613
|
+
const { agents, defaultCwd, signal, onUpdate, makeDetails } = mode
|
|
614
|
+
if (tasks.length > MAX_PARALLEL_TASKS)
|
|
615
|
+
return {
|
|
616
|
+
content: [
|
|
617
|
+
{
|
|
618
|
+
type: 'text',
|
|
619
|
+
text: `Too many parallel tasks (${tasks.length}). Max is ${MAX_PARALLEL_TASKS}.`,
|
|
620
|
+
},
|
|
621
|
+
],
|
|
622
|
+
details: makeDetails('parallel')([]),
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
// Track all results for streaming updates
|
|
626
|
+
const allResults: SingleResult[] = new Array(tasks.length)
|
|
627
|
+
|
|
628
|
+
// Initialize placeholder results
|
|
629
|
+
for (let i = 0; i < tasks.length; i++) {
|
|
630
|
+
allResults[i] = {
|
|
631
|
+
agent: tasks[i].agent,
|
|
632
|
+
agentSource: 'unknown',
|
|
633
|
+
task: tasks[i].task,
|
|
634
|
+
exitCode: -1, // -1 = still running
|
|
635
|
+
messages: [],
|
|
636
|
+
stderr: '',
|
|
637
|
+
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 },
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
const emitParallelUpdate = () => {
|
|
642
|
+
if (onUpdate) {
|
|
643
|
+
const running = allResults.filter((r) => r.exitCode === -1).length
|
|
644
|
+
const done = allResults.filter((r) => r.exitCode !== -1).length
|
|
645
|
+
onUpdate({
|
|
646
|
+
content: [{ type: 'text', text: `Parallel: ${done}/${allResults.length} done, ${running} running...` }],
|
|
647
|
+
details: makeDetails('parallel')([...allResults]),
|
|
648
|
+
})
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
const results = await mapWithConcurrencyLimit(tasks, MAX_CONCURRENCY, async (t, index) => {
|
|
653
|
+
const result = await runSingleAgent({
|
|
654
|
+
defaultCwd,
|
|
655
|
+
agents,
|
|
656
|
+
agentName: t.agent,
|
|
657
|
+
task: t.task,
|
|
658
|
+
cwd: t.cwd,
|
|
659
|
+
signal,
|
|
660
|
+
// Per-task update callback
|
|
661
|
+
onUpdate: (partial) => {
|
|
662
|
+
if (partial.details?.results[0]) {
|
|
663
|
+
allResults[index] = partial.details.results[0]
|
|
664
|
+
emitParallelUpdate()
|
|
665
|
+
}
|
|
666
|
+
},
|
|
667
|
+
makeDetails: makeDetails('parallel'),
|
|
668
|
+
})
|
|
669
|
+
allResults[index] = result
|
|
670
|
+
emitParallelUpdate()
|
|
671
|
+
return result
|
|
672
|
+
})
|
|
673
|
+
|
|
674
|
+
const successCount = results.filter((r) => r.exitCode === 0).length
|
|
675
|
+
const summaries = results.map((r) => {
|
|
676
|
+
const output = getFinalOutput(r.messages)
|
|
677
|
+
const preview = output.slice(0, 100) + (output.length > 100 ? '...' : '')
|
|
678
|
+
const status = r.exitCode === 0 ? 'completed' : 'failed'
|
|
679
|
+
return `[${r.agent}] ${status}: ${preview || '(no output)'}`
|
|
680
|
+
})
|
|
681
|
+
return {
|
|
682
|
+
content: [
|
|
683
|
+
{
|
|
684
|
+
type: 'text',
|
|
685
|
+
text: `Parallel: ${successCount}/${results.length} succeeded\n\n${summaries.join('\n\n')}`,
|
|
686
|
+
},
|
|
687
|
+
],
|
|
688
|
+
details: makeDetails('parallel')(results),
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
async function runSingleMode(agentName: string, task: string, cwd: string | undefined, mode: ModeContext): Promise<ToolResult> {
|
|
693
|
+
const { agents, defaultCwd, signal, onUpdate, makeDetails } = mode
|
|
694
|
+
const result = await runSingleAgent({
|
|
695
|
+
defaultCwd,
|
|
696
|
+
agents,
|
|
697
|
+
agentName,
|
|
698
|
+
task,
|
|
699
|
+
cwd,
|
|
700
|
+
signal,
|
|
701
|
+
onUpdate,
|
|
702
|
+
makeDetails: makeDetails('single'),
|
|
703
|
+
})
|
|
704
|
+
const isError = result.exitCode !== 0 || result.stopReason === 'error' || result.stopReason === 'aborted'
|
|
705
|
+
if (isError) {
|
|
706
|
+
const errorMsg = result.errorMessage || result.stderr || getFinalOutput(result.messages) || '(no output)'
|
|
707
|
+
return {
|
|
708
|
+
content: [{ type: 'text', text: `Agent ${result.stopReason || 'failed'}: ${errorMsg}` }],
|
|
709
|
+
details: makeDetails('single')([result]),
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
return {
|
|
713
|
+
content: [{ type: 'text', text: getFinalOutput(result.messages) || '(no output)' }],
|
|
714
|
+
details: makeDetails('single')([result]),
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
interface CallItem {
|
|
719
|
+
agent: string
|
|
720
|
+
task: string
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
function renderChainCall(chain: CallItem[], scope: AgentScope, theme: Theme): Text {
|
|
724
|
+
let text = theme.fg('toolTitle', theme.bold('subagent ')) + theme.fg('accent', `chain (${chain.length} steps)`) + theme.fg('muted', ` [${scope}]`)
|
|
725
|
+
for (let i = 0; i < Math.min(chain.length, 3); i++) {
|
|
726
|
+
const step = chain[i]
|
|
727
|
+
// Clean up {previous} placeholder for display
|
|
728
|
+
const cleanTask = step.task.replaceAll('{previous}', '').trim()
|
|
729
|
+
const preview = cleanTask.length > 40 ? `${cleanTask.slice(0, 40)}...` : cleanTask
|
|
730
|
+
const stepNumber = theme.fg('muted', `${i + 1}.`)
|
|
731
|
+
const stepLabel = theme.fg('accent', step.agent) + theme.fg('dim', ` ${preview}`)
|
|
732
|
+
text += `\n ${stepNumber} ${stepLabel}`
|
|
733
|
+
}
|
|
734
|
+
if (chain.length > 3) {
|
|
735
|
+
const more = theme.fg('muted', `... +${chain.length - 3} more`)
|
|
736
|
+
text += `\n ${more}`
|
|
737
|
+
}
|
|
738
|
+
return new Text(text, 0, 0)
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
function renderParallelCall(tasks: CallItem[], scope: AgentScope, theme: Theme): Text {
|
|
742
|
+
let text = theme.fg('toolTitle', theme.bold('subagent ')) + theme.fg('accent', `parallel (${tasks.length} tasks)`) + theme.fg('muted', ` [${scope}]`)
|
|
743
|
+
for (const t of tasks.slice(0, 3)) {
|
|
744
|
+
const preview = t.task.length > 40 ? `${t.task.slice(0, 40)}...` : t.task
|
|
745
|
+
const taskLabel = theme.fg('accent', t.agent) + theme.fg('dim', ` ${preview}`)
|
|
746
|
+
text += `\n ${taskLabel}`
|
|
747
|
+
}
|
|
748
|
+
if (tasks.length > 3) {
|
|
749
|
+
const more = theme.fg('muted', `... +${tasks.length - 3} more`)
|
|
750
|
+
text += `\n ${more}`
|
|
751
|
+
}
|
|
752
|
+
return new Text(text, 0, 0)
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
function renderSingleCall(agent: string | undefined, task: string | undefined, scope: AgentScope, theme: Theme): Text {
|
|
756
|
+
const agentName = agent || '...'
|
|
757
|
+
let preview = '...'
|
|
758
|
+
if (task) preview = task.length > 60 ? `${task.slice(0, 60)}...` : task
|
|
759
|
+
let text = theme.fg('toolTitle', theme.bold('subagent ')) + theme.fg('accent', agentName) + theme.fg('muted', ` [${scope}]`)
|
|
760
|
+
text += `\n ${theme.fg('dim', preview)}`
|
|
761
|
+
return new Text(text, 0, 0)
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
type MarkdownTheme = ReturnType<typeof getMarkdownTheme>
|
|
765
|
+
|
|
766
|
+
function aggregateUsage(results: SingleResult[]) {
|
|
767
|
+
const total = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 }
|
|
768
|
+
for (const r of results) {
|
|
769
|
+
total.input += r.usage.input
|
|
770
|
+
total.output += r.usage.output
|
|
771
|
+
total.cacheRead += r.usage.cacheRead
|
|
772
|
+
total.cacheWrite += r.usage.cacheWrite
|
|
773
|
+
total.cost += r.usage.cost
|
|
774
|
+
total.turns += r.usage.turns
|
|
775
|
+
}
|
|
776
|
+
return total
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
function renderDisplayItems(items: DisplayItem[], expanded: boolean, theme: Theme, limit?: number): string {
|
|
780
|
+
const toShow = limit ? items.slice(-limit) : items
|
|
781
|
+
const skipped = limit && items.length > limit ? items.length - limit : 0
|
|
782
|
+
let text = ''
|
|
783
|
+
if (skipped > 0) text += theme.fg('muted', `... ${skipped} earlier items\n`)
|
|
784
|
+
for (const item of toShow) {
|
|
785
|
+
if (item.type === 'text') {
|
|
786
|
+
const preview = expanded ? item.text : item.text.split('\n').slice(0, 3).join('\n')
|
|
787
|
+
text += `${theme.fg('toolOutput', preview)}\n`
|
|
788
|
+
} else {
|
|
789
|
+
text += `${theme.fg('muted', '→ ') + formatToolCall(item.name, item.args, theme.fg.bind(theme))}\n`
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
return text.trimEnd()
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
function addToolCallNodes(container: Container, items: DisplayItem[], theme: Theme): void {
|
|
796
|
+
for (const item of items) {
|
|
797
|
+
if (item.type === 'toolCall') {
|
|
798
|
+
container.addChild(new Text(theme.fg('muted', '→ ') + formatToolCall(item.name, item.args, theme.fg.bind(theme)), 0, 0))
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
function addTotalUsage(container: Container, results: SingleResult[], theme: Theme): void {
|
|
804
|
+
const usageStr = formatUsageStats(aggregateUsage(results))
|
|
805
|
+
if (usageStr) {
|
|
806
|
+
container.addChild(new Spacer(1))
|
|
807
|
+
const totalLine = theme.fg('dim', `Total: ${usageStr}`)
|
|
808
|
+
container.addChild(new Text(totalLine, 0, 0))
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
function renderSingleExpanded(r: SingleResult, isError: boolean, icon: string, theme: Theme, mdTheme: MarkdownTheme): Container {
|
|
813
|
+
const container = new Container()
|
|
814
|
+
const source = theme.fg('muted', ` (${r.agentSource})`)
|
|
815
|
+
let header = `${icon} ${theme.fg('toolTitle', theme.bold(r.agent))}${source}`
|
|
816
|
+
if (isError && r.stopReason) {
|
|
817
|
+
const reason = theme.fg('error', `[${r.stopReason}]`)
|
|
818
|
+
header += ` ${reason}`
|
|
819
|
+
}
|
|
820
|
+
container.addChild(new Text(header, 0, 0))
|
|
821
|
+
if (isError && r.errorMessage) container.addChild(new Text(theme.fg('error', `Error: ${r.errorMessage}`), 0, 0))
|
|
822
|
+
container.addChild(new Spacer(1))
|
|
823
|
+
container.addChild(new Text(theme.fg('muted', '─── Task ───'), 0, 0))
|
|
824
|
+
container.addChild(new Text(theme.fg('dim', r.task), 0, 0))
|
|
825
|
+
container.addChild(new Spacer(1))
|
|
826
|
+
container.addChild(new Text(theme.fg('muted', '─── Output ───'), 0, 0))
|
|
827
|
+
|
|
828
|
+
const displayItems = getDisplayItems(r.messages)
|
|
829
|
+
const finalOutput = getFinalOutput(r.messages)
|
|
830
|
+
if (displayItems.length === 0 && !finalOutput) {
|
|
831
|
+
container.addChild(new Text(theme.fg('muted', '(no output)'), 0, 0))
|
|
832
|
+
} else {
|
|
833
|
+
addToolCallNodes(container, displayItems, theme)
|
|
834
|
+
if (finalOutput) {
|
|
835
|
+
container.addChild(new Spacer(1))
|
|
836
|
+
container.addChild(new Markdown(finalOutput.trim(), 0, 0, mdTheme))
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
const usageStr = formatUsageStats(r.usage, r.model)
|
|
841
|
+
if (usageStr) {
|
|
842
|
+
container.addChild(new Spacer(1))
|
|
843
|
+
container.addChild(new Text(theme.fg('dim', usageStr), 0, 0))
|
|
844
|
+
}
|
|
845
|
+
return container
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
function renderSingleCollapsed(r: SingleResult, isError: boolean, icon: string, theme: Theme, expanded: boolean): Text {
|
|
849
|
+
const displayItems = getDisplayItems(r.messages)
|
|
850
|
+
const source = theme.fg('muted', ` (${r.agentSource})`)
|
|
851
|
+
let text = `${icon} ${theme.fg('toolTitle', theme.bold(r.agent))}${source}`
|
|
852
|
+
if (isError && r.stopReason) {
|
|
853
|
+
const reason = theme.fg('error', `[${r.stopReason}]`)
|
|
854
|
+
text += ` ${reason}`
|
|
855
|
+
}
|
|
856
|
+
if (isError && r.errorMessage) {
|
|
857
|
+
const errorLine = theme.fg('error', `Error: ${r.errorMessage}`)
|
|
858
|
+
text += `\n${errorLine}`
|
|
859
|
+
} else if (displayItems.length === 0) text += `\n${theme.fg('muted', '(no output)')}`
|
|
860
|
+
else {
|
|
861
|
+
text += `\n${renderDisplayItems(displayItems, expanded, theme, COLLAPSED_ITEM_COUNT)}`
|
|
862
|
+
if (displayItems.length > COLLAPSED_ITEM_COUNT) text += `\n${theme.fg('muted', '(Ctrl+O to expand)')}`
|
|
863
|
+
}
|
|
864
|
+
const usageStr = formatUsageStats(r.usage, r.model)
|
|
865
|
+
if (usageStr) text += `\n${theme.fg('dim', usageStr)}`
|
|
866
|
+
return new Text(text, 0, 0)
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
function renderSingleResult(r: SingleResult, expanded: boolean, theme: Theme, mdTheme: MarkdownTheme): Container | Text {
|
|
870
|
+
const isError = r.exitCode !== 0 || r.stopReason === 'error' || r.stopReason === 'aborted'
|
|
871
|
+
const icon = isError ? theme.fg('error', '✗') : theme.fg('success', '✓')
|
|
872
|
+
if (expanded) return renderSingleExpanded(r, isError, icon, theme, mdTheme)
|
|
873
|
+
return renderSingleCollapsed(r, isError, icon, theme, expanded)
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
function renderChainExpanded(results: SingleResult[], successCount: number, icon: string, theme: Theme, mdTheme: MarkdownTheme): Container {
|
|
877
|
+
const container = new Container()
|
|
878
|
+
const summary = theme.fg('accent', `${successCount}/${results.length} steps`)
|
|
879
|
+
container.addChild(new Text(`${icon} ${theme.fg('toolTitle', theme.bold('chain '))}${summary}`, 0, 0))
|
|
880
|
+
|
|
881
|
+
for (const r of results) {
|
|
882
|
+
const rIcon = r.exitCode === 0 ? theme.fg('success', '✓') : theme.fg('error', '✗')
|
|
883
|
+
const displayItems = getDisplayItems(r.messages)
|
|
884
|
+
const finalOutput = getFinalOutput(r.messages)
|
|
885
|
+
|
|
886
|
+
container.addChild(new Spacer(1))
|
|
887
|
+
const stepLabel = theme.fg('muted', `─── Step ${r.step}: `) + theme.fg('accent', r.agent)
|
|
888
|
+
container.addChild(new Text(`${stepLabel} ${rIcon}`, 0, 0))
|
|
889
|
+
container.addChild(new Text(theme.fg('muted', 'Task: ') + theme.fg('dim', r.task), 0, 0))
|
|
890
|
+
|
|
891
|
+
addToolCallNodes(container, displayItems, theme)
|
|
892
|
+
|
|
893
|
+
if (finalOutput) {
|
|
894
|
+
container.addChild(new Spacer(1))
|
|
895
|
+
container.addChild(new Markdown(finalOutput.trim(), 0, 0, mdTheme))
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
const stepUsage = formatUsageStats(r.usage, r.model)
|
|
899
|
+
if (stepUsage) container.addChild(new Text(theme.fg('dim', stepUsage), 0, 0))
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
addTotalUsage(container, results, theme)
|
|
903
|
+
return container
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
function renderChainCollapsed(results: SingleResult[], successCount: number, icon: string, theme: Theme, expanded: boolean): Text {
|
|
907
|
+
const summary = theme.fg('accent', `${successCount}/${results.length} steps`)
|
|
908
|
+
let text = `${icon} ${theme.fg('toolTitle', theme.bold('chain '))}${summary}`
|
|
909
|
+
for (const r of results) {
|
|
910
|
+
const rIcon = r.exitCode === 0 ? theme.fg('success', '✓') : theme.fg('error', '✗')
|
|
911
|
+
const displayItems = getDisplayItems(r.messages)
|
|
912
|
+
const stepLabel = theme.fg('muted', `─── Step ${r.step}: `)
|
|
913
|
+
text += `\n\n${stepLabel}${theme.fg('accent', r.agent)} ${rIcon}`
|
|
914
|
+
if (displayItems.length === 0) text += `\n${theme.fg('muted', '(no output)')}`
|
|
915
|
+
else text += `\n${renderDisplayItems(displayItems, expanded, theme, 5)}`
|
|
916
|
+
}
|
|
917
|
+
const usageStr = formatUsageStats(aggregateUsage(results))
|
|
918
|
+
if (usageStr) {
|
|
919
|
+
const totalLine = theme.fg('dim', `Total: ${usageStr}`)
|
|
920
|
+
text += `\n\n${totalLine}`
|
|
921
|
+
}
|
|
922
|
+
text += `\n${theme.fg('muted', '(Ctrl+O to expand)')}`
|
|
923
|
+
return new Text(text, 0, 0)
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
function renderChainResult(results: SingleResult[], expanded: boolean, theme: Theme, mdTheme: MarkdownTheme): Container | Text {
|
|
927
|
+
const successCount = results.filter((r) => r.exitCode === 0).length
|
|
928
|
+
const icon = successCount === results.length ? theme.fg('success', '✓') : theme.fg('error', '✗')
|
|
929
|
+
if (expanded) return renderChainExpanded(results, successCount, icon, theme, mdTheme)
|
|
930
|
+
return renderChainCollapsed(results, successCount, icon, theme, expanded)
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
function renderParallelExpanded(results: SingleResult[], icon: string, status: string, theme: Theme, mdTheme: MarkdownTheme): Container {
|
|
934
|
+
const container = new Container()
|
|
935
|
+
const summary = theme.fg('accent', status)
|
|
936
|
+
container.addChild(new Text(`${icon} ${theme.fg('toolTitle', theme.bold('parallel '))}${summary}`, 0, 0))
|
|
937
|
+
|
|
938
|
+
for (const r of results) {
|
|
939
|
+
const rIcon = r.exitCode === 0 ? theme.fg('success', '✓') : theme.fg('error', '✗')
|
|
940
|
+
const displayItems = getDisplayItems(r.messages)
|
|
941
|
+
const finalOutput = getFinalOutput(r.messages)
|
|
942
|
+
|
|
943
|
+
container.addChild(new Spacer(1))
|
|
944
|
+
const agentLabel = theme.fg('muted', '─── ') + theme.fg('accent', r.agent)
|
|
945
|
+
container.addChild(new Text(`${agentLabel} ${rIcon}`, 0, 0))
|
|
946
|
+
container.addChild(new Text(theme.fg('muted', 'Task: ') + theme.fg('dim', r.task), 0, 0))
|
|
947
|
+
|
|
948
|
+
addToolCallNodes(container, displayItems, theme)
|
|
949
|
+
|
|
950
|
+
if (finalOutput) {
|
|
951
|
+
container.addChild(new Spacer(1))
|
|
952
|
+
container.addChild(new Markdown(finalOutput.trim(), 0, 0, mdTheme))
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
const taskUsage = formatUsageStats(r.usage, r.model)
|
|
956
|
+
if (taskUsage) container.addChild(new Text(theme.fg('dim', taskUsage), 0, 0))
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
addTotalUsage(container, results, theme)
|
|
960
|
+
return container
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
function renderParallelCollapsed(results: SingleResult[], icon: string, status: string, theme: Theme, expanded: boolean, isRunning: boolean): Text {
|
|
964
|
+
const summary = theme.fg('accent', status)
|
|
965
|
+
let text = `${icon} ${theme.fg('toolTitle', theme.bold('parallel '))}${summary}`
|
|
966
|
+
for (const r of results) {
|
|
967
|
+
let rIcon = theme.fg('error', '✗')
|
|
968
|
+
if (r.exitCode === -1) rIcon = theme.fg('warning', '⏳')
|
|
969
|
+
else if (r.exitCode === 0) rIcon = theme.fg('success', '✓')
|
|
970
|
+
const displayItems = getDisplayItems(r.messages)
|
|
971
|
+
text += `\n\n${theme.fg('muted', '─── ')}${theme.fg('accent', r.agent)} ${rIcon}`
|
|
972
|
+
if (displayItems.length === 0) {
|
|
973
|
+
const placeholder = r.exitCode === -1 ? '(running...)' : '(no output)'
|
|
974
|
+
text += `\n${theme.fg('muted', placeholder)}`
|
|
975
|
+
} else text += `\n${renderDisplayItems(displayItems, expanded, theme, 5)}`
|
|
976
|
+
}
|
|
977
|
+
if (!isRunning) {
|
|
978
|
+
const usageStr = formatUsageStats(aggregateUsage(results))
|
|
979
|
+
if (usageStr) {
|
|
980
|
+
const totalLine = theme.fg('dim', `Total: ${usageStr}`)
|
|
981
|
+
text += `\n\n${totalLine}`
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
if (!expanded) text += `\n${theme.fg('muted', '(Ctrl+O to expand)')}`
|
|
985
|
+
return new Text(text, 0, 0)
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
function renderParallelResult(results: SingleResult[], expanded: boolean, theme: Theme, mdTheme: MarkdownTheme): Container | Text {
|
|
989
|
+
const running = results.filter((r) => r.exitCode === -1).length
|
|
990
|
+
const successCount = results.filter((r) => r.exitCode === 0).length
|
|
991
|
+
const failCount = results.filter((r) => r.exitCode > 0).length
|
|
992
|
+
const isRunning = running > 0
|
|
993
|
+
|
|
994
|
+
let icon = theme.fg('success', '✓')
|
|
995
|
+
if (isRunning) icon = theme.fg('warning', '⏳')
|
|
996
|
+
else if (failCount > 0) icon = theme.fg('warning', '◐')
|
|
997
|
+
|
|
998
|
+
let status = `${successCount}/${results.length} tasks`
|
|
999
|
+
if (isRunning) status = `${successCount + failCount}/${results.length} done, ${running} running`
|
|
1000
|
+
|
|
1001
|
+
if (expanded && !isRunning) return renderParallelExpanded(results, icon, status, theme, mdTheme)
|
|
1002
|
+
return renderParallelCollapsed(results, icon, status, theme, expanded, isRunning)
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
export default function subagentExtension(pi: ExtensionAPI) {
|
|
441
1006
|
pi.registerTool({
|
|
442
1007
|
name: 'subagent',
|
|
443
1008
|
label: 'Subagent',
|
|
@@ -454,7 +1019,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
454
1019
|
const agentScope: AgentScope = params.agentScope ?? 'user'
|
|
455
1020
|
const discovery = discoverAgents(ctx.cwd, agentScope)
|
|
456
1021
|
const agents = discovery.agents
|
|
457
|
-
const confirmProjectAgents = params.confirmProjectAgents ?? true
|
|
458
1022
|
|
|
459
1023
|
const hasChain = (params.chain?.length ?? 0) > 0
|
|
460
1024
|
const hasTasks = (params.tasks?.length ?? 0) > 0
|
|
@@ -488,218 +1052,19 @@ export default function (pi: ExtensionAPI) {
|
|
|
488
1052
|
}
|
|
489
1053
|
|
|
490
1054
|
// Gate repo-controlled project agents before any run (background included).
|
|
491
|
-
|
|
492
|
-
if (
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
const gateMode = hasChain ? 'chain' : hasTasks ? 'parallel' : 'single'
|
|
498
|
-
const gate = projectAgentGate(requestedProjectAgents.length, ctx.isProjectTrusted?.() ?? false, ctx.hasUI, confirmProjectAgents)
|
|
499
|
-
if (gate === 'refuse') {
|
|
500
|
-
const names = requestedProjectAgents.map((a) => a.name).join(', ')
|
|
501
|
-
return { content: [{ type: 'text', text: `Project-local agents (${names}) require a trusted project; refusing in non-interactive mode.` }], details: makeDetails(gateMode)([]) }
|
|
502
|
-
}
|
|
503
|
-
if (gate === 'confirm') {
|
|
504
|
-
const names = requestedProjectAgents.map((a) => a.name).join(', ')
|
|
505
|
-
const dir = discovery.projectAgentsDir ?? '(unknown)'
|
|
506
|
-
const ok = await ctx.ui.confirm('Run project-local agents?', `Agents: ${names}\nSource: ${dir}\n\nProject agents are repo-controlled. Only continue for trusted repositories.`)
|
|
507
|
-
if (!ok) return { content: [{ type: 'text', text: 'Canceled: project-local agents not approved.' }], details: makeDetails(gateMode)([]) }
|
|
508
|
-
}
|
|
1055
|
+
let gateMode: SubagentMode = 'single'
|
|
1056
|
+
if (hasChain) gateMode = 'chain'
|
|
1057
|
+
else if (hasTasks) gateMode = 'parallel'
|
|
1058
|
+
const gateResult = await checkProjectAgentGate(params, agents, ctx, discovery.projectAgentsDir, gateMode, makeDetails)
|
|
1059
|
+
if (gateResult) return gateResult
|
|
509
1060
|
|
|
510
|
-
if (params.background)
|
|
511
|
-
const task = params.task
|
|
512
|
-
const agentName = params.agent
|
|
513
|
-
if (!hasSingle || !task || !agentName) {
|
|
514
|
-
return {
|
|
515
|
-
content: [{ type: 'text', text: 'background: true requires single mode (agent + task).' }],
|
|
516
|
-
details: makeDetails('single')([]),
|
|
517
|
-
}
|
|
518
|
-
}
|
|
519
|
-
const agent = agents.find((a) => a.name === agentName)
|
|
520
|
-
if (!agent) {
|
|
521
|
-
const available = agents.map((a) => `"${a.name}"`).join(', ') || 'none'
|
|
522
|
-
return {
|
|
523
|
-
content: [{ type: 'text', text: `Unknown agent: "${agentName}". Available agents: ${available}.` }],
|
|
524
|
-
details: makeDetails('single')([]),
|
|
525
|
-
}
|
|
526
|
-
}
|
|
527
|
-
const args: string[] = ['--mode', 'json', '-p', '--no-session']
|
|
528
|
-
if (agent.model) args.push('--model', agent.model)
|
|
529
|
-
if (agent.tools && agent.tools.length > 0) args.push('--tools', agent.tools.join(','))
|
|
530
|
-
let tmpPrompt: { dir: string; filePath: string } | undefined
|
|
531
|
-
if (agent.systemPrompt.trim()) {
|
|
532
|
-
tmpPrompt = await writePromptToTempFile(agent.name, agent.systemPrompt)
|
|
533
|
-
args.push('--append-system-prompt', tmpPrompt.filePath)
|
|
534
|
-
}
|
|
535
|
-
args.push(`Task: ${task}`)
|
|
536
|
-
const invocation = getPiInvocation(args)
|
|
537
|
-
const id = startBackgroundRun(agent.name, task, { command: invocation.command, args: invocation.args, cwd: params.cwd ?? ctx.cwd }, (run) => {
|
|
538
|
-
if (tmpPrompt) {
|
|
539
|
-
try {
|
|
540
|
-
fs.unlinkSync(tmpPrompt.filePath)
|
|
541
|
-
} catch {
|
|
542
|
-
/* ignore */
|
|
543
|
-
}
|
|
544
|
-
try {
|
|
545
|
-
fs.rmdirSync(tmpPrompt.dir)
|
|
546
|
-
} catch {
|
|
547
|
-
/* ignore */
|
|
548
|
-
}
|
|
549
|
-
}
|
|
550
|
-
pi.sendMessage(
|
|
551
|
-
{
|
|
552
|
-
customType: 'subagent-background',
|
|
553
|
-
content: `Background subagent run ${run.id} (${run.agent}) ${run.state} after ${run.turns} turns.\n\n${run.output || '(no output)'}`,
|
|
554
|
-
display: true,
|
|
555
|
-
},
|
|
556
|
-
{ triggerTurn: true },
|
|
557
|
-
)
|
|
558
|
-
})
|
|
559
|
-
return {
|
|
560
|
-
content: [{ type: 'text', text: `Started background run ${id} (${agent.name}). A notification will arrive on completion; check progress with {status: true}.` }],
|
|
561
|
-
details: makeDetails('single')([]),
|
|
562
|
-
}
|
|
563
|
-
}
|
|
1061
|
+
if (params.background) return runBackgroundMode(params, agents, ctx.cwd, pi, makeDetails)
|
|
564
1062
|
|
|
565
|
-
|
|
566
|
-
const results: SingleResult[] = []
|
|
567
|
-
let previousOutput = ''
|
|
568
|
-
|
|
569
|
-
for (let i = 0; i < params.chain.length; i++) {
|
|
570
|
-
const step = params.chain[i]
|
|
571
|
-
const taskWithContext = step.task.replace(/\{previous\}/g, previousOutput)
|
|
572
|
-
|
|
573
|
-
// Create update callback that includes all previous results
|
|
574
|
-
const chainUpdate: OnUpdateCallback | undefined = onUpdate
|
|
575
|
-
? (partial) => {
|
|
576
|
-
// Combine completed results with current streaming result
|
|
577
|
-
const currentResult = partial.details?.results[0]
|
|
578
|
-
if (currentResult) {
|
|
579
|
-
const allResults = [...results, currentResult]
|
|
580
|
-
onUpdate({
|
|
581
|
-
content: partial.content,
|
|
582
|
-
details: makeDetails('chain')(allResults),
|
|
583
|
-
})
|
|
584
|
-
}
|
|
585
|
-
}
|
|
586
|
-
: undefined
|
|
587
|
-
|
|
588
|
-
const result = await runSingleAgent(ctx.cwd, agents, step.agent, taskWithContext, step.cwd, i + 1, signal, chainUpdate, makeDetails('chain'))
|
|
589
|
-
results.push(result)
|
|
590
|
-
|
|
591
|
-
const isError = result.exitCode !== 0 || result.stopReason === 'error' || result.stopReason === 'aborted'
|
|
592
|
-
if (isError) {
|
|
593
|
-
const errorMsg = result.errorMessage || result.stderr || getFinalOutput(result.messages) || '(no output)'
|
|
594
|
-
return {
|
|
595
|
-
content: [{ type: 'text', text: `Chain stopped at step ${i + 1} (${step.agent}): ${errorMsg}` }],
|
|
596
|
-
details: makeDetails('chain')(results),
|
|
597
|
-
isError: true,
|
|
598
|
-
}
|
|
599
|
-
}
|
|
600
|
-
previousOutput = getFinalOutput(result.messages)
|
|
601
|
-
}
|
|
602
|
-
return {
|
|
603
|
-
content: [{ type: 'text', text: getFinalOutput(results[results.length - 1].messages) || '(no output)' }],
|
|
604
|
-
details: makeDetails('chain')(results),
|
|
605
|
-
}
|
|
606
|
-
}
|
|
1063
|
+
const mode: ModeContext = { agents, defaultCwd: ctx.cwd, signal, onUpdate, makeDetails }
|
|
607
1064
|
|
|
608
|
-
if (params.
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
content: [
|
|
612
|
-
{
|
|
613
|
-
type: 'text',
|
|
614
|
-
text: `Too many parallel tasks (${params.tasks.length}). Max is ${MAX_PARALLEL_TASKS}.`,
|
|
615
|
-
},
|
|
616
|
-
],
|
|
617
|
-
details: makeDetails('parallel')([]),
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
// Track all results for streaming updates
|
|
621
|
-
const allResults: SingleResult[] = new Array(params.tasks.length)
|
|
622
|
-
|
|
623
|
-
// Initialize placeholder results
|
|
624
|
-
for (let i = 0; i < params.tasks.length; i++) {
|
|
625
|
-
allResults[i] = {
|
|
626
|
-
agent: params.tasks[i].agent,
|
|
627
|
-
agentSource: 'unknown',
|
|
628
|
-
task: params.tasks[i].task,
|
|
629
|
-
exitCode: -1, // -1 = still running
|
|
630
|
-
messages: [],
|
|
631
|
-
stderr: '',
|
|
632
|
-
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 },
|
|
633
|
-
}
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
const emitParallelUpdate = () => {
|
|
637
|
-
if (onUpdate) {
|
|
638
|
-
const running = allResults.filter((r) => r.exitCode === -1).length
|
|
639
|
-
const done = allResults.filter((r) => r.exitCode !== -1).length
|
|
640
|
-
onUpdate({
|
|
641
|
-
content: [{ type: 'text', text: `Parallel: ${done}/${allResults.length} done, ${running} running...` }],
|
|
642
|
-
details: makeDetails('parallel')([...allResults]),
|
|
643
|
-
})
|
|
644
|
-
}
|
|
645
|
-
}
|
|
646
|
-
|
|
647
|
-
const results = await mapWithConcurrencyLimit(params.tasks, MAX_CONCURRENCY, async (t, index) => {
|
|
648
|
-
const result = await runSingleAgent(
|
|
649
|
-
ctx.cwd,
|
|
650
|
-
agents,
|
|
651
|
-
t.agent,
|
|
652
|
-
t.task,
|
|
653
|
-
t.cwd,
|
|
654
|
-
undefined,
|
|
655
|
-
signal,
|
|
656
|
-
// Per-task update callback
|
|
657
|
-
(partial) => {
|
|
658
|
-
if (partial.details?.results[0]) {
|
|
659
|
-
allResults[index] = partial.details.results[0]
|
|
660
|
-
emitParallelUpdate()
|
|
661
|
-
}
|
|
662
|
-
},
|
|
663
|
-
makeDetails('parallel'),
|
|
664
|
-
)
|
|
665
|
-
allResults[index] = result
|
|
666
|
-
emitParallelUpdate()
|
|
667
|
-
return result
|
|
668
|
-
})
|
|
669
|
-
|
|
670
|
-
const successCount = results.filter((r) => r.exitCode === 0).length
|
|
671
|
-
const summaries = results.map((r) => {
|
|
672
|
-
const output = getFinalOutput(r.messages)
|
|
673
|
-
const preview = output.slice(0, 100) + (output.length > 100 ? '...' : '')
|
|
674
|
-
return `[${r.agent}] ${r.exitCode === 0 ? 'completed' : 'failed'}: ${preview || '(no output)'}`
|
|
675
|
-
})
|
|
676
|
-
return {
|
|
677
|
-
content: [
|
|
678
|
-
{
|
|
679
|
-
type: 'text',
|
|
680
|
-
text: `Parallel: ${successCount}/${results.length} succeeded\n\n${summaries.join('\n\n')}`,
|
|
681
|
-
},
|
|
682
|
-
],
|
|
683
|
-
details: makeDetails('parallel')(results),
|
|
684
|
-
}
|
|
685
|
-
}
|
|
686
|
-
|
|
687
|
-
if (params.agent && params.task) {
|
|
688
|
-
const result = await runSingleAgent(ctx.cwd, agents, params.agent, params.task, params.cwd, undefined, signal, onUpdate, makeDetails('single'))
|
|
689
|
-
const isError = result.exitCode !== 0 || result.stopReason === 'error' || result.stopReason === 'aborted'
|
|
690
|
-
if (isError) {
|
|
691
|
-
const errorMsg = result.errorMessage || result.stderr || getFinalOutput(result.messages) || '(no output)'
|
|
692
|
-
return {
|
|
693
|
-
content: [{ type: 'text', text: `Agent ${result.stopReason || 'failed'}: ${errorMsg}` }],
|
|
694
|
-
details: makeDetails('single')([result]),
|
|
695
|
-
isError: true,
|
|
696
|
-
}
|
|
697
|
-
}
|
|
698
|
-
return {
|
|
699
|
-
content: [{ type: 'text', text: getFinalOutput(result.messages) || '(no output)' }],
|
|
700
|
-
details: makeDetails('single')([result]),
|
|
701
|
-
}
|
|
702
|
-
}
|
|
1065
|
+
if (params.chain?.length) return runChainMode(params.chain, mode)
|
|
1066
|
+
if (params.tasks?.length) return runParallelMode(params.tasks, mode)
|
|
1067
|
+
if (params.agent && params.task) return runSingleMode(params.agent, params.task, params.cwd, mode)
|
|
703
1068
|
|
|
704
1069
|
const available = agents.map((a) => `${a.name} (${a.source})`).join(', ') || 'none'
|
|
705
1070
|
return {
|
|
@@ -710,32 +1075,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
710
1075
|
|
|
711
1076
|
renderCall(args, theme, _context) {
|
|
712
1077
|
const scope: AgentScope = args.agentScope ?? 'user'
|
|
713
|
-
if (args.chain && args.chain.length > 0)
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
const step = args.chain[i]
|
|
717
|
-
// Clean up {previous} placeholder for display
|
|
718
|
-
const cleanTask = step.task.replace(/\{previous\}/g, '').trim()
|
|
719
|
-
const preview = cleanTask.length > 40 ? `${cleanTask.slice(0, 40)}...` : cleanTask
|
|
720
|
-
text += `\n ${theme.fg('muted', `${i + 1}.`)} ${theme.fg('accent', step.agent)}${theme.fg('dim', ` ${preview}`)}`
|
|
721
|
-
}
|
|
722
|
-
if (args.chain.length > 3) text += `\n ${theme.fg('muted', `... +${args.chain.length - 3} more`)}`
|
|
723
|
-
return new Text(text, 0, 0)
|
|
724
|
-
}
|
|
725
|
-
if (args.tasks && args.tasks.length > 0) {
|
|
726
|
-
let text = theme.fg('toolTitle', theme.bold('subagent ')) + theme.fg('accent', `parallel (${args.tasks.length} tasks)`) + theme.fg('muted', ` [${scope}]`)
|
|
727
|
-
for (const t of args.tasks.slice(0, 3)) {
|
|
728
|
-
const preview = t.task.length > 40 ? `${t.task.slice(0, 40)}...` : t.task
|
|
729
|
-
text += `\n ${theme.fg('accent', t.agent)}${theme.fg('dim', ` ${preview}`)}`
|
|
730
|
-
}
|
|
731
|
-
if (args.tasks.length > 3) text += `\n ${theme.fg('muted', `... +${args.tasks.length - 3} more`)}`
|
|
732
|
-
return new Text(text, 0, 0)
|
|
733
|
-
}
|
|
734
|
-
const agentName = args.agent || '...'
|
|
735
|
-
const preview = args.task ? (args.task.length > 60 ? `${args.task.slice(0, 60)}...` : args.task) : '...'
|
|
736
|
-
let text = theme.fg('toolTitle', theme.bold('subagent ')) + theme.fg('accent', agentName) + theme.fg('muted', ` [${scope}]`)
|
|
737
|
-
text += `\n ${theme.fg('dim', preview)}`
|
|
738
|
-
return new Text(text, 0, 0)
|
|
1078
|
+
if (args.chain && args.chain.length > 0) return renderChainCall(args.chain, scope, theme)
|
|
1079
|
+
if (args.tasks && args.tasks.length > 0) return renderParallelCall(args.tasks, scope, theme)
|
|
1080
|
+
return renderSingleCall(args.agent, args.task, scope, theme)
|
|
739
1081
|
},
|
|
740
1082
|
|
|
741
1083
|
renderResult(result, { expanded }, theme, _context) {
|
|
@@ -747,204 +1089,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
747
1089
|
|
|
748
1090
|
const mdTheme = getMarkdownTheme()
|
|
749
1091
|
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
let text = ''
|
|
754
|
-
if (skipped > 0) text += theme.fg('muted', `... ${skipped} earlier items\n`)
|
|
755
|
-
for (const item of toShow) {
|
|
756
|
-
if (item.type === 'text') {
|
|
757
|
-
const preview = expanded ? item.text : item.text.split('\n').slice(0, 3).join('\n')
|
|
758
|
-
text += `${theme.fg('toolOutput', preview)}\n`
|
|
759
|
-
} else {
|
|
760
|
-
text += `${theme.fg('muted', '→ ') + formatToolCall(item.name, item.args, theme.fg.bind(theme))}\n`
|
|
761
|
-
}
|
|
762
|
-
}
|
|
763
|
-
return text.trimEnd()
|
|
764
|
-
}
|
|
765
|
-
|
|
766
|
-
if (details.mode === 'single' && details.results.length === 1) {
|
|
767
|
-
const r = details.results[0]
|
|
768
|
-
const isError = r.exitCode !== 0 || r.stopReason === 'error' || r.stopReason === 'aborted'
|
|
769
|
-
const icon = isError ? theme.fg('error', '✗') : theme.fg('success', '✓')
|
|
770
|
-
const displayItems = getDisplayItems(r.messages)
|
|
771
|
-
const finalOutput = getFinalOutput(r.messages)
|
|
772
|
-
|
|
773
|
-
if (expanded) {
|
|
774
|
-
const container = new Container()
|
|
775
|
-
let header = `${icon} ${theme.fg('toolTitle', theme.bold(r.agent))}${theme.fg('muted', ` (${r.agentSource})`)}`
|
|
776
|
-
if (isError && r.stopReason) header += ` ${theme.fg('error', `[${r.stopReason}]`)}`
|
|
777
|
-
container.addChild(new Text(header, 0, 0))
|
|
778
|
-
if (isError && r.errorMessage) container.addChild(new Text(theme.fg('error', `Error: ${r.errorMessage}`), 0, 0))
|
|
779
|
-
container.addChild(new Spacer(1))
|
|
780
|
-
container.addChild(new Text(theme.fg('muted', '─── Task ───'), 0, 0))
|
|
781
|
-
container.addChild(new Text(theme.fg('dim', r.task), 0, 0))
|
|
782
|
-
container.addChild(new Spacer(1))
|
|
783
|
-
container.addChild(new Text(theme.fg('muted', '─── Output ───'), 0, 0))
|
|
784
|
-
if (displayItems.length === 0 && !finalOutput) {
|
|
785
|
-
container.addChild(new Text(theme.fg('muted', '(no output)'), 0, 0))
|
|
786
|
-
} else {
|
|
787
|
-
for (const item of displayItems) {
|
|
788
|
-
if (item.type === 'toolCall') container.addChild(new Text(theme.fg('muted', '→ ') + formatToolCall(item.name, item.args, theme.fg.bind(theme)), 0, 0))
|
|
789
|
-
}
|
|
790
|
-
if (finalOutput) {
|
|
791
|
-
container.addChild(new Spacer(1))
|
|
792
|
-
container.addChild(new Markdown(finalOutput.trim(), 0, 0, mdTheme))
|
|
793
|
-
}
|
|
794
|
-
}
|
|
795
|
-
const usageStr = formatUsageStats(r.usage, r.model)
|
|
796
|
-
if (usageStr) {
|
|
797
|
-
container.addChild(new Spacer(1))
|
|
798
|
-
container.addChild(new Text(theme.fg('dim', usageStr), 0, 0))
|
|
799
|
-
}
|
|
800
|
-
return container
|
|
801
|
-
}
|
|
802
|
-
|
|
803
|
-
let text = `${icon} ${theme.fg('toolTitle', theme.bold(r.agent))}${theme.fg('muted', ` (${r.agentSource})`)}`
|
|
804
|
-
if (isError && r.stopReason) text += ` ${theme.fg('error', `[${r.stopReason}]`)}`
|
|
805
|
-
if (isError && r.errorMessage) text += `\n${theme.fg('error', `Error: ${r.errorMessage}`)}`
|
|
806
|
-
else if (displayItems.length === 0) text += `\n${theme.fg('muted', '(no output)')}`
|
|
807
|
-
else {
|
|
808
|
-
text += `\n${renderDisplayItems(displayItems, COLLAPSED_ITEM_COUNT)}`
|
|
809
|
-
if (displayItems.length > COLLAPSED_ITEM_COUNT) text += `\n${theme.fg('muted', '(Ctrl+O to expand)')}`
|
|
810
|
-
}
|
|
811
|
-
const usageStr = formatUsageStats(r.usage, r.model)
|
|
812
|
-
if (usageStr) text += `\n${theme.fg('dim', usageStr)}`
|
|
813
|
-
return new Text(text, 0, 0)
|
|
814
|
-
}
|
|
815
|
-
|
|
816
|
-
const aggregateUsage = (results: SingleResult[]) => {
|
|
817
|
-
const total = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 }
|
|
818
|
-
for (const r of results) {
|
|
819
|
-
total.input += r.usage.input
|
|
820
|
-
total.output += r.usage.output
|
|
821
|
-
total.cacheRead += r.usage.cacheRead
|
|
822
|
-
total.cacheWrite += r.usage.cacheWrite
|
|
823
|
-
total.cost += r.usage.cost
|
|
824
|
-
total.turns += r.usage.turns
|
|
825
|
-
}
|
|
826
|
-
return total
|
|
827
|
-
}
|
|
828
|
-
|
|
829
|
-
if (details.mode === 'chain') {
|
|
830
|
-
const successCount = details.results.filter((r) => r.exitCode === 0).length
|
|
831
|
-
const icon = successCount === details.results.length ? theme.fg('success', '✓') : theme.fg('error', '✗')
|
|
832
|
-
|
|
833
|
-
if (expanded) {
|
|
834
|
-
const container = new Container()
|
|
835
|
-
container.addChild(new Text(`${icon} ${theme.fg('toolTitle', theme.bold('chain '))}${theme.fg('accent', `${successCount}/${details.results.length} steps`)}`, 0, 0))
|
|
836
|
-
|
|
837
|
-
for (const r of details.results) {
|
|
838
|
-
const rIcon = r.exitCode === 0 ? theme.fg('success', '✓') : theme.fg('error', '✗')
|
|
839
|
-
const displayItems = getDisplayItems(r.messages)
|
|
840
|
-
const finalOutput = getFinalOutput(r.messages)
|
|
841
|
-
|
|
842
|
-
container.addChild(new Spacer(1))
|
|
843
|
-
container.addChild(new Text(`${theme.fg('muted', `─── Step ${r.step}: `) + theme.fg('accent', r.agent)} ${rIcon}`, 0, 0))
|
|
844
|
-
container.addChild(new Text(theme.fg('muted', 'Task: ') + theme.fg('dim', r.task), 0, 0))
|
|
845
|
-
|
|
846
|
-
// Show tool calls
|
|
847
|
-
for (const item of displayItems) {
|
|
848
|
-
if (item.type === 'toolCall') {
|
|
849
|
-
container.addChild(new Text(theme.fg('muted', '→ ') + formatToolCall(item.name, item.args, theme.fg.bind(theme)), 0, 0))
|
|
850
|
-
}
|
|
851
|
-
}
|
|
852
|
-
|
|
853
|
-
// Show final output as markdown
|
|
854
|
-
if (finalOutput) {
|
|
855
|
-
container.addChild(new Spacer(1))
|
|
856
|
-
container.addChild(new Markdown(finalOutput.trim(), 0, 0, mdTheme))
|
|
857
|
-
}
|
|
858
|
-
|
|
859
|
-
const stepUsage = formatUsageStats(r.usage, r.model)
|
|
860
|
-
if (stepUsage) container.addChild(new Text(theme.fg('dim', stepUsage), 0, 0))
|
|
861
|
-
}
|
|
862
|
-
|
|
863
|
-
const usageStr = formatUsageStats(aggregateUsage(details.results))
|
|
864
|
-
if (usageStr) {
|
|
865
|
-
container.addChild(new Spacer(1))
|
|
866
|
-
container.addChild(new Text(theme.fg('dim', `Total: ${usageStr}`), 0, 0))
|
|
867
|
-
}
|
|
868
|
-
return container
|
|
869
|
-
}
|
|
870
|
-
|
|
871
|
-
// Collapsed view
|
|
872
|
-
let text = `${icon} ${theme.fg('toolTitle', theme.bold('chain '))}${theme.fg('accent', `${successCount}/${details.results.length} steps`)}`
|
|
873
|
-
for (const r of details.results) {
|
|
874
|
-
const rIcon = r.exitCode === 0 ? theme.fg('success', '✓') : theme.fg('error', '✗')
|
|
875
|
-
const displayItems = getDisplayItems(r.messages)
|
|
876
|
-
text += `\n\n${theme.fg('muted', `─── Step ${r.step}: `)}${theme.fg('accent', r.agent)} ${rIcon}`
|
|
877
|
-
if (displayItems.length === 0) text += `\n${theme.fg('muted', '(no output)')}`
|
|
878
|
-
else text += `\n${renderDisplayItems(displayItems, 5)}`
|
|
879
|
-
}
|
|
880
|
-
const usageStr = formatUsageStats(aggregateUsage(details.results))
|
|
881
|
-
if (usageStr) text += `\n\n${theme.fg('dim', `Total: ${usageStr}`)}`
|
|
882
|
-
text += `\n${theme.fg('muted', '(Ctrl+O to expand)')}`
|
|
883
|
-
return new Text(text, 0, 0)
|
|
884
|
-
}
|
|
885
|
-
|
|
886
|
-
if (details.mode === 'parallel') {
|
|
887
|
-
const running = details.results.filter((r) => r.exitCode === -1).length
|
|
888
|
-
const successCount = details.results.filter((r) => r.exitCode === 0).length
|
|
889
|
-
const failCount = details.results.filter((r) => r.exitCode > 0).length
|
|
890
|
-
const isRunning = running > 0
|
|
891
|
-
const icon = isRunning ? theme.fg('warning', '⏳') : failCount > 0 ? theme.fg('warning', '◐') : theme.fg('success', '✓')
|
|
892
|
-
const status = isRunning ? `${successCount + failCount}/${details.results.length} done, ${running} running` : `${successCount}/${details.results.length} tasks`
|
|
893
|
-
|
|
894
|
-
if (expanded && !isRunning) {
|
|
895
|
-
const container = new Container()
|
|
896
|
-
container.addChild(new Text(`${icon} ${theme.fg('toolTitle', theme.bold('parallel '))}${theme.fg('accent', status)}`, 0, 0))
|
|
897
|
-
|
|
898
|
-
for (const r of details.results) {
|
|
899
|
-
const rIcon = r.exitCode === 0 ? theme.fg('success', '✓') : theme.fg('error', '✗')
|
|
900
|
-
const displayItems = getDisplayItems(r.messages)
|
|
901
|
-
const finalOutput = getFinalOutput(r.messages)
|
|
902
|
-
|
|
903
|
-
container.addChild(new Spacer(1))
|
|
904
|
-
container.addChild(new Text(`${theme.fg('muted', '─── ') + theme.fg('accent', r.agent)} ${rIcon}`, 0, 0))
|
|
905
|
-
container.addChild(new Text(theme.fg('muted', 'Task: ') + theme.fg('dim', r.task), 0, 0))
|
|
906
|
-
|
|
907
|
-
// Show tool calls
|
|
908
|
-
for (const item of displayItems) {
|
|
909
|
-
if (item.type === 'toolCall') {
|
|
910
|
-
container.addChild(new Text(theme.fg('muted', '→ ') + formatToolCall(item.name, item.args, theme.fg.bind(theme)), 0, 0))
|
|
911
|
-
}
|
|
912
|
-
}
|
|
913
|
-
|
|
914
|
-
// Show final output as markdown
|
|
915
|
-
if (finalOutput) {
|
|
916
|
-
container.addChild(new Spacer(1))
|
|
917
|
-
container.addChild(new Markdown(finalOutput.trim(), 0, 0, mdTheme))
|
|
918
|
-
}
|
|
919
|
-
|
|
920
|
-
const taskUsage = formatUsageStats(r.usage, r.model)
|
|
921
|
-
if (taskUsage) container.addChild(new Text(theme.fg('dim', taskUsage), 0, 0))
|
|
922
|
-
}
|
|
923
|
-
|
|
924
|
-
const usageStr = formatUsageStats(aggregateUsage(details.results))
|
|
925
|
-
if (usageStr) {
|
|
926
|
-
container.addChild(new Spacer(1))
|
|
927
|
-
container.addChild(new Text(theme.fg('dim', `Total: ${usageStr}`), 0, 0))
|
|
928
|
-
}
|
|
929
|
-
return container
|
|
930
|
-
}
|
|
931
|
-
|
|
932
|
-
// Collapsed view (or still running)
|
|
933
|
-
let text = `${icon} ${theme.fg('toolTitle', theme.bold('parallel '))}${theme.fg('accent', status)}`
|
|
934
|
-
for (const r of details.results) {
|
|
935
|
-
const rIcon = r.exitCode === -1 ? theme.fg('warning', '⏳') : r.exitCode === 0 ? theme.fg('success', '✓') : theme.fg('error', '✗')
|
|
936
|
-
const displayItems = getDisplayItems(r.messages)
|
|
937
|
-
text += `\n\n${theme.fg('muted', '─── ')}${theme.fg('accent', r.agent)} ${rIcon}`
|
|
938
|
-
if (displayItems.length === 0) text += `\n${theme.fg('muted', r.exitCode === -1 ? '(running...)' : '(no output)')}`
|
|
939
|
-
else text += `\n${renderDisplayItems(displayItems, 5)}`
|
|
940
|
-
}
|
|
941
|
-
if (!isRunning) {
|
|
942
|
-
const usageStr = formatUsageStats(aggregateUsage(details.results))
|
|
943
|
-
if (usageStr) text += `\n\n${theme.fg('dim', `Total: ${usageStr}`)}`
|
|
944
|
-
}
|
|
945
|
-
if (!expanded) text += `\n${theme.fg('muted', '(Ctrl+O to expand)')}`
|
|
946
|
-
return new Text(text, 0, 0)
|
|
947
|
-
}
|
|
1092
|
+
if (details.mode === 'single' && details.results.length === 1) return renderSingleResult(details.results[0], expanded, theme, mdTheme)
|
|
1093
|
+
if (details.mode === 'chain') return renderChainResult(details.results, expanded, theme, mdTheme)
|
|
1094
|
+
if (details.mode === 'parallel') return renderParallelResult(details.results, expanded, theme, mdTheme)
|
|
948
1095
|
|
|
949
1096
|
const text = result.content[0]
|
|
950
1097
|
return new Text(text?.type === 'text' ? text.text : '(no output)', 0, 0)
|