@narumitw/pi-subagents 0.30.1 → 0.33.0

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/src/config-ui.ts CHANGED
@@ -16,23 +16,27 @@ import {
16
16
  import { type CompletionDelivery, discoverAgents } from "./agents.js";
17
17
  import type { ManagedAgent } from "./registry.js";
18
18
  import {
19
+ type DelegationWorkflow,
19
20
  hasOwn,
20
21
  inspectCompletionDeliverySettings,
22
+ inspectDelegationWorkflowSettings,
21
23
  readSubagentSettings,
22
24
  sameToolSet,
23
25
  uniqueToolNames,
24
26
  updateAgentToolsSetting,
25
27
  updateCompletionDeliverySetting,
28
+ updateDelegationWorkflowSetting,
26
29
  } from "./settings.js";
27
30
  import { formatStatefulAgentLine, type StatefulSubagentRuntimeStatus } from "./stateful.js";
28
31
 
29
32
  const SUBCOMMANDS: AutocompleteItem[] = [
30
- { value: "settings", label: "settings", description: "Configure completion delivery" },
33
+ { value: "settings", label: "settings", description: "Configure completion behavior" },
31
34
  { value: "status", label: "status", description: "Show effective subagent settings" },
32
35
  { value: "help", label: "help", description: "Show subagent settings help" },
33
36
  ];
34
37
 
35
38
  export interface SubagentSettingsRuntime {
39
+ getBlockingEnabled(): boolean;
36
40
  getCompletionDelivery(): CompletionDelivery;
37
41
  setCompletionDelivery(value: CompletionDelivery): void;
38
42
  getRuntimeStatus(): StatefulSubagentRuntimeStatus;
@@ -102,17 +106,11 @@ export class ToolToggleList {
102
106
 
103
107
  export function registerSubagentConfigCommand(pi: ExtensionAPI, runtime: SubagentSettingsRuntime) {
104
108
  registerSubagentPrimaryCommand(pi, runtime);
105
- pi.registerCommand("subagents:config", {
106
- description: "Configure user tool settings for each subagent",
107
- handler: async (_args, ctx) => {
108
- await showSubagentToolSettings(pi, ctx);
109
- },
110
- });
111
109
  }
112
110
 
113
111
  async function showSubagentToolSettings(pi: ExtensionAPI, ctx: ExtensionCommandContext) {
114
112
  if (ctx.mode !== "tui") {
115
- if (ctx.hasUI) ctx.ui.notify("/subagents:config requires TUI mode", "info");
113
+ if (ctx.hasUI) ctx.ui.notify("Agent tool settings require TUI mode", "info");
116
114
  return;
117
115
  }
118
116
 
@@ -292,7 +290,8 @@ async function showSubagentToolSettings(pi: ExtensionAPI, ctx: ExtensionCommandC
292
290
  }
293
291
  }
294
292
 
295
- type ManagerAction = "settings" | "agent-tools" | "agents" | "status" | "help";
293
+ type ManagerAction = "workflow" | "agents" | "completion" | "advanced" | "help";
294
+ type AdvancedAction = "agent-tools" | "status" | "back";
296
295
  type AgentManagerAction = "back" | "clear";
297
296
 
298
297
  function registerSubagentPrimaryCommand(pi: ExtensionAPI, runtime: SubagentSettingsRuntime) {
@@ -317,7 +316,7 @@ function registerSubagentPrimaryCommand(pi: ExtensionAPI, runtime: SubagentSetti
317
316
  showSubagentStatus(ctx, runtime);
318
317
  return;
319
318
  case "help":
320
- showSubagentHelp(ctx, runtime);
319
+ showSubagentHelp(ctx);
321
320
  return;
322
321
  default:
323
322
  if (ctx.mode === "tui" || ctx.hasUI) {
@@ -341,20 +340,20 @@ async function showSubagentManager(
341
340
  const action = await selectManagerAction(ctx, runtime);
342
341
  if (!action) return;
343
342
  switch (action) {
344
- case "settings":
345
- await showSubagentSettings(ctx, runtime);
346
- break;
347
- case "agent-tools":
348
- await showSubagentToolSettings(pi, ctx);
343
+ case "workflow":
344
+ if (await showDelegationWorkflow(ctx, runtime)) return;
349
345
  break;
350
346
  case "agents":
351
347
  await showCurrentSessionAgents(ctx, runtime);
352
348
  break;
353
- case "status":
354
- showSubagentStatus(ctx, runtime);
349
+ case "completion":
350
+ await showSubagentSettings(ctx, runtime);
351
+ break;
352
+ case "advanced":
353
+ await showAdvancedSettings(pi, ctx, runtime);
355
354
  break;
356
355
  case "help":
357
- showSubagentHelp(ctx, runtime);
356
+ showSubagentHelp(ctx);
358
357
  break;
359
358
  }
360
359
  }
@@ -365,24 +364,28 @@ async function selectManagerAction(
365
364
  runtime: SubagentSettingsRuntime,
366
365
  ): Promise<ManagerAction | null> {
367
366
  const status = runtime.getRuntimeStatus();
368
- const settings = inspectCompletionDeliverySettings();
367
+ const workflow = inspectDelegationWorkflowSettings();
369
368
  const items: SelectItem[] = [
370
369
  {
371
- value: "settings",
372
- label: "Completion settings",
373
- description: "Change user completion delivery for this and future sessions",
374
- },
375
- {
376
- value: "agent-tools",
377
- label: "Agent tool settings",
378
- description: "Configure persistent per-agent tool allow-lists",
370
+ value: "workflow",
371
+ label: "Change delegation",
372
+ description: "Choose all methods, async only, or blocking only",
379
373
  },
380
374
  {
381
375
  value: "agents",
382
- label: "Current-session agents",
376
+ label: "Current agents",
383
377
  description: `${status.activeAgents} active · ${status.retainedAgents} retained`,
384
378
  },
385
- { value: "status", label: "Status", description: "Show effective runtime and settings state" },
379
+ {
380
+ value: "completion",
381
+ label: "Completion behavior",
382
+ description: "Choose whether async completion waits or resumes automatically",
383
+ },
384
+ {
385
+ value: "advanced",
386
+ label: "Advanced settings",
387
+ description: "Agent permissions, runtime details, and settings path",
388
+ },
386
389
  { value: "help", label: "Help", description: "Show commands and manual configuration" },
387
390
  ];
388
391
  return ctx.ui.custom<ManagerAction | null>((tui, theme, _keybindings, done) => {
@@ -390,7 +393,9 @@ async function selectManagerAction(
390
393
  container.addChild(new DynamicBorder((text: string) => theme.fg("accent", text)));
391
394
  container.addChild(new Text(theme.fg("accent", theme.bold("Subagents")), 1, 0));
392
395
  container.addChild(new Spacer(1));
393
- container.addChild(new Text(theme.fg("muted", formatManagerSummary(status, settings)), 1, 0));
396
+ container.addChild(
397
+ new Text(theme.fg("muted", formatManagerSummary(runtime, status, workflow)), 1, 0),
398
+ );
394
399
  container.addChild(new Spacer(1));
395
400
  const selectList = new SelectList(items, Math.min(items.length + 2, 15), {
396
401
  selectedPrefix: (text: string) => theme.fg("accent", text),
@@ -415,6 +420,257 @@ async function selectManagerAction(
415
420
  });
416
421
  }
417
422
 
423
+ async function showDelegationWorkflow(
424
+ ctx: ExtensionCommandContext,
425
+ runtime: SubagentSettingsRuntime,
426
+ ): Promise<boolean> {
427
+ const snapshot = inspectDelegationWorkflowSettings();
428
+ if (ctx.mode !== "tui") {
429
+ if (ctx.hasUI) {
430
+ ctx.ui.notify(
431
+ `Edit delegation settings manually: ${safeTerminalText(snapshot.path)}`,
432
+ "info",
433
+ );
434
+ }
435
+ return false;
436
+ }
437
+ if (snapshot.error) {
438
+ ctx.ui.notify(
439
+ `Delegation settings cannot be edited: ${safeTerminalText(snapshot.error)}. Repair ${safeTerminalText(snapshot.path)} and retry.`,
440
+ "error",
441
+ );
442
+ return false;
443
+ }
444
+ const activeWorkflow = currentWorkflow(runtime, runtime.getRuntimeStatus());
445
+ const choices: SelectItem[] = [
446
+ {
447
+ value: "all",
448
+ label: "All delegation methods",
449
+ description: "Allow blocking batches and reusable async agents",
450
+ },
451
+ {
452
+ value: "async-only",
453
+ label: "Async only",
454
+ description: "Keep the root responsive; remove blocking subagent",
455
+ },
456
+ {
457
+ value: "blocking-only",
458
+ label: "Blocking only",
459
+ description: "Keep blocking batches; remove reusable async agents",
460
+ },
461
+ ];
462
+ const selected = await ctx.ui.custom<DelegationWorkflow | null>(
463
+ (tui, theme, _keybindings, done) => {
464
+ const container = new Container();
465
+ container.addChild(new DynamicBorder((text: string) => theme.fg("accent", text)));
466
+ container.addChild(new Text(theme.fg("accent", theme.bold("Change Delegation")), 1, 0));
467
+ container.addChild(
468
+ new Text(
469
+ theme.fg(
470
+ "muted",
471
+ [
472
+ `Current: ${workflowLabel(activeWorkflow)}`,
473
+ ...(snapshot.value !== activeWorkflow
474
+ ? [`Configured after reload: ${workflowLabel(snapshot.value)}`]
475
+ : []),
476
+ ].join("\n"),
477
+ ),
478
+ 1,
479
+ 0,
480
+ ),
481
+ );
482
+ container.addChild(new Spacer(1));
483
+ const selectList = new SelectList(choices, Math.min(choices.length + 2, 10), {
484
+ selectedPrefix: (text: string) => theme.fg("accent", text),
485
+ selectedText: (text: string) => theme.fg("accent", text),
486
+ description: (text: string) => theme.fg("muted", text),
487
+ scrollInfo: (text: string) => theme.fg("dim", text),
488
+ noMatch: (text: string) => theme.fg("warning", text),
489
+ });
490
+ selectList.setSelectedIndex(
491
+ Math.max(
492
+ 0,
493
+ choices.findIndex((item) => item.value === snapshot.value),
494
+ ),
495
+ );
496
+ selectList.onSelect = (item) => done(item.value as DelegationWorkflow);
497
+ selectList.onCancel = () => done(null);
498
+ container.addChild(selectList);
499
+ container.addChild(new Text(theme.fg("dim", "↑↓ navigate · enter preview · esc back"), 1, 0));
500
+ container.addChild(new DynamicBorder((text: string) => theme.fg("accent", text)));
501
+ return {
502
+ render: (width: number) => container.render(width),
503
+ invalidate: () => container.invalidate(),
504
+ handleInput(data: string) {
505
+ selectList.handleInput(data);
506
+ tui.requestRender();
507
+ },
508
+ };
509
+ },
510
+ );
511
+ if (!selected) return false;
512
+ if (selected === activeWorkflow && selected === snapshot.value) {
513
+ ctx.ui.notify(`Delegation already uses ${workflowLabel(selected)}.`, "info");
514
+ return false;
515
+ }
516
+ const requiresReload = selected !== activeWorkflow;
517
+ if (requiresReload && blockReloadWithRetainedAgents(ctx, runtime)) return false;
518
+
519
+ const confirmed = await showWorkflowPreview(ctx, activeWorkflow, selected, requiresReload);
520
+ if (!confirmed) return false;
521
+ if (requiresReload && blockReloadWithRetainedAgents(ctx, runtime)) return false;
522
+ try {
523
+ updateDelegationWorkflowSetting(selected as Exclude<DelegationWorkflow, "disabled">);
524
+ } catch (error) {
525
+ ctx.ui.notify(
526
+ `Delegation settings were not saved: ${formatError(error)}. The current workflow is unchanged.`,
527
+ "error",
528
+ );
529
+ return false;
530
+ }
531
+ if (!requiresReload) {
532
+ ctx.ui.notify(
533
+ `Saved ${workflowLabel(selected)}. The current tool surface already matches.`,
534
+ "info",
535
+ );
536
+ return false;
537
+ }
538
+ ctx.ui.notify(
539
+ `Saved ${workflowLabel(selected)}. Reloading subagent tools… If the tool surface does not refresh, run /reload.`,
540
+ "info",
541
+ );
542
+ await ctx.reload();
543
+ return true;
544
+ }
545
+
546
+ function blockReloadWithRetainedAgents(
547
+ ctx: ExtensionCommandContext,
548
+ runtime: SubagentSettingsRuntime,
549
+ ): boolean {
550
+ const status = runtime.getRuntimeStatus();
551
+ if (status.retainedAgents === 0) return false;
552
+ ctx.ui.notify(
553
+ `Cannot reload while ${status.retainedAgents} detached subagent${status.retainedAgents === 1 ? " is" : "s are"} retained (${status.activeAgents} active). Open Current agents and clear them after their work is safe to discard, then change delegation.`,
554
+ "warning",
555
+ );
556
+ return true;
557
+ }
558
+
559
+ async function showWorkflowPreview(
560
+ ctx: ExtensionCommandContext,
561
+ current: DelegationWorkflow,
562
+ next: DelegationWorkflow,
563
+ requiresReload: boolean,
564
+ ): Promise<boolean> {
565
+ const workflowChanges = workflowEffects(current, next);
566
+ const effects = (
567
+ workflowChanges.length > 0
568
+ ? workflowChanges
569
+ : ["Keep the current registered tools and cancel the pending workflow change"]
570
+ )
571
+ .map((effect) => `- ${effect}`)
572
+ .join("\n");
573
+ return ctx.ui.custom<boolean>((tui, theme, _keybindings, done) => {
574
+ const container = new Container();
575
+ container.addChild(new DynamicBorder((text: string) => theme.fg("accent", text)));
576
+ container.addChild(new Text(theme.fg("accent", theme.bold("Review Delegation Change")), 1, 0));
577
+ container.addChild(
578
+ new Text(
579
+ theme.fg(
580
+ "muted",
581
+ `Current: ${workflowLabel(current)}\nNew: ${workflowLabel(next)}\n\nEffect:\n${effects}\n- ${requiresReload ? "Reload the extension to apply this tool surface" : "No reload is needed because the active tools already match"}`,
582
+ ),
583
+ 1,
584
+ 0,
585
+ ),
586
+ );
587
+ container.addChild(new Spacer(1));
588
+ const actions: SelectItem[] = [
589
+ {
590
+ value: "save",
591
+ label: requiresReload ? "Save and reload" : "Save",
592
+ description: requiresReload
593
+ ? "Persist and apply this workflow"
594
+ : "Persist the workflow that is already active",
595
+ },
596
+ { value: "cancel", label: "Cancel", description: "Leave settings and tools unchanged" },
597
+ ];
598
+ const selectList = new SelectList(actions, 4, {
599
+ selectedPrefix: (text: string) => theme.fg("accent", text),
600
+ selectedText: (text: string) => theme.fg("accent", text),
601
+ description: (text: string) => theme.fg("muted", text),
602
+ scrollInfo: (text: string) => theme.fg("dim", text),
603
+ noMatch: (text: string) => theme.fg("warning", text),
604
+ });
605
+ selectList.onSelect = (item) => done(item.value === "save");
606
+ selectList.onCancel = () => done(false);
607
+ container.addChild(selectList);
608
+ container.addChild(new Text(theme.fg("dim", "↑↓ navigate · enter choose · esc cancel"), 1, 0));
609
+ container.addChild(new DynamicBorder((text: string) => theme.fg("accent", text)));
610
+ return {
611
+ render: (width: number) => container.render(width),
612
+ invalidate: () => container.invalidate(),
613
+ handleInput(data: string) {
614
+ selectList.handleInput(data);
615
+ tui.requestRender();
616
+ },
617
+ };
618
+ });
619
+ }
620
+
621
+ async function showAdvancedSettings(
622
+ pi: ExtensionAPI,
623
+ ctx: ExtensionCommandContext,
624
+ runtime: SubagentSettingsRuntime,
625
+ ) {
626
+ while (true) {
627
+ const action = await ctx.ui.custom<AdvancedAction | null>((tui, theme, _keybindings, done) => {
628
+ const container = new Container();
629
+ container.addChild(new DynamicBorder((text: string) => theme.fg("accent", text)));
630
+ container.addChild(
631
+ new Text(theme.fg("accent", theme.bold("Advanced Subagent Settings")), 1, 0),
632
+ );
633
+ container.addChild(new Spacer(1));
634
+ const items: SelectItem[] = [
635
+ {
636
+ value: "agent-tools",
637
+ label: "Agent tool permissions",
638
+ description: "Customize persistent per-agent tool allow-lists",
639
+ },
640
+ {
641
+ value: "status",
642
+ label: "Runtime details",
643
+ description: "Show transport, configured source, and settings path",
644
+ },
645
+ { value: "back", label: "Back", description: "Return to the Subagents manager" },
646
+ ];
647
+ const selectList = new SelectList(items, 5, {
648
+ selectedPrefix: (text: string) => theme.fg("accent", text),
649
+ selectedText: (text: string) => theme.fg("accent", text),
650
+ description: (text: string) => theme.fg("muted", text),
651
+ scrollInfo: (text: string) => theme.fg("dim", text),
652
+ noMatch: (text: string) => theme.fg("warning", text),
653
+ });
654
+ selectList.onSelect = (item) => done(item.value as AdvancedAction);
655
+ selectList.onCancel = () => done(null);
656
+ container.addChild(selectList);
657
+ container.addChild(new Text(theme.fg("dim", "↑↓ navigate · enter select · esc back"), 1, 0));
658
+ container.addChild(new DynamicBorder((text: string) => theme.fg("accent", text)));
659
+ return {
660
+ render: (width: number) => container.render(width),
661
+ invalidate: () => container.invalidate(),
662
+ handleInput(data: string) {
663
+ selectList.handleInput(data);
664
+ tui.requestRender();
665
+ },
666
+ };
667
+ });
668
+ if (!action || action === "back") return;
669
+ if (action === "agent-tools") await showSubagentToolSettings(pi, ctx);
670
+ else showSubagentStatus(ctx, runtime);
671
+ }
672
+ }
673
+
418
674
  async function showCurrentSessionAgents(
419
675
  ctx: ExtensionCommandContext,
420
676
  runtime: SubagentSettingsRuntime,
@@ -516,11 +772,11 @@ async function showSubagentSettings(
516
772
  const items: SettingItem[] = [
517
773
  {
518
774
  id: "completionDelivery",
519
- label: "Completion delivery",
775
+ label: "When async work finishes",
520
776
  description:
521
- "User setting applied now and to future sessions. next-turn queues completion; auto-resume requests synthesis after settlement.",
522
- currentValue,
523
- values: ["next-turn", "auto-resume"],
777
+ "Wait for your next turn, or request one synthesis turn after the root settles.",
778
+ currentValue: completionLabel(currentValue),
779
+ values: ["Wait until my next turn", "Resume automatically when finished"],
524
780
  },
525
781
  ];
526
782
  const container = new Container();
@@ -541,17 +797,15 @@ async function showSubagentSettings(
541
797
  (id, newValue) => {
542
798
  if (id !== "completionDelivery") return;
543
799
  const previous = currentValue;
544
- const next = newValue as CompletionDelivery;
800
+ const next: CompletionDelivery =
801
+ newValue === "Resume automatically when finished" ? "auto-resume" : "next-turn";
545
802
  try {
546
803
  updateCompletionDeliverySetting(next);
547
804
  runtime.setCompletionDelivery(next);
548
805
  currentValue = next;
549
- ctx.ui.notify(
550
- `User completion delivery set to ${next} for this and future sessions.`,
551
- "info",
552
- );
806
+ ctx.ui.notify(`Saved and applied: ${completionLabel(next)}.`, "info");
553
807
  } catch (error) {
554
- settingsList.updateValue(id, previous);
808
+ settingsList.updateValue(id, completionLabel(previous));
555
809
  ctx.ui.notify(`Subagent settings were not saved: ${formatError(error)}`, "error");
556
810
  }
557
811
  tui.requestRender();
@@ -574,25 +828,20 @@ function showSubagentStatus(ctx: ExtensionCommandContext, runtime: SubagentSetti
574
828
  if (ctx.mode !== "tui" && !ctx.hasUI) return;
575
829
  const snapshot = inspectCompletionDeliverySettings();
576
830
  ctx.ui.notify(
577
- formatStatus(runtime.getRuntimeStatus(), snapshot),
831
+ formatStatus(runtime.getRuntimeStatus(), snapshot, runtime),
578
832
  snapshot.error ? "warning" : "info",
579
833
  );
580
834
  }
581
835
 
582
- function showSubagentHelp(ctx: ExtensionCommandContext, runtime: SubagentSettingsRuntime) {
836
+ function showSubagentHelp(ctx: ExtensionCommandContext) {
583
837
  if (ctx.mode !== "tui" && !ctx.hasUI) return;
584
838
  const snapshot = inspectCompletionDeliverySettings();
585
- const runtimeStatus = runtime.getRuntimeStatus();
586
839
  ctx.ui.notify(
587
840
  [
588
- "/subagents — open the current-session manager",
589
- "/subagents settings — configure user completion delivery",
841
+ "/subagents — choose delegation workflow, manage current agents, and configure agent tools",
842
+ "/subagents settings — configure async completion behavior",
590
843
  "/subagents status — show current-session and user-setting values",
591
844
  "/subagents help — show this help",
592
- "/subagents:config — compatibility route for per-agent user tool settings",
593
- runtimeStatus.enabled
594
- ? "/subagents:agents list|clear — compatibility route for current-session agents"
595
- : "/subagents:agents — unavailable while stateful lifecycle tools are disabled",
596
845
  `User settings: ${safeTerminalText(snapshot.path)}`,
597
846
  ].join("\n"),
598
847
  "info",
@@ -600,41 +849,48 @@ function showSubagentHelp(ctx: ExtensionCommandContext, runtime: SubagentSetting
600
849
  }
601
850
 
602
851
  function formatManagerSummary(
852
+ runtime: SubagentSettingsRuntime,
603
853
  status: StatefulSubagentRuntimeStatus,
604
- settings: ReturnType<typeof inspectCompletionDeliverySettings>,
854
+ configured: ReturnType<typeof inspectDelegationWorkflowSettings>,
605
855
  ): string {
856
+ const current = currentWorkflow(runtime, status);
606
857
  return [
607
- "Current session",
608
- `Lifecycle: ${status.enabled ? "enabled" : "disabled"}${status.initialized ? " · initialized" : " · not initialized"}`,
609
- `Transport: ${status.transport}`,
610
- `Completion delivery: ${status.completionDelivery}`,
858
+ `Delegation: ${workflowLabel(current)}`,
859
+ `Completion: ${completionLabel(status.completionDelivery)}`,
611
860
  `Agents: ${status.activeAgents} active · ${status.retainedAgents} retained`,
612
- "",
613
- "User settings",
614
- `Completion source: ${settings.source}`,
615
- `Path: ${safeTerminalText(settings.path)}`,
616
- ...(settings.error ? [`Warning: ${safeTerminalText(settings.error)}`] : []),
861
+ ...(configured.value !== current
862
+ ? [`Configured after reload: ${workflowLabel(configured.value)}`]
863
+ : []),
864
+ ...(configured.error ? ["Settings need repair; open Advanced settings for details."] : []),
617
865
  ].join("\n");
618
866
  }
619
867
 
620
868
  function formatStatus(
621
869
  status: StatefulSubagentRuntimeStatus,
622
870
  snapshot: ReturnType<typeof inspectCompletionDeliverySettings>,
871
+ runtime?: SubagentSettingsRuntime,
623
872
  ): string {
873
+ const configuredWorkflow = inspectDelegationWorkflowSettings();
874
+ const current = runtime ? currentWorkflow(runtime, status) : configuredWorkflow.value;
624
875
  return [
625
876
  "Current session",
626
- ` Lifecycle: ${status.enabled ? "enabled" : "disabled"}`,
627
- ` Runtime: ${status.initialized ? "initialized" : "not initialized"}`,
877
+ ` Delegation: ${workflowLabel(current)}`,
878
+ ` Async runtime: ${status.initialized ? "initialized" : status.enabled ? "not initialized" : "disabled"}`,
628
879
  ` Transport: ${status.transport}`,
629
- ` Completion delivery: ${status.completionDelivery}`,
880
+ ` Completion: ${completionLabel(status.completionDelivery)}`,
630
881
  ` Agents: ${status.activeAgents} active, ${status.retainedAgents} retained`,
631
882
  "User settings",
883
+ ` Delegation source: ${configuredWorkflow.source}`,
884
+ ` Configured delegation: ${workflowLabel(configuredWorkflow.value)}`,
632
885
  ` Completion source: ${snapshot.source}`,
886
+ ` Configured completion: ${completionLabel(snapshot.value)}`,
633
887
  ` Path: ${safeTerminalText(snapshot.path)}`,
634
- ` Configured completion delivery: ${snapshot.value}`,
635
- snapshot.error ? ` Warning: ${safeTerminalText(snapshot.error)}` : " Warning: none",
636
- "User settings persist for future sessions; /subagents settings also applies changes now.",
637
- "Manual file changes require /reload.",
888
+ configuredWorkflow.error || snapshot.error
889
+ ? ` Warning: ${safeTerminalText(configuredWorkflow.error ?? snapshot.error ?? "invalid settings")}`
890
+ : " Warning: none",
891
+ configuredWorkflow.value !== current
892
+ ? "Configured delegation differs from this session. Run /reload to apply it."
893
+ : "Manual file changes require /reload.",
638
894
  ].join("\n");
639
895
  }
640
896
 
@@ -644,6 +900,52 @@ function formatEmptyRuntime(status: StatefulSubagentRuntimeStatus): string {
644
900
  return "No current-session subagents.";
645
901
  }
646
902
 
903
+ function currentWorkflow(
904
+ runtime: SubagentSettingsRuntime,
905
+ status: StatefulSubagentRuntimeStatus,
906
+ ): DelegationWorkflow {
907
+ const blocking = runtime.getBlockingEnabled();
908
+ if (blocking && status.enabled) return "all";
909
+ if (status.enabled) return "async-only";
910
+ if (blocking) return "blocking-only";
911
+ return "disabled";
912
+ }
913
+
914
+ function workflowLabel(value: DelegationWorkflow): string {
915
+ switch (value) {
916
+ case "all":
917
+ return "All delegation methods";
918
+ case "async-only":
919
+ return "Async only";
920
+ case "blocking-only":
921
+ return "Blocking only";
922
+ case "disabled":
923
+ return "Delegation disabled";
924
+ }
925
+ }
926
+
927
+ function completionLabel(value: CompletionDelivery): string {
928
+ return value === "auto-resume" ? "Resume automatically when finished" : "Wait until my next turn";
929
+ }
930
+
931
+ function workflowEffects(current: DelegationWorkflow, next: DelegationWorkflow): string[] {
932
+ const blockingEnabled = (value: DelegationWorkflow) =>
933
+ value === "all" || value === "blocking-only";
934
+ const asyncEnabled = (value: DelegationWorkflow) => value === "all" || value === "async-only";
935
+ const effects: string[] = [];
936
+ if (blockingEnabled(current) !== blockingEnabled(next)) {
937
+ effects.push(blockingEnabled(next) ? "Add blocking `subagent`" : "Remove blocking `subagent`");
938
+ }
939
+ if (asyncEnabled(current) !== asyncEnabled(next)) {
940
+ effects.push(
941
+ asyncEnabled(next)
942
+ ? "Add reusable async lifecycle tools"
943
+ : "Remove reusable async lifecycle tools",
944
+ );
945
+ }
946
+ return effects;
947
+ }
948
+
647
949
  function safeTerminalText(value: string): string {
648
950
  // biome-ignore lint/suspicious/noControlCharactersInRegex: Escape untrusted terminal controls.
649
951
  return value.replace(/[\u0000-\u001f\u007f-\u009f]/gu, "?");
package/src/context.ts CHANGED
@@ -1,9 +1,5 @@
1
1
  import { createHash } from "node:crypto";
2
- import {
3
- DEFAULT_MAX_CONTEXT_BYTES,
4
- truncateUtf8,
5
- truncateUtf8Tail,
6
- } from "./limits.js";
2
+ import { DEFAULT_MAX_CONTEXT_BYTES, truncateUtf8, truncateUtf8Tail } from "./limits.js";
7
3
 
8
4
  export type ContextMode = "none" | "all" | "summary" | number;
9
5
 
@@ -107,19 +103,20 @@ export function buildContextSnapshot(
107
103
  const raw =
108
104
  mode === "summary" && selected.length > 4
109
105
  ? [
110
- `## Earlier context checkpoint\n${truncateUtf8(
111
- selected
112
- .slice(0, -4)
113
- .map((message) => `${message.role}: ${message.text}`)
114
- .join("\n"),
115
- Math.floor(maxBytes / 3),
116
- ).text}`,
117
- ...selected
118
- .slice(-4)
119
- .map((message) => `## ${message.role}\n${message.text}`),
106
+ `## Earlier context checkpoint\n${
107
+ truncateUtf8(
108
+ selected
109
+ .slice(0, -4)
110
+ .map((message) => `${message.role}: ${message.text}`)
111
+ .join("\n"),
112
+ Math.floor(maxBytes / 3),
113
+ ).text
114
+ }`,
115
+ ...selected.slice(-4).map((message) => `## ${message.role}\n${message.text}`),
120
116
  ].join("\n\n")
121
117
  : selected.map((message) => `## ${message.role}\n${message.text}`).join("\n\n");
122
- const bounded = mode === "summary" ? truncateUtf8Tail(raw, maxBytes) : truncateUtf8(raw, maxBytes);
118
+ const bounded =
119
+ mode === "summary" ? truncateUtf8Tail(raw, maxBytes) : truncateUtf8(raw, maxBytes);
123
120
  return {
124
121
  text: bounded.text,
125
122
  turns: selected.filter((message) => message.role === "user").length,