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.
- package/CHANGELOG.md +30 -3
- package/README.md +147 -58
- package/install.mjs +2 -1
- package/package.json +3 -2
- package/skills/pi-subagents/SKILL.md +70 -14
- package/src/agents/agent-management.ts +177 -5
- package/src/agents/agent-memory.ts +254 -0
- package/src/agents/agent-serializer.ts +11 -0
- package/src/agents/agents.ts +142 -12
- package/src/agents/chain-serializer.ts +27 -2
- package/src/extension/doctor.ts +1 -9
- package/src/extension/fanout-child.ts +2 -2
- package/src/extension/index.ts +65 -90
- package/src/extension/rpc.ts +369 -0
- package/src/extension/schemas.ts +52 -8
- package/src/extension/tool-description.ts +200 -0
- package/src/intercom/intercom-bridge.ts +21 -253
- package/src/intercom/native-supervisor-channel.ts +510 -0
- package/src/runs/background/async-execution.ts +51 -7
- package/src/runs/background/async-job-tracker.ts +12 -2
- package/src/runs/background/async-status.ts +27 -2
- package/src/runs/background/completion-batcher.ts +166 -0
- package/src/runs/background/control-channel.ts +106 -1
- package/src/runs/background/fleet-view.ts +515 -0
- package/src/runs/background/notify.ts +161 -44
- package/src/runs/background/result-watcher.ts +1 -2
- package/src/runs/background/run-id-resolver.ts +3 -2
- package/src/runs/background/run-status.ts +166 -6
- package/src/runs/background/scheduled-runs.ts +514 -0
- package/src/runs/background/subagent-runner.ts +409 -35
- package/src/runs/background/wait.ts +353 -0
- package/src/runs/foreground/chain-execution.ts +95 -21
- package/src/runs/foreground/execution.ts +150 -21
- package/src/runs/foreground/subagent-executor.ts +378 -64
- package/src/runs/shared/dynamic-fanout.ts +1 -1
- package/src/runs/shared/model-fallback.ts +167 -20
- package/src/runs/shared/model-scope.ts +128 -0
- package/src/runs/shared/nested-events.ts +31 -0
- package/src/runs/shared/parallel-utils.ts +1 -0
- package/src/runs/shared/pi-args.ts +30 -1
- package/src/runs/shared/subagent-prompt-runtime.ts +108 -2
- package/src/runs/shared/tool-budget.ts +74 -0
- package/src/runs/shared/turn-budget.ts +52 -0
- package/src/shared/artifacts.ts +1 -0
- package/src/shared/atomic-json.ts +15 -2
- package/src/shared/child-transcript.ts +212 -0
- package/src/shared/settings.ts +3 -1
- package/src/shared/types.ts +134 -19
- package/src/slash/prompt-workflows.ts +330 -0
- package/src/slash/slash-commands.ts +16 -2
- package/src/tui/render.ts +16 -8
- 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
|
|
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
|
-
|
|
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",
|
|
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",
|
|
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 ·
|
|
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",
|
|
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",
|
|
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",
|
|
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",
|
|
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
|
-
}
|