@pi-archimedes/subagent 2.2.0 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/compact.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { Text } from "@earendil-works/pi-tui";
2
2
  import type { SubagentDetails, SubagentProgress, SubagentResult, SubagentToolCall, SubagentToolResult } from "./types.js";
3
- import { formatTokens, formatDuration, formatCost, truncLine, buildStatsLine, buildAgentLabel } from "./format.js";
3
+ import { formatDuration, truncLine, buildStatsLine, buildAgentLabel } from "./format.js";
4
+ import { renderToolCallLine } from "@pi-archimedes/core/tool-render";
4
5
 
5
6
  type Theme = { fg: (token: string, text: string) => string; bold: (text: string) => string };
6
7
  type RenderContext = { state: Record<string, unknown>; invalidate: () => void };
@@ -35,52 +36,43 @@ export function buildActivityLine(
35
36
  return theme.fg("error", "✗ Failed");
36
37
  }
37
38
 
38
- // Running: show the current tool with live duration (grey while running)
39
+ // Running: show the current tool with the running glyph + live duration.
39
40
  if (data.currentTool) {
40
- const arrow = theme.fg("muted", "↳ ");
41
41
  const argsPreview = data.currentToolArgs
42
42
  ? truncLine(data.currentToolArgs, 60)
43
43
  : "";
44
44
  const durationPart = data.currentToolStartedAt
45
45
  ? " | " + formatDuration(Date.now() - data.currentToolStartedAt)
46
46
  : "";
47
- let line = theme.fg("muted", data.currentTool);
48
- if (argsPreview) {
49
- line += theme.fg("dim", ": " + argsPreview);
50
- }
51
- if (durationPart) {
52
- line += theme.fg("dim", durationPart);
53
- }
54
- return arrow + line;
47
+ const suffix = (argsPreview ? ": " + argsPreview : "") + durationPart;
48
+ return renderToolCallLine("running", data.currentTool, suffix, theme);
55
49
  }
56
50
 
57
- // Running, no active tool: show the most recently completed tool call
58
- // Color only the tool name green (success) or red (error)
51
+ // Running, no active tool: show the most recently completed tool call with
52
+ // its status glyph (✓ ok / ✗ error) and matching name colour.
59
53
  if (data.toolCalls && data.toolCalls.length > 0) {
60
54
  const lastCall = data.toolCalls[data.toolCalls.length - 1];
61
55
  if (lastCall) {
62
56
  if (typeof lastCall === "string") {
63
- return theme.fg("dim", "↳ " + truncLine(lastCall, 60));
57
+ return renderToolCallLine("success", truncLine(lastCall, 60), "", theme);
64
58
  }
65
- const color = lastCall.error ? "error" : "success";
66
- const arrow = theme.fg("muted", "↳ ");
67
- const name = theme.fg(color, lastCall.name);
68
- const argsPart = lastCall.argsPreview
69
- ? theme.fg("dim", ": " + truncLine(lastCall.argsPreview, 60))
59
+ const status = lastCall.error ? "error" : "success";
60
+ const suffix = lastCall.argsPreview
61
+ ? ": " + truncLine(lastCall.argsPreview, 60)
70
62
  : "";
71
- return arrow + name + argsPart;
63
+ return renderToolCallLine(status, lastCall.name, suffix, theme);
72
64
  }
73
65
  }
74
66
 
75
- // Running, no tool history: show first line of streamed output if any
67
+ // Running, no tool history: show first line of streamed output (▸, muted).
76
68
  if (data.finalOutput) {
77
69
  const firstLine = data.finalOutput.split("\n")[0] ?? "";
78
- return theme.fg("muted", "↳ " + truncLine(firstLine, 80));
70
+ return renderToolCallLine("running", truncLine(firstLine, 80), "", theme);
79
71
  }
80
72
 
81
73
  // Running, no info yet
82
74
  if (data.status === "running") {
83
- return theme.fg("muted", "Starting...");
75
+ return renderToolCallLine("running", "Starting...", "", theme);
84
76
  }
85
77
 
86
78
  return "";
@@ -88,13 +80,37 @@ export function buildActivityLine(
88
80
 
89
81
  // ── Status glyph ────────────────────────────────────────────────────────────
90
82
 
91
- function statusGlyph(isRunning: boolean, status: string): string {
92
- if (isRunning) return "↳";
93
- return status === "completed" ? "✓" : "✗";
94
- }
95
83
 
96
84
  // ── Compact single agent ────────────────────────────────────────────────────
97
85
 
86
+ type AgentBlockData = {
87
+ agentName: string;
88
+ task: string | undefined;
89
+ model: string | undefined;
90
+ statsData: Parameters<typeof buildStatsLine>[0];
91
+ activity: ActivityData;
92
+ };
93
+
94
+ // Shared 3-line agent block used by both the single and parallel compact
95
+ // views so they render identically:
96
+ // <agent>: <task>
97
+ // <model> · <stats>
98
+ // <activity> (status lives here — "✓ Done" / "✗ Failed" / ▸ tool)
99
+ function buildAgentBlock(data: AgentBlockData, theme: Theme): string {
100
+ const statsLine = buildStatsLine(data.statsData, theme);
101
+ const modelLabel = data.model ? theme.fg("accent", data.model) : "";
102
+ const activityLine = buildActivityLine(data.activity, theme);
103
+ const label = buildAgentLabel(data.agentName, data.task, theme);
104
+
105
+ return (
106
+ label +
107
+ "\n" +
108
+ [modelLabel, statsLine].filter(Boolean).join(" ") +
109
+ "\n" +
110
+ activityLine
111
+ );
112
+ }
113
+
98
114
  export function renderCompactSingle(
99
115
  text: Text,
100
116
  result: SubagentResult,
@@ -119,36 +135,30 @@ export function renderCompactSingle(
119
135
  ? Date.now() - (context.state[timeKey] as number)
120
136
  : summary.durationMs;
121
137
 
122
- const statsData = {
123
- turns: result.usage.turns ?? 0,
124
- toolCount: summary.toolCount,
125
- tokens: summary.tokens,
126
- durationMs: liveDuration,
127
- cost: result.usage.cost ?? 0,
128
- };
129
- const statsLine = buildStatsLine(statsData, theme);
130
-
131
- const statsPart = statsLine;
132
-
133
- // Activity: arrow + current tool if running, status if finished
134
- const activityLine = buildActivityLine({
135
- currentTool: progress?.currentTool,
136
- currentToolArgs: progress?.currentToolArgs,
137
- currentToolStartedAt: progress?.currentToolStartedAt,
138
- finalOutput: result.finalOutput,
139
- status: isRunning ? "running" : status,
140
- error: result.error,
141
- toolCalls: progress?.toolCalls,
142
- }, theme);
143
-
144
- const modelName = progress?.model ?? result.model;
145
- const modelLabel = modelName
146
- ? theme.fg("accent", modelName)
147
- : "";
148
- const expandHint = theme.fg("muted", "(ctrl+o)");
149
- const agentLabel = buildAgentLabel(agentName, result.task, theme);
150
- let output = agentLabel + "\n" + [modelLabel, statsPart, expandHint].filter(Boolean).join(" ");
151
- output += "\n" + activityLine;
138
+ const output = buildAgentBlock(
139
+ {
140
+ agentName,
141
+ task: result.task,
142
+ model: progress?.model ?? result.model,
143
+ statsData: {
144
+ turns: result.usage.turns ?? 0,
145
+ toolCount: summary.toolCount,
146
+ tokens: summary.tokens,
147
+ durationMs: liveDuration,
148
+ cost: result.usage.cost ?? 0,
149
+ },
150
+ activity: {
151
+ currentTool: progress?.currentTool,
152
+ currentToolArgs: progress?.currentToolArgs,
153
+ currentToolStartedAt: progress?.currentToolStartedAt,
154
+ finalOutput: result.finalOutput,
155
+ status: isRunning ? "running" : status,
156
+ error: result.error,
157
+ toolCalls: progress?.toolCalls,
158
+ },
159
+ },
160
+ theme,
161
+ );
152
162
 
153
163
  text.setText(output);
154
164
  return text;
@@ -175,39 +185,30 @@ export function renderCompactParallel(
175
185
  ? "running"
176
186
  : result.exitCode === 0 ? "completed" : "failed";
177
187
 
178
- const glyph = statusGlyph(isRunning, status);
179
- const glyphColored = status === "completed"
180
- ? theme.fg("success", glyph)
181
- : status === "failed"
182
- ? theme.fg("error", glyph)
183
- : theme.fg("muted", glyph);
184
-
185
- const statsData = {
186
- turns: result.usage.turns ?? 0,
187
- toolCount: summary.toolCount,
188
- tokens: summary.tokens,
189
- durationMs: summary.durationMs,
190
- cost: result.usage.cost ?? 0,
191
- };
192
- const statsLine = buildStatsLine(statsData, theme);
193
- const statsPart = statsLine ? " " + statsLine : "";
194
-
195
- const activityData = {
196
- currentTool: progress?.currentTool,
197
- currentToolArgs: progress?.currentToolArgs,
198
- currentToolStartedAt: progress?.currentToolStartedAt,
199
- finalOutput: result.finalOutput,
200
- status,
201
- error: result.error,
202
- toolCalls: progress?.toolCalls,
203
- };
204
- const activityLine = buildActivityLine(activityData, theme);
205
-
206
- let line = `${glyphColored} ${buildAgentLabel(agentName, result.task, theme)}${statsPart}`;
207
- if (activityLine) {
208
- line += "\n" + activityLine;
209
- }
210
- return line;
188
+ return buildAgentBlock(
189
+ {
190
+ agentName,
191
+ task: result.task,
192
+ model: progress?.model ?? result.model,
193
+ statsData: {
194
+ turns: result.usage.turns ?? 0,
195
+ toolCount: summary.toolCount,
196
+ tokens: summary.tokens,
197
+ durationMs: summary.durationMs,
198
+ cost: result.usage.cost ?? 0,
199
+ },
200
+ activity: {
201
+ currentTool: progress?.currentTool,
202
+ currentToolArgs: progress?.currentToolArgs,
203
+ currentToolStartedAt: progress?.currentToolStartedAt,
204
+ finalOutput: result.finalOutput,
205
+ status,
206
+ error: result.error,
207
+ toolCalls: progress?.toolCalls,
208
+ },
209
+ },
210
+ theme,
211
+ );
211
212
  });
212
213
 
213
214
  text.setText(lines.join("\n"));
@@ -227,13 +228,6 @@ export function renderCompactProgress(
227
228
  const status = progress.status;
228
229
  const isRunning = status === "running";
229
230
 
230
- const glyph = statusGlyph(isRunning, status);
231
- const glyphColored = status === "completed"
232
- ? theme.fg("success", glyph)
233
- : status === "failed"
234
- ? theme.fg("error", glyph)
235
- : theme.fg("muted", glyph);
236
-
237
231
  // Track start time for live duration
238
232
  const timeKey = "_subagentStartTime_" + agentName;
239
233
  if (isRunning && context.state[timeKey] === undefined) {
@@ -243,34 +237,30 @@ export function renderCompactProgress(
243
237
  ? Date.now() - (context.state[timeKey] as number)
244
238
  : progress.durationMs;
245
239
 
246
- const statsData = {
247
- turns: 0,
248
- toolCount: progress.toolCount,
249
- tokens: progress.tokens,
250
- durationMs: liveDuration,
251
- cost: progress.cost,
252
- };
253
- const statsLine = buildStatsLine(statsData, theme);
254
-
255
- // Activity: arrow + current tool if running, status if finished
256
- const activityLine = buildActivityLine({
257
- currentTool: progress.currentTool,
258
- currentToolArgs: progress.currentToolArgs,
259
- currentToolStartedAt: progress.currentToolStartedAt,
260
- finalOutput: undefined,
261
- status,
262
- error: progress.error,
263
- toolCalls: progress.toolCalls,
264
- }, theme);
265
-
266
- const modelLabel = progress.model
267
- ? theme.fg("accent", progress.model)
268
- : "";
269
- const statsPart = statsLine;
270
- const expandHint = theme.fg("muted", "(ctrl+o)");
271
- const agentLabel = buildAgentLabel(agentName, progress.task, theme);
272
- let output = agentLabel + "\n" + [modelLabel, statsPart, expandHint].filter(Boolean).join(" ");
273
- output += "\n" + activityLine;
240
+ const output = buildAgentBlock(
241
+ {
242
+ agentName,
243
+ task: progress.task,
244
+ model: progress.model,
245
+ statsData: {
246
+ turns: 0,
247
+ toolCount: progress.toolCount,
248
+ tokens: progress.tokens,
249
+ durationMs: liveDuration,
250
+ cost: progress.cost,
251
+ },
252
+ activity: {
253
+ currentTool: progress.currentTool,
254
+ currentToolArgs: progress.currentToolArgs,
255
+ currentToolStartedAt: progress.currentToolStartedAt,
256
+ finalOutput: undefined,
257
+ status,
258
+ error: progress.error,
259
+ toolCalls: progress.toolCalls,
260
+ },
261
+ },
262
+ theme,
263
+ );
274
264
 
275
265
  text.setText(output);
276
266
  return text;
@@ -284,44 +274,34 @@ export function renderCompactParallelProgress(
284
274
  theme: Theme,
285
275
  context: RenderContext,
286
276
  ): Text {
287
- const lines = (details.progress ?? []).map((progress, i) => {
277
+ const lines = (details.progress ?? []).map((progress) => {
288
278
  const agentName = progress.agent ?? "subagent";
289
279
  const status = progress.status;
290
- const isRunning = status === "running";
291
-
292
- const glyph = statusGlyph(isRunning, status);
293
- const glyphColored = status === "completed"
294
- ? theme.fg("success", glyph)
295
- : status === "failed"
296
- ? theme.fg("error", glyph)
297
- : theme.fg("muted", glyph);
298
-
299
- const statsData = {
300
- turns: 0,
301
- toolCount: progress.toolCount,
302
- tokens: progress.tokens,
303
- durationMs: progress.durationMs,
304
- cost: progress.cost,
305
- };
306
- const statsLine = buildStatsLine(statsData, theme);
307
- const statsPart = statsLine ? " " + statsLine : "";
308
-
309
- const activityData = {
310
- currentTool: progress.currentTool,
311
- currentToolArgs: progress.currentToolArgs,
312
- currentToolStartedAt: progress.currentToolStartedAt,
313
- finalOutput: undefined,
314
- status,
315
- error: progress.error,
316
- toolCalls: progress.toolCalls,
317
- };
318
- const activityLine = buildActivityLine(activityData, theme);
319
-
320
- let line = `${glyphColored} ${buildAgentLabel(agentName, progress.task, theme)}${statsPart}`;
321
- if (activityLine) {
322
- line += "\n" + activityLine;
323
- }
324
- return line;
280
+
281
+ return buildAgentBlock(
282
+ {
283
+ agentName,
284
+ task: progress.task,
285
+ model: progress.model,
286
+ statsData: {
287
+ turns: 0,
288
+ toolCount: progress.toolCount,
289
+ tokens: progress.tokens,
290
+ durationMs: progress.durationMs,
291
+ cost: progress.cost,
292
+ },
293
+ activity: {
294
+ currentTool: progress.currentTool,
295
+ currentToolArgs: progress.currentToolArgs,
296
+ currentToolStartedAt: progress.currentToolStartedAt,
297
+ finalOutput: undefined,
298
+ status,
299
+ error: progress.error,
300
+ toolCalls: progress.toolCalls,
301
+ },
302
+ },
303
+ theme,
304
+ );
325
305
  });
326
306
 
327
307
  text.setText(lines.join("\n"));
package/src/expanded.ts CHANGED
@@ -1,23 +1,22 @@
1
1
  import { Text } from "@earendil-works/pi-tui";
2
2
  import type { SubagentDetails, SubagentProgress, SubagentResult, SubagentToolCall } from "./types.js";
3
- import { formatTokens, formatDuration, truncLine, buildStatsLine, buildAgentLabel } from "./format.js";
3
+ import { formatDuration, truncLine, buildStatsLine, buildAgentLabel } from "./format.js";
4
+ import { renderToolCallLine } from "@pi-archimedes/core/tool-render";
4
5
 
5
6
  type Theme = { fg: (token: string, text: string) => string; bold: (text: string) => string };
6
7
 
7
8
  // ── Helpers ─────────────────────────────────────────────────────────────────
8
9
 
10
+ // A completed tool call renders with a status glyph (✓ ok / ✗ error) and its
11
+ // name coloured to match. Legacy string-form calls (old persisted sessions)
12
+ // carry no status, so they render as a neutral success line.
9
13
  function formatToolCall(call: SubagentToolCall | string, theme: Theme): string {
10
- // Backward compat: old persisted sessions may have toolCalls as string[]
11
14
  if (typeof call === "string") {
12
- return theme.fg("dim", "↳ " + call);
13
- }
14
- const arrow = theme.fg("muted", "↳ ");
15
- const color = call.error ? "error" : "success";
16
- const name = theme.fg(color, call.name);
17
- const argsPart = call.argsPreview
18
- ? theme.fg("dim", ": " + call.argsPreview)
19
- : "";
20
- return arrow + name + argsPart;
15
+ return renderToolCallLine("success", call, "", theme);
16
+ }
17
+ const status = call.error ? "error" : "success";
18
+ const suffix = call.argsPreview ? ": " + call.argsPreview : "";
19
+ return renderToolCallLine(status, call.name, suffix, theme);
21
20
  }
22
21
 
23
22
  // ── Expanded completed result ───────────────────────────────────────────────
@@ -42,8 +41,7 @@ export function buildExpandedText(
42
41
  durationMs: result.progressSummary?.durationMs,
43
42
  cost: result.usage.cost,
44
43
  }, theme);
45
- const expandHint = theme.fg("muted", "(ctrl+o)");
46
- const statsParts = [modelLabel, statsLine, expandHint].filter(Boolean);
44
+ const statsParts = [modelLabel, statsLine].filter(Boolean);
47
45
  if (statsParts.length > 0) {
48
46
  lines.push(statsParts.join(" "));
49
47
  }
@@ -116,8 +114,7 @@ export function renderProgressExpanded(
116
114
  durationMs: progress.durationMs,
117
115
  cost: progress.cost,
118
116
  }, theme);
119
- const expandHint = theme.fg("muted", "(ctrl+o)");
120
- const statsParts = [modelLabel, statsLine, expandHint].filter(Boolean);
117
+ const statsParts = [modelLabel, statsLine].filter(Boolean);
121
118
  if (statsParts.length > 0) {
122
119
  lines.push(statsParts.join(" "));
123
120
  }
@@ -136,7 +133,7 @@ export function renderProgressExpanded(
136
133
  }
137
134
  }
138
135
 
139
- // Activity (current tool with spinner)
136
+ // Activity (current tool, marked with the running glyph ▸)
140
137
  if (progress.currentTool) {
141
138
  lines.push("");
142
139
  const argsPreview = progress.currentToolArgs
@@ -145,14 +142,8 @@ export function renderProgressExpanded(
145
142
  const durationPart = progress.currentToolStartedAt
146
143
  ? " | " + formatDuration(Date.now() - progress.currentToolStartedAt)
147
144
  : "";
148
- let line = theme.fg("muted", progress.currentTool);
149
- if (argsPreview) {
150
- line += theme.fg("dim", ": " + argsPreview);
151
- }
152
- if (durationPart) {
153
- line += theme.fg("dim", durationPart);
154
- }
155
- lines.push(line);
145
+ const suffix = (argsPreview ? ": " + argsPreview : "") + durationPart;
146
+ lines.push(renderToolCallLine("running", progress.currentTool, suffix, theme));
156
147
  }
157
148
 
158
149
  // Status at bottom
@@ -192,8 +183,7 @@ export function buildProgressExpandedText(
192
183
  durationMs: progress.durationMs,
193
184
  cost: progress.cost,
194
185
  }, theme);
195
- const expandHint = theme.fg("muted", "(ctrl+o)");
196
- const statsParts = [modelLabel, statsLine, expandHint].filter(Boolean);
186
+ const statsParts = [modelLabel, statsLine].filter(Boolean);
197
187
  if (statsParts.length > 0) {
198
188
  lines.push(statsParts.join(" "));
199
189
  }
package/src/index.ts CHANGED
@@ -1,10 +1,12 @@
1
1
  import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
2
- import { Text, TUI } from "@earendil-works/pi-tui";
2
+ import { Text, type TUI } from "@earendil-works/pi-tui";
3
3
  import { Type } from "typebox";
4
+ import { SUBAGENT_PARAMS_SCHEMA } from "./tool-schema.js";
4
5
  import { executeSubagent, executeParallel } from "./execute.js";
5
6
  // agent-manager.js lazy-loaded below to keep subagent tool registration fast
6
7
  import { renderSubagentResult } from "./render.js";
7
8
  import { OVERLAY_CHROME } from "@pi-archimedes/core/overlay";
9
+ import { renderToolHeader } from "@pi-archimedes/core/tool-render";
8
10
  import { discoverAgents, discoverAgentsAll, findAgent, formatAgentList } from "./agents.js";
9
11
  import { validateModel, firstError } from "./model-validation.js";
10
12
  import type {
@@ -14,38 +16,6 @@ import type {
14
16
  SubagentToolResult,
15
17
  } from "./types.js";
16
18
 
17
- // ── JSON Schema for tool parameters (TypeBox) ──────────────────────────────
18
-
19
- const TaskItem = Type.Object({
20
- agent: Type.Optional(Type.String({
21
- description: "Agent name for this task (optional). If omitted, runs config-less.",
22
- })),
23
- task: Type.String(),
24
- model: Type.Optional(Type.String()),
25
- cwd: Type.Optional(Type.String()),
26
- });
27
-
28
- const SUBAGENT_PARAMS_SCHEMA = Type.Object({
29
- agent: Type.Optional(Type.String({
30
- description: "Agent name (optional). If omitted, the subagent runs config-less — parent's current model, all tools, no system-prompt override. Call list_agents to see available agents.",
31
- })),
32
- task: Type.Optional(Type.String({
33
- description: "Task description for the subagent. Required when not using 'tasks' array.",
34
- })),
35
- tasks: Type.Optional(Type.Array(TaskItem, {
36
- description: "Multiple tasks for parallel execution. Required when not using 'task'.",
37
- })),
38
- model: Type.Optional(Type.String({
39
- description: "Model override for the subagent",
40
- })),
41
- async: Type.Optional(Type.Boolean({
42
- description: "Run asynchronously (fire-and-forget)",
43
- })),
44
- cwd: Type.Optional(Type.String({
45
- description: "Working directory for the subagent",
46
- })),
47
- });
48
-
49
19
  // ── Theme helper type for render functions ──────────────────────────────────
50
20
 
51
21
  interface RenderTheme {
@@ -236,13 +206,13 @@ export function registerSubagent(pi: ExtensionAPI): void {
236
206
  (ctx as Record<string, unknown>).lastComponent = text;
237
207
 
238
208
  if (tasks && tasks.length > 0) {
239
- const label = theme.fg("toolTitle", theme.bold("subagent")) + " " + tasks.length + " tasks";
240
- text.setText(label);
209
+ text.setText(
210
+ renderToolHeader("subagent", `${tasks.length} tasks`, theme),
211
+ );
241
212
  } else if (agent) {
242
- const label = theme.fg("toolTitle", theme.bold("subagent")) + " " + theme.fg("accent", agent);
243
- text.setText(label);
213
+ text.setText(renderToolHeader("subagent", agent, theme));
244
214
  } else {
245
- text.setText(theme.fg("toolTitle", theme.bold("subagent")));
215
+ text.setText(renderToolHeader("subagent", undefined, theme));
246
216
  }
247
217
 
248
218
  return text;