@fosterg4/pi-subagent 1.0.0 → 1.0.2

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/agents/planner.md CHANGED
@@ -2,7 +2,6 @@
2
2
  name: planner
3
3
  description: Creates implementation plans from context and requirements
4
4
  tools: read, grep, find, ls
5
- model: claude-sonnet-4-5
6
5
  inputSchema:
7
6
  type: object
8
7
  properties:
@@ -48,6 +47,9 @@ outputSchema:
48
47
 
49
48
  You are a planning specialist. You receive context (from a scout) and requirements, then produce a clear implementation plan.
50
49
 
50
+ ## STRICT OUTPUT RULE
51
+ Your ENTIRE response must be ONLY a valid JSON object matching the outputSchema below. Do NOT include any conversational text, greetings, explanations, markdown code fences, or any wrapping. No ```json blocks. No "Here is the plan:" prefix. Nothing but the raw JSON object.
52
+
51
53
  You must NOT make any changes. Only read, analyze, and plan.
52
54
 
53
55
  Return your plan as a JSON object matching the outputSchema.
@@ -2,7 +2,6 @@
2
2
  name: reviewer
3
3
  description: Code review specialist for quality and security analysis
4
4
  tools: read, grep, find, ls, bash
5
- model: claude-sonnet-4-5
6
5
  inputSchema:
7
6
  type: object
8
7
  properties:
@@ -60,7 +59,8 @@ Strategy:
60
59
  2. Read the modified files
61
60
  3. Check for bugs, security issues, code smells
62
61
 
63
- Return your review as a JSON object matching the outputSchema.
62
+ ## STRICT OUTPUT RULE
63
+ Your ENTIRE response must be ONLY a valid JSON object matching the outputSchema below. Do NOT include any conversational text, greetings, explanations, markdown code fences, or any wrapping. No ```json blocks. Nothing but the raw JSON object.
64
64
 
65
65
  ## Output format (JSON)
66
66
 
package/agents/scout.md CHANGED
@@ -2,7 +2,6 @@
2
2
  name: scout
3
3
  description: Fast codebase recon that returns compressed context for handoff to other agents
4
4
  tools: read, grep, find, ls, bash
5
- model: claude-haiku-4-5
6
5
  inputSchema:
7
6
  type: object
8
7
  properties:
@@ -37,6 +36,9 @@ outputSchema:
37
36
 
38
37
  You are a scout. Quickly investigate a codebase and return structured findings that another agent can use without re-reading everything.
39
38
 
39
+ ## STRICT OUTPUT RULE
40
+ Your ENTIRE response must be ONLY a valid JSON object matching the outputSchema below. Do NOT include any conversational text, greetings, explanations, markdown code fences, or any wrapping. No ```json blocks. No "Here are my findings:" prefix. Nothing but the raw JSON object.
41
+
40
42
  Your output will be passed to an agent who has NOT seen the files you explored.
41
43
 
42
44
  Thoroughness (infer from task, default medium):
package/agents/worker.md CHANGED
@@ -1,7 +1,6 @@
1
1
  ---
2
2
  name: worker
3
3
  description: General-purpose subagent with full capabilities, isolated context
4
- model: claude-sonnet-4-5
5
4
  inputSchema:
6
5
  type: object
7
6
  properties:
@@ -33,7 +32,8 @@ You are a worker agent with full capabilities. You operate in an isolated contex
33
32
 
34
33
  Work autonomously to complete the assigned task. Use all available tools as needed.
35
34
 
36
- Return your results as a JSON object matching the outputSchema.
35
+ ## STRICT OUTPUT RULE
36
+ Your ENTIRE response must be ONLY a valid JSON object matching the outputSchema below. Do NOT include any conversational text, greetings, explanations, markdown code fences, or any wrapping. No ```json blocks. Nothing but the raw JSON object.
37
37
 
38
38
  ## Output format (JSON)
39
39
 
package/index.ts CHANGED
@@ -38,115 +38,9 @@ import { type ValidationResult, validateSchema } from "./validate.ts";
38
38
 
39
39
  const MAX_PARALLEL_TASKS = 8;
40
40
  const MAX_CONCURRENCY = 4;
41
- const COLLAPSED_ITEM_COUNT = 10;
42
41
  const PER_TASK_OUTPUT_CAP = 50 * 1024;
43
42
 
44
- // ---------------------------------------------------------------------------
45
- // Formatting utilities
46
- // ---------------------------------------------------------------------------
47
-
48
- function formatTokens(count: number): string {
49
- if (count < 1000) return count.toString();
50
- if (count < 10000) return `${(count / 1000).toFixed(1)}k`;
51
- if (count < 1000000) return `${Math.round(count / 1000)}k`;
52
- return `${(count / 1000000).toFixed(1)}M`;
53
- }
54
-
55
- function formatUsageStats(
56
- usage: {
57
- input: number;
58
- output: number;
59
- cacheRead: number;
60
- cacheWrite: number;
61
- cost: number;
62
- contextTokens?: number;
63
- turns?: number;
64
- },
65
- model?: string,
66
- ): string {
67
- const parts: string[] = [];
68
- if (usage.turns) parts.push(`${usage.turns} turn${usage.turns > 1 ? "s" : ""}`);
69
- if (usage.input) parts.push(`\u2191${formatTokens(usage.input)}`);
70
- if (usage.output) parts.push(`\u2193${formatTokens(usage.output)}`);
71
- if (usage.cacheRead) parts.push(`R${formatTokens(usage.cacheRead)}`);
72
- if (usage.cacheWrite) parts.push(`W${formatTokens(usage.cacheWrite)}`);
73
- if (usage.cost) parts.push(`$${usage.cost.toFixed(4)}`);
74
- if (usage.contextTokens && usage.contextTokens > 0) {
75
- parts.push(`ctx:${formatTokens(usage.contextTokens)}`);
76
- }
77
- if (model) parts.push(model);
78
- return parts.join(" ");
79
- }
80
-
81
- function formatToolCall(
82
- toolName: string,
83
- args: Record<string, unknown>,
84
- themeFg: (color: string, text: string) => string,
85
- ): string {
86
- const shortenPath = (p: string) => {
87
- const home = os.homedir();
88
- return p.startsWith(home) ? `~${p.slice(home.length)}` : p;
89
- };
90
43
 
91
- switch (toolName) {
92
- case "bash": {
93
- const command = (args.command as string) || "...";
94
- const preview = command.length > 60 ? `${command.slice(0, 60)}...` : command;
95
- return themeFg("muted", "$ ") + themeFg("toolOutput", preview);
96
- }
97
- case "read": {
98
- const rawPath = (args.file_path || args.path || "...") as string;
99
- const filePath = shortenPath(rawPath);
100
- const offset = args.offset as number | undefined;
101
- const limit = args.limit as number | undefined;
102
- let text = themeFg("accent", filePath);
103
- if (offset !== undefined || limit !== undefined) {
104
- const startLine = offset ?? 1;
105
- const endLine = limit !== undefined ? startLine + limit - 1 : "";
106
- text += themeFg("warning", `:${startLine}${endLine ? `-${endLine}` : ""}`);
107
- }
108
- return themeFg("muted", "read ") + text;
109
- }
110
- case "write": {
111
- const rawPath = (args.file_path || args.path || "...") as string;
112
- const filePath = shortenPath(rawPath);
113
- const content = (args.content || "") as string;
114
- const lines = content.split("\n").length;
115
- let text = themeFg("muted", "write ") + themeFg("accent", filePath);
116
- if (lines > 1) text += themeFg("dim", ` (${lines} lines)`);
117
- return text;
118
- }
119
- case "edit": {
120
- const rawPath = (args.file_path || args.path || "...") as string;
121
- return themeFg("muted", "edit ") + themeFg("accent", shortenPath(rawPath));
122
- }
123
- case "ls":
124
- return themeFg("muted", "ls ") + themeFg("accent", shortenPath((args.path || ".") as string));
125
- case "find":
126
- return (
127
- themeFg("muted", "find ") +
128
- themeFg("accent", (args.pattern || "*") as string) +
129
- themeFg("dim", ` in ${shortenPath((args.path || ".") as string)}`)
130
- );
131
- case "grep":
132
- return (
133
- themeFg("muted", "grep ") +
134
- themeFg("accent", `/${(args.pattern || "") as string}/`) +
135
- themeFg("dim", ` in ${shortenPath((args.path || ".") as string)}`)
136
- );
137
- default:
138
- return (
139
- themeFg("accent", toolName) +
140
- themeFg("dim", ` ${JSON.stringify(args).slice(0, 50)}...`)
141
- );
142
- }
143
- }
144
-
145
- // ---------------------------------------------------------------------------
146
- // Types
147
- // ---------------------------------------------------------------------------
148
-
149
- interface UsageStats {
150
44
  input: number;
151
45
  output: number;
152
46
  cacheRead: number;
@@ -225,25 +119,6 @@ function truncateParallelOutput(output: string): string {
225
119
  return `${truncated}\n\n[Output truncated: ${byteLength - Buffer.byteLength(truncated, "utf8")} bytes omitted. Full output preserved in tool details.]`;
226
120
  }
227
121
 
228
- type DisplayItem =
229
- | { type: "text"; text: string }
230
- | { type: "toolCall"; name: string; args: Record<string, unknown> };
231
-
232
- function getDisplayItems(messages: Message[]): DisplayItem[] {
233
- const items: DisplayItem[] = [];
234
- for (const msg of messages) {
235
- if (msg.role === "assistant") {
236
- for (const part of msg.content) {
237
- if (part.type === "text")
238
- items.push({ type: "text", text: part.text });
239
- else if (part.type === "toolCall")
240
- items.push({ type: "toolCall", name: part.name, args: part.arguments });
241
- }
242
- }
243
- }
244
- return items;
245
- }
246
-
247
122
  // ---------------------------------------------------------------------------
248
123
  // Concurrency
249
124
  // ---------------------------------------------------------------------------
@@ -1065,363 +940,131 @@ export default function (pi: ExtensionAPI) {
1065
940
 
1066
941
  const mdTheme = getMarkdownTheme();
1067
942
 
1068
- const renderDisplayItems = (items: DisplayItem[], limit?: number) => {
1069
- const toShow = limit ? items.slice(-limit) : items;
1070
- const skipped = limit && items.length > limit ? items.length - limit : 0;
1071
- let text = "";
1072
- if (skipped > 0)
1073
- text += theme.fg("muted", `... ${skipped} earlier items\n`);
1074
- for (const item of toShow) {
1075
- if (item.type === "text") {
1076
- const preview = expanded
1077
- ? item.text
1078
- : item.text.split("\n").slice(0, 3).join("\n");
1079
- text += `${theme.fg("toolOutput", preview)}\n`;
1080
- } else {
1081
- text += `${theme.fg("muted", "\u2192 ") + formatToolCall(item.name, item.args, theme.fg.bind(theme))}\n`;
1082
- }
1083
- }
1084
- return text.trimEnd();
943
+ const getFinalOutputText = (messages: Message[]): string => {
944
+ const text = getFinalOutput(messages);
945
+ // Try to extract clean text from JSON output (remove wrapper text)
946
+ const extracted = extractStructuredOutput(messages);
947
+ if (extracted) return JSON.stringify(extracted, null, 2);
948
+ return text;
949
+ };
950
+
951
+ const getStatusText = (r: SingleResult): string => {
952
+ if (r.exitCode === 0) return theme.fg("success", "\u2713");
953
+ if (r.exitCode === -1) return theme.fg("warning", "\u23F3");
954
+ return theme.fg("error", "\u2717");
1085
955
  };
1086
956
 
1087
957
  // --- Single mode ---
1088
958
  if (details.mode === "single" && details.results.length === 1) {
1089
959
  const r = details.results[0];
1090
- const isError = isFailedResult(r);
1091
- const icon = isError
1092
- ? theme.fg("error", "\u2717")
1093
- : theme.fg("success", "\u2713");
1094
- const displayItems = getDisplayItems(r.messages);
960
+ const status = getStatusText(r);
961
+ const finalOutput = getFinalOutputText(r.messages);
1095
962
 
1096
963
  if (expanded) {
1097
964
  const container = new Container();
1098
- let header = `${icon} ${theme.fg("toolTitle", theme.bold(r.agent))}${theme.fg("muted", ` (${r.agentSource})`)}`;
1099
- if (isError && r.stopReason)
1100
- header += ` ${theme.fg("error", `[${r.stopReason}]`)}`;
965
+ const header = `${status} ${theme.fg("accent", r.agent)}${theme.fg("muted", r.model ? ` \u00B7 ${r.model}` : "")}`;
1101
966
  container.addChild(new Text(header, 0, 0));
1102
- if (isError && r.errorMessage)
1103
- container.addChild(
1104
- new Text(theme.fg("error", `Error: ${r.errorMessage}`), 0, 0),
1105
- );
1106
- container.addChild(new Spacer(1));
1107
- container.addChild(new Text(theme.fg("muted", "\u2500\u2500\u2500 Task \u2500\u2500\u2500"), 0, 0));
1108
- container.addChild(new Text(theme.fg("dim", r.task), 0, 0));
1109
-
1110
- // Show tool calls
1111
- const toolCalls = displayItems.filter(
1112
- (i) => i.type === "toolCall",
1113
- );
1114
- if (toolCalls.length > 0) {
1115
- container.addChild(new Spacer(1));
1116
- container.addChild(
1117
- new Text(
1118
- theme.fg("muted", "\u2500\u2500\u2500 Tool Calls \u2500\u2500\u2500"),
1119
- 0,
1120
- 0,
1121
- ),
1122
- );
1123
- for (const item of toolCalls) {
1124
- container.addChild(
1125
- new Text(
1126
- theme.fg("muted", "\u2192 ") +
1127
- formatToolCall(
1128
- item.name,
1129
- item.args,
1130
- theme.fg.bind(theme),
1131
- ),
1132
- 0,
1133
- 0,
1134
- ),
1135
- );
1136
- }
1137
- }
1138
-
1139
- // Show structured output if available
1140
- if (r.structuredOutput && Object.keys(r.structuredOutput).length > 0) {
1141
- container.addChild(new Spacer(1));
1142
- container.addChild(
1143
- new Text(
1144
- theme.fg("muted", "\u2500\u2500\u2500 Structured Output \u2500\u2500\u2500"),
1145
- 0,
1146
- 0,
1147
- ),
1148
- );
1149
- container.addChild(
1150
- new Text(
1151
- theme.fg("toolOutput", JSON.stringify(r.structuredOutput, null, 2)),
1152
- 0,
1153
- 0,
1154
- ),
1155
- );
1156
- }
1157
-
1158
- // Show final output
1159
- const finalOutput = getFinalOutput(r.messages);
967
+ if (r.stderr) container.addChild(new Text(theme.fg("error", r.stderr), 0, 0));
1160
968
  if (finalOutput) {
1161
969
  container.addChild(new Spacer(1));
1162
- container.addChild(
1163
- new Text(theme.fg("muted", "\u2500\u2500\u2500 Output \u2500\u2500\u2500"), 0, 0),
1164
- );
1165
970
  container.addChild(new Markdown(finalOutput.trim(), 0, 0, mdTheme));
1166
971
  }
1167
-
1168
- const usageStr = formatUsageStats(r.usage, r.model);
1169
- if (usageStr) {
1170
- container.addChild(new Spacer(1));
1171
- container.addChild(new Text(theme.fg("dim", usageStr), 0, 0));
1172
- }
1173
972
  return container;
1174
973
  }
1175
974
 
1176
- let text = `${icon} ${theme.fg("toolTitle", theme.bold(r.agent))}${theme.fg("muted", ` (${r.agentSource})`)}`;
1177
- if (isError && r.stopReason) text += ` ${theme.fg("error", `[${r.stopReason}]`)}`;
1178
- if (isError && r.errorMessage)
1179
- text += `\n${theme.fg("error", `Error: ${r.errorMessage}`)}`;
1180
- else if (displayItems.length === 0)
975
+ let text = `${status} ${theme.fg("accent", r.agent)}`;
976
+ if (finalOutput) {
977
+ const preview = finalOutput.split("\n").slice(0, 5).join("\n");
978
+ const truncated = finalOutput.split("\n").length > 5;
979
+ text += `\n${theme.fg("toolOutput", preview)}`;
980
+ if (truncated) text += `\n${theme.fg("muted", "(Ctrl+O to expand)")}`;
981
+ } else {
1181
982
  text += `\n${theme.fg("muted", "(no output)")}`;
1182
- else {
1183
- text += `\n${renderDisplayItems(displayItems, COLLAPSED_ITEM_COUNT)}`;
1184
- if (displayItems.length > COLLAPSED_ITEM_COUNT)
1185
- text += `\n${theme.fg("muted", "(Ctrl+O to expand)")}`;
1186
983
  }
1187
- const usageStr = formatUsageStats(r.usage, r.model);
1188
- if (usageStr) text += `\n${theme.fg("dim", usageStr)}`;
1189
984
  return new Text(text, 0, 0);
1190
985
  }
1191
986
 
1192
- const aggregateUsage = (results: SingleResult[]) => {
1193
- const total = {
1194
- input: 0,
1195
- output: 0,
1196
- cacheRead: 0,
1197
- cacheWrite: 0,
1198
- cost: 0,
1199
- turns: 0,
1200
- };
1201
- for (const r of results) {
1202
- total.input += r.usage.input;
1203
- total.output += r.usage.output;
1204
- total.cacheRead += r.usage.cacheRead;
1205
- total.cacheWrite += r.usage.cacheWrite;
1206
- total.cost += r.usage.cost;
1207
- total.turns += r.usage.turns;
1208
- }
1209
- return total;
1210
- };
1211
-
1212
987
  // --- Chain mode ---
1213
988
  if (details.mode === "chain") {
1214
- const successCount = details.results.filter(
1215
- (r) => r.exitCode === 0,
1216
- ).length;
1217
- const icon =
1218
- successCount === details.results.length
1219
- ? theme.fg("success", "\u2713")
1220
- : theme.fg("error", "\u2717");
989
+ const lastResult = details.results[details.results.length - 1];
990
+ const finalOutput = lastResult ? getFinalOutputText(lastResult.messages) : "";
991
+ const allOk = details.results.every((r) => r.exitCode === 0);
992
+ const icon = allOk ? theme.fg("success", "\u2713") : theme.fg("error", "\u2717");
993
+ const steps = details.results.map((r) => r.agent).join(" \u2192 ");
1221
994
 
1222
995
  if (expanded) {
1223
996
  const container = new Container();
1224
997
  container.addChild(
1225
- new Text(
1226
- icon +
1227
- " " +
1228
- theme.fg("toolTitle", theme.bold("chain ")) +
1229
- theme.fg("accent", `${successCount}/${details.results.length} steps`),
1230
- 0,
1231
- 0,
1232
- ),
998
+ new Text(`${icon} ${theme.fg("accent", steps)}`, 0, 0),
1233
999
  );
1234
-
1235
1000
  for (const r of details.results) {
1236
- const rIcon =
1237
- r.exitCode === 0
1238
- ? theme.fg("success", "\u2713")
1239
- : theme.fg("error", "\u2717");
1240
- const displayItems = getDisplayItems(r.messages);
1241
- const finalOutput = getFinalOutput(r.messages);
1242
-
1243
- container.addChild(new Spacer(1));
1244
- container.addChild(
1245
- new Text(
1246
- `${theme.fg("muted", `\u2500\u2500\u2500 Step ${r.step}: `) + theme.fg("accent", r.agent)} ${rIcon}`,
1247
- 0,
1248
- 0,
1249
- ),
1250
- );
1251
- container.addChild(
1252
- new Text(theme.fg("muted", "Task: ") + theme.fg("dim", r.task), 0, 0),
1253
- );
1254
-
1255
- for (const item of displayItems) {
1256
- if (item.type === "toolCall") {
1257
- container.addChild(
1258
- new Text(
1259
- theme.fg("muted", "\u2192 ") +
1260
- formatToolCall(
1261
- item.name,
1262
- item.args,
1263
- theme.fg.bind(theme),
1264
- ),
1265
- 0,
1266
- 0,
1267
- ),
1268
- );
1269
- }
1270
- }
1271
-
1272
- if (finalOutput) {
1001
+ const out = getFinalOutputText(r.messages);
1002
+ if (out) {
1273
1003
  container.addChild(new Spacer(1));
1274
1004
  container.addChild(
1275
- new Markdown(finalOutput.trim(), 0, 0, mdTheme),
1005
+ new Text(
1006
+ `${getStatusText(r)} ${theme.fg("accent", r.agent)}${r.model ? theme.fg("muted", ` \u00B7 ${r.model}`) : ""}`,
1007
+ 0,
1008
+ 0,
1009
+ ),
1276
1010
  );
1011
+ container.addChild(new Markdown(out.trim(), 0, 0, mdTheme));
1277
1012
  }
1278
-
1279
- const stepUsage = formatUsageStats(r.usage, r.model);
1280
- if (stepUsage)
1281
- container.addChild(new Text(theme.fg("dim", stepUsage), 0, 0));
1282
- }
1283
-
1284
- const usageStr = formatUsageStats(aggregateUsage(details.results));
1285
- if (usageStr) {
1286
- container.addChild(new Spacer(1));
1287
- container.addChild(
1288
- new Text(theme.fg("dim", `Total: ${usageStr}`), 0, 0),
1289
- );
1290
1013
  }
1291
1014
  return container;
1292
1015
  }
1293
1016
 
1294
- let text =
1295
- icon +
1296
- " " +
1297
- theme.fg("toolTitle", theme.bold("chain ")) +
1298
- theme.fg("accent", `${successCount}/${details.results.length} steps`);
1299
- for (const r of details.results) {
1300
- const rIcon =
1301
- r.exitCode === 0
1302
- ? theme.fg("success", "\u2713")
1303
- : theme.fg("error", "\u2717");
1304
- const displayItems = getDisplayItems(r.messages);
1305
- text += `\n\n${theme.fg("muted", `\u2500\u2500\u2500 Step ${r.step}: `)}${theme.fg("accent", r.agent)} ${rIcon}`;
1306
- if (displayItems.length === 0)
1307
- text += `\n${theme.fg("muted", "(no output)")}`;
1308
- else text += `\n${renderDisplayItems(displayItems, 5)}`;
1017
+ let text = `${icon} ${theme.fg("accent", steps)}`;
1018
+ if (finalOutput) {
1019
+ const preview = finalOutput.split("\n").slice(0, 5).join("\n");
1020
+ const truncated = finalOutput.split("\n").length > 5;
1021
+ text += `\n${theme.fg("toolOutput", preview)}`;
1022
+ if (truncated) text += `\n${theme.fg("muted", "(Ctrl+O to expand)")}`;
1023
+ } else {
1024
+ text += `\n${theme.fg("muted", "(no output)")}`;
1309
1025
  }
1310
- const usageStr = formatUsageStats(aggregateUsage(details.results));
1311
- if (usageStr) text += `\n\n${theme.fg("dim", `Total: ${usageStr}`)}`;
1312
- text += `\n${theme.fg("muted", "(Ctrl+O to expand)")}`;
1313
1026
  return new Text(text, 0, 0);
1314
1027
  }
1315
1028
 
1316
1029
  // --- Parallel mode ---
1317
1030
  if (details.mode === "parallel") {
1318
- const running = details.results.filter(
1319
- (r) => r.exitCode === -1,
1320
- ).length;
1321
- const successCount = details.results.filter(
1322
- (r) => r.exitCode !== -1 && !isFailedResult(r),
1323
- ).length;
1324
- const failCount = details.results.filter(
1325
- (r) => r.exitCode !== -1 && isFailedResult(r),
1326
- ).length;
1327
- const isRunning = running > 0;
1328
- const icon = isRunning
1031
+ const running = details.results.filter((r) => r.exitCode === -1).length;
1032
+ const done = details.results.filter((r) => r.exitCode !== -1).length;
1033
+ const allOk = details.results.every((r) => r.exitCode === 0);
1034
+ const icon = running > 0
1329
1035
  ? theme.fg("warning", "\u23F3")
1330
- : failCount > 0
1331
- ? theme.fg("warning", "\u25D0")
1332
- : theme.fg("success", "\u2713");
1333
- const status = isRunning
1334
- ? `${successCount + failCount}/${details.results.length} done, ${running} running`
1335
- : `${successCount}/${details.results.length} tasks`;
1336
-
1337
- if (expanded && !isRunning) {
1036
+ : allOk
1037
+ ? theme.fg("success", "\u2713")
1038
+ : theme.fg("error", "\u2717");
1039
+
1040
+ if (expanded && running === 0) {
1338
1041
  const container = new Container();
1339
1042
  container.addChild(
1340
1043
  new Text(
1341
- `${icon} ${theme.fg("toolTitle", theme.bold("parallel "))}${theme.fg("accent", status)}`,
1044
+ `${icon} ${theme.fg("accent", `${done} tasks`)}`,
1342
1045
  0,
1343
1046
  0,
1344
1047
  ),
1345
1048
  );
1346
-
1347
1049
  for (const r of details.results) {
1348
- const rIcon = isFailedResult(r)
1349
- ? theme.fg("error", "\u2717")
1350
- : theme.fg("success", "\u2713");
1351
- const displayItems = getDisplayItems(r.messages);
1352
- const finalOutput = getFinalOutput(r.messages);
1353
-
1354
- container.addChild(new Spacer(1));
1355
- container.addChild(
1356
- new Text(
1357
- `${theme.fg("muted", "\u2500\u2500\u2500 ") + theme.fg("accent", r.agent)} ${rIcon}`,
1358
- 0,
1359
- 0,
1360
- ),
1361
- );
1362
- container.addChild(
1363
- new Text(theme.fg("muted", "Task: ") + theme.fg("dim", r.task), 0, 0),
1364
- );
1365
-
1366
- for (const item of displayItems) {
1367
- if (item.type === "toolCall") {
1368
- container.addChild(
1369
- new Text(
1370
- theme.fg("muted", "\u2192 ") +
1371
- formatToolCall(
1372
- item.name,
1373
- item.args,
1374
- theme.fg.bind(theme),
1375
- ),
1376
- 0,
1377
- 0,
1378
- ),
1379
- );
1380
- }
1381
- }
1382
-
1383
- if (finalOutput) {
1050
+ const out = getFinalOutputText(r.messages);
1051
+ if (out) {
1384
1052
  container.addChild(new Spacer(1));
1385
1053
  container.addChild(
1386
- new Markdown(finalOutput.trim(), 0, 0, mdTheme),
1054
+ new Text(
1055
+ `${getStatusText(r)} ${theme.fg("accent", r.agent)}`,
1056
+ 0,
1057
+ 0,
1058
+ ),
1387
1059
  );
1060
+ container.addChild(new Markdown(out.trim(), 0, 0, mdTheme));
1388
1061
  }
1389
-
1390
- const taskUsage = formatUsageStats(r.usage, r.model);
1391
- if (taskUsage)
1392
- container.addChild(new Text(theme.fg("dim", taskUsage), 0, 0));
1393
- }
1394
-
1395
- const usageStr = formatUsageStats(aggregateUsage(details.results));
1396
- if (usageStr) {
1397
- container.addChild(new Spacer(1));
1398
- container.addChild(
1399
- new Text(theme.fg("dim", `Total: ${usageStr}`), 0, 0),
1400
- );
1401
1062
  }
1402
1063
  return container;
1403
1064
  }
1404
1065
 
1405
- let text = `${icon} ${theme.fg("toolTitle", theme.bold("parallel "))}${theme.fg("accent", status)}`;
1406
- for (const r of details.results) {
1407
- const rIcon =
1408
- r.exitCode === -1
1409
- ? theme.fg("warning", "\u23F3")
1410
- : isFailedResult(r)
1411
- ? theme.fg("error", "\u2717")
1412
- : theme.fg("success", "\u2713");
1413
- const displayItems = getDisplayItems(r.messages);
1414
- text += `\n\n${theme.fg("muted", "\u2500\u2500\u2500 ")}${theme.fg("accent", r.agent)} ${rIcon}`;
1415
- if (displayItems.length === 0)
1416
- text += `\n${theme.fg("muted", r.exitCode === -1 ? "(running...)" : "(no output)")}`;
1417
- else text += `\n${renderDisplayItems(displayItems, 5)}`;
1418
- }
1419
- if (!isRunning) {
1420
- const usageStr = formatUsageStats(aggregateUsage(details.results));
1421
- if (usageStr) text += `\n\n${theme.fg("dim", `Total: ${usageStr}`)}`;
1422
- }
1423
- if (!expanded)
1424
- text += `\n${theme.fg("muted", "(Ctrl+O to expand)")}`;
1066
+ let text = `${icon} ${theme.fg("accent", `${done}/${details.results.length} tasks`)}`;
1067
+ if (running === 0 && !expanded) text += theme.fg("muted", " (Ctrl+O to expand)");
1425
1068
  return new Text(text, 0, 0);
1426
1069
  }
1427
1070
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fosterg4/pi-subagent",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "Delegate tasks to specialized subagents with isolated context windows, structured JSON handoff, contract schemas, and live TUI streaming",
5
5
  "keywords": [
6
6
  "pi-package",