pi-subagents 0.18.0 → 0.18.1
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 +13 -0
- package/README.md +2 -2
- package/agent-management.ts +11 -2
- package/async-execution.ts +2 -2
- package/execution.ts +6 -6
- package/index.ts +103 -10
- package/notify.ts +20 -10
- package/package.json +1 -1
- package/pi-args.ts +1 -0
- package/render.ts +371 -57
- package/schemas.ts +48 -13
- package/skills.ts +61 -0
- package/slash-commands.ts +55 -11
- package/slash-live-state.ts +3 -5
- package/subagent-executor.ts +8 -6
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.18.1] - 2026-04-25
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
- Restyled live subagent rendering, async widgets, and background completion notifications with compact Claude-style visual grammar while preserving existing observability paths.
|
|
9
|
+
- Parallel subagent result rendering now labels parallel workers as `Agent N` instead of `Step N`, while chain rendering keeps step terminology.
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
- `/run` and single-agent tool calls now allow self-contained agents to run without a task string.
|
|
13
|
+
- The `subagent` tool description no longer advertises hardcoded builtin agent names and management list output now separates disabled builtins from executable agents.
|
|
14
|
+
- Flexible `subagent` tool schema fields now include explicit JSON Schema types so llama.cpp and local OpenAI-compatible providers accept them.
|
|
15
|
+
- Settings package sources now resolve explicit `git:` and `npm:` entries from project and user package caches.
|
|
16
|
+
- Slash-command subagent results are now export-friendly, including completed output and child session paths in visible export content.
|
|
17
|
+
|
|
5
18
|
## [0.18.0] - 2026-04-23
|
|
6
19
|
|
|
7
20
|
### Added
|
package/README.md
CHANGED
|
@@ -222,7 +222,7 @@ Subagents only get direct MCP tools when `mcp:` items are explicitly listed. Eve
|
|
|
222
222
|
|
|
223
223
|
| Command | Description |
|
|
224
224
|
|---------|-------------|
|
|
225
|
-
| `/run <agent>
|
|
225
|
+
| `/run <agent> [task]` | Run a single agent; omit the task for self-contained agents |
|
|
226
226
|
| `/chain agent1 "task1" -> agent2 "task2"` | Run agents in sequence with per-step tasks |
|
|
227
227
|
| `/parallel agent1 "task1" -> agent2 "task2"` | Run agents in parallel with per-step tasks |
|
|
228
228
|
| `/subagents-status` | Open the async status overlay for active and recent runs |
|
|
@@ -438,7 +438,7 @@ Chains can be created from the Agents Manager template picker ("Blank Chain"), o
|
|
|
438
438
|
|
|
439
439
|
| Mode | Async Support | Notes |
|
|
440
440
|
|------|---------------|-------|
|
|
441
|
-
| Single | Yes | `{ agent, task }` - agents with `output` write to temp dir |
|
|
441
|
+
| Single | Yes | `{ agent, task? }` - omit `task` for self-contained agents; agents with `output` write to temp dir |
|
|
442
442
|
| Chain | Yes | `{ chain: [{agent, task}...] }` with `{task}`, `{previous}`, `{chain_dir}` variables |
|
|
443
443
|
| Parallel | Yes | `{ tasks: [{agent, task}...] }` - via TUI toggle or converted to chain for async |
|
|
444
444
|
|
package/agent-management.ts
CHANGED
|
@@ -370,9 +370,18 @@ export function formatChainDetail(chain: ChainConfig): string {
|
|
|
370
370
|
export function handleList(params: ManagementParams, ctx: ManagementContext): AgentToolResult<Details> {
|
|
371
371
|
const scope = normalizeListScope(params.agentScope) ?? "both";
|
|
372
372
|
const d = discoverAgentsAll(ctx.cwd);
|
|
373
|
-
const
|
|
373
|
+
const scopedAgents = allAgents(d).filter((a) => scope === "both" || a.source === "builtin" || a.source === scope).sort((a, b) => a.name.localeCompare(b.name));
|
|
374
|
+
const agents = scopedAgents.filter((a) => !a.disabled);
|
|
375
|
+
const disabledBuiltins = scopedAgents.filter((a) => a.source === "builtin" && a.disabled);
|
|
374
376
|
const chains = d.chains.filter((c) => scope === "both" || c.source === scope).sort((a, b) => a.name.localeCompare(b.name));
|
|
375
|
-
const lines = [
|
|
377
|
+
const lines = [
|
|
378
|
+
"Executable agents:",
|
|
379
|
+
...(agents.length ? agents.map((a) => `- ${a.name} (${a.source}): ${a.description}`) : ["- (none)"]),
|
|
380
|
+
...(disabledBuiltins.length ? ["", "Disabled builtins:", ...disabledBuiltins.map((a) => `- ${a.name} (${a.source}, disabled): ${a.description}`)] : []),
|
|
381
|
+
"",
|
|
382
|
+
"Chains:",
|
|
383
|
+
...(chains.length ? chains.map((c) => `- ${c.name} (${c.source}): ${c.description}`) : ["- (none)"]),
|
|
384
|
+
];
|
|
376
385
|
return result(lines.join("\n"));
|
|
377
386
|
}
|
|
378
387
|
|
package/async-execution.ts
CHANGED
|
@@ -84,7 +84,7 @@ export interface AsyncChainParams {
|
|
|
84
84
|
|
|
85
85
|
export interface AsyncSingleParams {
|
|
86
86
|
agent: string;
|
|
87
|
-
task
|
|
87
|
+
task?: string;
|
|
88
88
|
agentConfig: AgentConfig;
|
|
89
89
|
ctx: AsyncExecutionContext;
|
|
90
90
|
cwd?: string;
|
|
@@ -372,7 +372,6 @@ export function executeAsyncSingle(
|
|
|
372
372
|
): AsyncExecutionResult {
|
|
373
373
|
const {
|
|
374
374
|
agent,
|
|
375
|
-
task,
|
|
376
375
|
agentConfig,
|
|
377
376
|
ctx,
|
|
378
377
|
cwd,
|
|
@@ -389,6 +388,7 @@ export function executeAsyncSingle(
|
|
|
389
388
|
controlIntercomTarget,
|
|
390
389
|
childIntercomTarget,
|
|
391
390
|
} = params;
|
|
391
|
+
const task = params.task ?? "";
|
|
392
392
|
const runnerCwd = resolveChildCwd(ctx.cwd, cwd);
|
|
393
393
|
const skillNames = params.skills ?? agentConfig.skills ?? [];
|
|
394
394
|
const availableModels = params.availableModels;
|
package/execution.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { spawn } from "node:child_process";
|
|
6
|
+
import { existsSync } from "node:fs";
|
|
6
7
|
import type { Message } from "@mariozechner/pi-ai";
|
|
7
8
|
import type { AgentConfig } from "./agents.ts";
|
|
8
9
|
import {
|
|
@@ -726,12 +727,11 @@ export async function runSync(
|
|
|
726
727
|
if (truncationResult.truncated) result.truncation = truncationResult;
|
|
727
728
|
}
|
|
728
729
|
|
|
729
|
-
if (
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
}
|
|
730
|
+
if (options.sessionFile && (existsSync(options.sessionFile) || result.messages?.length)) {
|
|
731
|
+
result.sessionFile = options.sessionFile;
|
|
732
|
+
} else if (shareEnabled && options.sessionDir) {
|
|
733
|
+
const sessionFile = findLatestSessionFile(options.sessionDir);
|
|
734
|
+
if (sessionFile) result.sessionFile = sessionFile;
|
|
735
735
|
}
|
|
736
736
|
|
|
737
737
|
return result;
|
package/index.ts
CHANGED
|
@@ -21,7 +21,7 @@ import { Box, Container, Spacer, Text, truncateToWidth, visibleWidth, wrapTextWi
|
|
|
21
21
|
import { discoverAgents } from "./agents.ts";
|
|
22
22
|
import { cleanupAllArtifactDirs, cleanupOldArtifacts, getArtifactsDir } from "./artifacts.ts";
|
|
23
23
|
import { cleanupOldChainDirs } from "./settings.ts";
|
|
24
|
-
import { renderWidget, renderSubagentResult } from "./render.ts";
|
|
24
|
+
import { renderWidget, renderSubagentResult, stopResultAnimations, stopWidgetAnimation, syncResultAnimation } from "./render.ts";
|
|
25
25
|
import { SubagentParams } from "./schemas.ts";
|
|
26
26
|
import { createSubagentExecutor } from "./subagent-executor.ts";
|
|
27
27
|
import { createAsyncJobTracker } from "./async-job-tracker.ts";
|
|
@@ -32,7 +32,8 @@ import { registerPromptTemplateDelegationBridge } from "./prompt-template-bridge
|
|
|
32
32
|
import { registerSlashSubagentBridge } from "./slash-bridge.ts";
|
|
33
33
|
import { clearSlashSnapshots, getSlashRenderableSnapshot, resolveSlashMessageDetails, restoreSlashFinalSnapshots, type SlashMessageDetails } from "./slash-live-state.ts";
|
|
34
34
|
import { inspectSubagentStatus } from "./run-status.ts";
|
|
35
|
-
import registerSubagentNotify from "./notify.ts";
|
|
35
|
+
import registerSubagentNotify, { type SubagentNotifyDetails } from "./notify.ts";
|
|
36
|
+
import { formatDuration, shortenPath } from "./formatters.ts";
|
|
36
37
|
import {
|
|
37
38
|
type ControlEvent,
|
|
38
39
|
type Details,
|
|
@@ -130,12 +131,15 @@ function createSlashResultComponent(
|
|
|
130
131
|
details: SlashMessageDetails,
|
|
131
132
|
options: { expanded: boolean },
|
|
132
133
|
theme: ExtensionContext["ui"]["theme"],
|
|
134
|
+
requestRender: () => void,
|
|
133
135
|
): Container {
|
|
134
136
|
const container = new Container();
|
|
137
|
+
const animationState: { subagentResultAnimationTimer?: ReturnType<typeof setInterval> } = {};
|
|
135
138
|
let lastVersion = -1;
|
|
136
139
|
container.render = (width: number): string[] => {
|
|
137
140
|
const snapshot = getSlashRenderableSnapshot(details);
|
|
138
|
-
|
|
141
|
+
syncResultAnimation(snapshot.result, { state: animationState, invalidate: requestRender });
|
|
142
|
+
if (snapshot.version !== lastVersion || isSlashResultRunning(snapshot.result)) {
|
|
139
143
|
lastVersion = snapshot.version;
|
|
140
144
|
rebuildSlashResultContainer(container, snapshot.result, options, theme);
|
|
141
145
|
}
|
|
@@ -162,6 +166,38 @@ function formatSubagentControlNotice(details: SubagentControlMessageDetails, con
|
|
|
162
166
|
return details.noticeText ?? content ?? formatControlNoticeMessage(details.event, controlNoticeTarget(details));
|
|
163
167
|
}
|
|
164
168
|
|
|
169
|
+
function parseSubagentNotifyContent(content: string): SubagentNotifyDetails | undefined {
|
|
170
|
+
const lines = content.split("\n");
|
|
171
|
+
const header = lines[0] ?? "";
|
|
172
|
+
const match = header.match(/^Background task (completed|failed|paused): \*\*(.+?)\*\*(?:\s+(\([^)]*\)))?$/);
|
|
173
|
+
if (!match) return undefined;
|
|
174
|
+
const body = lines.slice(2);
|
|
175
|
+
let sessionIndex = -1;
|
|
176
|
+
for (let i = body.length - 1; i >= 1; i--) {
|
|
177
|
+
if (body[i - 1]?.trim() === "" && /^(Session|Session file|Session share error):\s+/.test(body[i]!)) {
|
|
178
|
+
sessionIndex = i;
|
|
179
|
+
break;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
const sessionLine = sessionIndex >= 0 ? body[sessionIndex] : undefined;
|
|
183
|
+
const resultLines = sessionIndex >= 0 ? body.slice(0, sessionIndex) : body;
|
|
184
|
+
const resultPreview = resultLines.join("\n").trim() || "(no output)";
|
|
185
|
+
let sessionLabel: string | undefined;
|
|
186
|
+
let sessionValue: string | undefined;
|
|
187
|
+
if (sessionLine) {
|
|
188
|
+
const separator = sessionLine.indexOf(":");
|
|
189
|
+
sessionLabel = sessionLine.slice(0, separator).toLowerCase();
|
|
190
|
+
sessionValue = sessionLine.slice(separator + 1).trim();
|
|
191
|
+
}
|
|
192
|
+
return {
|
|
193
|
+
agent: match[2]!,
|
|
194
|
+
status: match[1] as SubagentNotifyDetails["status"],
|
|
195
|
+
...(match[3] ? { taskInfo: match[3] } : {}),
|
|
196
|
+
resultPreview,
|
|
197
|
+
...(sessionLabel && sessionValue ? { sessionLabel, sessionValue } : {}),
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
165
201
|
class SubagentControlNoticeComponent implements Component {
|
|
166
202
|
constructor(
|
|
167
203
|
private readonly details: SubagentControlMessageDetails,
|
|
@@ -191,6 +227,17 @@ class SubagentControlNoticeComponent implements Component {
|
|
|
191
227
|
}
|
|
192
228
|
|
|
193
229
|
export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
230
|
+
const globalStore = globalThis as Record<string, unknown>;
|
|
231
|
+
const runtimeCleanupStoreKey = "__piSubagentRuntimeCleanup";
|
|
232
|
+
const previousRuntimeCleanup = globalStore[runtimeCleanupStoreKey];
|
|
233
|
+
if (typeof previousRuntimeCleanup === "function") {
|
|
234
|
+
try {
|
|
235
|
+
previousRuntimeCleanup();
|
|
236
|
+
} catch {
|
|
237
|
+
// Best effort cleanup for stale timers from an older reload.
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
194
241
|
ensureAccessibleDir(RESULTS_DIR);
|
|
195
242
|
ensureAccessibleDir(ASYNC_DIR);
|
|
196
243
|
cleanupOldChainDirs();
|
|
@@ -227,6 +274,16 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
227
274
|
startResultWatcher();
|
|
228
275
|
primeExistingResults();
|
|
229
276
|
|
|
277
|
+
const runtimeCleanup = () => {
|
|
278
|
+
stopWidgetAnimation();
|
|
279
|
+
stopResultAnimations();
|
|
280
|
+
if (state.poller) {
|
|
281
|
+
clearInterval(state.poller);
|
|
282
|
+
state.poller = null;
|
|
283
|
+
}
|
|
284
|
+
};
|
|
285
|
+
globalStore[runtimeCleanupStoreKey] = runtimeCleanup;
|
|
286
|
+
|
|
230
287
|
const { ensurePoller, handleStarted, handleComplete, resetJobs } = createAsyncJobTracker(pi, state, ASYNC_DIR);
|
|
231
288
|
const executor = createSubagentExecutor({
|
|
232
289
|
pi,
|
|
@@ -242,7 +299,37 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
242
299
|
pi.registerMessageRenderer<SlashMessageDetails>(SLASH_RESULT_TYPE, (message, options, theme) => {
|
|
243
300
|
const details = resolveSlashMessageDetails(message.details);
|
|
244
301
|
if (!details) return undefined;
|
|
245
|
-
return createSlashResultComponent(details, options, theme);
|
|
302
|
+
return createSlashResultComponent(details, options, theme, () => state.lastUiContext?.ui.requestRender?.());
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
pi.registerMessageRenderer<SubagentNotifyDetails>("subagent-notify", (message, options, theme) => {
|
|
306
|
+
const content = typeof message.content === "string" ? message.content : "";
|
|
307
|
+
const details = (message.details as SubagentNotifyDetails | undefined) ?? parseSubagentNotifyContent(content);
|
|
308
|
+
if (!details) return new Text(content, 0, 0);
|
|
309
|
+
const icon = details.status === "completed"
|
|
310
|
+
? theme.fg("success", "✓")
|
|
311
|
+
: details.status === "paused"
|
|
312
|
+
? theme.fg("warning", "■")
|
|
313
|
+
: theme.fg("error", "✗");
|
|
314
|
+
const parts: string[] = [];
|
|
315
|
+
if (details.taskInfo) parts.push(details.taskInfo);
|
|
316
|
+
if (details.durationMs !== undefined) parts.push(formatDuration(details.durationMs));
|
|
317
|
+
let text = `${icon} ${theme.bold(details.agent)} ${theme.fg("dim", details.status)}`;
|
|
318
|
+
if (parts.length > 0) text += ` ${theme.fg("dim", "·")} ${parts.map((part) => theme.fg("dim", part)).join(` ${theme.fg("dim", "·")} `)}`;
|
|
319
|
+
const trimmedPreview = details.resultPreview.trim();
|
|
320
|
+
const previewLines = options.expanded
|
|
321
|
+
? trimmedPreview.split("\n").filter((line) => line.trim())
|
|
322
|
+
: [trimmedPreview.split("\n", 1)[0] ?? ""].filter((line) => line.trim());
|
|
323
|
+
for (const line of previewLines.length > 0 ? previewLines : ["(no output)"]) {
|
|
324
|
+
text += `\n ${theme.fg("dim", `⎿ ${line}`)}`;
|
|
325
|
+
}
|
|
326
|
+
if (!options.expanded && trimmedPreview.includes("\n")) {
|
|
327
|
+
text += `\n ${theme.fg("dim", "Ctrl+O full notification")}`;
|
|
328
|
+
}
|
|
329
|
+
if (details.sessionLabel && details.sessionValue) {
|
|
330
|
+
text += `\n ${theme.fg("muted", `${details.sessionLabel}: ${shortenPath(details.sessionValue)}`)}`;
|
|
331
|
+
}
|
|
332
|
+
return new Text(text, 0, 0);
|
|
246
333
|
});
|
|
247
334
|
|
|
248
335
|
pi.registerMessageRenderer<SubagentControlMessageDetails>(SUBAGENT_CONTROL_MESSAGE_TYPE, (message, _options, theme) => {
|
|
@@ -311,8 +398,9 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
311
398
|
description: `Delegate to subagents or manage agent definitions.
|
|
312
399
|
|
|
313
400
|
EXECUTION (use exactly ONE mode):
|
|
314
|
-
•
|
|
315
|
-
•
|
|
401
|
+
• Before executing, use { action: "list" } to inspect configured agents/chains. Only execute agents listed as executable/non-disabled.
|
|
402
|
+
• SINGLE: { agent, task? } - one task; omit task for self-contained agents
|
|
403
|
+
• CHAIN: { chain: [{agent:"agent-a"}, {parallel:[{agent:"agent-b",count:3}]}] } - sequential pipeline with optional parallel fan-out
|
|
316
404
|
• PARALLEL: { tasks: [{agent,task,count?}, ...], concurrency?: number, worktree?: true } - concurrent execution (worktree: isolate each task in a git worktree)
|
|
317
405
|
• Optional context: { context: "fresh" | "fork" } (default: "fresh")
|
|
318
406
|
|
|
@@ -321,10 +409,10 @@ CHAIN TEMPLATE VARIABLES (use in task strings):
|
|
|
321
409
|
• {previous} - Text response from the previous step (empty for first step)
|
|
322
410
|
• {chain_dir} - Shared directory for chain files (e.g., <tmpdir>/pi-subagents-<scope>/chain-runs/abc123/)
|
|
323
411
|
|
|
324
|
-
Example: { chain: [{agent:"
|
|
412
|
+
Example: { chain: [{agent:"agent-a", task:"Analyze {task}"}, {agent:"agent-b", task:"Plan based on {previous}"}] }
|
|
325
413
|
|
|
326
414
|
MANAGEMENT (use action field, omit agent/task/chain/tasks):
|
|
327
|
-
• { action: "list" } - discover agents/chains
|
|
415
|
+
• { action: "list" } - discover executable agents/chains and any disabled builtins
|
|
328
416
|
• { action: "get", agent: "name" } - full detail
|
|
329
417
|
• { action: "create", config: { name, systemPrompt, systemPromptMode, inheritProjectContext, inheritSkills, ... } }
|
|
330
418
|
• { action: "update", agent: "name", config: { ... } } - merge
|
|
@@ -370,7 +458,8 @@ CONTROL:
|
|
|
370
458
|
);
|
|
371
459
|
},
|
|
372
460
|
|
|
373
|
-
renderResult(result, options, theme) {
|
|
461
|
+
renderResult(result, options, theme, context) {
|
|
462
|
+
syncResultAnimation(result, context);
|
|
374
463
|
return renderSubagentResult(result, options, theme);
|
|
375
464
|
},
|
|
376
465
|
|
|
@@ -381,7 +470,6 @@ CONTROL:
|
|
|
381
470
|
|
|
382
471
|
const eventUnsubscribeStoreKey = "__piSubagentEventUnsubscribes";
|
|
383
472
|
const controlNoticeSeenStoreKey = "__piSubagentVisibleControlNotices";
|
|
384
|
-
const globalStore = globalThis as Record<string, unknown>;
|
|
385
473
|
const previousEventUnsubscribes = globalStore[eventUnsubscribeStoreKey];
|
|
386
474
|
if (Array.isArray(previousEventUnsubscribes)) {
|
|
387
475
|
for (const unsubscribe of previousEventUnsubscribes) {
|
|
@@ -480,6 +568,11 @@ CONTROL:
|
|
|
480
568
|
slashBridge.dispose();
|
|
481
569
|
promptTemplateBridge.cancelAll();
|
|
482
570
|
promptTemplateBridge.dispose();
|
|
571
|
+
stopWidgetAnimation();
|
|
572
|
+
stopResultAnimations();
|
|
573
|
+
if (globalStore[runtimeCleanupStoreKey] === runtimeCleanup) {
|
|
574
|
+
delete globalStore[runtimeCleanupStoreKey];
|
|
575
|
+
}
|
|
483
576
|
if (state.lastUiContext?.hasUI) {
|
|
484
577
|
state.lastUiContext.ui.setWidget(WIDGET_KEY, undefined);
|
|
485
578
|
}
|
package/notify.ts
CHANGED
|
@@ -12,6 +12,16 @@ interface ChainStepResult {
|
|
|
12
12
|
success: boolean;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
export interface SubagentNotifyDetails {
|
|
16
|
+
agent: string;
|
|
17
|
+
status: "completed" | "failed" | "paused";
|
|
18
|
+
taskInfo?: string;
|
|
19
|
+
resultPreview: string;
|
|
20
|
+
durationMs?: number;
|
|
21
|
+
sessionLabel?: string;
|
|
22
|
+
sessionValue?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
15
25
|
interface SubagentResult {
|
|
16
26
|
id: string | null;
|
|
17
27
|
agent: string | null;
|
|
@@ -20,6 +30,7 @@ interface SubagentResult {
|
|
|
20
30
|
exitCode?: number;
|
|
21
31
|
state?: string;
|
|
22
32
|
timestamp: number;
|
|
33
|
+
durationMs?: number;
|
|
23
34
|
sessionFile?: string;
|
|
24
35
|
shareUrl?: string;
|
|
25
36
|
gistUrl?: string;
|
|
@@ -64,22 +75,21 @@ export default function registerSubagentNotify(pi: ExtensionAPI): void {
|
|
|
64
75
|
? ` (${result.taskIndex + 1}/${result.totalTasks})`
|
|
65
76
|
: "";
|
|
66
77
|
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
}
|
|
78
|
+
const sessionLine = result.shareUrl
|
|
79
|
+
? `Session: ${result.shareUrl}`
|
|
80
|
+
: result.shareError
|
|
81
|
+
? `Session share error: ${result.shareError}`
|
|
82
|
+
: result.sessionFile
|
|
83
|
+
? `Session file: ${result.sessionFile}`
|
|
84
|
+
: undefined;
|
|
75
85
|
|
|
76
86
|
const displaySummary = summary.trim() ? summary : "(no output)";
|
|
77
87
|
const content = [
|
|
78
88
|
`Background task ${status}: **${agent}**${taskInfo}`,
|
|
79
89
|
"",
|
|
80
90
|
displaySummary,
|
|
81
|
-
|
|
82
|
-
|
|
91
|
+
sessionLine ? "" : undefined,
|
|
92
|
+
sessionLine,
|
|
83
93
|
]
|
|
84
94
|
.filter((line) => line !== undefined)
|
|
85
95
|
.join("\n");
|
package/package.json
CHANGED
package/pi-args.ts
CHANGED
|
@@ -43,6 +43,7 @@ export function buildPiArgs(input: BuildPiArgsInput): BuildPiArgsResult {
|
|
|
43
43
|
const args = [...input.baseArgs];
|
|
44
44
|
|
|
45
45
|
if (input.sessionFile) {
|
|
46
|
+
fs.mkdirSync(path.dirname(input.sessionFile), { recursive: true });
|
|
46
47
|
args.push("--session", input.sessionFile);
|
|
47
48
|
} else {
|
|
48
49
|
if (!input.sessionEnabled) {
|
package/render.ts
CHANGED
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
WIDGET_KEY,
|
|
14
14
|
} from "./types.ts";
|
|
15
15
|
import { formatTokens, formatUsage, formatDuration, formatToolCall, shortenPath } from "./formatters.ts";
|
|
16
|
-
import { getDisplayItems, getLastActivity,
|
|
16
|
+
import { getDisplayItems, getLastActivity, getSingleResultOutput } from "./utils.ts";
|
|
17
17
|
|
|
18
18
|
type Theme = ExtensionContext["ui"]["theme"];
|
|
19
19
|
|
|
@@ -79,12 +79,49 @@ function truncLine(text: string, maxWidth: number): string {
|
|
|
79
79
|
return result + activeStyles.join("") + "…";
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
|
|
82
|
+
const SPINNER = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
83
|
+
const WIDGET_ANIMATION_MS = 80;
|
|
83
84
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
85
|
+
let widgetTimer: ReturnType<typeof setInterval> | undefined;
|
|
86
|
+
let latestWidgetCtx: ExtensionContext | undefined;
|
|
87
|
+
let latestWidgetJobs: AsyncJobState[] = [];
|
|
88
|
+
|
|
89
|
+
const resultAnimationTimers = new Map<ReturnType<typeof setInterval>, ResultAnimationContext["state"]>();
|
|
90
|
+
const outputActivityCache = new Map<string, { checkedAt: number; text: string }>();
|
|
91
|
+
|
|
92
|
+
export interface ResultAnimationContext {
|
|
93
|
+
state: { subagentResultAnimationTimer?: ReturnType<typeof setInterval> };
|
|
94
|
+
invalidate: () => void;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function spinnerFrame(): string {
|
|
98
|
+
return SPINNER[Math.floor(Date.now() / WIDGET_ANIMATION_MS) % SPINNER.length]!;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function resultIsRunning(result: AgentToolResult<Details>): boolean {
|
|
102
|
+
return result.details?.progress?.some((entry) => entry.status === "running")
|
|
103
|
+
|| result.details?.results.some((entry) => entry.progress?.status === "running")
|
|
104
|
+
|| false;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function stopResultAnimation(context: ResultAnimationContext): void {
|
|
108
|
+
const timer = context.state.subagentResultAnimationTimer;
|
|
109
|
+
if (!timer) return;
|
|
110
|
+
clearInterval(timer);
|
|
111
|
+
resultAnimationTimers.delete(timer);
|
|
112
|
+
context.state.subagentResultAnimationTimer = undefined;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function syncResultAnimation(result: AgentToolResult<Details>, context: ResultAnimationContext): void {
|
|
116
|
+
if (!resultIsRunning(result)) {
|
|
117
|
+
stopResultAnimation(context);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
if (context.state.subagentResultAnimationTimer) return;
|
|
121
|
+
const timer = setInterval(() => context.invalidate(), WIDGET_ANIMATION_MS);
|
|
122
|
+
timer.unref?.();
|
|
123
|
+
context.state.subagentResultAnimationTimer = timer;
|
|
124
|
+
resultAnimationTimers.set(timer, context.state);
|
|
88
125
|
}
|
|
89
126
|
|
|
90
127
|
function extractOutputTarget(task: string): string | undefined {
|
|
@@ -146,67 +183,340 @@ function buildLiveStatusLine(progress: Pick<AgentProgress, "activityState" | "la
|
|
|
146
183
|
return formatActivityLabel(progress.lastActivityAt, progress.activityState === "needs_attention");
|
|
147
184
|
}
|
|
148
185
|
|
|
186
|
+
function themeBold(theme: Theme, text: string): string {
|
|
187
|
+
return ((theme as { bold?: (value: string) => string }).bold?.(text)) ?? text;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function statJoin(theme: Theme, parts: string[]): string {
|
|
191
|
+
return parts.filter(Boolean).map((part) => theme.fg("dim", part)).join(` ${theme.fg("dim", "·")} `);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function formatTokenStat(tokens: number): string {
|
|
195
|
+
return `${formatTokens(tokens)} token`;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function formatToolUseStat(count: number): string {
|
|
199
|
+
return `${count} tool use${count === 1 ? "" : "s"}`;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function formatProgressStats(theme: Theme, progress: Pick<AgentProgress, "toolCount" | "tokens" | "durationMs"> | undefined, includeDuration = true): string {
|
|
203
|
+
if (!progress) return "";
|
|
204
|
+
const parts: string[] = [];
|
|
205
|
+
if (progress.toolCount > 0) parts.push(formatToolUseStat(progress.toolCount));
|
|
206
|
+
if (progress.tokens > 0) parts.push(formatTokenStat(progress.tokens));
|
|
207
|
+
if (includeDuration && progress.durationMs > 0) parts.push(formatDuration(progress.durationMs));
|
|
208
|
+
return statJoin(theme, parts);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function firstOutputLine(text: string): string {
|
|
212
|
+
return text.split("\n").find((line) => line.trim())?.trim() ?? "";
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function resultStatusLine(result: Details["results"][number], output: string): string {
|
|
216
|
+
if (result.detached) return result.detachedReason ? `Detached: ${result.detachedReason}` : "Detached";
|
|
217
|
+
if (result.interrupted) return "Paused";
|
|
218
|
+
if (result.exitCode !== 0) return `Error: ${result.error ?? (firstOutputLine(output) || `exit ${result.exitCode}`)}`;
|
|
219
|
+
if (hasEmptyTextOutputWithoutOutputTarget(result.task, output)) return "Done (no text output)";
|
|
220
|
+
return "Done";
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function resultGlyph(result: Details["results"][number], output: string, theme: Theme, running = result.progress?.status === "running"): string {
|
|
224
|
+
if (running) return theme.fg("accent", spinnerFrame());
|
|
225
|
+
if (result.detached) return theme.fg("warning", "■");
|
|
226
|
+
if (result.interrupted) return theme.fg("warning", "■");
|
|
227
|
+
if (result.exitCode !== 0) return theme.fg("error", "✗");
|
|
228
|
+
if (hasEmptyTextOutputWithoutOutputTarget(result.task, output)) return theme.fg("warning", "✓");
|
|
229
|
+
return theme.fg("success", "✓");
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function compactCurrentActivity(progress: AgentProgress): string {
|
|
233
|
+
return formatCurrentToolLine(progress, getTermWidth() - 4, false) ?? buildLiveStatusLine(progress) ?? "thinking…";
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function hasAnimatedWidgetJobs(jobs: AsyncJobState[]): boolean {
|
|
237
|
+
return jobs.some((job) => job.status === "running");
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function widgetJobName(job: AsyncJobState): string {
|
|
241
|
+
if (job.agents?.length) return job.agents.join(" → ");
|
|
242
|
+
return job.mode ?? "subagent";
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function getCachedLastActivity(outputFile: string | undefined): string {
|
|
246
|
+
if (!outputFile) return "";
|
|
247
|
+
const now = Date.now();
|
|
248
|
+
const cached = outputActivityCache.get(outputFile);
|
|
249
|
+
if (cached && now - cached.checkedAt < 1000) return cached.text;
|
|
250
|
+
const text = getLastActivity(outputFile);
|
|
251
|
+
outputActivityCache.set(outputFile, { checkedAt: now, text });
|
|
252
|
+
return text;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function widgetActivity(job: AsyncJobState): string {
|
|
256
|
+
if (job.currentTool && job.currentToolStartedAt !== undefined) {
|
|
257
|
+
return `${job.currentTool} ${formatDuration(Math.max(0, Date.now() - job.currentToolStartedAt))}`;
|
|
258
|
+
}
|
|
259
|
+
const activity = formatActivityLabel(job.lastActivityAt, job.activityState === "needs_attention")
|
|
260
|
+
?? (job.status === "running" ? getCachedLastActivity(job.outputFile) : "");
|
|
261
|
+
if (activity) return activity;
|
|
262
|
+
if (job.status === "queued") return "queued…";
|
|
263
|
+
if (job.status === "paused") return "Paused";
|
|
264
|
+
if (job.status === "failed") return "Failed";
|
|
265
|
+
return "Done";
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function widgetStatusGlyph(job: AsyncJobState, theme: Theme): string {
|
|
269
|
+
if (job.status === "running") return theme.fg("accent", spinnerFrame());
|
|
270
|
+
if (job.status === "queued") return theme.fg("muted", "◦");
|
|
271
|
+
if (job.status === "complete") return theme.fg("success", "✓");
|
|
272
|
+
if (job.status === "paused") return theme.fg("warning", "■");
|
|
273
|
+
return theme.fg("error", "✗");
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function widgetStats(job: AsyncJobState, theme: Theme): string {
|
|
277
|
+
const parts: string[] = [];
|
|
278
|
+
const stepsTotal = job.stepsTotal ?? (job.agents?.length ?? 1);
|
|
279
|
+
if (job.currentStep !== undefined) parts.push(`step ${job.currentStep + 1}/${stepsTotal}`);
|
|
280
|
+
else if (stepsTotal > 1) parts.push(`steps ${stepsTotal}`);
|
|
281
|
+
if (job.totalTokens?.total) parts.push(formatTokenStat(job.totalTokens.total));
|
|
282
|
+
const endTime = job.status === "complete" || job.status === "failed" || job.status === "paused" ? (job.updatedAt ?? Date.now()) : Date.now();
|
|
283
|
+
if (job.startedAt) parts.push(formatDuration(Math.max(0, endTime - job.startedAt)));
|
|
284
|
+
return statJoin(theme, parts);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
export function buildWidgetLines(jobs: AsyncJobState[], theme: Theme, width = getTermWidth()): string[] {
|
|
288
|
+
if (jobs.length === 0) return [];
|
|
289
|
+
const running = jobs.filter((job) => job.status === "running");
|
|
290
|
+
const queued = jobs.filter((job) => job.status === "queued");
|
|
291
|
+
const finished = jobs.filter((job) => job.status !== "running" && job.status !== "queued");
|
|
292
|
+
|
|
293
|
+
const lines: string[] = [];
|
|
294
|
+
const hasActive = running.length > 0 || queued.length > 0;
|
|
295
|
+
lines.push(truncLine(`${theme.fg(hasActive ? "accent" : "dim", hasActive ? "●" : "○")} ${theme.fg(hasActive ? "accent" : "dim", "Agents")} ${theme.fg("dim", "· /subagents-status")}`, width));
|
|
296
|
+
|
|
297
|
+
const items: string[][] = [];
|
|
298
|
+
let hiddenRunning = 0;
|
|
299
|
+
let hiddenFinished = 0;
|
|
300
|
+
let queuedSummaryShown = false;
|
|
301
|
+
let slots = MAX_WIDGET_JOBS;
|
|
302
|
+
|
|
303
|
+
for (const job of running) {
|
|
304
|
+
if (slots <= 0) { hiddenRunning++; continue; }
|
|
305
|
+
const stats = widgetStats(job, theme);
|
|
306
|
+
items.push([
|
|
307
|
+
`${widgetStatusGlyph(job, theme)} ${themeBold(theme, widgetJobName(job))}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`,
|
|
308
|
+
` ${theme.fg("dim", `⎿ ${widgetActivity(job)}`)}`,
|
|
309
|
+
]);
|
|
310
|
+
slots--;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (queued.length > 0 && slots > 0) {
|
|
314
|
+
items.push([`${theme.fg("muted", "◦")} ${theme.fg("dim", `${queued.length} queued`)}`]);
|
|
315
|
+
queuedSummaryShown = true;
|
|
316
|
+
slots--;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
for (const job of finished) {
|
|
320
|
+
if (slots <= 0) { hiddenFinished++; continue; }
|
|
321
|
+
const stats = widgetStats(job, theme);
|
|
322
|
+
items.push([
|
|
323
|
+
`${widgetStatusGlyph(job, theme)} ${themeBold(theme, widgetJobName(job))}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`,
|
|
324
|
+
` ${theme.fg("dim", `⎿ ${widgetActivity(job)}`)}`,
|
|
325
|
+
]);
|
|
326
|
+
slots--;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const hiddenQueued = queued.length > 0 && !queuedSummaryShown ? queued.length : 0;
|
|
330
|
+
const hiddenTotal = hiddenRunning + hiddenFinished + hiddenQueued;
|
|
331
|
+
if (hiddenTotal > 0) {
|
|
332
|
+
const parts: string[] = [];
|
|
333
|
+
if (hiddenRunning > 0) parts.push(`${hiddenRunning} running`);
|
|
334
|
+
if (hiddenQueued > 0) parts.push(`${hiddenQueued} queued`);
|
|
335
|
+
if (hiddenFinished > 0) parts.push(`${hiddenFinished} finished`);
|
|
336
|
+
items.push([theme.fg("dim", `+${hiddenTotal} more (${parts.join(", ")})`)]);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
for (let i = 0; i < items.length; i++) {
|
|
340
|
+
const item = items[i]!;
|
|
341
|
+
const last = i === items.length - 1;
|
|
342
|
+
const branch = last ? "└─" : "├─";
|
|
343
|
+
const continuation = last ? " " : "│ ";
|
|
344
|
+
lines.push(truncLine(`${theme.fg("dim", branch)} ${item[0]}`, width));
|
|
345
|
+
for (const detail of item.slice(1)) {
|
|
346
|
+
lines.push(truncLine(`${theme.fg("dim", continuation)} ${detail}`, width));
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
return lines;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function refreshAnimatedWidget(): void {
|
|
354
|
+
if (!latestWidgetCtx?.hasUI || latestWidgetJobs.length === 0) return;
|
|
355
|
+
latestWidgetCtx.ui.setWidget(WIDGET_KEY, buildWidgetLines(latestWidgetJobs, latestWidgetCtx.ui.theme));
|
|
356
|
+
latestWidgetCtx.ui.requestRender?.();
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function ensureWidgetAnimation(): void {
|
|
360
|
+
if (widgetTimer) return;
|
|
361
|
+
widgetTimer = setInterval(() => {
|
|
362
|
+
if (!hasAnimatedWidgetJobs(latestWidgetJobs)) {
|
|
363
|
+
stopWidgetAnimation();
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
refreshAnimatedWidget();
|
|
367
|
+
}, WIDGET_ANIMATION_MS);
|
|
368
|
+
widgetTimer.unref?.();
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
export function stopWidgetAnimation(): void {
|
|
372
|
+
if (widgetTimer) {
|
|
373
|
+
clearInterval(widgetTimer);
|
|
374
|
+
widgetTimer = undefined;
|
|
375
|
+
}
|
|
376
|
+
latestWidgetCtx = undefined;
|
|
377
|
+
latestWidgetJobs = [];
|
|
378
|
+
outputActivityCache.clear();
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
export function stopResultAnimations(): void {
|
|
382
|
+
for (const [timer, state] of resultAnimationTimers) {
|
|
383
|
+
clearInterval(timer);
|
|
384
|
+
state.subagentResultAnimationTimer = undefined;
|
|
385
|
+
}
|
|
386
|
+
resultAnimationTimers.clear();
|
|
387
|
+
}
|
|
388
|
+
|
|
149
389
|
/**
|
|
150
390
|
* Render the async jobs widget
|
|
151
391
|
*/
|
|
152
392
|
export function renderWidget(ctx: ExtensionContext, jobs: AsyncJobState[]): void {
|
|
153
|
-
if (!ctx.hasUI) return;
|
|
154
393
|
if (jobs.length === 0) {
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
ctx.ui.setWidget(WIDGET_KEY, undefined);
|
|
158
|
-
}
|
|
394
|
+
stopWidgetAnimation();
|
|
395
|
+
if (ctx.hasUI) ctx.ui.setWidget(WIDGET_KEY, undefined);
|
|
159
396
|
return;
|
|
160
397
|
}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
const hasRunningJobs = displayedJobs.some(job => job.status === "running");
|
|
164
|
-
const newHash = computeWidgetHash(jobs);
|
|
165
|
-
if (!hasRunningJobs && newHash === lastWidgetHash) {
|
|
398
|
+
if (!ctx.hasUI) {
|
|
399
|
+
stopWidgetAnimation();
|
|
166
400
|
return;
|
|
167
401
|
}
|
|
168
|
-
|
|
402
|
+
latestWidgetCtx = ctx;
|
|
403
|
+
latestWidgetJobs = [...jobs];
|
|
169
404
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
const
|
|
190
|
-
|
|
191
|
-
const
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
405
|
+
ctx.ui.setWidget(WIDGET_KEY, buildWidgetLines(jobs, ctx.ui.theme));
|
|
406
|
+
if (hasAnimatedWidgetJobs(jobs)) ensureWidgetAnimation();
|
|
407
|
+
else stopWidgetAnimation();
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function renderSingleCompact(d: Details, r: Details["results"][number], theme: Theme): Component {
|
|
411
|
+
const output = r.truncation?.text || getSingleResultOutput(r);
|
|
412
|
+
const progress = r.progress || r.progressSummary;
|
|
413
|
+
const isRunning = r.progress?.status === "running";
|
|
414
|
+
const contextBadge = d.context === "fork" ? theme.fg("warning", " [fork]") : "";
|
|
415
|
+
const stats = statJoin(theme, [
|
|
416
|
+
r.usage?.turns ? `⟳${r.usage.turns}` : "",
|
|
417
|
+
formatProgressStats(theme, progress),
|
|
418
|
+
]);
|
|
419
|
+
const c = new Container();
|
|
420
|
+
const width = getTermWidth() - 4;
|
|
421
|
+
c.addChild(new Text(truncLine(`${resultGlyph(r, output, theme, isRunning)} ${theme.fg("toolTitle", theme.bold(r.agent))}${contextBadge}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`, width), 0, 0));
|
|
422
|
+
|
|
423
|
+
if (isRunning && r.progress) {
|
|
424
|
+
const activity = compactCurrentActivity(r.progress);
|
|
425
|
+
c.addChild(new Text(truncLine(theme.fg("dim", ` ⎿ ${activity}`), width), 0, 0));
|
|
426
|
+
const liveStatus = buildLiveStatusLine(r.progress);
|
|
427
|
+
if (liveStatus && liveStatus !== activity) c.addChild(new Text(truncLine(theme.fg("dim", ` ${liveStatus}`), width), 0, 0));
|
|
428
|
+
c.addChild(new Text(truncLine(theme.fg("accent", " Press Ctrl+O for live detail"), width), 0, 0));
|
|
429
|
+
if (r.artifactPaths) c.addChild(new Text(truncLine(theme.fg("dim", ` output: ${shortenPath(r.artifactPaths.outputPath)}`), width), 0, 0));
|
|
430
|
+
return c;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
c.addChild(new Text(truncLine(theme.fg("dim", ` ⎿ ${resultStatusLine(r, output)}`), width), 0, 0));
|
|
434
|
+
const preview = firstOutputLine(output);
|
|
435
|
+
if (preview && r.exitCode === 0 && !hasEmptyTextOutputWithoutOutputTarget(r.task, output)) {
|
|
436
|
+
c.addChild(new Text(truncLine(theme.fg("dim", ` ${preview}`), width), 0, 0));
|
|
437
|
+
}
|
|
438
|
+
if (r.sessionFile) c.addChild(new Text(truncLine(theme.fg("dim", ` session: ${shortenPath(r.sessionFile)}`), width), 0, 0));
|
|
439
|
+
if (r.artifactPaths) c.addChild(new Text(truncLine(theme.fg("dim", ` output: ${shortenPath(r.artifactPaths.outputPath)}`), width), 0, 0));
|
|
440
|
+
if (r.truncation?.artifactPath) c.addChild(new Text(truncLine(theme.fg("dim", ` full output: ${shortenPath(r.truncation.artifactPath)}`), width), 0, 0));
|
|
441
|
+
return c;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function renderMultiCompact(d: Details, theme: Theme): Component {
|
|
445
|
+
const hasRunning = d.progress?.some((p) => p.status === "running")
|
|
446
|
+
|| d.results.some((r) => r.progress?.status === "running");
|
|
447
|
+
const ok = d.results.filter((r) =>
|
|
448
|
+
!r.interrupted
|
|
449
|
+
&& !r.detached
|
|
450
|
+
&& (r.progress?.status === "completed" || (r.exitCode === 0 && r.progress?.status !== "running" && r.progress?.status !== "pending"))
|
|
451
|
+
).length;
|
|
452
|
+
const failed = d.results.some((r) => r.exitCode !== 0 && r.progress?.status !== "running");
|
|
453
|
+
const paused = d.results.some((r) => (r.interrupted || r.detached) && r.progress?.status !== "running");
|
|
454
|
+
let totalSummary = d.progressSummary;
|
|
455
|
+
if (!totalSummary) {
|
|
456
|
+
let sawProgress = false;
|
|
457
|
+
const summary = { toolCount: 0, tokens: 0, durationMs: 0 };
|
|
458
|
+
for (const r of d.results) {
|
|
459
|
+
const prog = r.progress || r.progressSummary;
|
|
460
|
+
if (!prog) continue;
|
|
461
|
+
sawProgress = true;
|
|
462
|
+
summary.toolCount += prog.toolCount;
|
|
463
|
+
summary.tokens += prog.tokens;
|
|
464
|
+
summary.durationMs = d.mode === "chain" ? summary.durationMs + prog.durationMs : Math.max(summary.durationMs, prog.durationMs);
|
|
206
465
|
}
|
|
466
|
+
if (sawProgress) totalSummary = summary;
|
|
207
467
|
}
|
|
468
|
+
const hasParallelInChain = d.chainAgents?.some((a) => a.startsWith("["));
|
|
469
|
+
const totalCount = hasParallelInChain ? d.results.length : (d.totalSteps ?? d.results.length);
|
|
470
|
+
const currentStep = d.currentStepIndex !== undefined ? d.currentStepIndex + 1 : Math.min(totalCount, ok + (hasRunning ? 1 : 0));
|
|
471
|
+
const itemLabel = d.mode === "parallel" ? "agent" : "step";
|
|
472
|
+
const itemTitle = d.mode === "parallel" ? "Agent" : "Step";
|
|
473
|
+
const stepInfo = hasRunning ? `${itemLabel} ${currentStep}/${totalCount}` : `${itemLabel} ${ok}/${totalCount}`;
|
|
474
|
+
const stats = statJoin(theme, [stepInfo, formatProgressStats(theme, totalSummary)]);
|
|
475
|
+
const glyph = hasRunning
|
|
476
|
+
? theme.fg("accent", spinnerFrame())
|
|
477
|
+
: failed
|
|
478
|
+
? theme.fg("error", "✗")
|
|
479
|
+
: paused
|
|
480
|
+
? theme.fg("warning", "■")
|
|
481
|
+
: theme.fg("success", "✓");
|
|
482
|
+
const contextBadge = d.context === "fork" ? theme.fg("warning", " [fork]") : "";
|
|
483
|
+
const c = new Container();
|
|
484
|
+
const width = getTermWidth() - 4;
|
|
485
|
+
c.addChild(new Text(truncLine(`${glyph} ${theme.fg("toolTitle", theme.bold(d.mode))}${contextBadge}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`, width), 0, 0));
|
|
208
486
|
|
|
209
|
-
|
|
487
|
+
const useResultsDirectly = hasParallelInChain || !d.chainAgents?.length;
|
|
488
|
+
const stepsToShow = useResultsDirectly ? d.results.length : d.chainAgents!.length;
|
|
489
|
+
for (let i = 0; i < stepsToShow; i++) {
|
|
490
|
+
const r = d.results[i];
|
|
491
|
+
const agentName = useResultsDirectly ? (r?.agent || `${itemLabel}-${i + 1}`) : (d.chainAgents![i] || r?.agent || `${itemLabel}-${i + 1}`);
|
|
492
|
+
if (!r) {
|
|
493
|
+
c.addChild(new Text(truncLine(theme.fg("dim", ` ◦ ${itemTitle} ${i + 1}: ${agentName} · pending`), width), 0, 0));
|
|
494
|
+
continue;
|
|
495
|
+
}
|
|
496
|
+
const output = getSingleResultOutput(r);
|
|
497
|
+
const progressFromArray = d.progress?.find((p) => p.index === i) || d.progress?.find((p) => p.agent === r.agent && p.status === "running");
|
|
498
|
+
const rProg = r.progress || progressFromArray || r.progressSummary;
|
|
499
|
+
const rRunning = rProg && "status" in rProg && rProg.status === "running";
|
|
500
|
+
const rPending = rProg && "status" in rProg && rProg.status === "pending";
|
|
501
|
+
const stepNumber = r.progress?.index !== undefined ? r.progress.index + 1 : progressFromArray?.index !== undefined ? progressFromArray.index + 1 : i + 1;
|
|
502
|
+
const stepStats = formatProgressStats(theme, rProg);
|
|
503
|
+
const glyph = rPending ? theme.fg("dim", "◦") : resultGlyph(r, output, theme, rRunning);
|
|
504
|
+
const pendingLabel = rPending ? ` ${theme.fg("dim", "· pending")}` : "";
|
|
505
|
+
const line = `${glyph} ${itemTitle} ${stepNumber}: ${themeBold(theme, agentName)}${stepStats ? ` ${theme.fg("dim", "·")} ${stepStats}` : ""}${pendingLabel}`;
|
|
506
|
+
c.addChild(new Text(truncLine(` ${line}`, width), 0, 0));
|
|
507
|
+
if (rRunning && rProg && "status" in rProg) {
|
|
508
|
+
const activity = compactCurrentActivity(rProg);
|
|
509
|
+
c.addChild(new Text(truncLine(theme.fg("dim", ` ⎿ ${activity}`), width), 0, 0));
|
|
510
|
+
c.addChild(new Text(truncLine(theme.fg("accent", " Press Ctrl+O for live detail"), width), 0, 0));
|
|
511
|
+
} else if (!rPending && (r.exitCode !== 0 || r.interrupted || r.detached || hasEmptyTextOutputWithoutOutputTarget(r.task, output))) {
|
|
512
|
+
c.addChild(new Text(truncLine(theme.fg(r.exitCode !== 0 ? "error" : "dim", ` ⎿ ${resultStatusLine(r, output)}`), width), 0, 0));
|
|
513
|
+
}
|
|
514
|
+
const outputTarget = extractOutputTarget(r.task);
|
|
515
|
+
if (outputTarget) c.addChild(new Text(truncLine(theme.fg("dim", ` output: ${outputTarget}`), width), 0, 0));
|
|
516
|
+
if (r.artifactPaths) c.addChild(new Text(truncLine(theme.fg("dim", ` output: ${shortenPath(r.artifactPaths.outputPath)}`), width), 0, 0));
|
|
517
|
+
}
|
|
518
|
+
if (d.artifacts) c.addChild(new Text(truncLine(theme.fg("dim", ` artifacts: ${shortenPath(d.artifacts.dir)}`), width), 0, 0));
|
|
519
|
+
return c;
|
|
210
520
|
}
|
|
211
521
|
|
|
212
522
|
/**
|
|
@@ -230,6 +540,7 @@ export function renderSubagentResult(
|
|
|
230
540
|
|
|
231
541
|
if (d.mode === "single" && d.results.length === 1) {
|
|
232
542
|
const r = d.results[0];
|
|
543
|
+
if (!expanded) return renderSingleCompact(d, r, theme);
|
|
233
544
|
const isRunning = r.progress?.status === "running";
|
|
234
545
|
const icon = isRunning
|
|
235
546
|
? theme.fg("warning", "running")
|
|
@@ -322,6 +633,8 @@ export function renderSubagentResult(
|
|
|
322
633
|
return c;
|
|
323
634
|
}
|
|
324
635
|
|
|
636
|
+
if (!expanded) return renderMultiCompact(d, theme);
|
|
637
|
+
|
|
325
638
|
const hasRunning = d.progress?.some((p) => p.status === "running")
|
|
326
639
|
|| d.results.some((r) => r.progress?.status === "running");
|
|
327
640
|
const ok = d.results.filter((r) => r.progress?.status === "completed" || (r.exitCode === 0 && r.progress?.status !== "running")).length;
|
|
@@ -367,6 +680,7 @@ export function renderSubagentResult(
|
|
|
367
680
|
const totalCount = hasParallelInChain ? d.results.length : (d.totalSteps ?? d.results.length);
|
|
368
681
|
const currentStep = d.currentStepIndex !== undefined ? d.currentStepIndex + 1 : ok + 1;
|
|
369
682
|
const stepInfo = hasRunning ? ` ${currentStep}/${totalCount}` : ` ${ok}/${totalCount}`;
|
|
683
|
+
const itemTitle = d.mode === "parallel" ? "Agent" : "Step";
|
|
370
684
|
|
|
371
685
|
const chainVis = d.chainAgents?.length && !hasParallelInChain
|
|
372
686
|
? d.chainAgents
|
|
@@ -418,7 +732,7 @@ export function renderSubagentResult(
|
|
|
418
732
|
: (d.chainAgents![i] || r?.agent || `step-${i + 1}`);
|
|
419
733
|
|
|
420
734
|
if (!r) {
|
|
421
|
-
c.addChild(new Text(fit(theme.fg("dim", `
|
|
735
|
+
c.addChild(new Text(fit(theme.fg("dim", ` ${itemTitle} ${i + 1}: ${agentName}`)), 0, 0));
|
|
422
736
|
c.addChild(new Text(theme.fg("dim", ` status: pending`), 0, 0));
|
|
423
737
|
c.addChild(new Spacer(1));
|
|
424
738
|
continue;
|
|
@@ -441,8 +755,8 @@ export function renderSubagentResult(
|
|
|
441
755
|
const stats = rProg ? ` | ${rProg.toolCount} tools, ${formatDuration(rProg.durationMs)}` : "";
|
|
442
756
|
const modelDisplay = r.model ? theme.fg("dim", ` (${r.model})`) : "";
|
|
443
757
|
const stepHeader = rRunning
|
|
444
|
-
? `${statusIcon}
|
|
445
|
-
: `${statusIcon}
|
|
758
|
+
? `${statusIcon} ${itemTitle} ${stepNumber}: ${theme.bold(theme.fg("warning", r.agent))}${modelDisplay}${stats}`
|
|
759
|
+
: `${statusIcon} ${itemTitle} ${stepNumber}: ${theme.bold(r.agent)}${modelDisplay}${stats}`;
|
|
446
760
|
const toolCallLines = getToolCallLines(r, expanded);
|
|
447
761
|
c.addChild(new Text(fit(stepHeader), 0, 0));
|
|
448
762
|
|
package/schemas.ts
CHANGED
|
@@ -4,8 +4,22 @@
|
|
|
4
4
|
|
|
5
5
|
import { Type } from "typebox";
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
const SkillOverride = Type.Unsafe({
|
|
8
|
+
type: ["string", "array", "boolean"],
|
|
9
|
+
items: { type: "string" },
|
|
10
|
+
description: "Skill name(s) to inject (comma-separated), array of strings, or boolean (false disables, true uses default)",
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
const OutputOverride = Type.Unsafe({
|
|
14
|
+
type: ["string", "boolean"],
|
|
15
|
+
description: "Output filename/path (string), or false to disable file output",
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const ReadsOverride = Type.Unsafe({
|
|
19
|
+
type: ["array", "boolean"],
|
|
20
|
+
items: { type: "string" },
|
|
21
|
+
description: "Files to read before running (array of filenames), or false to disable",
|
|
22
|
+
});
|
|
9
23
|
|
|
10
24
|
export const TaskItem = Type.Object({
|
|
11
25
|
agent: Type.String(),
|
|
@@ -23,8 +37,8 @@ export const SequentialStepSchema = Type.Object({
|
|
|
23
37
|
description: "Task template with variables: {task}=original request, {previous}=prior step's text response, {chain_dir}=shared folder. Required for first step, defaults to '{previous}' for subsequent steps."
|
|
24
38
|
})),
|
|
25
39
|
cwd: Type.Optional(Type.String()),
|
|
26
|
-
output: Type.Optional(
|
|
27
|
-
reads: Type.Optional(
|
|
40
|
+
output: Type.Optional(OutputOverride),
|
|
41
|
+
reads: Type.Optional(ReadsOverride),
|
|
28
42
|
progress: Type.Optional(Type.Boolean({ description: "Enable progress.md tracking in {chain_dir}" })),
|
|
29
43
|
skill: Type.Optional(SkillOverride),
|
|
30
44
|
model: Type.Optional(Type.String({ description: "Override model for this step" })),
|
|
@@ -36,8 +50,8 @@ export const ParallelTaskSchema = Type.Object({
|
|
|
36
50
|
task: Type.Optional(Type.String({ description: "Task template with {task}, {previous}, {chain_dir} variables. Defaults to {previous}." })),
|
|
37
51
|
cwd: Type.Optional(Type.String()),
|
|
38
52
|
count: Type.Optional(Type.Integer({ minimum: 1, description: "Repeat this parallel task N times with the same settings." })),
|
|
39
|
-
output: Type.Optional(
|
|
40
|
-
reads: Type.Optional(
|
|
53
|
+
output: Type.Optional(OutputOverride),
|
|
54
|
+
reads: Type.Optional(ReadsOverride),
|
|
41
55
|
progress: Type.Optional(Type.Boolean({ description: "Enable progress.md tracking in {chain_dir}" })),
|
|
42
56
|
skill: Type.Optional(SkillOverride),
|
|
43
57
|
model: Type.Optional(Type.String({ description: "Override model for this task" })),
|
|
@@ -53,9 +67,25 @@ export const ParallelStepSchema = Type.Object({
|
|
|
53
67
|
})),
|
|
54
68
|
});
|
|
55
69
|
|
|
56
|
-
//
|
|
57
|
-
|
|
58
|
-
|
|
70
|
+
// Flattened so providers that reject anyOf/oneOf can still accept either sequential or parallel steps.
|
|
71
|
+
export const ChainItem = Type.Object({
|
|
72
|
+
agent: Type.Optional(Type.String({ description: "Sequential step agent name" })),
|
|
73
|
+
task: Type.Optional(Type.String({
|
|
74
|
+
description: "Task template with variables: {task}=original request, {previous}=prior step's text response, {chain_dir}=shared folder. Required for first step, defaults to '{previous}' for subsequent steps."
|
|
75
|
+
})),
|
|
76
|
+
cwd: Type.Optional(Type.String()),
|
|
77
|
+
output: Type.Optional(OutputOverride),
|
|
78
|
+
reads: Type.Optional(ReadsOverride),
|
|
79
|
+
progress: Type.Optional(Type.Boolean({ description: "Enable progress.md tracking in {chain_dir}" })),
|
|
80
|
+
skill: Type.Optional(SkillOverride),
|
|
81
|
+
model: Type.Optional(Type.String({ description: "Override model for this step" })),
|
|
82
|
+
parallel: Type.Optional(Type.Array(ParallelTaskSchema, { minItems: 1, description: "Tasks to run in parallel" })),
|
|
83
|
+
concurrency: Type.Optional(Type.Number({ description: "Max concurrent tasks (default: 4)" })),
|
|
84
|
+
failFast: Type.Optional(Type.Boolean({ description: "Stop on first failure (default: false)" })),
|
|
85
|
+
worktree: Type.Optional(Type.Boolean({
|
|
86
|
+
description: "Create isolated git worktrees for each parallel task."
|
|
87
|
+
})),
|
|
88
|
+
}, { description: "Chain step: use {agent, task?, ...} for sequential or {parallel: [...]} for concurrent execution" });
|
|
59
89
|
|
|
60
90
|
export const ControlOverrides = Type.Object({
|
|
61
91
|
enabled: Type.Optional(Type.Boolean({ description: "Enable/disable subagent control attention tracking for this run" })),
|
|
@@ -70,7 +100,7 @@ export const ControlOverrides = Type.Object({
|
|
|
70
100
|
|
|
71
101
|
export const SubagentParams = Type.Object({
|
|
72
102
|
agent: Type.Optional(Type.String({ description: "Agent name (SINGLE mode) or target for management get/update/delete" })),
|
|
73
|
-
task: Type.Optional(Type.String({ description: "Task (SINGLE mode)" })),
|
|
103
|
+
task: Type.Optional(Type.String({ description: "Task (SINGLE mode, optional for self-contained agents)" })),
|
|
74
104
|
// Management action (when present, tool operates in management mode)
|
|
75
105
|
action: Type.Optional(Type.String({
|
|
76
106
|
description: "Action: management ('list','get','create','update','delete') or control ('status','interrupt'). Omit for execution mode."
|
|
@@ -89,8 +119,10 @@ export const SubagentParams = Type.Object({
|
|
|
89
119
|
description: "Chain name for get/update/delete management actions"
|
|
90
120
|
})),
|
|
91
121
|
// Agent/chain configuration for create/update (nested to avoid conflicts with execution fields)
|
|
92
|
-
config: Type.Optional(Type.
|
|
93
|
-
|
|
122
|
+
config: Type.Optional(Type.Unsafe({
|
|
123
|
+
type: ["object", "string"],
|
|
124
|
+
additionalProperties: true,
|
|
125
|
+
description: "Agent or chain config for create/update. Agent: name, description, scope ('user'|'project', default 'user'), systemPrompt, systemPromptMode, inheritProjectContext, inheritSkills, model, tools (comma-separated), extensions (comma-separated), skills (comma-separated), thinking, output, reads, progress, maxSubagentDepth. Chain: name, description, scope, steps (array of {agent, task?, output?, reads?, model?, skills?, progress?}). Presence of 'steps' creates a chain instead of an agent. String values must be valid JSON."
|
|
94
126
|
})),
|
|
95
127
|
tasks: Type.Optional(Type.Array(TaskItem, { description: "PARALLEL mode: [{agent, task, count?}, ...]" })),
|
|
96
128
|
concurrency: Type.Optional(Type.Integer({ minimum: 1, description: "Top-level PARALLEL mode only: max concurrent tasks. Defaults to config.parallel.concurrency or 4." })),
|
|
@@ -118,7 +150,10 @@ export const SubagentParams = Type.Object({
|
|
|
118
150
|
clarify: Type.Optional(Type.Boolean({ description: "Show TUI to preview/edit before execution (default: true for chains, false for single/parallel). Implies sync mode." })),
|
|
119
151
|
control: Type.Optional(ControlOverrides),
|
|
120
152
|
// Solo agent overrides
|
|
121
|
-
output: Type.Optional(Type.
|
|
153
|
+
output: Type.Optional(Type.Unsafe({
|
|
154
|
+
type: ["string", "boolean"],
|
|
155
|
+
description: "Output file for single agent (string), or false to disable. Relative paths resolve against cwd.",
|
|
156
|
+
})),
|
|
122
157
|
skill: Type.Optional(SkillOverride),
|
|
123
158
|
model: Type.Optional(Type.String({ description: "Override model for single agent (e.g. 'anthropic/claude-sonnet-4')" })),
|
|
124
159
|
});
|
package/skills.ts
CHANGED
|
@@ -210,9 +210,70 @@ function collectSettingsSkillPaths(cwd: string): SkillSearchPath[] {
|
|
|
210
210
|
return results;
|
|
211
211
|
}
|
|
212
212
|
|
|
213
|
+
function isSafePackagePath(value: string): boolean {
|
|
214
|
+
return value.length > 0
|
|
215
|
+
&& !path.isAbsolute(value)
|
|
216
|
+
&& value.split(/[\\/]/).every((part) => part.length > 0 && part !== "." && part !== "..");
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function parseNpmPackageName(source: string): string | undefined {
|
|
220
|
+
const spec = source.slice(4).trim();
|
|
221
|
+
if (!spec) return undefined;
|
|
222
|
+
const match = spec.match(/^(@?[^@]+(?:\/[^@]+)?)(?:@(.+))?$/);
|
|
223
|
+
const packageName = match?.[1] ?? spec;
|
|
224
|
+
return isSafePackagePath(packageName) ? packageName : undefined;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function stripGitRef(repoPath: string): string {
|
|
228
|
+
const atIndex = repoPath.indexOf("@");
|
|
229
|
+
const hashIndex = repoPath.indexOf("#");
|
|
230
|
+
const refIndex = [atIndex, hashIndex].filter((index) => index >= 0).sort((a, b) => a - b)[0];
|
|
231
|
+
return refIndex === undefined ? repoPath : repoPath.slice(0, refIndex);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function parseGitPackagePath(source: string): { host: string; repoPath: string } | undefined {
|
|
235
|
+
const spec = source.slice(4).trim();
|
|
236
|
+
if (!spec) return undefined;
|
|
237
|
+
|
|
238
|
+
let host = "";
|
|
239
|
+
let repoPath = "";
|
|
240
|
+
const scpLike = spec.match(/^git@([^:]+):(.+)$/);
|
|
241
|
+
if (scpLike) {
|
|
242
|
+
host = scpLike[1] ?? "";
|
|
243
|
+
repoPath = scpLike[2] ?? "";
|
|
244
|
+
} else if (/^[a-z][a-z0-9+.-]*:\/\//i.test(spec)) {
|
|
245
|
+
try {
|
|
246
|
+
const url = new URL(spec);
|
|
247
|
+
host = url.hostname;
|
|
248
|
+
repoPath = url.pathname.replace(/^\/+/, "");
|
|
249
|
+
} catch {
|
|
250
|
+
return undefined;
|
|
251
|
+
}
|
|
252
|
+
} else {
|
|
253
|
+
const slashIndex = spec.indexOf("/");
|
|
254
|
+
if (slashIndex < 0) return undefined;
|
|
255
|
+
host = spec.slice(0, slashIndex);
|
|
256
|
+
repoPath = spec.slice(slashIndex + 1);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const normalizedPath = stripGitRef(repoPath).replace(/\.git$/, "").replace(/^\/+/, "");
|
|
260
|
+
if (!host || !isSafePackagePath(host) || !isSafePackagePath(normalizedPath) || normalizedPath.split(/[\\/]/).length < 2) {
|
|
261
|
+
return undefined;
|
|
262
|
+
}
|
|
263
|
+
return { host, repoPath: normalizedPath };
|
|
264
|
+
}
|
|
265
|
+
|
|
213
266
|
function resolveSettingsPackageRoot(source: string, baseDir: string): string | undefined {
|
|
214
267
|
const trimmed = source.trim();
|
|
215
268
|
if (!trimmed) return undefined;
|
|
269
|
+
if (trimmed.startsWith("git:")) {
|
|
270
|
+
const parsed = parseGitPackagePath(trimmed);
|
|
271
|
+
return parsed ? path.join(baseDir, "git", parsed.host, parsed.repoPath) : undefined;
|
|
272
|
+
}
|
|
273
|
+
if (trimmed.startsWith("npm:")) {
|
|
274
|
+
const packageName = parseNpmPackageName(trimmed);
|
|
275
|
+
return packageName ? path.join(baseDir, "npm", "node_modules", packageName) : undefined;
|
|
276
|
+
}
|
|
216
277
|
const normalized = trimmed.startsWith("file:") ? trimmed.slice(5) : trimmed;
|
|
217
278
|
if (normalized === "~") return os.homedir();
|
|
218
279
|
if (normalized.startsWith("~/")) return path.join(os.homedir(), normalized.slice(2));
|
package/slash-commands.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as path from "node:path";
|
|
2
4
|
import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
3
5
|
import { Key, matchesKey } from "@mariozechner/pi-tui";
|
|
4
6
|
import { discoverAgents, discoverAgentsAll } from "./agents.ts";
|
|
@@ -20,6 +22,7 @@ import {
|
|
|
20
22
|
SLASH_SUBAGENT_RESPONSE_EVENT,
|
|
21
23
|
SLASH_SUBAGENT_STARTED_EVENT,
|
|
22
24
|
SLASH_SUBAGENT_UPDATE_EVENT,
|
|
25
|
+
type SingleResult,
|
|
23
26
|
type SubagentState,
|
|
24
27
|
} from "./types.ts";
|
|
25
28
|
|
|
@@ -194,6 +197,46 @@ function extractSlashMessageText(content: string | Array<{ type?: string; text?:
|
|
|
194
197
|
.join("\n");
|
|
195
198
|
}
|
|
196
199
|
|
|
200
|
+
function formatExportPathList(paths: string[]): string {
|
|
201
|
+
return paths.map((file) => `- \`${file}\``).join("\n");
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function collectResultPaths(results: SingleResult[], getPath: (result: SingleResult) => string | undefined): string[] {
|
|
205
|
+
return results
|
|
206
|
+
.map(getPath)
|
|
207
|
+
.filter((file): file is string => typeof file === "string" && file.length > 0);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function buildSlashExportText(response: SlashSubagentResponse): string {
|
|
211
|
+
const output = extractSlashMessageText(response.result.content) || response.errorText || "(no output)";
|
|
212
|
+
const results = response.result.details?.results ?? [];
|
|
213
|
+
const sessionFiles = collectResultPaths(results, (result) => result.sessionFile);
|
|
214
|
+
const savedOutputs = collectResultPaths(results, (result) => result.savedOutputPath);
|
|
215
|
+
const artifactOutputs = collectResultPaths(results, (result) => result.artifactPaths?.outputPath);
|
|
216
|
+
const sections = ["## Subagent result", output];
|
|
217
|
+
if (sessionFiles.length > 0) sections.push("## Child session exports", formatExportPathList(sessionFiles));
|
|
218
|
+
if (savedOutputs.length > 0) sections.push("## Saved outputs", formatExportPathList(savedOutputs));
|
|
219
|
+
if (artifactOutputs.length > 0) sections.push("## Artifact outputs", formatExportPathList(artifactOutputs));
|
|
220
|
+
return sections.join("\n\n");
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function persistSlashSessionSnapshot(ctx: ExtensionContext): void {
|
|
224
|
+
try {
|
|
225
|
+
if (!ctx.sessionManager) return;
|
|
226
|
+
const sessionManager = ctx.sessionManager as typeof ctx.sessionManager & {
|
|
227
|
+
_rewriteFile?: () => void;
|
|
228
|
+
flushed?: boolean;
|
|
229
|
+
};
|
|
230
|
+
const sessionFile = sessionManager.getSessionFile();
|
|
231
|
+
if (!sessionFile || typeof sessionManager._rewriteFile !== "function") return;
|
|
232
|
+
fs.mkdirSync(path.dirname(sessionFile), { recursive: true });
|
|
233
|
+
sessionManager._rewriteFile();
|
|
234
|
+
sessionManager.flushed = true;
|
|
235
|
+
} catch (error) {
|
|
236
|
+
console.error("Failed to persist slash session snapshot for export:", error);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
197
240
|
async function runSlashSubagent(
|
|
198
241
|
pi: ExtensionAPI,
|
|
199
242
|
ctx: ExtensionContext,
|
|
@@ -208,17 +251,18 @@ async function runSlashSubagent(
|
|
|
208
251
|
display: true,
|
|
209
252
|
details: initialDetails,
|
|
210
253
|
});
|
|
254
|
+
persistSlashSessionSnapshot(ctx);
|
|
211
255
|
|
|
212
256
|
try {
|
|
213
257
|
const response = await requestSlashRun(pi, ctx, requestId, params);
|
|
214
258
|
const finalDetails = finalizeSlashResult(response);
|
|
215
|
-
const text = extractSlashMessageText(response.result.content) || response.errorText || "(no output)";
|
|
216
259
|
pi.sendMessage({
|
|
217
260
|
customType: SLASH_RESULT_TYPE,
|
|
218
|
-
content:
|
|
219
|
-
display:
|
|
261
|
+
content: buildSlashExportText(response),
|
|
262
|
+
display: true,
|
|
220
263
|
details: finalDetails,
|
|
221
264
|
});
|
|
265
|
+
persistSlashSessionSnapshot(ctx);
|
|
222
266
|
if (ctx.hasUI) {
|
|
223
267
|
ctx.ui.setStatus("subagent-slash", undefined);
|
|
224
268
|
}
|
|
@@ -227,13 +271,14 @@ async function runSlashSubagent(
|
|
|
227
271
|
}
|
|
228
272
|
} catch (error) {
|
|
229
273
|
const message = error instanceof Error ? error.message : String(error);
|
|
230
|
-
const failedDetails = failSlashResult(requestId, params, message
|
|
274
|
+
const failedDetails = failSlashResult(requestId, params, message);
|
|
231
275
|
pi.sendMessage({
|
|
232
276
|
customType: SLASH_RESULT_TYPE,
|
|
233
|
-
content: message
|
|
234
|
-
display:
|
|
277
|
+
content: `## Subagent result\n\n${message}`,
|
|
278
|
+
display: true,
|
|
235
279
|
details: failedDetails,
|
|
236
280
|
});
|
|
281
|
+
persistSlashSessionSnapshot(ctx);
|
|
237
282
|
if (ctx.hasUI) {
|
|
238
283
|
ctx.ui.setStatus("subagent-slash", undefined);
|
|
239
284
|
}
|
|
@@ -398,16 +443,15 @@ export function registerSlashCommands(
|
|
|
398
443
|
});
|
|
399
444
|
|
|
400
445
|
pi.registerCommand("run", {
|
|
401
|
-
description: "Run a subagent directly: /run agent[output=file] task [--bg] [--fork]",
|
|
446
|
+
description: "Run a subagent directly: /run agent[output=file] [task] [--bg] [--fork]",
|
|
402
447
|
getArgumentCompletions: makeAgentCompletions(state, false),
|
|
403
448
|
handler: async (args, ctx) => {
|
|
404
449
|
const { args: cleanedArgs, bg, fork } = extractExecutionFlags(args);
|
|
405
450
|
const input = cleanedArgs.trim();
|
|
406
451
|
const firstSpace = input.indexOf(" ");
|
|
407
|
-
if (
|
|
408
|
-
const { name: agentName, config: inline } = parseAgentToken(input.slice(0, firstSpace));
|
|
409
|
-
const task = input.slice(firstSpace + 1).trim();
|
|
410
|
-
if (!task) { ctx.ui.notify("Usage: /run <agent> <task> [--bg] [--fork]", "error"); return; }
|
|
452
|
+
if (!input) { ctx.ui.notify("Usage: /run <agent> [task] [--bg] [--fork]", "error"); return; }
|
|
453
|
+
const { name: agentName, config: inline } = parseAgentToken(firstSpace === -1 ? input : input.slice(0, firstSpace));
|
|
454
|
+
const task = firstSpace === -1 ? "" : input.slice(firstSpace + 1).trim();
|
|
411
455
|
|
|
412
456
|
const agents = discoverAgents(state.baseCwd, "both").agents;
|
|
413
457
|
if (!agents.find((a) => a.name === agentName)) { ctx.ui.notify(`Unknown agent: ${agentName}`, "error"); return; }
|
package/slash-live-state.ts
CHANGED
|
@@ -278,11 +278,9 @@ export function restoreSlashFinalSnapshots(entries: unknown[]): void {
|
|
|
278
278
|
liveSnapshots.clear();
|
|
279
279
|
finalSnapshots.clear();
|
|
280
280
|
for (const entry of entries) {
|
|
281
|
-
const e = entry as { type?: string;
|
|
282
|
-
if (e?.type !== "
|
|
283
|
-
const
|
|
284
|
-
if (!m || m.role !== "custom" || m.customType !== SLASH_RESULT_TYPE || m.display !== false) continue;
|
|
285
|
-
const details = resolveSlashMessageDetails(m.details);
|
|
281
|
+
const e = entry as { type?: string; customType?: string; details?: unknown };
|
|
282
|
+
if (e?.type !== "custom_message" || e.customType !== SLASH_RESULT_TYPE) continue;
|
|
283
|
+
const details = resolveSlashMessageDetails(e.details);
|
|
286
284
|
if (!details) continue;
|
|
287
285
|
finalSnapshots.set(details.requestId, { result: details.result, version: nextVersion() });
|
|
288
286
|
}
|
package/subagent-executor.ts
CHANGED
|
@@ -326,7 +326,7 @@ function validateExecutionInput(
|
|
|
326
326
|
function getRequestedModeLabel(params: SubagentParamsLike): Details["mode"] {
|
|
327
327
|
if ((params.chain?.length ?? 0) > 0) return "chain";
|
|
328
328
|
if ((params.tasks?.length ?? 0) > 0) return "parallel";
|
|
329
|
-
if (params.agent
|
|
329
|
+
if (params.agent) return "single";
|
|
330
330
|
return "single";
|
|
331
331
|
}
|
|
332
332
|
|
|
@@ -483,7 +483,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
|
|
|
483
483
|
} = data;
|
|
484
484
|
const hasChain = (params.chain?.length ?? 0) > 0;
|
|
485
485
|
const hasTasks = (params.tasks?.length ?? 0) > 0;
|
|
486
|
-
const hasSingle = Boolean(params.agent
|
|
486
|
+
const hasSingle = !hasChain && !hasTasks && Boolean(params.agent);
|
|
487
487
|
if (!effectiveAsync) return null;
|
|
488
488
|
|
|
489
489
|
if (hasChain && params.chain) {
|
|
@@ -614,7 +614,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
|
|
|
614
614
|
const modelOverride = resolveModelCandidate((params.model as string | undefined) ?? a.model, availableModels, currentProvider);
|
|
615
615
|
return executeAsyncSingle(id, {
|
|
616
616
|
agent: params.agent!,
|
|
617
|
-
task: params.context === "fork" ? wrapForkTask(params.task
|
|
617
|
+
task: params.context === "fork" ? wrapForkTask(params.task ?? "") : (params.task ?? ""),
|
|
618
618
|
agentConfig: a,
|
|
619
619
|
ctx: asyncCtx,
|
|
620
620
|
availableModels,
|
|
@@ -1211,7 +1211,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
|
|
|
1211
1211
|
id: m.id,
|
|
1212
1212
|
fullId: `${m.provider}/${m.id}`,
|
|
1213
1213
|
}));
|
|
1214
|
-
let task = params.task
|
|
1214
|
+
let task = params.task ?? "";
|
|
1215
1215
|
let modelOverride: string | undefined = resolveModelCandidate(
|
|
1216
1216
|
(params.model as string | undefined) ?? agentConfig.model,
|
|
1217
1217
|
availableModels,
|
|
@@ -1548,7 +1548,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
1548
1548
|
const shareEnabled = effectiveParams.share === true;
|
|
1549
1549
|
const hasChain = (effectiveParams.chain?.length ?? 0) > 0;
|
|
1550
1550
|
const hasTasks = (effectiveParams.tasks?.length ?? 0) > 0;
|
|
1551
|
-
const hasSingle = Boolean(effectiveParams.agent
|
|
1551
|
+
const hasSingle = !hasChain && !hasTasks && Boolean(effectiveParams.agent);
|
|
1552
1552
|
const allowClarifyTaskPrompt = hasChain
|
|
1553
1553
|
&& effectiveParams.clarify === true
|
|
1554
1554
|
&& ctx.hasUI
|
|
@@ -1602,6 +1602,8 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
1602
1602
|
}
|
|
1603
1603
|
const sessionDirForIndex = (idx?: number) =>
|
|
1604
1604
|
path.join(sessionRoot, `run-${idx ?? 0}`);
|
|
1605
|
+
const childSessionFileForIndex = (idx?: number) =>
|
|
1606
|
+
sessionFileForIndex(idx) ?? path.join(sessionDirForIndex(idx), "session.jsonl");
|
|
1605
1607
|
|
|
1606
1608
|
const onUpdateWithContext = onUpdate
|
|
1607
1609
|
? (r: AgentToolResult<Details>) => onUpdate(withForkContext(r, effectiveParams.context))
|
|
@@ -1618,7 +1620,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
1618
1620
|
shareEnabled,
|
|
1619
1621
|
sessionRoot,
|
|
1620
1622
|
sessionDirForIndex,
|
|
1621
|
-
sessionFileForIndex,
|
|
1623
|
+
sessionFileForIndex: childSessionFileForIndex,
|
|
1622
1624
|
artifactConfig,
|
|
1623
1625
|
artifactsDir,
|
|
1624
1626
|
backgroundRequestedWhileClarifying,
|