pi-subagents 0.63.0 → 0.65.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 +57 -1
- package/README.md +2 -2
- package/agents/reviewer.md +1 -1
- package/agents/scout.md +1 -1
- package/docs/agents.md +18 -16
- package/docs/configuration.md +7 -15
- package/docs/extension-api.md +15 -6
- package/docs/missions.md +2 -0
- package/docs/observability.md +10 -12
- package/docs/tool-reference.md +7 -6
- package/docs/watchdog.md +92 -114
- package/docs/workflows.md +5 -3
- package/package.json +3 -4
- package/skills/pi-subagents/references/constraints-and-recipes.md +1 -1
- package/skills/pi-subagents/references/execution-controls.md +6 -5
- package/src/agents/agent-management.ts +47 -15
- package/src/api/capability-ceiling.ts +0 -1
- package/src/api/{pi-args.ts → child-tool-plan.ts} +1 -1
- package/src/api/preflight.ts +2 -3
- package/src/extension/doctor.ts +2 -10
- package/src/extension/fanout-child.ts +9 -11
- package/src/extension/index.ts +27 -5
- package/src/extension/public-execution.ts +14 -0
- package/src/extension/rpc.ts +3 -2
- package/src/extension/schemas.ts +2 -1
- package/src/extension/tool-description.ts +9 -8
- package/src/intercom/native-supervisor-channel.ts +138 -60
- package/src/intercom/supervisor-ui.ts +243 -0
- package/src/runs/background/async-execution.ts +46 -24
- package/src/runs/background/async-job-tracker.ts +11 -0
- package/src/runs/background/async-resume.ts +11 -2
- package/src/runs/background/control-channel.ts +2 -204
- package/src/runs/background/notify.ts +41 -2
- package/src/runs/background/process-terminal.ts +1 -1
- package/src/runs/background/run-child-session.ts +613 -0
- package/src/runs/background/run-status.ts +0 -1
- package/src/runs/background/runner-aliases.ts +125 -0
- package/src/runs/background/runner-child-sessions.ts +31 -0
- package/src/runs/background/scheduled-runs.ts +18 -4
- package/src/runs/background/subagent-runner.ts +203 -898
- package/src/runs/foreground/async-steering-action.ts +1 -17
- package/src/runs/foreground/execution.ts +196 -378
- package/src/runs/foreground/foreground-control.ts +4 -0
- package/src/runs/foreground/subagent-executor.ts +121 -58
- package/src/runs/foreground/workflow-foreground-steering.ts +24 -98
- package/src/runs/shared/abort-recovery.ts +3 -3
- package/src/runs/shared/acceptance.ts +10 -0
- package/src/runs/shared/async-status-projection.ts +11 -43
- package/src/runs/shared/capability-ceiling.ts +1 -2
- package/src/runs/shared/child-hooks.ts +25 -0
- package/src/runs/shared/child-identity.ts +13 -2
- package/src/runs/shared/child-launch.ts +314 -0
- package/src/runs/shared/child-lifecycle.ts +25 -0
- package/src/runs/shared/child-runtime-config.ts +126 -0
- package/src/runs/shared/child-session.ts +342 -0
- package/src/runs/shared/child-tool-plan.ts +530 -0
- package/src/runs/shared/claude-code-adapter.ts +5 -1
- package/src/runs/shared/completion-guard.ts +1 -1
- package/src/runs/shared/external-cli-preflight.ts +16 -0
- package/src/runs/shared/mcp-direct-tool-allowlist.ts +5 -4
- package/src/runs/shared/model-exclusions.ts +82 -14
- package/src/runs/shared/model-fallback.ts +47 -4
- package/src/runs/shared/nested-events.ts +29 -45
- package/src/runs/shared/nested-path.ts +0 -14
- package/src/runs/shared/orca-progress-tabs.ts +12 -7
- package/src/runs/shared/parallel-utils.ts +0 -2
- package/src/runs/shared/permissions.ts +0 -13
- package/src/runs/shared/process-signal.ts +4 -1
- package/src/runs/shared/run-fanout-budget.ts +0 -13
- package/src/runs/shared/runtime-acknowledged-extensions.ts +0 -27
- package/src/runs/shared/structured-output.ts +17 -4
- package/src/runs/shared/subagent-control.ts +6 -2
- package/src/runs/shared/subagent-prompt-runtime.ts +87 -384
- package/src/runs/shared/tool-availability.ts +18 -62
- package/src/runs/shared/tool-budget.ts +0 -14
- package/src/runs/shared/worktree-cleanup-plan.ts +25 -6
- package/src/runs/shared/worktree.ts +117 -30
- package/src/shared/child-session-name.ts +1 -1
- package/src/shared/jsonl-writer.ts +11 -0
- package/src/shared/thinking-ceiling.ts +0 -6
- package/src/shared/types.ts +64 -30
- package/src/shared/utils.ts +3 -4
- package/src/slash/slash-commands.ts +0 -6
- package/src/tui/fleet.ts +0 -1
- package/src/tui/render.ts +238 -31
- package/src/watchdog/child-status.ts +54 -34
- package/src/watchdog/diff-tool.ts +77 -0
- package/src/watchdog/emission-guard.ts +5 -3
- package/src/watchdog/guidance.ts +20 -0
- package/src/watchdog/register-child.ts +28 -25
- package/src/watchdog/register-main.ts +10 -9
- package/src/watchdog/render.ts +4 -5
- package/src/watchdog/review.ts +15 -4
- package/src/watchdog/rules.ts +70 -0
- package/src/watchdog/runtime.ts +75 -92
- package/src/watchdog/scope.ts +0 -11
- package/src/watchdog/settings.ts +48 -104
- package/src/watchdog/types.ts +18 -32
- package/src/watchdog/warning-format.ts +0 -1
- package/src/workflows/chat-progress.ts +3 -2
- package/src/workflows/scripted-workflow.ts +48 -1
- package/src/workflows/workflow-checklist.ts +10 -12
- package/src/workflows/workflow-preflight.ts +28 -1
- package/src/runs/shared/child-protocol.ts +0 -415
- package/src/runs/shared/pi-args.ts +0 -1062
- package/src/runs/shared/subagent-startup-retry.ts +0 -116
- package/src/shared/post-exit-stdio-guard.ts +0 -85
|
@@ -4,17 +4,17 @@ import * as path from "node:path";
|
|
|
4
4
|
import type { AgentToolResult } from "@earendil-works/pi-agent-core";
|
|
5
5
|
import type { ExtensionAPI, ExtensionContext, ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
6
6
|
import { Type } from "typebox";
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
SUBAGENT_CHILD_INDEX_ENV,
|
|
10
|
-
SUBAGENT_ORCHESTRATOR_SESSION_ID_ENV,
|
|
11
|
-
SUBAGENT_ORCHESTRATOR_TARGET_ENV,
|
|
12
|
-
SUBAGENT_RUN_ID_ENV,
|
|
13
|
-
SUBAGENT_SUPERVISOR_CHANNEL_DIR_ENV,
|
|
14
|
-
} from "../runs/shared/pi-args.ts";
|
|
15
|
-
import { INTERCOM_DETACH_REQUEST_EVENT, POLL_INTERVAL_MS, TEMP_ROOT_DIR, type IntercomEventBus, type SubagentState } from "../shared/types.ts";
|
|
7
|
+
import type { ChildSupervisorMetadata } from "../runs/shared/child-runtime-config.ts";
|
|
8
|
+
import { INTERCOM_DETACH_REQUEST_EVENT, POLL_INTERVAL_MS, TEMP_ROOT_DIR, type ControlEvent, type IntercomEventBus, type SubagentState } from "../shared/types.ts";
|
|
16
9
|
import { writeAtomicJson } from "../shared/atomic-json.ts";
|
|
17
10
|
import { shouldUseNativeFsWatch } from "../shared/watch-strategy.ts";
|
|
11
|
+
import {
|
|
12
|
+
SUPERVISOR_REQUEST_MESSAGE_TYPE,
|
|
13
|
+
SUPERVISOR_REPLY_ENTRY_TYPE,
|
|
14
|
+
supervisorReplyHint,
|
|
15
|
+
type SupervisorReason,
|
|
16
|
+
type SupervisorReplyEntryData,
|
|
17
|
+
} from "./supervisor-ui.ts";
|
|
18
18
|
|
|
19
19
|
const SUPERVISOR_CHANNEL_ROOT = path.join(TEMP_ROOT_DIR, "supervisor-channels");
|
|
20
20
|
const REQUESTS_DIR = "requests";
|
|
@@ -26,8 +26,10 @@ const CHANNEL_POLL_MS = Math.min(POLL_INTERVAL_MS, 500);
|
|
|
26
26
|
const CHANNEL_SAFETY_POLL_MS = 5000;
|
|
27
27
|
const STALE_EMPTY_CHANNEL_AGE_MS = 60 * 1000;
|
|
28
28
|
const STALE_EMPTY_CHANNEL_CLEANUP_INTERVAL_MS = 60 * 1000;
|
|
29
|
+
const MAX_SUPERVISOR_REQUEST_CORRELATIONS = 256;
|
|
30
|
+
const SUPERVISOR_REQUEST_CORRELATION_RETENTION_MS = 10 * 60 * 1000;
|
|
29
31
|
|
|
30
|
-
type
|
|
32
|
+
export type SupervisorRequestState = "pending" | "resolved" | "unknown";
|
|
31
33
|
|
|
32
34
|
interface SupervisorRequest {
|
|
33
35
|
type: "subagent.supervisor.request";
|
|
@@ -42,6 +44,7 @@ interface SupervisorRequest {
|
|
|
42
44
|
runId: string;
|
|
43
45
|
agent: string;
|
|
44
46
|
childIndex: number;
|
|
47
|
+
toolCallId?: string;
|
|
45
48
|
childTarget?: string;
|
|
46
49
|
interview?: unknown;
|
|
47
50
|
}
|
|
@@ -51,6 +54,12 @@ interface PendingSupervisorRequest extends SupervisorRequest {
|
|
|
51
54
|
requestFile: string;
|
|
52
55
|
}
|
|
53
56
|
|
|
57
|
+
interface SupervisorRequestCorrelation {
|
|
58
|
+
request: PendingSupervisorRequest;
|
|
59
|
+
state: Exclude<SupervisorRequestState, "unknown">;
|
|
60
|
+
updatedAt: number;
|
|
61
|
+
}
|
|
62
|
+
|
|
54
63
|
interface SupervisorReply {
|
|
55
64
|
type: "subagent.supervisor.reply";
|
|
56
65
|
requestId: string;
|
|
@@ -113,37 +122,6 @@ function replyPath(channelDir: string, requestId: string): string {
|
|
|
113
122
|
return path.join(channelDir, REPLIES_DIR, `${safeSegment(requestId)}.json`);
|
|
114
123
|
}
|
|
115
124
|
|
|
116
|
-
function readTextEnv(name: string): string | undefined {
|
|
117
|
-
const value = process.env[name]?.trim();
|
|
118
|
-
return value ? value : undefined;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
function readChildMetadata(): {
|
|
122
|
-
channelDir: string;
|
|
123
|
-
runId: string;
|
|
124
|
-
agent: string;
|
|
125
|
-
childIndex: number;
|
|
126
|
-
orchestratorTarget?: string;
|
|
127
|
-
orchestratorSessionId?: string;
|
|
128
|
-
childTarget?: string;
|
|
129
|
-
} | undefined {
|
|
130
|
-
const channelDir = readTextEnv(SUBAGENT_SUPERVISOR_CHANNEL_DIR_ENV);
|
|
131
|
-
const runId = readTextEnv(SUBAGENT_RUN_ID_ENV);
|
|
132
|
-
const agent = readTextEnv(SUBAGENT_CHILD_AGENT_ENV);
|
|
133
|
-
const rawIndex = readTextEnv(SUBAGENT_CHILD_INDEX_ENV);
|
|
134
|
-
const orchestratorSessionId = readTextEnv(SUBAGENT_ORCHESTRATOR_SESSION_ID_ENV);
|
|
135
|
-
if (!channelDir || !runId || !agent || !orchestratorSessionId || rawIndex === undefined || !/^\d+$/.test(rawIndex)) return undefined;
|
|
136
|
-
return {
|
|
137
|
-
channelDir,
|
|
138
|
-
runId,
|
|
139
|
-
agent,
|
|
140
|
-
childIndex: Number(rawIndex),
|
|
141
|
-
orchestratorTarget: readTextEnv(SUBAGENT_ORCHESTRATOR_TARGET_ENV),
|
|
142
|
-
orchestratorSessionId,
|
|
143
|
-
childTarget: readTextEnv("PI_SUBAGENT_INTERCOM_SESSION_NAME"),
|
|
144
|
-
};
|
|
145
|
-
}
|
|
146
|
-
|
|
147
125
|
function reasonHeading(reason: SupervisorReason): string {
|
|
148
126
|
if (reason === "interview_request") return "Subagent requests a structured supervisor interview.";
|
|
149
127
|
if (reason === "progress_update") return "Subagent progress update.";
|
|
@@ -231,9 +209,7 @@ async function waitForReply(channelDir: string, requestId: string, deadline: num
|
|
|
231
209
|
throw new Error("Timed out waiting for supervisor reply.");
|
|
232
210
|
}
|
|
233
211
|
|
|
234
|
-
async function sendSupervisorRequest(params: ContactSupervisorParams, signal?: AbortSignal): Promise<AgentToolResult<Record<string, unknown>>> {
|
|
235
|
-
const metadata = readChildMetadata();
|
|
236
|
-
if (!metadata) throw new Error("Native supervisor channel is not available for this subagent.");
|
|
212
|
+
async function sendSupervisorRequest(params: ContactSupervisorParams, metadata: ChildSupervisorMetadata, signal?: AbortSignal, toolCallId?: string): Promise<AgentToolResult<Record<string, unknown>>> {
|
|
237
213
|
if (params.reason !== "progress_update" && !params.message?.trim() && params.reason !== "interview_request") {
|
|
238
214
|
throw new Error("message is required for supervisor decisions.");
|
|
239
215
|
}
|
|
@@ -244,6 +220,7 @@ async function sendSupervisorRequest(params: ContactSupervisorParams, signal?: A
|
|
|
244
220
|
const replyDeadline = createdAt + askTimeoutMs();
|
|
245
221
|
const expiresAt = expectsReply ? replyDeadline : undefined;
|
|
246
222
|
const message = formatChildMessage({ ...metadata, reason: params.reason, message: params.message, interview: params.interview });
|
|
223
|
+
const requestToolCallId = typeof toolCallId === "string" && toolCallId.length > 0 ? toolCallId : undefined;
|
|
247
224
|
const request: SupervisorRequest = {
|
|
248
225
|
type: "subagent.supervisor.request",
|
|
249
226
|
id: requestId,
|
|
@@ -257,6 +234,7 @@ async function sendSupervisorRequest(params: ContactSupervisorParams, signal?: A
|
|
|
257
234
|
runId: metadata.runId,
|
|
258
235
|
agent: metadata.agent,
|
|
259
236
|
childIndex: metadata.childIndex,
|
|
237
|
+
...(requestToolCallId ? { toolCallId: requestToolCallId } : {}),
|
|
260
238
|
...(metadata.childTarget ? { childTarget: metadata.childTarget } : {}),
|
|
261
239
|
...(params.interview !== undefined ? { interview: params.interview } : {}),
|
|
262
240
|
};
|
|
@@ -297,15 +275,19 @@ function hasTool(pi: ExtensionAPI, name: string): boolean {
|
|
|
297
275
|
}
|
|
298
276
|
}
|
|
299
277
|
|
|
300
|
-
|
|
301
|
-
|
|
278
|
+
/**
|
|
279
|
+
* Register the child-side `contact_supervisor` tool. The host passes the
|
|
280
|
+
* channel metadata in the child runtime config.
|
|
281
|
+
*/
|
|
282
|
+
export function registerNativeSupervisorClient(pi: ExtensionAPI, metadata: ChildSupervisorMetadata | undefined): void {
|
|
283
|
+
if (!metadata || hasTool(pi, "contact_supervisor")) return;
|
|
302
284
|
const tool: ToolDefinition<typeof ContactSupervisorParamsSchema, Record<string, unknown>> = {
|
|
303
285
|
name: "contact_supervisor",
|
|
304
286
|
label: "Contact Supervisor",
|
|
305
287
|
description: "Contact the parent/supervisor session for a blocking decision, structured interview, or progress update.",
|
|
306
288
|
parameters: ContactSupervisorParamsSchema,
|
|
307
|
-
execute(
|
|
308
|
-
return sendSupervisorRequest(params as ContactSupervisorParams, signal);
|
|
289
|
+
execute(id, params, signal) {
|
|
290
|
+
return sendSupervisorRequest(params as ContactSupervisorParams, metadata, signal, id);
|
|
309
291
|
},
|
|
310
292
|
};
|
|
311
293
|
pi.registerTool(tool);
|
|
@@ -319,7 +301,12 @@ function parseRequestFile(file: string, channelDir: string): PendingSupervisorRe
|
|
|
319
301
|
if (parsed.reason !== "need_decision" && parsed.reason !== "interview_request" && parsed.reason !== "progress_update") return undefined;
|
|
320
302
|
if (typeof parsed.message !== "string" || !parsed.message) return undefined;
|
|
321
303
|
if (typeof parsed.runId !== "string" || typeof parsed.agent !== "string" || typeof parsed.childIndex !== "number") return undefined;
|
|
322
|
-
return {
|
|
304
|
+
return {
|
|
305
|
+
...parsed as SupervisorRequest,
|
|
306
|
+
...(typeof parsed.toolCallId === "string" && parsed.toolCallId.length > 0 ? { toolCallId: parsed.toolCallId } : { toolCallId: undefined }),
|
|
307
|
+
channelDir,
|
|
308
|
+
requestFile: file,
|
|
309
|
+
};
|
|
323
310
|
} catch {
|
|
324
311
|
return undefined;
|
|
325
312
|
}
|
|
@@ -483,6 +470,7 @@ function removeRequestFile(file: string): void {
|
|
|
483
470
|
}
|
|
484
471
|
|
|
485
472
|
type SupervisorRequestLifecycle = "pending" | "resolved" | "expired" | "inactive" | "missing" | "wrong-session";
|
|
473
|
+
type SupervisorRequestLifecycleObserver = (request: PendingSupervisorRequest, lifecycle: SupervisorRequestLifecycle) => void;
|
|
486
474
|
|
|
487
475
|
function requestExpiresAt(request: SupervisorRequest, now: number): number {
|
|
488
476
|
const expiresAt = (request as { expiresAt?: unknown }).expiresAt;
|
|
@@ -515,30 +503,31 @@ function cleanupRequestLifecycle(request: PendingSupervisorRequest, lifecycle: S
|
|
|
515
503
|
if (lifecycle === "resolved" || lifecycle === "expired" || lifecycle === "inactive") removeRequestFile(request.requestFile);
|
|
516
504
|
}
|
|
517
505
|
|
|
518
|
-
function refreshPendingRequests(pending: Map<string, PendingSupervisorRequest>, state: SubagentState, ctx: ExtensionContext | undefined): void {
|
|
506
|
+
function refreshPendingRequests(pending: Map<string, PendingSupervisorRequest>, state: SubagentState, ctx: ExtensionContext | undefined, onLifecycle: SupervisorRequestLifecycleObserver): void {
|
|
519
507
|
const now = Date.now();
|
|
520
508
|
for (const request of pending.values()) {
|
|
521
509
|
const lifecycle = requestLifecycle(request, state, ctx, now);
|
|
522
510
|
if (lifecycle === "pending") continue;
|
|
523
511
|
pending.delete(request.id);
|
|
512
|
+
onLifecycle(request, lifecycle);
|
|
524
513
|
cleanupRequestLifecycle(request, lifecycle);
|
|
525
514
|
}
|
|
526
515
|
}
|
|
527
516
|
|
|
528
517
|
function formatPendingLine(request: PendingSupervisorRequest): string {
|
|
529
|
-
const replyHint = request.expectsReply ? ` Reply: ${
|
|
518
|
+
const replyHint = request.expectsReply ? ` Reply: ${supervisorReplyHint(request.id)}` : "";
|
|
530
519
|
return `- ${request.id}: ${request.agent} [${request.runId}#${request.childIndex}] ${request.reason}.${replyHint}`;
|
|
531
520
|
}
|
|
532
521
|
|
|
533
522
|
function requestVisibleText(request: PendingSupervisorRequest): string {
|
|
534
523
|
const lines = [request.message];
|
|
535
524
|
if (request.expectsReply) {
|
|
536
|
-
lines.push("", `Reply with: ${
|
|
525
|
+
lines.push("", `Reply with: ${supervisorReplyHint(request.id)}`);
|
|
537
526
|
}
|
|
538
527
|
return lines.join("\n");
|
|
539
528
|
}
|
|
540
529
|
|
|
541
|
-
function writeReply(request: PendingSupervisorRequest, message: string):
|
|
530
|
+
function writeReply(request: PendingSupervisorRequest, message: string): SupervisorReply {
|
|
542
531
|
if (!message.trim()) throw new Error("message is required for supervisor replies.");
|
|
543
532
|
const reply: SupervisorReply = {
|
|
544
533
|
type: "subagent.supervisor.reply",
|
|
@@ -548,6 +537,29 @@ function writeReply(request: PendingSupervisorRequest, message: string): void {
|
|
|
548
537
|
};
|
|
549
538
|
writeAtomicJson(replyPath(request.channelDir, request.id), reply);
|
|
550
539
|
removeRequestFile(request.requestFile);
|
|
540
|
+
return reply;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
function appendSupervisorReplyEntry(pi: ExtensionAPI, request: PendingSupervisorRequest, reply: SupervisorReply): void {
|
|
544
|
+
const appendEntry = (pi as unknown as {
|
|
545
|
+
appendEntry?: (customType: string, data?: SupervisorReplyEntryData) => void;
|
|
546
|
+
}).appendEntry;
|
|
547
|
+
if (typeof appendEntry !== "function") return;
|
|
548
|
+
try {
|
|
549
|
+
appendEntry.call(pi, SUPERVISOR_REPLY_ENTRY_TYPE, {
|
|
550
|
+
requestId: request.id,
|
|
551
|
+
reason: request.reason,
|
|
552
|
+
runId: request.runId,
|
|
553
|
+
agent: request.agent,
|
|
554
|
+
childIndex: request.childIndex,
|
|
555
|
+
...(request.childTarget ? { childTarget: request.childTarget } : {}),
|
|
556
|
+
message: reply.message,
|
|
557
|
+
createdAt: reply.createdAt,
|
|
558
|
+
});
|
|
559
|
+
} catch (error) {
|
|
560
|
+
// The reply file is authoritative; a UI journal failure must not undo a delivered reply.
|
|
561
|
+
console.error("Failed to journal native supervisor reply:", error);
|
|
562
|
+
}
|
|
551
563
|
}
|
|
552
564
|
|
|
553
565
|
function resolvePendingRequest(pending: Map<string, PendingSupervisorRequest>, params: IntercomParams): PendingSupervisorRequest {
|
|
@@ -583,14 +595,14 @@ function publicPendingRequests(pending: Map<string, PendingSupervisorRequest>):
|
|
|
583
595
|
}));
|
|
584
596
|
}
|
|
585
597
|
|
|
586
|
-
function buildParentSupervisorTool(pending: Map<string, PendingSupervisorRequest>, state: SubagentState): ToolDefinition<typeof IntercomParamsSchema, Record<string, unknown>> {
|
|
598
|
+
function buildParentSupervisorTool(pi: ExtensionAPI, pending: Map<string, PendingSupervisorRequest>, state: SubagentState, onLifecycle: SupervisorRequestLifecycleObserver): ToolDefinition<typeof IntercomParamsSchema, Record<string, unknown>> {
|
|
587
599
|
return {
|
|
588
600
|
name: NATIVE_SUPERVISOR_TOOL_NAME,
|
|
589
601
|
label: "Subagent Supervisor",
|
|
590
602
|
description: "Native pi-subagents supervisor channel. Use reply/pending/status to answer child subagent requests without overriding pi-intercom.",
|
|
591
603
|
parameters: IntercomParamsSchema,
|
|
592
604
|
async execute(_id, params) {
|
|
593
|
-
refreshPendingRequests(pending, state, state.lastUiContext ?? undefined);
|
|
605
|
+
refreshPendingRequests(pending, state, state.lastUiContext ?? undefined, onLifecycle);
|
|
594
606
|
const input = params as IntercomParams;
|
|
595
607
|
if (input.action === "status") {
|
|
596
608
|
return { content: [{ type: "text", text: `Native supervisor channel active. Pending replies: ${pending.size}.` }], details: { active: true, pending: pending.size, root: SUPERVISOR_CHANNEL_ROOT } };
|
|
@@ -601,7 +613,9 @@ function buildParentSupervisorTool(pending: Map<string, PendingSupervisorRequest
|
|
|
601
613
|
}
|
|
602
614
|
if (input.action === "reply") {
|
|
603
615
|
const request = resolvePendingRequest(pending, input);
|
|
604
|
-
writeReply(request, input.message ?? "");
|
|
616
|
+
const reply = writeReply(request, input.message ?? "");
|
|
617
|
+
appendSupervisorReplyEntry(pi, request, reply);
|
|
618
|
+
onLifecycle(request, "resolved");
|
|
605
619
|
pending.delete(request.id);
|
|
606
620
|
clearForegroundSupervisorAttention(request, pending, state);
|
|
607
621
|
return { content: [{ type: "text", text: `Replied to supervisor request ${request.id}.` }], details: { replyTo: request.id, runId: request.runId, agent: request.agent } };
|
|
@@ -614,10 +628,66 @@ function buildParentSupervisorTool(pending: Map<string, PendingSupervisorRequest
|
|
|
614
628
|
};
|
|
615
629
|
}
|
|
616
630
|
|
|
617
|
-
export function createNativeSupervisorChannel(pi: ExtensionAPI, state: SubagentState, deps: NativeSupervisorChannelDeps = {}): {
|
|
631
|
+
export function createNativeSupervisorChannel(pi: ExtensionAPI, state: SubagentState, deps: NativeSupervisorChannelDeps = {}): {
|
|
632
|
+
start: () => void;
|
|
633
|
+
activateTransport: () => void;
|
|
634
|
+
dispose: () => void;
|
|
635
|
+
pending: Map<string, PendingSupervisorRequest>;
|
|
636
|
+
getSupervisorRequestState: (event: ControlEvent) => SupervisorRequestState;
|
|
637
|
+
} {
|
|
618
638
|
const watch = deps.watch ?? fs.watch;
|
|
619
639
|
const timers = deps.timers ?? globalThis;
|
|
620
640
|
const pending = new Map<string, PendingSupervisorRequest>();
|
|
641
|
+
const requestCorrelations = new Map<string, SupervisorRequestCorrelation>();
|
|
642
|
+
const correlationKey = (request: { runId: string; agent: string; childIndex: number; toolCallId?: string }): string | undefined => {
|
|
643
|
+
if (!request.toolCallId) return undefined;
|
|
644
|
+
return JSON.stringify([request.runId, request.agent, request.childIndex, request.toolCallId]);
|
|
645
|
+
};
|
|
646
|
+
const pruneRequestCorrelations = (now = Date.now()): void => {
|
|
647
|
+
for (const [key, correlation] of requestCorrelations) {
|
|
648
|
+
if (correlation.state === "resolved" && now - correlation.updatedAt > SUPERVISOR_REQUEST_CORRELATION_RETENTION_MS) requestCorrelations.delete(key);
|
|
649
|
+
}
|
|
650
|
+
while (requestCorrelations.size > MAX_SUPERVISOR_REQUEST_CORRELATIONS) {
|
|
651
|
+
const oldest = requestCorrelations.keys().next().value;
|
|
652
|
+
if (oldest === undefined) break;
|
|
653
|
+
requestCorrelations.delete(oldest);
|
|
654
|
+
}
|
|
655
|
+
};
|
|
656
|
+
const rememberPendingRequest = (request: PendingSupervisorRequest): void => {
|
|
657
|
+
const key = correlationKey(request);
|
|
658
|
+
if (!key || !request.expectsReply) return;
|
|
659
|
+
requestCorrelations.set(key, { request, state: "pending", updatedAt: Date.now() });
|
|
660
|
+
pruneRequestCorrelations();
|
|
661
|
+
};
|
|
662
|
+
const rememberResolvedRequest = (request: PendingSupervisorRequest): void => {
|
|
663
|
+
const key = correlationKey(request);
|
|
664
|
+
if (!key || !request.expectsReply) return;
|
|
665
|
+
requestCorrelations.set(key, { request, state: "resolved", updatedAt: Date.now() });
|
|
666
|
+
pruneRequestCorrelations();
|
|
667
|
+
};
|
|
668
|
+
const observeRequestLifecycle: SupervisorRequestLifecycleObserver = (request, lifecycle) => {
|
|
669
|
+
if (lifecycle !== "wrong-session") rememberResolvedRequest(request);
|
|
670
|
+
};
|
|
671
|
+
const getSupervisorRequestState = (event: ControlEvent): SupervisorRequestState => {
|
|
672
|
+
if (event.currentTool === "intercom" || event.index === undefined) return "unknown";
|
|
673
|
+
const toolCallId = typeof event.toolCallId === "string" && event.toolCallId.length > 0 ? event.toolCallId : undefined;
|
|
674
|
+
if (!toolCallId) return "unknown";
|
|
675
|
+
pruneRequestCorrelations();
|
|
676
|
+
const key = correlationKey({ runId: event.runId, agent: event.agent, childIndex: event.index, toolCallId });
|
|
677
|
+
if (!key) return "unknown";
|
|
678
|
+
const correlation = requestCorrelations.get(key);
|
|
679
|
+
if (!correlation) return "unknown";
|
|
680
|
+
if (correlation.state === "resolved") return "resolved";
|
|
681
|
+
const now = Date.now();
|
|
682
|
+
if (!fs.existsSync(correlation.request.requestFile)
|
|
683
|
+
|| fs.existsSync(replyPath(correlation.request.channelDir, correlation.request.id))
|
|
684
|
+
|| now > requestExpiresAt(correlation.request, now)
|
|
685
|
+
|| requestRunInactive(correlation.request, state)) {
|
|
686
|
+
rememberResolvedRequest(correlation.request);
|
|
687
|
+
return "resolved";
|
|
688
|
+
}
|
|
689
|
+
return "pending";
|
|
690
|
+
};
|
|
621
691
|
const seenFiles = new Set<string>();
|
|
622
692
|
const requestWatchers = new Map<string, fs.FSWatcher>();
|
|
623
693
|
let rootWatcher: fs.FSWatcher | undefined;
|
|
@@ -635,7 +705,7 @@ export function createNativeSupervisorChannel(pi: ExtensionAPI, state: SubagentS
|
|
|
635
705
|
};
|
|
636
706
|
|
|
637
707
|
const registerParentTools = (): void => {
|
|
638
|
-
if (!hasTool(pi, NATIVE_SUPERVISOR_TOOL_NAME)) pi.registerTool(buildParentSupervisorTool(pending, state));
|
|
708
|
+
if (!hasTool(pi, NATIVE_SUPERVISOR_TOOL_NAME)) pi.registerTool(buildParentSupervisorTool(pi, pending, state, observeRequestLifecycle));
|
|
639
709
|
};
|
|
640
710
|
|
|
641
711
|
const cleanupStaleChannelsIfDue = (): void => {
|
|
@@ -653,7 +723,7 @@ export function createNativeSupervisorChannel(pi: ExtensionAPI, state: SubagentS
|
|
|
653
723
|
cleanupStaleChannelsIfDue();
|
|
654
724
|
const ctx = state.lastUiContext;
|
|
655
725
|
if (!ctx) return;
|
|
656
|
-
refreshPendingRequests(pending, state, ctx);
|
|
726
|
+
refreshPendingRequests(pending, state, ctx, observeRequestLifecycle);
|
|
657
727
|
const now = Date.now();
|
|
658
728
|
for (const { channelDir, file } of listRequestFiles()) {
|
|
659
729
|
if (seenFiles.has(file)) continue;
|
|
@@ -662,11 +732,13 @@ export function createNativeSupervisorChannel(pi: ExtensionAPI, state: SubagentS
|
|
|
662
732
|
const lifecycle = requestLifecycle(request, state, undefined, now);
|
|
663
733
|
if (lifecycle !== "pending") {
|
|
664
734
|
seenFiles.add(file);
|
|
735
|
+
observeRequestLifecycle(request, lifecycle);
|
|
665
736
|
cleanupRequestLifecycle(request, lifecycle);
|
|
666
737
|
continue;
|
|
667
738
|
}
|
|
668
739
|
seenFiles.add(file);
|
|
669
740
|
if (request.expectsReply) {
|
|
741
|
+
rememberPendingRequest(request);
|
|
670
742
|
pending.set(request.id, request);
|
|
671
743
|
markForegroundSupervisorAttention(request, state);
|
|
672
744
|
}
|
|
@@ -674,16 +746,20 @@ export function createNativeSupervisorChannel(pi: ExtensionAPI, state: SubagentS
|
|
|
674
746
|
removeRequestFile(request.requestFile);
|
|
675
747
|
}
|
|
676
748
|
pi.sendMessage({
|
|
677
|
-
customType:
|
|
749
|
+
customType: SUPERVISOR_REQUEST_MESSAGE_TYPE,
|
|
678
750
|
content: requestVisibleText(request),
|
|
679
751
|
display: true,
|
|
680
752
|
details: {
|
|
681
753
|
id: request.id,
|
|
754
|
+
requestId: request.id,
|
|
682
755
|
reason: request.reason,
|
|
683
756
|
expectsReply: request.expectsReply,
|
|
684
757
|
runId: request.runId,
|
|
685
758
|
agent: request.agent,
|
|
686
759
|
childIndex: request.childIndex,
|
|
760
|
+
...(request.childTarget ? { childTarget: request.childTarget } : {}),
|
|
761
|
+
...(request.interview !== undefined ? { interview: request.interview } : {}),
|
|
762
|
+
...(request.expectsReply ? { replyHint: supervisorReplyHint(request.id) } : {}),
|
|
687
763
|
},
|
|
688
764
|
}, { triggerTurn: true });
|
|
689
765
|
if (request.expectsReply) {
|
|
@@ -805,8 +881,10 @@ export function createNativeSupervisorChannel(pi: ExtensionAPI, state: SubagentS
|
|
|
805
881
|
if (deferredWatcherRefresh) timers.clearImmediate(deferredWatcherRefresh);
|
|
806
882
|
deferredWatcherRefresh = undefined;
|
|
807
883
|
pending.clear();
|
|
884
|
+
requestCorrelations.clear();
|
|
808
885
|
seenFiles.clear();
|
|
809
886
|
},
|
|
810
887
|
pending,
|
|
888
|
+
getSupervisorRequestState,
|
|
811
889
|
};
|
|
812
890
|
}
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import type { EntryRenderOptions, MessageRenderOptions, Theme } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { truncateToWidth, visibleWidth, wrapTextWithAnsi, type Component } from "@earendil-works/pi-tui";
|
|
3
|
+
import { safeTerminalText } from "../shared/display-text.ts";
|
|
4
|
+
|
|
5
|
+
export const SUPERVISOR_REQUEST_MESSAGE_TYPE = "subagent_supervisor_request";
|
|
6
|
+
export const SUPERVISOR_REPLY_ENTRY_TYPE = "subagent_supervisor_reply";
|
|
7
|
+
|
|
8
|
+
export type SupervisorReason = "need_decision" | "interview_request" | "progress_update";
|
|
9
|
+
|
|
10
|
+
export interface SupervisorRequestMessageDetails {
|
|
11
|
+
id?: string;
|
|
12
|
+
requestId?: string;
|
|
13
|
+
reason?: SupervisorReason;
|
|
14
|
+
expectsReply?: boolean;
|
|
15
|
+
runId?: string;
|
|
16
|
+
agent?: string;
|
|
17
|
+
childIndex?: number;
|
|
18
|
+
childTarget?: string;
|
|
19
|
+
interview?: unknown;
|
|
20
|
+
replyHint?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface SupervisorReplyEntryData {
|
|
24
|
+
requestId: string;
|
|
25
|
+
reason?: SupervisorReason;
|
|
26
|
+
runId: string;
|
|
27
|
+
agent: string;
|
|
28
|
+
childIndex: number;
|
|
29
|
+
childTarget?: string;
|
|
30
|
+
message: string;
|
|
31
|
+
createdAt: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface SupervisorMessageLike {
|
|
35
|
+
content: unknown;
|
|
36
|
+
details?: unknown;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface SupervisorEntryLike {
|
|
40
|
+
data?: unknown;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const MAX_FIELD_CHARS = 512;
|
|
44
|
+
const MAX_BODY_CHARS = 8_000;
|
|
45
|
+
const MAX_INTERVIEW_CHARS = 4_000;
|
|
46
|
+
const MAX_RENDER_LINES = 36;
|
|
47
|
+
const TRUNCATION_MARKER = "[truncated]";
|
|
48
|
+
|
|
49
|
+
export function supervisorReplyHint(requestId: string): string {
|
|
50
|
+
return `subagent_supervisor({ action: "reply", replyTo: "${requestId}", message: "..." })`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function boundedText(value: string, maxChars: number): string {
|
|
54
|
+
const safe = safeTerminalText(value);
|
|
55
|
+
if (safe.length <= maxChars) return safe;
|
|
56
|
+
const prefixLength = Math.max(0, maxChars - TRUNCATION_MARKER.length - 1);
|
|
57
|
+
let prefix = "";
|
|
58
|
+
for (const character of safe) {
|
|
59
|
+
if (prefix.length + character.length > prefixLength) break;
|
|
60
|
+
prefix += character;
|
|
61
|
+
}
|
|
62
|
+
return `${prefix} ${TRUNCATION_MARKER}`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function displayText(value: string, maxChars: number, expanded: boolean): string {
|
|
66
|
+
return expanded ? safeTerminalText(value) : boundedText(value, maxChars);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function boundedField(value: unknown, fallback = "unknown"): string {
|
|
70
|
+
return boundedText(typeof value === "string" ? value : value === undefined ? fallback : String(value), MAX_FIELD_CHARS);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function contentText(content: unknown): string {
|
|
74
|
+
if (typeof content === "string") return content;
|
|
75
|
+
if (!Array.isArray(content)) return "";
|
|
76
|
+
return content
|
|
77
|
+
.map((part) => {
|
|
78
|
+
if (!part || typeof part !== "object") return "";
|
|
79
|
+
const text = (part as { text?: unknown }).text;
|
|
80
|
+
return typeof text === "string" ? text : "";
|
|
81
|
+
})
|
|
82
|
+
.filter(Boolean)
|
|
83
|
+
.join("\n");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function interviewText(interview: unknown, expanded: boolean): string {
|
|
87
|
+
let serialized: string;
|
|
88
|
+
try {
|
|
89
|
+
serialized = JSON.stringify(interview, null, 2) ?? String(interview);
|
|
90
|
+
} catch {
|
|
91
|
+
serialized = "[unavailable]";
|
|
92
|
+
}
|
|
93
|
+
return displayText(serialized, MAX_INTERVIEW_CHARS, expanded);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
97
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function isSupervisorReason(value: unknown): value is SupervisorReason {
|
|
101
|
+
return value === "need_decision" || value === "interview_request" || value === "progress_update";
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function optionalString(value: unknown): boolean {
|
|
105
|
+
return value === undefined || typeof value === "string";
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function requestDetails(value: unknown): SupervisorRequestMessageDetails | undefined {
|
|
109
|
+
if (!isRecord(value)) return undefined;
|
|
110
|
+
if (!optionalString(value.id) || !optionalString(value.requestId) || !optionalString(value.replyHint) || !optionalString(value.runId) || !optionalString(value.agent) || !optionalString(value.childTarget)) return undefined;
|
|
111
|
+
if (value.reason !== undefined && !isSupervisorReason(value.reason)) return undefined;
|
|
112
|
+
if (value.expectsReply !== undefined && typeof value.expectsReply !== "boolean") return undefined;
|
|
113
|
+
if (value.childIndex !== undefined && (typeof value.childIndex !== "number" || !Number.isFinite(value.childIndex))) return undefined;
|
|
114
|
+
return value;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function replyData(value: unknown): SupervisorReplyEntryData | undefined {
|
|
118
|
+
if (!isRecord(value)) return undefined;
|
|
119
|
+
if (typeof value.requestId !== "string" || typeof value.runId !== "string" || typeof value.agent !== "string" || typeof value.message !== "string") return undefined;
|
|
120
|
+
if (value.reason !== undefined && !isSupervisorReason(value.reason)) return undefined;
|
|
121
|
+
if (value.childTarget !== undefined && typeof value.childTarget !== "string") return undefined;
|
|
122
|
+
if (typeof value.childIndex !== "number" || !Number.isFinite(value.childIndex) || typeof value.createdAt !== "number" || !Number.isFinite(value.createdAt)) return undefined;
|
|
123
|
+
return {
|
|
124
|
+
requestId: value.requestId,
|
|
125
|
+
...(value.reason === undefined ? {} : { reason: value.reason }),
|
|
126
|
+
runId: value.runId,
|
|
127
|
+
agent: value.agent,
|
|
128
|
+
childIndex: value.childIndex,
|
|
129
|
+
...(value.childTarget === undefined ? {} : { childTarget: value.childTarget }),
|
|
130
|
+
message: value.message,
|
|
131
|
+
createdAt: value.createdAt,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function withRequestId(details: SupervisorRequestMessageDetails): string {
|
|
136
|
+
return boundedField(details.requestId ?? details.id);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function requestHeading(reason: SupervisorReason | undefined): string {
|
|
140
|
+
if (reason === "interview_request") return "⚠ Supervisor interview request";
|
|
141
|
+
if (reason === "progress_update") return "ℹ Supervisor progress update";
|
|
142
|
+
return "⚠ Supervisor decision request";
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function requestLines(message: SupervisorMessageLike, details: SupervisorRequestMessageDetails, expanded: boolean): string[] {
|
|
146
|
+
const requestId = withRequestId(details);
|
|
147
|
+
const lines = [
|
|
148
|
+
`Reason: ${boundedField(details.reason)}`,
|
|
149
|
+
`Run: ${boundedField(details.runId)}`,
|
|
150
|
+
`Agent: ${boundedField(details.agent)}`,
|
|
151
|
+
`Child index: ${boundedField(details.childIndex)}`,
|
|
152
|
+
];
|
|
153
|
+
if (details.childTarget) lines.push(`Child target: ${boundedField(details.childTarget)}`);
|
|
154
|
+
lines.push(`Request ID: ${requestId}`);
|
|
155
|
+
if (details.expectsReply) lines.push(`Reply with: ${displayText(details.replyHint ?? supervisorReplyHint(requestId), MAX_BODY_CHARS, expanded)}`);
|
|
156
|
+
lines.push("", "Request:", displayText(contentText(message.content) || "(no request body)", MAX_BODY_CHARS, expanded));
|
|
157
|
+
if (details.interview !== undefined) lines.push("", "Interview shape:", interviewText(details.interview, expanded));
|
|
158
|
+
return lines;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function replyLines(data: SupervisorReplyEntryData, expanded: boolean): string[] {
|
|
162
|
+
const requestId = boundedField(data.requestId);
|
|
163
|
+
const lines = [
|
|
164
|
+
...(data.reason ? [`Reason: ${boundedField(data.reason)}`] : []),
|
|
165
|
+
`Run: ${boundedField(data.runId)}`,
|
|
166
|
+
`Agent: ${boundedField(data.agent)}`,
|
|
167
|
+
`Child index: ${boundedField(data.childIndex)}`,
|
|
168
|
+
];
|
|
169
|
+
if (data.childTarget) lines.push(`Child target: ${boundedField(data.childTarget)}`);
|
|
170
|
+
lines.push(`Reply to: ${requestId}`, "", "Reply:", displayText(data.message || "(empty reply)", MAX_BODY_CHARS, expanded));
|
|
171
|
+
return lines;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function renderCard(lines: string[], heading: string, theme: Theme, width: number, expanded: boolean): string[] {
|
|
175
|
+
const safeWidth = Math.max(0, Math.floor(width));
|
|
176
|
+
if (safeWidth < 3) return [truncateToWidth(heading, safeWidth, "")];
|
|
177
|
+
const bodyWidth = safeWidth - 2;
|
|
178
|
+
const headerText = truncateToWidth(` ${heading} `, bodyWidth, "");
|
|
179
|
+
const headerPadding = Math.max(0, bodyWidth - visibleWidth(headerText));
|
|
180
|
+
const border = (text: string) => theme.fg("accent", text);
|
|
181
|
+
const rendered = [border(`╭${headerText}${"─".repeat(headerPadding)}╮`)];
|
|
182
|
+
let hidden = false;
|
|
183
|
+
let interiorLines = 0;
|
|
184
|
+
for (const line of lines) {
|
|
185
|
+
const wrapped = wrapTextWithAnsi(line, Math.max(1, bodyWidth));
|
|
186
|
+
for (const wrappedLine of wrapped.length > 0 ? wrapped : [""]) {
|
|
187
|
+
if (!expanded && interiorLines >= MAX_RENDER_LINES) {
|
|
188
|
+
hidden = true;
|
|
189
|
+
break;
|
|
190
|
+
}
|
|
191
|
+
const text = truncateToWidth(wrappedLine, bodyWidth, "");
|
|
192
|
+
rendered.push(border(`│${text}${" ".repeat(Math.max(0, bodyWidth - visibleWidth(text)))}│`));
|
|
193
|
+
interiorLines++;
|
|
194
|
+
}
|
|
195
|
+
if (hidden) break;
|
|
196
|
+
}
|
|
197
|
+
if (hidden) {
|
|
198
|
+
const text = truncateToWidth(TRUNCATION_MARKER, bodyWidth, "");
|
|
199
|
+
rendered.push(border(`│${text}${" ".repeat(Math.max(0, bodyWidth - visibleWidth(text)))}│`));
|
|
200
|
+
}
|
|
201
|
+
rendered.push(border(`╰${"─".repeat(bodyWidth)}╯`));
|
|
202
|
+
return rendered;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
class SupervisorCardComponent implements Component {
|
|
206
|
+
private readonly expanded: boolean;
|
|
207
|
+
private readonly heading: string;
|
|
208
|
+
private readonly lines: string[];
|
|
209
|
+
private readonly theme: Theme;
|
|
210
|
+
|
|
211
|
+
constructor(heading: string, lines: string[], theme: Theme, expanded: boolean) {
|
|
212
|
+
this.expanded = expanded;
|
|
213
|
+
this.heading = heading;
|
|
214
|
+
this.lines = lines;
|
|
215
|
+
this.theme = theme;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
invalidate(): void {}
|
|
219
|
+
|
|
220
|
+
render(width: number): string[] {
|
|
221
|
+
return renderCard(this.lines, this.heading, this.theme, width, this.expanded);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export function renderSupervisorRequest(
|
|
226
|
+
message: SupervisorMessageLike,
|
|
227
|
+
options: MessageRenderOptions,
|
|
228
|
+
theme: Theme,
|
|
229
|
+
): Component | undefined {
|
|
230
|
+
const details = requestDetails(message.details);
|
|
231
|
+
if (!details) return undefined;
|
|
232
|
+
return new SupervisorCardComponent(requestHeading(details.reason), requestLines(message, details, options.expanded), theme, options.expanded);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export function renderSupervisorReply(
|
|
236
|
+
entry: SupervisorEntryLike,
|
|
237
|
+
options: EntryRenderOptions,
|
|
238
|
+
theme: Theme,
|
|
239
|
+
): Component | undefined {
|
|
240
|
+
const data = replyData(entry.data);
|
|
241
|
+
if (!data) return undefined;
|
|
242
|
+
return new SupervisorCardComponent("↩ Supervisor reply to child", replyLines(data, options.expanded), theme, options.expanded);
|
|
243
|
+
}
|