@leing2021/super-pi 0.23.9 → 0.23.10
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/extensions/ce-core/index.ts +77 -38
- package/extensions/ce-core/tools/parallel-subagent.ts +126 -36
- package/extensions/ce-core/tools/subagent-events.ts +287 -0
- package/extensions/ce-core/tools/subagent-json-runner.ts +175 -0
- package/extensions/ce-core/tools/subagent-renderer.ts +453 -0
- package/extensions/ce-core/tools/subagent.ts +97 -21
- package/package.json +1 -1
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spawn-based JSON runner for ce_subagent / ce_parallel_subagent.
|
|
3
|
+
*
|
|
4
|
+
* Spawns `pi --mode json --no-session ... -p <prompt>`, parses stdout
|
|
5
|
+
* JSON events in real-time, and triggers an onUpdate callback for
|
|
6
|
+
* live TUI progress.
|
|
7
|
+
*
|
|
8
|
+
* Key design decisions:
|
|
9
|
+
* - Per-process env via spawn options, NO global process.env mutation.
|
|
10
|
+
* - Injectable spawnFactory for testing.
|
|
11
|
+
* - AbortSignal support for Ctrl+C cancellation.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { type ChildProcess, spawn } from "node:child_process"
|
|
15
|
+
import {
|
|
16
|
+
type SingleResult,
|
|
17
|
+
parseJsonEvent,
|
|
18
|
+
applyEventToResult,
|
|
19
|
+
makeInitialResult,
|
|
20
|
+
} from "./subagent-events"
|
|
21
|
+
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
// Types
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
export interface SpawnInput {
|
|
27
|
+
cwd?: string
|
|
28
|
+
env?: Record<string, string | undefined>
|
|
29
|
+
stdio?: any
|
|
30
|
+
shell?: boolean
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export type SpawnFactory = (
|
|
34
|
+
command: string,
|
|
35
|
+
args: string[],
|
|
36
|
+
options: SpawnInput,
|
|
37
|
+
) => ChildProcessLike
|
|
38
|
+
|
|
39
|
+
export interface ChildProcessLike {
|
|
40
|
+
stdout: { on(event: string, cb: (data: Buffer) => void): void }
|
|
41
|
+
stderr: { on(event: string, cb: (data: Buffer) => void): void }
|
|
42
|
+
on(event: string, cb: (code: number) => void): void
|
|
43
|
+
on(event: string, cb: () => void): void
|
|
44
|
+
kill(signal?: string): void
|
|
45
|
+
readonly killed: boolean
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface JsonRunnerOptions {
|
|
49
|
+
/** Override spawn for testing. Defaults to real child_process.spawn */
|
|
50
|
+
spawnFactory?: SpawnFactory
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface JsonRunConfig {
|
|
54
|
+
prompt: string
|
|
55
|
+
agent: string
|
|
56
|
+
task: string
|
|
57
|
+
cwd: string
|
|
58
|
+
step?: number
|
|
59
|
+
extraFlags?: string[]
|
|
60
|
+
extraEnv?: Record<string, string>
|
|
61
|
+
signal?: AbortSignal
|
|
62
|
+
onUpdate?: (partial: SingleResult) => void
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
// Default spawn factory
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
|
|
69
|
+
const realSpawn: SpawnFactory = (
|
|
70
|
+
command: string,
|
|
71
|
+
args: string[],
|
|
72
|
+
options: SpawnInput,
|
|
73
|
+
): ChildProcessLike => {
|
|
74
|
+
return spawn(command, args, {
|
|
75
|
+
cwd: options.cwd,
|
|
76
|
+
env: options.env as Record<string, string>,
|
|
77
|
+
stdio: options.stdio ?? ["ignore", "pipe", "pipe"],
|
|
78
|
+
shell: options.shell ?? false,
|
|
79
|
+
}) as unknown as ChildProcessLike
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
// Runner
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
|
|
86
|
+
export function createJsonRunner(opts: JsonRunnerOptions = {}) {
|
|
87
|
+
const doSpawn = opts.spawnFactory ?? realSpawn
|
|
88
|
+
|
|
89
|
+
async function run(config: JsonRunConfig): Promise<SingleResult> {
|
|
90
|
+
const result = makeInitialResult(config.agent, config.task, config.step)
|
|
91
|
+
|
|
92
|
+
const args = buildArgs(config.prompt, config.extraFlags)
|
|
93
|
+
const env = buildEnv(config.extraEnv)
|
|
94
|
+
|
|
95
|
+
const emitUpdate = () => {
|
|
96
|
+
config.onUpdate?.(result)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
let wasAborted = false
|
|
100
|
+
|
|
101
|
+
const exitCode = await new Promise<number>((resolve) => {
|
|
102
|
+
const proc = doSpawn("pi", args, {
|
|
103
|
+
cwd: config.cwd,
|
|
104
|
+
env,
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
let buffer = ""
|
|
108
|
+
|
|
109
|
+
const processLine = (line: string) => {
|
|
110
|
+
const event = parseJsonEvent(line)
|
|
111
|
+
if (!event) return
|
|
112
|
+
applyEventToResult(result, event)
|
|
113
|
+
emitUpdate()
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
proc.stdout.on("data", (data: Buffer) => {
|
|
117
|
+
buffer += data.toString()
|
|
118
|
+
const lines = buffer.split("\n")
|
|
119
|
+
buffer = lines.pop() || ""
|
|
120
|
+
for (const line of lines) processLine(line)
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
proc.stderr.on("data", (data: Buffer) => {
|
|
124
|
+
result.stderr += data.toString()
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
proc.on("close", (code: number) => {
|
|
128
|
+
if (buffer.trim()) processLine(buffer)
|
|
129
|
+
resolve(code ?? 0)
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
proc.on("error", () => {
|
|
133
|
+
resolve(1)
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
if (config.signal) {
|
|
137
|
+
const killProc = () => {
|
|
138
|
+
wasAborted = true
|
|
139
|
+
proc.kill("SIGTERM")
|
|
140
|
+
setTimeout(() => {
|
|
141
|
+
if (!proc.killed) proc.kill("SIGKILL")
|
|
142
|
+
}, 5000)
|
|
143
|
+
}
|
|
144
|
+
if (config.signal.aborted) killProc()
|
|
145
|
+
else config.signal.addEventListener("abort", killProc, { once: true })
|
|
146
|
+
}
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
result.exitCode = exitCode
|
|
150
|
+
if (wasAborted) throw new Error("Subagent was aborted")
|
|
151
|
+
return result
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return { run }
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// ---------------------------------------------------------------------------
|
|
158
|
+
// Helpers
|
|
159
|
+
// ---------------------------------------------------------------------------
|
|
160
|
+
|
|
161
|
+
function buildArgs(prompt: string, extraFlags?: string[]): string[] {
|
|
162
|
+
const args: string[] = ["--mode", "json", "--no-session"]
|
|
163
|
+
if (extraFlags && extraFlags.length > 0) {
|
|
164
|
+
args.push(...extraFlags)
|
|
165
|
+
}
|
|
166
|
+
args.push("-p", prompt)
|
|
167
|
+
return args
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function buildEnv(extraEnv?: Record<string, string>): Record<string, string | undefined> {
|
|
171
|
+
return {
|
|
172
|
+
...process.env,
|
|
173
|
+
...(extraEnv ?? {}),
|
|
174
|
+
}
|
|
175
|
+
}
|
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TUI renderers for ce_subagent and ce_parallel_subagent.
|
|
3
|
+
*
|
|
4
|
+
* Displays real-time status in pi tool frames:
|
|
5
|
+
* - Collapsed: status icon + agent name + recent tool calls + usage
|
|
6
|
+
* - Expanded (Ctrl+O): full tool calls + final output + per-task usage
|
|
7
|
+
*
|
|
8
|
+
* Tool call formatting (formatToolCall) adapted directly from pi official
|
|
9
|
+
* subagent example with super-pi skill semantic adaptations.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import * as os from "node:os"
|
|
13
|
+
import { Container, Markdown, Spacer, Text } from "@mariozechner/pi-tui"
|
|
14
|
+
import {
|
|
15
|
+
type SingleResult,
|
|
16
|
+
getFinalOutput,
|
|
17
|
+
getDisplayItems,
|
|
18
|
+
isFailedResult,
|
|
19
|
+
formatUsageStats,
|
|
20
|
+
type DisplayItem,
|
|
21
|
+
} from "./subagent-events"
|
|
22
|
+
import type { SubagentLiveDetails } from "./subagent"
|
|
23
|
+
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
// Constants
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
|
|
28
|
+
const COLLAPSED_ITEM_COUNT = 10
|
|
29
|
+
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
// Tool call formatting (from pi official subagent example)
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
export function formatToolCall(
|
|
35
|
+
toolName: string,
|
|
36
|
+
args: Record<string, unknown>,
|
|
37
|
+
themeFg: (color: any, text: string) => string,
|
|
38
|
+
): string {
|
|
39
|
+
const shortenPath = (p: string) => {
|
|
40
|
+
const home = os.homedir()
|
|
41
|
+
return p.startsWith(home) ? `~${p.slice(home.length)}` : p
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
switch (toolName) {
|
|
45
|
+
case "bash": {
|
|
46
|
+
const command = (args.command as string) || "..."
|
|
47
|
+
const preview = command.length > 60 ? `${command.slice(0, 60)}...` : command
|
|
48
|
+
return themeFg("muted", "$ ") + themeFg("toolOutput", preview)
|
|
49
|
+
}
|
|
50
|
+
case "read": {
|
|
51
|
+
const rawPath = (args.file_path || args.path || "...") as string
|
|
52
|
+
const filePath = shortenPath(rawPath)
|
|
53
|
+
const offset = args.offset as number | undefined
|
|
54
|
+
const limit = args.limit as number | undefined
|
|
55
|
+
let text = themeFg("accent", filePath)
|
|
56
|
+
if (offset !== undefined || limit !== undefined) {
|
|
57
|
+
const startLine = offset ?? 1
|
|
58
|
+
const endLine = limit !== undefined ? startLine + limit - 1 : ""
|
|
59
|
+
text += themeFg("warning", `:${startLine}${endLine ? `-${endLine}` : ""}`)
|
|
60
|
+
}
|
|
61
|
+
return themeFg("muted", "read ") + text
|
|
62
|
+
}
|
|
63
|
+
case "write": {
|
|
64
|
+
const rawPath = (args.file_path || args.path || "...") as string
|
|
65
|
+
const filePath = shortenPath(rawPath)
|
|
66
|
+
const content = (args.content || "") as string
|
|
67
|
+
const lines = content.split("\n").length
|
|
68
|
+
let text = themeFg("muted", "write ") + themeFg("accent", filePath)
|
|
69
|
+
if (lines > 1) text += themeFg("dim", ` (${lines} lines)`)
|
|
70
|
+
return text
|
|
71
|
+
}
|
|
72
|
+
case "edit": {
|
|
73
|
+
const rawPath = (args.file_path || args.path || "...") as string
|
|
74
|
+
return themeFg("muted", "edit ") + themeFg("accent", shortenPath(rawPath))
|
|
75
|
+
}
|
|
76
|
+
case "ls": {
|
|
77
|
+
const rawPath = (args.path || ".") as string
|
|
78
|
+
return themeFg("muted", "ls ") + themeFg("accent", shortenPath(rawPath))
|
|
79
|
+
}
|
|
80
|
+
case "find": {
|
|
81
|
+
const pattern = (args.pattern || "*") as string
|
|
82
|
+
const rawPath = (args.path || ".") as string
|
|
83
|
+
return themeFg("muted", "find ") + themeFg("accent", pattern) + themeFg("dim", ` in ${shortenPath(rawPath)}`)
|
|
84
|
+
}
|
|
85
|
+
case "grep": {
|
|
86
|
+
const pattern = (args.pattern || "") as string
|
|
87
|
+
const rawPath = (args.path || ".") as string
|
|
88
|
+
return (
|
|
89
|
+
themeFg("muted", "grep ") +
|
|
90
|
+
themeFg("accent", `/${pattern}/`) +
|
|
91
|
+
themeFg("dim", ` in ${shortenPath(rawPath)}`)
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
default: {
|
|
95
|
+
const argsStr = JSON.stringify(args)
|
|
96
|
+
const preview = argsStr.length > 50 ? `${argsStr.slice(0, 50)}...` : argsStr
|
|
97
|
+
return themeFg("accent", toolName) + themeFg("dim", ` ${preview}`)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ---------------------------------------------------------------------------
|
|
103
|
+
// Theme type
|
|
104
|
+
// ---------------------------------------------------------------------------
|
|
105
|
+
|
|
106
|
+
interface Theme {
|
|
107
|
+
fg: (color: any, text: string) => string
|
|
108
|
+
bold: (text: string) => string
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ---------------------------------------------------------------------------
|
|
112
|
+
// renderCall
|
|
113
|
+
// ---------------------------------------------------------------------------
|
|
114
|
+
|
|
115
|
+
export function renderSubagentCall(
|
|
116
|
+
args: {
|
|
117
|
+
agent?: string
|
|
118
|
+
task?: string
|
|
119
|
+
chain?: Array<{ agent: string; task: string }>
|
|
120
|
+
tasks?: Array<{ agent: string; task: string }>
|
|
121
|
+
},
|
|
122
|
+
theme: Theme,
|
|
123
|
+
): any {
|
|
124
|
+
if (args.chain && args.chain.length > 0) {
|
|
125
|
+
let text =
|
|
126
|
+
theme.fg("toolTitle", theme.bold("ce_subagent ")) +
|
|
127
|
+
theme.fg("accent", `chain (${args.chain.length} steps)`)
|
|
128
|
+
for (let i = 0; i < Math.min(args.chain.length, 3); i++) {
|
|
129
|
+
const step = args.chain[i]
|
|
130
|
+
const cleanTask = step.task.replace(/\{previous\}/g, "").trim()
|
|
131
|
+
const preview = cleanTask.length > 40 ? `${cleanTask.slice(0, 40)}...` : cleanTask
|
|
132
|
+
text +=
|
|
133
|
+
"\n " +
|
|
134
|
+
theme.fg("muted", `${i + 1}.`) +
|
|
135
|
+
" " +
|
|
136
|
+
theme.fg("accent", step.agent) +
|
|
137
|
+
theme.fg("dim", ` ${preview}`)
|
|
138
|
+
}
|
|
139
|
+
if (args.chain.length > 3) text += `\n ${theme.fg("muted", `... +${args.chain.length - 3} more`)}`
|
|
140
|
+
return new Text(text, 0, 0)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (args.tasks && args.tasks.length > 0) {
|
|
144
|
+
let text =
|
|
145
|
+
theme.fg("toolTitle", theme.bold("ce_parallel_subagent ")) +
|
|
146
|
+
theme.fg("accent", `parallel (${args.tasks.length} tasks)`)
|
|
147
|
+
for (const t of args.tasks.slice(0, 3)) {
|
|
148
|
+
const preview = t.task.length > 40 ? `${t.task.slice(0, 40)}...` : t.task
|
|
149
|
+
text += `\n ${theme.fg("accent", t.agent)}${theme.fg("dim", ` ${preview}`)}`
|
|
150
|
+
}
|
|
151
|
+
if (args.tasks.length > 3) text += `\n ${theme.fg("muted", `... +${args.tasks.length - 3} more`)}`
|
|
152
|
+
return new Text(text, 0, 0)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Single mode
|
|
156
|
+
const agentName = args.agent || "..."
|
|
157
|
+
const preview = args.task ? (args.task.length > 60 ? `${args.task.slice(0, 60)}...` : args.task) : "..."
|
|
158
|
+
let text =
|
|
159
|
+
theme.fg("toolTitle", theme.bold("ce_subagent ")) +
|
|
160
|
+
theme.fg("accent", agentName)
|
|
161
|
+
text += `\n ${theme.fg("dim", preview)}`
|
|
162
|
+
return new Text(text, 0, 0)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ---------------------------------------------------------------------------
|
|
166
|
+
// renderResult
|
|
167
|
+
// ---------------------------------------------------------------------------
|
|
168
|
+
|
|
169
|
+
interface RenderContext {
|
|
170
|
+
expanded: boolean
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function renderSubagentResult(
|
|
174
|
+
details: SubagentLiveDetails,
|
|
175
|
+
context: RenderContext,
|
|
176
|
+
theme: Theme,
|
|
177
|
+
): any {
|
|
178
|
+
if (!details || !details.results || details.results.length === 0) {
|
|
179
|
+
return new Text("(no output)", 0, 0)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const mdTheme = getMarkdownTheme()
|
|
183
|
+
|
|
184
|
+
const renderDisplayItems = (items: DisplayItem[], limit?: number) => {
|
|
185
|
+
const toShow = limit ? items.slice(-limit) : items
|
|
186
|
+
const skipped = limit && items.length > limit ? items.length - limit : 0
|
|
187
|
+
let text = ""
|
|
188
|
+
if (skipped > 0) text += theme.fg("muted", `... ${skipped} earlier items\n`)
|
|
189
|
+
for (const item of toShow) {
|
|
190
|
+
if (item.type === "text") {
|
|
191
|
+
const preview = context.expanded ? item.text : item.text.split("\n").slice(0, 3).join("\n")
|
|
192
|
+
text += `${theme.fg("toolOutput", preview)}\n`
|
|
193
|
+
} else {
|
|
194
|
+
text += `${theme.fg("muted", "→ ") + formatToolCall(item.name, item.args, theme.fg.bind(theme))}\n`
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return text.trimEnd()
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// --- Single mode ---
|
|
201
|
+
if (details.mode === "single" && details.results.length === 1) {
|
|
202
|
+
return renderSingleResult(details.results[0], context, theme, mdTheme, renderDisplayItems)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// --- Chain mode ---
|
|
206
|
+
if (details.mode === "chain") {
|
|
207
|
+
return renderChainResult(details.results, context, theme, mdTheme, renderDisplayItems)
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// --- Parallel mode ---
|
|
211
|
+
if (details.mode === "parallel") {
|
|
212
|
+
return renderParallelResult(details.results, context, theme, mdTheme, renderDisplayItems)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return new Text("(no output)", 0, 0)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// ---------------------------------------------------------------------------
|
|
219
|
+
// Single result renderer
|
|
220
|
+
// ---------------------------------------------------------------------------
|
|
221
|
+
|
|
222
|
+
function renderSingleResult(
|
|
223
|
+
r: SingleResult,
|
|
224
|
+
context: RenderContext,
|
|
225
|
+
theme: Theme,
|
|
226
|
+
mdTheme: any,
|
|
227
|
+
renderDisplayItems: (items: DisplayItem[], limit?: number) => string,
|
|
228
|
+
): any {
|
|
229
|
+
const isError = isFailedResult(r)
|
|
230
|
+
const icon = isError ? theme.fg("error", "✗") : r.exitCode === -1 ? theme.fg("warning", "⏳") : theme.fg("success", "✓")
|
|
231
|
+
const displayItems = getDisplayItems(r.messages)
|
|
232
|
+
const finalOutput = getFinalOutput(r.messages)
|
|
233
|
+
|
|
234
|
+
if (context.expanded) {
|
|
235
|
+
const container = new Container()
|
|
236
|
+
let header = `${icon} ${theme.fg("toolTitle", theme.bold(r.agent))}`
|
|
237
|
+
if (isError && r.stopReason) header += ` ${theme.fg("error", `[${r.stopReason}]`)}`
|
|
238
|
+
container.addChild(new Text(header, 0, 0))
|
|
239
|
+
if (isError && r.errorMessage)
|
|
240
|
+
container.addChild(new Text(theme.fg("error", `Error: ${r.errorMessage}`), 0, 0))
|
|
241
|
+
container.addChild(new Spacer(1))
|
|
242
|
+
container.addChild(new Text(theme.fg("muted", "─── Task ───"), 0, 0))
|
|
243
|
+
container.addChild(new Text(theme.fg("dim", r.task), 0, 0))
|
|
244
|
+
container.addChild(new Spacer(1))
|
|
245
|
+
container.addChild(new Text(theme.fg("muted", "─── Output ───"), 0, 0))
|
|
246
|
+
if (displayItems.length === 0 && !finalOutput) {
|
|
247
|
+
container.addChild(new Text(theme.fg("muted", r.exitCode === -1 ? "(running...)" : "(no output)"), 0, 0))
|
|
248
|
+
} else {
|
|
249
|
+
for (const item of displayItems) {
|
|
250
|
+
if (item.type === "toolCall")
|
|
251
|
+
container.addChild(
|
|
252
|
+
new Text(theme.fg("muted", "→ ") + formatToolCall(item.name, item.args, theme.fg.bind(theme)), 0, 0),
|
|
253
|
+
)
|
|
254
|
+
}
|
|
255
|
+
if (finalOutput) {
|
|
256
|
+
container.addChild(new Spacer(1))
|
|
257
|
+
container.addChild(new Markdown(finalOutput.trim(), 0, 0, mdTheme))
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
const usageStr = formatUsageStats(r.usage, r.model)
|
|
261
|
+
if (usageStr) {
|
|
262
|
+
container.addChild(new Spacer(1))
|
|
263
|
+
container.addChild(new Text(theme.fg("dim", usageStr), 0, 0))
|
|
264
|
+
}
|
|
265
|
+
return container
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Collapsed
|
|
269
|
+
let text = `${icon} ${theme.fg("toolTitle", theme.bold(r.agent))}`
|
|
270
|
+
if (isError && r.stopReason) text += ` ${theme.fg("error", `[${r.stopReason}]`)}`
|
|
271
|
+
if (isError && r.errorMessage) text += `\n${theme.fg("error", `Error: ${r.errorMessage}`)}`
|
|
272
|
+
else if (r.exitCode === -1) text += `\n${theme.fg("muted", "(running...)")}`
|
|
273
|
+
else if (displayItems.length === 0) text += `\n${theme.fg("muted", "(no output)")}`
|
|
274
|
+
else {
|
|
275
|
+
text += `\n${renderDisplayItems(displayItems, COLLAPSED_ITEM_COUNT)}`
|
|
276
|
+
if (displayItems.length > COLLAPSED_ITEM_COUNT) text += `\n${theme.fg("muted", "(Ctrl+O to expand)")}`
|
|
277
|
+
}
|
|
278
|
+
const usageStr = formatUsageStats(r.usage, r.model)
|
|
279
|
+
if (usageStr) text += `\n${theme.fg("dim", usageStr)}`
|
|
280
|
+
return new Text(text, 0, 0)
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// ---------------------------------------------------------------------------
|
|
284
|
+
// Chain result renderer
|
|
285
|
+
// ---------------------------------------------------------------------------
|
|
286
|
+
|
|
287
|
+
function aggregateUsage(results: SingleResult[]) {
|
|
288
|
+
const total = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 }
|
|
289
|
+
for (const r of results) {
|
|
290
|
+
total.input += r.usage.input
|
|
291
|
+
total.output += r.usage.output
|
|
292
|
+
total.cacheRead += r.usage.cacheRead
|
|
293
|
+
total.cacheWrite += r.usage.cacheWrite
|
|
294
|
+
total.cost += r.usage.cost
|
|
295
|
+
total.contextTokens += r.usage.contextTokens
|
|
296
|
+
total.turns += r.usage.turns
|
|
297
|
+
}
|
|
298
|
+
return total
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function renderChainResult(
|
|
302
|
+
results: SingleResult[],
|
|
303
|
+
context: RenderContext,
|
|
304
|
+
theme: Theme,
|
|
305
|
+
mdTheme: any,
|
|
306
|
+
renderDisplayItems: (items: DisplayItem[], limit?: number) => string,
|
|
307
|
+
): any {
|
|
308
|
+
const successCount = results.filter(r => r.exitCode === 0).length
|
|
309
|
+
const icon = successCount === results.length ? theme.fg("success", "✓") : theme.fg("error", "✗")
|
|
310
|
+
|
|
311
|
+
if (context.expanded) {
|
|
312
|
+
const container = new Container()
|
|
313
|
+
container.addChild(
|
|
314
|
+
new Text(
|
|
315
|
+
`${icon} ${theme.fg("toolTitle", theme.bold("chain "))}${theme.fg("accent", `${successCount}/${results.length} steps`)}`,
|
|
316
|
+
0, 0,
|
|
317
|
+
),
|
|
318
|
+
)
|
|
319
|
+
for (const r of results) {
|
|
320
|
+
const rIcon = r.exitCode === 0 ? theme.fg("success", "✓") : theme.fg("error", "✗")
|
|
321
|
+
const displayItems = getDisplayItems(r.messages)
|
|
322
|
+
const finalOutput = getFinalOutput(r.messages)
|
|
323
|
+
|
|
324
|
+
container.addChild(new Spacer(1))
|
|
325
|
+
container.addChild(new Text(`${theme.fg("muted", `─── Step ${r.step ?? "?"}: `)}${theme.fg("accent", r.agent)} ${rIcon}`, 0, 0))
|
|
326
|
+
container.addChild(new Text(theme.fg("muted", "Task: ") + theme.fg("dim", r.task), 0, 0))
|
|
327
|
+
for (const item of displayItems) {
|
|
328
|
+
if (item.type === "toolCall")
|
|
329
|
+
container.addChild(new Text(theme.fg("muted", "→ ") + formatToolCall(item.name, item.args, theme.fg.bind(theme)), 0, 0))
|
|
330
|
+
}
|
|
331
|
+
if (finalOutput) {
|
|
332
|
+
container.addChild(new Spacer(1))
|
|
333
|
+
container.addChild(new Markdown(finalOutput.trim(), 0, 0, mdTheme))
|
|
334
|
+
}
|
|
335
|
+
const stepUsage = formatUsageStats(r.usage, r.model)
|
|
336
|
+
if (stepUsage) container.addChild(new Text(theme.fg("dim", stepUsage), 0, 0))
|
|
337
|
+
}
|
|
338
|
+
const usageStr = formatUsageStats(aggregateUsage(results))
|
|
339
|
+
if (usageStr) {
|
|
340
|
+
container.addChild(new Spacer(1))
|
|
341
|
+
container.addChild(new Text(theme.fg("dim", `Total: ${usageStr}`), 0, 0))
|
|
342
|
+
}
|
|
343
|
+
return container
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Collapsed
|
|
347
|
+
let text = `${icon} ${theme.fg("toolTitle", theme.bold("chain "))}${theme.fg("accent", `${successCount}/${results.length} steps`)}`
|
|
348
|
+
for (const r of results) {
|
|
349
|
+
const rIcon = r.exitCode === 0 ? theme.fg("success", "✓") : theme.fg("error", "✗")
|
|
350
|
+
const displayItems = getDisplayItems(r.messages)
|
|
351
|
+
text += `\n\n${theme.fg("muted", `─── Step ${r.step ?? "?"}: `)}${theme.fg("accent", r.agent)} ${rIcon}`
|
|
352
|
+
if (displayItems.length === 0) text += `\n${theme.fg("muted", "(no output)")}`
|
|
353
|
+
else text += `\n${renderDisplayItems(displayItems, 5)}`
|
|
354
|
+
}
|
|
355
|
+
const usageStr = formatUsageStats(aggregateUsage(results))
|
|
356
|
+
if (usageStr) text += `\n\n${theme.fg("dim", `Total: ${usageStr}`)}`
|
|
357
|
+
text += `\n${theme.fg("muted", "(Ctrl+O to expand)")}`
|
|
358
|
+
return new Text(text, 0, 0)
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// ---------------------------------------------------------------------------
|
|
362
|
+
// Parallel result renderer
|
|
363
|
+
// ---------------------------------------------------------------------------
|
|
364
|
+
|
|
365
|
+
function renderParallelResult(
|
|
366
|
+
results: SingleResult[],
|
|
367
|
+
context: RenderContext,
|
|
368
|
+
theme: Theme,
|
|
369
|
+
mdTheme: any,
|
|
370
|
+
renderDisplayItems: (items: DisplayItem[], limit?: number) => string,
|
|
371
|
+
): any {
|
|
372
|
+
const running = results.filter(r => r.exitCode === -1).length
|
|
373
|
+
const successCount = results.filter(r => r.exitCode !== -1 && !isFailedResult(r)).length
|
|
374
|
+
const failCount = results.filter(r => r.exitCode !== -1 && isFailedResult(r)).length
|
|
375
|
+
const isRunning = running > 0
|
|
376
|
+
const icon = isRunning
|
|
377
|
+
? theme.fg("warning", "⏳")
|
|
378
|
+
: failCount > 0
|
|
379
|
+
? theme.fg("warning", "◐")
|
|
380
|
+
: theme.fg("success", "✓")
|
|
381
|
+
const status = isRunning
|
|
382
|
+
? `${successCount + failCount}/${results.length} done, ${running} running`
|
|
383
|
+
: `${successCount}/${results.length} tasks`
|
|
384
|
+
|
|
385
|
+
if (context.expanded && !isRunning) {
|
|
386
|
+
const container = new Container()
|
|
387
|
+
container.addChild(new Text(`${icon} ${theme.fg("toolTitle", theme.bold("parallel "))}${theme.fg("accent", status)}`, 0, 0))
|
|
388
|
+
|
|
389
|
+
for (const r of results) {
|
|
390
|
+
const rIcon = isFailedResult(r) ? theme.fg("error", "✗") : theme.fg("success", "✓")
|
|
391
|
+
const displayItems = getDisplayItems(r.messages)
|
|
392
|
+
const finalOutput = getFinalOutput(r.messages)
|
|
393
|
+
|
|
394
|
+
container.addChild(new Spacer(1))
|
|
395
|
+
container.addChild(new Text(`${theme.fg("muted", "─── ")}${theme.fg("accent", r.agent)} ${rIcon}`, 0, 0))
|
|
396
|
+
container.addChild(new Text(theme.fg("muted", "Task: ") + theme.fg("dim", r.task), 0, 0))
|
|
397
|
+
for (const item of displayItems) {
|
|
398
|
+
if (item.type === "toolCall")
|
|
399
|
+
container.addChild(new Text(theme.fg("muted", "→ ") + formatToolCall(item.name, item.args, theme.fg.bind(theme)), 0, 0))
|
|
400
|
+
}
|
|
401
|
+
if (finalOutput) {
|
|
402
|
+
container.addChild(new Spacer(1))
|
|
403
|
+
container.addChild(new Markdown(finalOutput.trim(), 0, 0, mdTheme))
|
|
404
|
+
}
|
|
405
|
+
const taskUsage = formatUsageStats(r.usage, r.model)
|
|
406
|
+
if (taskUsage) container.addChild(new Text(theme.fg("dim", taskUsage), 0, 0))
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const usageStr = formatUsageStats(aggregateUsage(results))
|
|
410
|
+
if (usageStr) {
|
|
411
|
+
container.addChild(new Spacer(1))
|
|
412
|
+
container.addChild(new Text(theme.fg("dim", `Total: ${usageStr}`), 0, 0))
|
|
413
|
+
}
|
|
414
|
+
return container
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// Collapsed (or still running)
|
|
418
|
+
let text = `${icon} ${theme.fg("toolTitle", theme.bold("parallel "))}${theme.fg("accent", status)}`
|
|
419
|
+
for (const r of results) {
|
|
420
|
+
const rIcon =
|
|
421
|
+
r.exitCode === -1
|
|
422
|
+
? theme.fg("warning", "⏳")
|
|
423
|
+
: isFailedResult(r)
|
|
424
|
+
? theme.fg("error", "✗")
|
|
425
|
+
: theme.fg("success", "✓")
|
|
426
|
+
const displayItems = getDisplayItems(r.messages)
|
|
427
|
+
text += `\n\n${theme.fg("muted", "─── ")}${theme.fg("accent", r.agent)} ${rIcon}`
|
|
428
|
+
if (displayItems.length === 0)
|
|
429
|
+
text += `\n${theme.fg("muted", r.exitCode === -1 ? "(running...)" : "(no output)")}`
|
|
430
|
+
else text += `\n${renderDisplayItems(displayItems, 5)}`
|
|
431
|
+
}
|
|
432
|
+
if (!isRunning) {
|
|
433
|
+
const usageStr = formatUsageStats(aggregateUsage(results))
|
|
434
|
+
if (usageStr) text += `\n\n${theme.fg("dim", `Total: ${usageStr}`)}`
|
|
435
|
+
}
|
|
436
|
+
if (!context.expanded) text += `\n${theme.fg("muted", "(Ctrl+O to expand)")}`
|
|
437
|
+
return new Text(text, 0, 0)
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// ---------------------------------------------------------------------------
|
|
441
|
+
// Markdown theme helper
|
|
442
|
+
// ---------------------------------------------------------------------------
|
|
443
|
+
|
|
444
|
+
function getMarkdownTheme(): any {
|
|
445
|
+
// Return a simple theme for testing / default usage
|
|
446
|
+
// In production, pi provides getMarkdownTheme()
|
|
447
|
+
try {
|
|
448
|
+
const { getMarkdownTheme: gmt } = require("@mariozechner/pi-coding-agent")
|
|
449
|
+
return gmt()
|
|
450
|
+
} catch {
|
|
451
|
+
return undefined
|
|
452
|
+
}
|
|
453
|
+
}
|