@nmzpy/pi-ember-stack 0.1.6 → 0.2.2

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.
Files changed (36) hide show
  1. package/README.md +83 -83
  2. package/package.json +62 -48
  3. package/plugins/devin-auth/extensions/index.ts +68 -7
  4. package/plugins/devin-auth/src/cloud-direct/auth.ts +246 -246
  5. package/plugins/devin-auth/src/cloud-direct/catalog.ts +246 -246
  6. package/plugins/devin-auth/src/cloud-direct/chat.ts +1096 -1091
  7. package/plugins/devin-auth/src/cloud-direct/index.ts +41 -41
  8. package/plugins/devin-auth/src/cloud-direct/metadata.ts +78 -78
  9. package/plugins/devin-auth/src/cloud-direct/wire.ts +202 -202
  10. package/plugins/devin-auth/src/models.ts +42 -196
  11. package/plugins/devin-auth/src/oauth/register-user.ts +174 -174
  12. package/plugins/devin-auth/src/oauth/types.ts +71 -71
  13. package/plugins/devin-auth/src/stream.ts +1 -1
  14. package/plugins/index.ts +29 -6
  15. package/plugins/pi-compact-tools/index.ts +35 -192
  16. package/plugins/pi-compact-tools/renderer.ts +589 -0
  17. package/plugins/pi-custom-agents/index.ts +727 -373
  18. package/plugins/pi-custom-agents/questionnaire-tool.ts +14 -5
  19. package/plugins/pi-custom-agents/subagent/agents/coder.md +1 -0
  20. package/plugins/pi-custom-agents/subagent/agents/scout.md +16 -37
  21. package/plugins/pi-custom-agents/subagent/extensions/agents.ts +18 -1
  22. package/plugins/pi-custom-agents/subagent/extensions/index.ts +118 -226
  23. package/plugins/pi-custom-agents/subagent/extensions/model.ts +96 -96
  24. package/plugins/pi-custom-agents/subagent/extensions/render.ts +205 -1
  25. package/plugins/pi-custom-agents/subagent/extensions/runner.ts +260 -170
  26. package/plugins/pi-custom-agents/subagent/extensions/test/render.test.ts +145 -0
  27. package/plugins/pi-ember-fff/index.ts +975 -0
  28. package/plugins/pi-ember-fff/query.ts +247 -0
  29. package/plugins/pi-ember-fff/test/query.test.ts +222 -0
  30. package/plugins/pi-ember-fff/test/renderer.test.ts +727 -0
  31. package/plugins/pi-ember-tps/index.ts +144 -0
  32. package/plugins/pi-ember-ui/ember.json +99 -0
  33. package/plugins/pi-ember-ui/index.ts +805 -0
  34. package/plugins/pi-ember-ui/mode-colors.ts +196 -0
  35. package/tsconfig.json +2 -1
  36. package/plugins/pi-custom-agents/subagent/agents/architect.md +0 -56
@@ -1,5 +1,6 @@
1
1
  import { Type } from "typebox";
2
2
  import {
3
+ Box,
3
4
  Key,
4
5
  Text,
5
6
  matchesKey,
@@ -216,6 +217,7 @@ export function registerQuestionnaireTool(pi: any): void {
216
217
  description: "Ask the user one or more decision questions inline before continuing.",
217
218
  parameters: QuestionnaireParams,
218
219
  executionMode: "sequential",
220
+ renderShell: "self",
219
221
  async execute(
220
222
  _toolCallId: string,
221
223
  params: { questions: QuestionnaireQuestion[] },
@@ -258,22 +260,28 @@ export function registerQuestionnaireTool(pi: any): void {
258
260
  },
259
261
  renderCall(args: { questions?: QuestionnaireQuestion[] }, theme: any): any {
260
262
  const count = args.questions?.length ?? 0;
261
- return new Text(
263
+ const box = new Box(1, 1, (text: string) => theme.bg("userMessageBg", text));
264
+ box.addChild(new Text(
262
265
  theme.fg("muted", "• ") +
263
266
  theme.fg("toolTitle", theme.bold("questionnaire ")) +
264
267
  theme.fg("muted", `${count} question${count === 1 ? "" : "s"}`),
265
268
  0,
266
269
  0,
267
- );
270
+ ));
271
+ return box;
268
272
  },
269
273
  renderResult(result: any, _options: unknown, theme: any): any {
270
274
  const details = result.details as {
271
275
  answers?: QuestionnaireAnswer[];
272
276
  cancelled?: boolean;
273
277
  } | undefined;
274
- if (details?.cancelled) return new Text(theme.fg("warning", "Cancelled"), 0, 0);
278
+ const box = new Box(1, 1, (text: string) => theme.bg("userMessageBg", text));
279
+ if (details?.cancelled) {
280
+ box.addChild(new Text(theme.fg("warning", "Cancelled"), 0, 0));
281
+ return box;
282
+ }
275
283
  const answers = details?.answers ?? [];
276
- return new Text(
284
+ box.addChild(new Text(
277
285
  answers
278
286
  .map((answer) => {
279
287
  const label = `${answer.id}: ${answer.label}`;
@@ -282,7 +290,8 @@ export function registerQuestionnaireTool(pi: any): void {
282
290
  .join("\n"),
283
291
  0,
284
292
  0,
285
- );
293
+ ));
294
+ return box;
286
295
  },
287
296
  });
288
297
  }
@@ -1,4 +1,5 @@
1
1
  ---
2
+ model: devin/glm-5-2
2
3
  name: coder
3
4
  description: Implementation agent for writing, editing, testing, and verifying code. Spawn this for focused implementation tasks — bug fixes, feature additions, refactors, file edits. Full tool access.
4
5
  tools: read, bash, edit, write, grep, find, ls
@@ -1,44 +1,23 @@
1
1
  ---
2
+ thinking: high
3
+ model: devin/glm-5-2
2
4
  name: scout
3
- description: Fast codebase recon that returns compressed context for handoff. Use for finding files, understanding structure, locating symbols.
4
- tools: read, grep, find, ls
5
- thinking: low
5
+ description: Fast agent specialized for exploring codebases. Use when you need to quickly find files by patterns, search code for keywords, or answer questions about the codebase.
6
+ tools: read, bash, grep, find, ls
6
7
  ---
7
8
 
8
- You are a scout. Quickly investigate a codebase and return structured findings that another agent can use without re-reading everything.
9
+ You are a file search specialist. You excel at thoroughly navigating and exploring codebases.
9
10
 
10
- Your output will be passed to an agent who has NOT seen the files you explored.
11
+ Your strengths:
12
+ - Rapidly finding files using glob patterns
13
+ - Searching code and text with powerful regex patterns
14
+ - Reading and analyzing file contents
11
15
 
12
- Thoroughness (infer from task, default medium):
13
- - Quick: Targeted lookups, key files only
14
- - Medium: Follow imports, read critical sections
15
- - Thorough: Trace all dependencies, check tests/types
16
+ Guidelines:
17
+ - Use Glob for broad file pattern matching
18
+ - Use Grep for searching file contents with regex
19
+ - Use Read when you know the specific file path you need to read
20
+ - Use Bash for file operations like copying, moving, or listing directory contents
21
+ - Return file paths as absolute paths in your final response
16
22
 
17
- Strategy:
18
- 1. grep/find to locate relevant code
19
- 2. Read key sections (not entire files)
20
- 3. Identify types, interfaces, key functions
21
- 4. Note dependencies between files
22
-
23
- Output format:
24
-
25
- ## Evidence
26
- List exact file/symbol anchors and relevant line ranges:
27
- 1. `path/to/file.ts` (lines 10-50) - Description of what's here
28
- 2. `path/to/other.ts` (lines 100-150) - Description
29
- 3. ...
30
-
31
- ## Key Code
32
- Critical types, interfaces, or functions:
33
-
34
- ```typescript
35
- interface Example {
36
- // actual code from the files
37
- }
38
- ```
39
-
40
- ## Architecture
41
- Brief explanation of how the pieces connect.
42
-
43
- ## Start Here
44
- Which file to look at first and why.
23
+ Complete the user's search request efficiently and report your findings clearly.
@@ -38,8 +38,17 @@ interface AgentCache {
38
38
  projectAgentsDir: string | null;
39
39
  /** File-level signature per directory (name:mtime:size for each .md file) */
40
40
  dirSignatures: Map<string, string>;
41
+ /** Monotonic timestamp (ms) of the last fs-based signature validation.
42
+ * Within the TTL, cache hits skip dirSignature() entirely so no
43
+ * synchronous fs (readdirSync + statSync) hits the UI thread. */
44
+ validatedAt: number;
41
45
  }
42
46
 
47
+ /** Cache TTL: skip fs-based signature validation for this long after a
48
+ * successful validation. Agent .md edits within the window are not
49
+ * detected until the TTL expires or /subagent reload is invoked. */
50
+ const CACHE_VALIDATION_TTL_MS = 2000;
51
+
43
52
  let _cache: AgentCache | null = null;
44
53
 
45
54
  /** Clear the agent cache (call on /reload). */
@@ -153,7 +162,9 @@ export function discoverAgents(
153
162
  const userDir = path.join(getAgentDir(), "agents");
154
163
  const projectAgentsDir = findNearestProjectAgentsDir(cwd);
155
164
 
156
- // Check cache (with file-signature invalidation so editing agent .md files auto-detects changes)
165
+ // Check cache (with file-signature invalidation so editing agent .md files auto-detects changes).
166
+ // Within the TTL, skip the fs-based dirSignature() check entirely so
167
+ // cache hits do zero synchronous fs (readdirSync + statSync per file).
157
168
  if (
158
169
  _cache &&
159
170
  _cache.userDir === userDir &&
@@ -161,6 +172,10 @@ export function discoverAgents(
161
172
  _cache.bundledDir === bundledAgentsDir &&
162
173
  _cache.scope === scope
163
174
  ) {
175
+ const withinTtl = Date.now() - _cache.validatedAt < CACHE_VALIDATION_TTL_MS;
176
+ if (withinTtl) {
177
+ return { agents: _cache.agents, projectAgentsDir: _cache.projectAgentsDir };
178
+ }
164
179
  let stale = false;
165
180
  for (const [dir, cachedSig] of _cache.dirSignatures) {
166
181
  if (dirSignature(dir) !== cachedSig) {
@@ -169,6 +184,7 @@ export function discoverAgents(
169
184
  }
170
185
  }
171
186
  if (!stale) {
187
+ _cache.validatedAt = Date.now();
172
188
  return { agents: _cache.agents, projectAgentsDir: _cache.projectAgentsDir };
173
189
  }
174
190
  // Cache is stale — rebuild below
@@ -207,6 +223,7 @@ export function discoverAgents(
207
223
  agents,
208
224
  projectAgentsDir,
209
225
  dirSignatures,
226
+ validatedAt: Date.now(),
210
227
  };
211
228
 
212
229
  return { agents, projectAgentsDir };
@@ -23,10 +23,9 @@ import {
23
23
  type ExtensionAPI,
24
24
  type ExtensionContext,
25
25
  getAgentDir,
26
- getMarkdownTheme,
27
26
  ModelRegistry,
28
27
  } from "@earendil-works/pi-coding-agent";
29
- import { Container, Markdown, SelectList, Spacer, Text } from "@earendil-works/pi-tui";
28
+ import { Box, Container, SelectList, Text } from "@earendil-works/pi-tui";
30
29
  import { Type } from "typebox";
31
30
 
32
31
  import { type AgentConfig, type AgentScope, discoverAgents, formatAgentList, invalidateAgentCache } from "./agents.ts";
@@ -39,10 +38,13 @@ import {
39
38
  runSubAgent,
40
39
  } from "./runner.ts";
41
40
  import {
42
- aggregateUsage,
43
- formatUsageStats,
44
- renderSingleResult,
41
+ anySubagentRunning,
42
+ renderSubagentExpanded,
43
+ renderSubagentLayout,
44
+ SubagentCapLine,
45
45
  } from "./render.ts";
46
+ import { PulseManager } from "../../../pi-compact-tools/renderer.ts";
47
+ import { isLatestSubagentRunning } from "../../../pi-ember-ui/mode-colors.ts";
46
48
  import { type SubagentThread, threadStore } from "./threads.ts";
47
49
  import { SUBAGENT_REQUEST_EVENT, runNamedAgent, type SubagentRunRequest } from "./service.ts";
48
50
  import { ThreadViewer, type ThreadViewerCallbacks } from "./thread-viewer.ts";
@@ -139,21 +141,44 @@ interface SubagentDetails {
139
141
  export default function (pi: ExtensionAPI) {
140
142
  let currentCtx: ExtensionContext | undefined;
141
143
 
144
+ // Shared pulse timer for subagent flashing bullets. Reset on session
145
+ // replacement so stale invalidate callbacks from the previous session
146
+ // do not fire into a dead TUI.
147
+ const subagentPulses = new PulseManager();
148
+
149
+ function update_subagent_pulse(context: any, running: boolean): void {
150
+ if (!context.invalidate) return;
151
+ // ToolExecutionComponent creates a fresh invalidate closure for every
152
+ // render. Keep one callback per row so completed rows can actually be
153
+ // removed from the shared timer instead of leaking stale TUI callbacks.
154
+ const invalidate = context.state.subagentPulseInvalidate ?? context.invalidate;
155
+ context.state.subagentPulseInvalidate = invalidate;
156
+ if (running) subagentPulses.add(invalidate);
157
+ else subagentPulses.remove(invalidate);
158
+ }
159
+
142
160
  // Invalidate agent cache + clear thread store on session replacement.
143
161
  pi.on("session_start", (event, ctx) => {
144
162
  currentCtx = ctx;
145
163
  if (event.reason === "reload") invalidateAgentCache();
146
164
  threadStore.clear();
165
+ subagentPulses.clear();
166
+ });
167
+
168
+ pi.on("session_shutdown", () => {
169
+ currentCtx = undefined;
170
+ threadStore.clear();
171
+ subagentPulses.clear();
147
172
  });
148
173
 
149
174
  // Proactively steer agents toward sub-agent delegation when users mention it
150
175
  pi.on("before_agent_start", async (event) => {
151
176
  const prompt = event.prompt.toLowerCase();
152
- if (/\b(delegate to|use a subagent|run in parallel|spawn an agent|scout|review this|chain|worker agent)\b/.test(prompt)) {
177
+ if (/\b(delegate to|use a subagent|run in parallel|spawn an agent|scout|explore|review this|chain|worker agent)\b/.test(prompt)) {
153
178
  return {
154
179
  systemPrompt:
155
180
  event.systemPrompt +
156
- "\n\nThe subagent tool is available for delegating tasks to specialized agents with isolated context. Use /subagent to list available agents. Bundled: scout (fast recon), reviewer (code review), worker (implementation), general-purpose (fallback). Modes: single, parallel (max 8), chain.",
181
+ "\n\nThe subagent tool is available for delegating tasks to specialized agents with isolated context. Use /subagent to list available agents. Bundled: scout (fast codebase exploration), coder (implementation), reviewer (code review), worker (general implementation), general-purpose (fallback). Modes: single, parallel (max 8), chain.",
157
182
  };
158
183
  }
159
184
  });
@@ -165,7 +190,11 @@ export default function (pi: ExtensionAPI) {
165
190
  pi.events.on(SUBAGENT_REQUEST_EVENT, (raw) => {
166
191
  const request = raw as SubagentRunRequest;
167
192
  const ctx = currentCtx;
168
- if (!ctx || !request?.id || typeof request.respond !== "function") return;
193
+ if (!request?.id || typeof request.respond !== "function") return;
194
+ if (!ctx) {
195
+ request.respond({ id: request.id, ok: false, error: "Subagent session is not active." });
196
+ return;
197
+ }
169
198
  if (request.accept && !request.accept()) return;
170
199
  const agent = discoverAgents(ctx.cwd, "user", bundledAgentsDir).agents.find((item) => item.name === request.agent);
171
200
  if (!agent) {
@@ -279,6 +308,7 @@ export default function (pi: ExtensionAPI) {
279
308
  `To enable project-local agents in ${CONFIG_DIR_NAME}/agents, set agentScope: "both" or "project".`,
280
309
  ].join(" "),
281
310
  parameters: SubagentParams,
311
+ renderShell: "self",
282
312
  promptSnippet: "Delegate tasks to specialized sub-agents (scout, reviewer, worker, general-purpose)",
283
313
  promptGuidelines: [
284
314
  "Use subagent to delegate work that would flood the main context with search results or file contents.",
@@ -481,6 +511,22 @@ export default function (pi: ExtensionAPI) {
481
511
  mode: "chain-step",
482
512
  toolCallId: _toolCallId,
483
513
  });
514
+ // Publish the active step before awaiting it so chain mode shows
515
+ // the running agent's gradient instead of an empty group header.
516
+ results.push({
517
+ agent: step.agent,
518
+ task: taskWithContext,
519
+ exitCode: -1,
520
+ messages: [],
521
+ stderr: "",
522
+ usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 },
523
+ });
524
+ if (onUpdate) {
525
+ onUpdate({
526
+ content: [{ type: "text", text: "Running..." }],
527
+ details: makeDetails("chain")(results),
528
+ });
529
+ }
484
530
  const result = await runOne(
485
531
  step.agent, taskWithContext, step.cwd,
486
532
  signal, step.timeout ?? params.timeout,
@@ -490,7 +536,7 @@ export default function (pi: ExtensionAPI) {
490
536
  status: isFailedResult(result) ? (result.stopReason === "aborted" ? "aborted" : "failed") : "completed",
491
537
  result,
492
538
  });
493
- results.push(result);
539
+ results[i] = result;
494
540
 
495
541
  const isError = isFailedResult(result);
496
542
  if (isError) {
@@ -746,239 +792,85 @@ export default function (pi: ExtensionAPI) {
746
792
  },
747
793
 
748
794
  // ------------------------------------------------------------------
749
- // TUI rendering
795
+ // TUI rendering — compact grouped layout (Exploring-style)
750
796
  // ------------------------------------------------------------------
751
797
 
752
- renderCall(args, theme, _context) {
753
- const scope: AgentScope = args.agentScope ?? "user";
754
- const fg = theme.fg.bind(theme);
755
-
756
- // Chain
757
- if (args.chain && args.chain.length > 0) {
758
- let text =
759
- fg("muted", "• ") +
760
- fg("toolTitle", theme.bold("subagent ")) +
761
- fg("accent", `chain (${args.chain.length} steps)`) +
762
- fg("muted", ` [${scope}]`);
763
- for (let i = 0; i < Math.min(args.chain.length, 3); i++) {
764
- const step = args.chain[i];
765
- const cleanTask = step.task.replace(/\{previous\}/g, "").trim();
766
- const preview = cleanTask.length > 40 ? `${cleanTask.slice(0, 40)}...` : cleanTask;
767
- text +=
768
- "\n " +
769
- fg("muted", `${i + 1}.`) +
770
- " " +
771
- fg("accent", step.agent) +
772
- fg("dim", ` ${preview}`);
773
- }
774
- if (args.chain.length > 3)
775
- text += `\n ${fg("muted", `... +${args.chain.length - 3} more`)}`;
776
- return new Text(text, 0, 0);
798
+ renderCall(args, theme, context) {
799
+ // The cap is a full-width sibling above the padded subagent box.
800
+ // Its render(width) reads the live viewport width and visibility state.
801
+ let shell = context.state.shell;
802
+ if (!(shell instanceof Container)) {
803
+ shell = new Container();
804
+ context.state.shell = shell;
777
805
  }
806
+ shell.clear();
778
807
 
779
- // Parallel
780
- if (args.tasks && args.tasks.length > 0) {
781
- let text =
782
- fg("muted", "• ") +
783
- fg("toolTitle", theme.bold("subagent ")) +
784
- fg("accent", `parallel (${args.tasks.length} tasks)`) +
785
- fg("muted", ` [${scope}]`);
786
- for (const t of args.tasks.slice(0, 3)) {
787
- const preview = t.task.length > 40 ? `${t.task.slice(0, 40)}...` : t.task;
788
- text += `\n ${fg("accent", t.agent)}${fg("dim", ` ${preview}`)}`;
789
- }
790
- if (args.tasks.length > 3)
791
- text += `\n ${fg("muted", `... +${args.tasks.length - 3} more`)}`;
792
- return new Text(text, 0, 0);
808
+ let capLine = context.state.capLine;
809
+ if (!(capLine instanceof SubagentCapLine)) {
810
+ capLine = new SubagentCapLine(
811
+ () => isLatestSubagentRunning(),
812
+ (theme.fg as any).bind(theme),
813
+ );
814
+ context.state.capLine = capLine;
815
+ } else {
816
+ capLine.setForeground((theme.fg as any).bind(theme));
793
817
  }
818
+ shell.addChild(capLine);
794
819
 
795
- // Single
796
- const agentName = args.agent || "...";
797
- const preview = args.task
798
- ? args.task.length > 60
799
- ? `${args.task.slice(0, 60)}...`
800
- : args.task
801
- : "...";
802
- let text =
803
- fg("muted", "• ") +
804
- fg("toolTitle", theme.bold("subagent ")) +
805
- fg("accent", agentName) +
806
- fg("muted", ` [${scope}]`);
807
- text += `\n ${fg("dim", preview)}`;
808
- return new Text(text, 0, 0);
820
+ // Reuse or create the self-rendered Box with the subagentBg token.
821
+ let box = context.state.box;
822
+ if (!(box instanceof Box)) {
823
+ box = new Box(1, 0);
824
+ context.state.box = box;
825
+ }
826
+ box.setBgFn((s: string) => (theme.bg as any)("subagentBg", s));
827
+ box.clear();
828
+
829
+ // Reuse or create the single Text child that carries the layout.
830
+ let callText = context.state.callText;
831
+ if (!(callText instanceof Text)) {
832
+ callText = new Text("", 0, 0);
833
+ context.state.callText = callText;
834
+ }
835
+ const results = context.state.results ?? [];
836
+ callText.setText(renderSubagentLayout(args, results, theme));
837
+ box.addChild(callText);
838
+ shell.addChild(box);
839
+
840
+ // Flash while any agent is running.
841
+ update_subagent_pulse(context, anySubagentRunning(args, results));
842
+ return shell;
809
843
  },
810
844
 
811
- renderResult(result, { expanded }, theme, _context) {
845
+ renderResult(result, { expanded }, theme, context) {
812
846
  const details = result.details as SubagentDetails | undefined;
813
- if (!details || details.results.length === 0) {
814
- const text = result.content[0];
815
- return new Text(text?.type === "text" ? text.text : "(no output)", 0, 0);
847
+ const results = details?.results ?? [];
848
+ context.state.results = results;
849
+ update_subagent_pulse(context, details ? anySubagentRunning(context.args, results) : false);
850
+ if (!details) {
851
+ const outputBlock = result.content.find((item: any) => item.type === "text");
852
+ const output = outputBlock?.type === "text" ? outputBlock.text : "(no output)";
853
+ return new Text(output, 0, 0);
816
854
  }
817
855
 
818
- const fg = theme.fg.bind(theme);
819
- const mdTheme = getMarkdownTheme();
820
-
821
- // --- Single ---
822
- if (details.mode === "single" && details.results.length === 1) {
823
- return renderSingleResult(details.results[0], expanded, theme);
824
- }
825
-
826
- // --- Chain ---
827
- if (details.mode === "chain") {
828
- const successCount = details.results.filter((r) => !isFailedResult(r)).length;
829
- const icon =
830
- successCount === details.results.length
831
- ? fg("success", "✓")
832
- : fg("error", "✗");
833
-
834
- if (expanded) {
835
- const container = new Container();
836
- container.addChild(
837
- new Text(
838
- icon +
839
- " " +
840
- fg("toolTitle", theme.bold("chain ")) +
841
- fg("accent", `${successCount}/${details.results.length} steps`),
842
- 0,
843
- 0,
844
- ),
845
- );
846
- for (const r of details.results) {
847
- container.addChild(new Spacer(1));
848
- const stepIcon = isFailedResult(r) ? fg("error", "✗") : fg("success", "✓");
849
- container.addChild(
850
- new Text(
851
- fg("muted", `─── Step ${r.exitCode !== -1 ? "" : "?"}: `) +
852
- fg("accent", r.agent) +
853
- ` ${stepIcon}`,
854
- 0,
855
- 0,
856
- ),
857
- );
858
- if (r.errorMessage) {
859
- container.addChild(
860
- new Text(fg("error", `Error: ${r.errorMessage}`), 0, 0),
861
- );
862
- }
863
- const finalOutput = getResultOutput(r);
864
- if (finalOutput) {
865
- container.addChild(new Spacer(1));
866
- container.addChild(new Markdown(finalOutput.trim(), 0, 0, mdTheme));
867
- }
868
- const usageStr = formatUsageStats(r.usage, r.model);
869
- if (usageStr)
870
- container.addChild(new Text(fg("dim", usageStr), 0, 0));
871
- }
872
- const totalUsage = formatUsageStats(aggregateUsage(details.results));
873
- if (totalUsage) {
874
- container.addChild(new Spacer(1));
875
- container.addChild(new Text(fg("dim", `Total: ${totalUsage}`), 0, 0));
876
- }
877
- return container;
878
- }
879
-
880
- let text =
881
- icon +
882
- " " +
883
- fg("toolTitle", theme.bold("chain ")) +
884
- fg("accent", `${successCount}/${details.results.length} steps`);
885
- for (const r of details.results) {
886
- const stepIcon = isFailedResult(r) ? fg("error", "✗") : fg("success", "✓");
887
- text += `\n ${stepIcon} ${fg("accent", r.agent)}`;
888
- }
889
- const totalUsage = formatUsageStats(aggregateUsage(details.results));
890
- if (totalUsage) text += `\n${fg("dim", totalUsage)}`;
891
- text += `\n${fg("muted", "(Ctrl+O to expand)")}`;
892
- return new Text(text, 0, 0);
856
+ // Update the call-slot Text with the latest statuses. renderResult
857
+ // runs after renderCall in updateDisplay, so this wins the paint.
858
+ const callText = context.state.callText;
859
+ if (callText instanceof Text) {
860
+ callText.setText(renderSubagentLayout(context.args, results, theme));
893
861
  }
894
862
 
895
- // --- Parallel ---
896
- if (details.mode === "parallel") {
897
- const running = details.results.filter((r) => r.exitCode === -1).length;
898
- const successCount = details.results.filter(
899
- (r) => r.exitCode !== -1 && !isFailedResult(r),
900
- ).length;
901
- const failCount = details.results.filter(
902
- (r) => r.exitCode !== -1 && isFailedResult(r),
903
- ).length;
904
- const isRunning = running > 0;
905
- const icon = isRunning
906
- ? fg("warning", "⏳")
907
- : failCount > 0
908
- ? fg("warning", "◐")
909
- : fg("success", "✓");
910
- const status = isRunning
911
- ? `${successCount + failCount}/${details.results.length} done, ${running} running`
912
- : `${successCount}/${details.results.length} tasks`;
913
-
914
- if (expanded && !isRunning) {
915
- const container = new Container();
916
- container.addChild(
917
- new Text(
918
- `${icon} ${fg("toolTitle", theme.bold("parallel "))}${fg("accent", status)}`,
919
- 0,
920
- 0,
921
- ),
922
- );
923
- for (const r of details.results) {
924
- container.addChild(new Spacer(1));
925
- const taskIcon = isFailedResult(r)
926
- ? fg("error", "✗")
927
- : fg("success", "✓");
928
- container.addChild(
929
- new Text(
930
- fg("muted", "─── ") + fg("accent", r.agent) + ` ${taskIcon}`,
931
- 0,
932
- 0,
933
- ),
934
- );
935
- container.addChild(
936
- new Text(fg("muted", "Task: ") + fg("dim", r.task), 0, 0),
937
- );
938
- if (r.errorMessage) {
939
- container.addChild(
940
- new Text(fg("error", `Error: ${r.errorMessage}`), 0, 0),
941
- );
942
- }
943
- const finalOutput = getResultOutput(r);
944
- if (finalOutput) {
945
- container.addChild(new Spacer(1));
946
- container.addChild(
947
- new Markdown(finalOutput.trim(), 0, 0, mdTheme),
948
- );
949
- }
950
- const taskUsage = formatUsageStats(r.usage, r.model);
951
- if (taskUsage)
952
- container.addChild(new Text(fg("dim", taskUsage), 0, 0));
953
- }
954
- const totalUsage = formatUsageStats(aggregateUsage(details.results));
955
- if (totalUsage) {
956
- container.addChild(new Spacer(1));
957
- container.addChild(new Text(fg("dim", `Total: ${totalUsage}`), 0, 0));
958
- }
959
- return container;
960
- }
863
+ const isRunning = anySubagentRunning(context.args, results);
961
864
 
962
- let text = `${icon} ${fg("toolTitle", theme.bold("parallel "))}${fg("accent", status)}`;
963
- for (const r of details.results) {
964
- const taskIcon =
965
- r.exitCode === -1
966
- ? fg("warning", "⏳")
967
- : isFailedResult(r)
968
- ? fg("error", "✗")
969
- : fg("success", "✓");
970
- text += `\n ${taskIcon} ${fg("accent", r.agent)}`;
971
- }
972
- if (!isRunning) {
973
- const totalUsage = formatUsageStats(aggregateUsage(details.results));
974
- if (totalUsage) text += `\n${fg("dim", totalUsage)}`;
975
- }
976
- if (!expanded) text += `\n${fg("muted", "(Ctrl+O to expand)")}`;
977
- return new Text(text, 0, 0);
865
+ // Expanded view (Ctrl+O): detailed per-agent output, wrapped in
866
+ // the subagentBg Box so it stays visually integrated.
867
+ if (expanded && !isRunning && details && details.results.length > 0) {
868
+ const expandedContent = renderSubagentExpanded(details, theme);
869
+ if (expandedContent) return expandedContent;
978
870
  }
979
871
 
980
- const fallback = result.content[0];
981
- return new Text(fallback?.type === "text" ? fallback.text : "(no output)", 0, 0);
872
+ // Collapsed: the call-slot Box is the single visible component.
873
+ return new Text("", 0, 0);
982
874
  },
983
875
  });
984
876
  // /agent command — switch between subagent threads.