@ian-pascoe/pi-minimal-subagents 0.11.0 → 0.13.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.
@@ -1,21 +1,10 @@
1
1
  import { stripVTControlCharacters } from "node:util";
2
- import type { AgentMessage } from "@earendil-works/pi-agent-core";
3
- import { contentText } from "@earendil-works/pi-ai";
4
2
  import {
5
- AssistantMessageComponent,
6
- BashExecutionComponent,
7
- BranchSummaryMessageComponent,
8
- CompactionSummaryMessageComponent,
9
- CustomMessageComponent,
10
- ToolExecutionComponent,
11
- UserMessageComponent,
12
3
  type ExtensionContext,
13
4
  type KeybindingsManager,
14
5
  type Theme,
15
- type TruncationResult,
16
6
  } from "@earendil-works/pi-coding-agent";
17
7
  import {
18
- Container,
19
8
  Text,
20
9
  matchesKey,
21
10
  truncateToWidth,
@@ -28,11 +17,14 @@ import type { MinimalSubagentsCoordinator } from "./minimal-subagents-coordinato
28
17
  import {
29
18
  formatSubagentDuration,
30
19
  orderActiveAgentSubtrees,
31
- renderMinimalSubagentsMessage,
32
- renderMinimalSubagentsResult,
33
20
  subagentStatusLadder,
34
21
  } from "./minimal-subagents-rendering.js";
35
22
  import { COORDINATOR_TOOL_NAMES } from "./minimal-subagents-capabilities.js";
23
+ import {
24
+ createTranscriptRenderCache,
25
+ renderTranscriptSnapshot,
26
+ type TranscriptLayout,
27
+ } from "./minimal-subagents-transcript.js";
36
28
  import type { SubagentAccessSnapshot } from "./minimal-subagents-access.js";
37
29
  import type {
38
30
  AgentSummary,
@@ -66,7 +58,7 @@ function flattenStatusAgents(status: HierarchyStatusResult): FlattenedStatusAgen
66
58
  const flattened: FlattenedStatusAgent[] = [];
67
59
  const visit = (agent: AgentSummary, depth: number): void => {
68
60
  flattened.push({ agent, depth });
69
- for (const child of agent.children) visit(child, depth + 1);
61
+ for (const child of agent.children ?? []) visit(child, depth + 1);
70
62
  };
71
63
  const roots = "agents" in status ? status.agents : [status.agent];
72
64
  for (const agent of orderActiveAgentSubtrees(roots)) visit(agent, 0);
@@ -90,16 +82,20 @@ function statusAccessSourceLabel(source: SubagentAccessSnapshot["source"]): stri
90
82
  }
91
83
  }
92
84
 
93
- interface CachedTranscriptMessage {
94
- container: Container;
95
- tools: Map<string, ToolExecutionComponent>;
96
- expanded: boolean;
97
- streaming: boolean;
98
- }
99
-
100
- interface TranscriptLayout {
101
- lines: string[];
102
- messageStarts: number[];
85
+ const BACKGROUND_CLEARING_SEQUENCES = ["\u001b[0m", "\u001b[m", "\u001b[49m"] as const;
86
+
87
+ /**
88
+ * Re-open the panel background after each sequence that clears it. Truncation ends its cut text
89
+ * and ellipsis with full resets, and embedded native components may reset or close their own
90
+ * backgrounds; without this the rest of the row (ellipsis, padding, border) loses the panel fill.
91
+ */
92
+ function keepBackgroundThroughResets(text: string, backgroundOpen: string): string {
93
+ if (!backgroundOpen) return text;
94
+ let result = text;
95
+ for (const sequence of BACKGROUND_CLEARING_SEQUENCES) {
96
+ result = result.replaceAll(sequence, `${sequence}${backgroundOpen}`);
97
+ }
98
+ return result;
103
99
  }
104
100
 
105
101
  function transcriptText(line: string): string {
@@ -137,180 +133,6 @@ function anchoredTranscriptOffset(
137
133
  return nextStart + Math.max(0, newLines.length - 1);
138
134
  }
139
135
 
140
- interface TranscriptRenderCache {
141
- messages: WeakMap<AgentMessage, CachedTranscriptMessage>;
142
- results: WeakMap<ToolExecutionComponent, AgentMessage>;
143
- }
144
-
145
- function renderTranscriptSnapshot(
146
- snapshot: ChildAgentTranscriptSnapshot,
147
- tui: TUI,
148
- cwd: string,
149
- expanded: boolean,
150
- width: number,
151
- cache: TranscriptRenderCache,
152
- ): TranscriptLayout {
153
- if (snapshot.messages.length === 0) {
154
- return {
155
- lines: new Text(snapshot.fallback || "No conversation messages yet.", 3, 0).render(width),
156
- messageStarts: [0],
157
- };
158
- }
159
- const blocks: Container[] = [];
160
- const tools = new Map(
161
- snapshot.toolDefinitions.map((definition) => [definition.name, definition]),
162
- );
163
- const pendingTools = new Map<string, ToolExecutionComponent>();
164
- const currentMessages = new Set(snapshot.messages);
165
-
166
- for (const [messageIndex, message] of snapshot.messages.entries()) {
167
- if (message.role === "toolResult") {
168
- const paired = pendingTools.get(message.toolCallId);
169
- if (paired) {
170
- if (cache.results.get(paired) !== message) {
171
- paired.updateResult(message);
172
- cache.results.set(paired, message);
173
- }
174
- pendingTools.delete(message.toolCallId);
175
- blocks.push(new Container());
176
- continue;
177
- }
178
- }
179
- const streaming = messageIndex === snapshot.streamingAssistantIndex;
180
- const cached = cache.messages.get(message);
181
- const staleResult =
182
- cached &&
183
- [...cached.tools.values()].some((tool) => {
184
- const result = cache.results.get(tool);
185
- return result !== undefined && !currentMessages.has(result);
186
- });
187
- if (cached && !staleResult && cached.expanded === expanded && cached.streaming === streaming) {
188
- blocks.push(cached.container);
189
- for (const [id, tool] of cached.tools) pendingTools.set(id, tool);
190
- continue;
191
- }
192
- const container = new Container();
193
- const messageTools = new Map<string, ToolExecutionComponent>();
194
- switch (message.role) {
195
- case "user": {
196
- const text = contentText(message.content, "\n\n");
197
- if (text) container.addChild(new UserMessageComponent(text));
198
- break;
199
- }
200
- case "assistant": {
201
- const assistant = new AssistantMessageComponent(message);
202
- assistant.updateContent(message, messageIndex === snapshot.streamingAssistantIndex);
203
- container.addChild(assistant);
204
- for (const content of message.content) {
205
- if (content.type !== "toolCall") continue;
206
- const tool = new ToolExecutionComponent(
207
- content.name,
208
- content.id,
209
- content.arguments,
210
- { showImages: false },
211
- tools.get(content.name) ?? {},
212
- tui,
213
- cwd,
214
- );
215
- tool.setExpanded(expanded);
216
- container.addChild(tool);
217
- if (message.stopReason === "aborted" || message.stopReason === "error") {
218
- tool.updateResult({
219
- content: [
220
- {
221
- type: "text",
222
- text:
223
- message.stopReason === "aborted"
224
- ? "Operation aborted"
225
- : (message.errorMessage ?? "Error"),
226
- },
227
- ],
228
- isError: true,
229
- });
230
- } else {
231
- pendingTools.set(content.id, tool);
232
- messageTools.set(content.id, tool);
233
- }
234
- }
235
- break;
236
- }
237
- case "toolResult": {
238
- const inherited = new ToolExecutionComponent(
239
- message.toolName,
240
- message.toolCallId,
241
- {},
242
- { showImages: false },
243
- {},
244
- tui,
245
- cwd,
246
- );
247
- inherited.setExpanded(expanded);
248
- inherited.updateResult(message);
249
- container.addChild(inherited);
250
- break;
251
- }
252
- case "custom": {
253
- if (!message.display) break;
254
- const renderer =
255
- message.customType === "minimal-subagents.message"
256
- ? renderMinimalSubagentsMessage
257
- : message.customType === "minimal-subagents.result"
258
- ? renderMinimalSubagentsResult
259
- : undefined;
260
- const custom = new CustomMessageComponent(message, renderer);
261
- custom.setExpanded(expanded);
262
- container.addChild(custom);
263
- break;
264
- }
265
- case "bashExecution": {
266
- const bash = new BashExecutionComponent(message.command, tui, message.excludeFromContext);
267
- if (message.output) bash.appendOutput(message.output);
268
- bash.setComplete(
269
- message.exitCode,
270
- message.cancelled,
271
- // SAFETY: Persisted bash messages retain only the truncation flag; BashExecutionComponent reads that flag here.
272
- message.truncated ? ({ truncated: true } as TruncationResult) : undefined,
273
- message.fullOutputPath,
274
- );
275
- bash.setExpanded(expanded);
276
- container.addChild(bash);
277
- break;
278
- }
279
- case "branchSummary": {
280
- const summary = new BranchSummaryMessageComponent(message);
281
- summary.setExpanded(expanded);
282
- container.addChild(summary);
283
- break;
284
- }
285
- case "compactionSummary": {
286
- const summary = new CompactionSummaryMessageComponent(message);
287
- summary.setExpanded(expanded);
288
- container.addChild(summary);
289
- break;
290
- }
291
- }
292
- cache.messages.set(message, { container, tools: messageTools, expanded, streaming });
293
- blocks.push(container);
294
- }
295
- let length = 0;
296
- const messageStarts: number[] = [];
297
- const lines = blocks.flatMap((block) => {
298
- messageStarts.push(length);
299
- // Native user-message prompt zones belong to the main terminal, not an embedded overlay.
300
- const rendered = block
301
- .render(width)
302
- .map((line) =>
303
- line
304
- .replaceAll("\x1b]133;A\x07", "")
305
- .replaceAll("\x1b]133;B\x07", "")
306
- .replaceAll("\x1b]133;C\x07", ""),
307
- );
308
- length += rendered.length;
309
- return rendered;
310
- });
311
- return { lines, messageStarts };
312
- }
313
-
314
136
  /** Interactive, read-only Child Agent hierarchy and transcript status component. */
315
137
  export class MinimalSubagentsStatusPanelComponent implements Component {
316
138
  private status!: HierarchyStatusResult;
@@ -324,10 +146,7 @@ export class MinimalSubagentsStatusPanelComponent implements Component {
324
146
  private following = true;
325
147
  private transcriptLineCount = 0;
326
148
  private transcriptLayout?: TranscriptLayout;
327
- private readonly transcriptCache: TranscriptRenderCache = {
328
- messages: new WeakMap(),
329
- results: new WeakMap(),
330
- };
149
+ private readonly transcriptCache = createTranscriptRenderCache();
331
150
  private bodyHeight = 1;
332
151
  private ensureSelectionVisible = true;
333
152
  private toolOutputExpanded = false;
@@ -461,8 +280,12 @@ export class MinimalSubagentsStatusPanelComponent implements Component {
461
280
  const visibleBody = body.slice(this.scrollOffset, this.scrollOffset + this.bodyHeight);
462
281
  while (visibleBody.length < this.bodyHeight) visibleBody.push("");
463
282
  const border = (text: string) => this.theme.fg("border", text);
283
+ const backgroundOpen = this.theme.getBgAnsi("customMessageBg");
464
284
  const rows = [...visibleHeader, ...visibleBody, ...help].map((line) => {
465
- const content = truncateToWidth(line, innerWidth, "…");
285
+ const content = keepBackgroundThroughResets(
286
+ truncateToWidth(line, innerWidth, "…"),
287
+ backgroundOpen,
288
+ );
466
289
  return this.theme.bg(
467
290
  "customMessageBg",
468
291
  `${border("│")} ${content}${" ".repeat(innerWidth - visibleWidth(content))} ${border("│")}`,
@@ -2,7 +2,10 @@ import { StringEnum } from "@earendil-works/pi-ai";
2
2
  import { Type } from "typebox";
3
3
  import { THINKING_LEVELS } from "./minimal-subagents-capabilities.js";
4
4
 
5
- const SessionContextSchema = StringEnum(["inherit", "compact", "omit"] as const);
5
+ const SessionContextSchema = StringEnum(["inherit", "compact", "omit"] as const, {
6
+ description:
7
+ "Default omit: the child starts blank, so write a self-contained task. inherit copies your whole conversation (costly; no prompt-cache reuse); compact summarizes it first (adds a summarization call). Opt in only when the child needs the discussion itself.",
8
+ });
6
9
  const ProjectContextSchema = StringEnum(["inherit", "omit"] as const, {
7
10
  description:
8
11
  "Whether to include project-scoped AGENTS.md instructions and skills. Settings, extensions, providers, and tools remain available.",
@@ -38,7 +41,11 @@ export function createCoordinatorToolSchemas(modelIds: readonly string[]) {
38
41
  firstModelId === undefined ? Type.Never() : StringEnum([firstModelId, ...remainingModelIds]);
39
42
  return {
40
43
  subagent: Type.Object({
41
- task: Type.String({ minLength: 1, description: "Task for the persistent child agent" }),
44
+ task: Type.String({
45
+ minLength: 1,
46
+ description:
47
+ "Self-contained brief for the persistent child agent: the goal and relevant background, target files and non-goals, the change or question, and observable acceptance criteria. Reference large inputs by file path instead of pasting them.",
48
+ }),
42
49
  agent_id: Type.Optional(
43
50
  Type.String({
44
51
  pattern: FRIENDLY_AGENT_ID_PATTERN,
@@ -69,7 +76,9 @@ export function createCoordinatorToolSchemas(modelIds: readonly string[]) {
69
76
  timeout_ms: Type.Optional(Type.Integer({ minimum: 0 })),
70
77
  }),
71
78
  subagent_status: Type.Object({
72
- agent_id: Type.Optional(canonicalAgentIdSchema("Direct child canonical agent ID")),
79
+ agent_id: Type.Optional(
80
+ canonicalAgentIdSchema("Direct child canonical agent ID; the root may name any descendant"),
81
+ ),
73
82
  }),
74
83
  subagent_cancel: Type.Object({
75
84
  agent_id: canonicalAgentIdSchema("Direct child canonical agent ID"),