@aeondave/pi-persona 1.15.2 → 1.16.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/AGENTS.md +30 -6
- package/CHANGELOG.md +82 -0
- package/README.md +27 -8
- package/SECURITY.md +42 -0
- package/docs/ARCHITECTURE.md +52 -7
- package/docs/EXPERIENCE.md +21 -5
- package/docs/README.md +2 -0
- package/docs/REFERENCE.md +84 -14
- package/docs/STRATEGIES.md +58 -31
- package/docs/TELEMETRY.md +8 -0
- package/package.json +11 -9
- package/src/bus/broker/paths.ts +3 -2
- package/src/core/brief.ts +1 -1
- package/src/core/nudge.ts +3 -4
- package/src/core/pi-compat.ts +3 -3
- package/src/engine/adapter.ts +11 -6
- package/src/engine/async.ts +12 -6
- package/src/engine/inproc.ts +14 -6
- package/src/extension/engine.ts +25 -4
- package/src/extension/hooks.ts +68 -21
- package/src/extension.ts +209 -49
- package/src/orchestration/model-retry.ts +32 -61
- package/src/orchestration/params.ts +27 -1
- package/src/orchestration/roster.ts +80 -3
- package/src/orchestration/sdk.ts +150 -36
- package/src/orchestration/strategies/compete.ts +14 -8
- package/src/orchestration/strategies/council-rounds.ts +8 -1
- package/src/orchestration/strategies/critic-loop.ts +25 -11
- package/src/orchestration/strategies/judge.ts +18 -9
- package/src/orchestration/strategies/magi.ts +42 -93
- package/src/orchestration/strategies/map.ts +41 -19
- package/src/orchestration/strategies/pipeline.ts +2 -2
- package/src/orchestration/strategies/synthesize.ts +18 -11
- package/src/orchestration/types.ts +15 -0
- package/src/persona/model-participants.ts +240 -0
- package/src/persona/orchestrate.ts +44 -2
- package/src/persona/persona.ts +86 -14
- package/src/telemetry/contract.ts +1 -1
- package/src/telemetry/producer.ts +241 -8
- package/src/tools/council.ts +36 -8
- package/src/tools/delegate-tool.ts +162 -24
- package/src/tools/intercom-tool.ts +119 -32
- package/src/ui/agent-overlay.ts +15 -6
- package/src/ui/agent-tree.ts +39 -2
|
@@ -3,10 +3,10 @@ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-a
|
|
|
3
3
|
import type { AgentConfig } from "../agents/agent.ts";
|
|
4
4
|
import { Type } from "typebox";
|
|
5
5
|
import { fenceUntrusted } from "../core/fence.ts";
|
|
6
|
-
import { Text } from "@earendil-works/pi-tui";
|
|
6
|
+
import { Text, visibleWidth } from "@earendil-works/pi-tui";
|
|
7
7
|
import { Container, Spacer } from "@earendil-works/pi-tui";
|
|
8
8
|
import { configuredModels } from "../extension/engine.ts";
|
|
9
|
-
import { compactInlineText, sanitizeTerminalText } from "../ui/presentation.ts";
|
|
9
|
+
import { compactInlineText, MAX_COLLAPSED_CARD_COLUMNS, sanitizeTerminalText } from "../ui/presentation.ts";
|
|
10
10
|
import { compactVisibleText } from "../ui/presentation.ts";
|
|
11
11
|
import { resolveModelRef } from "../core/models.ts";
|
|
12
12
|
import { inventedLegNameHint } from "../core/naming.ts";
|
|
@@ -24,12 +24,125 @@ import type { AgentResult } from "../orchestration/types.ts";
|
|
|
24
24
|
import { agentNodeStatusForDelegate, sanitizeLabel } from "../extension/shared.ts";
|
|
25
25
|
import type { PersonaController } from "../persona/controller.ts";
|
|
26
26
|
import type { AgentTree } from "../ui/agent-tree.ts";
|
|
27
|
-
import type
|
|
27
|
+
import { runDisplayName, type AsyncRunTracker } from "../engine/async.ts";
|
|
28
28
|
import { emptyUsage, type ProgressSnapshot, type ToolEvent } from "../engine/stream.ts";
|
|
29
29
|
import { progressPatch } from "../ui/agent-tree.ts";
|
|
30
30
|
import type { AddNodeInput } from "../ui/agent-tree.ts";
|
|
31
31
|
import type { RunLimits } from "../core/capabilities.ts";
|
|
32
32
|
|
|
33
|
+
interface DelegateLaunchSnapshot {
|
|
34
|
+
id: string;
|
|
35
|
+
label: string;
|
|
36
|
+
agent: string;
|
|
37
|
+
model?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface DelegateLaunchRow {
|
|
41
|
+
id: string;
|
|
42
|
+
fullName: string;
|
|
43
|
+
displayName: string;
|
|
44
|
+
clipped: boolean;
|
|
45
|
+
hasAlias: boolean;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const COLLAPSED_LAUNCH_ROWS = 3;
|
|
49
|
+
|
|
50
|
+
/** Prefer session-persisted identity, then recover old launch cards from the live tracker. */
|
|
51
|
+
function launchRows(
|
|
52
|
+
details: { runId?: unknown; runIds?: unknown; runs?: unknown },
|
|
53
|
+
tracker: AsyncRunTracker,
|
|
54
|
+
allowTracker: boolean,
|
|
55
|
+
): DelegateLaunchRow[] {
|
|
56
|
+
const persisted = new Map<string, DelegateLaunchSnapshot>();
|
|
57
|
+
if (Array.isArray(details.runs)) {
|
|
58
|
+
for (const item of details.runs) {
|
|
59
|
+
if (!item || typeof item !== "object") continue;
|
|
60
|
+
const run = item as Record<string, unknown>;
|
|
61
|
+
if (typeof run.id !== "string" || typeof run.label !== "string" || typeof run.agent !== "string") continue;
|
|
62
|
+
persisted.set(run.id, {
|
|
63
|
+
id: run.id,
|
|
64
|
+
label: run.label,
|
|
65
|
+
agent: run.agent,
|
|
66
|
+
...(typeof run.model === "string" ? { model: run.model } : {}),
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
const storedIds = Array.isArray(details.runIds)
|
|
71
|
+
? details.runIds.filter((id): id is string => typeof id === "string")
|
|
72
|
+
: typeof details.runId === "string" ? [details.runId] : [];
|
|
73
|
+
const ids = [...storedIds];
|
|
74
|
+
const seenIds = new Set(ids);
|
|
75
|
+
for (const id of persisted.keys()) {
|
|
76
|
+
if (seenIds.has(id)) continue;
|
|
77
|
+
seenIds.add(id);
|
|
78
|
+
ids.push(id);
|
|
79
|
+
}
|
|
80
|
+
const entries = ids.map((id): { run: DelegateLaunchSnapshot; hasAlias: boolean } => {
|
|
81
|
+
const saved = persisted.get(id);
|
|
82
|
+
if (saved) return { run: saved, hasAlias: true };
|
|
83
|
+
const tracked = allowTracker ? tracker.peek(id) : undefined;
|
|
84
|
+
if (tracked) {
|
|
85
|
+
return {
|
|
86
|
+
run: {
|
|
87
|
+
id,
|
|
88
|
+
label: tracked.label ?? tracked.agent,
|
|
89
|
+
agent: tracked.agent,
|
|
90
|
+
...(tracked.model ? { model: tracked.model } : {}),
|
|
91
|
+
},
|
|
92
|
+
hasAlias: true,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
// Older history may outlive both its launch snapshot and tracker entry. Retain the
|
|
96
|
+
// routing id as an explicitly diagnostic fallback so the batch count stays complete.
|
|
97
|
+
return { run: { id, label: id, agent: id }, hasAlias: false };
|
|
98
|
+
});
|
|
99
|
+
const named = entries.map(({ run, hasAlias }) => ({
|
|
100
|
+
id: compactInlineText(run.id, { maxChars: 64 }) || "run",
|
|
101
|
+
fullName: runDisplayName(run),
|
|
102
|
+
hasAlias,
|
|
103
|
+
}));
|
|
104
|
+
const counts = new Map<string, number>();
|
|
105
|
+
for (const row of named) counts.set(row.fullName, (counts.get(row.fullName) ?? 0) + 1);
|
|
106
|
+
return named.map((row) => {
|
|
107
|
+
const displayName = counts.get(row.fullName)! > 1
|
|
108
|
+
? `${compactInlineText(row.fullName, { maxChars: 56 })} [${compactInlineText(row.id, { maxChars: 16 })}]`
|
|
109
|
+
: row.fullName;
|
|
110
|
+
return {
|
|
111
|
+
...row,
|
|
112
|
+
displayName,
|
|
113
|
+
clipped: compactInlineText(displayName, { maxChars: 80 }) !== displayName,
|
|
114
|
+
};
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function collapsedLaunchSummary(rows: readonly DelegateLaunchRow[], dropped: number | undefined): string {
|
|
119
|
+
const prefix = "delegate launched ";
|
|
120
|
+
const dropNote = dropped && dropped > 0 ? ` · ${dropped} task${dropped === 1 ? "" : "s"} dropped` : "";
|
|
121
|
+
const maxVisible = Math.min(rows.length, COLLAPSED_LAUNCH_ROWS);
|
|
122
|
+
for (let count = maxVisible; count >= 1; count--) {
|
|
123
|
+
const names = rows.slice(0, count).map((row) => row.displayName);
|
|
124
|
+
const omitted = rows.length - names.length;
|
|
125
|
+
const suffix = omitted > 0 ? `, +${omitted} more` : "";
|
|
126
|
+
const body = `${prefix}${names.join(", ")}${suffix}${dropNote}`;
|
|
127
|
+
const needsHint = omitted > 0 || rows.slice(0, count).some((row) => row.clipped) || body.length > MAX_COLLAPSED_CARD_COLUMNS || visibleWidth(body) > MAX_COLLAPSED_CARD_COLUMNS;
|
|
128
|
+
const candidate = `${body}${needsHint ? ` · ${expandDetailHint()}` : ""}`;
|
|
129
|
+
if (candidate.length <= MAX_COLLAPSED_CARD_COLUMNS && visibleWidth(candidate) <= MAX_COLLAPSED_CARD_COLUMNS) return candidate;
|
|
130
|
+
}
|
|
131
|
+
const omitted = rows.length - 1;
|
|
132
|
+
const suffix = omitted > 0 ? `, +${omitted} more` : "";
|
|
133
|
+
const hint = ` · ${expandDetailHint()}`;
|
|
134
|
+
const fixed = `${prefix}${suffix}${dropNote}${hint}`;
|
|
135
|
+
const aliasLimit = Math.max(16, Math.min(MAX_COLLAPSED_CARD_COLUMNS - visibleWidth(fixed), MAX_COLLAPSED_CARD_COLUMNS - fixed.length));
|
|
136
|
+
const first = compactInlineText(rows[0]!.displayName, { maxChars: aliasLimit });
|
|
137
|
+
return compactInlineText(`${prefix}${first}${suffix}${dropNote}${hint}`, { maxChars: MAX_COLLAPSED_CARD_COLUMNS });
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function expandedLaunchDetails(text: string, rows: readonly DelegateLaunchRow[]): string {
|
|
141
|
+
if (rows.length === 0 || !rows.some((row) => row.hasAlias)) return text;
|
|
142
|
+
const diagnostics = rows.map((row) => `- ${row.fullName} [${row.id}]`).join("\n");
|
|
143
|
+
return `${text}\n\nLaunched workers:\n${diagnostics}`;
|
|
144
|
+
}
|
|
145
|
+
|
|
33
146
|
export interface DelegateToolDeps {
|
|
34
147
|
get lastCtx(): ExtensionContext | undefined;
|
|
35
148
|
set lastCtx(value: ExtensionContext | undefined);
|
|
@@ -290,9 +403,10 @@ export function registerDelegateTool(pi: ExtensionAPI, d: DelegateToolDeps): voi
|
|
|
290
403
|
"Pass agent and task for one worker, or a tasks array for parallel workers. Follow the active persona's delegation policy for required fields.",
|
|
291
404
|
"When requireBrief is active, EVERY worker needs brief: { objective, scopeRoe, position, constraints, requiredArtifacts, stopConditions } with nonempty values, including read-only scouts. In parallel mode put it in each tasks[].brief; task prose and a top-level brief do not substitute. Read the current per-turn delegation brief for a complete example.",
|
|
292
405
|
"A persona may also require a structured output contract or disjoint writeSet ownership for parallel writers. Incomplete calls are rejected before any worker starts; fill the missing fields across the batch before retrying.",
|
|
293
|
-
"In interactive sessions it runs in the BACKGROUND by default: you get run ids at once
|
|
294
|
-
"
|
|
295
|
-
"
|
|
406
|
+
"In interactive sessions it runs in the BACKGROUND by default: you get run ids at once and stay free.",
|
|
407
|
+
"Work independently while children run; when only children remain, end your turn and let their automatic completion follow-ups wake you.",
|
|
408
|
+
"Do not monitor, poll, or automatically wait for all children. Use `intercom wait` only when a result is needed before your next step; `sync: true` intentionally blocks instead; headless runs default to sync.",
|
|
409
|
+
"Steering received during an active tool may be acted on after that tool returns; do not assume arbitrary tool work is interrupted immediately.",
|
|
296
410
|
"No fitting agent? Shape one on the fly: `operator` + `role` (extra system prompt) + `skills`.",
|
|
297
411
|
"Assign each worker's name before launch so it sees the same identity from its first turn; omitted names keep the generic fallback.",
|
|
298
412
|
"A `model` may be a loose name ('sonnet') — it resolves to YOUR provider's id; ambiguous names return",
|
|
@@ -449,13 +563,17 @@ export function registerDelegateTool(pi: ExtensionAPI, d: DelegateToolDeps): voi
|
|
|
449
563
|
const batchSlots = effectiveConcurrency < tasks.length ? new Semaphore(effectiveConcurrency) : undefined;
|
|
450
564
|
const nameOffset = asyncNameSequence;
|
|
451
565
|
asyncNameSequence += tasks.length;
|
|
452
|
-
const
|
|
566
|
+
const runs = tasks.map((t, i): DelegateLaunchSnapshot => {
|
|
453
567
|
// Routed through specOf() (not a hand-rolled field list) so this, the interactive
|
|
454
568
|
// DEFAULT delegate path, never drifts from the sync path's mapping — NP2's per-leg
|
|
455
569
|
// `timeoutMs` (and any future knob) lands here for free instead of needing a second copy.
|
|
456
570
|
const spec = specOf(t, nameOffset + i);
|
|
457
|
-
|
|
571
|
+
const label = nameFor(t, nameOffset + i);
|
|
572
|
+
const id = launchAsyncRun(t.agent, t.task, spec, label, batchSlots);
|
|
573
|
+
const model = shortModel(spec.model);
|
|
574
|
+
return { id, label, agent: t.agent, ...(model ? { model } : {}) };
|
|
458
575
|
});
|
|
576
|
+
const ids = runs.map((run) => run.id);
|
|
459
577
|
const droppedNote = dropped > 0 ? ` ${dropped} task(s) beyond the max-children limit (${d.RUN_LIMITS.maxChildren}) were dropped.` : "";
|
|
460
578
|
return {
|
|
461
579
|
content: [
|
|
@@ -464,7 +582,7 @@ export function registerDelegateTool(pi: ExtensionAPI, d: DelegateToolDeps): voi
|
|
|
464
582
|
text: `Launched ${ids.length} async runs in the background (${ids.join(", ")}) — keep working; each notifies on completion. /peek to watch.${droppedNote}`,
|
|
465
583
|
},
|
|
466
584
|
],
|
|
467
|
-
details: { runIds: ids },
|
|
585
|
+
details: { runIds: ids, runs, ...(dropped > 0 ? { dropped } : {}) },
|
|
468
586
|
isError: false,
|
|
469
587
|
};
|
|
470
588
|
}
|
|
@@ -476,7 +594,9 @@ export function registerDelegateTool(pi: ExtensionAPI, d: DelegateToolDeps): voi
|
|
|
476
594
|
// survive exactly as they do in fan-out and sync mode.
|
|
477
595
|
const single = { ...params, agent, task };
|
|
478
596
|
const runSpec = specOf(single, nameIndex);
|
|
479
|
-
const
|
|
597
|
+
const label = nameFor(single, nameIndex);
|
|
598
|
+
const id = launchAsyncRun(agent, task, runSpec, label);
|
|
599
|
+
const model = shortModel(runSpec.model);
|
|
480
600
|
return {
|
|
481
601
|
content: [
|
|
482
602
|
{
|
|
@@ -484,7 +604,7 @@ export function registerDelegateTool(pi: ExtensionAPI, d: DelegateToolDeps): voi
|
|
|
484
604
|
text: `Launched async run ${id} (${agent}) — runs in the background; you'll be notified on completion. /peek ${id} to watch.`,
|
|
485
605
|
},
|
|
486
606
|
],
|
|
487
|
-
details: { runId: id },
|
|
607
|
+
details: { runId: id, runs: [{ id, label, agent, ...(model ? { model } : {}) }] },
|
|
488
608
|
isError: false,
|
|
489
609
|
};
|
|
490
610
|
}
|
|
@@ -561,27 +681,25 @@ export function registerDelegateTool(pi: ExtensionAPI, d: DelegateToolDeps): voi
|
|
|
561
681
|
const title = theme.fg("toolTitle", theme.bold("delegate "));
|
|
562
682
|
const coerced = coerceDelegateParams(args);
|
|
563
683
|
const view = coerced.ok ? coerced.params : undefined;
|
|
684
|
+
const mode = wantsAsyncRun(view ?? args, true) ? theme.fg("warning", " async") : theme.fg("dim", " sync");
|
|
564
685
|
if (view?.tasks && view.tasks.length > 0) {
|
|
565
686
|
// Names live in the tree / final card — keep the call line itself minimal.
|
|
566
|
-
return new Text(`${title}${theme.fg("accent", `parallel (${view.tasks.length})`)}`, 0, 0);
|
|
687
|
+
return new Text(`${title}${theme.fg("accent", `parallel (${view.tasks.length})`)}${mode}`, 0, 0);
|
|
567
688
|
}
|
|
568
689
|
if (typeof args.tasks === "string" && args.tasks.trim()) {
|
|
569
|
-
return new Text(`${title}${theme.fg("accent", "parallel")}`, 0, 0);
|
|
690
|
+
return new Text(`${title}${theme.fg("accent", "parallel")}${mode}`, 0, 0);
|
|
570
691
|
}
|
|
571
|
-
const
|
|
572
|
-
const
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
// (defaulted) background run still shows the tag; `sync: true` drops it.
|
|
576
|
-
const asyncTag = wantsAsyncRun(view ?? args, true) ? theme.fg("warning", " async") : "";
|
|
577
|
-
return new Text(`${title}${theme.fg("accent", agent)}${asyncTag}${theme.fg("dim", ` ${preview}`)}`, 0, 0);
|
|
692
|
+
const identity = [view?.name ?? args.name, view?.agent ?? args.agent ?? "?"].filter(Boolean).join(" · ");
|
|
693
|
+
const modelValue = view?.model ?? args.model;
|
|
694
|
+
const model = typeof modelValue === "string" && modelValue.trim() ? theme.fg("dim", ` · ${compactInlineText(shortModel(modelValue), { maxChars: 48 })}`) : "";
|
|
695
|
+
return new Text(`${title}${theme.fg("accent", compactInlineText(identity, { maxChars: 96 }) || "?")}${model}${mode}`, 0, 0);
|
|
578
696
|
},
|
|
579
697
|
|
|
580
|
-
renderResult(result, { expanded }, theme) {
|
|
698
|
+
renderResult(result, { expanded }, theme, context) {
|
|
581
699
|
// Safe: the delegate `execute` above always stores `{ views: DelegateView[] }` (sync,
|
|
582
700
|
// single/parallel) or `{ runId | runIds }` (async) in `details`; the double cast just narrows
|
|
583
701
|
// Pi's opaque `details` type to that known shape for rendering.
|
|
584
|
-
const details = result.details as unknown as { views?: DelegateView[]; runId?: string; runIds?: string[] } | undefined;
|
|
702
|
+
const details = result.details as unknown as { views?: DelegateView[]; runId?: string; runIds?: string[]; runs?: unknown; dropped?: number } | undefined;
|
|
585
703
|
const views = details?.views ?? [];
|
|
586
704
|
if (views.length === 0) {
|
|
587
705
|
const first = result.content[0];
|
|
@@ -590,8 +708,28 @@ export function registerDelegateTool(pi: ExtensionAPI, d: DelegateToolDeps): voi
|
|
|
590
708
|
: details?.runId
|
|
591
709
|
? `async run ${details.runId}`
|
|
592
710
|
: "(no output)";
|
|
593
|
-
const text = first?.type === "text" ? first.text : fallback;
|
|
594
|
-
|
|
711
|
+
const text = sanitizeTerminalText(first?.type === "text" ? first.text : fallback);
|
|
712
|
+
const allowTracker = context === undefined || context.isPartial === true;
|
|
713
|
+
const rows = details ? launchRows(details, d.tracker, allowTracker) : [];
|
|
714
|
+
if (expanded) return new Text(expandedLaunchDetails(text, rows), 0, 0);
|
|
715
|
+
if (details?.runId || details?.runIds?.length) {
|
|
716
|
+
if (rows.length > 0 && !result.isError) {
|
|
717
|
+
return new Text(theme.fg("accent", collapsedLaunchSummary(rows, details.dropped)), 0, 0);
|
|
718
|
+
}
|
|
719
|
+
if (result.isError) {
|
|
720
|
+
const preview = compactVisibleText(text, { maxLines: 4, maxLineChars: 100 });
|
|
721
|
+
return new Text(`${theme.fg("error", theme.bold("failed"))}\n${theme.fg("toolOutput", preview.text)}${preview.truncated ? `\n${theme.fg("dim", expandDetailHint())}` : ""}`, 0, 0);
|
|
722
|
+
}
|
|
723
|
+
const ids = details.runIds?.length ? details.runIds.slice(0, 3) : [details.runId as string];
|
|
724
|
+
const omitted = (details.runIds?.length ?? 0) - ids.length;
|
|
725
|
+
const dropped = details.dropped ? ` · ${details.dropped} task${details.dropped === 1 ? "" : "s"} dropped (max children)` : "";
|
|
726
|
+
return new Text(`${theme.fg("toolTitle", theme.bold("delegate "))}${theme.fg("accent", `launched ${ids.join(", ")}${omitted > 0 ? `, +${omitted} more` : ""}`)}${theme.fg("dim", dropped)}`, 0, 0);
|
|
727
|
+
}
|
|
728
|
+
if (result.isError) {
|
|
729
|
+
const preview = compactVisibleText(text, { maxLines: 4, maxLineChars: 100 });
|
|
730
|
+
return new Text(`${theme.fg("error", theme.bold("delegate failed"))}\n${theme.fg("toolOutput", preview.text)}${preview.truncated ? `\n${theme.fg("dim", expandDetailHint())}` : ""}`, 0, 0);
|
|
731
|
+
}
|
|
732
|
+
return new Text(theme.fg("toolOutput", compactVisibleText(text, { maxLines: 4, maxLineChars: 100 }).text), 0, 0);
|
|
595
733
|
}
|
|
596
734
|
const title = theme.fg("toolTitle", theme.bold("delegate "));
|
|
597
735
|
const running = views.filter((v) => v.running).length;
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
type AsyncRun, AsyncRunTracker, boundCompletionSurface, buildPeekDigest, buildWaitTimeoutNote,
|
|
11
11
|
MAX_ASYNC_STATUS_ROWS,
|
|
12
12
|
dedupeRunsById, getFullRunOutput, IdleCoalescingNotifier,
|
|
13
|
-
renderCompletion, runDurationLabel,
|
|
13
|
+
renderCompletion, runDisplayName, runDurationLabel,
|
|
14
14
|
} from "../engine/async.ts";
|
|
15
15
|
import { fenceUntrusted } from "../core/fence.ts";
|
|
16
16
|
import { sanitizeDisplayLabel } from "../core/display-label.ts";
|
|
@@ -31,6 +31,7 @@ export interface IntercomToolDeps {
|
|
|
31
31
|
bus: InProcessBus;
|
|
32
32
|
SUPERVISOR: string;
|
|
33
33
|
STALL_FLAG_MS: number;
|
|
34
|
+
waitingRunIds?: () => ReadonlySet<string>;
|
|
34
35
|
missingRunMessage(id: string, display: string | undefined): string;
|
|
35
36
|
stopAgent(nodeId: string): boolean;
|
|
36
37
|
steerAgent(nodeId: string, text: string): boolean;
|
|
@@ -60,7 +61,22 @@ export function fenceIntercomToolOutcome(out: IntercomOutcome, action: string, f
|
|
|
60
61
|
return action === "message" && out.details.ok ? fence(out.text) : fenceIntercomOutcome(out, fence);
|
|
61
62
|
}
|
|
62
63
|
|
|
64
|
+
/** Renderer-local state only. The persisted result, not this cache, owns display identity. */
|
|
65
|
+
interface IntercomRenderState {
|
|
66
|
+
target?: { id: string; label: string };
|
|
67
|
+
callText?: Text;
|
|
68
|
+
formatCall?: (target: string) => string;
|
|
69
|
+
}
|
|
70
|
+
|
|
63
71
|
export function registerIntercomTool(pi: ExtensionAPI, d: IntercomToolDeps): void {
|
|
72
|
+
function displayRunTarget(id: string): string {
|
|
73
|
+
const run = d.tracker?.peek(id);
|
|
74
|
+
if (!run) return sanitizeDisplayLabel(id, "run");
|
|
75
|
+
const name = runDisplayName(run);
|
|
76
|
+
// An alias is presentation, never a unique address. Disambiguate collisions without rerouting.
|
|
77
|
+
const collision = d.tracker.list().some((other) => other.id !== id && runDisplayName(other) === name);
|
|
78
|
+
return collision ? `${name} [${sanitizeDisplayLabel(id, "run")}]` : name;
|
|
79
|
+
}
|
|
64
80
|
// ── intercom tool (supervisor side of the comm plane: read/answer children) ───
|
|
65
81
|
const IntercomToolParams = Type.Object({
|
|
66
82
|
action: Type.Union(
|
|
@@ -78,14 +94,15 @@ export function registerIntercomTool(pi: ExtensionAPI, d: IntercomToolDeps): voi
|
|
|
78
94
|
],
|
|
79
95
|
{
|
|
80
96
|
description:
|
|
81
|
-
"peek = watch async sub-agents · result = retrieve one complete settled result by run id · wait =
|
|
97
|
+
"peek = watch async sub-agents · result = retrieve one complete settled result by run id · wait = collect current reports; sync:true joins (default: nonblocking with UI, blocking headless) · steer = soft redirect into one by run id (it may ignore it) · stop = request engine cancellation by run id · message = retrieve one drained bus message by message id · list/inbox/reply/send = coaching messages (needs a coaching persona)",
|
|
82
98
|
},
|
|
83
99
|
),
|
|
84
|
-
to: Type.Optional(Type.String({ maxLength: MAX_INTERCOM_REF_CHARS, description: "result/steer/stop/peek: the async run id (e.g. 'run-1') · wait: an async run id; omit it to
|
|
100
|
+
to: Type.Optional(Type.String({ maxLength: MAX_INTERCOM_REF_CHARS, description: "result/steer/stop/peek: the async run id (e.g. 'run-1') · wait: an async run id; omit it to collect all current/pending completions · send: the child handle (from `list`)" })),
|
|
85
101
|
messageId: Type.Optional(Type.String({ maxLength: MAX_INTERCOM_REF_CHARS, description: "message: the drained bus message id shown by `inbox`" })),
|
|
86
102
|
askId: Type.Optional(Type.String({ maxLength: MAX_INTERCOM_REF_CHARS, description: "reply: the message id of the child's pending question" })),
|
|
87
103
|
message: Type.Optional(Type.String({ maxLength: MAX_INTERCOM_MESSAGE_CHARS, description: "steer/reply/send: the text to deliver" })),
|
|
88
|
-
|
|
104
|
+
sync: Type.Optional(Type.Boolean({ description: "wait: block for runs to settle; defaults to false in interactive/RPC sessions and true in headless sessions" })),
|
|
105
|
+
timeoutMs: Type.Optional(Type.Number({ description: "wait with sync:true: max ms to hold your turn (default 600000; values below 1000 clamp to 1000, values above 600000 clamp to 600000) — on timeout you get what settled + what's still running" })),
|
|
89
106
|
});
|
|
90
107
|
pi.registerTool({
|
|
91
108
|
name: "intercom",
|
|
@@ -93,9 +110,10 @@ export function registerIntercomTool(pi: ExtensionAPI, d: IntercomToolDeps): voi
|
|
|
93
110
|
description: [
|
|
94
111
|
"See, steer, message, and JOIN your running sub-agents.",
|
|
95
112
|
"`peek` watches what your async sub-agents are doing; `result` retrieves one complete settled",
|
|
96
|
-
"payload by run id; `wait`
|
|
113
|
+
"payload by run id; `wait` collects current reports and is nonblocking by default with UI (interactive/RPC); set `sync: true` for an intentional bounded join. Headless defaults to joining; set `sync: false` for a snapshot. Do not automatically wait for all children.",
|
|
114
|
+
"Work independently while children run; when only children remain, end your turn and let completion follow-ups wake you; do not monitor or poll healthy children.",
|
|
97
115
|
"`steer` sends a soft course-correction to one (by run id); in-process async runs can receive it mid-run,",
|
|
98
|
-
"while child-engine (MCP/worktree) async runs receive it as a brokered follow-up.",
|
|
116
|
+
"while child-engine (MCP/worktree) async runs receive it as a brokered follow-up. After remaining tools return, Pi can consume steering; arbitrary active tool work is not promised to stop immediately.",
|
|
99
117
|
"`message` retrieves a drained bus message by its message id; `list`/`inbox`/`reply`/`send` exchange coaching messages (a child reaches you via `contact_supervisor`)",
|
|
100
118
|
"and need a `coaching: on` persona.",
|
|
101
119
|
].join(" "),
|
|
@@ -105,6 +123,7 @@ export function registerIntercomTool(pi: ExtensionAPI, d: IntercomToolDeps): voi
|
|
|
105
123
|
// `to` comes from model-authored tool arguments. Use the exact value for routing, but never
|
|
106
124
|
// interpolate it into trusted prose without reducing it to compact identifier metadata.
|
|
107
125
|
const displayTarget = params.to === undefined ? undefined : sanitizeDisplayLabel(params.to, "run");
|
|
126
|
+
const targetSnapshot = params.to === undefined ? {} : { runId: params.to, target: displayRunTarget(params.to) };
|
|
108
127
|
// peek + wait + steer + stop are supervisor→child controls over the async d.tracker /
|
|
109
128
|
// steer handles — available to EVERY persona (no dependency on the coaching bus).
|
|
110
129
|
if (params.action === "peek") {
|
|
@@ -113,7 +132,7 @@ export function registerIntercomTool(pi: ExtensionAPI, d: IntercomToolDeps): voi
|
|
|
113
132
|
const runs = params.to
|
|
114
133
|
? [d.tracker.peek(params.to)].filter((r): r is AsyncRun => !!r)
|
|
115
134
|
: dedupeRunsById([...d.tracker.running(), ...d.completionNotifier.peekPending()]);
|
|
116
|
-
return { content: [{ type: "text", text: buildPeekDigest(runs, { now: Date.now(), stallMs: d.STALL_FLAG_MS }) }], details: { action: "peek", ok: true }, isError: false };
|
|
135
|
+
return { content: [{ type: "text", text: buildPeekDigest(runs, { now: Date.now(), stallMs: d.STALL_FLAG_MS, ...(d.waitingRunIds ? { waitingForSupervisor: d.waitingRunIds() } : {}) }) }], details: { action: "peek", ok: true, ...targetSnapshot }, isError: false };
|
|
117
136
|
}
|
|
118
137
|
if (params.action === "result") {
|
|
119
138
|
if (!params.to) {
|
|
@@ -121,12 +140,12 @@ export function registerIntercomTool(pi: ExtensionAPI, d: IntercomToolDeps): voi
|
|
|
121
140
|
}
|
|
122
141
|
const run = d.tracker.peek(params.to);
|
|
123
142
|
if (!run) {
|
|
124
|
-
return { content: [{ type: "text", text: d.missingRunMessage(params.to, displayTarget) }], details: failureDetails({ action: "result", ok: false }), isError: true };
|
|
143
|
+
return { content: [{ type: "text", text: d.missingRunMessage(params.to, displayTarget) }], details: failureDetails({ action: "result", ok: false, ...targetSnapshot }), isError: true };
|
|
125
144
|
}
|
|
126
145
|
if (run.status === "running") {
|
|
127
146
|
return {
|
|
128
|
-
content: [{ type: "text", text: `${
|
|
129
|
-
details: failureDetails({ action: "result", ok: false, status: run.status }),
|
|
147
|
+
content: [{ type: "text", text: `${targetSnapshot.target} is still running. Use intercom peek/wait, or request result after it settles.` }],
|
|
148
|
+
details: failureDetails({ action: "result", ok: false, status: run.status, ...targetSnapshot }),
|
|
130
149
|
isError: true,
|
|
131
150
|
};
|
|
132
151
|
}
|
|
@@ -149,7 +168,7 @@ export function registerIntercomTool(pi: ExtensionAPI, d: IntercomToolDeps): voi
|
|
|
149
168
|
d.publishPersonaCost();
|
|
150
169
|
return {
|
|
151
170
|
content: [{ type: "text", text: `${run.id} (${displayRun}) · ${run.status}${took ? ` · ${took}` : ""}${cause}\n${body}` }],
|
|
152
|
-
details: { action: "result", ok: true, status: run.status,
|
|
171
|
+
details: { action: "result", ok: true, status: run.status, ...targetSnapshot },
|
|
153
172
|
isError: false,
|
|
154
173
|
...toolUsageField(billed),
|
|
155
174
|
};
|
|
@@ -158,7 +177,7 @@ export function registerIntercomTool(pi: ExtensionAPI, d: IntercomToolDeps): voi
|
|
|
158
177
|
// No `to` → wait on running legs AND collect settled legs still queued for follow-up
|
|
159
178
|
// delivery, so a wait in the settle→deliver gap returns their results (not "nothing").
|
|
160
179
|
if (params.to && !d.tracker.peek(params.to)) {
|
|
161
|
-
return { content: [{ type: "text", text: d.missingRunMessage(params.to, displayTarget) }], details: failureDetails({ action: "wait", ok: false }), isError: true };
|
|
180
|
+
return { content: [{ type: "text", text: d.missingRunMessage(params.to, displayTarget) }], details: failureDetails({ action: "wait", ok: false, ...targetSnapshot }), isError: true };
|
|
162
181
|
}
|
|
163
182
|
const ids = params.to
|
|
164
183
|
? [params.to]
|
|
@@ -166,6 +185,31 @@ export function registerIntercomTool(pi: ExtensionAPI, d: IntercomToolDeps): voi
|
|
|
166
185
|
if (ids.length === 0) {
|
|
167
186
|
return { content: [{ type: "text", text: "No async runs to wait for." }], details: { action: "wait", ok: true }, isError: false };
|
|
168
187
|
}
|
|
188
|
+
// Interactive/RPC defaults to a snapshot: waiting inside a tool stalls queued user steering.
|
|
189
|
+
// Headless retains its historical join; callers can override either default with sync.
|
|
190
|
+
if (!(params.sync ?? ctx.hasUI !== true)) {
|
|
191
|
+
const current = ids.map((id) => d.tracker.peek(id)).filter((run): run is AsyncRun => !!run);
|
|
192
|
+
const settled = current.filter((run) => run.status !== "running");
|
|
193
|
+
const still = current.filter((run) => run.status === "running");
|
|
194
|
+
const settledIds = new Set(settled.map((run) => run.id));
|
|
195
|
+
d.completionNotifier.discard((run) => settledIds.has(run.id));
|
|
196
|
+
for (const id of settledIds) d.tracker.markCollected(id);
|
|
197
|
+
const report = settled.length > 0 ? renderCompletion(settled, fenceUntrusted, (t) => d.scanForSurrender(t)) : "";
|
|
198
|
+
const stillIds = still.map((run) => sanitizeDisplayLabel(run.id, "run"));
|
|
199
|
+
const omitted = stillIds.length > MAX_ASYNC_STATUS_ROWS;
|
|
200
|
+
const runningNote = still.length > 0
|
|
201
|
+
? `Background continues; automatic completion will notify you. End your turn when this is the only remaining work. Still running: ${stillIds.slice(0, MAX_ASYNC_STATUS_ROWS).join(", ")}${omitted ? `, … +${stillIds.length - MAX_ASYNC_STATUS_ROWS} more` : ""}.`
|
|
202
|
+
: "";
|
|
203
|
+
const text = boundCompletionSurface([report, runningNote].filter(Boolean).join("\n\n") || "Nothing to report (unknown run ids?).");
|
|
204
|
+
const billed = d.childUsage.accountMany(settled.map((run) => ({ key: run.id, usage: run.result?.usage ?? emptyUsage() })));
|
|
205
|
+
d.publishPersonaCost();
|
|
206
|
+
return {
|
|
207
|
+
content: [{ type: "text", text }],
|
|
208
|
+
details: { action: "wait", ok: true, settled: [...settledIds], running: still.map((run) => run.id), ...targetSnapshot },
|
|
209
|
+
isError: false,
|
|
210
|
+
...toolUsageField(billed),
|
|
211
|
+
};
|
|
212
|
+
}
|
|
169
213
|
// Bounded join: never longer than a child's ask timeout (bus `ask` default 600s),
|
|
170
214
|
// so a coaching child blocking on OUR reply can't deadlock us past its own timeout.
|
|
171
215
|
// Default matches that ceiling — heavy sub-agents (30+ turns) routinely outlast a
|
|
@@ -193,7 +237,7 @@ export function registerIntercomTool(pi: ExtensionAPI, d: IntercomToolDeps): voi
|
|
|
193
237
|
d.publishPersonaCost();
|
|
194
238
|
return {
|
|
195
239
|
content: [{ type: "text", text }],
|
|
196
|
-
details: { action: "wait", ok: true, settled: [...settledIds], running: still.map((r) => r.id) },
|
|
240
|
+
details: { action: "wait", ok: true, settled: [...settledIds], running: still.map((r) => r.id), ...targetSnapshot },
|
|
197
241
|
isError: false,
|
|
198
242
|
...toolUsageField(billed),
|
|
199
243
|
};
|
|
@@ -203,18 +247,26 @@ export function registerIntercomTool(pi: ExtensionAPI, d: IntercomToolDeps): voi
|
|
|
203
247
|
return { content: [{ type: "text", text: "intercom steer needs { to: <run id>, message }." }], details: failureDetails({ action: "steer", ok: false }), isError: true };
|
|
204
248
|
}
|
|
205
249
|
const nodeId = `async:${params.to}`;
|
|
250
|
+
const knownRun = d.tracker.peek(params.to);
|
|
251
|
+
if (knownRun && knownRun.status !== "running") {
|
|
252
|
+
return {
|
|
253
|
+
content: [{ type: "text", text: `Cannot steer "${targetSnapshot.target}" — this run is already settled (${knownRun.status}). Use intercom result with to="${displayTarget}" to inspect its retained result.` }],
|
|
254
|
+
details: failureDetails({ action: "steer", ok: false, status: knownRun.status, ...targetSnapshot }),
|
|
255
|
+
isError: true,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
206
258
|
if (!d.steerRegistry.has(nodeId)) {
|
|
207
259
|
return {
|
|
208
|
-
content: [{ type: "text", text: `Cannot steer "${
|
|
209
|
-
details: failureDetails({ action: "steer", ok: false }),
|
|
260
|
+
content: [{ type: "text", text: `Cannot steer "${targetSnapshot.target}" — no live steer handle is available (the run may not have started yet, or its engine/broker does not expose steering).` }],
|
|
261
|
+
details: failureDetails({ action: "steer", ok: false, ...targetSnapshot }),
|
|
210
262
|
isError: true,
|
|
211
263
|
};
|
|
212
264
|
}
|
|
213
265
|
// Routed through the guarded d.steerAgent so a just-finished/d.disposed handle can't throw.
|
|
214
266
|
const steered = d.steerAgent(nodeId, params.message);
|
|
215
267
|
return steered
|
|
216
|
-
? { content: [{ type: "text", text: `Steering
|
|
217
|
-
: { content: [{ type: "text", text: `Could not steer "${
|
|
268
|
+
? { content: [{ type: "text", text: `Steering queued for ${targetSnapshot.target} (${displayTarget}).` }], details: { action: "steer", ok: true, ...targetSnapshot, message: params.message }, isError: false }
|
|
269
|
+
: { content: [{ type: "text", text: `Could not steer "${targetSnapshot.target}" — it may have just finished, or the message was empty.` }], details: failureDetails({ action: "steer", ok: false, ...targetSnapshot }), isError: true };
|
|
218
270
|
}
|
|
219
271
|
if (params.action === "stop") {
|
|
220
272
|
if (!params.to) {
|
|
@@ -226,21 +278,21 @@ export function registerIntercomTool(pi: ExtensionAPI, d: IntercomToolDeps): voi
|
|
|
226
278
|
const repeated = d.stopRequested.has(nodeId);
|
|
227
279
|
const stopped = d.stopAgent(nodeId);
|
|
228
280
|
if (stopped && !repeated) {
|
|
229
|
-
return { content: [{ type: "text", text: `Cancellation requested for ${
|
|
281
|
+
return { content: [{ type: "text", text: `Cancellation requested for ${targetSnapshot.target}. Check its terminal result; changes already made are not undone.` }], details: { action: "stop", ok: true, ...targetSnapshot }, isError: false };
|
|
230
282
|
}
|
|
231
283
|
// A repeated stop has just invoked the REAL cancel handle again. Only now force-clear
|
|
232
284
|
// d.tracker state if engine settlement is still lagging; the handle remains registered
|
|
233
285
|
// until onComplete so cancellation can never be replaced by UI-only bookkeeping.
|
|
234
286
|
if (stopped && repeated && d.tracker.forceSettle(params.to, "force-stopped by supervisor after repeated engine cancellation")) {
|
|
235
287
|
return {
|
|
236
|
-
content: [{ type: "text", text: `Force-cleared ${
|
|
237
|
-
details: { action: "stop", ok: true },
|
|
288
|
+
content: [{ type: "text", text: `Force-cleared ${targetSnapshot.target} after repeating the engine cancellation; it will no longer be tracked as running.` }],
|
|
289
|
+
details: { action: "stop", ok: true, ...targetSnapshot },
|
|
238
290
|
isError: false,
|
|
239
291
|
};
|
|
240
292
|
}
|
|
241
293
|
return {
|
|
242
|
-
content: [{ type: "text", text: `Cannot stop "${
|
|
243
|
-
details: failureDetails({ action: "stop", ok: false }),
|
|
294
|
+
content: [{ type: "text", text: `Cannot stop "${targetSnapshot.target}" — no such running run (it already finished).` }],
|
|
295
|
+
details: failureDetails({ action: "stop", ok: false, ...targetSnapshot }),
|
|
244
296
|
isError: true,
|
|
245
297
|
};
|
|
246
298
|
}
|
|
@@ -261,28 +313,63 @@ export function registerIntercomTool(pi: ExtensionAPI, d: IntercomToolDeps): voi
|
|
|
261
313
|
}
|
|
262
314
|
return { content: [{ type: "text", text }], details: out.details.ok ? out.details : failureDetails(out.details), isError: !out.details.ok };
|
|
263
315
|
},
|
|
264
|
-
renderCall(args, theme) {
|
|
316
|
+
renderCall(args, theme, context) {
|
|
317
|
+
const state = context?.state as IntercomRenderState | undefined;
|
|
265
318
|
const action = compactInlineText(args.action ?? "?", { maxChars: 24 }) || "?";
|
|
266
319
|
let target = "";
|
|
267
|
-
|
|
268
|
-
|
|
320
|
+
const runTarget = typeof args.to === "string"
|
|
321
|
+
? state?.target?.id === args.to ? state.target.label : displayRunTarget(args.to)
|
|
322
|
+
: undefined;
|
|
323
|
+
if (action === "wait" || action === "peek") target = runTarget ?? "all";
|
|
324
|
+
else if (["result", "steer", "stop"].includes(action)) target = runTarget ?? "?";
|
|
325
|
+
else if (action === "send") target = compactInlineText(args.to ?? "?", { maxChars: 80 }) || "?";
|
|
269
326
|
else if (action === "message") target = compactInlineText(args.messageId ?? "?", { maxChars: 80 }) || "?";
|
|
270
327
|
else if (action === "reply") target = compactInlineText(args.askId ?? "?", { maxChars: 80 }) || "?";
|
|
271
328
|
const timeout = action === "wait" && Number.isFinite(args.timeoutMs) && args.timeoutMs !== undefined
|
|
272
329
|
? ` · ${Math.max(0, Math.floor(args.timeoutMs))}ms`
|
|
273
330
|
: "";
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
);
|
|
331
|
+
const formatCall = (label: string) => `${theme.fg("toolTitle", theme.bold("intercom "))}${theme.fg("accent", action)}${label ? theme.fg("dim", ` ${compactInlineText(label, { maxChars: 80 })}${timeout}`) : ""}`;
|
|
332
|
+
const component = new Text(formatCall(target), 0, 0);
|
|
333
|
+
if (state) { state.callText = component; state.formatCall = formatCall; }
|
|
334
|
+
return component;
|
|
279
335
|
},
|
|
280
|
-
renderResult(result, { expanded }, theme) {
|
|
281
|
-
const details = (result.details ?? {}) as { action?: string; ok?: boolean };
|
|
336
|
+
renderResult(result, { expanded }, theme, context) {
|
|
337
|
+
const details = (result.details ?? {}) as { action?: string; ok?: boolean; runId?: string; target?: string; message?: string };
|
|
338
|
+
const state = context?.state as IntercomRenderState | undefined;
|
|
339
|
+
const action = details.action ?? context?.args?.action;
|
|
340
|
+
const savedId = details.runId ?? context?.args?.to;
|
|
341
|
+
const legacyCompleted = !details.target && context !== undefined && context.isPartial !== true
|
|
342
|
+
&& ["steer", "stop", "result", "peek", "wait"].includes(action ?? "");
|
|
343
|
+
if (state && typeof savedId === "string" && (details.target || legacyCompleted)) {
|
|
344
|
+
// Old receipts have no display snapshot. A new session may reuse their run ID;
|
|
345
|
+
// the current tracker cannot supply a historical alias for that unrelated execution.
|
|
346
|
+
state.target = { id: savedId, label: details.target ? compactInlineText(details.target, { maxChars: 80 }) : sanitizeDisplayLabel(savedId, "run") };
|
|
347
|
+
// Pi renders the call before the result. Update that same Text rather than causing a
|
|
348
|
+
// recursive invalidation, so restored history uses its saved alias on its first frame.
|
|
349
|
+
if (state.callText && state.formatCall) state.callText.setText(state.formatCall(state.target.label));
|
|
350
|
+
}
|
|
282
351
|
const first = result.content[0];
|
|
283
352
|
const full = sanitizeTerminalText(first?.type === "text" ? first.text : "(no output)");
|
|
284
353
|
const failed = details.ok === false;
|
|
285
354
|
const prefix = failed ? `${theme.fg("error", theme.bold("failed"))}\n` : "";
|
|
355
|
+
if (details.action === "steer" && details.ok === true) {
|
|
356
|
+
const target = compactInlineText(details.target ?? state?.target?.label ?? (legacyCompleted && typeof savedId === "string" ? sanitizeDisplayLabel(savedId, "run") : details.runId ? displayRunTarget(details.runId) : ""), { maxChars: 80 });
|
|
357
|
+
const message = typeof details.message === "string" ? details.message : context?.args?.message;
|
|
358
|
+
// Old persisted receipts may predate message snapshots. Keep their original content
|
|
359
|
+
// available in expansion rather than fabricating a message that was never recorded.
|
|
360
|
+
if (expanded) {
|
|
361
|
+
const text = typeof message === "string"
|
|
362
|
+
? `Steering queued${target ? ` for ${target}` : ""}.${details.runId ? `\nRun: ${sanitizeDisplayLabel(details.runId, "run")}` : ""}\n\n${sanitizeTerminalText(message)}`
|
|
363
|
+
: full;
|
|
364
|
+
return new Text(theme.fg("toolOutput", text), 0, 0);
|
|
365
|
+
}
|
|
366
|
+
const title = compactInlineText(`steering queued${target ? ` · ${target}` : ""}`, { maxChars: 100 });
|
|
367
|
+
const preview = typeof message === "string" ? compactVisibleText(message, { maxLines: 3, maxLineChars: 100 }) : undefined;
|
|
368
|
+
// Native Pi already rendered `intercom steer <alias>` above this result. Show the
|
|
369
|
+
// instruction directly there; only standalone/older renderers need another identity row.
|
|
370
|
+
const showTitle = !state?.callText || !preview;
|
|
371
|
+
return new Text(`${showTitle ? theme.fg("success", title) : ""}${preview ? `${showTitle ? "\n" : ""}${theme.fg("toolOutput", preview.text)}${preview.truncated ? `\n${theme.fg("dim", expandDetailHint())}` : ""}` : ""}`, 0, 0);
|
|
372
|
+
}
|
|
286
373
|
if (expanded) return new Text(`${prefix}${theme.fg("toolOutput", full)}`, 0, 0);
|
|
287
374
|
const preview = compactVisibleText(full, { maxLines: 4, maxLineChars: 100 });
|
|
288
375
|
const hint = preview.truncated ? `\n${theme.fg("dim", expandDetailHint())}` : "";
|