@clanker-code/pi-subagents 0.10.8 → 0.11.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.
@@ -30,7 +30,6 @@ export const DEFAULT_AGENTS = new Map([
30
30
  builtinToolNames: READ_ONLY_TOOLS,
31
31
  extensions: true,
32
32
  skills: true,
33
- model: "anthropic/claude-haiku-4-5-20251001",
34
33
  systemPrompt: `# CRITICAL: READ-ONLY MODE - NO FILE MODIFICATIONS
35
34
  You are a file search specialist. You excel at thoroughly navigating and exploring codebases.
36
35
  Your role is EXCLUSIVELY to search and analyze existing code. You do NOT have access to file editing tools.
package/dist/index.js CHANGED
@@ -23,8 +23,10 @@ import { getAgentConversation, getCurrentExtensionAgentId, getCurrentExtensionDe
23
23
  import { buildAgentToolDescription, getModelLabelFromConfig } from "./agent-tool-description.js";
24
24
  import { BUILTIN_TOOL_NAMES, getAgentConfig, getAllTypes, getAvailableTypes, isDefaultsDisabled, registerAgents, resolveType, setDefaultsDisabled } from "./agent-types.js";
25
25
  import { formatOutputFileHint, limitText, MAX_RESULT_CHARS, MAX_VERBOSE_CHARS } from "./bounded-output.js";
26
+ import { extractText } from "./context.js";
26
27
  import { registerRpcHandlers } from "./cross-extension-rpc.js";
27
28
  import { loadCustomAgents } from "./custom-agents.js";
29
+ import { registerDashboardModules } from "./dashboard-ui.js";
28
30
  import { isModelInScope, readEnabledModels, resolveEnabledModels } from "./enabled-models.js";
29
31
  import { GroupJoinManager } from "./group-join.js";
30
32
  import { resolveAgentInvocationConfig, resolveJoinMode } from "./invocation-config.js";
@@ -36,8 +38,9 @@ import { SubagentScheduler } from "./schedule.js";
36
38
  import { resolveStorePath, ScheduleStore } from "./schedule-store.js";
37
39
  import { applyAndEmitLoaded, DEFAULT_WAIT_TIMEOUT_SECONDS, saveAndEmitChanged } from "./settings.js";
38
40
  import { getStatusNote } from "./status-note.js";
41
+ import { registerSubagentListClearTools } from "./subagent-list-clear.js";
39
42
  import { MAX_RECURSIVE_DEPTH } from "./types.js";
40
- import { renderAgentCall, renderAgentResult, renderSteerCall, tailPreview } from "./ui/agent-tool-rendering.js";
43
+ import { renderAgentCall, renderAgentResult, renderSteerCall, snipMiddleLines, tailPreview } from "./ui/agent-tool-rendering.js";
41
44
  import { AgentWidget, buildInvocationTags, describeActivity, formatContextWindow, formatDuration, getDisplayName, getPromptModeLabel, } from "./ui/agent-widget.js";
42
45
  import { menuSelect } from "./ui/menu-select.js";
43
46
  import { showSchedulesMenu } from "./ui/schedule-menu.js";
@@ -388,6 +391,8 @@ export default function (pi) {
388
391
  // Capture ctx from session_start for RPC spawn handler + start the scheduler.
389
392
  pi.on("session_start", async (_event, ctx) => {
390
393
  currentCtx = ctx;
394
+ clearBatchState();
395
+ groupJoin.dispose();
391
396
  manager.clearCompleted();
392
397
  widget.clearSnapshots();
393
398
  retryStash.clear();
@@ -395,8 +400,10 @@ export default function (pi) {
395
400
  startScheduler(ctx);
396
401
  });
397
402
  pi.on("session_before_switch", () => {
403
+ clearBatchState();
404
+ groupJoin.dispose();
398
405
  manager.clearCompleted();
399
- widget.clearSnapshots();
406
+ widget.dispose();
400
407
  retryStash.clear();
401
408
  scheduler.stop();
402
409
  });
@@ -414,13 +421,17 @@ export default function (pi) {
414
421
  unsubSpawnRpc();
415
422
  unsubStopRpc();
416
423
  unsubPingRpc();
424
+ unsubWidgetCreated?.();
417
425
  unsubWidgetStarted?.();
418
426
  unsubWidgetCompleted?.();
419
427
  unsubWidgetFailed?.();
420
428
  currentCtx = undefined;
421
429
  delete globalThis[MANAGER_KEY];
422
430
  scheduler.stop();
431
+ clearBatchState();
432
+ groupJoin.dispose();
423
433
  manager.abortAll();
434
+ widget.dispose();
424
435
  for (const timer of pendingNudges.values())
425
436
  clearTimeout(timer);
426
437
  pendingNudges.clear();
@@ -434,6 +445,7 @@ export default function (pi) {
434
445
  if (snapshot)
435
446
  widget.upsertSnapshot(snapshot);
436
447
  };
448
+ const unsubWidgetCreated = pi.events.on("subagents:created", upsertWidgetEventSnapshot);
437
449
  const unsubWidgetStarted = pi.events.on("subagents:started", upsertWidgetEventSnapshot);
438
450
  const unsubWidgetCompleted = pi.events.on("subagents:completed", upsertWidgetEventSnapshot);
439
451
  const unsubWidgetFailed = pi.events.on("subagents:failed", upsertWidgetEventSnapshot);
@@ -501,6 +513,13 @@ export default function (pi) {
501
513
  let currentBatchAgents = [];
502
514
  let batchFinalizeTimer;
503
515
  let batchCounter = 0;
516
+ function clearBatchState() {
517
+ if (batchFinalizeTimer) {
518
+ clearTimeout(batchFinalizeTimer);
519
+ batchFinalizeTimer = undefined;
520
+ }
521
+ currentBatchAgents = [];
522
+ }
504
523
  /** Finalize the current batch: if 2+ smart-mode agents, register as a group. */
505
524
  function finalizeBatch() {
506
525
  batchFinalizeTimer = undefined;
@@ -597,7 +616,7 @@ export default function (pi) {
597
616
  description: "A short (3-5 word) description of the task (shown in UI).",
598
617
  }),
599
618
  subagent_type: Type.Optional(Type.String({
600
- description: `The type of specialized agent to use. Available types: ${getAvailableTypes().join(", ")}. Custom agents from .pi/agents/*.md (project) or ${getAgentDir()}/agents/*.md (global) are also available. OMIT when retrying (preserved by the handle) unless you want to override it.`,
619
+ description: `The type of specialized agent to use. Defaults to general-purpose when omitted. Available types: ${getAvailableTypes().join(", ")}. Custom agents from .pi/agents/*.md (project) or ${getAgentDir()}/agents/*.md (global) are also available. OMIT when retrying (preserved by the handle) unless you want to override it.`,
601
620
  })),
602
621
  model: Type.Optional(Type.String({
603
622
  description: 'Optional model override. Accepts "provider/modelId" or fuzzy name (e.g. "haiku", "sonnet"). Omit to use the agent type\'s default.',
@@ -673,14 +692,14 @@ export default function (pi) {
673
692
  const { retry: _omit, ...overrides } = params;
674
693
  P = { ...stashed.params, ...overrides };
675
694
  }
676
- emitPromptPreview(P.prompt, P.description, P.subagent_type);
677
- // Retry supplied the prompt/type from the stash; otherwise both are required.
678
- if (!retryHandle && (!P.prompt || !P.subagent_type)) {
679
- return textResult(`Missing required argument${!P.prompt && !P.subagent_type ? "s" : ""}: ` +
680
- [!P.prompt && "prompt", !P.subagent_type && "subagent_type"].filter(Boolean).join(", ") +
681
- ".");
695
+ const requestedSubagentType = (P.subagent_type ?? "general-purpose");
696
+ emitPromptPreview(P.prompt, P.description, requestedSubagentType);
697
+ // Retry supplied the prompt from the stash; otherwise prompt is required.
698
+ // subagent_type defaults to general-purpose when omitted.
699
+ if (!retryHandle && !P.prompt) {
700
+ return textResult("Missing required argument: prompt.");
682
701
  }
683
- const rawType = P.subagent_type;
702
+ const rawType = requestedSubagentType;
684
703
  const resolved = resolveType(rawType);
685
704
  if (!resolved) {
686
705
  // Unknown agent type — recoverable. List valid types so the orchestrator
@@ -896,6 +915,10 @@ export default function (pi) {
896
915
  isBackground: true,
897
916
  depth: record?.depth ?? nextSubagentDepth,
898
917
  parentAgentId: extensionAgentId,
918
+ status: record?.status ?? "running",
919
+ startedAt: record?.startedAt,
920
+ toolUses: record?.toolUses ?? 0,
921
+ invocation: record?.invocation,
899
922
  });
900
923
  const isQueued = record?.status === "queued";
901
924
  return textResult(`Agent ${isQueued ? "queued" : "started"} in background.\nAgent ID: ${id}\nType: ${displayName}\nDescription: ${P.description}\n` +
@@ -928,6 +951,56 @@ export default function (pi) {
928
951
  description: "Return a lightweight tail/filter view of the agent's result or live output file, with line numbers. Ignored when verbose is true.",
929
952
  })),
930
953
  }),
954
+ renderResult(result, { expanded }, theme) {
955
+ const details = result.details;
956
+ const text = extractText(result.content);
957
+ // Header: status + stats + description
958
+ let line = "";
959
+ if (details) {
960
+ const icon = details.status === "error" || details.status === "stopped" || details.status === "aborted"
961
+ ? theme.fg("error", "✗")
962
+ : details.status === "running" || details.status === "queued"
963
+ ? theme.fg("accent", "◌")
964
+ : theme.fg("success", "✓");
965
+ const parts = [];
966
+ if (details.toolUses > 0)
967
+ parts.push(`${details.toolUses} tool use${details.toolUses === 1 ? "" : "s"}`);
968
+ if (details.tokens)
969
+ parts.push(details.tokens);
970
+ if (details.contextPercent !== null)
971
+ parts.push(`ctx ${Math.round(details.contextPercent)}%`);
972
+ if (details.duration)
973
+ parts.push(details.duration);
974
+ const stats = parts.map(p => theme.fg("dim", p)).join(" " + theme.fg("dim", "·") + " ");
975
+ line = `${icon} ${theme.bold(details.description)} ${theme.fg("dim", details.status)}`;
976
+ if (stats)
977
+ line += "\n " + stats;
978
+ }
979
+ // Body: snip when collapsed, full when expanded
980
+ // Extract the body portion (after the first blank line) to keep the
981
+ // tool-output header always visible and only snip the actual result.
982
+ if (text.trim()) {
983
+ const firstBlank = text.indexOf("\n\n");
984
+ const body = firstBlank >= 0 ? text.slice(firstBlank + 2) : text;
985
+ if (expanded) {
986
+ for (const l of text.split("\n")) {
987
+ line += "\n" + theme.fg("dim", ` ${l}`);
988
+ }
989
+ }
990
+ else {
991
+ // Show the tool-output header verbatim, then snip only the body
992
+ if (firstBlank >= 0) {
993
+ for (const l of text.slice(0, firstBlank).split("\n")) {
994
+ line += "\n" + theme.fg("dim", ` ${l}`);
995
+ }
996
+ }
997
+ for (const l of snipMiddleLines(body, 20)) {
998
+ line += "\n" + theme.fg("dim", ` ${l}`);
999
+ }
1000
+ }
1001
+ }
1002
+ return new Text(line, 0, 0);
1003
+ },
931
1004
  execute: async (_toolCallId, params, signal, _onUpdate, ctx) => {
932
1005
  const record = manager.getRecord(params.agent_id);
933
1006
  if (!record) {
@@ -974,6 +1047,15 @@ export default function (pi) {
974
1047
  if (record.compactionCount)
975
1048
  statsParts.push(`Compactions: ${record.compactionCount}`);
976
1049
  statsParts.push(`Duration: ${duration}`);
1050
+ const details = {
1051
+ status: record.status,
1052
+ description: record.description,
1053
+ toolUses: record.toolUses,
1054
+ tokens: tokens || null,
1055
+ contextPercent,
1056
+ duration,
1057
+ outputFile: record.outputFile,
1058
+ };
977
1059
  let output = `Agent: ${record.id}\n` +
978
1060
  `Type: ${displayName} | Status: ${record.status}${getStatusNote(record.status)} | ${statsParts.join(" | ")}\n` +
979
1061
  `Description: ${record.description}\n` +
@@ -1014,7 +1096,7 @@ export default function (pi) {
1014
1096
  }
1015
1097
  }
1016
1098
  }
1017
- return textResult(output);
1099
+ return textResult(output, details);
1018
1100
  },
1019
1101
  }));
1020
1102
  // ---- steer_subagent tool ----
@@ -1072,6 +1154,9 @@ export default function (pi) {
1072
1154
  }
1073
1155
  },
1074
1156
  }));
1157
+ // ---- list_subagents / clear_subagents tools ----
1158
+ registerSubagentListClearTools(pi, manager);
1159
+ registerDashboardModules(pi, manager);
1075
1160
  // ---- list_models tool ----
1076
1161
  pi.registerTool(defineTool({
1077
1162
  name: SUBAGENT_TOOL_NAMES.LIST_MODELS,
package/dist/peek.js CHANGED
@@ -81,6 +81,12 @@ function parseOutputFileLines(path) {
81
81
  return [];
82
82
  }
83
83
  const out = [];
84
+ const pushRenderedLines = (text) => {
85
+ for (const renderedLine of text.trimEnd().split("\n")) {
86
+ if (renderedLine.trim())
87
+ out.push(renderedLine);
88
+ }
89
+ };
84
90
  for (const line of raw.split("\n")) {
85
91
  const trimmed = line.trim();
86
92
  if (!trimmed)
@@ -96,12 +102,12 @@ function parseOutputFileLines(path) {
96
102
  if (!Array.isArray(content)) {
97
103
  // Some entries may carry a plain string content.
98
104
  if (typeof content === "string" && content.trim())
99
- out.push(content.trim());
105
+ pushRenderedLines(content);
100
106
  continue;
101
107
  }
102
108
  for (const block of content) {
103
109
  if (block?.type === "text" && typeof block.text === "string" && block.text.trim()) {
104
- out.push(block.text.trimEnd());
110
+ pushRenderedLines(block.text);
105
111
  }
106
112
  }
107
113
  }
@@ -0,0 +1,57 @@
1
+ import { type ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import type { Component } from "@earendil-works/pi-tui";
3
+ import type { AgentManager } from "./agent-manager.js";
4
+ import type { AgentRecord } from "./types.js";
5
+ export interface ListSubagentsOptions {
6
+ all?: boolean;
7
+ now?: number;
8
+ recentSuccessLimit?: number;
9
+ }
10
+ export interface ListSubagentsAgentDetails {
11
+ id: string;
12
+ type: AgentRecord["type"];
13
+ description: string;
14
+ status: AgentRecord["status"];
15
+ startedAt: number;
16
+ completedAt?: number;
17
+ }
18
+ export interface ListSubagentsDetails {
19
+ total: number;
20
+ all: boolean;
21
+ visible: ListSubagentsAgentDetails[];
22
+ hiddenDoneCount: number;
23
+ activeCount: number;
24
+ problemCount: number;
25
+ recentDoneCount: number;
26
+ now: number;
27
+ }
28
+ export interface ClearSubagentsOptions {
29
+ agentIds?: string[];
30
+ now?: number;
31
+ olderThanMs?: number;
32
+ includeErrors?: boolean;
33
+ }
34
+ export interface ClearSelectionResult {
35
+ clearIds: string[];
36
+ errors: string[];
37
+ requestedCount: number;
38
+ keptActiveCount: number;
39
+ keptFailedCount: number;
40
+ keptYoungSuccessCount: number;
41
+ }
42
+ export interface ClearSubagentsDetails extends ClearSelectionResult {
43
+ clearedCount: number;
44
+ }
45
+ export type RenderTheme = {
46
+ fg(color: string, text: string): string;
47
+ bold(text: string): string;
48
+ };
49
+ export declare function buildListSubagentsDetails(records: AgentRecord[], options?: ListSubagentsOptions): ListSubagentsDetails;
50
+ export declare function clearSubagentRecords(records: AgentRecord[], options?: ClearSubagentsOptions): ClearSelectionResult;
51
+ export declare function buildClearSubagentsDetails(result: ClearSelectionResult): ClearSubagentsDetails;
52
+ export declare function renderListSubagentsDetails(details: ListSubagentsDetails, theme: RenderTheme): Component;
53
+ export declare function renderClearSubagentsDetails(details: ClearSubagentsDetails, theme: RenderTheme): Component;
54
+ export declare function renderEmptyCall(): Component;
55
+ export declare function formatListSubagentsText(details: ListSubagentsDetails): string;
56
+ export declare function formatClearSubagentsText(details: ClearSubagentsDetails): string;
57
+ export declare function registerSubagentListClearTools(pi: ExtensionAPI, manager: AgentManager): void;
@@ -0,0 +1,331 @@
1
+ import { defineTool } from "@earendil-works/pi-coding-agent";
2
+ import { truncateToWidth } from "@earendil-works/pi-tui";
3
+ import { Type } from "@sinclair/typebox";
4
+ import { SUBAGENT_TOOL_NAMES } from "./agent-runner.js";
5
+ const DEFAULT_RECENT_SUCCESS_LIMIT = 2;
6
+ const DEFAULT_CLEAR_AGE_MS = 5 * 60_000;
7
+ const INDENT = " ";
8
+ const HEADER_GLYPH = "⏣";
9
+ const TREE_MID = "├─";
10
+ const TREE_END = "└─";
11
+ const TREE_GAP = " ";
12
+ const TREE_PREFIX_LEN = INDENT.length + TREE_MID.length + TREE_GAP.length;
13
+ const SUCCESS_STATUSES = new Set(["completed", "steered"]);
14
+ const ACTIVE_STATUSES = new Set(["running", "queued"]);
15
+ const PROBLEM_STATUSES = new Set(["error", "aborted", "stopped"]);
16
+ class LineListComponent {
17
+ getLines;
18
+ constructor(getLines) {
19
+ this.getLines = getLines;
20
+ }
21
+ render(width) { return this.getLines(width); }
22
+ invalidate() { }
23
+ }
24
+ function isActive(record) {
25
+ return ACTIVE_STATUSES.has(record.status);
26
+ }
27
+ function isSuccess(record) {
28
+ return SUCCESS_STATUSES.has(record.status);
29
+ }
30
+ function isProblem(record) {
31
+ return PROBLEM_STATUSES.has(record.status);
32
+ }
33
+ function recency(record) {
34
+ return record.completedAt ?? record.startedAt;
35
+ }
36
+ function newestFirst(a, b) {
37
+ return recency(b) - recency(a);
38
+ }
39
+ function plural(n, one, many = `${one}s`) {
40
+ return `${n} ${n === 1 ? one : many}`;
41
+ }
42
+ function toListSubagentsAgentDetails(record) {
43
+ const details = {
44
+ id: record.id,
45
+ type: record.type,
46
+ description: record.description,
47
+ status: record.status,
48
+ startedAt: record.startedAt,
49
+ };
50
+ if (record.completedAt !== undefined)
51
+ details.completedAt = record.completedAt;
52
+ return details;
53
+ }
54
+ function formatAge(record, now) {
55
+ const start = record.completedAt ?? record.startedAt;
56
+ const elapsed = Math.max(0, now - start);
57
+ if (elapsed < 1_000)
58
+ return "0s";
59
+ const seconds = Math.floor(elapsed / 1_000);
60
+ if (seconds < 60)
61
+ return `${seconds}s`;
62
+ const minutes = Math.floor(seconds / 60);
63
+ if (minutes < 60)
64
+ return `${minutes}m`;
65
+ const hours = Math.floor(minutes / 60);
66
+ if (hours < 48)
67
+ return `${hours}h`;
68
+ return `${Math.floor(hours / 24)}d`;
69
+ }
70
+ function pad(text, width) {
71
+ return text.length >= width ? text : text + " ".repeat(width - text.length);
72
+ }
73
+ function shortId(id) {
74
+ return id.slice(0, 8);
75
+ }
76
+ function displayType(type) {
77
+ return type === "general-purpose" ? "general" : type;
78
+ }
79
+ function statusLabel(status) {
80
+ if (status === "completed")
81
+ return "done";
82
+ if (status === "steered")
83
+ return "done";
84
+ return status;
85
+ }
86
+ function statusIcon(status) {
87
+ switch (status) {
88
+ case "running": return "⠋";
89
+ case "queued": return "◼";
90
+ case "completed":
91
+ case "steered": return "✓";
92
+ case "error":
93
+ case "aborted": return "✗";
94
+ case "stopped": return "■";
95
+ default: return "•";
96
+ }
97
+ }
98
+ function statusColor(status) {
99
+ switch (status) {
100
+ case "running":
101
+ case "queued": return "accent";
102
+ case "completed":
103
+ case "steered": return "success";
104
+ case "stopped": return "dim";
105
+ default: return "error";
106
+ }
107
+ }
108
+ function labeledPrefix(label, theme) {
109
+ return `${INDENT}${theme.fg("accent", HEADER_GLYPH)} ${theme.fg("accent", label)}${theme.fg("dim", " · ")}`;
110
+ }
111
+ function normalizeIds(agentIds) {
112
+ return [...new Set((agentIds ?? []).map((id) => id.trim()).filter(Boolean))];
113
+ }
114
+ function resolveId(records, query) {
115
+ const exact = records.find((record) => record.id === query);
116
+ if (exact)
117
+ return { id: exact.id };
118
+ const matches = records.filter((record) => record.id.startsWith(query));
119
+ if (matches.length === 0)
120
+ return { error: `${query} not found` };
121
+ if (matches.length > 1)
122
+ return { error: `${query} matched multiple agents: ${matches.map((r) => shortId(r.id)).join(", ")}` };
123
+ return { id: matches[0].id };
124
+ }
125
+ export function buildListSubagentsDetails(records, options = {}) {
126
+ const now = options.now ?? Date.now();
127
+ const all = options.all === true;
128
+ const sorted = [...records].sort(newestFirst);
129
+ const active = sorted.filter(isActive);
130
+ const problems = sorted.filter(isProblem);
131
+ const successes = sorted.filter(isSuccess);
132
+ const recentSuccessLimit = options.recentSuccessLimit ?? DEFAULT_RECENT_SUCCESS_LIMIT;
133
+ const recentSuccesses = successes.slice(0, recentSuccessLimit);
134
+ const visible = all ? sorted : [...active, ...problems, ...recentSuccesses];
135
+ return {
136
+ total: records.length,
137
+ all,
138
+ visible: visible.map(toListSubagentsAgentDetails),
139
+ hiddenDoneCount: all ? 0 : Math.max(0, successes.length - recentSuccesses.length),
140
+ activeCount: active.length,
141
+ problemCount: problems.length,
142
+ recentDoneCount: all ? successes.length : recentSuccesses.length,
143
+ now,
144
+ };
145
+ }
146
+ export function clearSubagentRecords(records, options = {}) {
147
+ const now = options.now ?? Date.now();
148
+ const olderThanMs = options.olderThanMs ?? DEFAULT_CLEAR_AGE_MS;
149
+ const requestedIds = normalizeIds(options.agentIds);
150
+ const errors = [];
151
+ const clearIds = [];
152
+ if (requestedIds.length > 0) {
153
+ for (const query of requestedIds) {
154
+ const resolved = resolveId(records, query);
155
+ if (resolved.error || !resolved.id) {
156
+ errors.push(resolved.error ?? `${query} not found`);
157
+ continue;
158
+ }
159
+ const record = records.find((r) => r.id === resolved.id);
160
+ if (isActive(record)) {
161
+ errors.push(`${query} matched ${record.status} agent ${record.id}`);
162
+ continue;
163
+ }
164
+ clearIds.push(record.id);
165
+ }
166
+ }
167
+ else {
168
+ for (const record of records) {
169
+ const age = now - (record.completedAt ?? record.startedAt);
170
+ if (age < olderThanMs)
171
+ continue;
172
+ if (isSuccess(record) || (options.includeErrors && isProblem(record))) {
173
+ clearIds.push(record.id);
174
+ }
175
+ }
176
+ }
177
+ const clearIdSet = new Set(clearIds);
178
+ const remaining = records.filter((record) => !clearIdSet.has(record.id));
179
+ const keptActiveCount = remaining.filter(isActive).length;
180
+ const keptFailedCount = remaining.filter(isProblem).length;
181
+ const keptYoungSuccessCount = remaining.filter((record) => isSuccess(record) && now - (record.completedAt ?? record.startedAt) < olderThanMs).length;
182
+ return { clearIds, errors, requestedCount: requestedIds.length, keptActiveCount, keptFailedCount, keptYoungSuccessCount };
183
+ }
184
+ export function buildClearSubagentsDetails(result) {
185
+ return { ...result, clearedCount: result.clearIds.length };
186
+ }
187
+ function renderAgentLine(record, theme, now) {
188
+ const icon = theme.fg(statusColor(record.status), statusIcon(record.status));
189
+ const id = theme.fg("muted", shortId(record.id));
190
+ const type = theme.fg("text", pad(displayType(record.type), 8));
191
+ const status = theme.fg(statusColor(record.status), pad(statusLabel(record.status), 8));
192
+ const age = theme.fg("dim", pad(formatAge(record, now), 4));
193
+ return `${icon} ${id} ${type} ${status} ${age} ${theme.fg("muted", record.description)}`;
194
+ }
195
+ function listSummary(details, theme) {
196
+ if (details.total === 0)
197
+ return `${theme.fg("text", "0 visible")} ${theme.fg("dim", "(empty)")}`;
198
+ if (details.all)
199
+ return `${theme.fg("text", plural(details.visible.length, "agent"))} ${theme.fg("dim", "(full list)")}`;
200
+ const parts = [
201
+ plural(details.activeCount, "active"),
202
+ plural(details.problemCount, "problem"),
203
+ `${plural(details.recentDoneCount, "recent done", "recent done")}`,
204
+ ];
205
+ const hidden = details.hiddenDoneCount > 0 ? `; ${plural(details.hiddenDoneCount, "hidden done", "hidden done")}` : "";
206
+ return `${theme.fg("text", `${details.visible.length} visible`)} ${theme.fg("dim", `(${parts.join(", ")}${hidden})`)}`;
207
+ }
208
+ export function renderListSubagentsDetails(details, theme) {
209
+ return new LineListComponent((width) => {
210
+ const header = `${labeledPrefix("List Agents", theme)}${listSummary(details, theme)}`;
211
+ if (details.visible.length === 0)
212
+ return [truncateToWidth(header, width)];
213
+ const lines = [truncateToWidth(header, width)];
214
+ const avail = Math.max(1, width - TREE_PREFIX_LEN);
215
+ details.visible.forEach((record, i) => {
216
+ const connector = i === details.visible.length - 1 ? TREE_END : TREE_MID;
217
+ const line = truncateToWidth(renderAgentLine(record, theme, details.now), avail);
218
+ lines.push(`${INDENT}${connector}${TREE_GAP}${line}`);
219
+ });
220
+ return lines;
221
+ });
222
+ }
223
+ function clearSummary(details, theme) {
224
+ const primary = theme.fg("text", `cleared ${plural(details.clearedCount, "record")}`);
225
+ const extra = [];
226
+ if (details.keptYoungSuccessCount)
227
+ extra.push(`${plural(details.keptYoungSuccessCount, "new done", "new done")} kept`);
228
+ if (details.keptFailedCount)
229
+ extra.push(`${plural(details.keptFailedCount, "failed", "failed")} kept`);
230
+ if (details.keptActiveCount)
231
+ extra.push(`${plural(details.keptActiveCount, "active", "active")} kept`);
232
+ if (details.errors.length)
233
+ extra.push(theme.fg("error", `${plural(details.errors.length, "error")}`));
234
+ if (extra.length === 0)
235
+ return primary;
236
+ return `${primary}${theme.fg("dim", " (")}${extra.join(theme.fg("dim", ", "))}${theme.fg("dim", ")")}`;
237
+ }
238
+ export function renderClearSubagentsDetails(details, theme) {
239
+ return new LineListComponent((width) => [truncateToWidth(`${labeledPrefix("Clear Agents", theme)}${clearSummary(details, theme)}`, width)]);
240
+ }
241
+ export function renderEmptyCall() {
242
+ return new LineListComponent(() => []);
243
+ }
244
+ export function formatListSubagentsText(details) {
245
+ const lines = [`${details.visible.length} visible of ${details.total} retained subagents.`];
246
+ for (const record of details.visible) {
247
+ lines.push(`${record.id} | ${displayType(record.type)} | ${record.status} | ${record.description}`);
248
+ }
249
+ if (details.hiddenDoneCount > 0) {
250
+ lines.push(`${details.hiddenDoneCount} successful completed subagent(s) hidden. Pass all: true for the full retained list.`);
251
+ }
252
+ return lines.join("\n");
253
+ }
254
+ export function formatClearSubagentsText(details) {
255
+ const lines = [`Cleared ${details.clearedCount} subagent record(s).`];
256
+ if (details.clearIds.length)
257
+ lines.push(`Cleared IDs: ${details.clearIds.join(", ")}`);
258
+ if (details.keptYoungSuccessCount)
259
+ lines.push(`Kept ${details.keptYoungSuccessCount} successful subagent(s) newer than the age threshold.`);
260
+ if (details.keptFailedCount)
261
+ lines.push(`Kept ${details.keptFailedCount} failed/stopped/aborted subagent(s).`);
262
+ if (details.keptActiveCount)
263
+ lines.push(`Kept ${details.keptActiveCount} active subagent(s).`);
264
+ if (details.errors.length)
265
+ lines.push(`Errors: ${details.errors.join("; ")}`);
266
+ return lines.join("\n");
267
+ }
268
+ function textResult(msg, details) {
269
+ return { content: [{ type: "text", text: msg }], details: details };
270
+ }
271
+ export function registerSubagentListClearTools(pi, manager) {
272
+ pi.registerTool(defineTool({
273
+ name: SUBAGENT_TOOL_NAMES.LIST_SUBAGENTS,
274
+ label: "List Agents",
275
+ description: "List retained subagent records. By default shows queued/running agents, failed/stopped/aborted agents, " +
276
+ "and the most recent 2 successful agents that have not been cleaned up. Also reports how many successful " +
277
+ "completed agents are hidden. Pass all: true to show the full retained list.",
278
+ promptSnippet: "List retained subagents and their current status",
279
+ parameters: Type.Object({
280
+ all: Type.Optional(Type.Boolean({
281
+ description: "If true, show every retained subagent record instead of the default compact view.",
282
+ })),
283
+ }),
284
+ renderShell: "self",
285
+ renderCall: () => renderEmptyCall(),
286
+ renderResult: (result, _options, theme) => {
287
+ const details = result?.details;
288
+ return details ? renderListSubagentsDetails(details, theme) : renderEmptyCall();
289
+ },
290
+ execute: async (_toolCallId, params) => {
291
+ const details = buildListSubagentsDetails(manager.listAgents(), { all: params.all === true });
292
+ return textResult(formatListSubagentsText(details), details);
293
+ },
294
+ }));
295
+ pi.registerTool(defineTool({
296
+ name: SUBAGENT_TOOL_NAMES.CLEAR_SUBAGENTS,
297
+ label: "Clear Agents",
298
+ description: "Clear retained terminal subagent records. By default clears successful completed/steered subagents older than 5 minutes. " +
299
+ "Provide agent_ids to clear specific terminal subagents by exact ID or unique prefix. Running and queued subagents are never cleared; " +
300
+ "specific attempts to clear them are reported as errors.",
301
+ promptSnippet: "Clear completed subagent records that are still retained",
302
+ parameters: Type.Object({
303
+ agent_ids: Type.Optional(Type.Array(Type.String(), {
304
+ description: "Optional exact IDs or unique prefixes to clear. When provided, the age threshold is ignored for those IDs.",
305
+ })),
306
+ older_than_minutes: Type.Optional(Type.Number({
307
+ description: "Default-mode age threshold in minutes. Defaults to 5. Ignored when agent_ids is provided.",
308
+ minimum: 0,
309
+ })),
310
+ include_errors: Type.Optional(Type.Boolean({
311
+ description: "In default mode, also clear failed/stopped/aborted terminal records older than the age threshold. Default false.",
312
+ })),
313
+ }),
314
+ renderShell: "self",
315
+ renderCall: () => renderEmptyCall(),
316
+ renderResult: (result, _options, theme) => {
317
+ const details = result?.details;
318
+ return details ? renderClearSubagentsDetails(details, theme) : renderEmptyCall();
319
+ },
320
+ execute: async (_toolCallId, params) => {
321
+ const selection = clearSubagentRecords(manager.listAgents(), {
322
+ agentIds: params.agent_ids,
323
+ olderThanMs: (params.older_than_minutes ?? 5) * 60_000,
324
+ includeErrors: params.include_errors === true,
325
+ });
326
+ const removed = manager.clearRecords(selection.clearIds);
327
+ const details = buildClearSubagentsDetails({ ...selection, clearIds: removed });
328
+ return textResult(formatClearSubagentsText(details), details);
329
+ },
330
+ }));
331
+ }
@@ -23,7 +23,7 @@ export function snipMiddleLines(text, edgeLines = 20) {
23
23
  const omitted = lines.length - maxLines;
24
24
  return [
25
25
  ...lines.slice(0, edgeLines),
26
- `... ${omitted} lines omitted; expand for full output ...`,
26
+ `─────── ${omitted} lines hidden from preview ───────`,
27
27
  ...lines.slice(-edgeLines),
28
28
  ];
29
29
  }