@nmzpy/pi-ember-stack 0.2.1 → 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 +63 -59
- package/plugins/devin-auth/extensions/index.ts +23 -0
- 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/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/pi-compact-tools/index.ts +19 -1
- package/plugins/pi-compact-tools/renderer.ts +231 -61
- package/plugins/pi-custom-agents/index.ts +310 -102
- package/plugins/pi-custom-agents/questionnaire-tool.ts +14 -5
- package/plugins/pi-custom-agents/subagent/extensions/agents.ts +18 -1
- package/plugins/pi-custom-agents/subagent/extensions/index.ts +116 -224
- 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 +241 -177
- package/plugins/pi-custom-agents/subagent/extensions/test/render.test.ts +145 -0
- package/plugins/pi-ember-fff/index.ts +275 -17
- package/plugins/pi-ember-fff/query.ts +170 -10
- package/plugins/pi-ember-fff/test/query.test.ts +157 -1
- package/plugins/pi-ember-fff/test/renderer.test.ts +367 -16
- package/plugins/pi-ember-tps/index.ts +27 -5
- package/plugins/pi-ember-ui/ember.json +3 -0
- package/plugins/pi-ember-ui/index.ts +223 -36
- package/plugins/pi-ember-ui/mode-colors.ts +36 -8
|
@@ -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,11 +141,34 @@ 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
|
|
@@ -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.
|
|
@@ -1,96 +1,96 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared model resolution for pi-subagent.
|
|
3
|
-
*
|
|
4
|
-
* Provides a single canonical resolveModel() used by both the tool handler
|
|
5
|
-
* (index.ts) and the event-driven service path (service.ts), ensuring
|
|
6
|
-
* consistent error reporting across all sub-agent invocation paths.
|
|
7
|
-
*
|
|
8
|
-
* Queries the parent ModelRegistry first (catches custom-configured models
|
|
9
|
-
* with overridden base URLs, headers, compatibility settings). Falls back
|
|
10
|
-
* to the built-in registry for unconfigured models.
|
|
11
|
-
* For unqualified names (no provider prefix), known naming conventions
|
|
12
|
-
* are tried before assuming Anthropic.
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
import type { Model } from "@earendil-works/pi-ai";
|
|
16
|
-
import type { ModelRegistry } from "@earendil-works/pi-coding-agent";
|
|
17
|
-
|
|
18
|
-
type BuiltInModelResolver = (provider: string, id: string) => Model<any> | undefined;
|
|
19
|
-
|
|
20
|
-
interface ModelModule {
|
|
21
|
-
getModel: BuiltInModelResolver;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
// Pi 0.80 exposes getModel from /compat; 0.79 exported it from the main entry.
|
|
25
|
-
const { getModel } = (await import("@earendil-works/pi-ai/compat").catch(
|
|
26
|
-
() => import("@earendil-works/pi-ai"),
|
|
27
|
-
)) as ModelModule;
|
|
28
|
-
|
|
29
|
-
export interface ResolvedModel {
|
|
30
|
-
model: Model<any> | null;
|
|
31
|
-
attempted: string[];
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/** Known provider prefixes for unqualified model names. */
|
|
35
|
-
const KNOWN_PROVIDERS: [string, RegExp][] = [
|
|
36
|
-
["openai", /^gpt-/i],
|
|
37
|
-
["anthropic", /^claude-/i],
|
|
38
|
-
["google", /^gemini-/i],
|
|
39
|
-
["cohere", /^command-/i],
|
|
40
|
-
["deepseek", /^(deepseek-|ds-)/i],
|
|
41
|
-
["mistral", /^mistral-/i],
|
|
42
|
-
["groq", /^(groq-|llama-)/i],
|
|
43
|
-
];
|
|
44
|
-
|
|
45
|
-
function tryGetModel(
|
|
46
|
-
provider: string,
|
|
47
|
-
id: string,
|
|
48
|
-
modelRegistry?: ModelRegistry,
|
|
49
|
-
): Model<any> | null {
|
|
50
|
-
// Query parent ModelRegistry first — it includes custom-configured models
|
|
51
|
-
// (overridden base URLs, headers, compatibility settings, per-model overrides).
|
|
52
|
-
// Fall back to built-in registry for unconfigured models.
|
|
53
|
-
if (modelRegistry) {
|
|
54
|
-
const found = modelRegistry.find(provider as any, id as any) ?? null;
|
|
55
|
-
if (found) return found;
|
|
56
|
-
}
|
|
57
|
-
const builtIn = getModel(provider as any, id as any) ?? null;
|
|
58
|
-
if (builtIn) return builtIn;
|
|
59
|
-
return null;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export function resolveModel(
|
|
63
|
-
modelName: string | undefined,
|
|
64
|
-
parentModel: Model<any> | undefined,
|
|
65
|
-
modelRegistry?: ModelRegistry,
|
|
66
|
-
): ResolvedModel {
|
|
67
|
-
const attempted: string[] = [];
|
|
68
|
-
if (modelName) {
|
|
69
|
-
const idx = modelName.indexOf("/");
|
|
70
|
-
if (idx > 0) {
|
|
71
|
-
// Provider-qualified: "openai/gpt-4o" or "openrouter/anthropic/claude-3.5"
|
|
72
|
-
const provider = modelName.slice(0, idx);
|
|
73
|
-
const id = modelName.slice(idx + 1);
|
|
74
|
-
attempted.push(modelName);
|
|
75
|
-
const found = tryGetModel(provider, id, modelRegistry);
|
|
76
|
-
if (found) return { model: found, attempted };
|
|
77
|
-
} else {
|
|
78
|
-
// Unqualified: try known providers by naming convention
|
|
79
|
-
for (const [provider, pattern] of KNOWN_PROVIDERS) {
|
|
80
|
-
if (pattern.test(modelName)) {
|
|
81
|
-
attempted.push(`${provider}/${modelName}`);
|
|
82
|
-
const found = tryGetModel(provider, modelName, modelRegistry);
|
|
83
|
-
if (found) return { model: found, attempted };
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
// Fall back to Anthropic shorthand (backward compat)
|
|
87
|
-
attempted.push(`anthropic/${modelName}`);
|
|
88
|
-
const found = tryGetModel("anthropic", modelName, modelRegistry);
|
|
89
|
-
if (found) return { model: found, attempted };
|
|
90
|
-
}
|
|
91
|
-
} else if (parentModel) {
|
|
92
|
-
attempted.push(`${parentModel.provider}/${parentModel.id}`);
|
|
93
|
-
return { model: parentModel, attempted };
|
|
94
|
-
}
|
|
95
|
-
return { model: null, attempted };
|
|
96
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Shared model resolution for pi-subagent.
|
|
3
|
+
*
|
|
4
|
+
* Provides a single canonical resolveModel() used by both the tool handler
|
|
5
|
+
* (index.ts) and the event-driven service path (service.ts), ensuring
|
|
6
|
+
* consistent error reporting across all sub-agent invocation paths.
|
|
7
|
+
*
|
|
8
|
+
* Queries the parent ModelRegistry first (catches custom-configured models
|
|
9
|
+
* with overridden base URLs, headers, compatibility settings). Falls back
|
|
10
|
+
* to the built-in registry for unconfigured models.
|
|
11
|
+
* For unqualified names (no provider prefix), known naming conventions
|
|
12
|
+
* are tried before assuming Anthropic.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { Model } from "@earendil-works/pi-ai";
|
|
16
|
+
import type { ModelRegistry } from "@earendil-works/pi-coding-agent";
|
|
17
|
+
|
|
18
|
+
type BuiltInModelResolver = (provider: string, id: string) => Model<any> | undefined;
|
|
19
|
+
|
|
20
|
+
interface ModelModule {
|
|
21
|
+
getModel: BuiltInModelResolver;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Pi 0.80 exposes getModel from /compat; 0.79 exported it from the main entry.
|
|
25
|
+
const { getModel } = (await import("@earendil-works/pi-ai/compat").catch(
|
|
26
|
+
() => import("@earendil-works/pi-ai"),
|
|
27
|
+
)) as ModelModule;
|
|
28
|
+
|
|
29
|
+
export interface ResolvedModel {
|
|
30
|
+
model: Model<any> | null;
|
|
31
|
+
attempted: string[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Known provider prefixes for unqualified model names. */
|
|
35
|
+
const KNOWN_PROVIDERS: [string, RegExp][] = [
|
|
36
|
+
["openai", /^gpt-/i],
|
|
37
|
+
["anthropic", /^claude-/i],
|
|
38
|
+
["google", /^gemini-/i],
|
|
39
|
+
["cohere", /^command-/i],
|
|
40
|
+
["deepseek", /^(deepseek-|ds-)/i],
|
|
41
|
+
["mistral", /^mistral-/i],
|
|
42
|
+
["groq", /^(groq-|llama-)/i],
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
function tryGetModel(
|
|
46
|
+
provider: string,
|
|
47
|
+
id: string,
|
|
48
|
+
modelRegistry?: ModelRegistry,
|
|
49
|
+
): Model<any> | null {
|
|
50
|
+
// Query parent ModelRegistry first — it includes custom-configured models
|
|
51
|
+
// (overridden base URLs, headers, compatibility settings, per-model overrides).
|
|
52
|
+
// Fall back to built-in registry for unconfigured models.
|
|
53
|
+
if (modelRegistry) {
|
|
54
|
+
const found = modelRegistry.find(provider as any, id as any) ?? null;
|
|
55
|
+
if (found) return found;
|
|
56
|
+
}
|
|
57
|
+
const builtIn = getModel(provider as any, id as any) ?? null;
|
|
58
|
+
if (builtIn) return builtIn;
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function resolveModel(
|
|
63
|
+
modelName: string | undefined,
|
|
64
|
+
parentModel: Model<any> | undefined,
|
|
65
|
+
modelRegistry?: ModelRegistry,
|
|
66
|
+
): ResolvedModel {
|
|
67
|
+
const attempted: string[] = [];
|
|
68
|
+
if (modelName) {
|
|
69
|
+
const idx = modelName.indexOf("/");
|
|
70
|
+
if (idx > 0) {
|
|
71
|
+
// Provider-qualified: "openai/gpt-4o" or "openrouter/anthropic/claude-3.5"
|
|
72
|
+
const provider = modelName.slice(0, idx);
|
|
73
|
+
const id = modelName.slice(idx + 1);
|
|
74
|
+
attempted.push(modelName);
|
|
75
|
+
const found = tryGetModel(provider, id, modelRegistry);
|
|
76
|
+
if (found) return { model: found, attempted };
|
|
77
|
+
} else {
|
|
78
|
+
// Unqualified: try known providers by naming convention
|
|
79
|
+
for (const [provider, pattern] of KNOWN_PROVIDERS) {
|
|
80
|
+
if (pattern.test(modelName)) {
|
|
81
|
+
attempted.push(`${provider}/${modelName}`);
|
|
82
|
+
const found = tryGetModel(provider, modelName, modelRegistry);
|
|
83
|
+
if (found) return { model: found, attempted };
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// Fall back to Anthropic shorthand (backward compat)
|
|
87
|
+
attempted.push(`anthropic/${modelName}`);
|
|
88
|
+
const found = tryGetModel("anthropic", modelName, modelRegistry);
|
|
89
|
+
if (found) return { model: found, attempted };
|
|
90
|
+
}
|
|
91
|
+
} else if (parentModel) {
|
|
92
|
+
attempted.push(`${parentModel.provider}/${parentModel.id}`);
|
|
93
|
+
return { model: parentModel, attempted };
|
|
94
|
+
}
|
|
95
|
+
return { model: null, attempted };
|
|
96
|
+
}
|