@dungle-scrubs/tallow 0.8.7 → 0.8.8

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 (76) hide show
  1. package/README.md +1 -1
  2. package/dist/cli.js +244 -54
  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/sdk.d.ts +39 -0
  7. package/dist/sdk.d.ts.map +1 -1
  8. package/dist/sdk.js +322 -13
  9. package/dist/sdk.js.map +1 -1
  10. package/dist/session-utils.d.ts +8 -0
  11. package/dist/session-utils.d.ts.map +1 -1
  12. package/dist/session-utils.js +38 -1
  13. package/dist/session-utils.js.map +1 -1
  14. package/extensions/__integration__/plan-rejection-feedback.test.ts +272 -0
  15. package/extensions/__integration__/worktree.test.ts +88 -0
  16. package/extensions/_icons/index.ts +2 -3
  17. package/extensions/_shared/inline-preview.ts +2 -4
  18. package/extensions/_shared/interop-events.ts +48 -0
  19. package/extensions/_shared/shell-policy.ts +2 -1
  20. package/extensions/_shared/tallow-paths.ts +48 -0
  21. package/extensions/agent-commands-tool/__tests__/parsing.test.ts +40 -1
  22. package/extensions/agent-commands-tool/index.ts +38 -6
  23. package/extensions/background-task-tool/index.ts +2 -2
  24. package/extensions/bash-tool-enhanced/index.ts +3 -4
  25. package/extensions/cd-tool/index.ts +3 -3
  26. package/extensions/claude-bridge/index.ts +2 -1
  27. package/extensions/command-prompt/__tests__/plugin-sources.test.ts +49 -0
  28. package/extensions/command-prompt/index.ts +36 -0
  29. package/extensions/context-files/index.ts +2 -1
  30. package/extensions/context-fork/index.ts +2 -1
  31. package/extensions/context-usage/__tests__/tool-result-memory.test.ts +62 -0
  32. package/extensions/context-usage/index.ts +169 -8
  33. package/extensions/debug/__tests__/analysis.test.ts +35 -2
  34. package/extensions/debug/__tests__/diagnostics-commands.test.ts +11 -2
  35. package/extensions/debug/analysis.ts +53 -16
  36. package/extensions/debug/index.ts +84 -10
  37. package/extensions/debug/logger.ts +2 -2
  38. package/extensions/health/index.ts +2 -2
  39. package/extensions/hooks/__tests__/claude-compat.test.ts +31 -0
  40. package/extensions/hooks/extension.json +3 -1
  41. package/extensions/hooks/hooks.schema.json +17 -1
  42. package/extensions/hooks/index.ts +52 -7
  43. package/extensions/lsp/index.ts +2 -7
  44. package/extensions/output-styles-tool/index.ts +2 -4
  45. package/extensions/plan-mode-tool/extension.json +1 -0
  46. package/extensions/plan-mode-tool/index.ts +119 -4
  47. package/extensions/prompt-suggestions/index.ts +2 -3
  48. package/extensions/random-spinner/index.ts +2 -3
  49. package/extensions/read-tool-enhanced/__tests__/notebook-read.test.ts +100 -0
  50. package/extensions/read-tool-enhanced/__tests__/notebook.test.ts +136 -0
  51. package/extensions/read-tool-enhanced/extension.json +4 -4
  52. package/extensions/read-tool-enhanced/index.ts +112 -10
  53. package/extensions/read-tool-enhanced/notebook.ts +526 -0
  54. package/extensions/rewind/__tests__/snapshots.test.ts +43 -1
  55. package/extensions/rewind/snapshots.ts +18 -6
  56. package/extensions/session-memory/index.ts +3 -2
  57. package/extensions/stats/stats-log.ts +2 -3
  58. package/extensions/subagent-tool/__tests__/discovery-defaults.test.ts +23 -0
  59. package/extensions/subagent-tool/__tests__/isolation-frontmatter.test.ts +100 -0
  60. package/extensions/subagent-tool/__tests__/model-router.test.ts +8 -0
  61. package/extensions/subagent-tool/agents.ts +77 -16
  62. package/extensions/subagent-tool/index.ts +213 -48
  63. package/extensions/subagent-tool/model-router.ts +3 -3
  64. package/extensions/subagent-tool/process.ts +233 -22
  65. package/extensions/subagent-tool/schema.ts +11 -0
  66. package/extensions/subagent-tool/widget.ts +15 -2
  67. package/extensions/tasks/__tests__/widget-subagents.test.ts +28 -7
  68. package/extensions/tasks/commands/register-tasks-extension.ts +15 -33
  69. package/extensions/tasks/state/index.ts +2 -2
  70. package/extensions/theme-selector/index.ts +3 -7
  71. package/extensions/worktree/__tests__/lifecycle.test.ts +115 -0
  72. package/extensions/worktree/extension.json +31 -0
  73. package/extensions/worktree/index.ts +149 -0
  74. package/extensions/worktree/lifecycle.ts +372 -0
  75. package/package.json +1 -1
  76. package/skills/tallow-expert/SKILL.md +1 -1
@@ -14,13 +14,23 @@ import type { AgentToolResult } from "@mariozechner/pi-agent-core";
14
14
  import type { Message } from "@mariozechner/pi-ai";
15
15
  import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
16
16
  import { extractPreview, isInlineResultsEnabled } from "../_shared/inline-preview.js";
17
+ import {
18
+ emitWorktreeLifecycleEvent,
19
+ type WorktreeLifecycleScope,
20
+ } from "../_shared/interop-events.js";
17
21
  import { expandFileReferences } from "../file-reference/index.js";
22
+ import { createWorktree, removeWorktree, validateGitRepo } from "../worktree/lifecycle.js";
18
23
  import type { AgentConfig, AgentDefaults } from "./agents.js";
19
- import { computeEffectiveTools, resolveAgentForExecution } from "./agents.js";
24
+ import {
25
+ computeEffectiveTools,
26
+ resolveAgentForExecution,
27
+ resolveEffectiveIsolation,
28
+ } from "./agents.js";
20
29
  import { getFinalOutput, type SingleResult, type SubagentDetails } from "./formatting.js";
21
30
  import type { RoutingHints } from "./model-router.js";
22
31
  import { routeModel } from "./model-router.js";
23
32
  import type {
33
+ IsolationMode,
24
34
  SubagentCompleteDetails,
25
35
  SubagentStartEvent,
26
36
  SubagentStopEvent,
@@ -266,6 +276,100 @@ export function applyBackgroundResultRetention(
266
276
  subagent.historyRetainedMessageCount = compacted.retainedMessageCount;
267
277
  }
268
278
 
279
+ /** Managed isolation metadata for one subagent invocation. */
280
+ interface SubagentIsolationContext {
281
+ readonly mode: IsolationMode;
282
+ readonly repoRoot: string;
283
+ readonly worktreePath: string;
284
+ }
285
+
286
+ /**
287
+ * Provision execution isolation for one subagent invocation.
288
+ *
289
+ * @param baseCwd - Base working directory before isolation
290
+ * @param requestedIsolation - Explicit per-call isolation, if any
291
+ * @param agent - Resolved agent config
292
+ * @param defaults - Defaults from _defaults.md
293
+ * @param lifecycleScope - Lifecycle scope for emitted events
294
+ * @param lifecycleId - Subagent/task identifier for lifecycle payloads
295
+ * @param events - Optional event bus for lifecycle hooks
296
+ * @returns Effective cwd and optional isolation context
297
+ */
298
+ function provisionIsolation(
299
+ baseCwd: string,
300
+ requestedIsolation: IsolationMode | undefined,
301
+ agent: AgentConfig,
302
+ defaults: AgentDefaults | undefined,
303
+ lifecycleScope: WorktreeLifecycleScope,
304
+ lifecycleId: string,
305
+ events?: ExtensionAPI["events"]
306
+ ): {
307
+ readonly isolation: SubagentIsolationContext | undefined;
308
+ readonly workingDirectory: string;
309
+ } {
310
+ const isolationMode = resolveEffectiveIsolation(
311
+ requestedIsolation,
312
+ agent.isolation,
313
+ defaults?.isolation
314
+ );
315
+ if (isolationMode !== "worktree") {
316
+ return { isolation: undefined, workingDirectory: baseCwd };
317
+ }
318
+
319
+ const repoRoot = validateGitRepo(baseCwd).repoRoot;
320
+ const created = createWorktree(repoRoot, {
321
+ agentId: lifecycleScope === "subagent" ? lifecycleId : undefined,
322
+ id: lifecycleId,
323
+ scope: "subagent",
324
+ });
325
+ const isolation: SubagentIsolationContext = {
326
+ mode: "worktree",
327
+ repoRoot,
328
+ worktreePath: created.worktreePath,
329
+ };
330
+
331
+ if (events) {
332
+ emitWorktreeLifecycleEvent(events, "worktree_create", {
333
+ agentId: lifecycleScope === "subagent" ? lifecycleId : undefined,
334
+ repoRoot,
335
+ scope: lifecycleScope,
336
+ timestamp: Date.now(),
337
+ worktreePath: created.worktreePath,
338
+ });
339
+ }
340
+
341
+ return {
342
+ isolation,
343
+ workingDirectory: created.worktreePath,
344
+ };
345
+ }
346
+
347
+ /**
348
+ * Cleanup worktree isolation for one subagent invocation.
349
+ *
350
+ * @param lifecycleScope - Lifecycle scope for emitted events
351
+ * @param lifecycleId - Subagent/task identifier
352
+ * @param isolation - Isolation context to cleanup
353
+ * @param events - Optional event bus for lifecycle hooks
354
+ */
355
+ function cleanupIsolation(
356
+ lifecycleScope: WorktreeLifecycleScope,
357
+ lifecycleId: string,
358
+ isolation: SubagentIsolationContext | undefined,
359
+ events?: ExtensionAPI["events"]
360
+ ): void {
361
+ if (!isolation) return;
362
+ removeWorktree(isolation.worktreePath);
363
+ if (!events) return;
364
+ emitWorktreeLifecycleEvent(events, "worktree_remove", {
365
+ agentId: lifecycleScope === "subagent" ? lifecycleId : undefined,
366
+ repoRoot: isolation.repoRoot,
367
+ scope: lifecycleScope,
368
+ timestamp: Date.now(),
369
+ worktreePath: isolation.worktreePath,
370
+ });
371
+ }
372
+
269
373
  /**
270
374
  * Write a subagent prompt to a temporary file for the pi subprocess.
271
375
  * @param agentName - Agent name (sanitized for filename)
@@ -513,6 +617,7 @@ export type OnUpdateCallback = (partial: AgentToolResult<SubagentDetails>) => vo
513
617
  * @param parentModelId - Parent model ID for inheritance
514
618
  * @param defaults - Optional agent defaults
515
619
  * @param hints - Optional routing hints
620
+ * @param isolationOverride - Optional per-call isolation override
516
621
  * @returns Background subagent ID, error string if model unresolvable, or null if agent not found
517
622
  */
518
623
  export async function spawnBackgroundSubagent(
@@ -526,11 +631,14 @@ export async function spawnBackgroundSubagent(
526
631
  modelOverride?: string,
527
632
  parentModelId?: string,
528
633
  defaults?: AgentDefaults,
529
- hints?: RoutingHints
634
+ hints?: RoutingHints,
635
+ isolationOverride?: IsolationMode
530
636
  ): Promise<string | null> {
531
637
  const resolved = resolveAgentForExecution(agentName, agents, defaults);
532
- const effectiveCwd = cwd ?? defaultCwd;
533
- // Route model via fuzzy resolution + auto-routing
638
+ const id = `bg_${generateId()}`;
639
+ const requestedCwd = cwd ?? defaultCwd;
640
+
641
+ // Route model via fuzzy resolution + auto-routing.
534
642
  const routing = await routeModel(
535
643
  task,
536
644
  modelOverride,
@@ -538,9 +646,29 @@ export async function spawnBackgroundSubagent(
538
646
  parentModelId,
539
647
  resolved.agent.description,
540
648
  hints,
541
- effectiveCwd
649
+ requestedCwd
542
650
  );
543
651
  if (!routing.ok) return routing.error;
652
+
653
+ let isolationContext: SubagentIsolationContext | undefined;
654
+ let effectiveCwd = requestedCwd;
655
+ try {
656
+ const provisioned = provisionIsolation(
657
+ requestedCwd,
658
+ isolationOverride,
659
+ resolved.agent,
660
+ defaults,
661
+ "subagent",
662
+ id,
663
+ piEvents
664
+ );
665
+ isolationContext = provisioned.isolation;
666
+ effectiveCwd = provisioned.workingDirectory;
667
+ } catch (error) {
668
+ const reason = error instanceof Error ? error.message : String(error);
669
+ return `Failed to create worktree isolation for ${agentName}: ${reason}`;
670
+ }
671
+
544
672
  const agent = { ...resolved.agent, model: routing.model.id };
545
673
  const agentSource = resolved.resolution === "ephemeral" ? ("ephemeral" as const) : agent.source;
546
674
 
@@ -573,7 +701,14 @@ export async function spawnBackgroundSubagent(
573
701
  args.push("--append-system-prompt", tmpPromptPath);
574
702
  }
575
703
 
576
- const expandedTask = await expandFileReferences(task, effectiveCwd);
704
+ let expandedTask: string;
705
+ try {
706
+ expandedTask = await expandFileReferences(task, effectiveCwd);
707
+ } catch (error) {
708
+ cleanupIsolation("subagent", id, isolationContext, piEvents);
709
+ const reason = error instanceof Error ? error.message : String(error);
710
+ return `Failed to expand task references for ${agentName}: ${reason}`;
711
+ }
577
712
  args.push(`Task: ${expandedTask}`);
578
713
 
579
714
  const childEnv: Record<string, string> = { ...process.env, PI_IS_SUBAGENT: "1" } as Record<
@@ -587,14 +722,19 @@ export async function spawnBackgroundSubagent(
587
722
  childEnv.PI_MCP_SERVERS = agent.mcpServers.join(",");
588
723
  }
589
724
 
590
- const proc = spawn("pi", args, {
591
- cwd: effectiveCwd,
592
- shell: false,
593
- stdio: ["ignore", "pipe", "pipe"],
594
- env: childEnv,
595
- });
596
-
597
- const id = `bg_${generateId()}`;
725
+ let proc: ReturnType<typeof spawn>;
726
+ try {
727
+ proc = spawn("pi", args, {
728
+ cwd: effectiveCwd,
729
+ shell: false,
730
+ stdio: ["ignore", "pipe", "pipe"],
731
+ env: childEnv,
732
+ });
733
+ } catch (error) {
734
+ cleanupIsolation("subagent", id, isolationContext, piEvents);
735
+ const reason = error instanceof Error ? error.message : String(error);
736
+ return `Failed to spawn background subagent ${agentName}: ${reason}`;
737
+ }
598
738
 
599
739
  // Emit subagent_start event
600
740
  piEvents?.emit("subagent_start", {
@@ -627,6 +767,7 @@ export async function spawnBackgroundSubagent(
627
767
  const bgSubagent: BackgroundSubagent = {
628
768
  id,
629
769
  agent: agentName,
770
+ isolationMode: isolationContext?.mode,
630
771
  model: agent.model,
631
772
  task,
632
773
  startTime: Date.now(),
@@ -635,12 +776,25 @@ export async function spawnBackgroundSubagent(
635
776
  status: "running",
636
777
  tmpPromptDir,
637
778
  tmpPromptPath,
779
+ worktreePath: isolationContext?.worktreePath,
638
780
  };
639
781
 
640
782
  backgroundSubagents.set(id, bgSubagent);
641
783
  publishSubagentSnapshot(piEvents);
642
784
  startBackgroundSubagentCleanupLoop(piEvents);
643
785
 
786
+ let isolationCleaned = false;
787
+ const cleanupBackgroundIsolation = () => {
788
+ if (isolationCleaned) return;
789
+ isolationCleaned = true;
790
+ cleanupIsolation("subagent", id, isolationContext, piEvents);
791
+ };
792
+
793
+ if (!proc.stdout || !proc.stderr) {
794
+ cleanupBackgroundIsolation();
795
+ return `Failed to spawn background subagent ${agentName}: missing stdio pipes`;
796
+ }
797
+
644
798
  // Collect output
645
799
  let buffer = "";
646
800
  let bgTurnCount = 0;
@@ -714,6 +868,11 @@ export async function spawnBackgroundSubagent(
714
868
  result.stderr += data.toString();
715
869
  });
716
870
 
871
+ proc.on("error", (error) => {
872
+ result.stderr += error.message;
873
+ cleanupBackgroundIsolation();
874
+ });
875
+
717
876
  proc.on("close", (code) => {
718
877
  if (buffer.trim()) {
719
878
  try {
@@ -757,6 +916,7 @@ export async function spawnBackgroundSubagent(
757
916
  /* ignore */
758
917
  }
759
918
 
919
+ cleanupBackgroundIsolation();
760
920
  updateWidget();
761
921
 
762
922
  // Post inline result for background subagent completion
@@ -812,6 +972,7 @@ export async function spawnBackgroundSubagent(
812
972
  * @param parentModelId - Parent model ID for inheritance
813
973
  * @param defaults - Optional agent defaults
814
974
  * @param hints - Optional routing hints
975
+ * @param isolationOverride - Optional per-call isolation override
815
976
  * @returns Result from the subagent execution
816
977
  */
817
978
  export async function runSingleAgent(
@@ -829,10 +990,11 @@ export async function runSingleAgent(
829
990
  modelOverride?: string,
830
991
  parentModelId?: string,
831
992
  defaults?: AgentDefaults,
832
- hints?: RoutingHints
993
+ hints?: RoutingHints,
994
+ isolationOverride?: IsolationMode
833
995
  ): Promise<SingleResult> {
834
996
  const resolved = resolveAgentForExecution(agentName, agents, defaults);
835
- const effectiveCwd = cwd ?? defaultCwd;
997
+ const requestedCwd = cwd ?? defaultCwd;
836
998
  // Route model via fuzzy resolution + auto-routing
837
999
  const routing = await routeModel(
838
1000
  task,
@@ -841,7 +1003,7 @@ export async function runSingleAgent(
841
1003
  parentModelId,
842
1004
  resolved.agent.description,
843
1005
  hints,
844
- effectiveCwd
1006
+ requestedCwd
845
1007
  );
846
1008
  if (!routing.ok) {
847
1009
  // Return a failed SingleResult so the caller can surface the error
@@ -870,7 +1032,53 @@ export async function runSingleAgent(
870
1032
  const agentSource = resolved.resolution === "ephemeral" ? ("ephemeral" as const) : agent.source;
871
1033
  const taskId = `fg_${generateId()}`;
872
1034
 
873
- registerForegroundSubagent(taskId, agentName, task, Date.now(), piEvents, agent.model);
1035
+ let isolationContext: SubagentIsolationContext | undefined;
1036
+ let effectiveCwd = requestedCwd;
1037
+ try {
1038
+ const provisioned = provisionIsolation(
1039
+ requestedCwd,
1040
+ isolationOverride,
1041
+ agent,
1042
+ defaults,
1043
+ "subagent",
1044
+ taskId,
1045
+ piEvents
1046
+ );
1047
+ isolationContext = provisioned.isolation;
1048
+ effectiveCwd = provisioned.workingDirectory;
1049
+ } catch (error) {
1050
+ const message = error instanceof Error ? error.message : String(error);
1051
+ return {
1052
+ agent: agentName,
1053
+ agentSource,
1054
+ task,
1055
+ exitCode: 1,
1056
+ messages: [],
1057
+ stderr: message,
1058
+ usage: {
1059
+ input: 0,
1060
+ output: 0,
1061
+ cacheRead: 0,
1062
+ cacheWrite: 0,
1063
+ cost: 0,
1064
+ contextTokens: 0,
1065
+ turns: 0,
1066
+ denials: 0,
1067
+ },
1068
+ errorMessage: message,
1069
+ step,
1070
+ };
1071
+ }
1072
+
1073
+ registerForegroundSubagent(
1074
+ taskId,
1075
+ agentName,
1076
+ task,
1077
+ Date.now(),
1078
+ piEvents,
1079
+ agent.model,
1080
+ isolationContext?.mode
1081
+ );
874
1082
 
875
1083
  // Emit subagent_start event
876
1084
  piEvents?.emit("subagent_start", {
@@ -993,7 +1201,7 @@ export async function runSingleAgent(
993
1201
  let fgTurnCount = 0;
994
1202
  const exitCode = await new Promise<number>((resolve) => {
995
1203
  const proc = spawn("pi", args, {
996
- cwd: cwd ?? defaultCwd,
1204
+ cwd: effectiveCwd,
997
1205
  shell: false,
998
1206
  stdio: ["ignore", "pipe", "pipe"],
999
1207
  env: fgChildEnv,
@@ -1210,7 +1418,7 @@ export async function runSingleAgent(
1210
1418
  agents,
1211
1419
  agentName,
1212
1420
  task,
1213
- cwd,
1421
+ effectiveCwd,
1214
1422
  step,
1215
1423
  signal,
1216
1424
  onUpdate,
@@ -1219,8 +1427,10 @@ export async function runSingleAgent(
1219
1427
  session,
1220
1428
  nextModel.id,
1221
1429
  parentModelId,
1222
- defaults
1223
- // Clear hints — the explicit model override will be used
1430
+ defaults,
1431
+ undefined,
1432
+ undefined
1433
+ // Clear hints and isolation override — explicit model + existing cwd are used
1224
1434
  );
1225
1435
  }
1226
1436
 
@@ -1228,5 +1438,6 @@ export async function runSingleAgent(
1228
1438
  } finally {
1229
1439
  completeForegroundSubagent(taskId, piEvents);
1230
1440
  cleanupTempFiles();
1441
+ cleanupIsolation("subagent", taskId, isolationContext, piEvents);
1231
1442
  }
1232
1443
  }
@@ -7,6 +7,14 @@ import { Type } from "@sinclair/typebox";
7
7
 
8
8
  // ── Parameter Schemas ────────────────────────────────────────────────────────
9
9
 
10
+ export const IsolationModeSchema = StringEnum(["worktree"] as const, {
11
+ description:
12
+ 'Isolation strategy for subagent execution. "worktree" runs the subagent in a temporary detached git worktree.',
13
+ });
14
+
15
+ /** Isolation mode supported by the subagent tool. */
16
+ export type IsolationMode = "worktree";
17
+
10
18
  export const TaskItem = Type.Object({
11
19
  agent: Type.String({ description: "Name of the agent to invoke" }),
12
20
  task: Type.String({ description: "Task to delegate to the agent" }),
@@ -14,6 +22,7 @@ export const TaskItem = Type.Object({
14
22
  model: Type.Optional(
15
23
  Type.String({ description: "Model ID to use for this agent (overrides agent default)" })
16
24
  ),
25
+ isolation: Type.Optional(IsolationModeSchema),
17
26
  });
18
27
 
19
28
  export const CentipedeItem = Type.Object({
@@ -25,6 +34,7 @@ export const CentipedeItem = Type.Object({
25
34
  model: Type.Optional(
26
35
  Type.String({ description: "Model ID to use for this step (overrides agent default)" })
27
36
  ),
37
+ isolation: Type.Optional(IsolationModeSchema),
28
38
  });
29
39
 
30
40
  export const AgentScopeSchema = StringEnum(["user", "project", "both"] as const, {
@@ -58,6 +68,7 @@ export const SubagentParams = Type.Object({
58
68
  cwd: Type.Optional(
59
69
  Type.String({ description: "Working directory for the agent process (single mode)" })
60
70
  ),
71
+ isolation: Type.Optional(IsolationModeSchema),
61
72
  background: Type.Optional(
62
73
  Type.Boolean({
63
74
  description: "Run in background, return immediately. Use subagent_status to check.",
@@ -16,6 +16,7 @@ import {
16
16
  type InteropSubagentView,
17
17
  } from "../_shared/interop-events.js";
18
18
  import { getFinalOutput, type SingleResult } from "./formatting.js";
19
+ import type { IsolationMode } from "./schema.js";
19
20
 
20
21
  // ── Types ────────────────────────────────────────────────────────────────────
21
22
 
@@ -23,6 +24,7 @@ import { getFinalOutput, type SingleResult } from "./formatting.js";
23
24
  export interface RunningSubagent {
24
25
  id: string;
25
26
  agent: string;
27
+ isolationMode?: IsolationMode;
26
28
  model?: string;
27
29
  task: string;
28
30
  startTime: number;
@@ -37,6 +39,7 @@ export interface BackgroundSubagent {
37
39
  historyOriginalMessageCount?: number;
38
40
  historyRetainedMessageCount?: number;
39
41
  id: string;
42
+ isolationMode?: IsolationMode;
40
43
  model?: string;
41
44
  process: ReturnType<typeof spawn>;
42
45
  result: SingleResult;
@@ -46,6 +49,7 @@ export interface BackgroundSubagent {
46
49
  task: string;
47
50
  tmpPromptDir?: string;
48
51
  tmpPromptPath?: string;
52
+ worktreePath?: string;
49
53
  }
50
54
 
51
55
  // ── State ────────────────────────────────────────────────────────────────────
@@ -324,9 +328,18 @@ export function registerForegroundSubagent(
324
328
  task: string,
325
329
  startTime: number,
326
330
  piEvents?: ExtensionAPI["events"],
327
- model?: string
331
+ model?: string,
332
+ isolationMode?: IsolationMode
328
333
  ): void {
329
- runningSubagents.set(id, { id, agent, model, task, startTime, status: "running" });
334
+ runningSubagents.set(id, {
335
+ id,
336
+ agent,
337
+ isolationMode,
338
+ model,
339
+ task,
340
+ startTime,
341
+ status: "running",
342
+ });
330
343
  publishSubagentSnapshot(piEvents);
331
344
  startWidgetUpdates();
332
345
  }
@@ -273,8 +273,8 @@ afterEach(() => {
273
273
  });
274
274
 
275
275
  describe("tasks widget subagent sections", () => {
276
- it("labels a foreground-only snapshot as blocking foreground subagents", async () => {
277
- const fixture = await createFixture();
276
+ it("omits subagent sections for foreground-only snapshots", async () => {
277
+ const fixture = await createFixture("Keep task list visible");
278
278
  try {
279
279
  emitSubagentsSnapshot(fixture.harness, {
280
280
  foreground: [
@@ -289,7 +289,8 @@ describe("tasks widget subagent sections", () => {
289
289
 
290
290
  const lines = renderWidget(fixture.captured, 90);
291
291
  const text = stripAnsi(lines.join("\n"));
292
- expect(text).toContain("Foreground Subagents (blocking · 1 running)");
292
+ expect(text).toContain("Tasks (0/1)");
293
+ expect(text).not.toContain("Foreground Subagents (blocking");
293
294
  expect(text).not.toContain("Background Subagents (non-blocking");
294
295
  expectLinesWithinWidth(lines, 90);
295
296
  } finally {
@@ -297,6 +298,26 @@ describe("tasks widget subagent sections", () => {
297
298
  }
298
299
  });
299
300
 
301
+ it("does not mount the widget when foreground subagents are the only activity", async () => {
302
+ const fixture = await createFixture();
303
+ try {
304
+ emitSubagentsSnapshot(fixture.harness, {
305
+ foreground: [
306
+ createSubagent({
307
+ agent: "reviewer",
308
+ id: "fg_only",
309
+ status: "running",
310
+ task: "Review changed files",
311
+ }),
312
+ ],
313
+ });
314
+
315
+ expect(fixture.captured.render).toBeNull();
316
+ } finally {
317
+ await shutdownFixture(fixture);
318
+ }
319
+ });
320
+
300
321
  it("labels a background-only snapshot as non-blocking background subagents", async () => {
301
322
  const fixture = await createFixture();
302
323
  try {
@@ -321,7 +342,7 @@ describe("tasks widget subagent sections", () => {
321
342
  }
322
343
  });
323
344
 
324
- it("renders separate foreground/background sections with distinct per-section counts", async () => {
345
+ it("renders only the background heading for mixed foreground/background snapshots", async () => {
325
346
  const fixture = await createFixture();
326
347
  try {
327
348
  emitSubagentsSnapshot(fixture.harness, {
@@ -345,8 +366,8 @@ describe("tasks widget subagent sections", () => {
345
366
 
346
367
  const lines = renderWidget(fixture.captured, 95);
347
368
  const text = stripAnsi(lines.join("\n"));
348
- expect(text).toContain("Foreground Subagents (blocking · 1 running)");
349
369
  expect(text).toContain("Background Subagents (non-blocking · 1 stalled)");
370
+ expect(text).not.toContain("Foreground Subagents (blocking");
350
371
  expect(text).not.toContain("Subagents (1 running · 1 stalled)");
351
372
  expectLinesWithinWidth(lines, 95);
352
373
  } finally {
@@ -363,10 +384,10 @@ describe("tasks widget subagent sections", () => {
363
384
 
364
385
  try {
365
386
  emitSubagentsSnapshot(fixture.harness, {
366
- foreground: [
387
+ background: [
367
388
  createSubagent({
368
389
  agent: "widthprobe",
369
- id: "width_fg",
390
+ id: "width_bg",
370
391
  status: "running",
371
392
  task: longTask,
372
393
  }),
@@ -233,22 +233,19 @@ export function registerTasksExtension(
233
233
  }
234
234
 
235
235
  /**
236
- * Render subagent lines (right column in side-by-side mode, or below tasks in stacked mode).
236
+ * Render background subagent lines (right column in side-by-side mode, or below tasks in
237
+ * stacked mode).
237
238
  */
238
239
  function renderSubagentLines(
239
240
  ctx: ExtensionContext,
240
241
  spinner: string,
241
- foreground: SubagentView[],
242
242
  background: SubagentView[],
243
243
  maxLineWidth: number
244
244
  ): string[] {
245
- const activeForeground = foreground.filter(
246
- (subagent) => subagent.status === "running" || subagent.status === "stalled"
247
- );
248
245
  const activeBackground = background.filter(
249
246
  (subagent) => subagent.status === "running" || subagent.status === "stalled"
250
247
  );
251
- if (activeForeground.length === 0 && activeBackground.length === 0) return [];
248
+ if (activeBackground.length === 0) return [];
252
249
 
253
250
  const safeLineWidth = Math.max(1, maxLineWidth);
254
251
 
@@ -355,21 +352,14 @@ export function registerTasksExtension(
355
352
  }
356
353
 
357
354
  /**
358
- * Append one subagent section (foreground/background) to the output.
355
+ * Append the background-subagent section to the output.
359
356
  * @param lines - Mutable output line sink
360
- * @param sectionTitle - Section title label
361
- * @param modeLabel - Blocking mode label
362
357
  * @param subagents - Section subagents
363
358
  */
364
- function appendSection(
365
- lines: string[],
366
- sectionTitle: string,
367
- modeLabel: string,
368
- subagents: SubagentView[]
369
- ): void {
359
+ function appendSection(lines: string[], subagents: SubagentView[]): void {
370
360
  if (subagents.length === 0) return;
371
361
 
372
- const sectionSummary = `${sectionTitle} (${modeLabel} · ${getStatusSummary(subagents)})`;
362
+ const sectionSummary = `Background Subagents (non-blocking · ${getStatusSummary(subagents)})`;
373
363
  lines.push(
374
364
  clampLine(`${formatWidgetRole(ctx, "title", sectionSummary)}${getModelSummary(subagents)}`)
375
365
  );
@@ -413,9 +403,7 @@ export function registerTasksExtension(
413
403
  }
414
404
 
415
405
  const lines: string[] = [];
416
- appendSection(lines, "Foreground Subagents", "blocking", activeForeground);
417
- if (activeForeground.length > 0 && activeBackground.length > 0) lines.push("");
418
- appendSection(lines, "Background Subagents", "non-blocking", activeBackground);
406
+ appendSection(lines, activeBackground);
419
407
  return lines;
420
408
  }
421
409
 
@@ -608,18 +596,15 @@ export function registerTasksExtension(
608
596
  return;
609
597
  }
610
598
 
611
- const fgSubagents = foregroundSubagents.filter(
612
- (subagent) => subagent.status === "running" || subagent.status === "stalled"
613
- );
614
599
  const bgSubagents = backgroundSubagents.filter(
615
600
  (subagent) => subagent.status === "running" || subagent.status === "stalled"
616
601
  );
617
602
  const runningBgTasks = backgroundTasks.filter((task) => task.status === "running");
618
603
 
619
- const hasSubagents = fgSubagents.length > 0 || bgSubagents.length > 0;
604
+ const hasBackgroundSubagents = bgSubagents.length > 0;
620
605
  const hasBgTasks = runningBgTasks.length > 0;
621
606
  const hasTeams = activeTeams.length > 0;
622
- const hasRightColumn = hasSubagents || hasBgTasks || hasTeams;
607
+ const hasRightColumn = hasBackgroundSubagents || hasBgTasks || hasTeams;
623
608
  const hasTasks = state.tasks.length > 0;
624
609
 
625
610
  if (!(state.visible && (hasTasks || hasRightColumn))) {
@@ -634,7 +619,6 @@ export function registerTasksExtension(
634
619
 
635
620
  // Build stable key for structure changes
636
621
  const taskStates = state.tasks.map((t) => `${t.id}:${t.status}`).join(",");
637
- const fgIds = fgSubagents.map((s) => `${s.id}:${s.status}`).join(",");
638
622
  const bgIds = bgSubagents.map((s) => `${s.id}:${s.status}`).join(",");
639
623
  const bgTaskIds = runningBgTasks.map((t) => t.id).join(",");
640
624
  const teamKey = activeTeams
@@ -643,7 +627,7 @@ export function registerTasksExtension(
643
627
  `${t.name}:${t.tasks.map((tk) => tk.status).join("")}:${t.teammates.map((m) => m.status).join("")}`
644
628
  )
645
629
  .join("|");
646
- const stableKey = `${taskStates}|${fgIds}|${bgIds}|${bgTaskIds}|${teamKey}`;
630
+ const stableKey = `${taskStates}|${bgIds}|${bgTaskIds}|${teamKey}`;
647
631
 
648
632
  // Re-render when structure changes, background items running (for animation),
649
633
  // or in_progress tasks exist (spinner needs to animate every frame).
@@ -678,16 +662,14 @@ export function registerTasksExtension(
678
662
 
679
663
  const taskLines = renderTaskLines(ctx, maxTitleLen);
680
664
 
681
- // Build right column: teams, then subagents, then bg tasks
665
+ // Build right column: teams, background subagents, then bg tasks
682
666
  const rightLines: string[] = [];
683
667
  if (hasTeams) {
684
668
  rightLines.push(...renderTeamLines(ctx, spinner, activeTeams, maxTeamLen));
685
669
  }
686
- if (hasSubagents) {
670
+ if (hasBackgroundSubagents) {
687
671
  if (rightLines.length > 0) rightLines.push(""); // Spacer
688
- rightLines.push(
689
- ...renderSubagentLines(ctx, spinner, fgSubagents, bgSubagents, rightColumnWidth)
690
- );
672
+ rightLines.push(...renderSubagentLines(ctx, spinner, bgSubagents, rightColumnWidth));
691
673
  }
692
674
  if (hasBgTasks) {
693
675
  if (rightLines.length > 0) rightLines.push(""); // Spacer
@@ -713,9 +695,9 @@ export function registerTasksExtension(
713
695
  lines.push(...renderTeamLines(ctx, spinner, activeTeams, maxTeamLen));
714
696
  }
715
697
 
716
- if (hasSubagents) {
698
+ if (hasBackgroundSubagents) {
717
699
  if (lines.length > 0) lines.push(""); // Spacer
718
- lines.push(...renderSubagentLines(ctx, spinner, fgSubagents, bgSubagents, width));
700
+ lines.push(...renderSubagentLines(ctx, spinner, bgSubagents, width));
719
701
  }
720
702
 
721
703
  if (hasBgTasks) {