@henryqw/pi-subagent 6.0.0 → 6.1.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/README.md +3 -1
- package/dist/ephemeral.d.ts +13 -0
- package/dist/ephemeral.js +85 -6
- package/dist/index.d.ts +1 -1
- package/docs/orchestration.md +13 -1
- package/extensions/delegate-flow.ts +62 -9
- package/extensions/subagent.ts +134 -23
- package/extensions/tool-render.ts +16 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -30,6 +30,8 @@ pi install npm:@henryqw/pi-subagent
|
|
|
30
30
|
| `delegate_flow` | tool | Package-owned parallel implementation and declared-order Git integration for 1–8 independent units. |
|
|
31
31
|
| `delegate_flow_continue` | tool | Repair the blocked Flow unit once in its existing worktree. |
|
|
32
32
|
|
|
33
|
+
All three delegation tool blocks use a compact self-rendered shell and never exceed five physical lines, including Pi's leading spacer: one call line plus at most three result lines in either collapsed or expanded view.
|
|
34
|
+
|
|
33
35
|
### `delegate_task`
|
|
34
36
|
|
|
35
37
|
Select exactly one shape:
|
|
@@ -51,7 +53,7 @@ Parallel mode starts entries concurrently, waits for every entry, and reports th
|
|
|
51
53
|
|
|
52
54
|
Background workflows are session-scoped. Session shutdown or reload aborts them and may deliver only recoverable-work evidence or no follow-up message.
|
|
53
55
|
|
|
54
|
-
The transient status widget owns live progress:
|
|
56
|
+
The transient two-line status widget owns deterministic live progress: status and task summary above thinking or the active tool (with elapsed time and path basename), completed turns, started tools, model, thinking level, tokens, and total duration; terminal activity is Done, Failed, or Stopped. At capacity, it evicts the oldest terminal row so new active work remains visible, and terminal rows otherwise clear on the next real user input. The final `delegate_task` block is deliberately minimal: bounded final summaries, role attribution for parallel/chain, and only retained-worktree recovery paths. It has no expanded view.
|
|
55
57
|
|
|
56
58
|
Each delegation resolves its own Role, resources, route, and optional worktree request. When available, `isolation: worktree` gives each entry a deterministic separate worktree; non-Git or unborn-`HEAD` contexts may use Main's cwd. Siblings and chain steps never implicitly share one created worktree.
|
|
57
59
|
|
package/dist/ephemeral.d.ts
CHANGED
|
@@ -8,10 +8,23 @@ export interface EphemeralSubagentExecutorOptions {
|
|
|
8
8
|
maxConcurrency: number;
|
|
9
9
|
timeout: EphemeralSubagentTimeout;
|
|
10
10
|
}
|
|
11
|
+
export type EphemeralSubagentActivityEvent = {
|
|
12
|
+
type: "tool_execution_start";
|
|
13
|
+
toolCallId: string;
|
|
14
|
+
toolName: string;
|
|
15
|
+
path?: string;
|
|
16
|
+
} | {
|
|
17
|
+
type: "tool_execution_end";
|
|
18
|
+
toolCallId: string;
|
|
19
|
+
toolName: string;
|
|
20
|
+
} | {
|
|
21
|
+
type: "message_end";
|
|
22
|
+
};
|
|
11
23
|
export interface EphemeralSubagentRunInput {
|
|
12
24
|
signal?: AbortSignal;
|
|
13
25
|
onUpdate?: (text: string) => void;
|
|
14
26
|
onTokens?: (tokens: number) => void;
|
|
27
|
+
onActivity?: (event: EphemeralSubagentActivityEvent) => void;
|
|
15
28
|
prepare: () => Promise<{
|
|
16
29
|
launch: PiLaunch;
|
|
17
30
|
task: string;
|
package/dist/ephemeral.js
CHANGED
|
@@ -4,6 +4,9 @@ import { basename } from "node:path";
|
|
|
4
4
|
import { StringDecoder } from "node:string_decoder";
|
|
5
5
|
const MAX_OUTPUT_BYTES = 50 * 1024;
|
|
6
6
|
const MAX_JSON_EVENT_BYTES = 1024 * 1024;
|
|
7
|
+
const MAX_ACTIVITY_TEXT_BYTES = 4 * 1024;
|
|
8
|
+
// A JSON string byte can take six source bytes (for example, \u0000).
|
|
9
|
+
const MAX_ACTIVITY_PREFIX_BYTES = 2 * MAX_ACTIVITY_TEXT_BYTES * 6 + 1024;
|
|
7
10
|
const MAX_TIMER_DELAY_MS = 2_147_483_647;
|
|
8
11
|
const POST_EXIT_STDIO_IDLE_MS = 250;
|
|
9
12
|
const POST_EXIT_STDIO_HARD_MS = 1_000;
|
|
@@ -34,6 +37,8 @@ const PI_JSON_EVENTS = {
|
|
|
34
37
|
};
|
|
35
38
|
const CONSUMED_JSON_EVENTS = new Set(["message_start", "message_update", "message_end"]);
|
|
36
39
|
const JSON_EVENT_TYPE = /^\s*\{\s*"type"\s*:\s*"([^"\\]+)"/;
|
|
40
|
+
const JSON_STRING = `"(?:[^"\\\\\u0000-\u001f]|\\\\(?:["\\\\/bfnrt]|u[0-9a-fA-F]{4}))*"`;
|
|
41
|
+
const JSON_OVERSIZED_TOOL_START = new RegExp(`^\\s*\\{\\s*"type"\\s*:\\s*"tool_execution_start"\\s*,\\s*"toolCallId"\\s*:\\s*(${JSON_STRING})\\s*,\\s*"toolName"\\s*:\\s*(${JSON_STRING})(?=\\s*,)`);
|
|
37
42
|
export class EphemeralSubagentError extends Error {
|
|
38
43
|
name = "EphemeralSubagentError";
|
|
39
44
|
code;
|
|
@@ -85,11 +90,15 @@ function validateRunInput(value) {
|
|
|
85
90
|
if (input.onTokens !== undefined && typeof input.onTokens !== "function") {
|
|
86
91
|
throw new TypeError("run.onTokens must be a function.");
|
|
87
92
|
}
|
|
93
|
+
if (input.onActivity !== undefined && typeof input.onActivity !== "function") {
|
|
94
|
+
throw new TypeError("run.onActivity must be a function.");
|
|
95
|
+
}
|
|
88
96
|
return {
|
|
89
97
|
signal: input.signal,
|
|
90
98
|
prepare: input.prepare,
|
|
91
99
|
onUpdate: input.onUpdate,
|
|
92
100
|
onTokens: input.onTokens,
|
|
101
|
+
onActivity: input.onActivity,
|
|
93
102
|
};
|
|
94
103
|
}
|
|
95
104
|
function record(value, field) {
|
|
@@ -229,6 +238,33 @@ function assistantText(message) {
|
|
|
229
238
|
.join("\n");
|
|
230
239
|
return text || undefined;
|
|
231
240
|
}
|
|
241
|
+
function activityTooLong(value) {
|
|
242
|
+
return typeof value === "string" && Buffer.byteLength(value, "utf8") > MAX_ACTIVITY_TEXT_BYTES;
|
|
243
|
+
}
|
|
244
|
+
function hasTerminalControlChars(text) {
|
|
245
|
+
return Array.from(text).some((character) => {
|
|
246
|
+
const code = character.codePointAt(0);
|
|
247
|
+
return code <= 0x1f || code >= 0x7f && code <= 0x9f || code === 0x2028 || code === 0x2029;
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
function activityText(value) {
|
|
251
|
+
return typeof value === "string" && value.trim().length > 0 && !activityTooLong(value) && !hasTerminalControlChars(value);
|
|
252
|
+
}
|
|
253
|
+
function oversizedToolStart(prefix) {
|
|
254
|
+
const match = JSON_OVERSIZED_TOOL_START.exec(prefix);
|
|
255
|
+
if (!match)
|
|
256
|
+
return;
|
|
257
|
+
try {
|
|
258
|
+
const toolCallId = JSON.parse(match[1]);
|
|
259
|
+
const toolName = JSON.parse(match[2]);
|
|
260
|
+
if (!activityText(toolCallId) || !activityText(toolName))
|
|
261
|
+
return;
|
|
262
|
+
return { type: "tool_execution_start", toolCallId, toolName };
|
|
263
|
+
}
|
|
264
|
+
catch {
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
232
268
|
function utf8Prefix(text, maxBytes) {
|
|
233
269
|
return new StringDecoder().write(Buffer.from(text).subarray(0, maxBytes));
|
|
234
270
|
}
|
|
@@ -471,16 +507,26 @@ async function runPi(prepared, input, timeoutPolicy, invocation) {
|
|
|
471
507
|
signalCallbackFailure();
|
|
472
508
|
stop(true);
|
|
473
509
|
};
|
|
510
|
+
let activityQueue = Promise.resolve();
|
|
474
511
|
const invokeCallback = (name, callback, value) => {
|
|
475
512
|
if (!callback || callbackFailure)
|
|
476
513
|
return;
|
|
477
514
|
let pending;
|
|
478
|
-
|
|
479
|
-
pending =
|
|
515
|
+
if (name === "onActivity") {
|
|
516
|
+
pending = activityQueue.then(() => {
|
|
517
|
+
if (!callbackFailure)
|
|
518
|
+
return callback(value);
|
|
519
|
+
}).catch((cause) => { failCallback(name, cause); });
|
|
520
|
+
activityQueue = pending;
|
|
480
521
|
}
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
522
|
+
else {
|
|
523
|
+
try {
|
|
524
|
+
pending = Promise.resolve(callback(value)).then(undefined, (cause) => { failCallback(name, cause); });
|
|
525
|
+
}
|
|
526
|
+
catch (cause) {
|
|
527
|
+
failCallback(name, cause);
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
484
530
|
}
|
|
485
531
|
pendingCallbacks.add(pending);
|
|
486
532
|
void pending.then(() => pendingCallbacks.delete(pending));
|
|
@@ -550,6 +596,29 @@ async function runPi(prepared, input, timeoutPolicy, invocation) {
|
|
|
550
596
|
}
|
|
551
597
|
return;
|
|
552
598
|
}
|
|
599
|
+
if (record.type === "tool_execution_start" || record.type === "tool_execution_end") {
|
|
600
|
+
const { toolCallId, toolName } = record;
|
|
601
|
+
if (!activityText(toolCallId) || !activityText(toolName))
|
|
602
|
+
return;
|
|
603
|
+
if (record.type === "tool_execution_start") {
|
|
604
|
+
const args = record.args;
|
|
605
|
+
const path = args && typeof args === "object" && !Array.isArray(args)
|
|
606
|
+
? args.path
|
|
607
|
+
: undefined;
|
|
608
|
+
if (activityTooLong(path))
|
|
609
|
+
return;
|
|
610
|
+
invokeCallback("onActivity", input.onActivity, {
|
|
611
|
+
type: "tool_execution_start",
|
|
612
|
+
toolCallId,
|
|
613
|
+
toolName,
|
|
614
|
+
...(activityText(path) ? { path } : {}),
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
else {
|
|
618
|
+
invokeCallback("onActivity", input.onActivity, { type: "tool_execution_end", toolCallId, toolName });
|
|
619
|
+
}
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
553
622
|
if (record.type !== "message_end")
|
|
554
623
|
return;
|
|
555
624
|
const text = assistantText(record.message);
|
|
@@ -566,6 +635,7 @@ async function runPi(prepared, input, timeoutPolicy, invocation) {
|
|
|
566
635
|
currentTokens = 0;
|
|
567
636
|
currentUsage = undefined;
|
|
568
637
|
invokeCallback("onTokens", input.onTokens, completedTokens);
|
|
638
|
+
invokeCallback("onActivity", input.onActivity, { type: "message_end" });
|
|
569
639
|
}
|
|
570
640
|
if (typeof message.stopReason === "string")
|
|
571
641
|
stopReason = message.stopReason;
|
|
@@ -628,7 +698,9 @@ async function runPi(prepared, input, timeoutPolicy, invocation) {
|
|
|
628
698
|
const end = newline === -1 ? data.length : newline;
|
|
629
699
|
const part = data.slice(offset, end);
|
|
630
700
|
if (!ignoreLine) {
|
|
631
|
-
|
|
701
|
+
const remainingPrefix = MAX_ACTIVITY_PREFIX_BYTES - Buffer.byteLength(linePrefix, "utf8");
|
|
702
|
+
if (remainingPrefix > 0)
|
|
703
|
+
linePrefix += utf8Prefix(part, remainingPrefix);
|
|
632
704
|
const eventType = JSON_EVENT_TYPE.exec(linePrefix)?.[1];
|
|
633
705
|
if (eventType && !lineEventType)
|
|
634
706
|
lineEventType = eventType;
|
|
@@ -652,6 +724,13 @@ async function runPi(prepared, input, timeoutPolicy, invocation) {
|
|
|
652
724
|
return;
|
|
653
725
|
if (!ignoreLine)
|
|
654
726
|
processLine(lineParts.join(""));
|
|
727
|
+
else if (lineEventType === "tool_execution_start") {
|
|
728
|
+
const activity = oversizedToolStart(linePrefix);
|
|
729
|
+
if (activity) {
|
|
730
|
+
observeEvent();
|
|
731
|
+
invokeCallback("onActivity", input.onActivity, activity);
|
|
732
|
+
}
|
|
733
|
+
}
|
|
655
734
|
if (callbackFailure)
|
|
656
735
|
return;
|
|
657
736
|
lineParts = [];
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { type HerdrExecutor } from "@henryqw/pi-herdr";
|
|
3
3
|
import { type AvailableModel, type ProfileName, type ResolvedTaskRoute, type ThinkingLevel } from "@henryqw/pi-task-models";
|
|
4
|
-
export { addUsage, capEphemeralSubagentOutput, createEphemeralSubagentExecutor, EphemeralSubagentError, formatDuration, type EphemeralSubagentErrorCode, type EphemeralSubagentExecutor, type EphemeralSubagentExecutorOptions, type EphemeralSubagentResult, type EphemeralSubagentRunInput, type EphemeralSubagentTimeout, } from "./ephemeral.ts";
|
|
4
|
+
export { addUsage, capEphemeralSubagentOutput, createEphemeralSubagentExecutor, EphemeralSubagentError, formatDuration, type EphemeralSubagentActivityEvent, type EphemeralSubagentErrorCode, type EphemeralSubagentExecutor, type EphemeralSubagentExecutorOptions, type EphemeralSubagentResult, type EphemeralSubagentRunInput, type EphemeralSubagentTimeout, } from "./ephemeral.ts";
|
|
5
5
|
export { createChildWorktree, finalizeChildWorktree, inspectIndexFlags, inspectWorktreeDirty, WorktreeSetupError, worktreeContextNote, type WorktreeDirtyInspection, type WorktreeInfo, type WorktreePayload, } from "./worktree.ts";
|
|
6
6
|
export { prepareExactReviewEvidence, REVIEW_MAX_PATCH_BYTES, REVIEW_MAX_PATHS, type PreparedReviewEvidence, type PrepareExactReviewEvidenceInput, } from "./review-evidence.ts";
|
|
7
7
|
export declare const ROLE_TOOL_POLICY_FLAG = "pi-subagent-role-tools";
|
package/docs/orchestration.md
CHANGED
|
@@ -165,7 +165,7 @@ const executorOptions = {
|
|
|
165
165
|
};
|
|
166
166
|
```
|
|
167
167
|
|
|
168
|
-
Concurrency is FIFO. `run` accepts optional `signal`, `onUpdate(text)`,
|
|
168
|
+
Concurrency is FIFO. `run` accepts optional `signal`, `onUpdate(text)`, `onTokens(number)`, and `onActivity(event)` callbacks plus required `prepare()`. A queued run receives its permit before `prepare` executes, so resource and route resolution can use the latest Pi state. Queued time does not consume child timeout. `maxConcurrency`, `idleMs`, and `maxMs` must be positive; `maxMs` must exceed `idleMs`.
|
|
169
169
|
|
|
170
170
|
The executor is **active-Pi-only**. It reuses the currently running Pi invocation and does not locate or support a standalone Node.js Pi installation. Once direct Pi exits, stdout/stderr drain normally until EOF; an escaped descendant retaining either stream is cut off after short output inactivity or a one-second hard deadline so it cannot retain the FIFO permit.
|
|
171
171
|
|
|
@@ -231,6 +231,18 @@ async function runRole(role, task, options = {}) {
|
|
|
231
231
|
|
|
232
232
|
`run` resolves to `EphemeralSubagentResult`. Both outcome variants contain `exitCode`, `output`, `stderr`, and optional `stopReason`, `errorMessage`, and `usage`. A launched child/model failure is a typed `{ outcome: "failure", ... }` result. Abort, timeout, spawn, protocol, preparation, and callback failures reject with `EphemeralSubagentError` and a stable `code`. Assistant `output` and `stderr` are bounded, and `usage` contains aggregate child usage when Pi supplies it.
|
|
233
233
|
|
|
234
|
+
### Activity callbacks
|
|
235
|
+
|
|
236
|
+
The optional `onActivity` callback receives structured activity events serially in child JSON-event order. This ordering applies only to `onActivity`; `onUpdate` and `onTokens` remain independent. A thrown or rejected activity callback fails the run with an `EphemeralSubagentError` whose code is `callback`.
|
|
237
|
+
|
|
238
|
+
| Event type | Fields |
|
|
239
|
+
| --- | --- |
|
|
240
|
+
| `tool_execution_start` | `toolCallId: string`, `toolName: string`, `path?: string` |
|
|
241
|
+
| `tool_execution_end` | `toolCallId: string`, `toolName: string` |
|
|
242
|
+
| `message_end` | none |
|
|
243
|
+
|
|
244
|
+
Activity text is limited to 4 KiB per field. An invalid `toolCallId` or `toolName`, or an oversized `path`, drops the event. A blank path or one containing C0/C1 terminal controls or Unicode line/paragraph separators is omitted from an otherwise valid start event.
|
|
245
|
+
|
|
234
246
|
The low-level executor does not interpret `Role.isolation`, discover resources, compose modes, create shared state, or promote child failure outcomes to tool errors. A direct caller that wants worktrees must call `createChildWorktree` after the permit, choose the returned `cwd`, call `finalizeChildWorktree` on every exit path, and preserve its recovery payload.
|
|
235
247
|
|
|
236
248
|
Generic managed Herdr exports (`managedSubagentWorkspaceId`, reconciliation helpers, `startManagedSubagent`, prompting/listing, and retirement) consume the same launch policy for durable workers. They intentionally contain no workflow prompts, semantic state, or retry policy.
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
inspectWorktreeDirty,
|
|
11
11
|
prepareExactReviewEvidence,
|
|
12
12
|
WorktreeSetupError,
|
|
13
|
+
type EphemeralSubagentActivityEvent,
|
|
13
14
|
type EphemeralSubagentExecutor,
|
|
14
15
|
type EphemeralSubagentResult,
|
|
15
16
|
type ResolvedRoleLaunch,
|
|
@@ -19,6 +20,7 @@ import {
|
|
|
19
20
|
import { Type, type Static } from "typebox";
|
|
20
21
|
import { Check } from "typebox/value";
|
|
21
22
|
import { runDelegation } from "./delegation.ts";
|
|
23
|
+
import { renderToolLines } from "./tool-render.ts";
|
|
22
24
|
|
|
23
25
|
const MAX_UNITS = 8;
|
|
24
26
|
const GIT_TIMEOUT_MS = 30_000;
|
|
@@ -129,6 +131,7 @@ export interface DelegateFlowRuntime {
|
|
|
129
131
|
ctx: ExtensionContext,
|
|
130
132
|
) => void;
|
|
131
133
|
updateWidgetTokens: (id: string, tokens: number) => void;
|
|
134
|
+
updateWidgetActivity: (id: string, event: EphemeralSubagentActivityEvent) => void;
|
|
132
135
|
finishWidget: (id: string, status: WidgetStatus) => void;
|
|
133
136
|
}
|
|
134
137
|
|
|
@@ -173,6 +176,35 @@ export function parseDelegateFlowContinue(value: unknown): Static<typeof Delegat
|
|
|
173
176
|
};
|
|
174
177
|
}
|
|
175
178
|
|
|
179
|
+
function flowCallLabel(args: { units?: unknown }): string {
|
|
180
|
+
const count = Array.isArray(args.units) ? args.units.length : 0;
|
|
181
|
+
return `delegate_flow · working: ${count} unit${count === 1 ? "" : "s"}`;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function flowResultLines(text: string): string[] {
|
|
185
|
+
const lines = text.split(/\r?\n/).filter((line) => line.trim());
|
|
186
|
+
const diagnosticHeader = lines.findIndex((line) => line.trim() === "Diagnostic:");
|
|
187
|
+
const diagnostic = diagnosticHeader === -1 ? undefined : lines[diagnosticHeader + 1];
|
|
188
|
+
const recoveryHeader = lines.findIndex((line) => line.trim() === "Retained Flow state:" || line.trim() === "Attempted allocations preserved without cleanup:");
|
|
189
|
+
const recovery = recoveryHeader === -1 || !lines[recoveryHeader + 1]?.trim().startsWith("- unit=")
|
|
190
|
+
? undefined
|
|
191
|
+
: lines[recoveryHeader + 1];
|
|
192
|
+
if (diagnostic === undefined) {
|
|
193
|
+
if (recovery === undefined) return lines;
|
|
194
|
+
const recoveryIndex = lines.indexOf(recovery);
|
|
195
|
+
return [lines[0]!, recovery, ...lines.filter((_, index) => index !== 0 && index !== recoveryIndex)];
|
|
196
|
+
}
|
|
197
|
+
const leading = lines.slice(0, Math.min(1, diagnosticHeader));
|
|
198
|
+
if (recovery !== undefined) return [...leading, `Diagnostic: ${diagnostic}`, recovery];
|
|
199
|
+
// Promote the first diagnostic ahead of the result cap.
|
|
200
|
+
return [
|
|
201
|
+
...leading,
|
|
202
|
+
`Diagnostic: ${diagnostic}`,
|
|
203
|
+
...lines.slice(leading.length, diagnosticHeader),
|
|
204
|
+
...lines.slice(diagnosticHeader + 2),
|
|
205
|
+
];
|
|
206
|
+
}
|
|
207
|
+
|
|
176
208
|
function errorText(error: unknown): string {
|
|
177
209
|
return capOutput(error instanceof Error ? error.message : String(error));
|
|
178
210
|
}
|
|
@@ -336,6 +368,7 @@ export function registerDelegateFlow(pi: ExtensionAPI, runtime: DelegateFlowRunt
|
|
|
336
368
|
role: Role,
|
|
337
369
|
modelClass: FlowModelClass,
|
|
338
370
|
task: string,
|
|
371
|
+
widgetTask: string,
|
|
339
372
|
cwd: string,
|
|
340
373
|
widgetId: string,
|
|
341
374
|
signal: AbortSignal | undefined,
|
|
@@ -348,13 +381,14 @@ export function registerDelegateFlow(pi: ExtensionAPI, runtime: DelegateFlowRunt
|
|
|
348
381
|
const result = await runDelegation(runtime.executor, {
|
|
349
382
|
signal,
|
|
350
383
|
onTokens: (tokens) => runtime.updateWidgetTokens(widgetId, tokens),
|
|
384
|
+
onActivity: (event) => runtime.updateWidgetActivity(widgetId, event),
|
|
351
385
|
prepare: async () => {
|
|
352
386
|
assertCurrent(flow);
|
|
353
387
|
const launch = runtime.resolveLaunch(role, modelClass, ctx);
|
|
354
388
|
if (launch.missingSkills.length) {
|
|
355
389
|
ctx.ui.notify(`Subagent role ${role.name} skipped unavailable Pi skills: ${launch.missingSkills.join(", ")}.`, "warning");
|
|
356
390
|
}
|
|
357
|
-
runtime.startWidget(widgetId, role.name, launch.model.id, launch.thinkingLevel,
|
|
391
|
+
runtime.startWidget(widgetId, role.name, launch.model.id, launch.thinkingLevel, widgetTask, ctx);
|
|
358
392
|
started = true;
|
|
359
393
|
return { launch, task, cwd };
|
|
360
394
|
},
|
|
@@ -464,6 +498,14 @@ export function registerDelegateFlow(pi: ExtensionAPI, runtime: DelegateFlowRunt
|
|
|
464
498
|
const lines = [
|
|
465
499
|
`Flow ${outcome}.`,
|
|
466
500
|
flow.completed.length ? `Completed units: ${flow.completed.map(({ id, noOp }) => `${JSON.stringify(id)}${noOp ? " (no-op)" : ""}`).join(", ")}` : "Completed units: none.",
|
|
501
|
+
...(flow.setupRecoveries.length ? [
|
|
502
|
+
"Attempted allocations preserved without cleanup:",
|
|
503
|
+
...flow.setupRecoveries.map((recovery) => `- unit=${JSON.stringify(recovery.id)} path=${JSON.stringify(recovery.path)} branch=${JSON.stringify(recovery.branch)} base=${recovery.base}`),
|
|
504
|
+
] : []),
|
|
505
|
+
...(retainedUnits.length ? [
|
|
506
|
+
"Retained Flow state:",
|
|
507
|
+
...retainedUnits.map((unit) => `- unit=${JSON.stringify(unit.id)} path=${JSON.stringify(unit.path)} branch=${JSON.stringify(unit.branch)} base=${unit.base} worktree=${unit.worktreeRetained} branch_ref=${unit.branchRetained}`),
|
|
508
|
+
] : []),
|
|
467
509
|
...(blocked ? [
|
|
468
510
|
`Blocked unit: ${JSON.stringify(blocked.unit.request.id)}.`,
|
|
469
511
|
`Classification: ${blocked.classification}.`,
|
|
@@ -473,14 +515,6 @@ export function registerDelegateFlow(pi: ExtensionAPI, runtime: DelegateFlowRunt
|
|
|
473
515
|
] : []),
|
|
474
516
|
...(failure ? [`Classification: ${failure.classification}.`, `Diagnostic:\n${failure.diagnostic}`] : []),
|
|
475
517
|
...(flow.warnings.length ? ["Warnings:", ...flow.warnings.map((warning) => `- ${warning}`)] : []),
|
|
476
|
-
...(flow.setupRecoveries.length ? [
|
|
477
|
-
"Attempted allocations preserved without cleanup:",
|
|
478
|
-
...flow.setupRecoveries.map((recovery) => `- unit=${JSON.stringify(recovery.id)} path=${JSON.stringify(recovery.path)} branch=${JSON.stringify(recovery.branch)} base=${recovery.base}`),
|
|
479
|
-
] : []),
|
|
480
|
-
...(retainedUnits.length ? [
|
|
481
|
-
"Retained Flow state:",
|
|
482
|
-
...retainedUnits.map((unit) => `- unit=${JSON.stringify(unit.id)} path=${JSON.stringify(unit.path)} branch=${JSON.stringify(unit.branch)} base=${unit.base} worktree=${unit.worktreeRetained} branch_ref=${unit.branchRetained}`),
|
|
483
|
-
] : []),
|
|
484
518
|
];
|
|
485
519
|
return {
|
|
486
520
|
content: [{ type: "text" as const, text: capOutput(lines.join("\n")) }],
|
|
@@ -672,6 +706,7 @@ export function registerDelegateFlow(pi: ExtensionAPI, runtime: DelegateFlowRunt
|
|
|
672
706
|
reviewer,
|
|
673
707
|
unit.modelClass,
|
|
674
708
|
reviewerTask(unit.request, reviewCriterion, { base: evidence.base, tip: evidence.tip, patchPath: evidence.patchPath }),
|
|
709
|
+
unit.request.task,
|
|
675
710
|
unit.worktree.cwd,
|
|
676
711
|
`${toolCallId}:flow:${flow.index}:review`,
|
|
677
712
|
signal,
|
|
@@ -752,6 +787,14 @@ export function registerDelegateFlow(pi: ExtensionAPI, runtime: DelegateFlowRunt
|
|
|
752
787
|
"If a Flow blocks, inspect its classification and call delegate_flow_continue once with explicit repair guidance; modelClass may replace that one repair's current class.",
|
|
753
788
|
],
|
|
754
789
|
parameters: DelegateFlowSchema,
|
|
790
|
+
renderShell: "self",
|
|
791
|
+
renderCall(args, theme, _context) {
|
|
792
|
+
return renderToolLines([theme.fg("toolTitle", flowCallLabel(args))], theme);
|
|
793
|
+
},
|
|
794
|
+
renderResult(result, _options, theme, _context) {
|
|
795
|
+
const text = result.content.find((part) => part.type === "text")?.text ?? "(no output)";
|
|
796
|
+
return renderToolLines(flowResultLines(text), theme);
|
|
797
|
+
},
|
|
755
798
|
prepareArguments: parseDelegateFlow,
|
|
756
799
|
async execute(toolCallId, params, signal, _onUpdate, ctx) {
|
|
757
800
|
const request = parseDelegateFlow(params);
|
|
@@ -817,6 +860,7 @@ export function registerDelegateFlow(pi: ExtensionAPI, runtime: DelegateFlowRunt
|
|
|
817
860
|
flow.implementer,
|
|
818
861
|
unit.modelClass,
|
|
819
862
|
implementerTask(unit.request),
|
|
863
|
+
unit.request.task,
|
|
820
864
|
unit.worktree.cwd,
|
|
821
865
|
`${toolCallId}:flow:${index}:implement`,
|
|
822
866
|
operationSignal,
|
|
@@ -848,6 +892,14 @@ export function registerDelegateFlow(pi: ExtensionAPI, runtime: DelegateFlowRunt
|
|
|
848
892
|
promptSnippet: "Repair and continue the blocked deterministic Flow",
|
|
849
893
|
promptGuidelines: ["Call delegate_flow_continue only after delegate_flow reports a repairable block, with explicit guidance addressing that block."],
|
|
850
894
|
parameters: DelegateFlowContinueSchema,
|
|
895
|
+
renderShell: "self",
|
|
896
|
+
renderCall(_args, theme, _context) {
|
|
897
|
+
return renderToolLines([theme.fg("toolTitle", "delegate_flow_continue · working: repair continuation")], theme);
|
|
898
|
+
},
|
|
899
|
+
renderResult(result, _options, theme, _context) {
|
|
900
|
+
const text = result.content.find((part) => part.type === "text")?.text ?? "(no output)";
|
|
901
|
+
return renderToolLines(flowResultLines(text), theme);
|
|
902
|
+
},
|
|
851
903
|
prepareArguments: parseDelegateFlowContinue,
|
|
852
904
|
async execute(toolCallId, params, signal, _onUpdate, ctx) {
|
|
853
905
|
const { guidance, modelClass } = parseDelegateFlowContinue(params);
|
|
@@ -870,6 +922,7 @@ export function registerDelegateFlow(pi: ExtensionAPI, runtime: DelegateFlowRunt
|
|
|
870
922
|
flow.implementer,
|
|
871
923
|
unit.modelClass,
|
|
872
924
|
repairTask(unit.request, blocked, guidance),
|
|
925
|
+
unit.request.task,
|
|
873
926
|
unit.worktree.cwd,
|
|
874
927
|
`${toolCallId}:flow:${flow.index}:repair`,
|
|
875
928
|
operationSignal,
|
package/extensions/subagent.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { basename } from "node:path";
|
|
1
2
|
import type { Usage } from "@earendil-works/pi-ai";
|
|
2
3
|
import { type ExtensionAPI, type ExtensionContext, type Theme } from "@earendil-works/pi-coding-agent";
|
|
3
|
-
import { type Component, type TUI,
|
|
4
|
+
import { type Component, type TUI, truncateToWidth } from "@earendil-works/pi-tui";
|
|
4
5
|
import {
|
|
5
6
|
availableTaskModels,
|
|
6
7
|
type ThinkingLevel,
|
|
@@ -21,6 +22,7 @@ import {
|
|
|
21
22
|
loadRoles,
|
|
22
23
|
resolveTaskRoute,
|
|
23
24
|
worktreeContextNote,
|
|
25
|
+
type EphemeralSubagentActivityEvent,
|
|
24
26
|
type EphemeralSubagentResult,
|
|
25
27
|
type EphemeralSubagentTimeout,
|
|
26
28
|
type Role,
|
|
@@ -29,6 +31,7 @@ import {
|
|
|
29
31
|
} from "@henryqw/pi-subagent";
|
|
30
32
|
import { DEFAULT_TIMEOUT_CONFIG, readSubagentConfig, type SubagentTimeoutConfig } from "./config.ts";
|
|
31
33
|
import { registerDelegateFlow } from "./delegate-flow.ts";
|
|
34
|
+
import { renderToolLines } from "./tool-render.ts";
|
|
32
35
|
import { runDelegation } from "./delegation.ts";
|
|
33
36
|
import {
|
|
34
37
|
formatBackgroundWorkflowResult,
|
|
@@ -55,6 +58,7 @@ const SUBAGENT_TASK = "pi-subagent/delegateTask";
|
|
|
55
58
|
const WIDGET_KEY = "subagent-status";
|
|
56
59
|
const WIDGET_INTERVAL_MS = 80;
|
|
57
60
|
const MAX_WIDGET_ROWS = 8;
|
|
61
|
+
export const MAX_WIDGET_ACTIVE_TOOLS = 8;
|
|
58
62
|
const DEFAULT_TIMEOUT_POLICY = {
|
|
59
63
|
idleMs: DEFAULT_TIMEOUT_CONFIG.idleMinutes * 60_000,
|
|
60
64
|
maxMs: DEFAULT_TIMEOUT_CONFIG.maxMinutes * 60_000,
|
|
@@ -71,6 +75,12 @@ export function resolveTimeoutPolicy(partial: SubagentTimeoutConfig | undefined)
|
|
|
71
75
|
};
|
|
72
76
|
}
|
|
73
77
|
type WidgetStatus = "working" | "success" | "failure" | "aborted";
|
|
78
|
+
type WidgetActiveTool = {
|
|
79
|
+
toolName: string;
|
|
80
|
+
path?: string;
|
|
81
|
+
startedAt: number;
|
|
82
|
+
order: number;
|
|
83
|
+
};
|
|
74
84
|
type WidgetItem = {
|
|
75
85
|
role: string;
|
|
76
86
|
model: string;
|
|
@@ -80,6 +90,11 @@ type WidgetItem = {
|
|
|
80
90
|
startedAt: number;
|
|
81
91
|
status: WidgetStatus;
|
|
82
92
|
finishedAt?: number;
|
|
93
|
+
completedAssistantTurns: number;
|
|
94
|
+
startedToolCount: number;
|
|
95
|
+
activeTools: Map<string, WidgetActiveTool>;
|
|
96
|
+
activeToolId?: string;
|
|
97
|
+
activityOrder: number;
|
|
83
98
|
};
|
|
84
99
|
|
|
85
100
|
function taskSummary(task: string): string {
|
|
@@ -111,6 +126,34 @@ function statusLabel(status: WidgetStatus): string {
|
|
|
111
126
|
}
|
|
112
127
|
}
|
|
113
128
|
|
|
129
|
+
function activityLabel(item: WidgetItem, now: number): string {
|
|
130
|
+
if (item.status === "success") return "Done";
|
|
131
|
+
if (item.status === "failure") return "Failed";
|
|
132
|
+
if (item.status === "aborted") return "Stopped";
|
|
133
|
+
const activeTool = item.activeToolId === undefined ? undefined : item.activeTools.get(item.activeToolId);
|
|
134
|
+
if (!activeTool) return "thinking…";
|
|
135
|
+
return [
|
|
136
|
+
activeTool.toolName,
|
|
137
|
+
formatDuration(now - activeTool.startedAt),
|
|
138
|
+
...(activeTool.path === undefined ? [] : [activeTool.path]),
|
|
139
|
+
].join(" · ");
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function activityMetrics(item: WidgetItem, now: number): string {
|
|
143
|
+
return [
|
|
144
|
+
...(item.completedAssistantTurns === 0
|
|
145
|
+
? []
|
|
146
|
+
: [`${item.completedAssistantTurns} turn${item.completedAssistantTurns === 1 ? "" : "s"}`]),
|
|
147
|
+
...(item.startedToolCount === 0
|
|
148
|
+
? []
|
|
149
|
+
: [`${item.startedToolCount} tool${item.startedToolCount === 1 ? "" : "s"}`]),
|
|
150
|
+
item.model,
|
|
151
|
+
item.thinkingLevel,
|
|
152
|
+
`${formatTokens(item.tokens)} tok`,
|
|
153
|
+
formatDuration((item.finishedAt ?? now) - item.startedAt),
|
|
154
|
+
].join(" · ");
|
|
155
|
+
}
|
|
156
|
+
|
|
114
157
|
function isWorkflowTransportDetails(value: unknown): value is WorkflowTransportDetails {
|
|
115
158
|
const isRecord = (candidate: unknown): candidate is Record<string, unknown> => typeof candidate === "object" && candidate !== null && !Array.isArray(candidate);
|
|
116
159
|
const isOptionalString = (candidate: unknown) => candidate === undefined || typeof candidate === "string";
|
|
@@ -134,18 +177,34 @@ function isWorkflowTransportDetails(value: unknown): value is WorkflowTransportD
|
|
|
134
177
|
&& entries.every((entry, index) => index === 0 || (entry as { index: number }).index > (entries[index - 1] as { index: number }).index);
|
|
135
178
|
}
|
|
136
179
|
|
|
137
|
-
function
|
|
180
|
+
function workflowCallLabel(args: { tasks?: unknown; chain?: unknown }): string {
|
|
181
|
+
if (Array.isArray(args.chain)) return `delegate_task · working: chain · ${args.chain.length} task${args.chain.length === 1 ? "" : "s"}`;
|
|
182
|
+
if (Array.isArray(args.tasks)) return `delegate_task · working: parallel · ${args.tasks.length} task${args.tasks.length === 1 ? "" : "s"}`;
|
|
183
|
+
return "delegate_task · working: single · 1 task";
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function workflowResultLines(details: WorkflowTransportDetails, theme: Theme): string[] {
|
|
138
187
|
if (details.entries.some(({ status }) => status === "pending" || status === "running")) return [];
|
|
139
|
-
|
|
140
|
-
|
|
188
|
+
const entries = details.entries.filter(({ status }) => status !== "skipped");
|
|
189
|
+
const withRecovery = (entry: typeof entries[0]) => entry.worktree && !entry.worktree.pruned;
|
|
190
|
+
const isTerminalFailure = (entry: typeof entries[0]) => entry.status === "failed" || entry.status === "rejected";
|
|
191
|
+
const sorted = [...entries].sort((a, b) => {
|
|
192
|
+
const aFailure = isTerminalFailure(a);
|
|
193
|
+
const bFailure = isTerminalFailure(b);
|
|
194
|
+
if (aFailure !== bFailure) return aFailure ? -1 : 1;
|
|
195
|
+
const aRecovery = !aFailure && withRecovery(a);
|
|
196
|
+
const bRecovery = !bFailure && withRecovery(b);
|
|
197
|
+
if (aRecovery !== bRecovery) return aRecovery ? -1 : 1;
|
|
198
|
+
return 0;
|
|
199
|
+
});
|
|
200
|
+
return sorted.map((entry) => {
|
|
141
201
|
const summary = entry.summary || "(no output)";
|
|
142
202
|
const text = details.mode === "single" ? summary : `${entry.role}: ${summary}`;
|
|
143
203
|
const style = entry.status === "failed" || entry.status === "rejected" ? "error" : "text";
|
|
144
|
-
const recovery = entry
|
|
145
|
-
return
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
];
|
|
204
|
+
const recovery = withRecovery(entry) ? `Recovery: ${entry.worktree!.path}` : undefined;
|
|
205
|
+
return recovery === undefined
|
|
206
|
+
? theme.fg(style, text)
|
|
207
|
+
: `${theme.fg("warning", recovery)} · ${theme.fg(style, text)}`;
|
|
149
208
|
});
|
|
150
209
|
}
|
|
151
210
|
|
|
@@ -159,13 +218,15 @@ function renderWidgetRows(
|
|
|
159
218
|
const visible = items.slice(0, MAX_WIDGET_ROWS);
|
|
160
219
|
if (!visible.length) return [];
|
|
161
220
|
const indent = " ".repeat(Math.min(2, Math.max(0, width - 1)));
|
|
162
|
-
const contentWidth = Math.max(
|
|
221
|
+
const contentWidth = Math.max(0, width - indent.length);
|
|
163
222
|
const lines = visible.flatMap((item) => [
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
223
|
+
truncateToWidth(
|
|
224
|
+
`${statusGlyph(item.status, spinnerIndex, theme)} ${theme.fg("accent", item.role)} · ${statusLabel(item.status)} · ${theme.fg("text", item.task)}`,
|
|
225
|
+
width,
|
|
226
|
+
),
|
|
227
|
+
`${indent}${truncateToWidth(`${theme.fg("text", activityLabel(item, now))} · ${theme.fg("muted", activityMetrics(item, now))}`, contentWidth)}`,
|
|
167
228
|
]);
|
|
168
|
-
if (items.length > visible.length) lines.push(
|
|
229
|
+
if (items.length > visible.length) lines.push(truncateToWidth(theme.fg("muted", `… ${items.length - visible.length} more`), width));
|
|
169
230
|
return lines;
|
|
170
231
|
}
|
|
171
232
|
|
|
@@ -311,6 +372,10 @@ export default function subagentExtension(
|
|
|
311
372
|
tokens: 0,
|
|
312
373
|
startedAt: Date.now(),
|
|
313
374
|
status: "working",
|
|
375
|
+
completedAssistantTurns: 0,
|
|
376
|
+
startedToolCount: 0,
|
|
377
|
+
activeTools: new Map(),
|
|
378
|
+
activityOrder: 0,
|
|
314
379
|
});
|
|
315
380
|
startWidgetTimer();
|
|
316
381
|
requestWidgetRender();
|
|
@@ -323,11 +388,55 @@ export default function subagentExtension(
|
|
|
323
388
|
requestWidgetRender();
|
|
324
389
|
};
|
|
325
390
|
|
|
391
|
+
const updateWidgetActivity = (id: string, event: EphemeralSubagentActivityEvent) => {
|
|
392
|
+
const item = widgetItems.get(id);
|
|
393
|
+
if (!item || item.status !== "working") return;
|
|
394
|
+
switch (event.type) {
|
|
395
|
+
case "tool_execution_start": {
|
|
396
|
+
if (item.activeTools.has(event.toolCallId)) break;
|
|
397
|
+
if (item.activeTools.size >= MAX_WIDGET_ACTIVE_TOOLS) {
|
|
398
|
+
let oldest: [string, WidgetActiveTool] | undefined;
|
|
399
|
+
for (const candidate of item.activeTools) {
|
|
400
|
+
if (!oldest || candidate[1].order < oldest[1].order) oldest = candidate;
|
|
401
|
+
}
|
|
402
|
+
if (oldest) item.activeTools.delete(oldest[0]);
|
|
403
|
+
}
|
|
404
|
+
const path = event.path === undefined ? undefined : basename(event.path);
|
|
405
|
+
item.startedToolCount += 1;
|
|
406
|
+
item.activeTools.set(event.toolCallId, {
|
|
407
|
+
toolName: event.toolName,
|
|
408
|
+
...(path ? { path } : {}),
|
|
409
|
+
startedAt: Date.now(),
|
|
410
|
+
order: ++item.activityOrder,
|
|
411
|
+
});
|
|
412
|
+
item.activeToolId = event.toolCallId;
|
|
413
|
+
break;
|
|
414
|
+
}
|
|
415
|
+
case "tool_execution_end": {
|
|
416
|
+
item.activeTools.delete(event.toolCallId);
|
|
417
|
+
if (item.activeToolId === event.toolCallId) {
|
|
418
|
+
let latest: [string, WidgetActiveTool] | undefined;
|
|
419
|
+
for (const candidate of item.activeTools) {
|
|
420
|
+
if (!latest || candidate[1].order > latest[1].order) latest = candidate;
|
|
421
|
+
}
|
|
422
|
+
item.activeToolId = latest?.[0];
|
|
423
|
+
}
|
|
424
|
+
break;
|
|
425
|
+
}
|
|
426
|
+
case "message_end":
|
|
427
|
+
item.completedAssistantTurns += 1;
|
|
428
|
+
break;
|
|
429
|
+
}
|
|
430
|
+
requestWidgetRender();
|
|
431
|
+
};
|
|
432
|
+
|
|
326
433
|
const finishWidgetItem = (id: string, status: Exclude<WidgetStatus, "working">) => {
|
|
327
434
|
const item = widgetItems.get(id);
|
|
328
435
|
if (!item) return;
|
|
329
436
|
item.status = status;
|
|
330
437
|
item.finishedAt = Date.now();
|
|
438
|
+
item.activeTools.clear();
|
|
439
|
+
item.activeToolId = undefined;
|
|
331
440
|
if (![...widgetItems.values()].some(({ status }) => status === "working")) stopWidgetTimer();
|
|
332
441
|
requestWidgetRender();
|
|
333
442
|
};
|
|
@@ -454,6 +563,7 @@ export default function subagentExtension(
|
|
|
454
563
|
},
|
|
455
564
|
startWidget: startWidgetItem,
|
|
456
565
|
updateWidgetTokens,
|
|
566
|
+
updateWidgetActivity,
|
|
457
567
|
finishWidget: finishWidgetItem,
|
|
458
568
|
});
|
|
459
569
|
|
|
@@ -470,19 +580,19 @@ export default function subagentExtension(
|
|
|
470
580
|
"delegate_task background applies to the whole selected workflow and returns before results exist; use it only when the user explicitly asks for non-blocking work.",
|
|
471
581
|
],
|
|
472
582
|
parameters: WorkflowSchema,
|
|
473
|
-
|
|
583
|
+
renderShell: "self",
|
|
584
|
+
renderCall(args, theme, _context) {
|
|
585
|
+
return renderToolLines([theme.fg("toolTitle", workflowCallLabel(args))], theme);
|
|
586
|
+
},
|
|
587
|
+
renderResult(result, { isPartial }, theme, _context) {
|
|
588
|
+
if (isPartial) return renderToolLines([], theme);
|
|
474
589
|
const details = result.details;
|
|
475
|
-
if (isWorkflowTransportDetails(details))
|
|
476
|
-
return {
|
|
477
|
-
invalidate() {},
|
|
478
|
-
render: (width) => renderWorkflowResult(details, width, theme),
|
|
479
|
-
};
|
|
480
|
-
}
|
|
590
|
+
if (isWorkflowTransportDetails(details)) return renderToolLines(workflowResultLines(details, theme), theme);
|
|
481
591
|
if (typeof details === "object" && details !== null && (details as { background?: unknown }).background === true) {
|
|
482
|
-
return
|
|
592
|
+
return renderToolLines([theme.fg("muted", "Background workflow accepted.")], theme);
|
|
483
593
|
}
|
|
484
594
|
const text = result.content.find((part) => part.type === "text")?.text ?? "(no output)";
|
|
485
|
-
return
|
|
595
|
+
return renderToolLines([theme.fg("muted", text)], theme);
|
|
486
596
|
},
|
|
487
597
|
prepareArguments(args) {
|
|
488
598
|
try {
|
|
@@ -593,6 +703,7 @@ export default function subagentExtension(
|
|
|
593
703
|
emitUpdate(emitToolUpdates);
|
|
594
704
|
},
|
|
595
705
|
onTokens: (tokens) => updateWidgetTokens(entry.id, tokens),
|
|
706
|
+
onActivity: (event) => updateWidgetActivity(entry.id, event),
|
|
596
707
|
prepare: async () => {
|
|
597
708
|
// Route and effective Role resources resolve only after this entry's
|
|
598
709
|
// shared executor permit, before isolated state is created.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { type Component, truncateToWidth } from "@earendil-works/pi-tui";
|
|
3
|
+
|
|
4
|
+
export const MAX_RENDERED_RESULT_LINES = 3;
|
|
5
|
+
|
|
6
|
+
export function renderToolLines(lines: readonly string[], theme: Theme): Component {
|
|
7
|
+
return {
|
|
8
|
+
invalidate() {},
|
|
9
|
+
render: (width) => {
|
|
10
|
+
const shown = lines.length > MAX_RENDERED_RESULT_LINES
|
|
11
|
+
? [...lines.slice(0, MAX_RENDERED_RESULT_LINES - 1), theme.fg("muted", `… ${lines.length - MAX_RENDERED_RESULT_LINES + 1} more`)]
|
|
12
|
+
: lines;
|
|
13
|
+
return shown.map((line) => truncateToWidth(line.replace(/[\r\n]+/g, " "), width));
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
}
|