@fosterg4/pi-subagent 1.0.1 → 1.0.3

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
@@ -47,6 +47,9 @@ outputSchema:
47
47
 
48
48
  You are a planning specialist. You receive context (from a scout) and requirements, then produce a clear implementation plan.
49
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
+
50
53
  You must NOT make any changes. Only read, analyze, and plan.
51
54
 
52
55
  Return your plan as a JSON object matching the outputSchema.
@@ -59,7 +59,8 @@ Strategy:
59
59
  2. Read the modified files
60
60
  3. Check for bugs, security issues, code smells
61
61
 
62
- 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.
63
64
 
64
65
  ## Output format (JSON)
65
66
 
package/agents/scout.md CHANGED
@@ -36,6 +36,9 @@ outputSchema:
36
36
 
37
37
  You are a scout. Quickly investigate a codebase and return structured findings that another agent can use without re-reading everything.
38
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
+
39
42
  Your output will be passed to an agent who has NOT seen the files you explored.
40
43
 
41
44
  Thoroughness (infer from task, default medium):
package/agents/worker.md CHANGED
@@ -32,7 +32,8 @@ You are a worker agent with full capabilities. You operate in an isolated contex
32
32
 
33
33
  Work autonomously to complete the assigned task. Use all available tools as needed.
34
34
 
35
- 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.
36
37
 
37
38
  ## Output format (JSON)
38
39
 
package/index.ts CHANGED
@@ -35,116 +35,13 @@ import {
35
35
  formatAgentList,
36
36
  } from "./agents.ts";
37
37
  import { type ValidationResult, validateSchema } from "./validate.ts";
38
+ import { fmt, usageLine, sumUsage } from "./utils.ts";
38
39
 
39
40
  const MAX_PARALLEL_TASKS = 8;
40
41
  const MAX_CONCURRENCY = 4;
41
- const COLLAPSED_ITEM_COUNT = 10;
42
42
  const PER_TASK_OUTPUT_CAP = 50 * 1024;
43
43
 
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
-
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
- // ---------------------------------------------------------------------------
44
+ /* eslint-disable @typescript-eslint/no-unused-vars */
148
45
 
149
46
  interface UsageStats {
150
47
  input: number;
@@ -225,25 +122,6 @@ function truncateParallelOutput(output: string): string {
225
122
  return `${truncated}\n\n[Output truncated: ${byteLength - Buffer.byteLength(truncated, "utf8")} bytes omitted. Full output preserved in tool details.]`;
226
123
  }
227
124
 
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
125
  // ---------------------------------------------------------------------------
248
126
  // Concurrency
249
127
  // ---------------------------------------------------------------------------
@@ -1065,363 +943,154 @@ export default function (pi: ExtensionAPI) {
1065
943
 
1066
944
  const mdTheme = getMarkdownTheme();
1067
945
 
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();
946
+ const getFinalOutputText = (messages: Message[]): string => {
947
+ const text = getFinalOutput(messages);
948
+ // Try to extract clean text from JSON output (remove wrapper text)
949
+ const extracted = extractStructuredOutput(messages);
950
+ if (extracted) return JSON.stringify(extracted, null, 2);
951
+ return text;
952
+ };
953
+
954
+ const getStatusText = (r: SingleResult): string => {
955
+ if (r.exitCode === 0) return theme.fg("success", "\u2713");
956
+ if (r.exitCode === -1) return theme.fg("warning", "\u23F3");
957
+ return theme.fg("error", "\u2717");
1085
958
  };
1086
959
 
1087
- // --- Single mode ---
960
+ // --- Single mode ---
1088
961
  if (details.mode === "single" && details.results.length === 1) {
1089
962
  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);
963
+ const status = getStatusText(r);
964
+ const finalOutput = getFinalOutputText(r.messages);
965
+ const usage = usageLine(r.usage);
966
+ const usg = usage ? theme.fg("dim", usage) : "";
1095
967
 
1096
968
  if (expanded) {
1097
969
  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}]`)}`;
970
+ const header = `${status} ${theme.fg("accent", r.agent)}${theme.fg("muted", r.model ? ` \u00B7 ${r.model}` : "")}`;
1101
971
  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);
972
+ if (r.stderr) container.addChild(new Text(theme.fg("error", r.stderr), 0, 0));
1160
973
  if (finalOutput) {
1161
974
  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
975
  container.addChild(new Markdown(finalOutput.trim(), 0, 0, mdTheme));
1166
976
  }
1167
-
1168
- const usageStr = formatUsageStats(r.usage, r.model);
1169
- if (usageStr) {
977
+ if (usg) {
1170
978
  container.addChild(new Spacer(1));
1171
- container.addChild(new Text(theme.fg("dim", usageStr), 0, 0));
979
+ container.addChild(new Text(usg, 0, 0));
1172
980
  }
1173
981
  return container;
1174
982
  }
1175
983
 
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)
984
+ let text = `${status} ${theme.fg("accent", r.agent)}`;
985
+ if (finalOutput) {
986
+ const preview = finalOutput.split("\n").slice(0, 5).join("\n");
987
+ const truncated = finalOutput.split("\n").length > 5;
988
+ text += `\n${theme.fg("toolOutput", preview)}`;
989
+ if (truncated) text += `\n${theme.fg("muted", "(Ctrl+O to expand)")}`;
990
+ } else {
1181
991
  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
992
  }
1187
- const usageStr = formatUsageStats(r.usage, r.model);
1188
- if (usageStr) text += `\n${theme.fg("dim", usageStr)}`;
993
+ if (usg) text += `\n${usg}`;
1189
994
  return new Text(text, 0, 0);
1190
995
  }
1191
996
 
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
997
  // --- Chain mode ---
1213
998
  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");
999
+ const lastResult = details.results[details.results.length - 1];
1000
+ const finalOutput = lastResult ? getFinalOutputText(lastResult.messages) : "";
1001
+ const allOk = details.results.every((r) => r.exitCode === 0);
1002
+ const icon = allOk ? theme.fg("success", "\u2713") : theme.fg("error", "\u2717");
1003
+ const steps = details.results.map((r) => r.agent).join(" \u2192 ");
1004
+ const total = sumUsage(details.results);
1005
+ const totalUsg = usageLine(total);
1221
1006
 
1222
1007
  if (expanded) {
1223
1008
  const container = new Container();
1224
1009
  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
- ),
1010
+ new Text(`${icon} ${theme.fg("accent", steps)}`, 0, 0),
1233
1011
  );
1234
-
1235
1012
  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) {
1013
+ const out = getFinalOutputText(r.messages);
1014
+ const stepUsage = usageLine(r.usage);
1015
+ const stepUsg = stepUsage ? theme.fg("dim", stepUsage) : "";
1016
+ const label = `${getStatusText(r)} ${theme.fg("accent", r.agent)}${r.model ? theme.fg("muted", ` \u00B7 ${r.model}`) : ""}${stepUsg ? " " + stepUsg : ""}`;
1017
+ if (out) {
1273
1018
  container.addChild(new Spacer(1));
1274
- container.addChild(
1275
- new Markdown(finalOutput.trim(), 0, 0, mdTheme),
1276
- );
1019
+ container.addChild(new Text(label, 0, 0));
1020
+ container.addChild(new Markdown(out.trim(), 0, 0, mdTheme));
1277
1021
  }
1278
-
1279
- const stepUsage = formatUsageStats(r.usage, r.model);
1280
- if (stepUsage)
1281
- container.addChild(new Text(theme.fg("dim", stepUsage), 0, 0));
1282
1022
  }
1283
-
1284
- const usageStr = formatUsageStats(aggregateUsage(details.results));
1285
- if (usageStr) {
1023
+ if (totalUsg && details.results.length > 1) {
1286
1024
  container.addChild(new Spacer(1));
1287
- container.addChild(
1288
- new Text(theme.fg("dim", `Total: ${usageStr}`), 0, 0),
1289
- );
1025
+ container.addChild(new Text(totalUsg, 0, 0));
1290
1026
  }
1291
1027
  return container;
1292
1028
  }
1293
1029
 
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)}`;
1030
+ let text = `${icon} ${theme.fg("accent", steps)}`;
1031
+ if (finalOutput) {
1032
+ const preview = finalOutput.split("\n").slice(0, 5).join("\n");
1033
+ const truncated = finalOutput.split("\n").length > 5;
1034
+ text += `\n${theme.fg("toolOutput", preview)}`;
1035
+ if (truncated) text += `\n${theme.fg("muted", "(Ctrl+O to expand)")}`;
1036
+ } else {
1037
+ text += `\n${theme.fg("muted", "(no output)")}`;
1309
1038
  }
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)")}`;
1039
+ if (totalUsg) text += `\n${totalUsg}`;
1313
1040
  return new Text(text, 0, 0);
1314
1041
  }
1315
1042
 
1316
1043
  // --- Parallel mode ---
1317
1044
  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
1045
+ const running = details.results.filter((r) => r.exitCode === -1).length;
1046
+ const done = details.results.filter((r) => r.exitCode !== -1).length;
1047
+ const allOk = details.results.every((r) => r.exitCode === 0);
1048
+ const icon = running > 0
1329
1049
  ? 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) {
1050
+ : allOk
1051
+ ? theme.fg("success", "\u2713")
1052
+ : theme.fg("error", "\u2717");
1053
+
1054
+ if (expanded && running === 0) {
1338
1055
  const container = new Container();
1056
+ const total = sumUsage(details.results);
1057
+ const totalUsg = usageLine(total);
1339
1058
  container.addChild(
1340
1059
  new Text(
1341
- `${icon} ${theme.fg("toolTitle", theme.bold("parallel "))}${theme.fg("accent", status)}`,
1060
+ `${icon} ${theme.fg("accent", `${done} tasks`)}`,
1342
1061
  0,
1343
1062
  0,
1344
1063
  ),
1345
1064
  );
1346
-
1347
1065
  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) {
1066
+ const out = getFinalOutputText(r.messages);
1067
+ const stepUsage = usageLine(r.usage);
1068
+ const stepUsg = stepUsage ? theme.fg("dim", stepUsage) : "";
1069
+ if (out) {
1384
1070
  container.addChild(new Spacer(1));
1385
1071
  container.addChild(
1386
- new Markdown(finalOutput.trim(), 0, 0, mdTheme),
1072
+ new Text(
1073
+ `${getStatusText(r)} ${theme.fg("accent", r.agent)}${stepUsg ? " " + stepUsg : ""}`,
1074
+ 0,
1075
+ 0,
1076
+ ),
1387
1077
  );
1078
+ container.addChild(new Markdown(out.trim(), 0, 0, mdTheme));
1388
1079
  }
1389
-
1390
- const taskUsage = formatUsageStats(r.usage, r.model);
1391
- if (taskUsage)
1392
- container.addChild(new Text(theme.fg("dim", taskUsage), 0, 0));
1393
1080
  }
1394
-
1395
- const usageStr = formatUsageStats(aggregateUsage(details.results));
1396
- if (usageStr) {
1081
+ if (totalUsg && details.results.length > 1) {
1397
1082
  container.addChild(new Spacer(1));
1398
- container.addChild(
1399
- new Text(theme.fg("dim", `Total: ${usageStr}`), 0, 0),
1400
- );
1083
+ container.addChild(new Text(totalUsg, 0, 0));
1401
1084
  }
1402
1085
  return container;
1403
1086
  }
1404
1087
 
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}`)}`;
1088
+ const usg = usageLine(sumUsage(details.results));
1089
+ let text = `${icon} ${theme.fg("accent", `${done}/${details.results.length} tasks`)}`;
1090
+ if (running === 0) {
1091
+ if (usg) text += `\n${usg}`;
1092
+ if (!expanded) text += theme.fg("muted", " (Ctrl+O to expand)");
1422
1093
  }
1423
- if (!expanded)
1424
- text += `\n${theme.fg("muted", "(Ctrl+O to expand)")}`;
1425
1094
  return new Text(text, 0, 0);
1426
1095
  }
1427
1096
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fosterg4/pi-subagent",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
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",
package/utils.ts ADDED
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Shared formatting helpers — testable without pi runtime deps
3
+ */
4
+
5
+ import type { UsageStats } from "./index.ts";
6
+
7
+ export function fmt(n: number): string {
8
+ if (n < 1000) return n.toString();
9
+ if (n < 10000) return (n / 1000).toFixed(1) + "k";
10
+ if (n < 1000000) return Math.round(n / 1000) + "k";
11
+ return (n / 1000000).toFixed(1) + "M";
12
+ }
13
+
14
+ export function usageLine(u: UsageStats): string {
15
+ const parts: string[] = [];
16
+ if (u.input) parts.push("\u2191" + fmt(u.input));
17
+ if (u.output) parts.push("\u2193" + fmt(u.output));
18
+ if (u.cacheRead) {
19
+ parts.push("R" + fmt(u.cacheRead));
20
+ const total = u.input + u.cacheRead;
21
+ if (total > 0) parts.push("CH" + ((u.cacheRead / total) * 100).toFixed(1) + "%");
22
+ }
23
+ if (u.cost) parts.push("$" + u.cost.toFixed(4));
24
+ return parts.join(" ");
25
+ }
26
+
27
+ export function sumUsage(results: ReadonlyArray<{ usage: UsageStats }>): UsageStats {
28
+ const u: UsageStats = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 };
29
+ for (const r of results) {
30
+ u.input += r.usage.input;
31
+ u.output += r.usage.output;
32
+ u.cacheRead += r.usage.cacheRead;
33
+ u.cacheWrite += r.usage.cacheWrite;
34
+ u.cost += r.usage.cost;
35
+ }
36
+ return u;
37
+ }