@oh-my-pi/pi-coding-agent 3.14.0 → 3.15.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.
Files changed (148) hide show
  1. package/CHANGELOG.md +79 -0
  2. package/docs/theme.md +38 -5
  3. package/examples/sdk/11-sessions.ts +2 -2
  4. package/package.json +7 -4
  5. package/src/cli/file-processor.ts +51 -2
  6. package/src/cli/plugin-cli.ts +25 -19
  7. package/src/cli/update-cli.ts +4 -3
  8. package/src/core/agent-session.ts +31 -4
  9. package/src/core/compaction/branch-summarization.ts +4 -32
  10. package/src/core/compaction/compaction.ts +6 -84
  11. package/src/core/compaction/utils.ts +2 -3
  12. package/src/core/custom-tools/types.ts +2 -0
  13. package/src/core/export-html/index.ts +1 -1
  14. package/src/core/hooks/tool-wrapper.ts +0 -1
  15. package/src/core/hooks/types.ts +2 -2
  16. package/src/core/plugins/doctor.ts +9 -1
  17. package/src/core/sdk.ts +2 -1
  18. package/src/core/session-manager.ts +518 -40
  19. package/src/core/settings-manager.ts +174 -0
  20. package/src/core/system-prompt.ts +9 -14
  21. package/src/core/title-generator.ts +2 -8
  22. package/src/core/tools/ask.ts +19 -37
  23. package/src/core/tools/bash.ts +2 -37
  24. package/src/core/tools/edit.ts +2 -9
  25. package/src/core/tools/exa/render.ts +52 -48
  26. package/src/core/tools/find.ts +10 -8
  27. package/src/core/tools/grep.ts +45 -17
  28. package/src/core/tools/ls.ts +22 -2
  29. package/src/core/tools/lsp/clients/biome-client.ts +207 -0
  30. package/src/core/tools/lsp/clients/index.ts +49 -0
  31. package/src/core/tools/lsp/clients/lsp-linter-client.ts +98 -0
  32. package/src/core/tools/lsp/config.ts +3 -0
  33. package/src/core/tools/lsp/index.ts +107 -55
  34. package/src/core/tools/lsp/render.ts +192 -79
  35. package/src/core/tools/lsp/types.ts +27 -0
  36. package/src/core/tools/lsp/utils.ts +62 -22
  37. package/src/core/tools/notebook.ts +9 -1
  38. package/src/core/tools/output.ts +37 -14
  39. package/src/core/tools/read.ts +349 -34
  40. package/src/core/tools/renderers.ts +290 -89
  41. package/src/core/tools/review.ts +12 -5
  42. package/src/core/tools/task/agents.ts +5 -5
  43. package/src/core/tools/task/commands.ts +3 -3
  44. package/src/core/tools/task/executor.ts +33 -1
  45. package/src/core/tools/task/index.ts +93 -6
  46. package/src/core/tools/task/render.ts +147 -66
  47. package/src/core/tools/task/types.ts +14 -9
  48. package/src/core/tools/web-fetch.ts +242 -103
  49. package/src/core/tools/web-search/index.ts +64 -20
  50. package/src/core/tools/web-search/providers/exa.ts +68 -172
  51. package/src/core/tools/web-search/render.ts +264 -74
  52. package/src/core/tools/write.ts +2 -8
  53. package/src/main.ts +10 -6
  54. package/src/modes/cleanup.ts +23 -0
  55. package/src/modes/index.ts +9 -4
  56. package/src/modes/interactive/components/bash-execution.ts +6 -3
  57. package/src/modes/interactive/components/branch-summary-message.ts +1 -1
  58. package/src/modes/interactive/components/compaction-summary-message.ts +1 -1
  59. package/src/modes/interactive/components/dynamic-border.ts +1 -1
  60. package/src/modes/interactive/components/extensions/extension-dashboard.ts +4 -5
  61. package/src/modes/interactive/components/extensions/extension-list.ts +18 -16
  62. package/src/modes/interactive/components/extensions/inspector-panel.ts +8 -8
  63. package/src/modes/interactive/components/hook-message.ts +2 -2
  64. package/src/modes/interactive/components/hook-selector.ts +1 -1
  65. package/src/modes/interactive/components/model-selector.ts +22 -9
  66. package/src/modes/interactive/components/oauth-selector.ts +20 -4
  67. package/src/modes/interactive/components/plugin-settings.ts +4 -2
  68. package/src/modes/interactive/components/session-selector.ts +9 -6
  69. package/src/modes/interactive/components/settings-defs.ts +285 -1
  70. package/src/modes/interactive/components/settings-selector.ts +176 -3
  71. package/src/modes/interactive/components/status-line/index.ts +4 -0
  72. package/src/modes/interactive/components/status-line/presets.ts +94 -0
  73. package/src/modes/interactive/components/status-line/segments.ts +350 -0
  74. package/src/modes/interactive/components/status-line/separators.ts +55 -0
  75. package/src/modes/interactive/components/status-line/types.ts +81 -0
  76. package/src/modes/interactive/components/status-line-segment-editor.ts +357 -0
  77. package/src/modes/interactive/components/status-line.ts +170 -223
  78. package/src/modes/interactive/components/tool-execution.ts +446 -211
  79. package/src/modes/interactive/components/tree-selector.ts +17 -6
  80. package/src/modes/interactive/components/ttsr-notification.ts +4 -4
  81. package/src/modes/interactive/components/welcome.ts +27 -19
  82. package/src/modes/interactive/interactive-mode.ts +98 -13
  83. package/src/modes/interactive/theme/dark.json +3 -2
  84. package/src/modes/interactive/theme/defaults/dark-arctic.json +111 -0
  85. package/src/modes/interactive/theme/defaults/dark-catppuccin.json +106 -0
  86. package/src/modes/interactive/theme/defaults/dark-cyberpunk.json +109 -0
  87. package/src/modes/interactive/theme/defaults/dark-dracula.json +105 -0
  88. package/src/modes/interactive/theme/defaults/dark-forest.json +103 -0
  89. package/src/modes/interactive/theme/defaults/dark-github.json +112 -0
  90. package/src/modes/interactive/theme/defaults/dark-gruvbox.json +119 -0
  91. package/src/modes/interactive/theme/defaults/dark-monochrome.json +101 -0
  92. package/src/modes/interactive/theme/defaults/dark-monokai.json +105 -0
  93. package/src/modes/interactive/theme/defaults/dark-nord.json +104 -0
  94. package/src/modes/interactive/theme/defaults/dark-ocean.json +108 -0
  95. package/src/modes/interactive/theme/defaults/dark-one.json +107 -0
  96. package/src/modes/interactive/theme/defaults/dark-retro.json +99 -0
  97. package/src/modes/interactive/theme/defaults/dark-rose-pine.json +95 -0
  98. package/src/modes/interactive/theme/defaults/dark-solarized.json +96 -0
  99. package/src/modes/interactive/theme/defaults/dark-sunset.json +106 -0
  100. package/src/modes/interactive/theme/defaults/dark-synthwave.json +102 -0
  101. package/src/modes/interactive/theme/defaults/dark-tokyo-night.json +108 -0
  102. package/src/modes/interactive/theme/defaults/index.ts +67 -0
  103. package/src/modes/interactive/theme/defaults/light-arctic.json +106 -0
  104. package/src/modes/interactive/theme/defaults/light-catppuccin.json +105 -0
  105. package/src/modes/interactive/theme/defaults/light-cyberpunk.json +103 -0
  106. package/src/modes/interactive/theme/defaults/light-forest.json +107 -0
  107. package/src/modes/interactive/theme/defaults/light-github.json +114 -0
  108. package/src/modes/interactive/theme/defaults/light-gruvbox.json +115 -0
  109. package/src/modes/interactive/theme/defaults/light-monochrome.json +100 -0
  110. package/src/modes/interactive/theme/defaults/light-ocean.json +106 -0
  111. package/src/modes/interactive/theme/defaults/light-one.json +105 -0
  112. package/src/modes/interactive/theme/defaults/light-retro.json +105 -0
  113. package/src/modes/interactive/theme/defaults/light-solarized.json +101 -0
  114. package/src/modes/interactive/theme/defaults/light-sunset.json +106 -0
  115. package/src/modes/interactive/theme/defaults/light-synthwave.json +105 -0
  116. package/src/modes/interactive/theme/defaults/light-tokyo-night.json +118 -0
  117. package/src/modes/interactive/theme/light.json +3 -2
  118. package/src/modes/interactive/theme/theme-schema.json +120 -4
  119. package/src/modes/interactive/theme/theme.ts +1228 -14
  120. package/src/prompts/branch-summary-preamble.md +3 -0
  121. package/src/prompts/branch-summary.md +28 -0
  122. package/src/prompts/compaction-summary.md +34 -0
  123. package/src/prompts/compaction-turn-prefix.md +16 -0
  124. package/src/prompts/compaction-update-summary.md +41 -0
  125. package/src/prompts/init.md +30 -0
  126. package/src/{core/tools/task/bundled-agents → prompts}/reviewer.md +6 -0
  127. package/src/prompts/summarization-system.md +3 -0
  128. package/src/prompts/system-prompt.md +27 -0
  129. package/src/{core/tools/task/bundled-agents → prompts}/task.md +2 -0
  130. package/src/prompts/title-system.md +8 -0
  131. package/src/prompts/tools/ask.md +24 -0
  132. package/src/prompts/tools/bash.md +23 -0
  133. package/src/prompts/tools/edit.md +9 -0
  134. package/src/prompts/tools/find.md +6 -0
  135. package/src/prompts/tools/grep.md +12 -0
  136. package/src/prompts/tools/lsp.md +14 -0
  137. package/src/prompts/tools/output.md +23 -0
  138. package/src/prompts/tools/read.md +25 -0
  139. package/src/prompts/tools/web-fetch.md +8 -0
  140. package/src/prompts/tools/web-search.md +10 -0
  141. package/src/prompts/tools/write.md +10 -0
  142. package/src/commands/init.md +0 -20
  143. /package/src/{core/tools/task/bundled-commands → prompts}/architect-plan.md +0 -0
  144. /package/src/{core/tools/task/bundled-agents → prompts}/browser.md +0 -0
  145. /package/src/{core/tools/task/bundled-agents → prompts}/explore.md +0 -0
  146. /package/src/{core/tools/task/bundled-commands → prompts}/implement-with-critic.md +0 -0
  147. /package/src/{core/tools/task/bundled-commands → prompts}/implement.md +0 -0
  148. /package/src/{core/tools/task/bundled-agents → prompts}/plan.md +0 -0
@@ -5,11 +5,11 @@
5
5
  */
6
6
 
7
7
  // Embed agent markdown files at build time
8
- import browserMd from "./bundled-agents/browser.md" with { type: "text" };
9
- import exploreMd from "./bundled-agents/explore.md" with { type: "text" };
10
- import planMd from "./bundled-agents/plan.md" with { type: "text" };
11
- import reviewerMd from "./bundled-agents/reviewer.md" with { type: "text" };
12
- import taskMd from "./bundled-agents/task.md" with { type: "text" };
8
+ import browserMd from "../../../prompts/browser.md" with { type: "text" };
9
+ import exploreMd from "../../../prompts/explore.md" with { type: "text" };
10
+ import planMd from "../../../prompts/plan.md" with { type: "text" };
11
+ import reviewerMd from "../../../prompts/reviewer.md" with { type: "text" };
12
+ import taskMd from "../../../prompts/task.md" with { type: "text" };
13
13
  import type { AgentDefinition, AgentSource } from "./types";
14
14
 
15
15
  const EMBEDDED_AGENTS: { name: string; content: string }[] = [
@@ -9,9 +9,9 @@ import { type SlashCommand, slashCommandCapability } from "../../../capability/s
9
9
  import { loadSync } from "../../../discovery";
10
10
 
11
11
  // Embed command markdown files at build time
12
- import architectPlanMd from "./bundled-commands/architect-plan.md" with { type: "text" };
13
- import implementMd from "./bundled-commands/implement.md" with { type: "text" };
14
- import implementWithCriticMd from "./bundled-commands/implement-with-critic.md" with { type: "text" };
12
+ import architectPlanMd from "../../../prompts/architect-plan.md" with { type: "text" };
13
+ import implementMd from "../../../prompts/implement.md" with { type: "text" };
14
+ import implementWithCriticMd from "../../../prompts/implement-with-critic.md" with { type: "text" };
15
15
 
16
16
  const EMBEDDED_COMMANDS: { name: string; content: string }[] = [
17
17
  { name: "architect-plan.md", content: architectPlanMd },
@@ -100,6 +100,38 @@ function extractToolArgsPreview(args: Record<string, unknown>): string {
100
100
  return "";
101
101
  }
102
102
 
103
+ function getNumberField(record: Record<string, unknown>, key: string): number | undefined {
104
+ if (!Object.hasOwn(record, key)) return undefined;
105
+ const value = record[key];
106
+ return typeof value === "number" && Number.isFinite(value) ? value : 0;
107
+ }
108
+
109
+ function firstNumberField(record: Record<string, unknown>, keys: string[]): number | undefined {
110
+ for (const key of keys) {
111
+ const value = getNumberField(record, key);
112
+ if (value !== undefined) return value;
113
+ }
114
+ return undefined;
115
+ }
116
+
117
+ /**
118
+ * Normalize usage objects from different event formats.
119
+ */
120
+ function getUsageTokens(usage: unknown): number {
121
+ if (!usage || typeof usage !== "object") return 0;
122
+ const record = usage as Record<string, unknown>;
123
+
124
+ const totalTokens = firstNumberField(record, ["totalTokens", "total_tokens"]);
125
+ if (totalTokens !== undefined && totalTokens > 0) return totalTokens;
126
+
127
+ const input = firstNumberField(record, ["input", "input_tokens", "inputTokens"]) ?? 0;
128
+ const output = firstNumberField(record, ["output", "output_tokens", "outputTokens"]) ?? 0;
129
+ const cacheRead = firstNumberField(record, ["cacheRead", "cache_read", "cacheReadTokens"]) ?? 0;
130
+ const cacheWrite = firstNumberField(record, ["cacheWrite", "cache_write", "cacheWriteTokens"]) ?? 0;
131
+
132
+ return input + output + cacheRead + cacheWrite;
133
+ }
134
+
103
135
  /**
104
136
  * Run a single agent as a subprocess.
105
137
  */
@@ -369,7 +401,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
369
401
  const messageUsage = event.message?.usage || event.usage;
370
402
  if (messageUsage) {
371
403
  // Accumulate tokens across messages (not overwrite)
372
- progress.tokens += (messageUsage.input_tokens || 0) + (messageUsage.output_tokens || 0);
404
+ progress.tokens += getUsageTokens(messageUsage);
373
405
  }
374
406
  // If pending termination, now we have tokens - terminate
375
407
  if (pendingTermination && !resolved) {
@@ -14,6 +14,7 @@
14
14
  */
15
15
 
16
16
  import type { AgentTool } from "@oh-my-pi/pi-agent-core";
17
+ import type { Usage } from "@oh-my-pi/pi-ai";
17
18
  import type { Theme } from "../../../modes/interactive/theme/theme";
18
19
  import { cleanupTempDir, createTempArtifactsDir, getArtifactsDir } from "./artifacts";
19
20
  import { discoverAgents, getAgent } from "./discovery";
@@ -42,6 +43,79 @@ function formatBytes(bytes: number): string {
42
43
  return `${(bytes / (1024 * 1024)).toFixed(1)}M`;
43
44
  }
44
45
 
46
+ function createUsageTotals(): Usage {
47
+ return {
48
+ input: 0,
49
+ output: 0,
50
+ cacheRead: 0,
51
+ cacheWrite: 0,
52
+ totalTokens: 0,
53
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
54
+ };
55
+ }
56
+
57
+ function addUsageTotals(target: Usage, usage: Partial<Usage>): void {
58
+ const input = usage.input ?? 0;
59
+ const output = usage.output ?? 0;
60
+ const cacheRead = usage.cacheRead ?? 0;
61
+ const cacheWrite = usage.cacheWrite ?? 0;
62
+ const totalTokens = usage.totalTokens ?? input + output + cacheRead + cacheWrite;
63
+ const cost =
64
+ usage.cost ??
65
+ ({
66
+ input: 0,
67
+ output: 0,
68
+ cacheRead: 0,
69
+ cacheWrite: 0,
70
+ total: 0,
71
+ } satisfies Usage["cost"]);
72
+
73
+ target.input += input;
74
+ target.output += output;
75
+ target.cacheRead += cacheRead;
76
+ target.cacheWrite += cacheWrite;
77
+ target.totalTokens += totalTokens;
78
+ target.cost.input += cost.input;
79
+ target.cost.output += cost.output;
80
+ target.cost.cacheRead += cost.cacheRead;
81
+ target.cost.cacheWrite += cost.cacheWrite;
82
+ target.cost.total += cost.total;
83
+ }
84
+
85
+ function parseSubagentUsage(events: string[] | undefined): Usage | undefined {
86
+ if (!events || events.length === 0) return undefined;
87
+
88
+ const totals = createUsageTotals();
89
+ let hasUsage = false;
90
+
91
+ for (const line of events) {
92
+ let event: unknown;
93
+ try {
94
+ event = JSON.parse(line);
95
+ } catch {
96
+ continue;
97
+ }
98
+
99
+ if (!event || typeof event !== "object") continue;
100
+ const record = event as Record<string, unknown>;
101
+ if (record.type !== "message_end") continue;
102
+
103
+ const message = record.message;
104
+ if (!message || typeof message !== "object") continue;
105
+ const msgRecord = message as Record<string, unknown>;
106
+ if (msgRecord.role !== "assistant") continue;
107
+ if (msgRecord.stopReason === "aborted" || msgRecord.stopReason === "error") continue;
108
+
109
+ const usage = msgRecord.usage;
110
+ if (!usage || typeof usage !== "object") continue;
111
+
112
+ addUsageTotals(totals, usage as Partial<Usage>);
113
+ hasUsage = true;
114
+ }
115
+
116
+ return hasUsage ? totals : undefined;
117
+ }
118
+
45
119
  /** Session context interface */
46
120
  interface SessionContext {
47
121
  getSessionFile: () => string | null;
@@ -393,19 +467,31 @@ export function createTaskTool(
393
467
  });
394
468
  });
395
469
 
470
+ const aggregatedUsage = createUsageTotals();
471
+ let hasAggregatedUsage = false;
472
+ const resultsWithUsage = results.map((result) => {
473
+ const usage = parseSubagentUsage(result.jsonlEvents);
474
+ if (usage) {
475
+ addUsageTotals(aggregatedUsage, usage);
476
+ hasAggregatedUsage = true;
477
+ return { ...result, usage };
478
+ }
479
+ return result;
480
+ });
481
+
396
482
  // Collect output paths (artifacts already written by executor in real-time)
397
483
  const outputPaths: string[] = [];
398
- for (const result of results) {
484
+ for (const result of resultsWithUsage) {
399
485
  if (result.artifactPaths) {
400
486
  outputPaths.push(result.artifactPaths.outputPath);
401
487
  }
402
488
  }
403
489
 
404
490
  // Build final output - match plugin format
405
- const successCount = results.filter((r) => r.exitCode === 0).length;
491
+ const successCount = resultsWithUsage.filter((r) => r.exitCode === 0).length;
406
492
  const totalDuration = Date.now() - startTime;
407
493
 
408
- const summaries = results.map((r) => {
494
+ const summaries = resultsWithUsage.map((r) => {
409
495
  const status = r.exitCode === 0 ? "completed" : `failed (exit ${r.exitCode})`;
410
496
  const output = r.output.trim() || r.stderr.trim() || "(no output)";
411
497
  const preview = output.split("\n").slice(0, 5).join("\n");
@@ -415,14 +501,14 @@ export function createTaskTool(
415
501
  ? ` [${r.outputMeta.lineCount} lines, ${formatBytes(r.outputMeta.charCount)}]`
416
502
  : "";
417
503
  const pathInfo = !hasOutputTool && r.artifactPaths?.outputPath ? ` (${r.artifactPaths.outputPath})` : "";
418
- return `[${r.agent}] ${status}${meta} ${outputId}${pathInfo}\n${preview}`;
504
+ return `[${r.agent}] ${status}${meta} ${outputId}${pathInfo}\n${preview}`;
419
505
  });
420
506
 
421
507
  const skippedNote =
422
508
  skippedSelfRecursion > 0
423
509
  ? ` (${skippedSelfRecursion} ${blockedAgent} task${skippedSelfRecursion > 1 ? "s" : ""} skipped - self-recursion blocked)`
424
510
  : "";
425
- const summary = `${successCount}/${results.length} succeeded${skippedNote} [${formatDuration(totalDuration)}]\n\n${summaries.join("\n\n---\n\n")}`;
511
+ const summary = `${successCount}/${resultsWithUsage.length} succeeded${skippedNote} [${formatDuration(totalDuration)}]\n\n${summaries.join("\n\n---\n\n")}`;
426
512
 
427
513
  // Cleanup temp directory if used
428
514
  if (tempArtifactsDir) {
@@ -433,8 +519,9 @@ export function createTaskTool(
433
519
  content: [{ type: "text", text: summary }],
434
520
  details: {
435
521
  projectAgentsDir,
436
- results,
522
+ results: resultsWithUsage,
437
523
  totalDurationMs: totalDuration,
524
+ usage: hasAggregatedUsage ? aggregatedUsage : undefined,
438
525
  outputPaths,
439
526
  },
440
527
  };
@@ -44,29 +44,91 @@ export function formatDuration(ms: number): string {
44
44
  /**
45
45
  * Truncate text to max length with ellipsis.
46
46
  */
47
- function truncate(text: string, maxLen: number): string {
47
+ function truncate(text: string, maxLen: number, ellipsis: string): string {
48
48
  if (text.length <= maxLen) return text;
49
- return `${text.slice(0, maxLen - 3)}...`;
49
+ const sliceLen = Math.max(0, maxLen - ellipsis.length);
50
+ return `${text.slice(0, sliceLen)}${ellipsis}`;
50
51
  }
51
52
 
52
53
  /**
53
54
  * Get status icon for agent state.
55
+ * For running status, uses animated spinner if spinnerFrame is provided.
54
56
  */
55
- function getStatusIcon(status: AgentProgress["status"]): string {
57
+ function getStatusIcon(status: AgentProgress["status"], theme: Theme, spinnerFrame?: number): string {
56
58
  switch (status) {
57
59
  case "pending":
58
- return "○";
59
- case "running":
60
- return "◐";
60
+ return theme.status.pending;
61
+ case "running": {
62
+ // Use animated spinner if frame is provided, otherwise static icon
63
+ if (spinnerFrame === undefined) return theme.status.running;
64
+ const frames = theme.spinnerFrames;
65
+ return frames[spinnerFrame % frames.length];
66
+ }
61
67
  case "completed":
62
- return "✓";
68
+ return theme.status.success;
63
69
  case "failed":
64
- return "✗";
70
+ return theme.status.error;
65
71
  case "aborted":
66
- return "⊘";
72
+ return theme.status.aborted;
67
73
  }
68
74
  }
69
75
 
76
+ function formatBadge(label: string, color: "success" | "error" | "warning" | "accent" | "muted", theme: Theme): string {
77
+ const left = theme.format.bracketLeft;
78
+ const right = theme.format.bracketRight;
79
+ return theme.fg(color, `${left}${label}${right}`);
80
+ }
81
+
82
+ function formatFindingSummary(findings: ReportFindingDetails[], theme: Theme): string {
83
+ if (findings.length === 0) return theme.fg("dim", "Findings: none");
84
+
85
+ const counts = new Map<number, number>();
86
+ for (const finding of findings) {
87
+ counts.set(finding.priority, (counts.get(finding.priority) ?? 0) + 1);
88
+ }
89
+
90
+ const parts: string[] = [];
91
+ for (const priority of [0, 1, 2, 3]) {
92
+ const label = PRIORITY_LABELS[priority] ?? "P?";
93
+ const color = priority === 0 ? "error" : priority === 1 ? "warning" : "muted";
94
+ const count = counts.get(priority) ?? 0;
95
+ parts.push(theme.fg(color, `${label}:${count}`));
96
+ }
97
+
98
+ return `${theme.fg("dim", "Findings:")} ${parts.join(theme.sep.dot)}`;
99
+ }
100
+
101
+ function renderOutputSection(
102
+ output: string,
103
+ continuePrefix: string,
104
+ expanded: boolean,
105
+ theme: Theme,
106
+ maxCollapsed = 3,
107
+ maxExpanded = 10,
108
+ ): string[] {
109
+ const lines: string[] = [];
110
+ const outputLines = output.split("\n").filter((line) => line.trim());
111
+ if (outputLines.length === 0) return lines;
112
+
113
+ lines.push(`${continuePrefix}${theme.fg("dim", "Output")}`);
114
+
115
+ const previewCount = expanded ? maxExpanded : maxCollapsed;
116
+ for (const line of outputLines.slice(0, previewCount)) {
117
+ lines.push(`${continuePrefix} ${theme.fg("dim", truncate(line, 70, theme.format.ellipsis))}`);
118
+ }
119
+
120
+ if (outputLines.length > previewCount) {
121
+ lines.push(
122
+ `${continuePrefix} ${theme.fg(
123
+ "dim",
124
+ `${theme.format.ellipsis} ${outputLines.length - previewCount} more lines`,
125
+ )}`,
126
+ );
127
+ }
128
+
129
+ return lines;
130
+ }
131
+
70
132
  /**
71
133
  * Render the tool call arguments.
72
134
  */
@@ -76,24 +138,36 @@ export function renderCall(args: TaskParams, theme: Theme): Component {
76
138
  if (args.tasks.length === 1) {
77
139
  // Single task - show agent and task preview
78
140
  const task = args.tasks[0];
79
- const taskPreview = truncate(task.task, 60);
141
+ const taskPreview = truncate(task.task, 60, theme.format.ellipsis);
80
142
  return new Text(`${label} ${theme.fg("accent", task.agent)}: ${theme.fg("muted", taskPreview)}`, 0, 0);
81
143
  }
82
144
 
83
145
  // Multiple tasks - show count and agent names
84
146
  const agents = args.tasks.map((t) => t.agent).join(", ");
85
- return new Text(`${label} ${theme.fg("muted", `${args.tasks.length} agents: ${truncate(agents, 50)}`)}`, 0, 0);
147
+ return new Text(
148
+ `${label} ${theme.fg("muted", `${args.tasks.length} agents: ${truncate(agents, 50, theme.format.ellipsis)}`)}`,
149
+ 0,
150
+ 0,
151
+ );
86
152
  }
87
153
 
88
154
  /**
89
155
  * Render streaming progress for a single agent.
90
156
  */
91
- function renderAgentProgress(progress: AgentProgress, isLast: boolean, expanded: boolean, theme: Theme): string[] {
157
+ function renderAgentProgress(
158
+ progress: AgentProgress,
159
+ isLast: boolean,
160
+ expanded: boolean,
161
+ theme: Theme,
162
+ spinnerFrame?: number,
163
+ ): string[] {
92
164
  const lines: string[] = [];
93
- const prefix = isLast ? "└─" : "├─";
94
- const continuePrefix = isLast ? " " : "│ ";
165
+ const prefix = isLast
166
+ ? `${theme.boxSharp.bottomLeft}${theme.boxSharp.horizontal}`
167
+ : `${theme.boxSharp.teeRight}${theme.boxSharp.horizontal}`;
168
+ const continuePrefix = isLast ? " " : `${theme.boxSharp.vertical} `;
95
169
 
96
- const icon = getStatusIcon(progress.status);
170
+ const icon = getStatusIcon(progress.status, theme, spinnerFrame);
97
171
  const iconColor =
98
172
  progress.status === "completed"
99
173
  ? "success"
@@ -105,35 +179,43 @@ function renderAgentProgress(progress: AgentProgress, isLast: boolean, expanded:
105
179
  const agentId = `${progress.agent}(${progress.index})`;
106
180
  let statusLine = `${prefix} ${theme.fg(iconColor, icon)} ${theme.fg("accent", agentId)}`;
107
181
 
182
+ // Only show badge for non-running states (spinner already indicates running)
183
+ if (progress.status !== "running") {
184
+ const statusLabel =
185
+ progress.status === "completed"
186
+ ? "done"
187
+ : progress.status === "failed"
188
+ ? "failed"
189
+ : progress.status === "aborted"
190
+ ? "aborted"
191
+ : "pending";
192
+ statusLine += ` ${formatBadge(statusLabel, iconColor, theme)}`;
193
+ }
194
+
108
195
  if (progress.status === "running") {
109
- const taskPreview = truncate(progress.task, 40);
110
- statusLine += `: ${theme.fg("muted", taskPreview)}`;
111
- statusLine += ` · ${theme.fg("dim", `${progress.toolCount} tools`)}`;
196
+ const taskPreview = truncate(progress.task, 40, theme.format.ellipsis);
197
+ statusLine += ` ${theme.fg("muted", taskPreview)}`;
198
+ statusLine += `${theme.sep.dot}${theme.fg("dim", `${progress.toolCount} tools`)}`;
112
199
  if (progress.tokens > 0) {
113
- statusLine += ` · ${theme.fg("dim", `${formatTokens(progress.tokens)} tokens`)}`;
200
+ statusLine += `${theme.sep.dot}${theme.fg("dim", `${formatTokens(progress.tokens)} tokens`)}`;
114
201
  }
115
202
  } else if (progress.status === "completed") {
116
- statusLine += `: ${theme.fg("success", "done")}`;
117
- statusLine += ` · ${theme.fg("dim", `${progress.toolCount} tools`)}`;
118
- statusLine += ` · ${theme.fg("dim", `${formatTokens(progress.tokens)} tokens`)}`;
119
- } else if (progress.status === "aborted") {
120
- statusLine += `: ${theme.fg("error", "aborted")}`;
121
- } else if (progress.status === "failed") {
122
- statusLine += `: ${theme.fg("error", "failed")}`;
203
+ statusLine += `${theme.sep.dot}${theme.fg("dim", `${progress.toolCount} tools`)}`;
204
+ statusLine += `${theme.sep.dot}${theme.fg("dim", `${formatTokens(progress.tokens)} tokens`)}`;
123
205
  }
124
206
 
125
207
  lines.push(statusLine);
126
208
 
127
209
  // Current tool (if running)
128
210
  if (progress.status === "running" && progress.currentTool) {
129
- let toolLine = `${continuePrefix} ${theme.fg("muted", progress.currentTool)}`;
211
+ let toolLine = `${continuePrefix}${theme.tree.hook} ${theme.fg("muted", progress.currentTool)}`;
130
212
  if (progress.currentToolArgs) {
131
- toolLine += `: ${theme.fg("dim", truncate(progress.currentToolArgs, 40))}`;
213
+ toolLine += `: ${theme.fg("dim", truncate(progress.currentToolArgs, 40, theme.format.ellipsis))}`;
132
214
  }
133
215
  if (progress.currentToolStartMs) {
134
216
  const elapsed = Date.now() - progress.currentToolStartMs;
135
217
  if (elapsed > 5000) {
136
- toolLine += ` · ${theme.fg("warning", formatDuration(elapsed))}`;
218
+ toolLine += `${theme.sep.dot}${theme.fg("warning", formatDuration(elapsed))}`;
137
219
  }
138
220
  }
139
221
  lines.push(toolLine);
@@ -153,7 +235,9 @@ function renderAgentProgress(progress: AgentProgress, isLast: boolean, expanded:
153
235
  }
154
236
  }
155
237
  if (dataArray.length > 3) {
156
- lines.push(`${continuePrefix}${theme.fg("dim", `... ${dataArray.length - 3} more`)}`);
238
+ lines.push(
239
+ `${continuePrefix}${theme.fg("dim", `${theme.format.ellipsis} ${dataArray.length - 3} more`)}`,
240
+ );
157
241
  }
158
242
  }
159
243
  }
@@ -161,10 +245,8 @@ function renderAgentProgress(progress: AgentProgress, isLast: boolean, expanded:
161
245
 
162
246
  // Expanded view: recent output and tools
163
247
  if (expanded && progress.status === "running") {
164
- // Recent output
165
- for (const line of progress.recentOutput.slice(0, 3)) {
166
- lines.push(`${continuePrefix} ${theme.fg("dim", truncate(line, 60))}`);
167
- }
248
+ const output = progress.recentOutput.join("\n");
249
+ lines.push(...renderOutputSection(output, continuePrefix, true, theme, 2, 6));
168
250
  }
169
251
 
170
252
  return lines;
@@ -184,7 +266,7 @@ function renderReviewResult(
184
266
 
185
267
  // Verdict line
186
268
  const verdictColor = summary.overall_correctness === "correct" ? "success" : "error";
187
- const verdictIcon = summary.overall_correctness === "correct" ? "✓" : "✗";
269
+ const verdictIcon = summary.overall_correctness === "correct" ? theme.status.success : theme.status.error;
188
270
  lines.push(
189
271
  `${continuePrefix}${theme.fg(verdictColor, verdictIcon)} Patch is ${theme.fg(verdictColor, summary.overall_correctness)} ${theme.fg("dim", `(${(summary.confidence * 100).toFixed(0)}% confidence)`)}`,
190
272
  );
@@ -192,19 +274,21 @@ function renderReviewResult(
192
274
  // Explanation preview (first ~80 chars when collapsed, full when expanded)
193
275
  if (summary.explanation) {
194
276
  if (expanded) {
195
- // Full explanation, wrapped
277
+ lines.push(`${continuePrefix}${theme.fg("dim", "Summary")}`);
196
278
  const explanationLines = summary.explanation.split("\n");
197
279
  for (const line of explanationLines) {
198
- lines.push(`${continuePrefix}${theme.fg("dim", line)}`);
280
+ lines.push(`${continuePrefix} ${theme.fg("dim", line)}`);
199
281
  }
200
282
  } else {
201
283
  // Preview: first sentence or ~100 chars
202
- const preview = truncate(`${summary.explanation.split(/[.!?]/)[0]}.`, 100);
203
- lines.push(`${continuePrefix}${theme.fg("dim", preview)}`);
284
+ const preview = truncate(`${summary.explanation.split(/[.!?]/)[0]}.`, 100, theme.format.ellipsis);
285
+ lines.push(`${continuePrefix}${theme.fg("dim", `Summary: ${preview}`)}`);
204
286
  }
205
287
  }
206
288
 
207
- // Findings in tree structure
289
+ // Findings summary + list
290
+ lines.push(`${continuePrefix}${formatFindingSummary(findings, theme)}`);
291
+
208
292
  if (findings.length > 0) {
209
293
  lines.push(`${continuePrefix}`); // Spacing
210
294
  lines.push(...renderFindings(findings, continuePrefix, expanded, theme));
@@ -228,8 +312,10 @@ function renderFindings(
228
312
  for (let i = 0; i < displayCount; i++) {
229
313
  const finding = findings[i];
230
314
  const isLastFinding = i === displayCount - 1 && (expanded || findings.length <= 3);
231
- const findingPrefix = isLastFinding ? "└─" : "├─";
232
- const findingContinue = isLastFinding ? " " : "│ ";
315
+ const findingPrefix = isLastFinding
316
+ ? `${theme.boxSharp.bottomLeft}${theme.boxSharp.horizontal}`
317
+ : `${theme.boxSharp.teeRight}${theme.boxSharp.horizontal}`;
318
+ const findingContinue = isLastFinding ? " " : `${theme.boxSharp.vertical} `;
233
319
 
234
320
  const priority = PRIORITY_LABELS[finding.priority] ?? "P?";
235
321
  const color = finding.priority === 0 ? "error" : finding.priority === 1 ? "warning" : "muted";
@@ -251,7 +337,9 @@ function renderFindings(
251
337
  }
252
338
 
253
339
  if (!expanded && findings.length > 3) {
254
- lines.push(`${continuePrefix}${theme.fg("dim", `... ${findings.length - 3} more findings`)}`);
340
+ lines.push(
341
+ `${continuePrefix}${theme.fg("dim", `${theme.format.ellipsis} ${findings.length - 3} more findings`)}`,
342
+ );
255
343
  }
256
344
 
257
345
  return lines;
@@ -262,23 +350,24 @@ function renderFindings(
262
350
  */
263
351
  function renderAgentResult(result: SingleResult, isLast: boolean, expanded: boolean, theme: Theme): string[] {
264
352
  const lines: string[] = [];
265
- const prefix = isLast ? "└─" : "├─";
266
- const continuePrefix = isLast ? " " : "│ ";
353
+ const prefix = isLast
354
+ ? `${theme.boxSharp.bottomLeft}${theme.boxSharp.horizontal}`
355
+ : `${theme.boxSharp.teeRight}${theme.boxSharp.horizontal}`;
356
+ const continuePrefix = isLast ? " " : `${theme.boxSharp.vertical} `;
267
357
 
268
358
  const aborted = result.aborted ?? false;
269
359
  const success = !aborted && result.exitCode === 0;
270
- const icon = aborted ? "⊘" : success ? "✓" : "✗";
360
+ const icon = aborted ? theme.status.aborted : success ? theme.status.success : theme.status.error;
271
361
  const iconColor = success ? "success" : "error";
272
362
  const statusText = aborted ? "aborted" : success ? "done" : "failed";
273
363
 
274
364
  // Main status line - include index for Output tool ID derivation
275
365
  const agentId = `${result.agent}(${result.index})`;
276
- let statusLine = `${prefix} ${theme.fg(iconColor, icon)} ${theme.fg("accent", agentId)}`;
277
- statusLine += `: ${theme.fg(iconColor, statusText)}`;
366
+ let statusLine = `${prefix} ${theme.fg(iconColor, icon)} ${theme.fg("accent", agentId)} ${formatBadge(statusText, iconColor, theme)}`;
278
367
  if (result.tokens > 0) {
279
- statusLine += ` · ${theme.fg("dim", `${formatTokens(result.tokens)} tokens`)}`;
368
+ statusLine += `${theme.sep.dot}${theme.fg("dim", `${formatTokens(result.tokens)} tokens`)}`;
280
369
  }
281
- statusLine += ` · ${theme.fg("dim", formatDuration(result.durationMs))}`;
370
+ statusLine += `${theme.sep.dot}${theme.fg("dim", formatDuration(result.durationMs))}`;
282
371
 
283
372
  if (result.truncated) {
284
373
  statusLine += ` ${theme.fg("warning", "[truncated]")}`;
@@ -299,8 +388,9 @@ function renderAgentResult(result: SingleResult, isLast: boolean, expanded: bool
299
388
  }
300
389
  if (reportFindingData && reportFindingData.length > 0) {
301
390
  lines.push(
302
- `${continuePrefix}${theme.fg("warning", "!")} ${theme.fg("dim", "Review summary missing (submit_review not called)")}`,
391
+ `${continuePrefix}${theme.fg("warning", theme.status.warning)} ${theme.fg("dim", "Review summary missing (submit_review not called)")}`,
303
392
  );
393
+ lines.push(`${continuePrefix}${formatFindingSummary(reportFindingData, theme)}`);
304
394
  lines.push(`${continuePrefix}`); // Spacing
305
395
  lines.push(...renderFindings(reportFindingData, continuePrefix, expanded, theme));
306
396
  return lines;
@@ -339,21 +429,12 @@ function renderAgentResult(result: SingleResult, isLast: boolean, expanded: bool
339
429
 
340
430
  // Fallback to output preview if no custom rendering
341
431
  if (!hasCustomRendering) {
342
- const outputLines = result.output.split("\n").filter((l) => l.trim());
343
- const previewCount = expanded ? 8 : 3;
344
-
345
- for (const line of outputLines.slice(0, previewCount)) {
346
- lines.push(`${continuePrefix}${theme.fg("dim", truncate(line, 70))}`);
347
- }
348
-
349
- if (outputLines.length > previewCount) {
350
- lines.push(`${continuePrefix}${theme.fg("dim", `... ${outputLines.length - previewCount} more lines`)}`);
351
- }
432
+ lines.push(...renderOutputSection(result.output, continuePrefix, expanded, theme, 3, 12));
352
433
  }
353
434
 
354
435
  // Error message
355
436
  if (result.error && !success) {
356
- lines.push(`${continuePrefix}${theme.fg("error", truncate(result.error, 70))}`);
437
+ lines.push(`${continuePrefix}${theme.fg("error", truncate(result.error, 70, theme.format.ellipsis))}`);
357
438
  }
358
439
 
359
440
  return lines;
@@ -367,13 +448,13 @@ export function renderResult(
367
448
  options: RenderResultOptions,
368
449
  theme: Theme,
369
450
  ): Component {
370
- const { expanded, isPartial } = options;
451
+ const { expanded, isPartial, spinnerFrame } = options;
371
452
  const details = result.details;
372
453
 
373
454
  if (!details) {
374
455
  // Fallback to simple text
375
456
  const text = result.content.find((c) => c.type === "text")?.text || "";
376
- return new Text(theme.fg("dim", truncate(text, 100)), 0, 0);
457
+ return new Text(theme.fg("dim", truncate(text, 100, theme.format.ellipsis)), 0, 0);
377
458
  }
378
459
 
379
460
  const lines: string[] = [];
@@ -382,7 +463,7 @@ export function renderResult(
382
463
  // Streaming progress view
383
464
  details.progress.forEach((progress, i) => {
384
465
  const isLast = i === details.progress!.length - 1;
385
- lines.push(...renderAgentProgress(progress, isLast, expanded, theme));
466
+ lines.push(...renderAgentProgress(progress, isLast, expanded, theme, spinnerFrame));
386
467
  });
387
468
  } else if (details.results.length > 0) {
388
469
  // Final results view
@@ -407,7 +488,7 @@ export function renderResult(
407
488
  if (failCount > 0) {
408
489
  summary += theme.fg("error", `${failCount} failed`);
409
490
  }
410
- summary += ` · ${theme.fg("dim", formatDuration(details.totalDurationMs))}`;
491
+ summary += `${theme.sep.dot}${theme.fg("dim", formatDuration(details.totalDurationMs))}`;
411
492
  lines.push(summary);
412
493
 
413
494
  // Artifacts suppressed from user view - available via session file
@@ -1,17 +1,9 @@
1
+ import type { Usage } from "@oh-my-pi/pi-ai";
1
2
  import { type Static, Type } from "@sinclair/typebox";
2
3
 
3
4
  /** Source of an agent definition */
4
5
  export type AgentSource = "bundled" | "user" | "project";
5
6
 
6
- /** Single task item for parallel execution */
7
- export const taskItemSchema = Type.Object({
8
- agent: Type.String({ description: "Agent name" }),
9
- task: Type.String({ description: "Task description for the agent" }),
10
- model: Type.Optional(Type.String({ description: "Model override for this task" })),
11
- });
12
-
13
- export type TaskItem = Static<typeof taskItemSchema>;
14
-
15
7
  /** Maximum tasks per call */
16
8
  export const MAX_PARALLEL_TASKS = 32;
17
9
 
@@ -36,6 +28,15 @@ export const OMP_BLOCKED_AGENT_ENV = "OMP_BLOCKED_AGENT";
36
28
  /** Environment variable containing allowed spawn list (propagated to subprocesses) */
37
29
  export const OMP_SPAWNS_ENV = "OMP_SPAWNS";
38
30
 
31
+ /** Single task item for parallel execution */
32
+ export const taskItemSchema = Type.Object({
33
+ agent: Type.String({ description: "Agent name" }),
34
+ task: Type.String({ description: "Task description for the agent" }),
35
+ model: Type.Optional(Type.String({ description: "Model override for this task" })),
36
+ });
37
+
38
+ export type TaskItem = Static<typeof taskItemSchema>;
39
+
39
40
  /** Task tool parameters */
40
41
  export const taskSchema = Type.Object({
41
42
  context: Type.Optional(Type.String({ description: "Shared context prepended to all task prompts" })),
@@ -121,6 +122,8 @@ export interface SingleResult {
121
122
  aborted?: boolean;
122
123
  jsonlEvents?: string[];
123
124
  artifactPaths?: { inputPath: string; outputPath: string; jsonlPath?: string };
125
+ /** Aggregated usage from the subprocess, if available. */
126
+ usage?: Usage;
124
127
  /** Data extracted by registered subprocess tool handlers (keyed by tool name) */
125
128
  extractedToolData?: Record<string, unknown[]>;
126
129
  /** Output metadata for Output tool integration */
@@ -132,6 +135,8 @@ export interface TaskToolDetails {
132
135
  projectAgentsDir: string | null;
133
136
  results: SingleResult[];
134
137
  totalDurationMs: number;
138
+ /** Aggregated usage across all subagents. */
139
+ usage?: Usage;
135
140
  outputPaths?: string[];
136
141
  progress?: AgentProgress[];
137
142
  }