@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.
Files changed (114) hide show
  1. package/README.md +42 -1
  2. package/dist/cli.js +7 -1
  3. package/dist/cli.js.map +1 -1
  4. package/dist/config.d.ts +1 -1
  5. package/dist/config.js +1 -1
  6. package/dist/interactive-mode-patch.d.ts +1 -0
  7. package/dist/interactive-mode-patch.d.ts.map +1 -1
  8. package/dist/interactive-mode-patch.js +40 -1
  9. package/dist/interactive-mode-patch.js.map +1 -1
  10. package/dist/model-metadata-overrides.d.ts +2 -5
  11. package/dist/model-metadata-overrides.d.ts.map +1 -1
  12. package/dist/model-metadata-overrides.js +23 -12
  13. package/dist/model-metadata-overrides.js.map +1 -1
  14. package/dist/pid-manager.d.ts +2 -9
  15. package/dist/pid-manager.d.ts.map +1 -1
  16. package/dist/pid-manager.js +1 -58
  17. package/dist/pid-manager.js.map +1 -1
  18. package/dist/pid-schema.d.ts +51 -0
  19. package/dist/pid-schema.d.ts.map +1 -0
  20. package/dist/pid-schema.js +70 -0
  21. package/dist/pid-schema.js.map +1 -0
  22. package/dist/sdk.d.ts.map +1 -1
  23. package/dist/sdk.js +24 -17
  24. package/dist/sdk.js.map +1 -1
  25. package/dist/workspace-transition-interactive.d.ts.map +1 -1
  26. package/dist/workspace-transition-interactive.js +53 -3
  27. package/dist/workspace-transition-interactive.js.map +1 -1
  28. package/dist/workspace-transition.d.ts +2 -1
  29. package/dist/workspace-transition.d.ts.map +1 -1
  30. package/dist/workspace-transition.js +16 -4
  31. package/dist/workspace-transition.js.map +1 -1
  32. package/extensions/__integration__/audit-findings.test.ts +309 -0
  33. package/extensions/__integration__/cd-tool-guidelines.test.ts +46 -0
  34. package/extensions/__integration__/tasks-runtime.test.ts +63 -12
  35. package/extensions/__integration__/welcome-screen.test.ts +240 -0
  36. package/extensions/_shared/lazy-init.ts +88 -3
  37. package/extensions/_shared/pid-registry.ts +8 -82
  38. package/extensions/background-task-tool/index.ts +1 -1
  39. package/extensions/cd-tool/index.ts +4 -1
  40. package/extensions/cheatsheet/__tests__/cheatsheet.test.ts +47 -0
  41. package/extensions/clear/__tests__/clear.test.ts +38 -0
  42. package/extensions/edit-tool-enhanced/index.ts +3 -1
  43. package/extensions/git-status/__tests__/git-status.test.ts +32 -0
  44. package/extensions/health/__tests__/diagnostics.test.ts +25 -0
  45. package/extensions/health/index.ts +61 -0
  46. package/extensions/loop/__tests__/loop.test.ts +365 -1
  47. package/extensions/loop/index.ts +213 -3
  48. package/extensions/mcp-adapter-tool/index.ts +1 -1
  49. package/extensions/minimal-skill-display/__tests__/minimal-skill-display.test.ts +20 -0
  50. package/extensions/permissions/__tests__/permissions.test.ts +213 -0
  51. package/extensions/progress-indicator/__tests__/progress-indicator.test.ts +104 -0
  52. package/extensions/prompt-suggestions/__tests__/autocomplete.test.ts +111 -3
  53. package/extensions/prompt-suggestions/autocomplete.ts +23 -5
  54. package/extensions/prompt-suggestions/index.ts +62 -3
  55. package/extensions/random-spinner/__tests__/random-spinner.test.ts +35 -0
  56. package/extensions/read-tool-enhanced/index.ts +5 -1
  57. package/extensions/session-memory/index.ts +1 -1
  58. package/extensions/session-namer/index.ts +1 -1
  59. package/extensions/show-system-prompt/__tests__/show-system-prompt.test.ts +51 -0
  60. package/extensions/subagent-tool/__tests__/presentation-rendering.test.ts +9 -8
  61. package/extensions/subagent-tool/__tests__/process-liveness.test.ts +51 -0
  62. package/extensions/subagent-tool/__tests__/subprocess-args.test.ts +120 -0
  63. package/extensions/subagent-tool/formatting.ts +2 -0
  64. package/extensions/subagent-tool/index.ts +160 -97
  65. package/extensions/subagent-tool/process.ts +152 -40
  66. package/extensions/tasks/commands/register-tasks-extension.ts +64 -20
  67. package/extensions/tasks/extension.json +1 -0
  68. package/extensions/tasks/index.ts +2 -12
  69. package/extensions/tasks/state/index.ts +26 -0
  70. package/extensions/teams-tool/dashboard.ts +13 -1
  71. package/extensions/teams-tool/sessions/spawn.ts +2 -2
  72. package/extensions/teams-tool/tools/register-extension.ts +10 -2
  73. package/extensions/upstream-check/__tests__/upstream-check.test.ts +49 -0
  74. package/extensions/welcome-screen/__tests__/welcome-screen.test.ts +35 -0
  75. package/extensions/welcome-screen/extension.json +20 -0
  76. package/extensions/welcome-screen/index.ts +189 -0
  77. package/extensions/wezterm-notify/__tests__/index.test.ts +49 -11
  78. package/extensions/wezterm-notify/index.ts +5 -3
  79. package/extensions/write-tool-enhanced/__tests__/write-tool-enhanced.test.ts +296 -0
  80. package/node_modules/@mariozechner/pi-tui/dist/index.d.ts +2 -2
  81. package/node_modules/@mariozechner/pi-tui/dist/index.d.ts.map +1 -1
  82. package/node_modules/@mariozechner/pi-tui/dist/index.js +2 -2
  83. package/node_modules/@mariozechner/pi-tui/dist/index.js.map +1 -1
  84. package/node_modules/@mariozechner/pi-tui/dist/keybindings.d.ts +309 -25
  85. package/node_modules/@mariozechner/pi-tui/dist/keybindings.d.ts.map +1 -1
  86. package/node_modules/@mariozechner/pi-tui/dist/keybindings.js +392 -72
  87. package/node_modules/@mariozechner/pi-tui/dist/keybindings.js.map +1 -1
  88. package/node_modules/@mariozechner/pi-tui/dist/keys.d.ts +30 -0
  89. package/node_modules/@mariozechner/pi-tui/dist/keys.d.ts.map +1 -1
  90. package/node_modules/@mariozechner/pi-tui/dist/keys.js +50 -6
  91. package/node_modules/@mariozechner/pi-tui/dist/keys.js.map +1 -1
  92. package/node_modules/@mariozechner/pi-tui/dist/terminal.d.ts +27 -0
  93. package/node_modules/@mariozechner/pi-tui/dist/terminal.d.ts.map +1 -1
  94. package/node_modules/@mariozechner/pi-tui/dist/terminal.js +59 -4
  95. package/node_modules/@mariozechner/pi-tui/dist/terminal.js.map +1 -1
  96. package/node_modules/@mariozechner/pi-tui/dist/tui.d.ts +9 -0
  97. package/node_modules/@mariozechner/pi-tui/dist/tui.d.ts.map +1 -1
  98. package/node_modules/@mariozechner/pi-tui/dist/tui.js +50 -1
  99. package/node_modules/@mariozechner/pi-tui/dist/tui.js.map +1 -1
  100. package/node_modules/@mariozechner/pi-tui/package.json +1 -1
  101. package/node_modules/@mariozechner/pi-tui/src/__tests__/mouse-events.test.ts +134 -0
  102. package/node_modules/@mariozechner/pi-tui/src/__tests__/tmux-compat.test.ts +204 -0
  103. package/node_modules/@mariozechner/pi-tui/src/__tests__/tui-diff-regression.test.ts +49 -0
  104. package/node_modules/@mariozechner/pi-tui/src/__tests__/tui-render-scheduling.test.ts +2 -0
  105. package/node_modules/@mariozechner/pi-tui/src/index.ts +11 -0
  106. package/node_modules/@mariozechner/pi-tui/src/keybindings.ts +478 -140
  107. package/node_modules/@mariozechner/pi-tui/src/keys.ts +84 -6
  108. package/node_modules/@mariozechner/pi-tui/src/terminal.ts +69 -4
  109. package/node_modules/@mariozechner/pi-tui/src/tui.ts +64 -1
  110. package/package.json +11 -10
  111. package/runtime/config.ts +7 -0
  112. package/runtime/model-metadata-overrides.ts +7 -0
  113. package/runtime/pid-schema.ts +13 -0
  114. 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
- const retryResult = await runSingleAgent(
814
- ctx.cwd,
815
- agents,
816
- stalledTask.agent,
817
- retryTask,
818
- stalledTask.cwd,
819
- undefined,
820
- signal,
821
- (partial) => {
822
- if (partial.details?.results[0]) {
823
- const partialResult = {
824
- ...partial.details.results[0],
825
- task: stalledTask.task,
826
- };
827
- allResults[stalledIndex] = partialResult;
828
- emitParallelUpdate();
829
- }
830
- },
831
- makeDetails("parallel"),
832
- pi.events,
833
- undefined,
834
- explicitRetryModel,
835
- parentModelId,
836
- defaults,
837
- retryRoutingHints,
838
- stalledTask.isolation
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
- appendSection(lines, [formatSubagentHeader(theme, "single", agentName)]);
1394
- const metaLine = formatMetaLine(theme, [`scope:${scope}`, model ? `model:${model}` : undefined]);
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: true,
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 = isRunning ? "running" : isError ? "failed" : "completed";
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[] = [formatSubagentHeader(theme, "centipede", undefined, icon)];
1739
- if (summaryLine) appendSection(headerLines, [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
- ? "running"
1751
- : isResultError(stepResult)
1752
- ? "failed"
1753
- : "completed";
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[] = [formatSubagentHeader(theme, "centipede", undefined, icon)];
1843
- if (summaryLine) appendSection(lines, [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
- ? "running"
1856
- : isResultError(stepResult)
1857
- ? "failed"
1858
- : "done";
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 = [formatSubagentHeader(theme, "parallel", undefined, icon)];
1936
- if (summaryLine) appendSection(headerLines, [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 = resultState === "completed" ? "completed" : resultState;
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[] = [formatSubagentHeader(theme, "parallel", undefined, icon)];
2032
- if (summaryLine) appendSection(lines, [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 = resultState === "completed" ? "done" : resultState;
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"