pi-subagents 0.32.0 → 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.
Files changed (52) hide show
  1. package/CHANGELOG.md +30 -3
  2. package/README.md +147 -58
  3. package/install.mjs +2 -1
  4. package/package.json +3 -2
  5. package/skills/pi-subagents/SKILL.md +70 -14
  6. package/src/agents/agent-management.ts +177 -5
  7. package/src/agents/agent-memory.ts +254 -0
  8. package/src/agents/agent-serializer.ts +11 -0
  9. package/src/agents/agents.ts +142 -12
  10. package/src/agents/chain-serializer.ts +27 -2
  11. package/src/extension/doctor.ts +1 -9
  12. package/src/extension/fanout-child.ts +2 -2
  13. package/src/extension/index.ts +65 -90
  14. package/src/extension/rpc.ts +369 -0
  15. package/src/extension/schemas.ts +52 -8
  16. package/src/extension/tool-description.ts +200 -0
  17. package/src/intercom/intercom-bridge.ts +21 -253
  18. package/src/intercom/native-supervisor-channel.ts +510 -0
  19. package/src/runs/background/async-execution.ts +51 -7
  20. package/src/runs/background/async-job-tracker.ts +12 -2
  21. package/src/runs/background/async-status.ts +27 -2
  22. package/src/runs/background/completion-batcher.ts +166 -0
  23. package/src/runs/background/control-channel.ts +106 -1
  24. package/src/runs/background/fleet-view.ts +515 -0
  25. package/src/runs/background/notify.ts +161 -44
  26. package/src/runs/background/result-watcher.ts +1 -2
  27. package/src/runs/background/run-id-resolver.ts +3 -2
  28. package/src/runs/background/run-status.ts +166 -6
  29. package/src/runs/background/scheduled-runs.ts +514 -0
  30. package/src/runs/background/subagent-runner.ts +409 -35
  31. package/src/runs/background/wait.ts +353 -0
  32. package/src/runs/foreground/chain-execution.ts +95 -21
  33. package/src/runs/foreground/execution.ts +150 -21
  34. package/src/runs/foreground/subagent-executor.ts +378 -64
  35. package/src/runs/shared/dynamic-fanout.ts +1 -1
  36. package/src/runs/shared/model-fallback.ts +167 -20
  37. package/src/runs/shared/model-scope.ts +128 -0
  38. package/src/runs/shared/nested-events.ts +31 -0
  39. package/src/runs/shared/parallel-utils.ts +1 -0
  40. package/src/runs/shared/pi-args.ts +30 -1
  41. package/src/runs/shared/subagent-prompt-runtime.ts +108 -2
  42. package/src/runs/shared/tool-budget.ts +74 -0
  43. package/src/runs/shared/turn-budget.ts +52 -0
  44. package/src/shared/artifacts.ts +1 -0
  45. package/src/shared/atomic-json.ts +15 -2
  46. package/src/shared/child-transcript.ts +212 -0
  47. package/src/shared/settings.ts +3 -1
  48. package/src/shared/types.ts +134 -19
  49. package/src/slash/prompt-workflows.ts +330 -0
  50. package/src/slash/slash-commands.ts +16 -2
  51. package/src/tui/render.ts +16 -8
  52. package/src/extension/companion-suggestions.ts +0 -359
@@ -1,7 +1,7 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import * as fs from "node:fs";
3
3
  import * as path from "node:path";
4
- import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
4
+ import { keyText, type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
5
5
  import { Key, matchesKey } from "@earendil-works/pi-tui";
6
6
  import { BUILTIN_AGENT_NAMES, discoverAgents, discoverAgentsAll, type ChainConfig } from "../agents/agents.ts";
7
7
  import {
@@ -20,6 +20,7 @@ import { formatTokens } from "../shared/formatters.ts";
20
20
  import { assertJsonSchemaObject } from "../runs/shared/structured-output.ts";
21
21
  import { validateAcceptanceInput } from "../runs/shared/acceptance.ts";
22
22
  import type { SlashSubagentResponse, SlashSubagentUpdate } from "./slash-bridge.ts";
23
+ import { registerPromptWorkflowCommands } from "./prompt-workflows.ts";
23
24
  import {
24
25
  applySlashUpdate,
25
26
  buildSlashInitialResult,
@@ -432,7 +433,8 @@ async function requestSlashRun(
432
433
  if (!ctx.hasUI) return;
433
434
  const tool = update.currentTool ? ` ${update.currentTool}` : "";
434
435
  const count = update.toolCount ?? 0;
435
- ctx.ui.setStatus("subagent-slash", `${count} tools${tool} | Ctrl+O live detail`);
436
+ const liveDetailKey = keyText("app.tools.expand");
437
+ ctx.ui.setStatus("subagent-slash", `${count} tools${tool} | ${liveDetailKey} live detail`);
436
438
  };
437
439
 
438
440
  const onTerminalInput = ctx.hasUI
@@ -1087,6 +1089,18 @@ export function registerSlashCommands(
1087
1089
  },
1088
1090
  });
1089
1091
 
1092
+ pi.registerCommand("subagents-fleet", {
1093
+ description: "Show active subagent fleet status and transcript commands",
1094
+ handler: async (_args, ctx) => {
1095
+ await runSlashSubagent(pi, ctx, { action: "status", view: "fleet" });
1096
+ },
1097
+ });
1098
+
1099
+ registerPromptWorkflowCommands({
1100
+ pi,
1101
+ run: (params, ctx) => runSlashSubagent(pi, ctx, params),
1102
+ });
1103
+
1090
1104
  pi.registerCommand("subagents-models", {
1091
1105
  description: "Show runtime-loaded builtin subagent models",
1092
1106
  getArgumentCompletions: makeBuiltinAgentNameCompletions(),
package/src/tui/render.ts CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  import * as path from "node:path";
6
6
  import type { AgentToolResult } from "@earendil-works/pi-agent-core";
7
- import { getMarkdownTheme, type ExtensionContext } from "@earendil-works/pi-coding-agent";
7
+ import { getMarkdownTheme, keyText, type ExtensionContext } from "@earendil-works/pi-coding-agent";
8
8
  import { Container, Markdown, Spacer, Text, visibleWidth, type Component } from "@earendil-works/pi-tui";
9
9
  import {
10
10
  type AgentProgress,
@@ -26,6 +26,14 @@ import { aggregateStepStatus, formatActivityLabel, formatAgentRunningLabel, form
26
26
 
27
27
  type Theme = ExtensionContext["ui"]["theme"];
28
28
 
29
+ function liveDetailKeyText(): string {
30
+ return keyText("app.tools.expand");
31
+ }
32
+
33
+ function liveDetailHintText(): string {
34
+ return `Press ${liveDetailKeyText()} for live detail`;
35
+ }
36
+
29
37
  function getTermWidth(): number {
30
38
  return process.stdout.columns || 120;
31
39
  }
@@ -867,7 +875,7 @@ function foregroundStyleWidgetStepLines(
867
875
  lines.push(` ${nestedLine}`);
868
876
  }
869
877
  if (step.status === "running") {
870
- if (!expanded) lines.push(` ${theme.fg("accent", "Press Ctrl+O for live detail")}`);
878
+ if (!expanded) lines.push(` ${theme.fg("accent", liveDetailHintText())}`);
871
879
  const output = widgetOutputPath(job, step);
872
880
  if (output) lines.push(` ${theme.fg("dim", `output: ${shortenPath(output)}`)}`);
873
881
  if (expanded) {
@@ -934,7 +942,7 @@ function compactSingleWidgetLines(job: AsyncJobState, theme: Theme, width: numbe
934
942
  lines.push(` ${widgetStepGlyph(step.status, theme, widgetStepRunningSeed(step, index))} ${itemTitle} ${index + 1}/${total}: ${themeBold(theme, step.agent)} ${theme.fg("dim", "·")} ${status}${modelDisplay}${activitySuffix}${stepStats ? ` ${theme.fg("dim", "·")} ${stepStats}` : ""}`);
935
943
  for (const nestedLine of formatNestedWidgetLines(step.children, theme, width, false, job.updatedAt)) lines.push(` ${nestedLine}`);
936
944
  }
937
- if (job.steps.some((step) => step.status === "running")) lines.push(theme.fg("accent", " Press Ctrl+O for live detail"));
945
+ if (job.steps.some((step) => step.status === "running")) lines.push(theme.fg("accent", ` ${liveDetailHintText()}`));
938
946
  return lines.map((line) => truncLine(line, width));
939
947
  }
940
948
 
@@ -1126,7 +1134,7 @@ function fitWidgetLineBudget(lines: string[], theme: Theme, width: number, expan
1126
1134
  const hiddenCount = lines.length - visibleLines;
1127
1135
  const hint = expanded
1128
1136
  ? `… ${hiddenCount} live-detail lines hidden`
1129
- : `… ${hiddenCount} lines hidden · Ctrl+O expands`;
1137
+ : `… ${hiddenCount} lines hidden · ${liveDetailKeyText()} expands`;
1130
1138
  return [...lines.slice(0, visibleLines), truncLine(theme.fg("dim", hint), width)];
1131
1139
  }
1132
1140
 
@@ -1284,7 +1292,7 @@ function renderSingleCompact(d: Details, r: Details["results"][number], theme: T
1284
1292
  c.addChild(new Text(truncLine(theme.fg("dim", ` ⎿ ${activity}`), width), 0, 0));
1285
1293
  const liveStatus = buildLiveStatusLine(r.progress, progressSnapshotNow);
1286
1294
  if (liveStatus && liveStatus !== activity) c.addChild(new Text(truncLine(theme.fg("dim", ` ${liveStatus}`), width), 0, 0));
1287
- c.addChild(new Text(truncLine(theme.fg("accent", " Press Ctrl+O for live detail"), width), 0, 0));
1295
+ c.addChild(new Text(truncLine(theme.fg("accent", ` ${liveDetailHintText()}`), width), 0, 0));
1288
1296
  if (r.artifactPaths) c.addChild(new Text(truncLine(theme.fg("dim", ` output: ${shortenPath(r.artifactPaths.outputPath)}`), width), 0, 0));
1289
1297
  return c;
1290
1298
  }
@@ -1380,7 +1388,7 @@ function renderMultiCompact(d: Details, theme: Theme, frame?: number): Component
1380
1388
  if (rRunning && rProg && "status" in rProg) {
1381
1389
  const activity = compactCurrentActivity(rProg);
1382
1390
  c.addChild(new Text(truncLine(theme.fg("dim", ` ⎿ ${activity}`), width), 0, 0));
1383
- c.addChild(new Text(truncLine(theme.fg("accent", " Press Ctrl+O for live detail"), width), 0, 0));
1391
+ c.addChild(new Text(truncLine(theme.fg("accent", ` ${liveDetailHintText()}`), width), 0, 0));
1384
1392
  } else if (!rPending && (r.exitCode !== 0 || r.interrupted || r.detached || hasEmptyTextOutputWithoutOutputTarget(r.task, output))) {
1385
1393
  c.addChild(new Text(truncLine(theme.fg(r.exitCode !== 0 ? "error" : "dim", ` ⎿ ${resultStatusLine(r, output)}`), width), 0, 0));
1386
1394
  }
@@ -1462,7 +1470,7 @@ export function renderSubagentResult(
1462
1470
  if (liveStatusLine) {
1463
1471
  c.addChild(new Text(fit(theme.fg("accent", liveStatusLine)), 0, 0));
1464
1472
  }
1465
- c.addChild(new Text(fit(theme.fg("accent", "Press Ctrl+O for live detail")), 0, 0));
1473
+ c.addChild(new Text(fit(theme.fg("accent", liveDetailHintText())), 0, 0));
1466
1474
  if (r.artifactPaths) {
1467
1475
  c.addChild(new Text(fit(theme.fg("dim", `Artifacts: ${shortenPath(r.artifactPaths.outputPath)}`)), 0, 0));
1468
1476
  }
@@ -1699,7 +1707,7 @@ export function renderSubagentResult(
1699
1707
  if (liveStatusLine) {
1700
1708
  c.addChild(new Text(fit(theme.fg("accent", ` ${liveStatusLine}`)), 0, 0));
1701
1709
  }
1702
- c.addChild(new Text(fit(theme.fg("accent", " Press Ctrl+O for live detail")), 0, 0));
1710
+ c.addChild(new Text(fit(theme.fg("accent", ` ${liveDetailHintText()}`)), 0, 0));
1703
1711
  if (r.artifactPaths) {
1704
1712
  c.addChild(new Text(fit(theme.fg("dim", ` artifacts: ${shortenPath(r.artifactPaths.outputPath)}`)), 0, 0));
1705
1713
  }
@@ -1,359 +0,0 @@
1
- import * as fs from "node:fs";
2
- import * as path from "node:path";
3
- import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
4
- import { diagnoseIntercomBridge, type IntercomBridgeDiagnostic, resolveIntercomSessionTarget } from "../intercom/intercom-bridge.ts";
5
- import type { CompanionSuggestionPackage, CompanionSuggestionSurface, ExtensionConfig, SubagentState } from "../shared/types.ts";
6
- import { getAgentDir } from "../shared/utils.ts";
7
- import { updateConfig } from "./config.ts";
8
-
9
- const PROMPT_TEMPLATE_MODEL: CompanionSuggestionPackage = "pi-prompt-template-model";
10
- const PI_INTERCOM: CompanionSuggestionPackage = "pi-intercom";
11
- const COMPANION_PACKAGES = [PI_INTERCOM, PROMPT_TEMPLATE_MODEL] as const;
12
- const DEFAULT_SURFACES: CompanionSuggestionSurface[] = ["session_start", "list", "doctor"];
13
-
14
- interface SourceInfoLike {
15
- path?: unknown;
16
- source?: unknown;
17
- baseDir?: unknown;
18
- }
19
-
20
- interface NamedRuntimeResource {
21
- name?: unknown;
22
- sourceInfo?: unknown;
23
- }
24
-
25
- interface IntercomConfigStatus {
26
- enabled: boolean;
27
- error?: string;
28
- }
29
-
30
- export interface CompanionPackageStatus {
31
- packageName: CompanionSuggestionPackage;
32
- active: boolean;
33
- disabled: boolean;
34
- dismissed: boolean;
35
- surfaces: Set<CompanionSuggestionSurface>;
36
- installCommand: string;
37
- benefit: string;
38
- statusSource: string;
39
- reason: string;
40
- details?: string[];
41
- intercomBridge?: IntercomBridgeDiagnostic;
42
- }
43
-
44
- interface CollectCompanionStatusesInput {
45
- pi: Pick<ExtensionAPI, "getAllTools" | "getCommands">;
46
- config: ExtensionConfig;
47
- cwd: string;
48
- context?: "fresh" | "fork";
49
- orchestratorTarget?: string;
50
- workspaceKey?: string;
51
- fast?: boolean;
52
- }
53
-
54
- interface CompanionMessageInput {
55
- pi: Pick<ExtensionAPI, "sendMessage">;
56
- ctx: ExtensionContext;
57
- state: SubagentState;
58
- statuses: CompanionPackageStatus[];
59
- }
60
-
61
- function commandBaseName(name: string): string {
62
- return name.replace(/:\d+$/, "");
63
- }
64
-
65
- function sourceValueMatchesPackage(value: string, packageName: CompanionSuggestionPackage): boolean {
66
- const normalized = value.replaceAll("\\", "/").toLowerCase();
67
- const escaped = packageName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
68
- return new RegExp(`(^|[/:@])${escaped}($|[/:@])`).test(normalized);
69
- }
70
-
71
- function sourceInfoMatchesPackage(sourceInfo: unknown, packageName: CompanionSuggestionPackage): boolean {
72
- if (!sourceInfo || typeof sourceInfo !== "object" || Array.isArray(sourceInfo)) return false;
73
- const info = sourceInfo as SourceInfoLike;
74
- return [info.path, info.source, info.baseDir]
75
- .some((value) => typeof value === "string" && sourceValueMatchesPackage(value, packageName));
76
- }
77
-
78
- function namedResourcesFrom(value: unknown): NamedRuntimeResource[] {
79
- return Array.isArray(value) ? value.filter((entry): entry is NamedRuntimeResource => Boolean(entry) && typeof entry === "object" && !Array.isArray(entry)) : [];
80
- }
81
-
82
- function hasPackageCommand(pi: Pick<ExtensionAPI, "getCommands">, packageName: CompanionSuggestionPackage, commandName: string): boolean {
83
- return namedResourcesFrom(pi.getCommands()).some((command) =>
84
- typeof command.name === "string"
85
- && commandBaseName(command.name) === commandName
86
- && sourceInfoMatchesPackage(command.sourceInfo, packageName)
87
- );
88
- }
89
-
90
- function hasPackageTool(pi: Pick<ExtensionAPI, "getAllTools">, packageName: CompanionSuggestionPackage, toolName: string): boolean {
91
- return namedResourcesFrom(pi.getAllTools()).some((tool) =>
92
- typeof tool.name === "string"
93
- && commandBaseName(tool.name) === toolName
94
- && sourceInfoMatchesPackage(tool.sourceInfo, packageName)
95
- );
96
- }
97
-
98
- function nearestGitRoot(cwd: string): string | undefined {
99
- let current = path.resolve(cwd);
100
- while (true) {
101
- if (fs.existsSync(path.join(current, ".git"))) return current;
102
- const parent = path.dirname(current);
103
- if (parent === current) return undefined;
104
- current = parent;
105
- }
106
- }
107
-
108
- export function companionWorkspaceKey(cwd: string): string {
109
- return nearestGitRoot(cwd) ?? path.resolve(cwd);
110
- }
111
-
112
- function normalizeSurface(value: unknown): CompanionSuggestionSurface | undefined {
113
- return value === "session_start" || value === "list" || value === "doctor" ? value : undefined;
114
- }
115
-
116
- function packageConfig(config: ExtensionConfig, packageName: CompanionSuggestionPackage) {
117
- const companionConfig = config.companionSuggestions;
118
- if (companionConfig === false) return { enabled: false, surfaces: new Set<CompanionSuggestionSurface>(), dismissed: false };
119
- const packageSpecific = companionConfig?.packages?.[packageName];
120
- const surfaces = Array.isArray(packageSpecific?.surfaces)
121
- ? packageSpecific.surfaces.map(normalizeSurface).filter((surface): surface is CompanionSuggestionSurface => Boolean(surface))
122
- : DEFAULT_SURFACES;
123
- return {
124
- enabled: companionConfig?.enabled !== false && packageSpecific?.enabled !== false,
125
- surfaces: new Set(surfaces),
126
- dismissedConfig: packageSpecific?.dismissed,
127
- };
128
- }
129
-
130
- function isDismissed(config: ExtensionConfig, packageName: CompanionSuggestionPackage, workspaceKey: string): boolean {
131
- const dismissed = packageConfig(config, packageName).dismissedConfig;
132
- return dismissed?.user === true || dismissed?.workspaces?.includes(workspaceKey) === true;
133
- }
134
-
135
- function readPiIntercomConfigStatus(agentDir = getAgentDir()): IntercomConfigStatus {
136
- const configPath = path.join(agentDir, "intercom", "config.json");
137
- if (!fs.existsSync(configPath)) return { enabled: true };
138
- try {
139
- const parsed = JSON.parse(fs.readFileSync(configPath, "utf-8")) as unknown;
140
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return { enabled: true };
141
- return { enabled: (parsed as { enabled?: unknown }).enabled !== false };
142
- } catch (error) {
143
- return { enabled: true, error: error instanceof Error ? `${error.name}: ${error.message}` : String(error) };
144
- }
145
- }
146
-
147
- function promptTemplateModelStatus(input: CollectCompanionStatusesInput, workspaceKey: string): CompanionPackageStatus {
148
- const config = packageConfig(input.config, PROMPT_TEMPLATE_MODEL);
149
- const active = hasPackageCommand(input.pi, PROMPT_TEMPLATE_MODEL, "prompt-tool");
150
- return {
151
- packageName: PROMPT_TEMPLATE_MODEL,
152
- active,
153
- disabled: !config.enabled,
154
- dismissed: isDismissed(input.config, PROMPT_TEMPLATE_MODEL, workspaceKey),
155
- surfaces: config.surfaces,
156
- installCommand: "pi install npm:pi-prompt-template-model",
157
- benefit: "reusable prompt-template workflows with model/thinking/skill/subagent frontmatter",
158
- statusSource: "active runtime command: prompt-tool",
159
- reason: active ? "active" : "prompt-tool command from pi-prompt-template-model is not active in this session",
160
- };
161
- }
162
-
163
- function piIntercomStatus(input: CollectCompanionStatusesInput, workspaceKey: string): CompanionPackageStatus {
164
- const config = packageConfig(input.config, PI_INTERCOM);
165
- const parentToolActive = hasPackageTool(input.pi, PI_INTERCOM, "intercom");
166
- const intercomConfig = readPiIntercomConfigStatus();
167
- const bridge = diagnoseIntercomBridge({
168
- config: input.config.intercomBridge,
169
- context: input.context,
170
- orchestratorTarget: input.orchestratorTarget,
171
- cwd: input.cwd,
172
- globalNpmRoot: input.fast ? null : undefined,
173
- });
174
- const active = parentToolActive && intercomConfig.enabled && (!bridge.wantsIntercom || bridge.piIntercomAvailable);
175
- const details = [
176
- `parent runtime tool: ${parentToolActive ? "active" : "inactive"}`,
177
- `bridge: ${bridge.active ? "active" : "inactive"}${bridge.reason ? ` (${bridge.reason})` : ""}`,
178
- ...(intercomConfig.error ? [`intercom config warning: ${intercomConfig.error}; runtime assumes enabled`] : []),
179
- ];
180
- return {
181
- packageName: PI_INTERCOM,
182
- active,
183
- disabled: !config.enabled,
184
- dismissed: isDismissed(input.config, PI_INTERCOM, workspaceKey),
185
- surfaces: config.surfaces,
186
- installCommand: "pi install npm:pi-intercom",
187
- benefit: "live supervisor decisions, progress updates, and grouped result delivery",
188
- statusSource: "active runtime intercom tool plus intercom bridge diagnostics",
189
- reason: active
190
- ? "active"
191
- : !intercomConfig.enabled
192
- ? "pi-intercom config is disabled"
193
- : parentToolActive
194
- ? "pi-intercom is active in the parent runtime, but child bridge discovery is not ready"
195
- : "intercom tool from pi-intercom is not active in this session",
196
- details,
197
- intercomBridge: bridge,
198
- };
199
- }
200
-
201
- export function collectCompanionStatuses(input: CollectCompanionStatusesInput): CompanionPackageStatus[] {
202
- const workspaceKey = input.workspaceKey ?? companionWorkspaceKey(input.cwd);
203
- return [
204
- piIntercomStatus(input, workspaceKey),
205
- promptTemplateModelStatus(input, workspaceKey),
206
- ];
207
- }
208
-
209
- function shouldRecommend(status: CompanionPackageStatus, surface: CompanionSuggestionSurface): boolean {
210
- if (status.disabled || status.dismissed || status.active || !status.surfaces.has(surface)) return false;
211
- if (status.packageName === PI_INTERCOM && status.reason === "pi-intercom config is disabled" && surface !== "doctor") return false;
212
- if (status.packageName === PI_INTERCOM && status.intercomBridge?.wantsIntercom === false && surface !== "doctor") return false;
213
- return true;
214
- }
215
-
216
- export function buildCompanionListLines(statuses: CompanionPackageStatus[]): string[] {
217
- const recommended = statuses.filter((status) => shouldRecommend(status, "list"));
218
- if (recommended.length === 0) return [];
219
- const lines = ["Recommended companions:"];
220
- for (const status of recommended) {
221
- lines.push(`- ${status.packageName} is not active in this session.`);
222
- lines.push(` Benefit: ${status.benefit}.`);
223
- lines.push(` Run: ${status.installCommand}, then restart Pi or /reload.`);
224
- lines.push(` Hide: /subagents-companions hide ${status.packageName} workspace`);
225
- }
226
- return lines;
227
- }
228
-
229
- export function buildCompanionDoctorLines(statuses: CompanionPackageStatus[]): string[] {
230
- const lines = ["Companion packages"];
231
- for (const status of statuses) {
232
- const hidden = status.dismissed ? " recommendation hidden by config" : "";
233
- const disabled = status.disabled ? " disabled by config" : "";
234
- lines.push(`- ${status.packageName}: ${status.active ? "active" : "inactive"}${hidden}${disabled}`);
235
- lines.push(` install: ${status.installCommand}`);
236
- lines.push(` benefit: ${status.benefit}`);
237
- lines.push(` status source: ${status.statusSource}`);
238
- lines.push(` reason: ${status.reason}`);
239
- for (const detail of status.details ?? []) lines.push(` ${detail}`);
240
- }
241
- return lines;
242
- }
243
-
244
- export function buildCompanionStartupMessage(statuses: CompanionPackageStatus[]): string | null {
245
- const recommended = statuses.filter((status) => shouldRecommend(status, "session_start"));
246
- if (recommended.length === 0) return null;
247
- const lines = [
248
- recommended.length === 1
249
- ? `Recommended: install ${recommended[0]!.packageName} for pi-subagents.`
250
- : "Recommended: install companion packages for pi-subagents.",
251
- "",
252
- ];
253
- for (const status of recommended) {
254
- lines.push(`- ${status.packageName}: ${status.benefit}.`);
255
- lines.push(` Run: ${status.installCommand}`);
256
- }
257
- lines.push(
258
- "",
259
- recommended.length === 1 ? "I can help you run that install command." : "I can help you run those install commands.",
260
- "",
261
- "Or hide a recommendation:",
262
- );
263
- for (const status of recommended) {
264
- lines.push(` /subagents-companions hide ${status.packageName} workspace`);
265
- }
266
- return lines.join("\n");
267
- }
268
-
269
- export function maybeSendCompanionStartupMessage(input: CompanionMessageInput): void {
270
- if (!input.ctx.hasUI || input.state.companionSuggestionStartupShown) return;
271
- const message = buildCompanionStartupMessage(input.statuses);
272
- if (!message) return;
273
- input.state.companionSuggestionStartupShown = true;
274
- input.pi.sendMessage({
275
- customType: "subagent_companion_suggestions",
276
- content: message,
277
- display: true,
278
- details: { packages: input.statuses.filter((status) => shouldRecommend(status, "session_start")).map((status) => status.packageName) },
279
- });
280
- }
281
-
282
- function parseCompanionPackage(value: string | undefined): CompanionSuggestionPackage | undefined {
283
- return COMPANION_PACKAGES.find((packageName) => packageName === value);
284
- }
285
-
286
- function packageDismissedWorkspaces(value: unknown): string[] {
287
- return Array.isArray(value) ? value.filter((entry): entry is string => typeof entry === "string") : [];
288
- }
289
-
290
- export function updateCompanionDismissal(packageName: CompanionSuggestionPackage, scope: "workspace" | "user" | "show", cwd: string): ExtensionConfig {
291
- const workspaceKey = companionWorkspaceKey(cwd);
292
- return updateConfig((current) => {
293
- const companionSuggestions = current.companionSuggestions === false
294
- ? { enabled: false }
295
- : current.companionSuggestions ?? {};
296
- const packages = companionSuggestions.packages ?? {};
297
- const packageConfig = packages[packageName] ?? {};
298
- const dismissed = { ...(packageConfig.dismissed ?? {}) };
299
- if (scope === "user") {
300
- dismissed.user = true;
301
- } else if (scope === "workspace") {
302
- dismissed.workspaces = [...new Set([...packageDismissedWorkspaces(dismissed.workspaces), workspaceKey])];
303
- } else {
304
- delete dismissed.user;
305
- dismissed.workspaces = packageDismissedWorkspaces(dismissed.workspaces).filter((entry) => entry !== workspaceKey);
306
- if (dismissed.workspaces.length === 0) delete dismissed.workspaces;
307
- }
308
- return {
309
- ...current,
310
- companionSuggestions: {
311
- ...companionSuggestions,
312
- packages: {
313
- ...packages,
314
- [packageName]: {
315
- ...packageConfig,
316
- ...(Object.keys(dismissed).length > 0 ? { dismissed } : { dismissed: undefined }),
317
- },
318
- },
319
- },
320
- };
321
- });
322
- }
323
-
324
- export function buildCompanionCommandStatus(statuses: CompanionPackageStatus[]): string {
325
- return buildCompanionDoctorLines(statuses).join("\n");
326
- }
327
-
328
- export function handleCompanionCommand(args: string, ctx: ExtensionContext, statuses: CompanionPackageStatus[]): { text: string; updatedConfig?: ExtensionConfig; error?: boolean } {
329
- const parts = args.trim().split(/\s+/).filter(Boolean);
330
- if (parts.length === 0 || parts[0] === "status") {
331
- return { text: buildCompanionCommandStatus(statuses) };
332
- }
333
- if (parts[0] !== "hide" && parts[0] !== "show") {
334
- return { text: "Usage: /subagents-companions status | hide <pi-intercom|pi-prompt-template-model> <workspace|user> | show <pi-intercom|pi-prompt-template-model>", error: true };
335
- }
336
- const packageName = parseCompanionPackage(parts[1]);
337
- if (!packageName) {
338
- return { text: "Unknown companion package. Use pi-intercom or pi-prompt-template-model.", error: true };
339
- }
340
- if (parts[0] === "show") {
341
- return { text: `Showing ${packageName} recommendations for this workspace again.`, updatedConfig: updateCompanionDismissal(packageName, "show", ctx.cwd) };
342
- }
343
- const scope = parts[2];
344
- if (scope !== "workspace" && scope !== "user") {
345
- return { text: "Usage: /subagents-companions hide <pi-intercom|pi-prompt-template-model> <workspace|user>", error: true };
346
- }
347
- return {
348
- text: scope === "user" ? `Hid ${packageName} recommendations for this user.` : `Hid ${packageName} recommendations for this workspace.`,
349
- updatedConfig: updateCompanionDismissal(packageName, scope, ctx.cwd),
350
- };
351
- }
352
-
353
- export function resolveCompanionOrchestratorTarget(pi: Pick<ExtensionAPI, "getSessionName">, ctx: ExtensionContext): string | undefined {
354
- try {
355
- return resolveIntercomSessionTarget(pi.getSessionName(), ctx.sessionManager.getSessionId());
356
- } catch {
357
- return undefined;
358
- }
359
- }