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