@dungle-scrubs/tallow 0.8.26 → 0.8.28
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 +42 -1
- package/dist/cli.js +7 -1
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +1 -1
- package/dist/config.js +1 -1
- package/dist/interactive-mode-patch.d.ts +1 -0
- package/dist/interactive-mode-patch.d.ts.map +1 -1
- package/dist/interactive-mode-patch.js +40 -1
- package/dist/interactive-mode-patch.js.map +1 -1
- package/dist/model-metadata-overrides.d.ts +2 -5
- package/dist/model-metadata-overrides.d.ts.map +1 -1
- package/dist/model-metadata-overrides.js +23 -12
- package/dist/model-metadata-overrides.js.map +1 -1
- package/dist/pid-manager.d.ts +2 -9
- package/dist/pid-manager.d.ts.map +1 -1
- package/dist/pid-manager.js +1 -58
- package/dist/pid-manager.js.map +1 -1
- package/dist/pid-schema.d.ts +51 -0
- package/dist/pid-schema.d.ts.map +1 -0
- package/dist/pid-schema.js +70 -0
- package/dist/pid-schema.js.map +1 -0
- package/dist/sdk.d.ts.map +1 -1
- package/dist/sdk.js +24 -17
- package/dist/sdk.js.map +1 -1
- package/dist/workspace-transition-interactive.d.ts.map +1 -1
- package/dist/workspace-transition-interactive.js +53 -3
- package/dist/workspace-transition-interactive.js.map +1 -1
- package/dist/workspace-transition.d.ts +2 -1
- package/dist/workspace-transition.d.ts.map +1 -1
- package/dist/workspace-transition.js +16 -4
- package/dist/workspace-transition.js.map +1 -1
- package/extensions/__integration__/audit-findings.test.ts +309 -0
- package/extensions/__integration__/cd-tool-guidelines.test.ts +46 -0
- package/extensions/__integration__/tasks-runtime.test.ts +63 -12
- package/extensions/__integration__/welcome-screen.test.ts +240 -0
- package/extensions/_shared/lazy-init.ts +88 -3
- package/extensions/_shared/pid-registry.ts +8 -82
- package/extensions/background-task-tool/index.ts +1 -1
- package/extensions/cd-tool/index.ts +4 -1
- package/extensions/cheatsheet/__tests__/cheatsheet.test.ts +47 -0
- package/extensions/clear/__tests__/clear.test.ts +38 -0
- package/extensions/edit-tool-enhanced/index.ts +3 -1
- package/extensions/git-status/__tests__/git-status.test.ts +32 -0
- package/extensions/health/__tests__/diagnostics.test.ts +25 -0
- package/extensions/health/index.ts +61 -0
- package/extensions/loop/__tests__/loop.test.ts +365 -1
- package/extensions/loop/index.ts +213 -3
- package/extensions/mcp-adapter-tool/index.ts +1 -1
- package/extensions/minimal-skill-display/__tests__/minimal-skill-display.test.ts +20 -0
- package/extensions/permissions/__tests__/permissions.test.ts +213 -0
- package/extensions/progress-indicator/__tests__/progress-indicator.test.ts +104 -0
- package/extensions/prompt-suggestions/__tests__/autocomplete.test.ts +111 -3
- package/extensions/prompt-suggestions/autocomplete.ts +23 -5
- package/extensions/prompt-suggestions/index.ts +62 -3
- package/extensions/random-spinner/__tests__/random-spinner.test.ts +35 -0
- package/extensions/read-tool-enhanced/index.ts +5 -1
- package/extensions/session-memory/index.ts +1 -1
- package/extensions/session-namer/index.ts +1 -1
- package/extensions/show-system-prompt/__tests__/show-system-prompt.test.ts +51 -0
- package/extensions/subagent-tool/__tests__/presentation-rendering.test.ts +9 -8
- package/extensions/subagent-tool/__tests__/process-liveness.test.ts +51 -0
- package/extensions/subagent-tool/__tests__/subprocess-args.test.ts +120 -0
- package/extensions/subagent-tool/formatting.ts +2 -0
- package/extensions/subagent-tool/index.ts +160 -97
- package/extensions/subagent-tool/process.ts +152 -40
- package/extensions/tasks/commands/register-tasks-extension.ts +64 -20
- package/extensions/tasks/extension.json +1 -0
- package/extensions/tasks/index.ts +2 -12
- package/extensions/tasks/state/index.ts +26 -0
- package/extensions/teams-tool/dashboard.ts +13 -1
- package/extensions/teams-tool/sessions/spawn.ts +2 -2
- package/extensions/teams-tool/tools/register-extension.ts +10 -2
- package/extensions/upstream-check/__tests__/upstream-check.test.ts +49 -0
- package/extensions/welcome-screen/__tests__/welcome-screen.test.ts +35 -0
- package/extensions/welcome-screen/extension.json +20 -0
- package/extensions/welcome-screen/index.ts +189 -0
- package/extensions/wezterm-notify/__tests__/index.test.ts +49 -11
- package/extensions/wezterm-notify/index.ts +5 -3
- package/extensions/write-tool-enhanced/__tests__/write-tool-enhanced.test.ts +296 -0
- package/node_modules/@mariozechner/pi-tui/dist/index.d.ts +2 -2
- package/node_modules/@mariozechner/pi-tui/dist/index.d.ts.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/index.js +2 -2
- package/node_modules/@mariozechner/pi-tui/dist/index.js.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/keybindings.d.ts +309 -25
- package/node_modules/@mariozechner/pi-tui/dist/keybindings.d.ts.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/keybindings.js +392 -72
- package/node_modules/@mariozechner/pi-tui/dist/keybindings.js.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/keys.d.ts +30 -0
- package/node_modules/@mariozechner/pi-tui/dist/keys.d.ts.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/keys.js +50 -6
- package/node_modules/@mariozechner/pi-tui/dist/keys.js.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/terminal.d.ts +27 -0
- package/node_modules/@mariozechner/pi-tui/dist/terminal.d.ts.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/terminal.js +59 -4
- package/node_modules/@mariozechner/pi-tui/dist/terminal.js.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/tui.d.ts +9 -0
- package/node_modules/@mariozechner/pi-tui/dist/tui.d.ts.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/tui.js +50 -1
- package/node_modules/@mariozechner/pi-tui/dist/tui.js.map +1 -1
- package/node_modules/@mariozechner/pi-tui/package.json +1 -1
- package/node_modules/@mariozechner/pi-tui/src/__tests__/mouse-events.test.ts +134 -0
- package/node_modules/@mariozechner/pi-tui/src/__tests__/tmux-compat.test.ts +204 -0
- package/node_modules/@mariozechner/pi-tui/src/__tests__/tui-diff-regression.test.ts +49 -0
- package/node_modules/@mariozechner/pi-tui/src/__tests__/tui-render-scheduling.test.ts +2 -0
- package/node_modules/@mariozechner/pi-tui/src/index.ts +11 -0
- package/node_modules/@mariozechner/pi-tui/src/keybindings.ts +478 -140
- package/node_modules/@mariozechner/pi-tui/src/keys.ts +84 -6
- package/node_modules/@mariozechner/pi-tui/src/terminal.ts +69 -4
- package/node_modules/@mariozechner/pi-tui/src/tui.ts +64 -1
- package/package.json +11 -10
- package/runtime/config.ts +7 -0
- package/runtime/model-metadata-overrides.ts +7 -0
- package/runtime/pid-schema.ts +13 -0
- package/skills/tallow-expert/SKILL.md +7 -5
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import { buildSubprocessArgs, type SubprocessArgsOptions } from "../process.js";
|
|
3
|
+
|
|
4
|
+
describe("buildSubprocessArgs", () => {
|
|
5
|
+
/** Minimal options — no session, no model, just a task. */
|
|
6
|
+
const minimal: SubprocessArgsOptions = { task: "do something" };
|
|
7
|
+
|
|
8
|
+
it("always places -p as the last flag, right before the task text", () => {
|
|
9
|
+
const args = buildSubprocessArgs(minimal);
|
|
10
|
+
const pIdx = args.lastIndexOf("-p");
|
|
11
|
+
expect(pIdx).toBeGreaterThanOrEqual(0);
|
|
12
|
+
expect(pIdx).toBe(args.length - 2);
|
|
13
|
+
expect(args[pIdx + 1]).toBe("Task: do something");
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("uses --no-session when session is omitted", () => {
|
|
17
|
+
const args = buildSubprocessArgs(minimal);
|
|
18
|
+
expect(args).toContain("--no-session");
|
|
19
|
+
expect(args).not.toContain("--session");
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("uses --session <id> when session is provided", () => {
|
|
23
|
+
const args = buildSubprocessArgs({ ...minimal, session: "my-session" });
|
|
24
|
+
expect(args).toContain("--session");
|
|
25
|
+
expect(args[args.indexOf("--session") + 1]).toBe("my-session");
|
|
26
|
+
expect(args).not.toContain("--no-session");
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("--no-session is never consumed as -p value (regression: 86a8d26e)", () => {
|
|
30
|
+
// The original bug: -p was placed before --no-session, so Commander
|
|
31
|
+
// treated "--no-session" as the prompt text and the real task became
|
|
32
|
+
// a stray positional argument → "too many arguments".
|
|
33
|
+
const args = buildSubprocessArgs(minimal);
|
|
34
|
+
const pIdx = args.indexOf("-p");
|
|
35
|
+
const noSessionIdx = args.indexOf("--no-session");
|
|
36
|
+
// -p must come AFTER --no-session in the array
|
|
37
|
+
expect(pIdx).toBeGreaterThan(noSessionIdx);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("includes --model when modelDisplayName is provided", () => {
|
|
41
|
+
const args = buildSubprocessArgs({
|
|
42
|
+
...minimal,
|
|
43
|
+
modelDisplayName: "anthropic/claude-sonnet-4-6",
|
|
44
|
+
});
|
|
45
|
+
const mIdx = args.indexOf("--model");
|
|
46
|
+
expect(mIdx).toBeGreaterThanOrEqual(0);
|
|
47
|
+
expect(args[mIdx + 1]).toBe("anthropic/claude-sonnet-4-6");
|
|
48
|
+
// Still before -p
|
|
49
|
+
expect(mIdx).toBeLessThan(args.lastIndexOf("-p"));
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("includes --tools when tools are provided", () => {
|
|
53
|
+
const args = buildSubprocessArgs({ ...minimal, tools: ["read", "bash", "edit"] });
|
|
54
|
+
const tIdx = args.indexOf("--tools");
|
|
55
|
+
expect(tIdx).toBeGreaterThanOrEqual(0);
|
|
56
|
+
expect(args[tIdx + 1]).toBe("read,bash,edit");
|
|
57
|
+
expect(tIdx).toBeLessThan(args.lastIndexOf("-p"));
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("includes --skill for each skill", () => {
|
|
61
|
+
const args = buildSubprocessArgs({ ...minimal, skills: ["tdd", "git"] });
|
|
62
|
+
const firstSkill = args.indexOf("--skill");
|
|
63
|
+
expect(firstSkill).toBeGreaterThanOrEqual(0);
|
|
64
|
+
expect(args[firstSkill + 1]).toBe("tdd");
|
|
65
|
+
const secondSkill = args.indexOf("--skill", firstSkill + 1);
|
|
66
|
+
expect(secondSkill).toBeGreaterThanOrEqual(0);
|
|
67
|
+
expect(args[secondSkill + 1]).toBe("git");
|
|
68
|
+
// Both before -p
|
|
69
|
+
expect(secondSkill).toBeLessThan(args.lastIndexOf("-p"));
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("includes --append-system-prompt when path is provided", () => {
|
|
73
|
+
const args = buildSubprocessArgs({
|
|
74
|
+
...minimal,
|
|
75
|
+
systemPromptPath: "/tmp/prompt.md",
|
|
76
|
+
});
|
|
77
|
+
const sIdx = args.indexOf("--append-system-prompt");
|
|
78
|
+
expect(sIdx).toBeGreaterThanOrEqual(0);
|
|
79
|
+
expect(args[sIdx + 1]).toBe("/tmp/prompt.md");
|
|
80
|
+
expect(sIdx).toBeLessThan(args.lastIndexOf("-p"));
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("produces correct full arg array with all options", () => {
|
|
84
|
+
const args = buildSubprocessArgs({
|
|
85
|
+
session: "sess-123",
|
|
86
|
+
modelDisplayName: "openai/gpt-5",
|
|
87
|
+
tools: ["read", "write"],
|
|
88
|
+
skills: ["tdd"],
|
|
89
|
+
systemPromptPath: "/tmp/prompt.md",
|
|
90
|
+
task: "fix the tests",
|
|
91
|
+
});
|
|
92
|
+
expect(args).toEqual([
|
|
93
|
+
"--mode",
|
|
94
|
+
"json",
|
|
95
|
+
"--session",
|
|
96
|
+
"sess-123",
|
|
97
|
+
"--model",
|
|
98
|
+
"openai/gpt-5",
|
|
99
|
+
"--tools",
|
|
100
|
+
"read,write",
|
|
101
|
+
"--skill",
|
|
102
|
+
"tdd",
|
|
103
|
+
"--append-system-prompt",
|
|
104
|
+
"/tmp/prompt.md",
|
|
105
|
+
"-p",
|
|
106
|
+
"Task: fix the tests",
|
|
107
|
+
]);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("omits optional flags when not provided", () => {
|
|
111
|
+
const args = buildSubprocessArgs({ task: "hello" });
|
|
112
|
+
expect(args).toEqual(["--mode", "json", "--no-session", "-p", "Task: hello"]);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("always starts with --mode json", () => {
|
|
116
|
+
const args = buildSubprocessArgs(minimal);
|
|
117
|
+
expect(args[0]).toBe("--mode");
|
|
118
|
+
expect(args[1]).toBe("json");
|
|
119
|
+
});
|
|
120
|
+
});
|
|
@@ -37,6 +37,8 @@ export interface SingleResult {
|
|
|
37
37
|
stopReason?: string;
|
|
38
38
|
errorMessage?: string;
|
|
39
39
|
step?: number;
|
|
40
|
+
/** Timestamp (ms) when this subagent started executing. */
|
|
41
|
+
startTime?: number;
|
|
40
42
|
/** Tool names that were denied permission during execution. */
|
|
41
43
|
deniedTools?: string[];
|
|
42
44
|
}
|
|
@@ -50,6 +50,7 @@ import {
|
|
|
50
50
|
applyBackgroundResultRetention,
|
|
51
51
|
mapWithConcurrencyLimit,
|
|
52
52
|
type OnUpdateCallback,
|
|
53
|
+
resolveRetryPhaseTimeoutMs,
|
|
53
54
|
runSingleAgent,
|
|
54
55
|
setPiRef,
|
|
55
56
|
setTelemetryHandle,
|
|
@@ -181,6 +182,25 @@ export default function (pi: ExtensionAPI) {
|
|
|
181
182
|
}
|
|
182
183
|
});
|
|
183
184
|
|
|
185
|
+
// Kill all running background subagents on session shutdown (SIGTERM during user input).
|
|
186
|
+
// Unlike agent_end, this fires when the entire session is exiting — we don't need to
|
|
187
|
+
// retain results, just ensure orphaned subagent processes are terminated promptly.
|
|
188
|
+
pi.on("session_shutdown", async () => {
|
|
189
|
+
let mutated = false;
|
|
190
|
+
for (const [_id, bg] of backgroundSubagents) {
|
|
191
|
+
if (bg.status === "running" && bg.process && !bg.process.killed) {
|
|
192
|
+
bg.process.kill("SIGTERM");
|
|
193
|
+
bg.completedAt = Date.now();
|
|
194
|
+
bg.status = "failed";
|
|
195
|
+
bg.result.stopReason = "shutdown";
|
|
196
|
+
mutated = true;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
if (mutated) {
|
|
200
|
+
publishSubagentSnapshot(pi.events);
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
|
|
184
204
|
pi.registerTool({
|
|
185
205
|
name: "subagent",
|
|
186
206
|
label: "subagent",
|
|
@@ -674,6 +694,7 @@ async function executeParallel(
|
|
|
674
694
|
const allResults: SingleResult[] = new Array(tasks.length);
|
|
675
695
|
|
|
676
696
|
// Initialize placeholder results
|
|
697
|
+
const parallelStartTime = Date.now();
|
|
677
698
|
for (let i = 0; i < tasks.length; i++) {
|
|
678
699
|
allResults[i] = {
|
|
679
700
|
agent: tasks[i].agent,
|
|
@@ -692,6 +713,7 @@ async function executeParallel(
|
|
|
692
713
|
turns: 0,
|
|
693
714
|
denials: 0,
|
|
694
715
|
},
|
|
716
|
+
startTime: parallelStartTime,
|
|
695
717
|
};
|
|
696
718
|
}
|
|
697
719
|
|
|
@@ -788,12 +810,34 @@ async function executeParallel(
|
|
|
788
810
|
const retrySummaryLines: string[] = [];
|
|
789
811
|
const totalRetries = initialStalledIndexes.length;
|
|
790
812
|
|
|
813
|
+
// Cap the entire retry phase with a wall-clock timeout so N sequential
|
|
814
|
+
// retries × per-worker watchdog timeouts don't block the parent for 30+ min.
|
|
815
|
+
const retryPhaseTimeoutMs = resolveRetryPhaseTimeoutMs();
|
|
816
|
+
const retryAbort = new AbortController();
|
|
817
|
+
const retryTimer = setTimeout(() => retryAbort.abort(), retryPhaseTimeoutMs);
|
|
818
|
+
// Propagate parent abort to the retry phase.
|
|
819
|
+
const parentAbortHandler = signal ? () => retryAbort.abort() : undefined;
|
|
820
|
+
if (signal && parentAbortHandler) {
|
|
821
|
+
if (signal.aborted) retryAbort.abort();
|
|
822
|
+
else signal.addEventListener("abort", parentAbortHandler, { once: true });
|
|
823
|
+
}
|
|
824
|
+
const retrySignal = retryAbort.signal;
|
|
825
|
+
|
|
791
826
|
try {
|
|
792
827
|
ctx.ui.setWorkingMessage(
|
|
793
828
|
`Rerunning ${totalRetries} stalled worker${totalRetries === 1 ? "" : "s"} individually`
|
|
794
829
|
);
|
|
795
830
|
|
|
796
831
|
for (let retryIndex = 0; retryIndex < initialStalledIndexes.length; retryIndex++) {
|
|
832
|
+
// Bail out if the retry phase deadline has elapsed.
|
|
833
|
+
if (retrySignal.aborted) {
|
|
834
|
+
for (let remaining = retryIndex; remaining < initialStalledIndexes.length; remaining++) {
|
|
835
|
+
const idx = initialStalledIndexes[remaining];
|
|
836
|
+
retrySummaryLines.push(`- [${tasks[idx].agent}] skipped (retry phase timeout)`);
|
|
837
|
+
}
|
|
838
|
+
break;
|
|
839
|
+
}
|
|
840
|
+
|
|
797
841
|
const stalledIndex = initialStalledIndexes[retryIndex];
|
|
798
842
|
const stalledTask = tasks[stalledIndex];
|
|
799
843
|
const priorResult = allResults[stalledIndex];
|
|
@@ -810,33 +854,49 @@ async function executeParallel(
|
|
|
810
854
|
`Retrying stalled worker ${retryIndex + 1}/${totalRetries}: ${stalledTask.agent}`
|
|
811
855
|
);
|
|
812
856
|
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
857
|
+
let retryResult: SingleResult;
|
|
858
|
+
try {
|
|
859
|
+
retryResult = await runSingleAgent(
|
|
860
|
+
ctx.cwd,
|
|
861
|
+
agents,
|
|
862
|
+
stalledTask.agent,
|
|
863
|
+
retryTask,
|
|
864
|
+
stalledTask.cwd,
|
|
865
|
+
undefined,
|
|
866
|
+
retrySignal,
|
|
867
|
+
(partial) => {
|
|
868
|
+
if (partial.details?.results[0]) {
|
|
869
|
+
const partialResult = {
|
|
870
|
+
...partial.details.results[0],
|
|
871
|
+
task: stalledTask.task,
|
|
872
|
+
};
|
|
873
|
+
allResults[stalledIndex] = partialResult;
|
|
874
|
+
emitParallelUpdate();
|
|
875
|
+
}
|
|
876
|
+
},
|
|
877
|
+
makeDetails("parallel"),
|
|
878
|
+
pi.events,
|
|
879
|
+
undefined,
|
|
880
|
+
explicitRetryModel,
|
|
881
|
+
parentModelId,
|
|
882
|
+
defaults,
|
|
883
|
+
retryRoutingHints,
|
|
884
|
+
stalledTask.isolation
|
|
885
|
+
);
|
|
886
|
+
} catch {
|
|
887
|
+
// Retry-phase timeout aborted this worker. Mark remaining
|
|
888
|
+
// workers as skipped and exit the loop.
|
|
889
|
+
retrySummaryLines.push(`- [${stalledTask.agent}] aborted (retry phase timeout)`);
|
|
890
|
+
for (
|
|
891
|
+
let remaining = retryIndex + 1;
|
|
892
|
+
remaining < initialStalledIndexes.length;
|
|
893
|
+
remaining++
|
|
894
|
+
) {
|
|
895
|
+
const idx = initialStalledIndexes[remaining];
|
|
896
|
+
retrySummaryLines.push(`- [${tasks[idx].agent}] skipped (retry phase timeout)`);
|
|
897
|
+
}
|
|
898
|
+
break;
|
|
899
|
+
}
|
|
840
900
|
|
|
841
901
|
retryResult.task = stalledTask.task;
|
|
842
902
|
retryResult.stderr = appendStderrNote(
|
|
@@ -848,6 +908,10 @@ async function executeParallel(
|
|
|
848
908
|
emitParallelUpdate();
|
|
849
909
|
}
|
|
850
910
|
} finally {
|
|
911
|
+
clearTimeout(retryTimer);
|
|
912
|
+
if (signal && parentAbortHandler) {
|
|
913
|
+
signal.removeEventListener("abort", parentAbortHandler);
|
|
914
|
+
}
|
|
851
915
|
ctx.ui.setWorkingMessage();
|
|
852
916
|
}
|
|
853
917
|
|
|
@@ -1066,8 +1130,6 @@ interface DisplayRenderOptions {
|
|
|
1066
1130
|
* Shared preview budgets for compact subagent presentation lines.
|
|
1067
1131
|
*/
|
|
1068
1132
|
const SUBAGENT_PREVIEW_LIMITS = {
|
|
1069
|
-
callCentipedeStep: 90,
|
|
1070
|
-
callParallelTask: 90,
|
|
1071
1133
|
collapsedParallelResult: 88,
|
|
1072
1134
|
} as const;
|
|
1073
1135
|
|
|
@@ -1333,71 +1395,35 @@ function renderSubagentCall(args: Record<string, unknown>, theme: Theme) {
|
|
|
1333
1395
|
args.tasks as { agent: string; model?: string; task: string }[] | string | undefined
|
|
1334
1396
|
);
|
|
1335
1397
|
const lines: string[] = [];
|
|
1398
|
+
// Only show scope when non-default (user is the default)
|
|
1399
|
+
const scopeEntry = scope !== "user" ? `scope:${scope}` : undefined;
|
|
1336
1400
|
|
|
1337
1401
|
if (centipedeArr && centipedeArr.length > 0) {
|
|
1338
1402
|
appendSection(lines, [formatSubagentHeader(theme, `centipede (${centipedeArr.length} steps)`)]);
|
|
1339
|
-
const metaLine = formatMetaLine(theme, [
|
|
1340
|
-
`scope:${scope}`,
|
|
1341
|
-
model ? `model:${model}` : undefined,
|
|
1342
|
-
]);
|
|
1403
|
+
const metaLine = formatMetaLine(theme, [scopeEntry, model ? `model:${model}` : undefined]);
|
|
1343
1404
|
if (metaLine) appendSection(lines, [metaLine]);
|
|
1344
|
-
|
|
1345
|
-
const previewLines = centipedeArr.slice(0, 3).map((step, index) => {
|
|
1346
|
-
const task = step.task.replace(/\{previous\}/g, "").trim();
|
|
1347
|
-
const preview = toCompactPreview(
|
|
1348
|
-
task || "(uses previous output)",
|
|
1349
|
-
SUBAGENT_PREVIEW_LIMITS.callCentipedeStep
|
|
1350
|
-
);
|
|
1351
|
-
const modelTag = formatModelTag(theme, step.model);
|
|
1352
|
-
const identity = modelTag
|
|
1353
|
-
? `${formatSubagentIdentity(step.agent)} ${modelTag}`
|
|
1354
|
-
: formatSubagentIdentity(step.agent);
|
|
1355
|
-
return `${formatPresentationText(theme, "meta", `${index + 1}.`)} ${identity} ${formatPresentationText(theme, "process_output", preview)}`;
|
|
1356
|
-
});
|
|
1357
|
-
if (previewLines.length > 0) appendSection(lines, previewLines, { blankBefore: true });
|
|
1358
|
-
if (centipedeArr.length > 3) {
|
|
1359
|
-
appendSection(lines, [
|
|
1360
|
-
formatPresentationText(theme, "hint", `… +${centipedeArr.length - 3} more steps`),
|
|
1361
|
-
]);
|
|
1362
|
-
}
|
|
1363
1405
|
return new Text(lines.join("\n"), 0, 0);
|
|
1364
1406
|
}
|
|
1365
1407
|
|
|
1366
1408
|
if (tasksArr && tasksArr.length > 0) {
|
|
1367
1409
|
appendSection(lines, [formatSubagentHeader(theme, `parallel (${tasksArr.length} tasks)`)]);
|
|
1368
|
-
const metaLine = formatMetaLine(theme, [
|
|
1369
|
-
`scope:${scope}`,
|
|
1370
|
-
model ? `model:${model}` : undefined,
|
|
1371
|
-
]);
|
|
1410
|
+
const metaLine = formatMetaLine(theme, [scopeEntry, model ? `model:${model}` : undefined]);
|
|
1372
1411
|
if (metaLine) appendSection(lines, [metaLine]);
|
|
1373
|
-
|
|
1374
|
-
const previewLines = tasksArr.slice(0, 2).map((task, index) => {
|
|
1375
|
-
const taskPreview = toCompactPreview(task.task, SUBAGENT_PREVIEW_LIMITS.callParallelTask);
|
|
1376
|
-
const modelTag = formatModelTag(theme, task.model);
|
|
1377
|
-
const identity = modelTag
|
|
1378
|
-
? `${formatSubagentIdentity(task.agent)} ${modelTag}`
|
|
1379
|
-
: formatSubagentIdentity(task.agent);
|
|
1380
|
-
return `${formatPresentationText(theme, "meta", `${index + 1}.`)} ${identity} ${formatPresentationText(theme, "process_output", taskPreview)}`;
|
|
1381
|
-
});
|
|
1382
|
-
if (previewLines.length > 0) appendSection(lines, previewLines, { blankBefore: true });
|
|
1383
|
-
if (tasksArr.length > 2) {
|
|
1384
|
-
appendSection(lines, [
|
|
1385
|
-
formatPresentationText(theme, "hint", `… +${tasksArr.length - 2} more tasks`),
|
|
1386
|
-
]);
|
|
1387
|
-
}
|
|
1388
1412
|
return new Text(lines.join("\n"), 0, 0);
|
|
1389
1413
|
}
|
|
1390
1414
|
|
|
1391
1415
|
const agentName = (args.agent as string) || "...";
|
|
1392
1416
|
const task = typeof args.task === "string" ? args.task : "...";
|
|
1393
|
-
|
|
1394
|
-
|
|
1417
|
+
// Single mode: skip the redundant "subagent single" header — the result
|
|
1418
|
+
// renderer already shows "subagent running <duration> <agent>" with a spinner.
|
|
1419
|
+
// Just show the task preview so the user sees what was requested.
|
|
1420
|
+
const metaLine = formatMetaLine(theme, [scopeEntry, model ? `model:${model}` : undefined]);
|
|
1395
1421
|
if (metaLine) appendSection(lines, [metaLine]);
|
|
1396
1422
|
appendSection(
|
|
1397
1423
|
lines,
|
|
1398
1424
|
[formatPresentationText(theme, "process_output", toCompactPreview(task, 200))],
|
|
1399
1425
|
{
|
|
1400
|
-
blankBefore:
|
|
1426
|
+
blankBefore: false,
|
|
1401
1427
|
}
|
|
1402
1428
|
);
|
|
1403
1429
|
return new Text(lines.join("\n"), 0, 0);
|
|
@@ -1523,7 +1549,14 @@ function renderSingleResult(
|
|
|
1523
1549
|
: isError
|
|
1524
1550
|
? theme.fg("error", getIcon("error"))
|
|
1525
1551
|
: theme.fg("success", getIcon("success"));
|
|
1526
|
-
const statusLabel =
|
|
1552
|
+
const statusLabel =
|
|
1553
|
+
isRunning && r.startTime
|
|
1554
|
+
? `running ${formatDuration(Date.now() - r.startTime)}`
|
|
1555
|
+
: isRunning
|
|
1556
|
+
? "running"
|
|
1557
|
+
: isError
|
|
1558
|
+
? "failed"
|
|
1559
|
+
: "completed";
|
|
1527
1560
|
const headerLine = formatSubagentHeader(theme, statusLabel, r.agent, icon);
|
|
1528
1561
|
const metaLine = formatMetaLine(theme, [
|
|
1529
1562
|
`source:${r.agentSource}`,
|
|
@@ -1727,17 +1760,25 @@ function renderCentipedeResult(
|
|
|
1727
1760
|
: failCount > 0
|
|
1728
1761
|
? theme.fg("error", getIcon("error"))
|
|
1729
1762
|
: theme.fg("success", getIcon("success"));
|
|
1763
|
+
const earliestStart = details.results.reduce(
|
|
1764
|
+
(min, r) => (r.startTime && r.startTime < min ? r.startTime : min),
|
|
1765
|
+
Number.POSITIVE_INFINITY
|
|
1766
|
+
);
|
|
1767
|
+
const elapsed = Number.isFinite(earliestStart)
|
|
1768
|
+
? formatDuration(Date.now() - earliestStart)
|
|
1769
|
+
: undefined;
|
|
1730
1770
|
const summaryLine = formatMetaLine(theme, [
|
|
1731
1771
|
`${successCount + failCount}/${totalSteps} done`,
|
|
1732
1772
|
runningCount > 0 ? `${runningCount} running` : undefined,
|
|
1733
1773
|
failCount > 0 ? `${failCount} failed` : undefined,
|
|
1774
|
+
elapsed,
|
|
1734
1775
|
]);
|
|
1735
1776
|
|
|
1736
1777
|
if (expanded) {
|
|
1737
1778
|
const container = new Container();
|
|
1738
|
-
const headerLines: string[] = [
|
|
1739
|
-
if (summaryLine)
|
|
1740
|
-
container.addChild(new Text(headerLines.join("\n"), 0, 0));
|
|
1779
|
+
const headerLines: string[] = [];
|
|
1780
|
+
if (summaryLine) headerLines.push(`${icon} ${summaryLine}`);
|
|
1781
|
+
if (headerLines.length > 0) container.addChild(new Text(headerLines.join("\n"), 0, 0));
|
|
1741
1782
|
|
|
1742
1783
|
for (let si = 0; si < totalSteps; si++) {
|
|
1743
1784
|
const stepNum = si + 1;
|
|
@@ -1746,11 +1787,13 @@ function renderCentipedeResult(
|
|
|
1746
1787
|
stepResult?.agent ?? details.centipedeSteps?.[si]?.agent ?? `step ${stepNum}`;
|
|
1747
1788
|
const stepStatus = !stepResult
|
|
1748
1789
|
? "pending"
|
|
1749
|
-
: stepResult.exitCode === -1
|
|
1750
|
-
?
|
|
1751
|
-
:
|
|
1752
|
-
? "
|
|
1753
|
-
:
|
|
1790
|
+
: stepResult.exitCode === -1 && stepResult.startTime
|
|
1791
|
+
? `running ${formatDuration(Date.now() - stepResult.startTime)}`
|
|
1792
|
+
: stepResult.exitCode === -1
|
|
1793
|
+
? "running"
|
|
1794
|
+
: isResultError(stepResult)
|
|
1795
|
+
? "failed"
|
|
1796
|
+
: "completed";
|
|
1754
1797
|
const stepStatusRole = !stepResult
|
|
1755
1798
|
? "meta"
|
|
1756
1799
|
: stepResult.exitCode === -1
|
|
@@ -1839,8 +1882,8 @@ function renderCentipedeResult(
|
|
|
1839
1882
|
return container;
|
|
1840
1883
|
}
|
|
1841
1884
|
|
|
1842
|
-
const lines: string[] = [
|
|
1843
|
-
if (summaryLine)
|
|
1885
|
+
const lines: string[] = [];
|
|
1886
|
+
if (summaryLine) lines.push(`${icon} ${summaryLine}`);
|
|
1844
1887
|
|
|
1845
1888
|
for (let si = 0; si < totalSteps; si++) {
|
|
1846
1889
|
const stepNum = si + 1;
|
|
@@ -1851,11 +1894,13 @@ function renderCentipedeResult(
|
|
|
1851
1894
|
const stem = isLast ? " " : `${formatPresentationText(theme, "meta", "│")} `;
|
|
1852
1895
|
const stepStatus = !stepResult
|
|
1853
1896
|
? "pending"
|
|
1854
|
-
: stepResult.exitCode === -1
|
|
1855
|
-
?
|
|
1856
|
-
:
|
|
1857
|
-
? "
|
|
1858
|
-
:
|
|
1897
|
+
: stepResult.exitCode === -1 && stepResult.startTime
|
|
1898
|
+
? `running ${formatDuration(Date.now() - stepResult.startTime)}`
|
|
1899
|
+
: stepResult.exitCode === -1
|
|
1900
|
+
? "running"
|
|
1901
|
+
: isResultError(stepResult)
|
|
1902
|
+
? "failed"
|
|
1903
|
+
: "done";
|
|
1859
1904
|
const statusRole = !stepResult
|
|
1860
1905
|
? "meta"
|
|
1861
1906
|
: stepResult.exitCode === -1
|
|
@@ -1922,23 +1967,36 @@ function renderParallelResult(
|
|
|
1922
1967
|
: counts.stalled > 0
|
|
1923
1968
|
? theme.fg("warning", getIcon("blocked"))
|
|
1924
1969
|
: theme.fg("success", getIcon("success"));
|
|
1970
|
+
const earliestStart = details.results.reduce(
|
|
1971
|
+
(min, r) => (r.startTime && r.startTime < min ? r.startTime : min),
|
|
1972
|
+
Number.POSITIVE_INFINITY
|
|
1973
|
+
);
|
|
1974
|
+
const elapsed = Number.isFinite(earliestStart)
|
|
1975
|
+
? formatDuration(Date.now() - earliestStart)
|
|
1976
|
+
: undefined;
|
|
1925
1977
|
const summaryLine = formatMetaLine(theme, [
|
|
1926
1978
|
`${counts.finished}/${details.results.length} done`,
|
|
1927
1979
|
`${counts.completed} completed`,
|
|
1928
1980
|
counts.failed > 0 ? `${counts.failed} failed` : undefined,
|
|
1929
1981
|
`${counts.stalled} stalled`,
|
|
1930
1982
|
counts.running > 0 ? `${counts.running} running` : undefined,
|
|
1983
|
+
elapsed,
|
|
1931
1984
|
]);
|
|
1932
1985
|
|
|
1933
1986
|
if (expanded && !isRunning) {
|
|
1934
1987
|
const container = new Container();
|
|
1935
|
-
const headerLines = [
|
|
1936
|
-
if (summaryLine)
|
|
1937
|
-
container.addChild(new Text(headerLines.join("\n"), 0, 0));
|
|
1988
|
+
const headerLines: string[] = [];
|
|
1989
|
+
if (summaryLine) headerLines.push(`${icon} ${summaryLine}`);
|
|
1990
|
+
if (headerLines.length > 0) container.addChild(new Text(headerLines.join("\n"), 0, 0));
|
|
1938
1991
|
|
|
1939
1992
|
for (const result of details.results) {
|
|
1940
1993
|
const resultState = getParallelResultState(result);
|
|
1941
|
-
const resultStatus =
|
|
1994
|
+
const resultStatus =
|
|
1995
|
+
resultState === "completed"
|
|
1996
|
+
? "completed"
|
|
1997
|
+
: resultState === "running" && result.startTime
|
|
1998
|
+
? `running ${formatDuration(Date.now() - result.startTime)}`
|
|
1999
|
+
: resultState;
|
|
1942
2000
|
const resultStatusRole =
|
|
1943
2001
|
resultState === "failed"
|
|
1944
2002
|
? "status_error"
|
|
@@ -2028,8 +2086,8 @@ function renderParallelResult(
|
|
|
2028
2086
|
return container;
|
|
2029
2087
|
}
|
|
2030
2088
|
|
|
2031
|
-
const lines: string[] = [
|
|
2032
|
-
if (summaryLine)
|
|
2089
|
+
const lines: string[] = [];
|
|
2090
|
+
if (summaryLine) lines.push(`${icon} ${summaryLine}`);
|
|
2033
2091
|
|
|
2034
2092
|
for (let index = 0; index < details.results.length; index++) {
|
|
2035
2093
|
const result = details.results[index];
|
|
@@ -2037,7 +2095,12 @@ function renderParallelResult(
|
|
|
2037
2095
|
const branch = formatPresentationText(theme, "meta", isLast ? "└─" : "├─");
|
|
2038
2096
|
const stem = isLast ? " " : `${formatPresentationText(theme, "meta", "│")} `;
|
|
2039
2097
|
const resultState = getParallelResultState(result);
|
|
2040
|
-
const resultStatus =
|
|
2098
|
+
const resultStatus =
|
|
2099
|
+
resultState === "completed"
|
|
2100
|
+
? "done"
|
|
2101
|
+
: resultState === "running" && result.startTime
|
|
2102
|
+
? `running ${formatDuration(Date.now() - result.startTime)}`
|
|
2103
|
+
: resultState;
|
|
2041
2104
|
const statusRole =
|
|
2042
2105
|
resultState === "failed"
|
|
2043
2106
|
? "status_error"
|