@jerryan/pi-subagent-tools 0.1.1 → 0.4.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/index.ts CHANGED
@@ -1,429 +1,19 @@
1
- /**
2
- * pi-subagent-tools — Specialized subagent delegation for pi
3
- *
4
- * Registers three tools for the "agent as project manager" workflow:
5
- * review — Read-only code review in the current project
6
- * explore — Read-only exploration of external projects (requires cwd)
7
- * delegate — General-purpose worker with optional readonly and context inheritance
8
- *
9
- * Recursion guard via PI_SUBAGENT_TOOLS_ROLE env var:
10
- * - Parent (unset): all three tools active
11
- * - Delegate child: review/explore active, delegate registered but rejects at runtime
12
- * - Review/explore child: no subagent tools registered
13
- */
14
-
15
- import * as fs from "node:fs";
16
- import { execFileSync } from "node:child_process";
17
- import { fileURLToPath } from "node:url";
18
- import * as path from "node:path";
19
- import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
20
- import { getMarkdownTheme } from "@earendil-works/pi-coding-agent";
21
- import { Container, Spacer, Text, Markdown } from "@earendil-works/pi-tui";
22
- import { Type } from "@sinclair/typebox";
23
- import {
24
- runSubagent,
25
- READONLY_TOOLS,
26
- } from "./spawn.ts";
27
- import { shortenPath, formatUsage, createSpinner } from "./tui.ts";
28
-
29
- const subagentSpinners = new Map<string, () => string>();
30
-
31
- const EXTENSION_DIR = path.dirname(fileURLToPath(import.meta.url));
32
-
33
- // ---------------------------------------------------------------------------
34
- // Compact preview component — delegates to Text.render(width) for wrapping
35
- // ---------------------------------------------------------------------------
36
-
37
- export class CompactPreview {
38
- private textComp: Text;
39
- private maxLines: number;
40
-
41
- constructor(text: string, maxLines: number = 5) {
42
- this.maxLines = maxLines;
43
- this.textComp = new Text(text, 0, 0);
44
- }
45
-
46
- render(width: number): string[] {
47
- const lines = this.textComp.render(width);
48
- if (lines.length === 0) return [];
49
-
50
- if (lines.length <= this.maxLines) return lines;
51
-
52
- return ["...", ...lines.slice(-this.maxLines)];
53
- }
54
-
55
- invalidate() {
56
- this.textComp.invalidate();
57
- }
58
- }
59
-
60
- const PROMPTS_DIR = path.resolve(EXTENSION_DIR, "prompts");
61
- const REVIEW_PROMPT_FILE = path.join(PROMPTS_DIR, "review.md");
62
- const EXPLORE_PROMPT_FILE = path.join(PROMPTS_DIR, "explore.md");
63
- const DELEGATE_PROMPT_FILE = path.join(PROMPTS_DIR, "delegate.md");
64
- const EXTENSION_FILE = path.join(EXTENSION_DIR, "index.ts");
65
-
66
- // Role guard
67
- // ---------------------------------------------------------------------------
68
-
69
- export const ROLE_ENV = "PI_SUBAGENT_TOOLS_ROLE";
70
-
71
- export type SubagentRole = "delegate" | "review" | "explore";
72
-
73
- export function getCurrentRole(): SubagentRole | undefined {
74
- const value = process.env[ROLE_ENV];
75
- if (value === "delegate" || value === "review" || value === "explore") {
76
- return value;
77
- }
78
- return undefined;
79
- }
80
-
81
- // ---------------------------------------------------------------------------
82
- // Shared parameter types
83
- // ---------------------------------------------------------------------------
84
-
85
- export const TaskParam = Type.String({
86
- description: "Task to delegate to the subagent",
87
- });
88
-
89
- export const SkillsParam = Type.Optional(
90
- Type.Array(Type.String(), {
91
- description: "Optional startup skills to load via --skill",
92
- }),
93
- );
94
-
95
- export const CwdParam = Type.Optional(
96
- Type.String({
97
- description:
98
- "Working directory for the subagent. Defaults to the parent's cwd.",
99
- }),
100
- );
101
-
102
- export const ContextParam = Type.Optional(
103
- Type.String({
104
- enum: ["fresh", "inherit"],
105
- description:
106
- 'Context mode. "fresh" (default): isolated process with no parent history. "inherit": forks parent session so child sees conversation history.',
107
- }),
108
- );
109
-
110
- // Tool-specific parameter schemas
111
- export const ReviewParams = Type.Object({
112
- task: TaskParam,
113
- skills: SkillsParam,
114
- });
115
-
116
- export const ExploreParams = Type.Object({
117
- task: TaskParam,
118
- cwd: Type.String({
119
- description:
120
- "Working directory for the explorer. Required — the explorer runs in the target project to pick up its settings, skills, and context files.",
121
- }),
122
- skills: SkillsParam,
123
- });
124
-
125
- export const DelegateParams = Type.Object({
126
- task: TaskParam,
127
- cwd: CwdParam,
128
- skills: SkillsParam,
129
- context: ContextParam,
130
- });
131
-
132
- // ---------------------------------------------------------------------------
133
- // Shared execute helper
134
- // ---------------------------------------------------------------------------
135
-
136
- async function subagentExecute(
137
- cwd: string,
138
- task: string,
139
- skills: string[] | undefined,
140
- allowedTools: string[] | undefined,
141
- extensionFile: string | undefined,
142
- systemPromptFile: string | undefined,
143
- childRole: "review" | "explore" | "delegate" | undefined,
144
- signal: AbortSignal | undefined,
145
- onUpdate: ((result: any) => void) | undefined,
146
- ) {
147
- // Validate cwd exists before spawning
148
- if (!cwd) {
149
- return {
150
- content: [{ type: "text" as const, text: "Cannot spawn subagent: cwd is required." }],
151
- isError: true,
152
- details: { final: true },
153
- };
154
- }
155
- try {
156
- const stat = fs.statSync(cwd);
157
- if (!stat.isDirectory()) {
158
- return {
159
- content: [{
160
- type: "text" as const,
161
- text: `Cannot spawn subagent: "${cwd}" is not a directory.`,
162
- }],
163
- isError: true,
164
- details: { final: true },
165
- };
166
- }
167
- } catch (err: any) {
168
- return {
169
- content: [{
170
- type: "text" as const,
171
- text: `Cannot spawn subagent: cwd "${cwd}" does not exist or is not accessible (${err.message}).`,
172
- }],
173
- isError: true,
174
- details: { final: true },
175
- };
176
- }
177
-
178
- try {
179
- const result = await runSubagent({
180
- cwd,
181
- task,
182
- skills,
183
- allowedTools,
184
- extensionFile,
185
- systemPromptFile,
186
- childRole,
187
- signal,
188
- onUpdate,
189
- });
190
- return {
191
- content: [{ type: "text" as const, text: result.output }],
192
- isError: result.isError,
193
- details: {
194
- usage: {
195
- turns: result.turns,
196
- total: result.tokens?.total,
197
- input: result.tokens?.input,
198
- output: result.tokens?.output,
199
- durationMs: result.durationMs,
200
- },
201
- final: true,
202
- stderr: result.stderr,
203
- },
204
- toolCalls: result.toolCalls,
205
- };
206
- } catch (err: any) {
207
- return {
208
- content: [{
209
- type: "text" as const,
210
- text: `Subagent failed: ${err.message || String(err)}`,
211
- }],
212
- isError: true,
213
- details: { final: true },
214
- };
215
- }
216
- }
217
-
218
- // ---------------------------------------------------------------------------
219
- // Shared render helpers
220
- // ---------------------------------------------------------------------------
221
-
222
- function renderSubagentCall(
223
- label: string,
224
- args: { task: string; cwd?: string },
225
- theme: any,
226
- ) {
227
- const firstLine = args.task.split("\n")[0];
228
- const combined = args.cwd ? `${shortenPath(args.cwd)} | ${firstLine}` : firstLine;
229
- const preview = combined.length > 80 ? combined.slice(0, 77) + "..." : combined;
230
- return new Text(
231
- `${theme.fg("toolTitle", theme.bold(label))}${theme.fg("dim", preview)}`,
232
- 0, 0,
233
- );
234
- }
235
-
236
- function renderSubagentResult(
237
- result: { content: Array<{ type: string; text?: string }>; isError?: boolean; details?: any },
238
- options: { expanded: boolean },
239
- theme: any,
240
- context: { toolCallId: string },
241
- ) {
242
- if (!subagentSpinners.has(context.toolCallId)) {
243
- subagentSpinners.set(context.toolCallId, createSpinner());
244
- }
245
- const spin = subagentSpinners.get(context.toolCallId)!;
246
- const raw = result.content.filter((c: any) => c.type === "text").map((c: any) => c.text).join("");
247
- const output = raw.split("\n").map(l => l.startsWith("▸") ? theme.fg("muted", l) : l).join("\n");
248
- const container = new Container();
249
-
250
- if (result.details?.final) subagentSpinners.delete(context.toolCallId);
251
-
252
- if (!options.expanded) {
253
- const text = output || theme.fg("dim", "(no output)");
254
- const usage = result.details?.usage;
255
- const hint = ` · ${theme.fg("dim", "Ctrl+O to expand")}`;
256
- const status = usage ? formatUsage(usage, spin) + hint : "";
257
- container.addChild(new CompactPreview(text));
258
- if (status) container.addChild(new Text(theme.fg("dim", status), 0, 0));
259
- return container;
260
- }
261
-
262
- const isFinal = !!result.details?.final;
263
- if (isFinal) {
264
- const mdTheme = getMarkdownTheme();
265
- container.addChild(new Markdown(raw, 0, 0, mdTheme));
266
- } else {
267
- container.addChild(new CompactPreview(output, 15));
268
- }
269
- if (result.details?.usage) {
270
- container.addChild(new Spacer(1));
271
- container.addChild(new Text(theme.fg("dim", formatUsage(result.details.usage, spin)), 0, 0));
272
- }
273
-
274
- return container;
275
- }
276
-
277
- // ---------------------------------------------------------------------------
278
- // Extension entry point
279
- // ---------------------------------------------------------------------------
280
-
281
- export default function (pi: ExtensionAPI) {
282
- const role = getCurrentRole();
283
-
284
- // Review/explore children: register git tool only, then return
285
- if (role === "review" || role === "explore") {
286
- pi.registerTool({
287
- name: "git",
288
- label: "Git",
289
- description: "Run read-only git commands (log, diff, status, show, branch, blame).",
290
- promptSnippet: "Run a read-only git command",
291
- parameters: Type.Object({
292
- command: Type.String({
293
- description: "Git command with arguments (e.g. 'log --oneline -10', 'diff HEAD~1', 'status').",
294
- }),
295
- }),
296
- async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
297
- try {
298
- const args = params.command.trim().split(/\s+/);
299
- if (args.length === 0) {
300
- return { content: [{ type: "text" as const, text: "No git command provided." }], isError: true };
301
- }
302
- const subcommand = args[0];
303
- const READONLY_GIT = new Set(["log","diff","show","status","branch","blame",
304
- "rev-parse","rev-list","ls-tree","ls-files","grep","describe","shortlog","whatchanged"]);
305
- if (!READONLY_GIT.has(subcommand)) {
306
- return {
307
- content: [{ type: "text" as const, text: `git ${subcommand} is not allowed (read-only commands only).` }],
308
- isError: true,
309
- };
310
- }
311
- const output = execFileSync("git", args, {
312
- cwd: ctx.cwd,
313
- encoding: "utf-8",
314
- maxBuffer: 10 * 1024 * 1024,
315
- timeout: 30_000,
316
- });
317
- return { content: [{ type: "text" as const, text: output }] };
318
- } catch (err: any) {
319
- return {
320
- content: [{ type: "text" as const, text: `git ${params.command}\n\n${err.stderr || err.message}` }],
321
- isError: true,
322
- };
323
- }
324
- },
325
- });
326
- return;
327
- }
328
-
329
-
330
- // Delegate tool: always registered (cache compatibility), rejects in delegate children
331
- pi.registerTool({
332
- name: "delegate",
333
- label: "Delegate",
334
- description:
335
- "Delegate a task to an isolated subagent process. " +
336
- "For general-purpose work that doesn't fit the review or explore tools.",
337
- promptSnippet: "Delegate a task to a worker subagent",
338
- promptGuidelines: [
339
- "Use the delegate tool for non-trivial, self-contained implementation tasks — it has full tool access unlike the read-only review and explore tools.",
340
- ],
341
- parameters: DelegateParams,
342
- async execute(_toolCallId, params, signal, onUpdate, ctx) {
343
- if (role === "delegate") {
344
- return {
345
- content: [
346
- {
347
- type: "text" as const,
348
- text: "Delegation not available in delegate subagents. Use review or explore instead.",
349
- },
350
- ],
351
- isError: true,
352
- };
353
- }
354
- return subagentExecute(
355
- params.cwd ?? ctx.cwd,
356
- params.task,
357
- params.skills,
358
- undefined, // no tool filtering — cache-compatible
359
- EXTENSION_FILE,
360
- DELEGATE_PROMPT_FILE,
361
- "delegate", // delegate children can spawn review/explore but not delegate further
362
- signal,
363
- onUpdate,
364
- );
365
- },
366
- renderCall: (args, theme) => renderSubagentCall("delegate ", args, theme),
367
- renderResult: (result, options, theme, context) => renderSubagentResult(result as any, options, theme, context),
368
- });
369
-
370
- // Review tool
371
- pi.registerTool({
372
- name: "review",
373
- label: "Review",
374
- description:
375
- "Review code changes or files in the current project. " +
376
- "The reviewer is always read-only and runs in the parent's working directory. " +
377
- "Use for code review, diff inspection, issue analysis, and quality checks.",
378
- promptSnippet: "Review code or files with a read-only subagent",
379
- promptGuidelines: [
380
- "Use the review tool for code review, diff inspection, and quality checks — it is read-only and cannot modify files.",
381
- ],
382
- parameters: ReviewParams,
383
- async execute(_toolCallId, params, signal, onUpdate, ctx) {
384
- return subagentExecute(
385
- ctx.cwd,
386
- params.task,
387
- params.skills,
388
- READONLY_TOOLS,
389
- EXTENSION_FILE,
390
- REVIEW_PROMPT_FILE,
391
- "review",
392
- signal,
393
- onUpdate,
394
- );
395
- },
396
- renderCall: (args, theme) => renderSubagentCall("review ", args, theme),
397
- renderResult: (result, options, theme, context) => renderSubagentResult(result as any, options, theme, context),
398
- });
399
-
400
- // Explore tool
401
- pi.registerTool({
402
- name: "explore",
403
- label: "Explore",
404
- description:
405
- "Explore a project directory to understand its structure, patterns, and key files. " +
406
- "The explorer is always read-only and runs in the specified working directory. " +
407
- "Use for mapping codebases, scouting dependencies, or understanding unfamiliar projects.",
408
- promptSnippet: "Explore a project directory with a read-only subagent",
409
- promptGuidelines: [
410
- "Use the explore tool to map unfamiliar codebases or scout dependencies — it runs read-only in the specified directory.",
411
- ],
412
- parameters: ExploreParams,
413
- async execute(_toolCallId, params, signal, onUpdate, ctx) {
414
- return subagentExecute(
415
- params.cwd,
416
- params.task,
417
- params.skills,
418
- READONLY_TOOLS,
419
- EXTENSION_FILE,
420
- EXPLORE_PROMPT_FILE,
421
- "explore",
422
- signal,
423
- onUpdate,
424
- );
425
- },
426
- renderCall: (args, theme) => renderSubagentCall("explore ", args, theme),
427
- renderResult: (result, options, theme, context) => renderSubagentResult(result as any, options, theme, context),
428
- });
429
- }
1
+ /**
2
+ * pi-subagent-tools — Specialized subagent delegation for pi
3
+ *
4
+ * Registers four tools for the "agent as project manager" workflow:
5
+ * review — Read-only code review in the current project
6
+ * explore — Read-only exploration of external projects (requires cwd)
7
+ * delegate — General-purpose worker with full tool access
8
+ * follow_up — Continue an existing subagent's session with full context
9
+ *
10
+ * Subagents run in-process as SDK AgentSessions (see agents.ts). Interactive
11
+ * prompts route to the parent TUI via the UI bridge (see ui-bridge.ts).
12
+ */
13
+
14
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
15
+ import { AgentManager, registerAgentTools } from "./agents.ts";
16
+
17
+ export default function (pi: ExtensionAPI) {
18
+ registerAgentTools(pi, new AgentManager());
19
+ }
package/package.json CHANGED
@@ -1,54 +1,63 @@
1
- {
2
- "name": "@jerryan/pi-subagent-tools",
3
- "version": "0.1.1",
4
- "description": "Minimal pi extension: delegate, review, and explore via isolated subagent processes",
5
- "type": "module",
6
- "main": "index.ts",
7
- "files": [
8
- "index.ts",
9
- "spawn.ts",
10
- "tui.ts",
11
- "prompts/*.md",
12
- "CHANGELOG.md",
13
- "README.md",
14
- "LICENSE"
15
- ],
16
- "keywords": [
17
- "pi",
18
- "pi-extension",
19
- "pi-package",
20
- "subagent",
21
- "delegate",
22
- "review",
23
- "explore"
24
- ],
25
- "license": "MIT",
26
- "author": "Zerui An",
27
- "repository": {
28
- "type": "git",
29
- "url": "https://github.com/JerryAZR/pi-subagent-tools"
30
- },
31
- "publishConfig": {
32
- "registry": "https://registry.npmjs.org/"
33
- },
34
- "scripts": {
35
- "pretest": "node -e \"const{cp}=require('fs');cp('test/mocks/@earendil-works','node_modules/@earendil-works',{recursive:true},()=>{})\"",
36
- "test": "node --experimental-strip-types --test test/*.test.ts"
37
- },
38
- "pi": {
39
- "extensions": ["./index.ts"]
40
- },
41
- "peerDependencies": {
42
- "@earendil-works/pi-ai": "*",
43
- "@earendil-works/pi-coding-agent": "*",
44
- "@earendil-works/pi-tui": "*"
45
- },
46
- "devDependencies": {
47
- "@earendil-works/pi-ai": "^0.74.0",
48
- "@earendil-works/pi-coding-agent": "^0.74.0",
49
- "@earendil-works/pi-tui": "^0.74.0",
50
- "@sinclair/typebox": "^0.34.0",
51
- "@types/node": "^20.0.0",
52
- "typescript": "^5.4.0"
53
- }
54
- }
1
+ {
2
+ "name": "@jerryan/pi-subagent-tools",
3
+ "version": "0.4.0",
4
+ "description": "Minimal pi extension: delegate, review, and explore via in-process subagent sessions",
5
+ "type": "module",
6
+ "main": "index.ts",
7
+ "files": [
8
+ "index.ts",
9
+ "agents.ts",
10
+ "render.ts",
11
+ "sandbox-bash.ts",
12
+ "sandbox-python.ts",
13
+ "ui-bridge.ts",
14
+ "tui.ts",
15
+ "prompts/*.md",
16
+ "CHANGELOG.md",
17
+ "README.md",
18
+ "LICENSE"
19
+ ],
20
+ "keywords": [
21
+ "pi",
22
+ "pi-extension",
23
+ "pi-package",
24
+ "subagent",
25
+ "delegate",
26
+ "review",
27
+ "explore"
28
+ ],
29
+ "license": "MIT",
30
+ "author": "Zerui An",
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "https://github.com/JerryAZR/pi-subagent-tools"
34
+ },
35
+ "publishConfig": {
36
+ "registry": "https://registry.npmjs.org/"
37
+ },
38
+ "scripts": {
39
+ "test": "node --experimental-strip-types --import ./test/register-mocks.mjs --test test/*.test.ts"
40
+ },
41
+ "pi": {
42
+ "extensions": [
43
+ "./index.ts"
44
+ ]
45
+ },
46
+ "peerDependencies": {
47
+ "@earendil-works/pi-ai": ">=0.84.0",
48
+ "@earendil-works/pi-coding-agent": ">=0.84.0",
49
+ "@earendil-works/pi-tui": ">=0.84.0"
50
+ },
51
+ "devDependencies": {
52
+ "@earendil-works/pi-ai": "^0.84.4",
53
+ "@earendil-works/pi-coding-agent": "^0.84.4",
54
+ "@earendil-works/pi-tui": "^0.84.4",
55
+ "@sinclair/typebox": "^0.34.0",
56
+ "@types/node": "^20.0.0",
57
+ "typescript": "^5.4.0"
58
+ },
59
+ "dependencies": {
60
+ "@jerryan/just-bash": "^3.10.0",
61
+ "just-git": "^1.9.0"
62
+ }
63
+ }
@@ -1,4 +1,4 @@
1
- You are a codebase explorer. Map the structure, identify key files, and report patterns.
2
- You are in read-only mode — use the git tool for repository inspection. Do not modify files or run destructive commands.
3
- Focus on entry points, architecture, dependencies, and areas of interest.
4
- End with a clear summary of the codebase structure and notable findings.
1
+ You are a codebase explorer. Map the structure, identify key files, and report patterns.
2
+ You are in read-only mode — project files cannot be modified (writes to them fail by design). Use bash for searching, file inspection, and git history.
3
+ Focus on entry points, architecture, dependencies, and areas of interest.
4
+ End with a clear summary of the codebase structure and notable findings.
package/prompts/review.md CHANGED
@@ -1,4 +1,4 @@
1
- You are a code reviewer. Inspect code, find issues, and report findings clearly.
2
- You are in read-only mode — use the git tool for repository inspection. Do not modify files or run destructive commands.
3
- Focus on correctness, edge cases, readability, and adherence to the task requirements.
4
- End with a clear summary of findings organized by severity.
1
+ You are a code reviewer. Inspect code, find issues, and report findings clearly.
2
+ You are in read-only mode — project files cannot be modified (writes to them fail by design). Use bash for searching, file inspection, and git history.
3
+ Focus on correctness, edge cases, readability, and adherence to the task requirements.
4
+ End with a clear summary of findings organized by severity.
package/render.ts ADDED
@@ -0,0 +1,87 @@
1
+ /**
2
+ * TUI rendering for subagent tool calls and results.
3
+ */
4
+
5
+ import { getMarkdownTheme } from "@earendil-works/pi-coding-agent";
6
+ import { Container, Spacer, Text, Markdown } from "@earendil-works/pi-tui";
7
+ import { formatUsage, spinnerFrame, truncate, TOOL_LINE_PREFIX } from "./tui.ts";
8
+
9
+ // ---------------------------------------------------------------------------
10
+ // Compact preview component — delegates to Text.render(width) for wrapping
11
+ // ---------------------------------------------------------------------------
12
+
13
+ export class CompactPreview {
14
+ private textComp: Text;
15
+ private maxLines: number;
16
+
17
+ constructor(text: string, maxLines: number = 5) {
18
+ this.maxLines = maxLines;
19
+ this.textComp = new Text(text, 0, 0);
20
+ }
21
+
22
+ render(width: number): string[] {
23
+ const lines = this.textComp.render(width);
24
+ if (lines.length === 0) return [];
25
+
26
+ if (lines.length <= this.maxLines) return lines;
27
+
28
+ return ["...", ...lines.slice(-this.maxLines)];
29
+ }
30
+
31
+ invalidate() {
32
+ this.textComp.invalidate();
33
+ }
34
+ }
35
+
36
+ // ---------------------------------------------------------------------------
37
+ // Call / result renderers shared by all subagent tools
38
+ // ---------------------------------------------------------------------------
39
+
40
+ export function renderSubagentCall(
41
+ label: string,
42
+ args: { task: string; hint?: string },
43
+ theme: any,
44
+ ) {
45
+ const firstLine = args.task.split("\n")[0];
46
+ const combined = args.hint ? `${args.hint} | ${firstLine}` : firstLine;
47
+ const preview = truncate(combined, 80);
48
+ return new Text(
49
+ `${theme.fg("toolTitle", theme.bold(label))}${theme.fg("dim", preview)}`,
50
+ 0, 0,
51
+ );
52
+ }
53
+
54
+ export function renderSubagentResult(
55
+ result: { content: Array<{ type: string; text?: string }>; isError?: boolean; details?: any },
56
+ options: { expanded: boolean },
57
+ theme: any,
58
+ _context: { toolCallId: string },
59
+ ) {
60
+ const raw = result.content.filter((c: any) => c.type === "text").map((c: any) => c.text).join("");
61
+ const output = raw.split("\n").map(l => l.startsWith(TOOL_LINE_PREFIX) ? theme.fg("muted", l) : l).join("\n");
62
+ const container = new Container();
63
+
64
+ if (!options.expanded) {
65
+ const text = output || theme.fg("dim", "(no output)");
66
+ const usage = result.details?.usage;
67
+ const hint = ` · ${theme.fg("dim", "Ctrl+O to expand")}`;
68
+ const status = usage ? formatUsage(usage, spinnerFrame) + hint : "";
69
+ container.addChild(new CompactPreview(text));
70
+ if (status) container.addChild(new Text(theme.fg("dim", status), 0, 0));
71
+ return container;
72
+ }
73
+
74
+ const isFinal = !!result.details?.final;
75
+ if (isFinal) {
76
+ const mdTheme = getMarkdownTheme();
77
+ container.addChild(new Markdown(raw, 0, 0, mdTheme));
78
+ } else {
79
+ container.addChild(new CompactPreview(output, 15));
80
+ }
81
+ if (result.details?.usage) {
82
+ container.addChild(new Spacer(1));
83
+ container.addChild(new Text(theme.fg("dim", formatUsage(result.details.usage, spinnerFrame)), 0, 0));
84
+ }
85
+
86
+ return container;
87
+ }