pi-subagents 0.46.0 → 0.47.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 +23 -0
- package/docs/agents.md +1 -1
- package/docs/configuration.md +14 -6
- package/docs/extension-api.md +1 -1
- package/docs/missions.md +5 -3
- package/docs/models.md +3 -1
- package/docs/observability.md +3 -3
- package/docs/tool-reference.md +2 -2
- package/docs/watchdog.md +1 -1
- package/package.json +1 -1
- package/skills/pi-subagents/references/execution-controls.md +4 -4
- package/skills/pi-subagents/references/management-authoring-rpc.md +1 -1
- package/src/extension/config.ts +3 -0
- package/src/extension/fanout-child.ts +5 -4
- package/src/extension/index.ts +30 -3
- package/src/extension/rpc.ts +3 -6
- package/src/extension/schemas.ts +25 -5
- package/src/extension/tool-description.ts +26 -8
- package/src/inspectors/herdr/project-panes.ts +2 -1
- package/src/missions/store.ts +2 -1
- package/src/missions/workflow-state.ts +19 -13
- package/src/runs/background/async-execution.ts +10 -5
- package/src/runs/background/async-job-tracker.ts +15 -0
- package/src/runs/background/async-resume.ts +19 -3
- package/src/runs/background/async-status.ts +6 -1
- package/src/runs/background/control-channel.ts +36 -0
- package/src/runs/background/result-watcher.ts +16 -2
- package/src/runs/background/scheduled-runs.ts +2 -1
- package/src/runs/background/stale-run-reconciler.ts +2 -21
- package/src/runs/background/subagent-runner.ts +47 -6
- package/src/runs/foreground/async-steering-action.ts +1 -1
- package/src/runs/foreground/chain-execution.ts +3 -0
- package/src/runs/foreground/execution.ts +3 -0
- package/src/runs/foreground/subagent-executor.ts +95 -12
- package/src/runs/foreground/workflow-foreground-steering.ts +187 -0
- package/src/runs/shared/dynamic-fanout.ts +1 -1
- package/src/runs/shared/model-fallback.ts +8 -4
- package/src/runs/shared/model-scope.ts +12 -2
- package/src/runs/shared/parallel-utils.ts +1 -0
- package/src/runs/shared/worktree.ts +3 -2
- package/src/shared/artifacts.ts +14 -14
- package/src/shared/display-text.ts +100 -0
- package/src/shared/formatters.ts +4 -6
- package/src/shared/settings.ts +15 -2
- package/src/shared/types.ts +11 -1
- package/src/shared/utils.ts +43 -33
- package/src/slash/slash-commands.ts +3 -1
- package/src/tui/fleet-status.ts +14 -10
- package/src/tui/render.ts +30 -26
- package/src/watchdog/change-signature.ts +4 -3
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
function codePointWidth(codePoint: number): 1 | 2 {
|
|
2
|
+
return codePoint > 0xffff ? 2 : 1;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
function isWhitespaceOrControl(codePoint: number): boolean {
|
|
6
|
+
if (codePoint <= 0x20 || (codePoint >= 0x7f && codePoint <= 0x9f)) return true;
|
|
7
|
+
if (codePoint >= 0xd800 && codePoint <= 0xdfff) return true;
|
|
8
|
+
return String.fromCodePoint(codePoint).trim().length === 0;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function consumeControlString(value: string, index: number, osc: boolean): number {
|
|
12
|
+
while (index < value.length) {
|
|
13
|
+
const codePoint = value.codePointAt(index)!;
|
|
14
|
+
const width = codePointWidth(codePoint);
|
|
15
|
+
if (osc && codePoint === 0x07) return index + width;
|
|
16
|
+
if (codePoint === 0x9c) return index + width;
|
|
17
|
+
if (codePoint === 0x1b && value.charCodeAt(index + 1) === 0x5c) return index + 2;
|
|
18
|
+
index += width;
|
|
19
|
+
}
|
|
20
|
+
return value.length;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function consumeCsi(value: string, index: number): number {
|
|
24
|
+
while (index < value.length) {
|
|
25
|
+
const codePoint = value.codePointAt(index)!;
|
|
26
|
+
const width = codePointWidth(codePoint);
|
|
27
|
+
if (codePoint >= 0x40 && codePoint <= 0x7e) return index + width;
|
|
28
|
+
index += width;
|
|
29
|
+
}
|
|
30
|
+
return value.length;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function sanitizeDisplayText(value: string): string {
|
|
34
|
+
const output: string[] = [];
|
|
35
|
+
let pendingSpace = false;
|
|
36
|
+
|
|
37
|
+
const appendSpace = (): void => {
|
|
38
|
+
if (output.length > 0) pendingSpace = true;
|
|
39
|
+
};
|
|
40
|
+
const appendText = (text: string): void => {
|
|
41
|
+
if (pendingSpace) output.push(" ");
|
|
42
|
+
output.push(text);
|
|
43
|
+
pendingSpace = false;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
for (let index = 0; index < value.length;) {
|
|
47
|
+
const codePoint = value.codePointAt(index)!;
|
|
48
|
+
const width = codePointWidth(codePoint);
|
|
49
|
+
|
|
50
|
+
if (codePoint === 0x1b) {
|
|
51
|
+
const next = value.charCodeAt(index + 1);
|
|
52
|
+
appendSpace();
|
|
53
|
+
if (next === 0x5b) {
|
|
54
|
+
index = consumeCsi(value, index + 2);
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (next === 0x5d || next === 0x50 || next === 0x58 || next === 0x5e || next === 0x5f) {
|
|
58
|
+
index = consumeControlString(value, index + 2, next === 0x5d);
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
index += next ? 2 : 1;
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (codePoint === 0x9b) {
|
|
66
|
+
appendSpace();
|
|
67
|
+
index = consumeCsi(value, index + width);
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (codePoint === 0x90 || codePoint === 0x98 || codePoint === 0x9d || codePoint === 0x9e || codePoint === 0x9f) {
|
|
71
|
+
appendSpace();
|
|
72
|
+
index = consumeControlString(value, index + width, codePoint === 0x9d);
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (isWhitespaceOrControl(codePoint)) appendSpace();
|
|
77
|
+
else appendText(String.fromCodePoint(codePoint));
|
|
78
|
+
index += width;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return output.join("");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function truncateDisplayText(value: string, maxLength: number): string {
|
|
85
|
+
if (maxLength <= 0) return "";
|
|
86
|
+
if (value.length <= maxLength) return value;
|
|
87
|
+
let output = "";
|
|
88
|
+
for (const char of value) {
|
|
89
|
+
if (output.length + char.length > maxLength) break;
|
|
90
|
+
output += char;
|
|
91
|
+
}
|
|
92
|
+
return output;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function previewDisplayText(value: string, maxLength: number): string {
|
|
96
|
+
const normalized = sanitizeDisplayText(value);
|
|
97
|
+
if (normalized.length <= maxLength) return normalized;
|
|
98
|
+
if (maxLength <= 3) return truncateDisplayText(normalized, maxLength);
|
|
99
|
+
return `${truncateDisplayText(normalized, maxLength - 3)}...`;
|
|
100
|
+
}
|
package/src/shared/formatters.ts
CHANGED
|
@@ -7,6 +7,7 @@ import * as path from "node:path";
|
|
|
7
7
|
import type { Usage, SingleResult } from "./types.ts";
|
|
8
8
|
import type { ChainStep } from "./settings.ts";
|
|
9
9
|
import { isDynamicParallelStep, isParallelStep } from "./settings.ts";
|
|
10
|
+
import { previewDisplayText, sanitizeDisplayText } from "./display-text.ts";
|
|
10
11
|
import { splitKnownThinkingSuffix, THINKING_LEVELS } from "./model-info.ts";
|
|
11
12
|
|
|
12
13
|
/**
|
|
@@ -100,8 +101,7 @@ export function formatToolCall(name: string, args: Record<string, unknown>, expa
|
|
|
100
101
|
switch (name) {
|
|
101
102
|
case "bash": {
|
|
102
103
|
const command = typeof args.command === "string" ? args.command : "";
|
|
103
|
-
|
|
104
|
-
return `$ ${command.slice(0, maxLength)}${command.length > maxLength ? "..." : ""}`;
|
|
104
|
+
return `$ ${previewDisplayText(command, expanded ? 240 : 60)}`;
|
|
105
105
|
}
|
|
106
106
|
case "read":
|
|
107
107
|
case "write":
|
|
@@ -111,12 +111,10 @@ export function formatToolCall(name: string, args: Record<string, unknown>, expa
|
|
|
111
111
|
: typeof args.file_path === "string"
|
|
112
112
|
? args.file_path
|
|
113
113
|
: "";
|
|
114
|
-
return `${name} ${shortenPath(target)}`;
|
|
114
|
+
return `${name} ${sanitizeDisplayText(shortenPath(target))}`;
|
|
115
115
|
}
|
|
116
116
|
default: {
|
|
117
|
-
|
|
118
|
-
const maxLength = expanded ? 160 : 40;
|
|
119
|
-
return `${name} ${s.slice(0, maxLength)}${s.length > maxLength ? "..." : ""}`;
|
|
117
|
+
return `${name} ${previewDisplayText(JSON.stringify(args), expanded ? 160 : 40)}`;
|
|
120
118
|
}
|
|
121
119
|
}
|
|
122
120
|
}
|
package/src/shared/settings.ts
CHANGED
|
@@ -353,6 +353,18 @@ export function resolveChainPath(filePath: string, chainDir: string): string {
|
|
|
353
353
|
return path.isAbsolute(expanded) ? expanded : path.join(chainDir, expanded);
|
|
354
354
|
}
|
|
355
355
|
|
|
356
|
+
export function resolveExistingReadInstructionPaths(reads: readonly string[], instructionCwd: string, existenceCwd = instructionCwd): string[] {
|
|
357
|
+
return reads.flatMap((filePath) => {
|
|
358
|
+
const instructionPath = resolveChainPath(filePath, instructionCwd);
|
|
359
|
+
const existencePath = resolveChainPath(filePath, existenceCwd);
|
|
360
|
+
return fs.existsSync(existencePath) ? [instructionPath] : [];
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
export function resolveExistingReadPaths(reads: readonly string[], cwd: string): string[] {
|
|
365
|
+
return resolveExistingReadInstructionPaths(reads, cwd);
|
|
366
|
+
}
|
|
367
|
+
|
|
356
368
|
/**
|
|
357
369
|
* Build chain instructions from resolved behavior.
|
|
358
370
|
* These are appended to the task to tell the agent what to read/write.
|
|
@@ -367,14 +379,15 @@ export function buildChainInstructions(
|
|
|
367
379
|
chainDir: string,
|
|
368
380
|
isFirstProgressAgent: boolean,
|
|
369
381
|
previousSummary?: string,
|
|
382
|
+
readExistenceDir = chainDir,
|
|
370
383
|
): { prefix: string; suffix: string } {
|
|
371
384
|
const prefixParts: string[] = [];
|
|
372
385
|
const suffixParts: string[] = [];
|
|
373
386
|
|
|
374
387
|
// READS - prepend to override any hardcoded filenames in task text
|
|
375
388
|
if (behavior.reads && behavior.reads.length > 0) {
|
|
376
|
-
const files = behavior.reads
|
|
377
|
-
prefixParts.push(`[Read from: ${files.join(", ")}]`);
|
|
389
|
+
const files = resolveExistingReadInstructionPaths(behavior.reads, chainDir, readExistenceDir);
|
|
390
|
+
if (files.length > 0) prefixParts.push(`[Read from: ${files.join(", ")}]`);
|
|
378
391
|
}
|
|
379
392
|
|
|
380
393
|
// OUTPUT - prepend so agent knows where to write
|
package/src/shared/types.ts
CHANGED
|
@@ -1545,6 +1545,8 @@ export interface ForegroundRunControl {
|
|
|
1545
1545
|
parentWorkflowRunId?: string;
|
|
1546
1546
|
/** Stable workflow lane key for this live foreground child. */
|
|
1547
1547
|
workflowKey?: string;
|
|
1548
|
+
/** Private control root used to steer a live workflow-owned child. */
|
|
1549
|
+
workflowSteeringDir?: string;
|
|
1548
1550
|
/** Originating parent session; required for public fleet projection. */
|
|
1549
1551
|
sessionId?: string;
|
|
1550
1552
|
mode: SubagentRunMode;
|
|
@@ -1614,6 +1616,8 @@ export interface SubagentState {
|
|
|
1614
1616
|
fleetJobs?: Map<string, AsyncJobState>;
|
|
1615
1617
|
/** Suppress dynamic status widgets while the fleet overlay owns the viewport. */
|
|
1616
1618
|
fleetInspectorOpen?: boolean;
|
|
1619
|
+
/** Temporarily suppress dynamic widgets while Pi compacts the session. */
|
|
1620
|
+
widgetsSuspended?: boolean;
|
|
1617
1621
|
foregroundRuns?: Map<string, ForegroundResumeRun>;
|
|
1618
1622
|
foregroundControls: Map<string, ForegroundRunControl>;
|
|
1619
1623
|
lastForegroundControlId: string | null;
|
|
@@ -1680,6 +1684,10 @@ export interface RunSyncOptions {
|
|
|
1680
1684
|
permissions?: import("../runs/shared/permissions.ts").PermissionConfig;
|
|
1681
1685
|
/** Session id of the direct parent session for permission-system ask forwarding. */
|
|
1682
1686
|
parentSessionId?: string;
|
|
1687
|
+
/** Private prompt-runtime steering transport for workflow-owned foreground children. */
|
|
1688
|
+
steerInboxDir?: string;
|
|
1689
|
+
steerCapabilityPath?: string;
|
|
1690
|
+
steerAckDir?: string;
|
|
1683
1691
|
/** Resolved launch context for this child. */
|
|
1684
1692
|
context?: "fresh" | "fork";
|
|
1685
1693
|
cwd?: string;
|
|
@@ -1819,6 +1827,8 @@ export interface ExtensionConfig {
|
|
|
1819
1827
|
asyncWidget?: boolean;
|
|
1820
1828
|
/** Tool description variant registered for the parent-facing subagent tool. Defaults to full. */
|
|
1821
1829
|
toolDescriptionMode?: ToolDescriptionMode;
|
|
1830
|
+
/** Include legacy append-step and checkpoint controls in the registered tool schema. Defaults to false. */
|
|
1831
|
+
legacyChainControls?: boolean;
|
|
1822
1832
|
/** Inline chat rendering for the subagent tool. Defaults to rich. */
|
|
1823
1833
|
inlineToolDisplay?: InlineToolDisplay;
|
|
1824
1834
|
forceTopLevelAsync?: boolean;
|
|
@@ -1842,7 +1852,7 @@ export interface ExtensionConfig {
|
|
|
1842
1852
|
worktreeSetupHook?: string;
|
|
1843
1853
|
worktreeSetupHookTimeoutMs?: number;
|
|
1844
1854
|
worktreeBaseDir?: string;
|
|
1845
|
-
/** Where to store subagent artifact files. Defaults to "project" (cwd/.pi
|
|
1855
|
+
/** Where to store subagent artifact files. Defaults to "project" (cwd/.pi/subagents). Set to "session" for pi session dir, or "temp" for OS temp. */
|
|
1846
1856
|
artifactDir?: ArtifactDirPreference;
|
|
1847
1857
|
intercomBridge?: IntercomBridgeConfig;
|
|
1848
1858
|
proactiveSkillSubagents?: ProactiveSkillSubagentsConfig | false;
|
package/src/shared/utils.ts
CHANGED
|
@@ -6,6 +6,7 @@ import * as fs from "node:fs";
|
|
|
6
6
|
import * as os from "node:os";
|
|
7
7
|
import * as path from "node:path";
|
|
8
8
|
import type { Message } from "@earendil-works/pi-ai";
|
|
9
|
+
import { previewDisplayText, sanitizeDisplayText, truncateDisplayText } from "./display-text.ts";
|
|
9
10
|
import { formatToolCall } from "./formatters.ts";
|
|
10
11
|
import type { AgentProgress, AsyncStatus, Details, DisplayItem, ErrorInfo, NestedRunSummary, SingleResult, ToolCallSummary, Usage } from "./types.ts";
|
|
11
12
|
|
|
@@ -101,6 +102,23 @@ export function getAgentDir(): string {
|
|
|
101
102
|
|
|
102
103
|
const statusCache = new Map<string, { mtime: number; ctime: number; size: number; ino: number; status: AsyncStatus }>();
|
|
103
104
|
|
|
105
|
+
export function pruneStatusCacheForAsyncRoot(asyncDirRoot: string, runIds: Iterable<string>): number {
|
|
106
|
+
const root = path.resolve(asyncDirRoot);
|
|
107
|
+
const currentStatusPaths = new Set(
|
|
108
|
+
Array.from(runIds, (runId) => path.resolve(root, runId, "status.json")),
|
|
109
|
+
);
|
|
110
|
+
let removed = 0;
|
|
111
|
+
for (const statusPath of statusCache.keys()) {
|
|
112
|
+
const resolved = path.resolve(statusPath);
|
|
113
|
+
const relative = path.relative(root, resolved);
|
|
114
|
+
if (relative && !relative.startsWith("..") && !path.isAbsolute(relative) && !currentStatusPaths.has(resolved)) {
|
|
115
|
+
statusCache.delete(statusPath);
|
|
116
|
+
removed++;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return removed;
|
|
120
|
+
}
|
|
121
|
+
|
|
104
122
|
function getErrorMessage(error: unknown): string {
|
|
105
123
|
return error instanceof Error ? error.message : String(error);
|
|
106
124
|
}
|
|
@@ -127,7 +145,10 @@ export function readStatus(asyncDir: string): AsyncStatus | null {
|
|
|
127
145
|
try {
|
|
128
146
|
stat = fs.statSync(statusPath);
|
|
129
147
|
} catch (error) {
|
|
130
|
-
if (isNotFoundError(error))
|
|
148
|
+
if (isNotFoundError(error)) {
|
|
149
|
+
statusCache.delete(statusPath);
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
131
152
|
throw new Error(`Failed to inspect async status file '${statusPath}': ${getErrorMessage(error)}`, {
|
|
132
153
|
cause: error instanceof Error ? error : undefined,
|
|
133
154
|
});
|
|
@@ -148,7 +169,10 @@ export function readStatus(asyncDir: string): AsyncStatus | null {
|
|
|
148
169
|
try {
|
|
149
170
|
content = fs.readFileSync(statusPath, "utf-8");
|
|
150
171
|
} catch (error) {
|
|
151
|
-
if (isNotFoundError(error))
|
|
172
|
+
if (isNotFoundError(error)) {
|
|
173
|
+
statusCache.delete(statusPath);
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
152
176
|
throw new Error(`Failed to read async status file '${statusPath}': ${getErrorMessage(error)}`, {
|
|
153
177
|
cause: error instanceof Error ? error : undefined,
|
|
154
178
|
});
|
|
@@ -170,10 +194,6 @@ export function readStatus(asyncDir: string): AsyncStatus | null {
|
|
|
170
194
|
ino: stat.ino,
|
|
171
195
|
status,
|
|
172
196
|
});
|
|
173
|
-
if (statusCache.size > 50) {
|
|
174
|
-
const firstKey = statusCache.keys().next().value;
|
|
175
|
-
if (firstKey) statusCache.delete(firstKey);
|
|
176
|
-
}
|
|
177
197
|
return status;
|
|
178
198
|
}
|
|
179
199
|
|
|
@@ -522,11 +542,8 @@ export function detectSubagentError(messages: Message[]): ErrorInfo {
|
|
|
522
542
|
* Extract a preview of tool arguments for display
|
|
523
543
|
*/
|
|
524
544
|
export function extractToolArgsPreview(args: Record<string, unknown>): string {
|
|
525
|
-
const truncatePreview = (value: string, maxLength: number): string =>
|
|
526
|
-
value.length > maxLength ? `${value.slice(0, maxLength - 3)}...` : value;
|
|
527
|
-
|
|
528
545
|
const stringifyPreviewValue = (value: unknown): string | undefined => {
|
|
529
|
-
if (typeof value === "string" && value.trim().length > 0) return value;
|
|
546
|
+
if (typeof value === "string" && value.trim().length > 0) return sanitizeDisplayText(value);
|
|
530
547
|
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
531
548
|
return undefined;
|
|
532
549
|
};
|
|
@@ -539,39 +556,32 @@ export function extractToolArgsPreview(args: Record<string, unknown>): string {
|
|
|
539
556
|
return `${first}${suffix}`;
|
|
540
557
|
};
|
|
541
558
|
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
const
|
|
545
|
-
|
|
546
|
-
return `${server}${args.tool}${toolArgs}`;
|
|
559
|
+
if (typeof args.tool === "string") {
|
|
560
|
+
const server = typeof args.server === "string" ? `${sanitizeDisplayText(args.server)}/` : "";
|
|
561
|
+
const toolArgs = typeof args.args === "string" ? ` ${truncateDisplayText(sanitizeDisplayText(args.args), 40)}` : "";
|
|
562
|
+
return sanitizeDisplayText(`${server}${args.tool}${toolArgs}`);
|
|
547
563
|
}
|
|
548
564
|
|
|
549
565
|
const queriesPreview = previewArray(args.queries);
|
|
550
|
-
if (queriesPreview) return
|
|
551
|
-
if (typeof args.query === "string" && args.query.trim().length > 0) return
|
|
552
|
-
if (typeof args.workflow === "string" && args.workflow.trim().length > 0) return `workflow=${
|
|
566
|
+
if (queriesPreview) return previewDisplayText(queriesPreview, 60);
|
|
567
|
+
if (typeof args.query === "string" && args.query.trim().length > 0) return previewDisplayText(args.query, 60);
|
|
568
|
+
if (typeof args.workflow === "string" && args.workflow.trim().length > 0) return `workflow=${previewDisplayText(args.workflow, 48)}`;
|
|
553
569
|
|
|
554
|
-
if (typeof args.url === "string" && args.url.trim().length > 0) return
|
|
570
|
+
if (typeof args.url === "string" && args.url.trim().length > 0) return previewDisplayText(args.url, 60);
|
|
555
571
|
const urlsPreview = previewArray(args.urls);
|
|
556
|
-
if (urlsPreview) return
|
|
557
|
-
if (typeof args.prompt === "string" && args.prompt.trim().length > 0) return
|
|
558
|
-
|
|
572
|
+
if (urlsPreview) return previewDisplayText(urlsPreview, 60);
|
|
573
|
+
if (typeof args.prompt === "string" && args.prompt.trim().length > 0) return previewDisplayText(args.prompt, 60);
|
|
574
|
+
|
|
559
575
|
const previewKeys = ["command", "path", "file_path", "pattern", "query", "url", "task", "describe", "search"];
|
|
560
576
|
for (const key of previewKeys) {
|
|
561
|
-
if (
|
|
562
|
-
const value = args[key] as string;
|
|
563
|
-
return truncatePreview(value, 60);
|
|
564
|
-
}
|
|
577
|
+
if (typeof args[key] === "string") return previewDisplayText(args[key], 60);
|
|
565
578
|
}
|
|
566
|
-
|
|
567
|
-
// Fallback: show first string value found
|
|
579
|
+
|
|
568
580
|
for (const [key, value] of Object.entries(args)) {
|
|
581
|
+
const displayKey = sanitizeDisplayText(key);
|
|
569
582
|
const arrayPreview = previewArray(value);
|
|
570
|
-
if (arrayPreview) return `${
|
|
571
|
-
if (typeof value === "string" && value.length > 0) {
|
|
572
|
-
const preview = truncatePreview(value, 50);
|
|
573
|
-
return `${key}=${preview}`;
|
|
574
|
-
}
|
|
583
|
+
if (arrayPreview) return `${displayKey}=${previewDisplayText(arrayPreview, 50)}`;
|
|
584
|
+
if (typeof value === "string" && value.length > 0) return `${displayKey}=${previewDisplayText(value, 50)}`;
|
|
575
585
|
}
|
|
576
586
|
return "";
|
|
577
587
|
}
|
|
@@ -4,6 +4,7 @@ import * as path from "node:path";
|
|
|
4
4
|
import { keyText, type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
5
5
|
import { Key, matchesKey, truncateToWidth, type Component, type TUI } from "@earendil-works/pi-tui";
|
|
6
6
|
import { BUILTIN_AGENT_NAMES, discoverAgents } from "../agents/agents.ts";
|
|
7
|
+
import { resolveExistingReadPaths } from "../shared/settings.ts";
|
|
7
8
|
import {
|
|
8
9
|
DEFAULT_PROVIDER_MODELS_MAX_AGE_DAYS,
|
|
9
10
|
applySubagentProfile,
|
|
@@ -675,7 +676,8 @@ export function registerSlashCommands(
|
|
|
675
676
|
|
|
676
677
|
let finalTask = task;
|
|
677
678
|
if (inline.reads && Array.isArray(inline.reads) && inline.reads.length > 0) {
|
|
678
|
-
|
|
679
|
+
const existingReads = inline.reads.filter((read) => resolveExistingReadPaths([read], state.baseCwd).length > 0);
|
|
680
|
+
if (existingReads.length > 0) finalTask = `[Read from: ${existingReads.join(", ")}]\n\n${finalTask}`;
|
|
679
681
|
}
|
|
680
682
|
const child: Record<string, unknown> = { agent: agentName, task: finalTask, agentScope: "both" };
|
|
681
683
|
if (inline.output !== undefined) child.output = inline.output;
|
package/src/tui/fleet-status.ts
CHANGED
|
@@ -307,26 +307,22 @@ export class SubagentFleetStatus {
|
|
|
307
307
|
refresh(): void {
|
|
308
308
|
const ctx = this.getActiveUiContext();
|
|
309
309
|
if (!ctx) return;
|
|
310
|
+
if (this.state.widgetsSuspended) {
|
|
311
|
+
this.clearWidget();
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
310
314
|
this.entries = collectFleetStatusEntries(this.state);
|
|
311
315
|
this.clampSelection();
|
|
312
316
|
if (this.inspectorOpen || this.state.fleetInspectorOpen) {
|
|
313
317
|
this.lastRenderKey = "";
|
|
314
|
-
|
|
315
|
-
ctx.ui.setWidget(FLEET_STATUS_WIDGET_KEY, undefined);
|
|
316
|
-
this.widgetRegistered = false;
|
|
317
|
-
this.tui = undefined;
|
|
318
|
-
}
|
|
318
|
+
this.clearWidget();
|
|
319
319
|
return;
|
|
320
320
|
}
|
|
321
321
|
if (this.entries.length === 0) {
|
|
322
322
|
this.active = false;
|
|
323
323
|
this.selectedKey = "main";
|
|
324
324
|
this.lastRenderKey = "";
|
|
325
|
-
|
|
326
|
-
ctx.ui.setWidget(FLEET_STATUS_WIDGET_KEY, undefined);
|
|
327
|
-
this.widgetRegistered = false;
|
|
328
|
-
this.tui = undefined;
|
|
329
|
-
}
|
|
325
|
+
this.clearWidget();
|
|
330
326
|
return;
|
|
331
327
|
}
|
|
332
328
|
|
|
@@ -356,6 +352,7 @@ export class SubagentFleetStatus {
|
|
|
356
352
|
}
|
|
357
353
|
|
|
358
354
|
handleKey(data: string): { consume?: boolean; data?: string } | undefined {
|
|
355
|
+
if (this.state.widgetsSuspended) return undefined;
|
|
359
356
|
const ctx = this.getActiveUiContext();
|
|
360
357
|
if (!ctx || this.entries.length === 0 || isKeyRelease(data)) return undefined;
|
|
361
358
|
if (this.inspectorOpen) return undefined;
|
|
@@ -536,6 +533,13 @@ export class SubagentFleetStatus {
|
|
|
536
533
|
}
|
|
537
534
|
}
|
|
538
535
|
|
|
536
|
+
private clearWidget(): void {
|
|
537
|
+
if (!this.widgetRegistered) return;
|
|
538
|
+
this.ui?.setWidget(FLEET_STATUS_WIDGET_KEY, undefined);
|
|
539
|
+
this.widgetRegistered = false;
|
|
540
|
+
this.tui = undefined;
|
|
541
|
+
}
|
|
542
|
+
|
|
539
543
|
private clearUiRegistration(): void {
|
|
540
544
|
if (this.timer) clearInterval(this.timer);
|
|
541
545
|
this.timer = undefined;
|
package/src/tui/render.ts
CHANGED
|
@@ -16,8 +16,10 @@ import {
|
|
|
16
16
|
type NestedStepSummary,
|
|
17
17
|
type WorkflowNodeStatus,
|
|
18
18
|
MAX_WIDGET_JOBS,
|
|
19
|
+
POLL_INTERVAL_MS,
|
|
19
20
|
WIDGET_KEY,
|
|
20
21
|
} from "../shared/types.ts";
|
|
22
|
+
import { sanitizeDisplayText, truncateDisplayText } from "../shared/display-text.ts";
|
|
21
23
|
import { formatTokens, formatUsage, formatDuration, formatModelThinking, formatToolCall, shortenPath } from "../shared/formatters.ts";
|
|
22
24
|
import { getDisplayItems, getSingleResultOutput } from "../shared/utils.ts";
|
|
23
25
|
import { flatToLogicalStepIndex } from "../runs/background/parallel-groups.ts";
|
|
@@ -320,6 +322,12 @@ function snapshotNowForProgress(progress: Pick<AgentProgress, "currentToolStarte
|
|
|
320
322
|
return progress.lastActivityAt;
|
|
321
323
|
}
|
|
322
324
|
|
|
325
|
+
function renderToolArgsPreview(value: string, maxLength: number, expanded: boolean): string {
|
|
326
|
+
const normalized = sanitizeDisplayText(value);
|
|
327
|
+
if (expanded || normalized.length <= maxLength) return normalized;
|
|
328
|
+
return `${truncateDisplayText(normalized, maxLength)}...`;
|
|
329
|
+
}
|
|
330
|
+
|
|
323
331
|
function formatCurrentToolLine(
|
|
324
332
|
progress: Pick<AgentProgress, "currentTool" | "currentToolArgs" | "currentToolStartedAt">,
|
|
325
333
|
availableWidth: number,
|
|
@@ -329,9 +337,7 @@ function formatCurrentToolLine(
|
|
|
329
337
|
if (!progress.currentTool) return undefined;
|
|
330
338
|
const maxToolArgsLen = Math.max(50, availableWidth - 20);
|
|
331
339
|
const toolArgsPreview = progress.currentToolArgs
|
|
332
|
-
? (
|
|
333
|
-
? progress.currentToolArgs
|
|
334
|
-
: `${progress.currentToolArgs.slice(0, maxToolArgsLen)}...`)
|
|
340
|
+
? renderToolArgsPreview(progress.currentToolArgs, maxToolArgsLen, expanded)
|
|
335
341
|
: "";
|
|
336
342
|
const durationSuffix = progress.currentToolStartedAt !== undefined && snapshotNow !== undefined
|
|
337
343
|
? ` | ${formatDuration(Math.max(0, snapshotNow - progress.currentToolStartedAt))}`
|
|
@@ -622,7 +628,7 @@ function widgetStepActivity(step: NonNullable<AsyncJobState["steps"]>[number], s
|
|
|
622
628
|
}
|
|
623
629
|
|
|
624
630
|
|
|
625
|
-
function widgetChainDetails(job: AsyncJobState, theme: Theme, expanded = false, width = getTermWidth()): string[] {
|
|
631
|
+
function widgetChainDetails(job: AsyncJobState, theme: Theme, expanded = false, width = getTermWidth(), frame?: number): string[] {
|
|
626
632
|
if (!job.steps?.length) return [];
|
|
627
633
|
const total = job.chainStepCount ?? job.steps.length;
|
|
628
634
|
const lines: string[] = [];
|
|
@@ -630,7 +636,7 @@ function widgetChainDetails(job: AsyncJobState, theme: Theme, expanded = false,
|
|
|
630
636
|
const steps = job.steps.slice(span.start, span.start + span.count);
|
|
631
637
|
if (span.isParallel) {
|
|
632
638
|
const status = aggregateStepStatus(steps);
|
|
633
|
-
lines.push(` ${widgetStepGlyph(status, theme, widgetStepsRunningSeed(steps))} Step ${span.stepIndex + 1}/${total}: ${themeBold(theme, "parallel group")} ${theme.fg("dim", "·")} ${theme.fg("dim", formatParallelOutcome(steps, span.count))}`);
|
|
639
|
+
lines.push(` ${widgetStepGlyph(status, theme, widgetStepsRunningSeed(steps), frame)} Step ${span.stepIndex + 1}/${total}: ${themeBold(theme, "parallel group")} ${theme.fg("dim", "·")} ${theme.fg("dim", formatParallelOutcome(steps, span.count))}`);
|
|
634
640
|
continue;
|
|
635
641
|
}
|
|
636
642
|
const step = steps[0];
|
|
@@ -638,15 +644,15 @@ function widgetChainDetails(job: AsyncJobState, theme: Theme, expanded = false,
|
|
|
638
644
|
lines.push(` ${theme.fg("dim", `◦ Step ${span.stepIndex + 1}/${total}: pending`)}`);
|
|
639
645
|
continue;
|
|
640
646
|
}
|
|
641
|
-
lines.push(...foregroundStyleWidgetStepLines(job, theme, step, "Step", span.stepIndex + 1, total, expanded, width));
|
|
647
|
+
lines.push(...foregroundStyleWidgetStepLines(job, theme, step, "Step", span.stepIndex + 1, total, expanded, width, frame));
|
|
642
648
|
}
|
|
643
649
|
return lines;
|
|
644
650
|
}
|
|
645
651
|
|
|
646
|
-
function widgetParallelAgentDetails(job: AsyncJobState, theme: Theme, expanded = false, width = getTermWidth()): string[] {
|
|
652
|
+
function widgetParallelAgentDetails(job: AsyncJobState, theme: Theme, expanded = false, width = getTermWidth(), frame?: number): string[] {
|
|
647
653
|
if (!job.steps?.length) return [];
|
|
648
654
|
if (job.mode !== "parallel" && job.mode !== "chain") return [];
|
|
649
|
-
if (job.mode === "chain" && !job.activeParallelGroup && job.parallelGroups?.length) return widgetChainDetails(job, theme, expanded, width);
|
|
655
|
+
if (job.mode === "chain" && !job.activeParallelGroup && job.parallelGroups?.length) return widgetChainDetails(job, theme, expanded, width, frame);
|
|
650
656
|
const total = job.stepsTotal ?? job.steps.length;
|
|
651
657
|
const lines: string[] = [];
|
|
652
658
|
for (const [index, step] of job.steps.entries()) {
|
|
@@ -654,7 +660,7 @@ function widgetParallelAgentDetails(job: AsyncJobState, theme: Theme, expanded =
|
|
|
654
660
|
const activity = widgetStepActivity(step, job.updatedAt);
|
|
655
661
|
const itemTitle = job.mode === "parallel" || job.activeParallelGroup ? "Agent" : "Step";
|
|
656
662
|
const modelDisplay = modelThinkingBadge(theme, step.model, step.thinking);
|
|
657
|
-
lines.push(` ${theme.fg("dim", `${marker} ${widgetStepGlyph(step.status, theme, widgetStepRunningSeed(step, index))} ${itemTitle} ${index + 1}/${total}: ${step.agent} · ${widgetStepStatus(step.status, theme)}${modelDisplay}${activity ? ` · ${activity}` : ""}`)}`);
|
|
663
|
+
lines.push(` ${theme.fg("dim", `${marker} ${widgetStepGlyph(step.status, theme, widgetStepRunningSeed(step, index), frame)} ${itemTitle} ${index + 1}/${total}: ${step.agent} · ${widgetStepStatus(step.status, theme)}${modelDisplay}${activity ? ` · ${activity}` : ""}`)}`);
|
|
658
664
|
for (const nestedLine of formatNestedWidgetLines(step.children, theme, width, expanded, job.updatedAt, expanded ? 8 : 6)) lines.push(` ${nestedLine}`);
|
|
659
665
|
}
|
|
660
666
|
return lines;
|
|
@@ -1147,7 +1153,7 @@ function foregroundStyleWidgetStepLines(
|
|
|
1147
1153
|
if (liveStatus && liveStatus !== activity) lines.push(` ${theme.fg("accent", liveStatus)}`);
|
|
1148
1154
|
for (const tool of step.recentTools?.slice(-3) ?? []) {
|
|
1149
1155
|
const maxArgsLen = Math.max(40, width - 30);
|
|
1150
|
-
const argsPreview = tool.args
|
|
1156
|
+
const argsPreview = renderToolArgsPreview(tool.args, maxArgsLen, expanded);
|
|
1151
1157
|
lines.push(` ${theme.fg("dim", `${tool.tool}${argsPreview ? `: ${argsPreview}` : ""}`)}`);
|
|
1152
1158
|
}
|
|
1153
1159
|
for (const line of compactRecentOutputLines(step.recentOutput)) {
|
|
@@ -1163,7 +1169,7 @@ function foregroundStyleWidgetDetails(job: AsyncJobState, theme: Theme, expanded
|
|
|
1163
1169
|
` ${theme.fg("dim", `⎿ ${widgetActivity(job)}`)}`,
|
|
1164
1170
|
...formatNestedWidgetLines(job.nestedChildren, theme, width, expanded, job.updatedAt, expanded ? 12 : 6).map((line) => ` ${line}`),
|
|
1165
1171
|
];
|
|
1166
|
-
if (job.mode === "chain" && !job.activeParallelGroup && job.parallelGroups?.length) return widgetChainDetails(job, theme, expanded, width);
|
|
1172
|
+
if (job.mode === "chain" && !job.activeParallelGroup && job.parallelGroups?.length) return widgetChainDetails(job, theme, expanded, width, frame);
|
|
1167
1173
|
const total = job.stepsTotal ?? job.steps.length;
|
|
1168
1174
|
const itemTitle = job.mode === "parallel" || job.activeParallelGroup ? "Agent" : "Step";
|
|
1169
1175
|
const lines: string[] = [];
|
|
@@ -1411,10 +1417,10 @@ function fitWidgetLineBudget(lines: string[], theme: Theme, width: number, expan
|
|
|
1411
1417
|
return [...lines.slice(0, visibleLines), truncLine(theme.fg("dim", hint), width)];
|
|
1412
1418
|
}
|
|
1413
1419
|
|
|
1414
|
-
function fitAdaptiveWidgetLines(jobs: AsyncJobState[],
|
|
1420
|
+
function fitAdaptiveWidgetLines(jobs: AsyncJobState[], buildLines: () => string[], theme: Theme, width: number, expanded: boolean, frame?: number): string[] {
|
|
1415
1421
|
if (expanded) {
|
|
1416
1422
|
resetWidgetLayoutSession();
|
|
1417
|
-
return fitWidgetLineBudget(
|
|
1423
|
+
return fitWidgetLineBudget(buildLines(), theme, width, true);
|
|
1418
1424
|
}
|
|
1419
1425
|
|
|
1420
1426
|
const hasMatchingSession = widgetSessionMatches(expanded);
|
|
@@ -1432,6 +1438,7 @@ function fitAdaptiveWidgetLines(jobs: AsyncJobState[], lines: string[], theme: T
|
|
|
1432
1438
|
return rendered.lines;
|
|
1433
1439
|
}
|
|
1434
1440
|
|
|
1441
|
+
const lines = buildLines();
|
|
1435
1442
|
if (lines.length <= availableRows) {
|
|
1436
1443
|
widgetLayoutSession = { expanded, rows, columns, tier: "full", visibleJobKeys: [] };
|
|
1437
1444
|
return fitWidgetLineBudget(lines, theme, width, false);
|
|
@@ -1453,12 +1460,13 @@ function buildWidgetComponent(jobs: AsyncJobState[], expanded: boolean): (_tui:
|
|
|
1453
1460
|
const container = new Container();
|
|
1454
1461
|
container.render = (renderWidth: number): string[] => {
|
|
1455
1462
|
const width = Math.max(0, renderWidth - 2);
|
|
1456
|
-
const
|
|
1457
|
-
|
|
1463
|
+
const frame = Math.floor(Date.now() / POLL_INTERVAL_MS);
|
|
1464
|
+
const buildLines = (): string[] => expanded
|
|
1465
|
+
? buildWidgetLines(jobs, theme, width, true, frame)
|
|
1458
1466
|
: jobs.length === 1
|
|
1459
|
-
? compactSingleWidgetLines(jobs[0]!, theme, width)
|
|
1460
|
-
: buildWidgetLines(jobs, theme, width, false);
|
|
1461
|
-
return fitAdaptiveWidgetLines(jobs,
|
|
1467
|
+
? compactSingleWidgetLines(jobs[0]!, theme, width, frame)
|
|
1468
|
+
: buildWidgetLines(jobs, theme, width, false, frame);
|
|
1469
|
+
return fitAdaptiveWidgetLines(jobs, buildLines, theme, width, expanded, frame).map((line) => paddedWidgetLine(line, renderWidth));
|
|
1462
1470
|
};
|
|
1463
1471
|
return container;
|
|
1464
1472
|
};
|
|
@@ -1488,7 +1496,7 @@ export function buildWidgetLines(jobs: AsyncJobState[], theme: Theme, width = ge
|
|
|
1488
1496
|
items.push([
|
|
1489
1497
|
`${widgetStatusGlyph(job, theme, frame)} ${themeBold(theme, widgetJobName(job))}${contextModeBadge(theme, job.context)}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`,
|
|
1490
1498
|
` ${theme.fg("dim", `⎿ ${widgetActivity(job)}`)}`,
|
|
1491
|
-
...widgetParallelAgentDetails(job, theme, expanded, width),
|
|
1499
|
+
...widgetParallelAgentDetails(job, theme, expanded, width, frame),
|
|
1492
1500
|
]);
|
|
1493
1501
|
slots--;
|
|
1494
1502
|
}
|
|
@@ -1505,7 +1513,7 @@ export function buildWidgetLines(jobs: AsyncJobState[], theme: Theme, width = ge
|
|
|
1505
1513
|
items.push([
|
|
1506
1514
|
`${widgetStatusGlyph(job, theme, frame)} ${themeBold(theme, widgetJobName(job))}${contextModeBadge(theme, job.context)}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`,
|
|
1507
1515
|
` ${theme.fg("dim", `⎿ ${widgetActivity(job)}`)}`,
|
|
1508
|
-
...widgetParallelAgentDetails(job, theme, expanded, width),
|
|
1516
|
+
...widgetParallelAgentDetails(job, theme, expanded, width, frame),
|
|
1509
1517
|
]);
|
|
1510
1518
|
slots--;
|
|
1511
1519
|
}
|
|
@@ -1870,9 +1878,7 @@ export function renderSubagentResult(
|
|
|
1870
1878
|
if (r.progress.recentTools?.length) {
|
|
1871
1879
|
for (const t of r.progress.recentTools.slice(-3)) {
|
|
1872
1880
|
const maxArgsLen = Math.max(40, w - 24);
|
|
1873
|
-
const argsPreview =
|
|
1874
|
-
? t.args
|
|
1875
|
-
: `${t.args.slice(0, maxArgsLen)}...`;
|
|
1881
|
+
const argsPreview = renderToolArgsPreview(t.args, maxArgsLen, expanded);
|
|
1876
1882
|
c.addChild(new Text(fit(theme.fg("dim", `${t.tool}: ${argsPreview}`)), 0, 0));
|
|
1877
1883
|
}
|
|
1878
1884
|
}
|
|
@@ -2103,9 +2109,7 @@ export function renderSubagentResult(
|
|
|
2103
2109
|
if (rProg.recentTools?.length) {
|
|
2104
2110
|
for (const t of rProg.recentTools.slice(-3)) {
|
|
2105
2111
|
const maxArgsLen = Math.max(40, w - 30);
|
|
2106
|
-
const argsPreview =
|
|
2107
|
-
? t.args
|
|
2108
|
-
: `${t.args.slice(0, maxArgsLen)}...`;
|
|
2112
|
+
const argsPreview = renderToolArgsPreview(t.args, maxArgsLen, expanded);
|
|
2109
2113
|
c.addChild(new Text(fit(theme.fg("dim", ` ${t.tool}: ${argsPreview}`)), 0, 0));
|
|
2110
2114
|
}
|
|
2111
2115
|
}
|
|
@@ -2,10 +2,11 @@ import { spawnSync } from "node:child_process";
|
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
3
|
import * as fs from "node:fs";
|
|
4
4
|
import * as path from "node:path";
|
|
5
|
+
import { PROJECT_SUBAGENTS_RELATIVE_DIR } from "../shared/artifacts.ts";
|
|
5
6
|
|
|
6
|
-
const IGNORED_CHANGE_PREFIXES = [
|
|
7
|
-
const IGNORED_CHANGE_PATHS = new Set([
|
|
8
|
-
const IGNORED_CHANGE_SEGMENTS = new Set([".git", "
|
|
7
|
+
const IGNORED_CHANGE_PREFIXES = [`${PROJECT_SUBAGENTS_RELATIVE_DIR}/`, "tmp/", "node_modules/"];
|
|
8
|
+
const IGNORED_CHANGE_PATHS = new Set([PROJECT_SUBAGENTS_RELATIVE_DIR, "tmp", "node_modules"]);
|
|
9
|
+
const IGNORED_CHANGE_SEGMENTS = new Set([".git", "node_modules"]);
|
|
9
10
|
|
|
10
11
|
const DEFAULT_MAX_HASH_FILE_BYTES = 64 * 1024 * 1024;
|
|
11
12
|
const DEFAULT_MAX_HASH_TOTAL_BYTES = 64 * 1024 * 1024;
|