Skip to content

Commit 7848f9f

Browse files
committed
fix: slash commands and overlay launches render inline result cards
Replace sendUserMessage relay with event-bus bridge for all slash and overlay subagent execution. Launches now show a live streaming card with current tool, recent tools, and output. Adds slash-bridge.ts and slash-live-state.ts for the bridge protocol and snapshot store.
1 parent ed5673f commit 7848f9f

File tree

12 files changed

+1012
-142
lines changed

12 files changed

+1012
-142
lines changed

CHANGELOG.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,22 @@
22

33
## [Unreleased]
44

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+
521
## [0.11.8] - 2026-03-21
622

723
### Added

README.md

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -97,9 +97,11 @@ Semantics:
9797

9898
When `extensions` is present, it takes precedence over extension paths implied by `tools` entries.
9999

100-
**MCP Tools**
100+
**MCP Tools (optional)**
101101

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:
103105

104106
```yaml
105107
# All tools from a server
@@ -118,7 +120,7 @@ The `mcp:` items are additive — they don't affect which builtins the agent get
118120

119121
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.
120122

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.
122124

123125
**Resolution priority:** step override > agent frontmatter > disabled
124126

@@ -425,7 +427,9 @@ Skills are specialized instructions loaded from SKILL.md files and injected into
425427

426428
## Usage
427429

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:**
429433
```typescript
430434
// Single agent
431435
{ agent: "worker", task: "refactor auth" }

index.ts

Lines changed: 65 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,9 @@
1515
import * as fs from "node:fs";
1616
import * as os from "node:os";
1717
import * as path from "node:path";
18+
import type { AgentToolResult } from "@mariozechner/pi-agent-core";
1819
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";
2021
import { discoverAgents } from "./agents.js";
2122
import { cleanupAllArtifactDirs, cleanupOldArtifacts, getArtifactsDir } from "./artifacts.js";
2223
import { cleanupOldChainDirs } from "./settings.js";
@@ -28,13 +29,16 @@ import { createAsyncJobTracker } from "./async-job-tracker.js";
2829
import { createResultWatcher } from "./result-watcher.js";
2930
import { registerSlashCommands } from "./slash-commands.js";
3031
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";
3134
import {
3235
type Details,
3336
type ExtensionConfig,
3437
type SubagentState,
3538
ASYNC_DIR,
3639
DEFAULT_ARTIFACT_CONFIG,
3740
RESULTS_DIR,
41+
SLASH_RESULT_TYPE,
3842
WIDGET_KEY,
3943
} from "./types.js";
4044

@@ -92,6 +96,48 @@ function ensureAccessibleDir(dirPath: string): void {
9296
}
9397
}
9498

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+
95141
export default function registerSubagentExtension(pi: ExtensionAPI): void {
96142
ensureAccessibleDir(RESULTS_DIR);
97143
ensureAccessibleDir(ASYNC_DIR);
@@ -139,6 +185,19 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
139185
discoverAgents,
140186
});
141187

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+
142201
const promptTemplateBridge = registerPromptTemplateDelegationBridge({
143202
events: pi.events,
144203
getContext: () => state.lastUiContext,
@@ -340,7 +399,7 @@ MANAGEMENT (use action field — omit agent/task/chain/tasks):
340399

341400
pi.registerTool(tool);
342401
pi.registerTool(statusTool);
343-
registerSlashCommands(pi, state, getSubagentSessionRoot);
402+
registerSlashCommands(pi, state);
344403

345404
pi.events.on("subagent:started", handleStarted);
346405
pi.events.on("subagent:complete", handleComplete);
@@ -372,6 +431,7 @@ MANAGEMENT (use action field — omit agent/task/chain/tasks):
372431
state.lastUiContext = ctx;
373432
cleanupSessionArtifacts(ctx);
374433
resetJobs(ctx);
434+
restoreSlashFinalSnapshots(ctx.sessionManager.getEntries());
375435
};
376436

377437
pi.on("session_start", (_event, ctx) => {
@@ -392,6 +452,9 @@ MANAGEMENT (use action field — omit agent/task/chain/tasks):
392452
}
393453
state.cleanupTimers.clear();
394454
state.asyncJobs.clear();
455+
clearSlashSnapshots();
456+
slashBridge.cancelAll();
457+
slashBridge.dispose();
395458
promptTemplateBridge.cancelAll();
396459
promptTemplateBridge.dispose();
397460
if (state.lastUiContext?.hasUI) {

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "pi-subagents",
3-
"version": "0.11.8",
3+
"version": "0.11.9",
44
"description": "Pi extension for delegating tasks to subagents with chains, parallel execution, and TUI clarification",
55
"author": "Nico Bailon",
66
"license": "MIT",

render.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,36 @@ export function renderSubagentResult(
220220
);
221221
c.addChild(new Spacer(1));
222222

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+
223253
const items = getDisplayItems(r.messages);
224254
for (const item of items) {
225255
if (item.type === "tool")

0 commit comments

Comments
 (0)