pi-subagents 0.29.0 → 0.31.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 (48) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/README.md +125 -19
  3. package/agents/context-builder.md +3 -3
  4. package/agents/planner.md +1 -1
  5. package/agents/researcher.md +1 -1
  6. package/agents/scout.md +1 -1
  7. package/package.json +7 -7
  8. package/skills/pi-subagents/SKILL.md +30 -0
  9. package/src/agents/agent-management.ts +189 -8
  10. package/src/agents/agent-serializer.ts +35 -12
  11. package/src/agents/agents.ts +243 -24
  12. package/src/agents/frontmatter.ts +66 -2
  13. package/src/agents/proactive-skills.ts +191 -0
  14. package/src/agents/skills.ts +117 -20
  15. package/src/extension/doctor.ts +20 -0
  16. package/src/extension/fanout-child.ts +2 -1
  17. package/src/extension/index.ts +50 -5
  18. package/src/extension/schemas.ts +40 -79
  19. package/src/intercom/intercom-bridge.ts +2 -3
  20. package/src/runs/background/async-execution.ts +180 -67
  21. package/src/runs/background/async-job-tracker.ts +56 -11
  22. package/src/runs/background/async-resume.ts +53 -5
  23. package/src/runs/background/async-status.ts +4 -1
  24. package/src/runs/background/chain-append.ts +282 -0
  25. package/src/runs/background/chain-root-attachment.ts +161 -0
  26. package/src/runs/background/result-watcher.ts +11 -2
  27. package/src/runs/background/run-status.ts +1 -0
  28. package/src/runs/background/stale-run-reconciler.ts +9 -4
  29. package/src/runs/background/subagent-runner.ts +158 -11
  30. package/src/runs/foreground/chain-execution.ts +26 -2
  31. package/src/runs/foreground/execution.ts +114 -8
  32. package/src/runs/foreground/subagent-executor.ts +611 -87
  33. package/src/runs/shared/acceptance.ts +285 -34
  34. package/src/runs/shared/chain-outputs.ts +23 -8
  35. package/src/runs/shared/completion-guard.ts +1 -1
  36. package/src/runs/shared/dynamic-fanout.ts +5 -3
  37. package/src/runs/shared/mcp-direct-tool-allowlist.ts +2 -2
  38. package/src/runs/shared/parallel-utils.ts +13 -1
  39. package/src/runs/shared/pi-args.ts +12 -3
  40. package/src/runs/shared/single-output.ts +15 -1
  41. package/src/runs/shared/subagent-control.ts +8 -11
  42. package/src/shared/settings.ts +1 -0
  43. package/src/shared/types.ts +17 -2
  44. package/src/shared/utils.ts +19 -1
  45. package/src/slash/prompt-template-bridge.ts +26 -3
  46. package/src/slash/slash-bridge.ts +3 -1
  47. package/src/slash/slash-commands.ts +34 -4
  48. package/src/tui/render.ts +265 -13
package/src/tui/render.ts CHANGED
@@ -88,6 +88,33 @@ function truncLine(text: string, maxWidth: number): string {
88
88
  return result + activeStyles.join("") + "…";
89
89
  }
90
90
 
91
+ function wrapPlainText(text: string, maxWidth: number): string[] {
92
+ if (maxWidth <= 0) return [""];
93
+ const lines: string[] = [];
94
+ for (const rawLine of text.split("\n")) {
95
+ if (rawLine.length === 0) {
96
+ lines.push("");
97
+ continue;
98
+ }
99
+ let current = "";
100
+ let currentWidth = 0;
101
+ for (const seg of segmenter.segment(rawLine)) {
102
+ const grapheme = seg.segment;
103
+ const graphemeWidth = visibleWidth(grapheme);
104
+ if (currentWidth > 0 && currentWidth + graphemeWidth > maxWidth) {
105
+ lines.push(current);
106
+ current = grapheme;
107
+ currentWidth = graphemeWidth;
108
+ continue;
109
+ }
110
+ current += grapheme;
111
+ currentWidth += graphemeWidth;
112
+ }
113
+ lines.push(current);
114
+ }
115
+ return lines;
116
+ }
117
+
91
118
  const RUNNING_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
92
119
  const STATIC_RUNNING_GLYPH = "●";
93
120
 
@@ -134,7 +161,7 @@ export function clearLegacyResultAnimationTimer(context: LegacyResultAnimationCo
134
161
  function extractOutputTarget(task: string): string | undefined {
135
162
  const writeToMatch = task.match(/\[Write to:\s*([^\]\n]+)\]/i);
136
163
  if (writeToMatch?.[1]?.trim()) return writeToMatch[1].trim();
137
- const findingsMatch = task.match(/Write your findings to:\s*(\S+)/i);
164
+ const findingsMatch = task.match(/Write your findings to(?: exactly this path)?:\s*([^\r\n]+)/i);
138
165
  if (findingsMatch?.[1]?.trim()) return findingsMatch[1].trim();
139
166
  const outputMatch = task.match(/[Oo]utput(?:\s+to)?\s*:\s*(\S+)/i);
140
167
  if (outputMatch?.[1]?.trim()) return outputMatch[1].trim();
@@ -231,8 +258,11 @@ function resultStatusLine(result: Details["results"][number], output: string): s
231
258
  return "Done";
232
259
  }
233
260
 
234
- function resultGlyph(result: Details["results"][number], output: string, theme: Theme, running = result.progress?.status === "running", seed = progressRunningSeed(result.progress ?? result.progressSummary)): string {
235
- if (running) return theme.fg("accent", runningGlyph(seed));
261
+ function resultGlyph(result: Details["results"][number], output: string, theme: Theme, running = result.progress?.status === "running", seed = progressRunningSeed(result.progress ?? result.progressSummary), frame?: number): string {
262
+ if (running) {
263
+ if (frame !== undefined) return theme.fg("accent", runningGlyph((seed ?? 0) + frame));
264
+ return theme.fg("accent", runningGlyph(seed));
265
+ }
236
266
  if (result.detached) return theme.fg("warning", "■");
237
267
  if (result.interrupted) return theme.fg("warning", "■");
238
268
  if (result.exitCode !== 0) return theme.fg("error", "✗");
@@ -899,11 +929,189 @@ function compactSingleWidgetLines(job: AsyncJobState, theme: Theme, width: numbe
899
929
  return lines.map((line) => truncLine(line, width));
900
930
  }
901
931
 
932
+ type WidgetRenderTier = "full" | "single-line" | "progressive";
933
+
934
+ interface WidgetLayoutSession {
935
+ expanded: boolean;
936
+ rows: number;
937
+ columns: number;
938
+ tier: WidgetRenderTier;
939
+ lockedRows?: number;
940
+ visibleJobKeys: string[];
941
+ }
942
+
943
+ const RESERVED_NON_WIDGET_ROWS = 19;
944
+
945
+ let widgetLayoutSession: WidgetLayoutSession | undefined;
946
+
947
+ function resetWidgetLayoutSession(): void {
948
+ widgetLayoutSession = undefined;
949
+ }
950
+
951
+ function estimateAvailableWidgetRows(): number {
952
+ const rows = process.stdout.rows || 30;
953
+ return Math.max(1, rows - RESERVED_NON_WIDGET_ROWS);
954
+ }
955
+
956
+ function currentTerminalRows(): number {
957
+ return process.stdout.rows || 30;
958
+ }
959
+
960
+ function currentTerminalColumns(): number {
961
+ return process.stdout.columns || 120;
962
+ }
963
+
964
+ function widgetSessionMatches(expanded: boolean): boolean {
965
+ return widgetLayoutSession?.expanded === expanded
966
+ && widgetLayoutSession.rows === currentTerminalRows()
967
+ && widgetLayoutSession.columns === currentTerminalColumns();
968
+ }
969
+
970
+ function widgetHeaderCounts(jobs: AsyncJobState[]): { running: AsyncJobState[]; queued: AsyncJobState[]; complete: AsyncJobState[]; failed: AsyncJobState[]; paused: AsyncJobState[] } {
971
+ return {
972
+ running: jobs.filter((job) => job.status === "running"),
973
+ queued: jobs.filter((job) => job.status === "queued"),
974
+ complete: jobs.filter((job) => job.status === "complete"),
975
+ failed: jobs.filter((job) => job.status === "failed"),
976
+ paused: jobs.filter((job) => job.status === "paused"),
977
+ };
978
+ }
979
+
980
+ function buildSingleLineWidgetLines(jobs: AsyncJobState[], theme: Theme, width: number): string[] {
981
+ const counts = widgetHeaderCounts(jobs);
982
+ const hasActive = counts.running.length > 0 || counts.queued.length > 0;
983
+ const glyph = counts.running.length > 0 ? runningGlyph(widgetJobsRunningSeed(counts.running)) : hasActive ? "●" : "○";
984
+ const parts: string[] = [];
985
+ if (counts.running.length > 0) parts.push(`${counts.running.length}/${jobs.length} running`);
986
+ if (counts.queued.length > 0) parts.push(`${counts.queued.length} queued`);
987
+ if (counts.failed.length > 0) parts.push(`${counts.failed.length} failed`);
988
+ if (counts.paused.length > 0) parts.push(`${counts.paused.length} paused`);
989
+ if (!hasActive && counts.complete.length > 0) parts.push(`${counts.complete.length}/${jobs.length} done`);
990
+ return [truncLine(`${theme.fg(hasActive ? "accent" : "dim", glyph)} ${theme.fg(hasActive ? "accent" : "dim", "subagents")} (${parts.join(", ") || `${jobs.length} total`})`, width)];
991
+ }
992
+
993
+ function orderedWidgetJobs(jobs: AsyncJobState[]): AsyncJobState[] {
994
+ return [
995
+ ...jobs.filter((job) => job.status === "running"),
996
+ ...jobs.filter((job) => job.status === "queued"),
997
+ ...jobs.filter((job) => job.status !== "running" && job.status !== "queued"),
998
+ ];
999
+ }
1000
+
1001
+ function progressiveJobKey(job: AsyncJobState): string {
1002
+ return job.asyncId;
1003
+ }
1004
+
1005
+ function isProgressiveActiveJob(job: AsyncJobState | undefined): boolean {
1006
+ return job?.status === "running" || job?.status === "queued";
1007
+ }
1008
+
1009
+ function selectProgressiveJobKeys(jobs: AsyncJobState[], previousKeys: string[], bodyRows: number): string[] {
1010
+ if (bodyRows <= 0) return [];
1011
+ const jobsByKey = new Map(jobs.map((job) => [progressiveJobKey(job), job]));
1012
+ const selected: string[] = [];
1013
+ const append = (key: string): void => {
1014
+ if (selected.includes(key) || !jobsByKey.has(key)) return;
1015
+ selected.push(key);
1016
+ };
1017
+ for (const key of previousKeys) {
1018
+ if (!isProgressiveActiveJob(jobsByKey.get(key))) continue;
1019
+ append(key);
1020
+ if (selected.length >= bodyRows) return selected;
1021
+ }
1022
+ for (const job of orderedWidgetJobs(jobs)) {
1023
+ if (!isProgressiveActiveJob(job)) continue;
1024
+ const key = progressiveJobKey(job);
1025
+ append(key);
1026
+ if (selected.length >= bodyRows) break;
1027
+ }
1028
+ if (selected.length >= bodyRows) return selected;
1029
+ for (const key of previousKeys) {
1030
+ if (isProgressiveActiveJob(jobsByKey.get(key))) continue;
1031
+ append(key);
1032
+ if (selected.length >= bodyRows) return selected;
1033
+ }
1034
+ for (const job of orderedWidgetJobs(jobs)) {
1035
+ const key = progressiveJobKey(job);
1036
+ append(key);
1037
+ if (selected.length >= bodyRows) break;
1038
+ }
1039
+ return selected;
1040
+ }
1041
+
1042
+ function progressiveHeaderLine(jobs: AsyncJobState[], theme: Theme, width: number): string {
1043
+ const counts = widgetHeaderCounts(jobs);
1044
+ const hasActive = counts.running.length > 0 || counts.queued.length > 0;
1045
+ const glyph = counts.running.length > 0 ? runningGlyph(widgetJobsRunningSeed(counts.running)) : hasActive ? "●" : "○";
1046
+ const parts: string[] = [];
1047
+ if (counts.running.length > 0) parts.push(formatAgentRunningLabel(counts.running.length));
1048
+ if (counts.queued.length > 0) parts.push(`${counts.queued.length} queued`);
1049
+ if (!hasActive) {
1050
+ if (counts.failed.length > 0) parts.push(`${counts.failed.length} failed`);
1051
+ if (counts.paused.length > 0) parts.push(`${counts.paused.length} paused`);
1052
+ if (counts.complete.length > 0) parts.push(`${counts.complete.length}/${jobs.length} done`);
1053
+ }
1054
+ return truncLine(`${theme.fg(hasActive ? "accent" : "dim", glyph)} ${theme.fg(hasActive ? "accent" : "dim", "Async agents")} ${theme.fg("dim", "·")} ${theme.fg("dim", parts.join(", ") || `${jobs.length} total`)}`, width);
1055
+ }
1056
+
1057
+ function progressiveJobLine(job: AsyncJobState, theme: Theme, width: number): string {
1058
+ const stats = widgetStats(job, theme);
1059
+ const activity = widgetActivity(job);
1060
+ const status = job.status === "complete" ? "done" : job.status;
1061
+ const parts = [
1062
+ themeBold(theme, widgetJobName(job)),
1063
+ theme.fg("dim", status),
1064
+ stats,
1065
+ activity && activity.toLowerCase() !== status ? theme.fg("dim", activity) : "",
1066
+ ].filter(Boolean);
1067
+ return truncLine(` ${widgetStatusGlyph(job, theme)} ${parts.join(` ${theme.fg("dim", "·")} `)}`, width);
1068
+ }
1069
+
1070
+ function progressiveHiddenLine(hiddenJobs: AsyncJobState[], theme: Theme, width: number): string {
1071
+ const counts = widgetHeaderCounts(hiddenJobs);
1072
+ const parts: string[] = [];
1073
+ if (counts.running.length > 0) parts.push(`${counts.running.length} running`);
1074
+ if (counts.queued.length > 0) parts.push(`${counts.queued.length} queued`);
1075
+ const finished = counts.complete.length + counts.failed.length + counts.paused.length;
1076
+ if (finished > 0) parts.push(`${finished} finished`);
1077
+ return truncLine(theme.fg("dim", ` +${hiddenJobs.length} more${parts.length ? ` (${parts.join(", ")})` : ""}`), width);
1078
+ }
1079
+
1080
+ function buildProgressiveWidgetLines(jobs: AsyncJobState[], theme: Theme, width: number, lockedRows: number, previousKeys: string[]): { lines: string[]; visibleJobKeys: string[] } {
1081
+ const rowCount = Math.max(1, lockedRows);
1082
+ if (rowCount === 1) return { lines: buildSingleLineWidgetLines(jobs, theme, width), visibleJobKeys: [] };
1083
+
1084
+ const bodyRows = rowCount - 1;
1085
+ let visibleJobKeys = selectProgressiveJobKeys(jobs, previousKeys, bodyRows);
1086
+ const jobsByKey = new Map(jobs.map((job) => [progressiveJobKey(job), job]));
1087
+ let visibleJobs = visibleJobKeys.map((key) => jobsByKey.get(key)).filter((job): job is AsyncJobState => Boolean(job));
1088
+ let hiddenJobs = jobs.filter((job) => !visibleJobKeys.includes(progressiveJobKey(job)));
1089
+ const needsHiddenLine = hiddenJobs.length > 0;
1090
+
1091
+ if (needsHiddenLine && visibleJobs.length >= bodyRows && bodyRows > 0) {
1092
+ visibleJobs = visibleJobs.slice(0, bodyRows - 1);
1093
+ visibleJobKeys = visibleJobs.map(progressiveJobKey);
1094
+ hiddenJobs = jobs.filter((job) => !visibleJobKeys.includes(progressiveJobKey(job)));
1095
+ }
1096
+
1097
+ const lines = [
1098
+ progressiveHeaderLine(jobs, theme, width),
1099
+ ...visibleJobs.map((job) => progressiveJobLine(job, theme, width)),
1100
+ ];
1101
+ if (hiddenJobs.length > 0 && lines.length < rowCount) lines.push(progressiveHiddenLine(hiddenJobs, theme, width));
1102
+ while (lines.length < rowCount) lines.push(" ");
1103
+ return { lines: lines.slice(0, rowCount), visibleJobKeys };
1104
+ }
1105
+
1106
+ function collapsedWidgetLineBudget(rows: number): number {
1107
+ return Math.max(10, Math.min(14, Math.floor(rows * 0.35)));
1108
+ }
1109
+
902
1110
  function fitWidgetLineBudget(lines: string[], theme: Theme, width: number, expanded: boolean): string[] {
903
1111
  const rows = process.stdout.rows || 30;
904
1112
  const budget = expanded
905
1113
  ? Math.max(12, Math.min(24, Math.floor(rows * 0.55)))
906
- : Math.max(10, Math.min(14, Math.floor(rows * 0.35)));
1114
+ : collapsedWidgetLineBudget(rows);
907
1115
  if (lines.length <= budget) return lines;
908
1116
  const visibleLines = Math.max(1, budget - 1);
909
1117
  const hiddenCount = lines.length - visibleLines;
@@ -913,6 +1121,43 @@ function fitWidgetLineBudget(lines: string[], theme: Theme, width: number, expan
913
1121
  return [...lines.slice(0, visibleLines), truncLine(theme.fg("dim", hint), width)];
914
1122
  }
915
1123
 
1124
+ function fitAdaptiveWidgetLines(jobs: AsyncJobState[], lines: string[], theme: Theme, width: number, expanded: boolean): string[] {
1125
+ if (expanded) {
1126
+ resetWidgetLayoutSession();
1127
+ return fitWidgetLineBudget(lines, theme, width, true);
1128
+ }
1129
+
1130
+ const hasMatchingSession = widgetSessionMatches(expanded);
1131
+ const rows = currentTerminalRows();
1132
+ const columns = currentTerminalColumns();
1133
+ const availableRows = estimateAvailableWidgetRows();
1134
+
1135
+ if (hasMatchingSession && widgetLayoutSession?.tier === "single-line") {
1136
+ return buildSingleLineWidgetLines(jobs, theme, width);
1137
+ }
1138
+
1139
+ if (hasMatchingSession && widgetLayoutSession?.tier === "progressive" && widgetLayoutSession.lockedRows !== undefined) {
1140
+ const rendered = buildProgressiveWidgetLines(jobs, theme, width, widgetLayoutSession.lockedRows, widgetLayoutSession.visibleJobKeys);
1141
+ widgetLayoutSession.visibleJobKeys = rendered.visibleJobKeys;
1142
+ return rendered.lines;
1143
+ }
1144
+
1145
+ if (lines.length <= availableRows) {
1146
+ widgetLayoutSession = { expanded, rows, columns, tier: "full", visibleJobKeys: [] };
1147
+ return fitWidgetLineBudget(lines, theme, width, false);
1148
+ }
1149
+
1150
+ if (availableRows <= 2) {
1151
+ widgetLayoutSession = { expanded, rows, columns, tier: "single-line", visibleJobKeys: [] };
1152
+ return buildSingleLineWidgetLines(jobs, theme, width);
1153
+ }
1154
+
1155
+ const lockedRows = Math.min(availableRows, collapsedWidgetLineBudget(rows));
1156
+ const rendered = buildProgressiveWidgetLines(jobs, theme, width, lockedRows, []);
1157
+ widgetLayoutSession = { expanded, rows, columns, tier: "progressive", lockedRows, visibleJobKeys: rendered.visibleJobKeys };
1158
+ return rendered.lines;
1159
+ }
1160
+
916
1161
  function buildWidgetComponent(jobs: AsyncJobState[], expanded: boolean): (_tui: unknown, theme: Theme) => Component {
917
1162
  return (_tui, theme) => {
918
1163
  const width = getTermWidth();
@@ -922,7 +1167,7 @@ function buildWidgetComponent(jobs: AsyncJobState[], expanded: boolean): (_tui:
922
1167
  ? compactSingleWidgetLines(jobs[0]!, theme, width)
923
1168
  : buildWidgetLines(jobs, theme, width, false);
924
1169
  const container = new Container();
925
- for (const line of fitWidgetLineBudget(lines, theme, width, expanded)) container.addChild(new Text(line, 1, 0));
1170
+ for (const line of fitAdaptiveWidgetLines(jobs, lines, theme, width, expanded)) container.addChild(new Text(line, 1, 0));
926
1171
  return container;
927
1172
  };
928
1173
  }
@@ -1002,6 +1247,7 @@ export function buildWidgetLines(jobs: AsyncJobState[], theme: Theme, width = ge
1002
1247
  */
1003
1248
  export function renderWidget(ctx: ExtensionContext, jobs: AsyncJobState[]): void {
1004
1249
  if (jobs.length === 0) {
1250
+ resetWidgetLayoutSession();
1005
1251
  if (ctx.hasUI) ctx.ui.setWidget(WIDGET_KEY, undefined);
1006
1252
  return;
1007
1253
  }
@@ -1009,7 +1255,7 @@ export function renderWidget(ctx: ExtensionContext, jobs: AsyncJobState[]): void
1009
1255
  ctx.ui.setWidget(WIDGET_KEY, buildWidgetComponent(jobs, ctx.ui.getToolsExpanded?.() ?? false));
1010
1256
  }
1011
1257
 
1012
- function renderSingleCompact(d: Details, r: Details["results"][number], theme: Theme): Component {
1258
+ function renderSingleCompact(d: Details, r: Details["results"][number], theme: Theme, frame?: number): Component {
1013
1259
  const output = r.truncation?.text || getSingleResultOutput(r);
1014
1260
  const progress = r.progress || r.progressSummary;
1015
1261
  const isRunning = r.progress?.status === "running";
@@ -1021,7 +1267,7 @@ function renderSingleCompact(d: Details, r: Details["results"][number], theme: T
1021
1267
  const c = new Container();
1022
1268
  const width = getTermWidth() - 4;
1023
1269
  const modelDisplay = modelThinkingBadge(theme, r.model);
1024
- c.addChild(new Text(truncLine(`${resultGlyph(r, output, theme, isRunning)} ${theme.fg("toolTitle", theme.bold(r.agent))}${modelDisplay}${contextBadge}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`, width), 0, 0));
1270
+ c.addChild(new Text(truncLine(`${resultGlyph(r, output, theme, isRunning, undefined, frame)} ${theme.fg("toolTitle", theme.bold(r.agent))}${modelDisplay}${contextBadge}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`, width), 0, 0));
1025
1271
 
1026
1272
  if (isRunning && r.progress) {
1027
1273
  const progressSnapshotNow = snapshotNowForProgress(r.progress);
@@ -1045,7 +1291,7 @@ function renderSingleCompact(d: Details, r: Details["results"][number], theme: T
1045
1291
  return c;
1046
1292
  }
1047
1293
 
1048
- function renderMultiCompact(d: Details, theme: Theme): Component {
1294
+ function renderMultiCompact(d: Details, theme: Theme, frame?: number): Component {
1049
1295
  const hasRunning = d.progress?.some((p) => p.status === "running")
1050
1296
  || d.results.some((r) => r.progress?.status === "running")
1051
1297
  || workflowGraphHasStatus(d, ["running"]);
@@ -1071,7 +1317,7 @@ function renderMultiCompact(d: Details, theme: Theme): Component {
1071
1317
  const itemTitle = multiLabel.itemTitle;
1072
1318
  const stats = statJoin(theme, [multiLabel.headerLabel, formatProgressStats(theme, totalSummary)]);
1073
1319
  const glyph = hasRunning
1074
- ? theme.fg("accent", runningGlyph(runningSeed(progressRunningSeed(totalSummary), d.currentStepIndex)))
1320
+ ? theme.fg("accent", runningGlyph(frame !== undefined ? (runningSeed(progressRunningSeed(totalSummary), d.currentStepIndex) ?? 0) + frame : runningSeed(progressRunningSeed(totalSummary), d.currentStepIndex)))
1075
1321
  : failed
1076
1322
  ? theme.fg("error", "✗")
1077
1323
  : paused
@@ -1117,7 +1363,7 @@ function renderMultiCompact(d: Details, theme: Theme): Component {
1117
1363
  const rPending = rProg && "status" in rProg && rProg.status === "pending";
1118
1364
  const stepNumber = r.progress?.index !== undefined ? r.progress.index + 1 : progressFromArray?.index !== undefined ? progressFromArray.index + 1 : i + 1;
1119
1365
  const stepStats = formatProgressStats(theme, rProg);
1120
- const glyph = rPending ? theme.fg("dim", "◦") : resultGlyph(r, output, theme, rRunning, progressRunningSeed(rProg));
1366
+ const glyph = rPending ? theme.fg("dim", "◦") : resultGlyph(r, output, theme, rRunning, progressRunningSeed(rProg), frame);
1121
1367
  const pendingLabel = rPending ? ` ${theme.fg("dim", "· pending")}` : "";
1122
1368
  const stepLabel = resultRowLabel(d, multiLabel, i, stepNumber);
1123
1369
  const line = `${glyph} ${stepLabel}: ${themeBold(theme, agentName)}${stepStats ? ` ${theme.fg("dim", "·")} ${stepStats}` : ""}${pendingLabel}`;
@@ -1144,13 +1390,19 @@ export function renderSubagentResult(
1144
1390
  result: AgentToolResult<Details>,
1145
1391
  options: { expanded: boolean },
1146
1392
  theme: Theme,
1393
+ frame?: number,
1147
1394
  ): Component {
1148
1395
  const d = result.details;
1149
1396
  if (!d || !d.results.length) {
1150
1397
  const t = result.content[0];
1151
1398
  const text = t?.type === "text" ? t.text : "(no output)";
1152
1399
  const contextPrefix = d?.context === "fork" ? `${theme.fg("warning", "[fork]")} ` : "";
1153
- return new Text(truncLine(`${contextPrefix}${text}`, getTermWidth() - 4), 0, 0);
1400
+ const width = getTermWidth() - 4;
1401
+ if (!text.includes("\n")) return new Text(truncLine(`${contextPrefix}${text}`, width), 0, 0);
1402
+ const c = new Container();
1403
+ const wrapped = wrapPlainText(`${contextPrefix}${text}`, width);
1404
+ for (const line of wrapped) c.addChild(new Text(line, 0, 0));
1405
+ return c;
1154
1406
  }
1155
1407
 
1156
1408
  const expanded = options.expanded;
@@ -1158,7 +1410,7 @@ export function renderSubagentResult(
1158
1410
 
1159
1411
  if (d.mode === "single" && d.results.length === 1) {
1160
1412
  const r = d.results[0];
1161
- if (!expanded) return renderSingleCompact(d, r, theme);
1413
+ if (!expanded) return renderSingleCompact(d, r, theme, frame);
1162
1414
  const isRunning = r.progress?.status === "running";
1163
1415
  const icon = isRunning
1164
1416
  ? theme.fg("warning", "running")
@@ -1252,7 +1504,7 @@ export function renderSubagentResult(
1252
1504
  return c;
1253
1505
  }
1254
1506
 
1255
- if (!expanded) return renderMultiCompact(d, theme);
1507
+ if (!expanded) return renderMultiCompact(d, theme, frame);
1256
1508
 
1257
1509
  const hasRunning = d.progress?.some((p) => p.status === "running")
1258
1510
  || d.results.some((r) => r.progress?.status === "running")