pi-subagents 0.37.0 → 0.37.2
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 +29 -0
- package/README.md +18 -8
- package/agents/planner.md +2 -1
- package/package.json +3 -3
- package/skills/pi-subagents/SKILL.md +18 -989
- package/skills/pi-subagents/references/constraints-and-recipes.md +256 -0
- package/skills/pi-subagents/references/execution-controls.md +411 -0
- package/skills/pi-subagents/references/management-authoring-rpc.md +140 -0
- package/skills/pi-subagents/references/prompting-and-roles.md +268 -0
- package/src/agents/skills.ts +14 -12
- package/src/api/delegation.ts +3 -0
- package/src/extension/index.ts +14 -7
- package/src/extension/rpc.ts +25 -2
- package/src/extension/schemas.ts +2 -2
- package/src/extension/tool-description.ts +2 -2
- package/src/intercom/intercom-bridge.ts +5 -2
- package/src/runs/background/async-job-tracker.ts +19 -12
- package/src/runs/background/async-resume.ts +6 -5
- package/src/runs/background/notify.ts +3 -0
- package/src/runs/background/result-watcher.ts +9 -3
- package/src/runs/foreground/subagent-executor.ts +111 -46
- package/src/runs/shared/mcp-direct-tool-allowlist.ts +44 -11
- package/src/runs/shared/model-fallback.ts +8 -0
- package/src/runs/shared/pi-args.ts +3 -0
- package/src/runs/shared/task-intent.ts +1 -1
- package/src/shared/types.ts +14 -1
- package/src/slash/delegation-adapters.ts +5 -0
- package/src/tui/fleet-status.ts +62 -16
- package/src/tui/fleet.ts +1 -1
- package/src/tui/render.ts +23 -26
|
@@ -27,6 +27,7 @@ type ImportKind = keyof typeof IMPORT_PATHS;
|
|
|
27
27
|
interface ServerEntry {
|
|
28
28
|
command?: string;
|
|
29
29
|
args?: string[];
|
|
30
|
+
socket?: string;
|
|
30
31
|
env?: Record<string, string>;
|
|
31
32
|
cwd?: string;
|
|
32
33
|
url?: string;
|
|
@@ -35,6 +36,7 @@ interface ServerEntry {
|
|
|
35
36
|
bearerToken?: string;
|
|
36
37
|
bearerTokenEnv?: string;
|
|
37
38
|
exposeResources?: boolean;
|
|
39
|
+
includeTools?: string[];
|
|
38
40
|
excludeTools?: string[];
|
|
39
41
|
directTools?: boolean | string[];
|
|
40
42
|
}
|
|
@@ -271,14 +273,16 @@ export function computeMcpServerHash(definition: ServerEntry): string {
|
|
|
271
273
|
const identity: Record<string, unknown> = {
|
|
272
274
|
command: definition.command,
|
|
273
275
|
args: definition.args,
|
|
276
|
+
socket: resolveConfigPath(definition.socket),
|
|
274
277
|
env: interpolateEnvRecord(definition.env),
|
|
275
278
|
cwd: resolveConfigPath(definition.cwd),
|
|
276
|
-
url: definition
|
|
279
|
+
url: resolveServerUrl(definition),
|
|
277
280
|
headers: interpolateEnvRecord(definition.headers),
|
|
278
281
|
auth: definition.auth,
|
|
279
282
|
bearerToken: resolveBearerToken(definition),
|
|
280
283
|
bearerTokenEnv: definition.bearerTokenEnv,
|
|
281
284
|
exposeResources: definition.exposeResources,
|
|
285
|
+
includeTools: definition.includeTools,
|
|
282
286
|
excludeTools: definition.excludeTools,
|
|
283
287
|
};
|
|
284
288
|
return createHash("sha256").update(stableStringify(identity)).digest("hex");
|
|
@@ -333,22 +337,51 @@ function resourceNameToToolName(name: string): string {
|
|
|
333
337
|
}
|
|
334
338
|
|
|
335
339
|
function interpolateEnvRecord(values: Record<string, string> | undefined): Record<string, string> | undefined {
|
|
336
|
-
if (!values
|
|
337
|
-
|
|
338
|
-
for (const [key, value] of Object.entries(values)) {
|
|
339
|
-
if (typeof value === "string") resolved[key] = interpolateEnvVars(value);
|
|
340
|
-
}
|
|
341
|
-
return resolved;
|
|
340
|
+
if (!values) return undefined;
|
|
341
|
+
return Object.fromEntries(Object.entries(values).map(([key, value]) => [key, interpolateSecretExpression(value)]));
|
|
342
342
|
}
|
|
343
343
|
|
|
344
344
|
function interpolateEnvVars(value: string): string {
|
|
345
345
|
return value
|
|
346
346
|
.replace(/\$\{(\w+)\}/g, (_, name: string) => process.env[name] ?? "")
|
|
347
|
-
.replace(/\$env:(\w+)/g, (_, name: string) => process.env[name] ?? "")
|
|
347
|
+
.replace(/\$env:(\w+)/g, (_, name: string) => process.env[name] ?? "")
|
|
348
|
+
.replace(/\{env:(\w+)\}/g, (_, name: string) => process.env[name] ?? "");
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function interpolateSecretExpression(value: string): string {
|
|
352
|
+
if (value.startsWith("!!")) return interpolateEnvVars(value.slice(1));
|
|
353
|
+
return value.startsWith("!") ? value : interpolateEnvVars(value);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function getMissingEnvVars(value: string): string[] {
|
|
357
|
+
const missing = new Set<string>();
|
|
358
|
+
for (const match of value.matchAll(/\$\{(\w+)\}|\$env:(\w+)|\{env:(\w+)\}/g)) {
|
|
359
|
+
const name = match[1] ?? match[2] ?? match[3];
|
|
360
|
+
if (name && process.env[name] === undefined) missing.add(name);
|
|
361
|
+
}
|
|
362
|
+
return [...missing];
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function resolveServerUrl(definition: Pick<ServerEntry, "url">): string | undefined {
|
|
366
|
+
if (definition.url == null) return undefined;
|
|
367
|
+
if (typeof definition.url !== "string") throw new Error("MCP server URL must be a string");
|
|
368
|
+
|
|
369
|
+
const missing = getMissingEnvVars(definition.url);
|
|
370
|
+
if (missing.length > 0) {
|
|
371
|
+
throw new Error(`Missing environment variable${missing.length === 1 ? "" : "s"} in MCP server URL: ${missing.join(", ")}`);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const resolved = interpolateEnvVars(definition.url);
|
|
375
|
+
try {
|
|
376
|
+
new URL(resolved);
|
|
377
|
+
} catch (error) {
|
|
378
|
+
throw new Error(`Invalid MCP server URL after environment interpolation: ${resolved}`, { cause: error });
|
|
379
|
+
}
|
|
380
|
+
return resolved;
|
|
348
381
|
}
|
|
349
382
|
|
|
350
383
|
function resolveConfigPath(value: string | undefined): string | undefined {
|
|
351
|
-
if (
|
|
384
|
+
if (value === undefined) return undefined;
|
|
352
385
|
const resolved = interpolateEnvVars(value);
|
|
353
386
|
if (resolved === "~") return os.homedir();
|
|
354
387
|
if (resolved.startsWith("~/") || resolved.startsWith("~\\")) return path.join(os.homedir(), resolved.slice(2));
|
|
@@ -356,8 +389,8 @@ function resolveConfigPath(value: string | undefined): string | undefined {
|
|
|
356
389
|
}
|
|
357
390
|
|
|
358
391
|
function resolveBearerToken(definition: Pick<ServerEntry, "bearerToken" | "bearerTokenEnv">): string | undefined {
|
|
359
|
-
if (
|
|
360
|
-
return
|
|
392
|
+
if (definition.bearerToken !== undefined) return interpolateSecretExpression(definition.bearerToken);
|
|
393
|
+
return definition.bearerTokenEnv ? process.env[definition.bearerTokenEnv] : undefined;
|
|
361
394
|
}
|
|
362
395
|
|
|
363
396
|
function stableStringify(value: unknown): string {
|
|
@@ -30,6 +30,14 @@ export interface ParentModel {
|
|
|
30
30
|
id: string;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
export function normalizeParentModel(model: unknown): ParentModel | undefined {
|
|
34
|
+
if (!model || typeof model !== "object") return undefined;
|
|
35
|
+
const candidate = model as { provider?: unknown; id?: unknown };
|
|
36
|
+
if (typeof candidate.provider !== "string" || typeof candidate.id !== "string") return undefined;
|
|
37
|
+
if (!candidate.provider || !candidate.id) return undefined;
|
|
38
|
+
return { provider: candidate.provider, id: candidate.id };
|
|
39
|
+
}
|
|
40
|
+
|
|
33
41
|
/**
|
|
34
42
|
* Normalize a model id or provider segment for fuzzy comparison: case-fold,
|
|
35
43
|
* treat dots/underscores as dashes (so `4.5` matches `4-5`), and collapse
|
|
@@ -253,6 +253,9 @@ export function buildPiArgs(input: BuildPiArgsInput): BuildPiArgsResult {
|
|
|
253
253
|
}
|
|
254
254
|
for (const extPath of toolPlan.extensionArgs) args.push("--extension", extPath);
|
|
255
255
|
|
|
256
|
+
if (!input.inheritProjectContext) {
|
|
257
|
+
args.push("--no-context-files");
|
|
258
|
+
}
|
|
256
259
|
if (!input.inheritSkills) {
|
|
257
260
|
args.push("--no-skills");
|
|
258
261
|
}
|
|
@@ -40,7 +40,7 @@ const REVIEWER_REQUIRED_EDIT_PATTERNS = [
|
|
|
40
40
|
const NO_EDIT_PROHIBITION_PATTERN = /\b(?:do not|don't|must not)\s+(?:edit|modify|write(?:\s+to)?|touch|change)\b((?:(?!\b(?:but|and|then)\b)[^.;,:!?\n–—-])*)/gi;
|
|
41
41
|
|
|
42
42
|
/** Objects of a no-edit prohibition that mean "the codebase in general" rather than a named scope. */
|
|
43
|
-
const GENERIC_PROHIBITION_OBJECT = /^\s*(?:(?:any|all|the|these|those|your|our|existing|project|source|sources|repo|repository)[\s/,-]*)*(?:files?|code|codebase|sources?|anything|repo(?:sitory)?)?\s*$/i;
|
|
43
|
+
const GENERIC_PROHIBITION_OBJECT = /^\s*(?:(?:any|all|the|these|those|your|our|existing|project|product|source|sources|config|configs|repo|repository)[\s/,-]*)*(?:files?|code|codebase|sources?|anything|repo(?:sitory)?)?\s*$/i;
|
|
44
44
|
|
|
45
45
|
const SCOPED_NO_EDIT_CONSTRAINT_PATTERNS = [
|
|
46
46
|
/\bdo not edit files?\s+outside\b/i,
|
package/src/shared/types.ts
CHANGED
|
@@ -877,6 +877,8 @@ export interface Details {
|
|
|
877
877
|
processTerminal?: ProcessTerminalV1;
|
|
878
878
|
};
|
|
879
879
|
launchContractDigest?: string;
|
|
880
|
+
/** Original launch contract whose persisted session is being revived. */
|
|
881
|
+
sourceLaunchContractDigest?: string;
|
|
880
882
|
}
|
|
881
883
|
|
|
882
884
|
// ============================================================================
|
|
@@ -1202,6 +1204,8 @@ export interface ForegroundResumeChild {
|
|
|
1202
1204
|
index: number;
|
|
1203
1205
|
context?: "fresh" | "fork";
|
|
1204
1206
|
sessionFile?: string;
|
|
1207
|
+
model?: string;
|
|
1208
|
+
thinking?: string;
|
|
1205
1209
|
status: SubagentResultStatus;
|
|
1206
1210
|
activityState?: ActivityState;
|
|
1207
1211
|
lastActivityAt?: number;
|
|
@@ -1223,6 +1227,7 @@ export interface ForegroundResumeChild {
|
|
|
1223
1227
|
detachedReason?: string;
|
|
1224
1228
|
acceptance?: AcceptanceLedger;
|
|
1225
1229
|
agentContract?: AgentContract;
|
|
1230
|
+
launchContractDigest?: string;
|
|
1226
1231
|
execution?: ExecutionProjection;
|
|
1227
1232
|
review?: ReviewProjection;
|
|
1228
1233
|
effects?: EffectsProjection;
|
|
@@ -1290,6 +1295,8 @@ export interface SubagentState {
|
|
|
1290
1295
|
/** Runtime-owned artifact resolution inputs used by Fleet transcript targeting. */
|
|
1291
1296
|
artifactDirPreference?: ArtifactDirPreference;
|
|
1292
1297
|
parentSessionFile?: string | null;
|
|
1298
|
+
/** Last valid parent session model observed for this session; used when continuation contexts omit ctx.model. */
|
|
1299
|
+
lastParentModel?: { provider: string; id: string };
|
|
1293
1300
|
subagentInProgress?: boolean;
|
|
1294
1301
|
subagentSpawns?: {
|
|
1295
1302
|
sessionId: string | null;
|
|
@@ -1429,6 +1436,8 @@ export type IntercomBridgeMode = "off" | "fork-only" | "always";
|
|
|
1429
1436
|
export interface IntercomBridgeConfig {
|
|
1430
1437
|
mode?: IntercomBridgeMode;
|
|
1431
1438
|
instructionFile?: string;
|
|
1439
|
+
/** Deliver grouped completion messages through an external acknowledged intercom listener. */
|
|
1440
|
+
resultDelivery?: boolean;
|
|
1432
1441
|
}
|
|
1433
1442
|
|
|
1434
1443
|
interface TopLevelParallelConfig {
|
|
@@ -1457,10 +1466,14 @@ export interface ScheduledRunsConfig {
|
|
|
1457
1466
|
maxPending?: number;
|
|
1458
1467
|
}
|
|
1459
1468
|
|
|
1469
|
+
export type FleetViewPlacement = "aboveEditor" | "belowEditor";
|
|
1470
|
+
|
|
1460
1471
|
export interface ExtensionConfig {
|
|
1461
1472
|
asyncByDefault?: boolean;
|
|
1462
|
-
/** Show the Claude Code-style navigable fleet
|
|
1473
|
+
/** Show the Claude Code-style navigable fleet. Defaults to true. */
|
|
1463
1474
|
fleetView?: boolean;
|
|
1475
|
+
/** Place the persistent FleetView above or below the editor. Defaults to belowEditor. */
|
|
1476
|
+
fleetViewPlacement?: FleetViewPlacement;
|
|
1464
1477
|
/** Show the legacy above-editor async runs widget. Defaults to true only when fleetView is disabled. */
|
|
1465
1478
|
asyncWidget?: boolean;
|
|
1466
1479
|
/** Tool description variant registered for the parent-facing subagent tool. Defaults to full. */
|
|
@@ -69,6 +69,7 @@ interface PromptTemplateDelegationTaskProgress {
|
|
|
69
69
|
|
|
70
70
|
export interface PromptTemplateDelegationUpdate {
|
|
71
71
|
requestId: string;
|
|
72
|
+
runId?: string;
|
|
72
73
|
currentTool?: string;
|
|
73
74
|
currentToolArgs?: string;
|
|
74
75
|
recentOutput?: string;
|
|
@@ -323,6 +324,7 @@ export function toDelegationUpdate(requestId: string, update: PromptTemplateBrid
|
|
|
323
324
|
: undefined;
|
|
324
325
|
return {
|
|
325
326
|
requestId,
|
|
327
|
+
...(update.details?.runId ? { runId: update.details.runId } : {}),
|
|
326
328
|
currentTool: progress?.currentTool,
|
|
327
329
|
currentToolArgs: progress?.currentToolArgs,
|
|
328
330
|
recentOutput: safeLastOutput,
|
|
@@ -414,6 +416,7 @@ export function toSubagentDelegationUpdate(requestId: string, result: PromptTemp
|
|
|
414
416
|
return {
|
|
415
417
|
version: SUBAGENT_DELEGATION_PROTOCOL_VERSION,
|
|
416
418
|
requestId,
|
|
419
|
+
...(legacy.runId ? { runId: legacy.runId } : {}),
|
|
417
420
|
...(legacy.currentTool ? { currentTool: legacy.currentTool } : {}),
|
|
418
421
|
...(legacy.currentToolArgs ? { currentToolArgs: legacy.currentToolArgs } : {}),
|
|
419
422
|
...(legacy.recentOutput ? { recentOutput: legacy.recentOutput } : {}),
|
|
@@ -437,6 +440,7 @@ export function toSubagentDelegationV2Update(
|
|
|
437
440
|
requestId: request.requestId,
|
|
438
441
|
ownerRunId: request.ownerRunId,
|
|
439
442
|
nodeId: request.nodeId,
|
|
443
|
+
...(legacy.runId ? { runId: legacy.runId } : {}),
|
|
440
444
|
...(legacy.currentTool ? { currentTool: legacy.currentTool } : {}),
|
|
441
445
|
...(legacy.currentToolArgs ? { currentToolArgs: legacy.currentToolArgs } : {}),
|
|
442
446
|
...(legacy.recentOutput ? { recentOutput: legacy.recentOutput } : {}),
|
|
@@ -553,6 +557,7 @@ export function toSubagentDelegationV2Response(
|
|
|
553
557
|
...(child?.model ? { model: child.model } : {}),
|
|
554
558
|
...(child?.thinking ? { thinking: child.thinking } : {}),
|
|
555
559
|
...(typeof child?.exitCode === "number" ? { exitCode: child.exitCode } : {}),
|
|
560
|
+
...(child?.launchContractDigest ? { launchContractDigest: child.launchContractDigest } : {}),
|
|
556
561
|
...(projectedResult ? { result: projectedResult } : {}),
|
|
557
562
|
...(usage ? {
|
|
558
563
|
usage: {
|
package/src/tui/fleet-status.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { Editor, isKeyRelease, Key, matchesKey, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
3
|
-
import type { AsyncJobStep, SubagentState } from "../shared/types.ts";
|
|
3
|
+
import type { AsyncJobStep, FleetViewPlacement, SubagentState } from "../shared/types.ts";
|
|
4
4
|
|
|
5
5
|
export const FLEET_STATUS_WIDGET_KEY = "subagent-fleet-status";
|
|
6
6
|
|
|
@@ -22,6 +22,11 @@ type FleetStatusEntry = {
|
|
|
22
22
|
export interface FleetStatusOptions {
|
|
23
23
|
refreshMs?: number;
|
|
24
24
|
maxAgentRows?: number;
|
|
25
|
+
placement?: FleetViewPlacement;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function resolveFleetViewPlacement(value: unknown): FleetViewPlacement {
|
|
29
|
+
return value === "aboveEditor" ? "aboveEditor" : "belowEditor";
|
|
25
30
|
}
|
|
26
31
|
|
|
27
32
|
export function formatFleetElapsed(ms: number): string {
|
|
@@ -48,6 +53,13 @@ function isActiveState(value: string): boolean {
|
|
|
48
53
|
return value === "running" || value === "queued" || value === "pending";
|
|
49
54
|
}
|
|
50
55
|
|
|
56
|
+
function isStaleExtensionContextError(error: unknown): boolean {
|
|
57
|
+
// Pi currently exposes stale contexts as plain Errors without a stable code or subtype.
|
|
58
|
+
return error instanceof Error
|
|
59
|
+
&& (error.message.includes("This extension ctx is stale")
|
|
60
|
+
|| error.message.includes("Extension context no longer active"));
|
|
61
|
+
}
|
|
62
|
+
|
|
51
63
|
export function collectFleetStatusEntries(state: SubagentState): FleetStatusEntry[] {
|
|
52
64
|
const entries: FleetStatusEntry[] = [];
|
|
53
65
|
for (const control of state.foregroundControls.values()) {
|
|
@@ -111,6 +123,7 @@ export function collectFleetStatusEntries(state: SubagentState): FleetStatusEntr
|
|
|
111
123
|
|
|
112
124
|
export class SubagentFleetStatus {
|
|
113
125
|
private ctx: ExtensionContext | undefined;
|
|
126
|
+
private ui: ExtensionContext["ui"] | undefined;
|
|
114
127
|
private tui: FleetStatusTui | undefined;
|
|
115
128
|
private inputUnsubscribe: (() => void) | undefined;
|
|
116
129
|
private timer: ReturnType<typeof setInterval> | undefined;
|
|
@@ -124,6 +137,7 @@ export class SubagentFleetStatus {
|
|
|
124
137
|
private readonly openInspector: (itemKey: string) => Promise<void> | void;
|
|
125
138
|
private readonly refreshMs: number;
|
|
126
139
|
private readonly maxAgentRows: number;
|
|
140
|
+
private readonly placement: FleetViewPlacement;
|
|
127
141
|
|
|
128
142
|
constructor(
|
|
129
143
|
state: SubagentState,
|
|
@@ -134,19 +148,22 @@ export class SubagentFleetStatus {
|
|
|
134
148
|
this.openInspector = openInspector;
|
|
135
149
|
this.refreshMs = options.refreshMs ?? REFRESH_MS;
|
|
136
150
|
this.maxAgentRows = options.maxAgentRows ?? MAX_AGENT_ROWS;
|
|
151
|
+
this.placement = options.placement ?? "belowEditor";
|
|
137
152
|
}
|
|
138
153
|
|
|
139
154
|
setContext(ctx: ExtensionContext): void {
|
|
140
155
|
if (!ctx.hasUI) return;
|
|
141
|
-
|
|
156
|
+
const ui = ctx.ui;
|
|
157
|
+
if (this.ui === ui) {
|
|
142
158
|
this.ctx = ctx;
|
|
143
159
|
this.refresh();
|
|
144
160
|
return;
|
|
145
161
|
}
|
|
146
162
|
this.clearUiRegistration();
|
|
147
163
|
this.ctx = ctx;
|
|
148
|
-
|
|
149
|
-
|
|
164
|
+
this.ui = ui;
|
|
165
|
+
if (typeof ui.onTerminalInput === "function") {
|
|
166
|
+
this.inputUnsubscribe = ui.onTerminalInput((data) => this.handleKey(data));
|
|
150
167
|
}
|
|
151
168
|
this.timer = setInterval(() => this.refresh(), this.refreshMs);
|
|
152
169
|
this.timer.unref?.();
|
|
@@ -156,6 +173,7 @@ export class SubagentFleetStatus {
|
|
|
156
173
|
dispose(): void {
|
|
157
174
|
this.clearUiRegistration();
|
|
158
175
|
this.ctx = undefined;
|
|
176
|
+
this.ui = undefined;
|
|
159
177
|
this.entries = [];
|
|
160
178
|
this.active = false;
|
|
161
179
|
this.selectedKey = "main";
|
|
@@ -164,8 +182,8 @@ export class SubagentFleetStatus {
|
|
|
164
182
|
}
|
|
165
183
|
|
|
166
184
|
refresh(): void {
|
|
167
|
-
const ctx = this.
|
|
168
|
-
if (!ctx
|
|
185
|
+
const ctx = this.getActiveUiContext();
|
|
186
|
+
if (!ctx) return;
|
|
169
187
|
this.entries = collectFleetStatusEntries(this.state);
|
|
170
188
|
this.clampSelection();
|
|
171
189
|
if (this.inspectorOpen || this.state.fleetInspectorOpen) {
|
|
@@ -204,7 +222,7 @@ export class SubagentFleetStatus {
|
|
|
204
222
|
this.tui = undefined;
|
|
205
223
|
},
|
|
206
224
|
};
|
|
207
|
-
}, { placement:
|
|
225
|
+
}, { placement: this.placement });
|
|
208
226
|
this.widgetRegistered = true;
|
|
209
227
|
this.lastRenderKey = renderKey;
|
|
210
228
|
return;
|
|
@@ -215,8 +233,8 @@ export class SubagentFleetStatus {
|
|
|
215
233
|
}
|
|
216
234
|
|
|
217
235
|
handleKey(data: string): { consume?: boolean; data?: string } | undefined {
|
|
218
|
-
const ctx = this.
|
|
219
|
-
if (!ctx
|
|
236
|
+
const ctx = this.getActiveUiContext();
|
|
237
|
+
if (!ctx || this.entries.length === 0 || isKeyRelease(data)) return undefined;
|
|
220
238
|
if (this.inspectorOpen) return undefined;
|
|
221
239
|
if (!this.editorHasFocus()) {
|
|
222
240
|
if (this.active) this.deactivate();
|
|
@@ -344,19 +362,47 @@ export class SubagentFleetStatus {
|
|
|
344
362
|
});
|
|
345
363
|
}
|
|
346
364
|
|
|
365
|
+
private getActiveUiContext(): ExtensionContext | undefined {
|
|
366
|
+
const ctx = this.ctx;
|
|
367
|
+
if (!ctx) return undefined;
|
|
368
|
+
try {
|
|
369
|
+
return ctx.hasUI ? ctx : undefined;
|
|
370
|
+
} catch (error) {
|
|
371
|
+
if (!isStaleExtensionContextError(error)) throw error;
|
|
372
|
+
this.clearUiRegistration();
|
|
373
|
+
return undefined;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
347
377
|
private clearUiRegistration(): void {
|
|
348
378
|
if (this.timer) clearInterval(this.timer);
|
|
349
379
|
this.timer = undefined;
|
|
350
|
-
|
|
380
|
+
|
|
381
|
+
const inputUnsubscribe = this.inputUnsubscribe;
|
|
382
|
+
const ui = this.ui;
|
|
383
|
+
const widgetRegistered = this.widgetRegistered;
|
|
351
384
|
this.inputUnsubscribe = undefined;
|
|
352
|
-
|
|
385
|
+
this.ctx = undefined;
|
|
386
|
+
this.ui = undefined;
|
|
387
|
+
this.widgetRegistered = false;
|
|
388
|
+
this.tui = undefined;
|
|
389
|
+
|
|
390
|
+
const cleanupErrors: unknown[] = [];
|
|
391
|
+
try {
|
|
392
|
+
inputUnsubscribe?.();
|
|
393
|
+
} catch (error) {
|
|
394
|
+
if (!isStaleExtensionContextError(error)) cleanupErrors.push(error);
|
|
395
|
+
}
|
|
396
|
+
if (ui && widgetRegistered) {
|
|
353
397
|
try {
|
|
354
|
-
|
|
355
|
-
} catch {
|
|
356
|
-
|
|
398
|
+
ui.setWidget(FLEET_STATUS_WIDGET_KEY, undefined);
|
|
399
|
+
} catch (error) {
|
|
400
|
+
if (!isStaleExtensionContextError(error)) cleanupErrors.push(error);
|
|
357
401
|
}
|
|
358
402
|
}
|
|
359
|
-
|
|
360
|
-
|
|
403
|
+
if (cleanupErrors.length === 1) throw cleanupErrors[0];
|
|
404
|
+
if (cleanupErrors.length > 1) {
|
|
405
|
+
throw new AggregateError(cleanupErrors, "Failed to clean up FleetView UI registration");
|
|
406
|
+
}
|
|
361
407
|
}
|
|
362
408
|
}
|
package/src/tui/fleet.ts
CHANGED
|
@@ -463,7 +463,7 @@ export class SubagentFleetComponent implements Component {
|
|
|
463
463
|
this.refresh();
|
|
464
464
|
this.timer = setInterval(() => {
|
|
465
465
|
if (this.disposed) return;
|
|
466
|
-
this.
|
|
466
|
+
this.invalidate();
|
|
467
467
|
this.tui.requestRender();
|
|
468
468
|
}, options.refreshMs ?? REFRESH_MS);
|
|
469
469
|
this.timer.unref?.();
|
package/src/tui/render.ts
CHANGED
|
@@ -40,6 +40,7 @@ function getTermWidth(): number {
|
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
const segmenter = new Intl.Segmenter(undefined, { granularity: "grapheme" });
|
|
43
|
+
const ansiStylePattern = /\x1b\[[0-9;]*m/y;
|
|
43
44
|
|
|
44
45
|
/**
|
|
45
46
|
* Truncate a line to maxWidth, preserving ANSI styling through the ellipsis.
|
|
@@ -50,7 +51,7 @@ const segmenter = new Intl.Segmenter(undefined, { granularity: "grapheme" });
|
|
|
50
51
|
*
|
|
51
52
|
* Uses Intl.Segmenter for proper Unicode/emoji handling (not char-by-char).
|
|
52
53
|
*/
|
|
53
|
-
function truncLine(text: string, maxWidth: number): string {
|
|
54
|
+
export function truncLine(text: string, maxWidth: number): string {
|
|
54
55
|
if (visibleWidth(text) <= maxWidth) return text;
|
|
55
56
|
|
|
56
57
|
const targetWidth = maxWidth - 1;
|
|
@@ -60,7 +61,8 @@ function truncLine(text: string, maxWidth: number): string {
|
|
|
60
61
|
let i = 0;
|
|
61
62
|
|
|
62
63
|
while (i < text.length) {
|
|
63
|
-
|
|
64
|
+
ansiStylePattern.lastIndex = i;
|
|
65
|
+
const ansiMatch = ansiStylePattern.exec(text);
|
|
64
66
|
if (ansiMatch) {
|
|
65
67
|
const code = ansiMatch[0];
|
|
66
68
|
result += code;
|
|
@@ -74,11 +76,9 @@ function truncLine(text: string, maxWidth: number): string {
|
|
|
74
76
|
continue;
|
|
75
77
|
}
|
|
76
78
|
|
|
77
|
-
let end = i;
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
}
|
|
81
|
-
|
|
79
|
+
let end = text.indexOf("\x1b[", i);
|
|
80
|
+
if (end === i) end = text.indexOf("\x1b[", i + 2);
|
|
81
|
+
if (end === -1) end = text.length;
|
|
82
82
|
const textPortion = text.slice(i, end);
|
|
83
83
|
for (const seg of segmenter.segment(textPortion)) {
|
|
84
84
|
const grapheme = seg.segment;
|
|
@@ -529,17 +529,15 @@ function buildChainStepSpans(details: Pick<Details, "chainAgents" | "workflowGra
|
|
|
529
529
|
return spans;
|
|
530
530
|
}
|
|
531
531
|
|
|
532
|
-
function isChainParallelGroupActive(details: Pick<Details, "mode" | "chainAgents" | "currentStepIndex" | "workflowGraph">): boolean {
|
|
533
|
-
if (details.mode !== "chain") return false;
|
|
534
|
-
if (details.currentStepIndex === undefined) return false;
|
|
535
|
-
return buildChainStepSpans(details).some((span) => span.stepIndex === details.currentStepIndex && span.isParallel);
|
|
536
|
-
}
|
|
537
|
-
|
|
538
532
|
function buildAsyncChainStepSpans(total: number, stepCount: number, parallelGroups: AsyncParallelGroupStatus[] = []): ChainStepSpan[] {
|
|
533
|
+
const groupsByStep = new Map<number, AsyncParallelGroupStatus>();
|
|
534
|
+
for (const group of parallelGroups) {
|
|
535
|
+
if (!groupsByStep.has(group.stepIndex)) groupsByStep.set(group.stepIndex, group);
|
|
536
|
+
}
|
|
539
537
|
const spans: ChainStepSpan[] = [];
|
|
540
538
|
let flatIndex = 0;
|
|
541
539
|
for (let stepIndex = 0; stepIndex < total; stepIndex++) {
|
|
542
|
-
const group =
|
|
540
|
+
const group = groupsByStep.get(stepIndex);
|
|
543
541
|
if (group) {
|
|
544
542
|
spans.push({ stepIndex, start: group.start, count: group.count, isParallel: true });
|
|
545
543
|
flatIndex = Math.max(flatIndex, group.start + group.count);
|
|
@@ -567,6 +565,7 @@ interface ChainRenderResultEntry {
|
|
|
567
565
|
kind: "result";
|
|
568
566
|
resultIndex: number;
|
|
569
567
|
rowNumber: number;
|
|
568
|
+
rowLabel?: string;
|
|
570
569
|
agentName: string;
|
|
571
570
|
}
|
|
572
571
|
|
|
@@ -601,6 +600,7 @@ function buildChainRenderEntries(details: Details, label: MultiProgressLabel): C
|
|
|
601
600
|
kind: "result",
|
|
602
601
|
resultIndex: index,
|
|
603
602
|
rowNumber: index + 1,
|
|
603
|
+
rowLabel: span.isParallel ? `Agent ${index - span.start + 1}/${span.count}` : `Step ${span.stepIndex + 1}`,
|
|
604
604
|
agentName: details.results[index]?.agent ?? details.chainAgents?.[span.stepIndex] ?? `step-${span.stepIndex + 1}`,
|
|
605
605
|
});
|
|
606
606
|
}
|
|
@@ -622,7 +622,9 @@ interface MultiProgressLabel {
|
|
|
622
622
|
function buildMultiProgressLabel(details: Pick<Details, "mode" | "results" | "progress" | "totalSteps" | "currentStepIndex" | "chainAgents" | "workflowGraph">, hasRunning: boolean): MultiProgressLabel {
|
|
623
623
|
const stepSpans = buildChainStepSpans(details);
|
|
624
624
|
const hasParallelInChain = details.mode === "chain" && stepSpans.some((span) => span.isParallel);
|
|
625
|
-
const activeParallelGroup =
|
|
625
|
+
const activeParallelGroup = details.mode === "chain"
|
|
626
|
+
&& details.currentStepIndex !== undefined
|
|
627
|
+
&& stepSpans.some((span) => span.stepIndex === details.currentStepIndex && span.isParallel);
|
|
626
628
|
const itemTitle: "Step" | "Agent" = details.mode === "parallel" || activeParallelGroup ? "Agent" : "Step";
|
|
627
629
|
|
|
628
630
|
if (details.mode === "parallel") {
|
|
@@ -708,15 +710,10 @@ function buildMultiProgressLabel(details: Pick<Details, "mode" | "results" | "pr
|
|
|
708
710
|
return { headerLabel, itemTitle, totalCount, hasParallelInChain, activeParallelGroup, groupStartIndex: 0, groupEndIndex: details.results.length, showActiveGroupOnly: false };
|
|
709
711
|
}
|
|
710
712
|
|
|
711
|
-
function resultRowLabel(
|
|
712
|
-
if (details.mode === "chain" && label.hasParallelInChain) {
|
|
713
|
-
const span = buildChainStepSpans(details).find((candidate) => resultIndex >= candidate.start && resultIndex < candidate.start + candidate.count);
|
|
714
|
-
if (span?.isParallel) return `Agent ${resultIndex - span.start + 1}/${span.count}`;
|
|
715
|
-
if (span) return `Step ${span.stepIndex + 1}`;
|
|
716
|
-
}
|
|
713
|
+
function resultRowLabel(label: MultiProgressLabel, resultIndex: number, stepNumber: number): string {
|
|
717
714
|
if (label.itemTitle === "Agent") {
|
|
718
715
|
const localStepNumber = label.activeParallelGroup
|
|
719
|
-
?
|
|
716
|
+
? resultIndex - label.groupStartIndex + 1
|
|
720
717
|
: stepNumber;
|
|
721
718
|
return `Agent ${localStepNumber}/${label.totalCount}`;
|
|
722
719
|
}
|
|
@@ -1417,7 +1414,7 @@ function renderMultiCompact(d: Details, theme: Theme, frame?: number): Component
|
|
|
1417
1414
|
const rowNumber = entry.rowNumber;
|
|
1418
1415
|
const agentName = entry.agentName;
|
|
1419
1416
|
if (!r) {
|
|
1420
|
-
const pendingLabel =
|
|
1417
|
+
const pendingLabel = entry.rowLabel ?? `${itemTitle} ${rowNumber}`;
|
|
1421
1418
|
c.addChild(new Text(truncLine(theme.fg("dim", ` ◦ ${pendingLabel}: ${agentName} · pending`), width), 0, 0));
|
|
1422
1419
|
continue;
|
|
1423
1420
|
}
|
|
@@ -1430,7 +1427,7 @@ function renderMultiCompact(d: Details, theme: Theme, frame?: number): Component
|
|
|
1430
1427
|
const stepStats = formatProgressStats(theme, rProg);
|
|
1431
1428
|
const glyph = rPending ? theme.fg("dim", "◦") : resultGlyph(r, output, theme, rRunning, progressRunningSeed(rProg), frame);
|
|
1432
1429
|
const pendingLabel = rPending ? ` ${theme.fg("dim", "· pending")}` : "";
|
|
1433
|
-
const stepLabel = resultRowLabel(
|
|
1430
|
+
const stepLabel = entry.rowLabel ?? resultRowLabel(multiLabel, i, stepNumber);
|
|
1434
1431
|
const line = `${glyph} ${stepLabel}: ${themeBold(theme, agentName)}${contextModeBadge(theme, r.context)}${stepStats ? ` ${theme.fg("dim", "·")} ${stepStats}` : ""}${pendingLabel}`;
|
|
1435
1432
|
c.addChild(new Text(truncLine(` ${line}`, width), 0, 0));
|
|
1436
1433
|
if (rRunning && rProg && "status" in rProg) {
|
|
@@ -1717,7 +1714,7 @@ export function renderSubagentResult(
|
|
|
1717
1714
|
const agentName = entry.agentName;
|
|
1718
1715
|
|
|
1719
1716
|
if (!r) {
|
|
1720
|
-
const pendingLabel =
|
|
1717
|
+
const pendingLabel = entry.rowLabel ?? `${itemTitle} ${rowNumber}`;
|
|
1721
1718
|
c.addChild(new Text(fit(theme.fg("dim", ` ${pendingLabel}: ${agentName}`)), 0, 0));
|
|
1722
1719
|
c.addChild(new Text(theme.fg("dim", ` status: pending`), 0, 0));
|
|
1723
1720
|
c.addChild(new Spacer(1));
|
|
@@ -1740,7 +1737,7 @@ export function renderSubagentResult(
|
|
|
1740
1737
|
: theme.fg("success", "done");
|
|
1741
1738
|
const stats = rProg ? ` | ${rProg.toolCount} tools, ${formatDuration(rProg.durationMs)}` : "";
|
|
1742
1739
|
const modelDisplay = modelThinkingBadge(theme, r.model);
|
|
1743
|
-
const stepLabel = resultRowLabel(
|
|
1740
|
+
const stepLabel = entry.rowLabel ?? resultRowLabel(multiLabel, i, stepNumber);
|
|
1744
1741
|
const contextBadge = contextModeBadge(theme, r.context);
|
|
1745
1742
|
const stepHeader = rRunning
|
|
1746
1743
|
? `${statusIcon} ${stepLabel}: ${theme.bold(theme.fg("warning", r.agent))}${contextBadge}${modelDisplay}${stats}`
|