@clanker-code/pi-subagents 0.10.7 → 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.
@@ -0,0 +1,270 @@
1
+ /**
2
+ * dashboard-ui.ts — Register dashboard UI modules for subagent visibility.
3
+ *
4
+ * Provides three integration points with pi-agent-dashboard:
5
+ * 1. Footer-segment decorator showing running/completed agent counts
6
+ * 2. Management-modal module with a table view of all subagent history
7
+ * 3. Round-trip event handlers for data fetch, abort, and steer actions
8
+ */
9
+
10
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
11
+ import type { AgentManager } from "./agent-manager.js";
12
+ import { formatMs, getDisplayName } from "./ui/agent-widget.js";
13
+ import { getLifetimeTotal } from "./usage.js";
14
+
15
+ // Dashboard shared types (inlined to avoid adding a dependency)
16
+ interface DecoratorDescriptor {
17
+ kind: "footer-segment" | "agent-metric" | "breadcrumb" | "gate" | "toast";
18
+ namespace: string;
19
+ id: string;
20
+ payload: Record<string, unknown>;
21
+ }
22
+
23
+ interface ExtensionUiModule {
24
+ kind: "management-modal";
25
+ id: string;
26
+ command: string;
27
+ title: string;
28
+ description?: string;
29
+ icon?: string;
30
+ category?: string;
31
+ view: {
32
+ kind: "table" | "grid" | "form";
33
+ dataEvent?: string;
34
+ rowKey?: string;
35
+ fields?: Array<{
36
+ key: string;
37
+ label: string;
38
+ kind: string;
39
+ width?: string | number;
40
+ }>;
41
+ rowActions?: Array<{
42
+ id: string;
43
+ label: string;
44
+ icon?: string;
45
+ variant?: "primary" | "secondary" | "danger";
46
+ event: string;
47
+ confirm?: string;
48
+ }>;
49
+ emptyState?: string;
50
+ actions?: Array<{
51
+ id: string;
52
+ label: string;
53
+ icon?: string;
54
+ variant?: "primary" | "secondary" | "danger";
55
+ event: string;
56
+ }>;
57
+ };
58
+ }
59
+
60
+ type ModuleProbe = { modules: Array<ExtensionUiModule | DecoratorDescriptor> };
61
+
62
+ const NAMESPACE = "subagents";
63
+ const MODULE_ID = "subagents-overview";
64
+ const DATA_EVENT = "subagents:rows";
65
+ const INVALIDATE_DEBOUNCE_MS = 500;
66
+
67
+ /**
68
+ * Build a row for the management-modal table from an AgentRecord.
69
+ */
70
+ function buildAgentRow(record: any) {
71
+ const durationMs = record.completedAt
72
+ ? record.completedAt - record.startedAt
73
+ : Date.now() - record.startedAt;
74
+ const totalTokens = getLifetimeTotal(record.lifetimeUsage);
75
+
76
+ return {
77
+ id: record.id,
78
+ type: getDisplayName(record.type),
79
+ description: record.description ?? "",
80
+ status: record.status,
81
+ toolUses: record.toolUses ?? 0,
82
+ tokens: totalTokens > 0 ? formatTokenCount(totalTokens) : "—",
83
+ duration: formatMs(durationMs),
84
+ outputFile: record.outputFile ?? "",
85
+ startedAt: record.startedAt,
86
+ };
87
+ }
88
+
89
+ function formatTokenCount(n: number): string {
90
+ if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;
91
+ if (n >= 1_000) return `${(n / 1_000).toFixed(1)}k`;
92
+ return String(n);
93
+ }
94
+
95
+ /**
96
+ * Register all dashboard UI integration points.
97
+ * Call once during extension setup when pi.events is available.
98
+ */
99
+ export function registerDashboardModules(pi: ExtensionAPI, manager: AgentManager): void {
100
+ if (!pi.events) return;
101
+
102
+ let invalidateTimer: ReturnType<typeof setTimeout> | undefined;
103
+
104
+ function scheduleInvalidate() {
105
+ if (invalidateTimer) return;
106
+ invalidateTimer = setTimeout(() => {
107
+ invalidateTimer = undefined;
108
+ pi.events.emit("ui:invalidate", {});
109
+ }, INVALIDATE_DEBOUNCE_MS);
110
+ }
111
+
112
+ // ── 1. Module Discovery (ui:list-modules) ──────────────────────────
113
+ pi.events.on("ui:list-modules", ((probe: ModuleProbe) => {
114
+ const agents = manager.listAgents();
115
+ const running = agents.filter(a => a.status === "running").length;
116
+ const completed = agents.filter(a => a.status === "completed").length;
117
+ const total = agents.length;
118
+
119
+ // Footer-segment: running/completed counts
120
+ const parts: string[] = [];
121
+ if (running > 0) parts.push(`● ${running} running`);
122
+ if (completed > 0) parts.push(`✓ ${completed} done`);
123
+ if (total === 0) parts.push("No agents");
124
+
125
+ probe.modules.push({
126
+ kind: "footer-segment",
127
+ namespace: NAMESPACE,
128
+ id: "agent-counts",
129
+ payload: {
130
+ text: parts.join(" · "),
131
+ tooltip: `${total} total agents (${running} running, ${completed} completed)`,
132
+ icon: "mdiRobot",
133
+ },
134
+ } as DecoratorDescriptor);
135
+
136
+ // Management-modal: subagent overview table
137
+ probe.modules.push({
138
+ kind: "management-modal",
139
+ id: MODULE_ID,
140
+ command: "/subagents",
141
+ title: "Subagents",
142
+ description: "View and manage background subagents",
143
+ icon: "mdiRobotOutline",
144
+ category: "subagents",
145
+ view: {
146
+ kind: "table",
147
+ dataEvent: DATA_EVENT,
148
+ rowKey: "id",
149
+ fields: [
150
+ { key: "id", label: "ID", kind: "text", width: 120 },
151
+ { key: "type", label: "Type", kind: "text", width: 100 },
152
+ { key: "description", label: "Description", kind: "text" },
153
+ { key: "status", label: "Status", kind: "text", width: 90 },
154
+ { key: "toolUses", label: "Tools", kind: "number", width: 60 },
155
+ { key: "tokens", label: "Tokens", kind: "text", width: 80 },
156
+ { key: "duration", label: "Duration", kind: "text", width: 80 },
157
+ ],
158
+ rowActions: [
159
+ {
160
+ id: "view-result",
161
+ label: "View Result",
162
+ icon: "mdiEye",
163
+ variant: "primary",
164
+ event: "subagents:ui:view-result",
165
+ },
166
+ {
167
+ id: "abort",
168
+ label: "Abort",
169
+ icon: "mdiStop",
170
+ variant: "danger",
171
+ event: "subagents:ui:abort",
172
+ confirm: "Abort this running agent?",
173
+ },
174
+ {
175
+ id: "steer",
176
+ label: "Steer",
177
+ icon: "mdiMessageArrowRight",
178
+ variant: "secondary",
179
+ event: "subagents:ui:steer",
180
+ },
181
+ ],
182
+ emptyState: "No subagents have been spawned in this session.",
183
+ actions: [
184
+ {
185
+ id: "refresh",
186
+ label: "Refresh",
187
+ icon: "mdiRefresh",
188
+ variant: "secondary",
189
+ event: "subagents:ui:refresh",
190
+ },
191
+ ],
192
+ },
193
+ } as ExtensionUiModule);
194
+ }) as any);
195
+
196
+ // ── 2. Data Fetch Handler ──────────────────────────────────────────
197
+ pi.events.on(DATA_EVENT, ((data: any) => {
198
+ const agents = manager.listAgents();
199
+ data.items = agents.map(buildAgentRow);
200
+ }) as any);
201
+
202
+ // ── 3. Action Handlers ─────────────────────────────────────────────
203
+
204
+ // Refresh: just invalidate to re-probe + re-fetch
205
+ pi.events.on("subagents:ui:refresh", (() => {
206
+ scheduleInvalidate();
207
+ }) as any);
208
+
209
+ // View Result: emit the result as a toast so the dashboard shows it
210
+ pi.events.on("subagents:ui:view-result", ((data: any) => {
211
+ const agentId = data.params?.id ?? data.id;
212
+ const record = manager.getRecord(agentId);
213
+ if (!record) {
214
+ pi.events.emit("ui:invalidate", { id: MODULE_ID });
215
+ return;
216
+ }
217
+
218
+ const resultText = record.result?.trim() || "No output yet.";
219
+ const preview = resultText.length > 2000
220
+ ? resultText.slice(0, 2000) + "\n…(truncated)"
221
+ : resultText;
222
+
223
+ pi.events.emit("ui:invalidate", { id: MODULE_ID });
224
+
225
+ // Return result as items so it shows in a detail view
226
+ data.items = [{
227
+ id: record.id,
228
+ type: getDisplayName(record.type),
229
+ description: record.description,
230
+ status: record.status,
231
+ result: preview,
232
+ outputFile: record.outputFile ?? "",
233
+ }];
234
+ }) as any);
235
+
236
+ // Abort: signal the agent to stop
237
+ pi.events.on("subagents:ui:abort", ((data: any) => {
238
+ const agentId = data.params?.id ?? data.id;
239
+ const record = manager.getRecord(agentId);
240
+ if (record?.session) {
241
+ // Use the session's abort mechanism
242
+ try {
243
+ record.session.dispose?.();
244
+ } catch {
245
+ // Ignore disposal errors
246
+ }
247
+ }
248
+ scheduleInvalidate();
249
+ }) as any);
250
+
251
+ // Steer: for now just invalidate — full steer requires a prompt input
252
+ // which the management-modal form view could support in the future
253
+ pi.events.on("subagents:ui:steer", ((_data: any) => {
254
+ // TODO: Could open a form view for entering the steer message
255
+ scheduleInvalidate();
256
+ }) as any);
257
+
258
+ // ── 4. Invalidate on agent lifecycle events ────────────────────────
259
+ const lifecycleEvents = [
260
+ "subagents:created",
261
+ "subagents:started",
262
+ "subagents:completed",
263
+ "subagents:failed",
264
+ "subagents:compacted",
265
+ ];
266
+
267
+ for (const event of lifecycleEvents) {
268
+ pi.events.on(event, (() => scheduleInvalidate()) as any);
269
+ }
270
+ }
@@ -34,7 +34,6 @@ export const DEFAULT_AGENTS: Map<string, AgentConfig> = new Map([
34
34
  builtinToolNames: READ_ONLY_TOOLS,
35
35
  extensions: true,
36
36
  skills: true,
37
- model: "anthropic/claude-haiku-4-5-20251001",
38
37
  systemPrompt: `# CRITICAL: READ-ONLY MODE - NO FILE MODIFICATIONS
39
38
  You are a file search specialist. You excel at thoroughly navigating and exploring codebases.
40
39
  Your role is EXCLUSIVELY to search and analyze existing code. You do NOT have access to file editing tools.
package/src/index.ts CHANGED
@@ -24,8 +24,10 @@ import { getAgentConversation, getCurrentExtensionAgentId, getCurrentExtensionDe
24
24
  import { buildAgentToolDescription, getModelLabelFromConfig } from "./agent-tool-description.js";
25
25
  import { BUILTIN_TOOL_NAMES, getAgentConfig, getAllTypes, getAvailableTypes, isDefaultsDisabled, registerAgents, resolveType, setDefaultsDisabled } from "./agent-types.js";
26
26
  import { formatOutputFileHint, limitText, MAX_RESULT_CHARS, MAX_VERBOSE_CHARS } from "./bounded-output.js";
27
+ import { extractText } from "./context.js";
27
28
  import { registerRpcHandlers } from "./cross-extension-rpc.js";
28
29
  import { loadCustomAgents } from "./custom-agents.js";
30
+ import { registerDashboardModules } from "./dashboard-ui.js";
29
31
  import { isModelInScope, readEnabledModels, resolveEnabledModels } from "./enabled-models.js";
30
32
  import { GroupJoinManager } from "./group-join.js";
31
33
  import { resolveAgentInvocationConfig, resolveJoinMode } from "./invocation-config.js";
@@ -37,11 +39,11 @@ import { SubagentScheduler } from "./schedule.js";
37
39
  import { resolveStorePath, ScheduleStore } from "./schedule-store.js";
38
40
  import { applyAndEmitLoaded, DEFAULT_WAIT_TIMEOUT_SECONDS, type SubagentsSettings, saveAndEmitChanged, type ToolDescriptionMode } from "./settings.js";
39
41
  import { getStatusNote } from "./status-note.js";
42
+ import { registerSubagentListClearTools } from "./subagent-list-clear.js";
40
43
  import { type AgentConfig, type AgentInvocation, type AgentRecord, type JoinMode, MAX_RECURSIVE_DEPTH, type NotificationDetails, type SubagentType } from "./types.js";
41
- import { renderAgentCall, renderAgentResult, renderSteerCall, tailPreview } from "./ui/agent-tool-rendering.js";
44
+ import { renderAgentCall, renderAgentResult, renderSteerCall, snipMiddleLines, tailPreview } from "./ui/agent-tool-rendering.js";
42
45
  import {
43
46
  type AgentActivity,
44
- type AgentDetails,
45
47
  AgentWidget,
46
48
  buildInvocationTags,
47
49
  describeActivity,
@@ -55,15 +57,26 @@ import type { WidgetAgentSnapshot, WidgetDisplayMode } from "./ui/agent-widget-t
55
57
  import { menuSelect } from "./ui/menu-select.js";
56
58
  import { showSchedulesMenu } from "./ui/schedule-menu.js";
57
59
  import { addUsage, getLifetimeTotal, getSessionContextPercent } from "./usage.js";
58
- import { formatWaitTimeout, raceWait, type WaitOutcome, waitTimeoutMessage } from "./wait.js";
60
+ import { formatWaitTimeout, pollPendingMessages, raceWait, type WaitOutcome, waitTimeoutMessage } from "./wait.js";
59
61
 
60
62
  // ---- Shared helpers ----
61
63
 
62
64
  /** Tool execute return value for a text response. */
63
- function textResult(msg: string, details?: AgentDetails) {
65
+ function textResult(msg: string, details?: unknown) {
64
66
  return { content: [{ type: "text" as const, text: msg }], details: details as any };
65
67
  }
66
68
 
69
+ /** Metadata attached to get_subagent_result for compact UI rendering. */
70
+ interface GetResultDetails {
71
+ status: AgentRecord["status"];
72
+ description: string;
73
+ toolUses: number;
74
+ tokens: string | null;
75
+ contextPercent: number | null;
76
+ duration: string;
77
+ outputFile?: string;
78
+ }
79
+
67
80
  /**
68
81
  * Create an AgentActivity state and spawn callbacks for tracking tool usage.
69
82
  * Used by the background spawn path to track tool usage.
@@ -443,6 +456,8 @@ export default function (pi: ExtensionAPI) {
443
456
  // Capture ctx from session_start for RPC spawn handler + start the scheduler.
444
457
  pi.on("session_start", async (_event, ctx) => {
445
458
  currentCtx = ctx;
459
+ clearBatchState();
460
+ groupJoin.dispose();
446
461
  manager.clearCompleted();
447
462
  widget.clearSnapshots();
448
463
  retryStash.clear();
@@ -450,8 +465,10 @@ export default function (pi: ExtensionAPI) {
450
465
  });
451
466
 
452
467
  pi.on("session_before_switch", () => {
468
+ clearBatchState();
469
+ groupJoin.dispose();
453
470
  manager.clearCompleted();
454
- widget.clearSnapshots();
471
+ widget.dispose();
455
472
  retryStash.clear();
456
473
  scheduler.stop();
457
474
  });
@@ -472,13 +489,17 @@ export default function (pi: ExtensionAPI) {
472
489
  unsubSpawnRpc();
473
490
  unsubStopRpc();
474
491
  unsubPingRpc();
492
+ unsubWidgetCreated?.();
475
493
  unsubWidgetStarted?.();
476
494
  unsubWidgetCompleted?.();
477
495
  unsubWidgetFailed?.();
478
496
  currentCtx = undefined;
479
497
  delete (globalThis as any)[MANAGER_KEY];
480
498
  scheduler.stop();
499
+ clearBatchState();
500
+ groupJoin.dispose();
481
501
  manager.abortAll();
502
+ widget.dispose();
482
503
  for (const timer of pendingNudges.values()) clearTimeout(timer);
483
504
  pendingNudges.clear();
484
505
  retryStash.clear();
@@ -491,6 +512,7 @@ export default function (pi: ExtensionAPI) {
491
512
  const snapshot = widgetSnapshotFromEvent(payload);
492
513
  if (snapshot) widget.upsertSnapshot(snapshot);
493
514
  };
515
+ const unsubWidgetCreated = pi.events.on("subagents:created", upsertWidgetEventSnapshot);
494
516
  const unsubWidgetStarted = pi.events.on("subagents:started", upsertWidgetEventSnapshot);
495
517
  const unsubWidgetCompleted = pi.events.on("subagents:completed", upsertWidgetEventSnapshot);
496
518
  const unsubWidgetFailed = pi.events.on("subagents:failed", upsertWidgetEventSnapshot);
@@ -567,6 +589,14 @@ export default function (pi: ExtensionAPI) {
567
589
  let batchFinalizeTimer: ReturnType<typeof setTimeout> | undefined;
568
590
  let batchCounter = 0;
569
591
 
592
+ function clearBatchState() {
593
+ if (batchFinalizeTimer) {
594
+ clearTimeout(batchFinalizeTimer);
595
+ batchFinalizeTimer = undefined;
596
+ }
597
+ currentBatchAgents = [];
598
+ }
599
+
570
600
  /** Finalize the current batch: if 2+ smart-mode agents, register as a group. */
571
601
  function finalizeBatch() {
572
602
  batchFinalizeTimer = undefined;
@@ -679,7 +709,7 @@ export default function (pi: ExtensionAPI) {
679
709
  }),
680
710
  subagent_type: Type.Optional(
681
711
  Type.String({
682
- 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.`,
712
+ 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.`,
683
713
  }),
684
714
  ),
685
715
  model: Type.Optional(
@@ -782,18 +812,16 @@ export default function (pi: ExtensionAPI) {
782
812
  const { retry: _omit, ...overrides } = params;
783
813
  P = { ...stashed.params, ...overrides } as typeof params;
784
814
  }
785
- emitPromptPreview(P.prompt, P.description, P.subagent_type);
815
+ const requestedSubagentType = (P.subagent_type ?? "general-purpose") as SubagentType;
816
+ emitPromptPreview(P.prompt, P.description, requestedSubagentType);
786
817
 
787
- // Retry supplied the prompt/type from the stash; otherwise both are required.
788
- if (!retryHandle && (!P.prompt || !P.subagent_type)) {
789
- return textResult(
790
- `Missing required argument${!P.prompt && !P.subagent_type ? "s" : ""}: ` +
791
- [!P.prompt && "prompt", !P.subagent_type && "subagent_type"].filter(Boolean).join(", ") +
792
- ".",
793
- );
818
+ // Retry supplied the prompt from the stash; otherwise prompt is required.
819
+ // subagent_type defaults to general-purpose when omitted.
820
+ if (!retryHandle && !P.prompt) {
821
+ return textResult("Missing required argument: prompt.");
794
822
  }
795
823
 
796
- const rawType = P.subagent_type as SubagentType;
824
+ const rawType = requestedSubagentType;
797
825
  const resolved = resolveType(rawType);
798
826
  if (!resolved) {
799
827
  // Unknown agent type — recoverable. List valid types so the orchestrator
@@ -1032,6 +1060,10 @@ export default function (pi: ExtensionAPI) {
1032
1060
  isBackground: true,
1033
1061
  depth: record?.depth ?? nextSubagentDepth,
1034
1062
  parentAgentId: extensionAgentId,
1063
+ status: record?.status ?? "running",
1064
+ startedAt: record?.startedAt,
1065
+ toolUses: record?.toolUses ?? 0,
1066
+ invocation: record?.invocation,
1035
1067
  });
1036
1068
 
1037
1069
  const isQueued = record?.status === "queued";
@@ -1077,7 +1109,58 @@ export default function (pi: ExtensionAPI) {
1077
1109
  }),
1078
1110
  ),
1079
1111
  }),
1080
- execute: async (_toolCallId, params, signal, _onUpdate, _ctx) => {
1112
+ renderResult(result, { expanded }, theme) {
1113
+ const details = result.details as GetResultDetails | undefined;
1114
+ const text = extractText(result.content);
1115
+
1116
+ // Header: status + stats + description
1117
+ let line = "";
1118
+ if (details) {
1119
+ const icon = details.status === "error" || details.status === "stopped" || details.status === "aborted"
1120
+ ? theme.fg("error", "✗")
1121
+ : details.status === "running" || details.status === "queued"
1122
+ ? theme.fg("accent", "◌")
1123
+ : theme.fg("success", "✓");
1124
+
1125
+ const parts: string[] = [];
1126
+ if (details.toolUses > 0) parts.push(`${details.toolUses} tool use${details.toolUses === 1 ? "" : "s"}`);
1127
+ if (details.tokens) parts.push(details.tokens);
1128
+ if (details.contextPercent !== null) parts.push(`ctx ${Math.round(details.contextPercent)}%`);
1129
+ if (details.duration) parts.push(details.duration);
1130
+ const stats = parts.map(p => theme.fg("dim", p)).join(" " + theme.fg("dim", "·") + " ");
1131
+
1132
+ line = `${icon} ${theme.bold(details.description)} ${theme.fg("dim", details.status)}`;
1133
+ if (stats) line += "\n " + stats;
1134
+ }
1135
+
1136
+ // Body: snip when collapsed, full when expanded
1137
+ // Extract the body portion (after the first blank line) to keep the
1138
+ // tool-output header always visible and only snip the actual result.
1139
+ if (text.trim()) {
1140
+ const firstBlank = text.indexOf("\n\n");
1141
+ const body = firstBlank >= 0 ? text.slice(firstBlank + 2) : text;
1142
+
1143
+ if (expanded) {
1144
+ for (const l of text.split("\n")) {
1145
+ line += "\n" + theme.fg("dim", ` ${l}`);
1146
+ }
1147
+ } else {
1148
+ // Show the tool-output header verbatim, then snip only the body
1149
+ if (firstBlank >= 0) {
1150
+ for (const l of text.slice(0, firstBlank).split("\n")) {
1151
+ line += "\n" + theme.fg("dim", ` ${l}`);
1152
+ }
1153
+ }
1154
+ for (const l of snipMiddleLines(body, 20)) {
1155
+ line += "\n" + theme.fg("dim", ` ${l}`);
1156
+ }
1157
+ }
1158
+ }
1159
+
1160
+ return new Text(line, 0, 0);
1161
+ },
1162
+
1163
+ execute: async (_toolCallId, params, signal, _onUpdate, ctx) => {
1081
1164
  const record = manager.getRecord(params.agent_id);
1082
1165
  if (!record) {
1083
1166
  return textResult(`Agent not found: "${params.agent_id}". It may have been cleaned up.`);
@@ -1099,7 +1182,17 @@ export default function (pi: ExtensionAPI) {
1099
1182
  let waitOutcome: WaitOutcome = "completed";
1100
1183
  if (params.wait && record.status === "running" && record.promise) {
1101
1184
  cancelNudge(params.agent_id);
1102
- waitOutcome = await raceWait(record.promise, signal, getWaitTimeoutSeconds());
1185
+ // Poll for queued user messages so we can return early and let the
1186
+ // parent LLM process them immediately instead of blocking for the
1187
+ // full wait timeout.
1188
+ const pending = typeof ctx?.hasPendingMessages === "function"
1189
+ ? pollPendingMessages(() => ctx.hasPendingMessages())
1190
+ : undefined;
1191
+ try {
1192
+ waitOutcome = await raceWait(record.promise, signal, getWaitTimeoutSeconds(), pending?.promise);
1193
+ } finally {
1194
+ pending?.cancel();
1195
+ }
1103
1196
  if (waitOutcome === "completed") {
1104
1197
  record.resultConsumed = true;
1105
1198
  }
@@ -1115,6 +1208,16 @@ export default function (pi: ExtensionAPI) {
1115
1208
  if (record.compactionCount) statsParts.push(`Compactions: ${record.compactionCount}`);
1116
1209
  statsParts.push(`Duration: ${duration}`);
1117
1210
 
1211
+ const details: GetResultDetails = {
1212
+ status: record.status,
1213
+ description: record.description,
1214
+ toolUses: record.toolUses,
1215
+ tokens: tokens || null,
1216
+ contextPercent,
1217
+ duration,
1218
+ outputFile: record.outputFile,
1219
+ };
1220
+
1118
1221
  let output =
1119
1222
  `Agent: ${record.id}\n` +
1120
1223
  `Type: ${displayName} | Status: ${record.status}${getStatusNote(record.status)} | ${statsParts.join(" | ")}\n` +
@@ -1158,7 +1261,7 @@ export default function (pi: ExtensionAPI) {
1158
1261
  }
1159
1262
  }
1160
1263
 
1161
- return textResult(output);
1264
+ return textResult(output, details);
1162
1265
  },
1163
1266
  }));
1164
1267
 
@@ -1218,6 +1321,11 @@ export default function (pi: ExtensionAPI) {
1218
1321
  },
1219
1322
  }));
1220
1323
 
1324
+ // ---- list_subagents / clear_subagents tools ----
1325
+
1326
+ registerSubagentListClearTools(pi, manager);
1327
+ registerDashboardModules(pi, manager);
1328
+
1221
1329
  // ---- list_models tool ----
1222
1330
 
1223
1331
  pi.registerTool(defineTool({
package/src/peek.ts CHANGED
@@ -112,6 +112,11 @@ function parseOutputFileLines(path: string): string[] {
112
112
  return [];
113
113
  }
114
114
  const out: string[] = [];
115
+ const pushRenderedLines = (text: string) => {
116
+ for (const renderedLine of text.trimEnd().split("\n")) {
117
+ if (renderedLine.trim()) out.push(renderedLine);
118
+ }
119
+ };
115
120
  for (const line of raw.split("\n")) {
116
121
  const trimmed = line.trim();
117
122
  if (!trimmed) continue;
@@ -124,12 +129,12 @@ function parseOutputFileLines(path: string): string[] {
124
129
  const content = entry?.message?.content;
125
130
  if (!Array.isArray(content)) {
126
131
  // Some entries may carry a plain string content.
127
- if (typeof content === "string" && content.trim()) out.push(content.trim());
132
+ if (typeof content === "string" && content.trim()) pushRenderedLines(content);
128
133
  continue;
129
134
  }
130
135
  for (const block of content) {
131
136
  if (block?.type === "text" && typeof block.text === "string" && block.text.trim()) {
132
- out.push(block.text.trimEnd());
137
+ pushRenderedLines(block.text);
133
138
  }
134
139
  }
135
140
  }