@narumitw/pi-subagents 0.42.0 → 0.43.1

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
@@ -1,23 +1,33 @@
1
1
  import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
2
2
  import { defineMenu, runMenu } from "@narumitw/pi-tui-kit";
3
- import { type CompletionDelivery, discoverAgents } from "./agents.js";
3
+ import {
4
+ type CompletionDelivery,
5
+ type ConsultationCwdPolicy,
6
+ type ConsultResourcePolicy,
7
+ type DelegationCwdPolicy,
8
+ discoverAgents,
9
+ } from "./agents.js";
4
10
  import type { ManagedAgent } from "./registry.js";
5
11
  import {
6
12
  type DelegationWorkflow,
7
13
  hasOwn,
8
14
  inspectCompletionDeliverySettings,
15
+ inspectConsultResourceSettings,
16
+ inspectCwdPolicySettings,
9
17
  inspectDelegationWorkflowSettings,
10
18
  readSubagentSettings,
11
19
  sameToolSet,
12
20
  uniqueToolNames,
13
21
  updateAgentToolsSetting,
14
22
  updateCompletionDeliverySetting,
23
+ updateConsultResourceSetting,
24
+ updateCwdPolicySetting,
15
25
  updateDelegationWorkflowSetting,
16
26
  } from "./settings.js";
17
27
  import { formatStatefulAgentLine, type StatefulSubagentRuntimeStatus } from "./stateful.js";
18
28
 
19
29
  const SUBCOMMANDS = [
20
- { value: "settings", label: "settings", description: "Configure completion behavior" },
30
+ { value: "settings", label: "settings", description: "Configure subagent user settings" },
21
31
  { value: "status", label: "status", description: "Show effective subagent settings" },
22
32
  { value: "help", label: "help", description: "Show subagent settings help" },
23
33
  ];
@@ -26,7 +36,13 @@ const TOOL_VIEWPORT_SIZE = 10;
26
36
  export interface SubagentSettingsRuntime {
27
37
  getBlockingEnabled(): boolean;
28
38
  getCompletionDelivery(): CompletionDelivery;
39
+ getConsultResourcePolicy(): ConsultResourcePolicy;
40
+ getConsultationCwdPolicy(): ConsultationCwdPolicy;
41
+ getDelegationCwdPolicy(): DelegationCwdPolicy;
29
42
  setCompletionDelivery(value: CompletionDelivery): void;
43
+ setConsultResourcePolicy(value: ConsultResourcePolicy): void;
44
+ setConsultationCwdPolicy(value: ConsultationCwdPolicy): void;
45
+ setDelegationCwdPolicy(value: DelegationCwdPolicy): void;
30
46
  getRuntimeStatus(): StatefulSubagentRuntimeStatus;
31
47
  listAgents(includeClosed?: boolean): ManagedAgent[];
32
48
  clearAgents(): Promise<number>;
@@ -86,7 +102,7 @@ function registerSubagentPrimaryCommand(
86
102
  showSubagentStatus(ctx, runtime);
87
103
  return;
88
104
  case "help":
89
- showSubagentHelp(ctx);
105
+ showSubagentHelp(ctx, runtime);
90
106
  return;
91
107
  default:
92
108
  if (ctx.mode === "tui" || ctx.hasUI) {
@@ -114,7 +130,7 @@ async function showSubagentManager(
114
130
  | "main"
115
131
  | "workflow"
116
132
  | "agents"
117
- | "completion"
133
+ | "settings"
118
134
  | "advanced"
119
135
  | "status"
120
136
  | "help"
@@ -124,6 +140,9 @@ async function showSubagentManager(
124
140
  | "set-workflow"
125
141
  | "clear-agents"
126
142
  | "set-completion"
143
+ | "set-consult-resources"
144
+ | "set-consultation-cwd"
145
+ | "set-delegation-cwd"
127
146
  | "load-agent-picker"
128
147
  | "pick-agent"
129
148
  | "toggle-tool"
@@ -154,10 +173,10 @@ async function showSubagentManager(
154
173
  to: "agents",
155
174
  },
156
175
  {
157
- id: "completion",
158
- label: "Completion behavior",
159
- description: "Choose whether async completion waits or resumes automatically",
160
- to: "completion",
176
+ id: "settings",
177
+ label: "Settings",
178
+ description: "Configure targets, trusted resources, and async completion",
179
+ to: "settings",
161
180
  },
162
181
  {
163
182
  id: "advanced",
@@ -236,7 +255,7 @@ async function showSubagentManager(
236
255
  hint: "back",
237
256
  };
238
257
  },
239
- completion: () => completionSettingsScreen(),
258
+ settings: () => subagentSettingsScreen(runtime),
240
259
  advanced: () => ({
241
260
  kind: "actions",
242
261
  title: "Advanced Subagent Settings",
@@ -266,7 +285,7 @@ async function showSubagentManager(
266
285
  help: () => ({
267
286
  kind: "detail",
268
287
  title: "Subagents help",
269
- lines: helpLines(),
288
+ lines: helpLines(runtime),
270
289
  hint: "back",
271
290
  }),
272
291
  "agent-picker": () => {
@@ -388,6 +407,10 @@ async function showSubagentManager(
388
407
  return { kind: "stay" };
389
408
  },
390
409
  "set-completion": async ({ value }) => applyCompletionSetting(value, ctx, runtime),
410
+ "set-consult-resources": async ({ value }) =>
411
+ applyConsultResourceSetting(value, ctx, runtime),
412
+ "set-consultation-cwd": async ({ value }) => applyConsultationCwdSetting(value, ctx, runtime),
413
+ "set-delegation-cwd": async ({ value }) => applyDelegationCwdSetting(value, ctx, runtime),
391
414
  "load-agent-picker": async () => {
392
415
  availableAgents = discoverAgents(ctx.cwd, "user", readSubagentSettings() ?? {}).agents;
393
416
  if (availableAgents.length === 0) {
@@ -468,7 +491,7 @@ async function showSubagentSettings(
468
491
  runtime: SubagentSettingsRuntime,
469
492
  owner: MenuOwner,
470
493
  ) {
471
- const snapshot = inspectCompletionDeliverySettings();
494
+ const snapshot = inspectConsultResourceSettings();
472
495
  if (ctx.mode !== "tui") {
473
496
  if (ctx.hasUI) {
474
497
  ctx.ui.notify(
@@ -479,11 +502,20 @@ async function showSubagentSettings(
479
502
  return;
480
503
  }
481
504
  const generation = owner.generation;
482
- const menu = defineMenu<undefined, "completion", "set-completion", ExtensionCommandContext>({
483
- start: "completion",
484
- screens: { completion: () => completionSettingsScreen() },
505
+ type SettingsAction =
506
+ | "set-completion"
507
+ | "set-consult-resources"
508
+ | "set-consultation-cwd"
509
+ | "set-delegation-cwd";
510
+ const menu = defineMenu<undefined, "settings", SettingsAction, ExtensionCommandContext>({
511
+ start: "settings",
512
+ screens: { settings: () => subagentSettingsScreen(runtime) },
485
513
  actions: {
486
514
  "set-completion": async ({ value }) => applyCompletionSetting(value, ctx, runtime),
515
+ "set-consult-resources": async ({ value }) =>
516
+ applyConsultResourceSetting(value, ctx, runtime),
517
+ "set-consultation-cwd": async ({ value }) => applyConsultationCwdSetting(value, ctx, runtime),
518
+ "set-delegation-cwd": async ({ value }) => applyDelegationCwdSetting(value, ctx, runtime),
487
519
  },
488
520
  });
489
521
  await runMenu(ctx, menu, {
@@ -493,25 +525,61 @@ async function showSubagentSettings(
493
525
  });
494
526
  }
495
527
 
496
- function completionSettingsScreen() {
497
- const snapshot = inspectCompletionDeliverySettings();
528
+ function subagentSettingsScreen(runtime: SubagentSettingsRuntime) {
529
+ const completion = inspectCompletionDeliverySettings();
530
+ const consult = inspectConsultResourceSettings();
531
+ const cwdPolicy = inspectCwdPolicySettings();
532
+ const error = completion.error ?? consult.error ?? cwdPolicy.error;
498
533
  return {
499
534
  kind: "settings" as const,
500
- title: snapshot.error ? "Subagent User Settings · Read only" : "Subagent User Settings",
535
+ title: error ? "Subagent User Settings · Read only" : "Subagent User Settings",
501
536
  lines: [
502
537
  "Applies now and to future sessions",
503
- safeTerminalText(snapshot.path),
504
- ...(snapshot.error ? [`Settings cannot be edited: ${safeTerminalText(snapshot.error)}`] : []),
538
+ "Target and trust settings control startup resources, not filesystem access or sandboxing.",
539
+ "Manage folder trust with Pi /trust; restart Pi after changing it.",
540
+ safeTerminalText(consult.path),
541
+ ...(error ? [`Settings cannot be edited: ${safeTerminalText(error)}`] : []),
505
542
  ],
506
- items: snapshot.error
543
+ items: error
507
544
  ? []
508
545
  : [
546
+ {
547
+ id: "consultationCwd",
548
+ label: "Read-only consultation target",
549
+ description:
550
+ "Untrusted external targets inherit no target/project resources; agent and package read-only prompts remain.",
551
+ currentValue: consultationCwdLabel(runtime.getConsultationCwdPolicy()),
552
+ values: ["Anywhere · untrusted targets inherit nothing", "Current workspace only"],
553
+ action: "set-consultation-cwd" as const,
554
+ },
555
+ {
556
+ id: "delegationCwd",
557
+ label: "General delegation target",
558
+ description:
559
+ "Controls starting directories, not absolute paths, shell commands, or OS permissions.",
560
+ currentValue: delegationCwdLabel(runtime.getDelegationCwdPolicy()),
561
+ values: [
562
+ "Current or saved-trusted folders",
563
+ "Current workspace only",
564
+ "Anywhere · normal Pi permissions",
565
+ ],
566
+ action: "set-delegation-cwd" as const,
567
+ },
568
+ {
569
+ id: "consultResources",
570
+ label: "Consultation resources for trusted targets",
571
+ description:
572
+ "Choose which trusted context, system, skill, and prompt resources a consultation inherits.",
573
+ currentValue: consultResourceLabel(runtime.getConsultResourcePolicy()),
574
+ values: ["Project context only", "No inherited resources", "All trusted resources"],
575
+ action: "set-consult-resources" as const,
576
+ },
509
577
  {
510
578
  id: "completionDelivery",
511
579
  label: "When async work finishes",
512
580
  description:
513
581
  "Wait for your next turn, or request one synthesis turn after the root settles.",
514
- currentValue: completionLabel(snapshot.value),
582
+ currentValue: completionLabel(runtime.getCompletionDelivery()),
515
583
  values: ["Wait until my next turn", "Resume automatically when finished"],
516
584
  action: "set-completion" as const,
517
585
  },
@@ -539,6 +607,74 @@ function applyCompletionSetting(
539
607
  }
540
608
  }
541
609
 
610
+ function applyConsultResourceSetting(
611
+ value: string | undefined,
612
+ ctx: ExtensionCommandContext,
613
+ runtime: SubagentSettingsRuntime,
614
+ ) {
615
+ const previous = runtime.getConsultResourcePolicy();
616
+ const next: ConsultResourcePolicy =
617
+ value === "No inherited resources"
618
+ ? "none"
619
+ : value === "All trusted resources"
620
+ ? "all"
621
+ : "project-context";
622
+ if (next === previous) return { kind: "stay" as const };
623
+ try {
624
+ updateConsultResourceSetting(next);
625
+ runtime.setConsultResourcePolicy(next);
626
+ ctx.ui.notify(`Saved and applied: ${consultResourceLabel(next)}.`, "info");
627
+ return { kind: "stay" as const };
628
+ } catch (error) {
629
+ ctx.ui.notify(`Subagent settings were not saved: ${formatError(error)}`, "error");
630
+ return { kind: "rejected" as const };
631
+ }
632
+ }
633
+
634
+ function applyConsultationCwdSetting(
635
+ value: string | undefined,
636
+ ctx: ExtensionCommandContext,
637
+ runtime: SubagentSettingsRuntime,
638
+ ) {
639
+ const previous = runtime.getConsultationCwdPolicy();
640
+ const next: ConsultationCwdPolicy =
641
+ value === "Current workspace only" ? "current-workspace" : "anywhere";
642
+ if (next === previous) return { kind: "stay" as const };
643
+ try {
644
+ updateCwdPolicySetting("consultation", next);
645
+ runtime.setConsultationCwdPolicy(next);
646
+ ctx.ui.notify(`Saved and applied: ${consultationCwdLabel(next)}.`, "info");
647
+ return { kind: "stay" as const };
648
+ } catch (error) {
649
+ ctx.ui.notify(`Subagent settings were not saved: ${formatError(error)}`, "error");
650
+ return { kind: "rejected" as const };
651
+ }
652
+ }
653
+
654
+ function applyDelegationCwdSetting(
655
+ value: string | undefined,
656
+ ctx: ExtensionCommandContext,
657
+ runtime: SubagentSettingsRuntime,
658
+ ) {
659
+ const previous = runtime.getDelegationCwdPolicy();
660
+ const next: DelegationCwdPolicy =
661
+ value === "Current workspace only"
662
+ ? "current-workspace"
663
+ : value === "Anywhere · normal Pi permissions"
664
+ ? "anywhere"
665
+ : "trusted-targets";
666
+ if (next === previous) return { kind: "stay" as const };
667
+ try {
668
+ updateCwdPolicySetting("delegation", next);
669
+ runtime.setDelegationCwdPolicy(next);
670
+ ctx.ui.notify(`Saved and applied: ${delegationCwdLabel(next)}.`, "info");
671
+ return { kind: "stay" as const };
672
+ } catch (error) {
673
+ ctx.ui.notify(`Subagent settings were not saved: ${formatError(error)}`, "error");
674
+ return { kind: "rejected" as const };
675
+ }
676
+ }
677
+
542
678
  function blockReloadWithRetainedAgents(
543
679
  ctx: ExtensionCommandContext,
544
680
  runtime: SubagentSettingsRuntime,
@@ -583,9 +719,9 @@ function showSubagentStatus(ctx: ExtensionCommandContext, runtime: SubagentSetti
583
719
  );
584
720
  }
585
721
 
586
- function showSubagentHelp(ctx: ExtensionCommandContext) {
722
+ function showSubagentHelp(ctx: ExtensionCommandContext, runtime: SubagentSettingsRuntime) {
587
723
  if (ctx.mode !== "tui" && !ctx.hasUI) return;
588
- ctx.ui.notify(helpLines().join("\n"), "info");
724
+ ctx.ui.notify(helpLines(runtime).join("\n"), "info");
589
725
  }
590
726
 
591
727
  function statusLines(runtime: SubagentSettingsRuntime): string[] {
@@ -593,13 +729,20 @@ function statusLines(runtime: SubagentSettingsRuntime): string[] {
593
729
  return formatStatus(runtime.getRuntimeStatus(), snapshot, runtime).split("\n");
594
730
  }
595
731
 
596
- function helpLines(): string[] {
732
+ function helpLines(runtime: SubagentSettingsRuntime): string[] {
597
733
  const snapshot = inspectCompletionDeliverySettings();
734
+ const cwdPolicy = inspectCwdPolicySettings();
598
735
  return [
599
736
  "/subagents — choose delegation workflow, manage current agents, and configure agent tools",
600
- "/subagents settings — configure async completion behavior",
737
+ "/subagents settings — configure target locations, trusted resources, and async completion",
601
738
  "/subagents status — show current-session and user-setting values",
602
739
  "/subagents help — show this help",
740
+ "Target policies control startup directories and resources, not filesystem access or sandboxing.",
741
+ "Manage saved folder trust with Pi /trust and restart Pi after changing it.",
742
+ `Runtime consultation target: ${consultationCwdLabel(runtime.getConsultationCwdPolicy())}`,
743
+ `Configured consultation target: ${consultationCwdLabel(cwdPolicy.consultation.value)} (${cwdPolicy.consultation.source})`,
744
+ `Runtime delegation target: ${delegationCwdLabel(runtime.getDelegationCwdPolicy())}`,
745
+ `Configured delegation target: ${delegationCwdLabel(cwdPolicy.delegation.value)} (${cwdPolicy.delegation.source})`,
603
746
  `User settings: ${safeTerminalText(snapshot.path)}`,
604
747
  ];
605
748
  }
@@ -610,9 +753,18 @@ function formatManagerSummary(
610
753
  configured: ReturnType<typeof inspectDelegationWorkflowSettings>,
611
754
  ): string {
612
755
  const current = currentWorkflow(runtime, status);
756
+ const cwdPolicy = inspectCwdPolicySettings();
757
+ const consult = inspectConsultResourceSettings();
613
758
  return [
614
759
  `Delegation: ${workflowLabel(current)}`,
615
760
  `Completion: ${completionLabel(status.completionDelivery)}`,
761
+ `Consult target: ${consultationCwdLabel(runtime.getConsultationCwdPolicy())}`,
762
+ `Delegation target: ${delegationCwdLabel(runtime.getDelegationCwdPolicy())}`,
763
+ `Consult resources: ${consultResourceLabel(runtime.getConsultResourcePolicy())}`,
764
+ `Configured consult target: ${consultationCwdLabel(cwdPolicy.consultation.value)} · ${cwdPolicy.consultation.source}`,
765
+ `Configured delegation target: ${delegationCwdLabel(cwdPolicy.delegation.value)} · ${cwdPolicy.delegation.source}`,
766
+ `Configured consult resources: ${consultResourceLabel(consult.value)} · ${consult.source}`,
767
+ `Settings: ${safeTerminalText(cwdPolicy.path)}`,
616
768
  `Agents: ${status.activeAgents} active · ${status.retainedAgents} retained`,
617
769
  ...(configured.value !== current
618
770
  ? [`Configured after reload: ${workflowLabel(configured.value)}`]
@@ -627,6 +779,8 @@ function formatStatus(
627
779
  runtime?: SubagentSettingsRuntime,
628
780
  ): string {
629
781
  const configuredWorkflow = inspectDelegationWorkflowSettings();
782
+ const consult = inspectConsultResourceSettings();
783
+ const cwdPolicy = inspectCwdPolicySettings();
630
784
  const current = runtime ? currentWorkflow(runtime, status) : configuredWorkflow.value;
631
785
  return [
632
786
  "Current session",
@@ -634,15 +788,24 @@ function formatStatus(
634
788
  ` Async runtime: ${status.initialized ? "initialized" : status.enabled ? "not initialized" : "disabled"}`,
635
789
  ` Transport: ${status.transport}`,
636
790
  ` Completion: ${completionLabel(status.completionDelivery)}`,
791
+ ` Consultation target: ${consultationCwdLabel(runtime?.getConsultationCwdPolicy() ?? cwdPolicy.consultation.value)}`,
792
+ ` Delegation target: ${delegationCwdLabel(runtime?.getDelegationCwdPolicy() ?? cwdPolicy.delegation.value)}`,
793
+ ` Consultation resources: ${consultResourceLabel(runtime?.getConsultResourcePolicy() ?? consult.value)}`,
637
794
  ` Agents: ${status.activeAgents} active, ${status.retainedAgents} retained`,
638
795
  "User settings",
639
796
  ` Delegation source: ${configuredWorkflow.source}`,
640
797
  ` Configured delegation: ${workflowLabel(configuredWorkflow.value)}`,
641
798
  ` Completion source: ${snapshot.source}`,
642
799
  ` Configured completion: ${completionLabel(snapshot.value)}`,
800
+ ` Configured consultation target: ${consultationCwdLabel(cwdPolicy.consultation.value)}`,
801
+ ` Consultation target source: ${cwdPolicy.consultation.source}`,
802
+ ` Configured delegation target: ${delegationCwdLabel(cwdPolicy.delegation.value)}`,
803
+ ` Delegation target source: ${cwdPolicy.delegation.source}`,
804
+ ` Configured consultation resources: ${consultResourceLabel(consult.value)}`,
805
+ ` Consultation resource source: ${consult.source}`,
643
806
  ` Path: ${safeTerminalText(snapshot.path)}`,
644
- configuredWorkflow.error || snapshot.error
645
- ? ` Warning: ${safeTerminalText(configuredWorkflow.error ?? snapshot.error ?? "invalid settings")}`
807
+ configuredWorkflow.error || snapshot.error || cwdPolicy.error
808
+ ? ` Warning: ${safeTerminalText(configuredWorkflow.error ?? snapshot.error ?? cwdPolicy.error ?? "invalid settings")}`
646
809
  : " Warning: none",
647
810
  configuredWorkflow.value !== current
648
811
  ? "Configured delegation differs from this session. Run /reload to apply it."
@@ -688,13 +851,45 @@ function completionLabel(value: CompletionDelivery): string {
688
851
  return value === "auto-resume" ? "Resume automatically when finished" : "Wait until my next turn";
689
852
  }
690
853
 
854
+ function consultationCwdLabel(value: ConsultationCwdPolicy): string {
855
+ return value === "current-workspace"
856
+ ? "Current workspace only"
857
+ : "Anywhere · untrusted targets inherit nothing";
858
+ }
859
+
860
+ function delegationCwdLabel(value: DelegationCwdPolicy): string {
861
+ switch (value) {
862
+ case "trusted-targets":
863
+ return "Current or saved-trusted folders";
864
+ case "current-workspace":
865
+ return "Current workspace only";
866
+ case "anywhere":
867
+ return "Anywhere · normal Pi permissions";
868
+ }
869
+ }
870
+
871
+ function consultResourceLabel(value: ConsultResourcePolicy): string {
872
+ switch (value) {
873
+ case "project-context":
874
+ return "Project context only";
875
+ case "none":
876
+ return "No inherited resources";
877
+ case "all":
878
+ return "All trusted resources";
879
+ }
880
+ }
881
+
691
882
  function workflowEffects(current: DelegationWorkflow, next: DelegationWorkflow): string[] {
692
883
  const blockingEnabled = (value: DelegationWorkflow) =>
693
884
  value === "all" || value === "blocking-only";
694
885
  const asyncEnabled = (value: DelegationWorkflow) => value === "all" || value === "async-only";
695
886
  const effects: string[] = [];
696
887
  if (blockingEnabled(current) !== blockingEnabled(next)) {
697
- effects.push(blockingEnabled(next) ? "Add blocking `subagent`" : "Remove blocking `subagent`");
888
+ effects.push(
889
+ blockingEnabled(next)
890
+ ? "Add blocking `subagent` and read-only `subagent_consult`"
891
+ : "Remove blocking `subagent` and read-only `subagent_consult`",
892
+ );
698
893
  }
699
894
  if (asyncEnabled(current) !== asyncEnabled(next)) {
700
895
  effects.push(
@@ -0,0 +1,15 @@
1
+ export const READ_ONLY_CONSULT_TOOLS = ["read", "grep", "find", "ls"] as const;
2
+
3
+ const READ_ONLY_CONSULT_TOOL_SET = new Set<string>(READ_ONLY_CONSULT_TOOLS);
4
+
5
+ export function resolveConsultTools(tools: readonly string[] | undefined): string[] {
6
+ if (tools === undefined) return [...READ_ONLY_CONSULT_TOOLS];
7
+ const seen = new Set<string>();
8
+ const effective: string[] = [];
9
+ for (const tool of tools) {
10
+ if (!READ_ONLY_CONSULT_TOOL_SET.has(tool) || seen.has(tool)) continue;
11
+ seen.add(tool);
12
+ effective.push(tool);
13
+ }
14
+ return effective;
15
+ }
@@ -0,0 +1,194 @@
1
+ import type { AgentToolResult } from "@earendil-works/pi-agent-core";
2
+ import {
3
+ getMarkdownTheme,
4
+ type Theme,
5
+ type ToolRenderResultOptions,
6
+ } from "@earendil-works/pi-coding-agent";
7
+ import { Container, Markdown, Spacer, Text } from "@earendil-works/pi-tui";
8
+ import type { ConsultDetails, SubagentConsultParams } from "./consult.js";
9
+ import { formatUsageStats } from "./render.js";
10
+ import {
11
+ COLLAPSED_ANSWER_LINES,
12
+ COLLAPSED_LIST_LIMIT,
13
+ expansionHint,
14
+ previewLines,
15
+ projectRenderActivity,
16
+ type RenderStatus,
17
+ recordValue,
18
+ renderActivityLines,
19
+ renderFallbackResult,
20
+ safeBlock,
21
+ safeLine,
22
+ statusBadge,
23
+ stringValue,
24
+ type ToolRendererContext,
25
+ textResult,
26
+ toolHeader,
27
+ } from "./render-common.js";
28
+
29
+ export function renderConsultCall(args: Partial<SubagentConsultParams>, theme: Theme) {
30
+ const scope = args.agentScope ?? "user";
31
+ const text = [
32
+ toolHeader(theme, "subagent_consult", args.agent, [`[${scope}]`, "read-only"]),
33
+ ` ${theme.fg("dim", safeLine(args.task, "...", 2 * 1024))}`,
34
+ ].join("\n");
35
+ return new Text(text, 0, 0);
36
+ }
37
+
38
+ export function renderConsultResult(
39
+ result: AgentToolResult<ConsultDetails>,
40
+ options: ToolRenderResultOptions,
41
+ theme: Theme,
42
+ context: ToolRendererContext<SubagentConsultParams>,
43
+ ) {
44
+ const details = recordValue(result.details);
45
+ if (!details || !recordValue(details.policy)) {
46
+ return renderFallbackResult(result, options, theme, context.isError);
47
+ }
48
+
49
+ const progress = recordValue(details.progress);
50
+ const child = recordValue(details.child);
51
+ const status = consultStatus(details, progress, options, context.isError);
52
+ const agent = safeLine(details.agent, safeLine(context.args?.agent, "subagent"), 256);
53
+ const source = safeLine(details.agentSource, "unknown", 128);
54
+ const usage = recordValue(progress?.usage ?? child?.usage);
55
+ const usageText = formatUnknownUsage(
56
+ usage,
57
+ stringValue(details.model) || undefined,
58
+ stringValue(details.thinkingLevel) || undefined,
59
+ stringValue(progress?.actualProvider ?? child?.actualProvider) || undefined,
60
+ stringValue(progress?.actualModel ?? child?.actualModel) || undefined,
61
+ );
62
+ const effectiveTools = stringList(recordValue(details.policy)?.effectiveTools);
63
+ const activity = projectRenderActivity(progress?.recentActivity);
64
+ const activityTotal = Math.max(
65
+ activity.length,
66
+ typeof progress?.recentActivityTotal === "number" ? progress.recentActivityTotal : 0,
67
+ );
68
+ const answer = safeBlock(textResult(result), "", 50 * 1024).trim();
69
+
70
+ if (options.expanded) {
71
+ const container = new Container();
72
+ container.addChild(
73
+ new Text(
74
+ `${statusBadge(theme, status)} · ${theme.fg("toolTitle", theme.bold(agent))}${theme.fg("muted", ` (${source})`)}`,
75
+ 0,
76
+ 0,
77
+ ),
78
+ );
79
+ if (usageText) container.addChild(new Text(theme.fg("dim", usageText), 0, 0));
80
+ container.addChild(new Spacer(1));
81
+ container.addChild(new Text(theme.fg("muted", "─── Task ───"), 0, 0));
82
+ container.addChild(
83
+ new Text(theme.fg("dim", safeBlock(context.args?.task, "(unavailable)", 50 * 1024)), 0, 0),
84
+ );
85
+ container.addChild(new Spacer(1));
86
+ container.addChild(new Text(theme.fg("muted", "─── Policy ───"), 0, 0));
87
+ container.addChild(new Text(theme.fg("dim", policySummary(details.policy)), 0, 0));
88
+ if (activity.length > 0) {
89
+ container.addChild(new Spacer(1));
90
+ container.addChild(new Text(theme.fg("muted", "─── Activity ───"), 0, 0));
91
+ container.addChild(
92
+ new Text(renderActivityLines(activity, theme, undefined, activityTotal), 0, 0),
93
+ );
94
+ }
95
+ container.addChild(new Spacer(1));
96
+ container.addChild(new Text(theme.fg("muted", "─── Answer ───"), 0, 0));
97
+ if (answer) container.addChild(new Markdown(answer, 0, 0, getMarkdownTheme()));
98
+ else
99
+ container.addChild(
100
+ new Text(
101
+ theme.fg("muted", options.isPartial ? "(waiting for output)" : "(no output)"),
102
+ 0,
103
+ 0,
104
+ ),
105
+ );
106
+ return container;
107
+ }
108
+
109
+ const lines = [
110
+ `${statusBadge(theme, status)} · ${theme.fg("toolTitle", theme.bold(agent))}${theme.fg("muted", ` (${source})`)}`,
111
+ ];
112
+ if (usageText) lines.push(theme.fg("dim", usageText));
113
+ if (effectiveTools.length > 0 && options.isPartial) {
114
+ lines.push(theme.fg("dim", `tools: ${effectiveTools.join(", ")}`));
115
+ }
116
+ if (activity.length > 0) {
117
+ lines.push(renderActivityLines(activity, theme, COLLAPSED_LIST_LIMIT, activityTotal));
118
+ } else if (options.isPartial) {
119
+ lines.push(
120
+ theme.fg("muted", status === "starting" ? "(starting child)" : "(waiting for activity)"),
121
+ );
122
+ } else if (answer) {
123
+ lines.push(theme.fg("toolOutput", previewLines(answer, COLLAPSED_ANSWER_LINES)));
124
+ } else {
125
+ const error = safeBlock(child?.error, "", 2 * 1024).trim();
126
+ lines.push(theme.fg(status === "failed" ? "error" : "muted", error || "(no output)"));
127
+ }
128
+ if (!options.isPartial) lines.push(expansionHint());
129
+ return new Text(lines.filter(Boolean).join("\n"), 0, 0);
130
+ }
131
+
132
+ function consultStatus(
133
+ details: Record<string, unknown>,
134
+ progress: Record<string, unknown> | undefined,
135
+ options: ToolRenderResultOptions,
136
+ isError: boolean,
137
+ ): RenderStatus {
138
+ if (details.cancelled === true) return "cancelled";
139
+ if (isError || details.isError === true) {
140
+ const child = recordValue(details.child);
141
+ return child?.aborted === true && child?.timedOut !== true ? "cancelled" : "failed";
142
+ }
143
+ if (options.isPartial) return progress?.phase === "starting" ? "starting" : "running";
144
+ return "completed";
145
+ }
146
+
147
+ function stringList(value: unknown): string[] {
148
+ return Array.isArray(value)
149
+ ? value.flatMap((item) => (typeof item === "string" ? [safeLine(item, "", 256)] : []))
150
+ : [];
151
+ }
152
+
153
+ function policySummary(value: unknown): string {
154
+ const policy = recordValue(value);
155
+ if (!policy) return "(unavailable)";
156
+ const resources = recordValue(policy.effectiveResources);
157
+ const tools = stringList(policy.effectiveTools);
158
+ return [
159
+ `tools: ${tools.length > 0 ? tools.join(", ") : "none"}`,
160
+ `resources: ${safeLine(resources?.policy ?? policy.requestedResources, "unknown", 256)}`,
161
+ `extensions: ${safeLine(policy.extensions, "unknown", 128)}`,
162
+ `session persistence: ${safeLine(policy.sessionPersistence, "unknown", 128)}`,
163
+ `retained agent: ${policy.retainedAgent === false ? "no" : "unknown"}`,
164
+ ].join("\n");
165
+ }
166
+
167
+ function formatUnknownUsage(
168
+ value: Record<string, unknown> | undefined,
169
+ model?: string,
170
+ thinkingLevel?: string,
171
+ actualProvider?: string,
172
+ actualModel?: string,
173
+ ): string {
174
+ const usage = value ?? {};
175
+ return formatUsageStats(
176
+ {
177
+ input: safeNumber(usage.input),
178
+ output: safeNumber(usage.output),
179
+ cacheRead: safeNumber(usage.cacheRead),
180
+ cacheWrite: safeNumber(usage.cacheWrite),
181
+ cost: safeNumber(usage.cost),
182
+ contextTokens: safeNumber(usage.contextTokens),
183
+ turns: safeNumber(usage.turns),
184
+ },
185
+ model,
186
+ thinkingLevel as never,
187
+ actualProvider,
188
+ actualModel,
189
+ );
190
+ }
191
+
192
+ function safeNumber(value: unknown): number {
193
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : 0;
194
+ }