@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.
- package/README.md +83 -83
- package/package.json +62 -48
- package/plugins/devin-auth/extensions/index.ts +68 -7
- package/plugins/devin-auth/src/cloud-direct/auth.ts +246 -246
- package/plugins/devin-auth/src/cloud-direct/catalog.ts +246 -246
- package/plugins/devin-auth/src/cloud-direct/chat.ts +1096 -1091
- package/plugins/devin-auth/src/cloud-direct/index.ts +41 -41
- package/plugins/devin-auth/src/cloud-direct/metadata.ts +78 -78
- package/plugins/devin-auth/src/cloud-direct/wire.ts +202 -202
- package/plugins/devin-auth/src/models.ts +42 -196
- package/plugins/devin-auth/src/oauth/register-user.ts +174 -174
- package/plugins/devin-auth/src/oauth/types.ts +71 -71
- package/plugins/devin-auth/src/stream.ts +1 -1
- package/plugins/index.ts +29 -6
- package/plugins/pi-compact-tools/index.ts +35 -192
- package/plugins/pi-compact-tools/renderer.ts +589 -0
- package/plugins/pi-custom-agents/index.ts +727 -373
- package/plugins/pi-custom-agents/questionnaire-tool.ts +14 -5
- package/plugins/pi-custom-agents/subagent/agents/coder.md +1 -0
- package/plugins/pi-custom-agents/subagent/agents/scout.md +16 -37
- package/plugins/pi-custom-agents/subagent/extensions/agents.ts +18 -1
- package/plugins/pi-custom-agents/subagent/extensions/index.ts +118 -226
- package/plugins/pi-custom-agents/subagent/extensions/model.ts +96 -96
- package/plugins/pi-custom-agents/subagent/extensions/render.ts +205 -1
- package/plugins/pi-custom-agents/subagent/extensions/runner.ts +260 -170
- package/plugins/pi-custom-agents/subagent/extensions/test/render.test.ts +145 -0
- package/plugins/pi-ember-fff/index.ts +975 -0
- package/plugins/pi-ember-fff/query.ts +247 -0
- package/plugins/pi-ember-fff/test/query.test.ts +222 -0
- package/plugins/pi-ember-fff/test/renderer.test.ts +727 -0
- package/plugins/pi-ember-tps/index.ts +144 -0
- package/plugins/pi-ember-ui/ember.json +99 -0
- package/plugins/pi-ember-ui/index.ts +805 -0
- package/plugins/pi-ember-ui/mode-colors.ts +196 -0
- package/tsconfig.json +2 -1
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
9
|
+
You are a file search specialist. You excel at thoroughly navigating and exploring codebases.
|
|
9
10
|
|
|
10
|
-
Your
|
|
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
|
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
|
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 (!
|
|
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
|
|
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,
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
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
|
-
|
|
780
|
-
if (
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
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
|
-
//
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
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,
|
|
845
|
+
renderResult(result, { expanded }, theme, context) {
|
|
812
846
|
const details = result.details as SubagentDetails | undefined;
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
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
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
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
|
-
|
|
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
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
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
|
-
|
|
981
|
-
return new Text(
|
|
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.
|