pi-subagents 0.11.8 → 0.11.9

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 CHANGED
@@ -2,6 +2,22 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.11.9] - 2026-03-21
6
+
7
+ ### Fixed
8
+ - `/agents` overlay launches (single, chain, parallel) and slash commands (`/run`, `/chain`, `/parallel`) now render an inline result card in chat instead of relaying through `sendUserMessage`.
9
+ - `/agents` overlay chain launches no longer bypass the executor for async fallback, fixing a path where async chain errors were silently swallowed.
10
+
11
+ ### Changed
12
+ - All slash and overlay subagent execution now routes through an event bus request/response protocol (`slash-bridge.ts`), matching the pattern used by pi-prompt-template-model. This replaces both the old `sendUserMessage` relay and the direct `executeChain` call in the overlay handler.
13
+ - Slash launches show a live inline card immediately on start that streams current tool, recent tools, and output in real time, rather than appearing only after completion.
14
+ - `/parallel` now uses the native `tasks` parameter directly instead of wrapping through `{ chain: [{ parallel: tasks }] }`.
15
+
16
+ ### Added
17
+ - `slash-bridge.ts` — event bus bridge for slash command execution. Manages AbortController lifecycle, cancel-before-start races, and progress streaming via `subagent:slash:*` events.
18
+ - `slash-live-state.ts` — request-id keyed snapshot store that drives live inline card rendering during execution and restores finalized results from session entries on reload.
19
+ - Clarified README Usage section to distinguish LLM tool parameters from user-facing slash commands.
20
+
5
21
  ## [0.11.8] - 2026-03-21
6
22
 
7
23
  ### Added
package/README.md CHANGED
@@ -97,9 +97,11 @@ Semantics:
97
97
 
98
98
  When `extensions` is present, it takes precedence over extension paths implied by `tools` entries.
99
99
 
100
- **MCP Tools**
100
+ **MCP Tools (optional)**
101
101
 
102
- Agents can use MCP server tools directly (requires the [pi-mcp-adapter](https://github.com/nicobailon/pi-mcp-adapter) extension). Add `mcp:` prefixed entries to the `tools` field:
102
+ If you have the [pi-mcp-adapter](https://github.com/nicobailon/pi-mcp-adapter) extension installed, subagents can use MCP server tools directly. Without that extension, everything below is ignored — MCP integration is entirely optional.
103
+
104
+ Add `mcp:` prefixed entries to the `tools` field in agent frontmatter:
103
105
 
104
106
  ```yaml
105
107
  # All tools from a server
@@ -118,7 +120,7 @@ The `mcp:` items are additive — they don't affect which builtins the agent get
118
120
 
119
121
  Subagents only get direct MCP tools when `mcp:` items are explicitly listed. Even if your `mcp.json` has `directTools: true` globally, a subagent without `mcp:` in its frontmatter won't get any direct tools — keeping it lean. The `mcp` proxy tool is still available for discovery if needed.
120
122
 
121
- The MCP adapter's metadata cache must be populated for direct tools to work. On the first session with a new MCP server, tools will only be available through the `mcp` proxy. Restart Pi after the first session and direct tools become available.
123
+ > **First-run caveat:** The MCP adapter caches tool metadata at startup. The first time you connect to a new MCP server, that cache is empty, so tools are only available through the generic `mcp` proxy. After that first session, restart pi and direct tools become available.
122
124
 
123
125
  **Resolution priority:** step override > agent frontmatter > disabled
124
126
 
@@ -425,7 +427,9 @@ Skills are specialized instructions loaded from SKILL.md files and injected into
425
427
 
426
428
  ## Usage
427
429
 
428
- **subagent tool:**
430
+ These are the parameters the **LLM agent** passes when it calls the `subagent` tool — not something you type directly. The agent decides to use these based on your conversation. For user-facing commands, see [Quick Commands](#quick-commands) above.
431
+
432
+ **subagent tool parameters:**
429
433
  ```typescript
430
434
  // Single agent
431
435
  { agent: "worker", task: "refactor auth" }
package/index.ts CHANGED
@@ -15,8 +15,9 @@
15
15
  import * as fs from "node:fs";
16
16
  import * as os from "node:os";
17
17
  import * as path from "node:path";
18
+ import type { AgentToolResult } from "@mariozechner/pi-agent-core";
18
19
  import { type ExtensionAPI, type ExtensionContext, type ToolDefinition } from "@mariozechner/pi-coding-agent";
19
- import { Text } from "@mariozechner/pi-tui";
20
+ import { Box, Container, Spacer, Text } from "@mariozechner/pi-tui";
20
21
  import { discoverAgents } from "./agents.js";
21
22
  import { cleanupAllArtifactDirs, cleanupOldArtifacts, getArtifactsDir } from "./artifacts.js";
22
23
  import { cleanupOldChainDirs } from "./settings.js";
@@ -28,6 +29,8 @@ import { createAsyncJobTracker } from "./async-job-tracker.js";
28
29
  import { createResultWatcher } from "./result-watcher.js";
29
30
  import { registerSlashCommands } from "./slash-commands.js";
30
31
  import { registerPromptTemplateDelegationBridge } from "./prompt-template-bridge.js";
32
+ import { registerSlashSubagentBridge } from "./slash-bridge.js";
33
+ import { clearSlashSnapshots, getSlashRenderableSnapshot, resolveSlashMessageDetails, restoreSlashFinalSnapshots, type SlashMessageDetails } from "./slash-live-state.js";
31
34
  import {
32
35
  type Details,
33
36
  type ExtensionConfig,
@@ -35,6 +38,7 @@ import {
35
38
  ASYNC_DIR,
36
39
  DEFAULT_ARTIFACT_CONFIG,
37
40
  RESULTS_DIR,
41
+ SLASH_RESULT_TYPE,
38
42
  WIDGET_KEY,
39
43
  } from "./types.js";
40
44
 
@@ -92,6 +96,48 @@ function ensureAccessibleDir(dirPath: string): void {
92
96
  }
93
97
  }
94
98
 
99
+ function isSlashResultRunning(result: { details?: Details }): boolean {
100
+ return result.details?.progress?.some((entry) => entry.status === "running")
101
+ || result.details?.results.some((entry) => entry.progress?.status === "running")
102
+ || false;
103
+ }
104
+
105
+ function isSlashResultError(result: { details?: Details }): boolean {
106
+ return result.details?.results.some((entry) => entry.exitCode !== 0 && entry.progress?.status !== "running") || false;
107
+ }
108
+
109
+ function rebuildSlashResultContainer(
110
+ container: Container,
111
+ result: AgentToolResult<Details>,
112
+ options: { expanded: boolean },
113
+ theme: ExtensionContext["ui"]["theme"],
114
+ ): void {
115
+ container.clear();
116
+ container.addChild(new Spacer(1));
117
+ const boxTheme = isSlashResultRunning(result) ? "toolPendingBg" : isSlashResultError(result) ? "toolErrorBg" : "toolSuccessBg";
118
+ const box = new Box(1, 1, (text: string) => theme.bg(boxTheme, text));
119
+ box.addChild(renderSubagentResult(result, options, theme));
120
+ container.addChild(box);
121
+ }
122
+
123
+ function createSlashResultComponent(
124
+ details: SlashMessageDetails,
125
+ options: { expanded: boolean },
126
+ theme: ExtensionContext["ui"]["theme"],
127
+ ): Container {
128
+ const container = new Container();
129
+ let lastVersion = -1;
130
+ container.render = (width: number): string[] => {
131
+ const snapshot = getSlashRenderableSnapshot(details);
132
+ if (snapshot.version !== lastVersion) {
133
+ lastVersion = snapshot.version;
134
+ rebuildSlashResultContainer(container, snapshot.result, options, theme);
135
+ }
136
+ return Container.prototype.render.call(container, width);
137
+ };
138
+ return container;
139
+ }
140
+
95
141
  export default function registerSubagentExtension(pi: ExtensionAPI): void {
96
142
  ensureAccessibleDir(RESULTS_DIR);
97
143
  ensureAccessibleDir(ASYNC_DIR);
@@ -139,6 +185,19 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
139
185
  discoverAgents,
140
186
  });
141
187
 
188
+ pi.registerMessageRenderer<SlashMessageDetails>(SLASH_RESULT_TYPE, (message, options, theme) => {
189
+ const details = resolveSlashMessageDetails(message.details);
190
+ if (!details) return undefined;
191
+ return createSlashResultComponent(details, options, theme);
192
+ });
193
+
194
+ const slashBridge = registerSlashSubagentBridge({
195
+ events: pi.events,
196
+ getContext: () => state.lastUiContext,
197
+ execute: (id, params, signal, onUpdate, ctx) =>
198
+ executor.execute(id, params, signal, onUpdate, ctx),
199
+ });
200
+
142
201
  const promptTemplateBridge = registerPromptTemplateDelegationBridge({
143
202
  events: pi.events,
144
203
  getContext: () => state.lastUiContext,
@@ -340,7 +399,7 @@ MANAGEMENT (use action field — omit agent/task/chain/tasks):
340
399
 
341
400
  pi.registerTool(tool);
342
401
  pi.registerTool(statusTool);
343
- registerSlashCommands(pi, state, getSubagentSessionRoot);
402
+ registerSlashCommands(pi, state);
344
403
 
345
404
  pi.events.on("subagent:started", handleStarted);
346
405
  pi.events.on("subagent:complete", handleComplete);
@@ -372,6 +431,7 @@ MANAGEMENT (use action field — omit agent/task/chain/tasks):
372
431
  state.lastUiContext = ctx;
373
432
  cleanupSessionArtifacts(ctx);
374
433
  resetJobs(ctx);
434
+ restoreSlashFinalSnapshots(ctx.sessionManager.getEntries());
375
435
  };
376
436
 
377
437
  pi.on("session_start", (_event, ctx) => {
@@ -392,6 +452,9 @@ MANAGEMENT (use action field — omit agent/task/chain/tasks):
392
452
  }
393
453
  state.cleanupTimers.clear();
394
454
  state.asyncJobs.clear();
455
+ clearSlashSnapshots();
456
+ slashBridge.cancelAll();
457
+ slashBridge.dispose();
395
458
  promptTemplateBridge.cancelAll();
396
459
  promptTemplateBridge.dispose();
397
460
  if (state.lastUiContext?.hasUI) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-subagents",
3
- "version": "0.11.8",
3
+ "version": "0.11.9",
4
4
  "description": "Pi extension for delegating tasks to subagents with chains, parallel execution, and TUI clarification",
5
5
  "author": "Nico Bailon",
6
6
  "license": "MIT",
package/render.ts CHANGED
@@ -220,6 +220,36 @@ export function renderSubagentResult(
220
220
  );
221
221
  c.addChild(new Spacer(1));
222
222
 
223
+ if (isRunning && r.progress) {
224
+ if (r.progress.currentTool) {
225
+ const maxToolArgsLen = Math.max(50, w - 20);
226
+ const toolArgsPreview = r.progress.currentToolArgs
227
+ ? (r.progress.currentToolArgs.length > maxToolArgsLen
228
+ ? `${r.progress.currentToolArgs.slice(0, maxToolArgsLen)}...`
229
+ : r.progress.currentToolArgs)
230
+ : "";
231
+ const toolLine = toolArgsPreview
232
+ ? `${r.progress.currentTool}: ${toolArgsPreview}`
233
+ : r.progress.currentTool;
234
+ c.addChild(new Text(truncLine(theme.fg("warning", `> ${toolLine}`), w), 0, 0));
235
+ }
236
+ if (r.progress.recentTools?.length) {
237
+ for (const t of r.progress.recentTools.slice(-3)) {
238
+ const maxArgsLen = Math.max(40, w - 24);
239
+ const argsPreview = t.args.length > maxArgsLen
240
+ ? `${t.args.slice(0, maxArgsLen)}...`
241
+ : t.args;
242
+ c.addChild(new Text(truncLine(theme.fg("dim", `${t.tool}: ${argsPreview}`), w), 0, 0));
243
+ }
244
+ }
245
+ for (const line of (r.progress.recentOutput ?? []).slice(-5)) {
246
+ c.addChild(new Text(truncLine(theme.fg("dim", ` ${line}`), w), 0, 0));
247
+ }
248
+ if (r.progress.currentTool || r.progress.recentTools?.length || r.progress.recentOutput?.length) {
249
+ c.addChild(new Spacer(1));
250
+ }
251
+ }
252
+
223
253
  const items = getDisplayItems(r.messages);
224
254
  for (const item of items) {
225
255
  if (item.type === "tool")
@@ -0,0 +1,174 @@
1
+ import type { AgentToolResult } from "@mariozechner/pi-agent-core";
2
+ import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
3
+ import type { SubagentParamsLike } from "./subagent-executor.js";
4
+ import {
5
+ SLASH_SUBAGENT_CANCEL_EVENT,
6
+ SLASH_SUBAGENT_REQUEST_EVENT,
7
+ SLASH_SUBAGENT_RESPONSE_EVENT,
8
+ SLASH_SUBAGENT_STARTED_EVENT,
9
+ SLASH_SUBAGENT_UPDATE_EVENT,
10
+ type Details,
11
+ } from "./types.js";
12
+
13
+ export interface SlashSubagentRequest {
14
+ requestId: string;
15
+ params: SubagentParamsLike;
16
+ }
17
+
18
+ export interface SlashSubagentResponse {
19
+ requestId: string;
20
+ result: AgentToolResult<Details>;
21
+ isError: boolean;
22
+ errorText?: string;
23
+ }
24
+
25
+ export interface SlashSubagentUpdate {
26
+ requestId: string;
27
+ progress?: Details["progress"];
28
+ currentTool?: string;
29
+ toolCount?: number;
30
+ }
31
+
32
+ interface EventBus {
33
+ on(event: string, handler: (data: unknown) => void): (() => void) | void;
34
+ emit(event: string, data: unknown): void;
35
+ }
36
+
37
+ interface SlashBridgeOptions {
38
+ events: EventBus;
39
+ getContext: () => ExtensionContext | null;
40
+ execute: (
41
+ id: string,
42
+ params: SubagentParamsLike,
43
+ signal: AbortSignal,
44
+ onUpdate: ((r: AgentToolResult<Details>) => void) | undefined,
45
+ ctx: ExtensionContext,
46
+ ) => Promise<AgentToolResult<Details>>;
47
+ }
48
+
49
+ export function registerSlashSubagentBridge(options: SlashBridgeOptions): {
50
+ cancelAll: () => void;
51
+ dispose: () => void;
52
+ } {
53
+ const controllers = new Map<string, AbortController>();
54
+ const pendingCancels = new Set<string>();
55
+ const subscriptions: Array<() => void> = [];
56
+
57
+ const subscribe = (event: string, handler: (data: unknown) => void): void => {
58
+ const unsubscribe = options.events.on(event, handler);
59
+ if (typeof unsubscribe === "function") subscriptions.push(unsubscribe);
60
+ };
61
+
62
+ subscribe(SLASH_SUBAGENT_CANCEL_EVENT, (data) => {
63
+ if (!data || typeof data !== "object") return;
64
+ const requestId = (data as { requestId?: unknown }).requestId;
65
+ if (typeof requestId !== "string") return;
66
+ const controller = controllers.get(requestId);
67
+ if (controller) {
68
+ controller.abort();
69
+ return;
70
+ }
71
+ pendingCancels.add(requestId);
72
+ });
73
+
74
+ subscribe(SLASH_SUBAGENT_REQUEST_EVENT, async (data) => {
75
+ if (!data || typeof data !== "object") return;
76
+ const request = data as Partial<SlashSubagentRequest>;
77
+ if (typeof request.requestId !== "string" || !request.params) return;
78
+ const { requestId, params } = request as SlashSubagentRequest;
79
+
80
+ const ctx = options.getContext();
81
+ if (!ctx) {
82
+ const response: SlashSubagentResponse = {
83
+ requestId,
84
+ result: {
85
+ content: [{ type: "text", text: "No active extension context for slash subagent execution." }],
86
+ details: { mode: "single" as const, results: [] },
87
+ },
88
+ isError: true,
89
+ errorText: "No active extension context.",
90
+ };
91
+ options.events.emit(SLASH_SUBAGENT_RESPONSE_EVENT, response);
92
+ return;
93
+ }
94
+
95
+ const controller = new AbortController();
96
+ controllers.set(requestId, controller);
97
+
98
+ if (pendingCancels.delete(requestId)) {
99
+ controller.abort();
100
+ const response: SlashSubagentResponse = {
101
+ requestId,
102
+ result: {
103
+ content: [{ type: "text", text: "Cancelled." }],
104
+ details: { mode: "single" as const, results: [] },
105
+ },
106
+ isError: true,
107
+ errorText: "Cancelled before start.",
108
+ };
109
+ options.events.emit(SLASH_SUBAGENT_RESPONSE_EVENT, response);
110
+ controllers.delete(requestId);
111
+ return;
112
+ }
113
+
114
+ options.events.emit(SLASH_SUBAGENT_STARTED_EVENT, { requestId });
115
+
116
+ try {
117
+ const result = await options.execute(
118
+ requestId,
119
+ params,
120
+ controller.signal,
121
+ (update) => {
122
+ const progress = update.details?.progress;
123
+ const first = progress?.[0];
124
+ const payload: SlashSubagentUpdate = {
125
+ requestId,
126
+ progress,
127
+ currentTool: first?.currentTool,
128
+ toolCount: first?.toolCount,
129
+ };
130
+ options.events.emit(SLASH_SUBAGENT_UPDATE_EVENT, payload);
131
+ },
132
+ ctx,
133
+ );
134
+
135
+ const response: SlashSubagentResponse = {
136
+ requestId,
137
+ result,
138
+ isError: (result as { isError?: boolean }).isError === true,
139
+ errorText: (result as { isError?: boolean }).isError
140
+ ? result.content.find((c) => c.type === "text")?.text
141
+ : undefined,
142
+ };
143
+ options.events.emit(SLASH_SUBAGENT_RESPONSE_EVENT, response);
144
+ } catch (error) {
145
+ const response: SlashSubagentResponse = {
146
+ requestId,
147
+ result: {
148
+ content: [{ type: "text", text: error instanceof Error ? error.message : String(error) }],
149
+ details: { mode: "single" as const, results: [] },
150
+ },
151
+ isError: true,
152
+ errorText: error instanceof Error ? error.message : String(error),
153
+ };
154
+ options.events.emit(SLASH_SUBAGENT_RESPONSE_EVENT, response);
155
+ } finally {
156
+ controllers.delete(requestId);
157
+ }
158
+ });
159
+
160
+ return {
161
+ cancelAll: () => {
162
+ for (const controller of controllers.values()) {
163
+ controller.abort();
164
+ }
165
+ controllers.clear();
166
+ pendingCancels.clear();
167
+ },
168
+ dispose: () => {
169
+ for (const unsubscribe of subscriptions) unsubscribe();
170
+ subscriptions.length = 0;
171
+ pendingCancels.clear();
172
+ },
173
+ };
174
+ }
package/slash-commands.ts CHANGED
@@ -1,15 +1,27 @@
1
1
  import { randomUUID } from "node:crypto";
2
- import * as fs from "node:fs";
3
- import * as path from "node:path";
4
2
  import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
3
+ import { Key, matchesKey } from "@mariozechner/pi-tui";
5
4
  import { discoverAgents, discoverAgentsAll } from "./agents.js";
6
- import { executeAsyncChain, isAsyncAvailable } from "./async-execution.js";
7
- import { executeChain } from "./chain-execution.js";
8
5
  import { AgentManagerComponent, type ManagerResult } from "./agent-manager.js";
9
6
  import { discoverAvailableSkills } from "./skills.js";
10
- import { getArtifactsDir } from "./artifacts.js";
11
- import { DEFAULT_ARTIFACT_CONFIG, MAX_PARALLEL, type SubagentState, type ArtifactConfig } from "./types.js";
12
- import type { SequentialStep } from "./settings.js";
7
+ import type { SubagentParamsLike } from "./subagent-executor.js";
8
+ import type { SlashSubagentResponse, SlashSubagentUpdate } from "./slash-bridge.js";
9
+ import {
10
+ applySlashUpdate,
11
+ buildSlashInitialResult,
12
+ failSlashResult,
13
+ finalizeSlashResult,
14
+ } from "./slash-live-state.js";
15
+ import {
16
+ MAX_PARALLEL,
17
+ SLASH_RESULT_TYPE,
18
+ SLASH_SUBAGENT_CANCEL_EVENT,
19
+ SLASH_SUBAGENT_REQUEST_EVENT,
20
+ SLASH_SUBAGENT_RESPONSE_EVENT,
21
+ SLASH_SUBAGENT_STARTED_EVENT,
22
+ SLASH_SUBAGENT_UPDATE_EVENT,
23
+ type SubagentState,
24
+ } from "./types.js";
13
25
 
14
26
  interface InlineConfig {
15
27
  output?: string | false;
@@ -71,25 +83,6 @@ const extractExecutionFlags = (rawArgs: string): { args: string; bg: boolean; fo
71
83
  return { args, bg, fork };
72
84
  };
73
85
 
74
- function setupDirectRun(ctx: ExtensionContext, getSubagentSessionRoot: (parentSessionFile: string | null) => string) {
75
- const runId = randomUUID().slice(0, 8);
76
- const parentSessionFile = ctx.sessionManager.getSessionFile() ?? null;
77
- const sessionRoot = path.join(getSubagentSessionRoot(parentSessionFile), runId);
78
- try {
79
- fs.mkdirSync(sessionRoot, { recursive: true });
80
- } catch (error) {
81
- const message = error instanceof Error ? error.message : String(error);
82
- throw new Error(`Failed to create session directory '${sessionRoot}': ${message}`);
83
- }
84
- return {
85
- runId,
86
- shareEnabled: false,
87
- sessionDirForIndex: (idx?: number) => path.join(sessionRoot, `run-${idx ?? 0}`),
88
- artifactsDir: getArtifactsDir(parentSessionFile),
89
- artifactConfig: { ...DEFAULT_ARTIFACT_CONFIG } as ArtifactConfig,
90
- };
91
- }
92
-
93
86
  const makeAgentCompletions = (state: SubagentState, multiAgent: boolean) => (prefix: string) => {
94
87
  const agents = discoverAgents(state.baseCwd, "both").agents;
95
88
  if (!multiAgent) {
@@ -111,11 +104,145 @@ const makeAgentCompletions = (state: SubagentState, multiAgent: boolean) => (pre
111
104
  return agents.filter((a) => a.name.startsWith(lastWord)).map((a) => ({ value: `${beforeLastWord}${a.name}`, label: a.name }));
112
105
  };
113
106
 
107
+ async function requestSlashRun(
108
+ pi: ExtensionAPI,
109
+ ctx: ExtensionContext,
110
+ requestId: string,
111
+ params: SubagentParamsLike,
112
+ ): Promise<SlashSubagentResponse> {
113
+ return new Promise((resolve, reject) => {
114
+ let done = false;
115
+ let started = false;
116
+
117
+ const startTimeoutMs = 15_000;
118
+ const startTimeout = setTimeout(() => {
119
+ finish(() => reject(new Error(
120
+ "Slash subagent bridge did not start within 15s. Ensure the extension is loaded correctly.",
121
+ )));
122
+ }, startTimeoutMs);
123
+
124
+ const onStarted = (data: unknown) => {
125
+ if (done || !data || typeof data !== "object") return;
126
+ if ((data as { requestId?: unknown }).requestId !== requestId) return;
127
+ started = true;
128
+ clearTimeout(startTimeout);
129
+ if (ctx.hasUI) ctx.ui.setStatus("subagent-slash", "running...");
130
+ };
131
+
132
+ const onResponse = (data: unknown) => {
133
+ if (done || !data || typeof data !== "object") return;
134
+ const response = data as Partial<SlashSubagentResponse>;
135
+ if (response.requestId !== requestId) return;
136
+ clearTimeout(startTimeout);
137
+ finish(() => resolve(response as SlashSubagentResponse));
138
+ };
139
+
140
+ const onUpdate = (data: unknown) => {
141
+ if (done || !data || typeof data !== "object") return;
142
+ const update = data as SlashSubagentUpdate;
143
+ if (update.requestId !== requestId) return;
144
+ applySlashUpdate(requestId, update);
145
+ if (!ctx.hasUI) return;
146
+ const tool = update.currentTool ? ` ${update.currentTool}` : "";
147
+ const count = update.toolCount ?? 0;
148
+ ctx.ui.setStatus("subagent-slash", `${count} tools${tool}`);
149
+ };
150
+
151
+ const onTerminalInput = ctx.hasUI
152
+ ? ctx.ui.onTerminalInput((input) => {
153
+ if (!matchesKey(input, Key.escape)) return undefined;
154
+ pi.events.emit(SLASH_SUBAGENT_CANCEL_EVENT, { requestId });
155
+ finish(() => reject(new Error("Cancelled")));
156
+ return { consume: true };
157
+ })
158
+ : undefined;
159
+
160
+ const unsubStarted = pi.events.on(SLASH_SUBAGENT_STARTED_EVENT, onStarted);
161
+ const unsubResponse = pi.events.on(SLASH_SUBAGENT_RESPONSE_EVENT, onResponse);
162
+ const unsubUpdate = pi.events.on(SLASH_SUBAGENT_UPDATE_EVENT, onUpdate);
163
+
164
+ const finish = (next: () => void) => {
165
+ if (done) return;
166
+ done = true;
167
+ clearTimeout(startTimeout);
168
+ unsubStarted();
169
+ unsubResponse();
170
+ unsubUpdate();
171
+ onTerminalInput?.();
172
+ if (ctx.hasUI) ctx.ui.setStatus("subagent-slash", undefined);
173
+ next();
174
+ };
175
+
176
+ pi.events.emit(SLASH_SUBAGENT_REQUEST_EVENT, { requestId, params });
177
+
178
+ // Bridge emits STARTED synchronously during REQUEST emit.
179
+ // If not started, no bridge received the request.
180
+ if (!started && done) return;
181
+ if (!started) {
182
+ finish(() => reject(new Error(
183
+ "No slash subagent bridge responded. Ensure the subagent extension is loaded correctly.",
184
+ )));
185
+ }
186
+ });
187
+ }
188
+
189
+ function extractSlashMessageText(content: string | Array<{ type?: string; text?: string }>): string {
190
+ if (typeof content === "string") return content;
191
+ if (!Array.isArray(content)) return "";
192
+ return content
193
+ .filter((part): part is { type: "text"; text: string } => part?.type === "text" && typeof part.text === "string")
194
+ .map((part) => part.text)
195
+ .join("\n");
196
+ }
197
+
198
+ async function runSlashSubagent(
199
+ pi: ExtensionAPI,
200
+ ctx: ExtensionContext,
201
+ params: SubagentParamsLike,
202
+ ): Promise<void> {
203
+ const requestId = randomUUID();
204
+ const initialDetails = buildSlashInitialResult(requestId, params);
205
+ const initialText = extractSlashMessageText(initialDetails.result.content) || "Running subagent...";
206
+ pi.sendMessage({
207
+ customType: SLASH_RESULT_TYPE,
208
+ content: initialText,
209
+ display: true,
210
+ details: initialDetails,
211
+ });
212
+
213
+ try {
214
+ const response = await requestSlashRun(pi, ctx, requestId, params);
215
+ const finalDetails = finalizeSlashResult(response);
216
+ const text = extractSlashMessageText(response.result.content) || response.errorText || "(no output)";
217
+ pi.sendMessage({
218
+ customType: SLASH_RESULT_TYPE,
219
+ content: text,
220
+ display: false,
221
+ details: finalDetails,
222
+ });
223
+ if (response.isError && ctx.hasUI) {
224
+ ctx.ui.notify(response.errorText || "Subagent failed", "error");
225
+ }
226
+ } catch (error) {
227
+ const message = error instanceof Error ? error.message : String(error);
228
+ const failedDetails = failSlashResult(requestId, params, message === "Cancelled" ? "Cancelled" : message);
229
+ pi.sendMessage({
230
+ customType: SLASH_RESULT_TYPE,
231
+ content: message,
232
+ display: false,
233
+ details: failedDetails,
234
+ });
235
+ if (message === "Cancelled") {
236
+ if (ctx.hasUI) ctx.ui.notify("Cancelled", "warning");
237
+ return;
238
+ }
239
+ if (ctx.hasUI) ctx.ui.notify(message, "error");
240
+ }
241
+ }
242
+
114
243
  async function openAgentManager(
115
244
  pi: ExtensionAPI,
116
- state: SubagentState,
117
245
  ctx: ExtensionContext,
118
- getSubagentSessionRoot: (parentSessionFile: string | null) => string,
119
246
  ): Promise<void> {
120
247
  const agentData = { ...discoverAgentsAll(ctx.cwd), cwd: ctx.cwd };
121
248
  const models = ctx.modelRegistry.getAvailable().map((m) => ({
@@ -132,54 +259,26 @@ async function openAgentManager(
132
259
  if (!result) return;
133
260
 
134
261
  if (result.action === "chain") {
135
- const agents = discoverAgents(state.baseCwd, "both").agents;
136
- const exec = setupDirectRun(ctx, getSubagentSessionRoot);
137
- const chain: SequentialStep[] = result.agents.map((name, i) => ({
262
+ const chain = result.agents.map((name, i) => ({
138
263
  agent: name,
139
264
  ...(i === 0 ? { task: result.task } : {}),
140
265
  }));
141
- executeChain({ chain, task: result.task, agents, ctx, ...exec, clarify: true })
142
- .then((r) => {
143
- if (r.requestedAsync) {
144
- if (!isAsyncAvailable()) {
145
- pi.sendUserMessage("Background mode requires jiti for TypeScript execution but it could not be found.");
146
- return;
147
- }
148
- const id = randomUUID();
149
- const asyncCtx = { pi, cwd: ctx.cwd, currentSessionId: ctx.sessionManager.getSessionId() ?? id };
150
- const asyncSessionRoot = getSubagentSessionRoot(ctx.sessionManager.getSessionFile() ?? null);
151
- fs.mkdirSync(asyncSessionRoot, { recursive: true });
152
- executeAsyncChain(id, {
153
- chain: r.requestedAsync.chain,
154
- agents,
155
- ctx: asyncCtx,
156
- maxOutput: undefined,
157
- artifactsDir: exec.artifactsDir,
158
- artifactConfig: exec.artifactConfig,
159
- shareEnabled: false,
160
- sessionRoot: asyncSessionRoot,
161
- chainSkills: r.requestedAsync.chainSkills,
162
- }).then((asyncResult) => {
163
- pi.sendUserMessage(asyncResult.content[0]?.text || "(launched in background)");
164
- }).catch((err) => {
165
- pi.sendUserMessage(`Async launch failed: ${err instanceof Error ? err.message : String(err)}`);
166
- });
167
- return;
168
- }
169
- pi.sendUserMessage(r.content[0]?.text || "(no output)");
170
- })
171
- .catch((err) => pi.sendUserMessage(`Chain failed: ${err instanceof Error ? err.message : String(err)}`));
266
+ await runSlashSubagent(pi, ctx, {
267
+ chain,
268
+ task: result.task,
269
+ clarify: true,
270
+ agentScope: "both",
271
+ });
172
272
  return;
173
273
  }
174
274
 
175
- const sendToolCall = (params: Record<string, unknown>) => {
176
- pi.sendUserMessage(
177
- `Call the subagent tool with these exact parameters: ${JSON.stringify({ ...params, agentScope: "both" })}`,
178
- );
179
- };
180
-
181
275
  if (result.action === "launch") {
182
- sendToolCall({ agent: result.agent, task: result.task, clarify: !result.skipClarify });
276
+ await runSlashSubagent(pi, ctx, {
277
+ agent: result.agent,
278
+ task: result.task,
279
+ clarify: !result.skipClarify,
280
+ agentScope: "both",
281
+ });
183
282
  } else if (result.action === "launch-chain") {
184
283
  const chainParam = result.chain.steps.map((step) => ({
185
284
  agent: step.agent,
@@ -190,9 +289,18 @@ async function openAgentManager(
190
289
  skill: step.skills,
191
290
  model: step.model,
192
291
  }));
193
- sendToolCall({ chain: chainParam, task: result.task, clarify: !result.skipClarify });
292
+ await runSlashSubagent(pi, ctx, {
293
+ chain: chainParam,
294
+ task: result.task,
295
+ clarify: !result.skipClarify,
296
+ agentScope: "both",
297
+ });
194
298
  } else if (result.action === "parallel") {
195
- sendToolCall({ tasks: result.tasks, clarify: !result.skipClarify });
299
+ await runSlashSubagent(pi, ctx, {
300
+ tasks: result.tasks,
301
+ clarify: !result.skipClarify,
302
+ agentScope: "both",
303
+ });
196
304
  }
197
305
  }
198
306
 
@@ -276,12 +384,11 @@ const parseAgentArgs = (
276
384
  export function registerSlashCommands(
277
385
  pi: ExtensionAPI,
278
386
  state: SubagentState,
279
- getSubagentSessionRoot: (parentSessionFile: string | null) => string,
280
387
  ): void {
281
388
  pi.registerCommand("agents", {
282
389
  description: "Open the Agents Manager",
283
390
  handler: async (_args, ctx) => {
284
- await openAgentManager(pi, state, ctx, getSubagentSessionRoot);
391
+ await openAgentManager(pi, ctx);
285
392
  },
286
393
  });
287
394
 
@@ -304,13 +411,13 @@ export function registerSlashCommands(
304
411
  if (inline.reads && Array.isArray(inline.reads) && inline.reads.length > 0) {
305
412
  finalTask = `[Read from: ${inline.reads.join(", ")}]\n\n${finalTask}`;
306
413
  }
307
- const params: Record<string, unknown> = { agent: agentName, task: finalTask, clarify: false };
414
+ const params: SubagentParamsLike = { agent: agentName, task: finalTask, clarify: false, agentScope: "both" };
308
415
  if (inline.output !== undefined) params.output = inline.output;
309
416
  if (inline.skill !== undefined) params.skill = inline.skill;
310
417
  if (inline.model) params.model = inline.model;
311
418
  if (bg) params.async = true;
312
419
  if (fork) params.context = "fork";
313
- pi.sendUserMessage(`Call the subagent tool with these exact parameters: ${JSON.stringify({ ...params, agentScope: "both" })}`);
420
+ await runSlashSubagent(pi, ctx, params);
314
421
  },
315
422
  });
316
423
 
@@ -330,10 +437,10 @@ export function registerSlashCommands(
330
437
  ...(config.skill !== undefined ? { skill: config.skill } : {}),
331
438
  ...(config.progress !== undefined ? { progress: config.progress } : {}),
332
439
  }));
333
- const params: Record<string, unknown> = { chain, task: parsed.task, clarify: false, agentScope: "both" };
440
+ const params: SubagentParamsLike = { chain, task: parsed.task, clarify: false, agentScope: "both" };
334
441
  if (bg) params.async = true;
335
442
  if (fork) params.context = "fork";
336
- pi.sendUserMessage(`Call the subagent tool with these exact parameters: ${JSON.stringify(params)}`);
443
+ await runSlashSubagent(pi, ctx, params);
337
444
  },
338
445
  });
339
446
 
@@ -354,16 +461,16 @@ export function registerSlashCommands(
354
461
  ...(config.skill !== undefined ? { skill: config.skill } : {}),
355
462
  ...(config.progress !== undefined ? { progress: config.progress } : {}),
356
463
  }));
357
- const params: Record<string, unknown> = { chain: [{ parallel: tasks }], task: parsed.task, clarify: false, agentScope: "both" };
464
+ const params: SubagentParamsLike = { tasks, clarify: false, agentScope: "both" };
358
465
  if (bg) params.async = true;
359
466
  if (fork) params.context = "fork";
360
- pi.sendUserMessage(`Call the subagent tool with these exact parameters: ${JSON.stringify(params)}`);
467
+ await runSlashSubagent(pi, ctx, params);
361
468
  },
362
469
  });
363
470
 
364
471
  pi.registerShortcut("ctrl+shift+a", {
365
472
  handler: async (ctx) => {
366
- await openAgentManager(pi, state, ctx, getSubagentSessionRoot);
473
+ await openAgentManager(pi, ctx);
367
474
  },
368
475
  });
369
476
  }
@@ -0,0 +1,294 @@
1
+ import type { AgentToolResult } from "@mariozechner/pi-agent-core";
2
+ import type { Message } from "@mariozechner/pi-ai";
3
+ import type { SubagentParamsLike } from "./subagent-executor.js";
4
+ import type { SlashSubagentResponse, SlashSubagentUpdate } from "./slash-bridge.js";
5
+ import { type Details, type SingleResult, type Usage, SLASH_RESULT_TYPE } from "./types.js";
6
+
7
+ export interface SlashMessageDetails {
8
+ requestId: string;
9
+ result: AgentToolResult<Details>;
10
+ }
11
+
12
+ interface SlashSnapshot {
13
+ result: AgentToolResult<Details>;
14
+ version: number;
15
+ }
16
+
17
+ interface SequentialChainStepLike {
18
+ agent: string;
19
+ task?: string;
20
+ }
21
+
22
+ interface ParallelChainStepLike {
23
+ parallel: Array<{ agent: string; task?: string }>;
24
+ }
25
+
26
+ type ChainStepLike = SequentialChainStepLike | ParallelChainStepLike;
27
+
28
+ const liveSnapshots = new Map<string, SlashSnapshot>();
29
+ const finalSnapshots = new Map<string, SlashSnapshot>();
30
+ let versionCounter = 1;
31
+
32
+ const EMPTY_MESSAGES: Message[] = [];
33
+ const EMPTY_USAGE: Usage = {
34
+ input: 0,
35
+ output: 0,
36
+ cacheRead: 0,
37
+ cacheWrite: 0,
38
+ cost: 0,
39
+ turns: 0,
40
+ };
41
+
42
+ function nextVersion(): number {
43
+ return versionCounter++;
44
+ }
45
+
46
+ function cloneUsage(): Usage {
47
+ return { ...EMPTY_USAGE };
48
+ }
49
+
50
+ function createPlaceholderResult(
51
+ agent: string,
52
+ task: string,
53
+ status: "pending" | "running",
54
+ index?: number,
55
+ ): SingleResult {
56
+ return {
57
+ agent,
58
+ task,
59
+ exitCode: 0,
60
+ messages: EMPTY_MESSAGES,
61
+ usage: cloneUsage(),
62
+ progress: {
63
+ ...(index !== undefined ? { index } : {}),
64
+ agent,
65
+ status,
66
+ task,
67
+ recentTools: [],
68
+ recentOutput: [],
69
+ toolCount: 0,
70
+ tokens: 0,
71
+ durationMs: 0,
72
+ },
73
+ };
74
+ }
75
+
76
+ function buildParallelInitialResult(params: SubagentParamsLike): AgentToolResult<Details> {
77
+ const tasks = params.tasks ?? [];
78
+ return {
79
+ content: [{ type: "text", text: tasks.map((task) => `${task.agent}: ${task.task}`).join("\n\n") }],
80
+ details: {
81
+ mode: "parallel",
82
+ ...(params.context ? { context: params.context } : {}),
83
+ results: tasks.map((task, index) => createPlaceholderResult(task.agent, task.task, "running", index)),
84
+ progress: tasks.map((task, index) => ({
85
+ index,
86
+ agent: task.agent,
87
+ status: "running" as const,
88
+ task: task.task,
89
+ recentTools: [],
90
+ recentOutput: [],
91
+ toolCount: 0,
92
+ tokens: 0,
93
+ durationMs: 0,
94
+ })),
95
+ },
96
+ };
97
+ }
98
+
99
+ function isParallelChainStep(step: ChainStepLike): step is ParallelChainStepLike {
100
+ return "parallel" in step && Array.isArray(step.parallel);
101
+ }
102
+
103
+ function chainStepLabel(step: ChainStepLike): string {
104
+ if (isParallelChainStep(step)) {
105
+ return `[${step.parallel.map((entry) => entry.agent).join("+")}]`;
106
+ }
107
+ return step.agent;
108
+ }
109
+
110
+ function flattenChainResults(chain: ChainStepLike[], fallbackTask: string | undefined): SingleResult[] {
111
+ const results: SingleResult[] = [];
112
+ let flatIndex = 0;
113
+ for (const step of chain) {
114
+ if (isParallelChainStep(step)) {
115
+ for (const task of step.parallel) {
116
+ results.push(createPlaceholderResult(task.agent, task.task ?? fallbackTask ?? "", results.length === 0 ? "running" : "pending", flatIndex));
117
+ flatIndex++;
118
+ }
119
+ continue;
120
+ }
121
+ results.push(createPlaceholderResult(step.agent, step.task ?? fallbackTask ?? "", results.length === 0 ? "running" : "pending", flatIndex));
122
+ flatIndex++;
123
+ }
124
+ return results;
125
+ }
126
+
127
+ function buildChainInitialResult(params: SubagentParamsLike): AgentToolResult<Details> {
128
+ const chain = (params.chain ?? []) as ChainStepLike[];
129
+ const results = flattenChainResults(chain, params.task);
130
+ return {
131
+ content: [{
132
+ type: "text",
133
+ text: results.map((result, index) => `Step ${index + 1}: ${result.agent}\n${result.task}`).join("\n\n"),
134
+ }],
135
+ details: {
136
+ mode: "chain",
137
+ ...(params.context ? { context: params.context } : {}),
138
+ results,
139
+ progress: results.map((result, index) => ({
140
+ index,
141
+ agent: result.agent,
142
+ status: index === 0 ? "running" as const : "pending" as const,
143
+ task: result.task,
144
+ recentTools: [],
145
+ recentOutput: [],
146
+ toolCount: 0,
147
+ tokens: 0,
148
+ durationMs: 0,
149
+ })),
150
+ chainAgents: chain.map((step) => chainStepLabel(step)),
151
+ totalSteps: chain.length,
152
+ currentStepIndex: 0,
153
+ },
154
+ };
155
+ }
156
+
157
+ function buildSingleInitialResult(params: SubagentParamsLike): AgentToolResult<Details> {
158
+ const agent = params.agent ?? "subagent";
159
+ const task = params.task ?? "";
160
+ return {
161
+ content: [{ type: "text", text: task }],
162
+ details: {
163
+ mode: "single",
164
+ ...(params.context ? { context: params.context } : {}),
165
+ results: [createPlaceholderResult(agent, task, "running")],
166
+ progress: [{
167
+ agent,
168
+ status: "running",
169
+ task,
170
+ recentTools: [],
171
+ recentOutput: [],
172
+ toolCount: 0,
173
+ tokens: 0,
174
+ durationMs: 0,
175
+ }],
176
+ },
177
+ };
178
+ }
179
+
180
+ export function buildSlashInitialResult(requestId: string, params: SubagentParamsLike): SlashMessageDetails {
181
+ const result = (params.tasks?.length ?? 0) > 0
182
+ ? buildParallelInitialResult(params)
183
+ : (params.chain?.length ?? 0) > 0
184
+ ? buildChainInitialResult(params)
185
+ : buildSingleInitialResult(params);
186
+ liveSnapshots.set(requestId, { result, version: nextVersion() });
187
+ finalSnapshots.delete(requestId);
188
+ return { requestId, result };
189
+ }
190
+
191
+ function cloneResultsWithProgress(
192
+ results: SingleResult[],
193
+ progress: NonNullable<Details["progress"]> | undefined,
194
+ ): SingleResult[] {
195
+ return results.map((result, index) => {
196
+ const nextProgress = progress?.find((entry) => entry.index === index)
197
+ ?? progress?.[index]
198
+ ?? result.progress;
199
+ return nextProgress ? { ...result, progress: nextProgress } : result;
200
+ });
201
+ }
202
+
203
+ export function applySlashUpdate(requestId: string, update: SlashSubagentUpdate): void {
204
+ const snapshot = liveSnapshots.get(requestId);
205
+ if (!snapshot) return;
206
+ const progress = update.progress;
207
+ if (!progress || !snapshot.result.details) return;
208
+ const currentStepIndex = progress.findIndex((entry) => entry.status === "running");
209
+ const nextDetails: Details = {
210
+ ...snapshot.result.details,
211
+ progress,
212
+ results: cloneResultsWithProgress(snapshot.result.details.results, progress),
213
+ ...(snapshot.result.details.mode === "chain" && currentStepIndex >= 0 ? { currentStepIndex } : {}),
214
+ };
215
+ liveSnapshots.set(requestId, {
216
+ result: {
217
+ ...snapshot.result,
218
+ details: nextDetails,
219
+ },
220
+ version: nextVersion(),
221
+ });
222
+ }
223
+
224
+ export function finalizeSlashResult(response: SlashSubagentResponse): SlashMessageDetails {
225
+ const snapshot = {
226
+ result: response.result,
227
+ version: nextVersion(),
228
+ };
229
+ finalSnapshots.set(response.requestId, snapshot);
230
+ liveSnapshots.delete(response.requestId);
231
+ return {
232
+ requestId: response.requestId,
233
+ result: response.result,
234
+ };
235
+ }
236
+
237
+ export function failSlashResult(requestId: string, params: SubagentParamsLike, message: string): SlashMessageDetails {
238
+ const initial = buildSlashInitialResult(requestId, params).result;
239
+ const failedResults = initial.details.results.map((result) => ({
240
+ ...result,
241
+ exitCode: 1,
242
+ error: message,
243
+ progress: result.progress ? { ...result.progress, status: "failed" as const } : result.progress,
244
+ }));
245
+ const result: AgentToolResult<Details> = {
246
+ content: [{ type: "text", text: message }],
247
+ details: {
248
+ ...initial.details,
249
+ results: failedResults,
250
+ progress: failedResults.map((entry) => entry.progress!).filter(Boolean),
251
+ },
252
+ };
253
+ const snapshot = { result, version: nextVersion() };
254
+ finalSnapshots.set(requestId, snapshot);
255
+ liveSnapshots.delete(requestId);
256
+ return { requestId, result };
257
+ }
258
+
259
+ function isSlashMessageDetails(value: unknown): value is SlashMessageDetails {
260
+ if (!value || typeof value !== "object") return false;
261
+ const v = value as { requestId?: string; result?: { content?: unknown; details?: { results?: unknown } } };
262
+ if (typeof v.requestId !== "string" || !v.requestId) return false;
263
+ if (!v.result || !Array.isArray(v.result.content)) return false;
264
+ return !!v.result.details && Array.isArray(v.result.details.results);
265
+ }
266
+
267
+ export function resolveSlashMessageDetails(value: unknown): SlashMessageDetails | undefined {
268
+ return isSlashMessageDetails(value) ? value : undefined;
269
+ }
270
+
271
+ export function getSlashRenderableSnapshot(details: SlashMessageDetails): SlashSnapshot {
272
+ return finalSnapshots.get(details.requestId)
273
+ ?? liveSnapshots.get(details.requestId)
274
+ ?? { result: details.result, version: 0 };
275
+ }
276
+
277
+ export function restoreSlashFinalSnapshots(entries: unknown[]): void {
278
+ liveSnapshots.clear();
279
+ finalSnapshots.clear();
280
+ for (const entry of entries) {
281
+ const e = entry as { type?: string; message?: { role?: string; customType?: string; display?: boolean; details?: unknown } };
282
+ if (e?.type !== "message") continue;
283
+ const m = e.message;
284
+ if (!m || m.role !== "custom" || m.customType !== SLASH_RESULT_TYPE || m.display !== false) continue;
285
+ const details = resolveSlashMessageDetails(m.details);
286
+ if (!details) continue;
287
+ finalSnapshots.set(details.requestId, { result: details.result, version: nextVersion() });
288
+ }
289
+ }
290
+
291
+ export function clearSlashSnapshots(): void {
292
+ liveSnapshots.clear();
293
+ finalSnapshots.clear();
294
+ }
@@ -51,7 +51,7 @@ interface TaskParam {
51
51
  progress?: boolean;
52
52
  }
53
53
 
54
- interface SubagentParamsLike {
54
+ export interface SubagentParamsLike {
55
55
  action?: string;
56
56
  agent?: string;
57
57
  task?: string;
@@ -107,6 +107,7 @@ function validateExecutionInput(
107
107
  hasChain: boolean,
108
108
  hasTasks: boolean,
109
109
  hasSingle: boolean,
110
+ allowClarifyTaskPrompt: boolean,
110
111
  ): AgentToolResult<Details> | null {
111
112
  if (Number(hasChain) + Number(hasTasks) + Number(hasSingle) !== 1) {
112
113
  return {
@@ -139,7 +140,7 @@ function validateExecutionInput(
139
140
  details: { mode: "chain" as const, results: [] },
140
141
  };
141
142
  }
142
- } else if (!(firstStep as SequentialStep).task && !params.task) {
143
+ } else if (!(firstStep as SequentialStep).task && !params.task && !allowClarifyTaskPrompt) {
143
144
  return {
144
145
  content: [{ type: "text", text: "First step in chain must have a task" }],
145
146
  isError: true,
@@ -806,8 +807,19 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
806
807
  const hasChain = (params.chain?.length ?? 0) > 0;
807
808
  const hasTasks = (params.tasks?.length ?? 0) > 0;
808
809
  const hasSingle = Boolean(params.agent && params.task);
810
+ const allowClarifyTaskPrompt = hasChain
811
+ && params.clarify === true
812
+ && ctx.hasUI
813
+ && !(params.chain?.some(isParallelStep) ?? false);
809
814
 
810
- const validationError = validateExecutionInput(params, agents, hasChain, hasTasks, hasSingle);
815
+ const validationError = validateExecutionInput(
816
+ params,
817
+ agents,
818
+ hasChain,
819
+ hasTasks,
820
+ hasSingle,
821
+ allowClarifyTaskPrompt,
822
+ );
811
823
  if (validationError) return validationError;
812
824
 
813
825
  let sessionFileForIndex: (idx?: number) => string | undefined = () => undefined;
package/types.ts CHANGED
@@ -263,6 +263,12 @@ export const MAX_CONCURRENCY = 4;
263
263
  export const RESULTS_DIR = path.join(os.tmpdir(), "pi-async-subagent-results");
264
264
  export const ASYNC_DIR = path.join(os.tmpdir(), "pi-async-subagent-runs");
265
265
  export const WIDGET_KEY = "subagent-async";
266
+ export const SLASH_RESULT_TYPE = "subagent-slash-result";
267
+ export const SLASH_SUBAGENT_REQUEST_EVENT = "subagent:slash:request";
268
+ export const SLASH_SUBAGENT_STARTED_EVENT = "subagent:slash:started";
269
+ export const SLASH_SUBAGENT_RESPONSE_EVENT = "subagent:slash:response";
270
+ export const SLASH_SUBAGENT_UPDATE_EVENT = "subagent:slash:update";
271
+ export const SLASH_SUBAGENT_CANCEL_EVENT = "subagent:slash:cancel";
266
272
  export const POLL_INTERVAL_MS = 250;
267
273
  export const MAX_WIDGET_JOBS = 4;
268
274
  export const DEFAULT_SUBAGENT_MAX_DEPTH = 2;