Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/pi-herd/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
}
},
"devDependencies": {
"@earendil-works/pi-coding-agent": "^0.79.3",
"@earendil-works/pi-coding-agent": "^0.80.2",
"@weshipwork/pi-subagents": "workspace:*",
"@types/node": "^24.10.1",
"tsx": "^4.20.6",
Expand Down
6 changes: 3 additions & 3 deletions packages/pi-herdr/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,9 @@
}
},
"devDependencies": {
"@earendil-works/pi-ai": "^0.79.3",
"@earendil-works/pi-coding-agent": "^0.79.3",
"@earendil-works/pi-tui": "^0.79.3",
"@earendil-works/pi-ai": "^0.80.2",
"@earendil-works/pi-coding-agent": "^0.80.2",
"@earendil-works/pi-tui": "^0.80.2",
"@types/node": "^24.10.1",
"tsx": "^4.20.6",
"typebox": "^1.1.24",
Expand Down
24 changes: 24 additions & 0 deletions packages/pi-subagents/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.12.0-mirror.0] - 2026-06-26

### Changed
- Updated the WeShip.work fork from upstream `@tintinweb/pi-subagents` `v0.10.3` to `v0.12.0`, including upstream `v0.10.4`, `v0.11.0`, and `v0.12.0` changes.
- Preserved the read-only Mirror mode observation seam for `@weshipwork/pi-herd`.

## [0.12.0] - 2026-06-24

### Added
- **FleetView — a Claude Code-style subagent navigator below the editor.** A persistent, navigable list of `main` + every active subagent renders beneath the editor whenever agents are running — **auto-shown, no keypress needed** — mirroring Claude Code's bottom fleet bar: `⏺`/`◯` selection markers, agent type + description, right-aligned `elapsed · ↓ tokens`, and a `↓ N more` overflow once past five rows. Press `↓` (or `←`) at an **empty prompt** to move focus into the list, `↑`/`↓` to select, `Enter` to open the selected agent's live, auto-updating conversation overlay, and `Esc` (or `↑` above `main`) to return to the prompt. Implemented as a `belowEditor` widget with all key handling routed through `onTerminalInput` (which fires before the editor); it only captures arrow keys at an empty prompt — and acts on key-**press** only (kitty-protocol release events are ignored, otherwise each tap moved twice) — so typing, history, and cursor movement are untouched. Rows are ordered **earliest-launched first**; only openable agents (those with a session) are shown, so pending/queued agents appear once they start and `Enter` never dead-ends; **finished agents linger ~4s** before dropping out (their elapsed freezes at completion); and a viewer **stays open through its agent's completion** so the final output remains readable. Selection follows the viewed agent by id, so closing a viewer returns you to the same agent even if the list reordered while it was open. Every rendered line is width-clamped — the narrow-terminal crash/flicker class previously fixed in v0.2.7 and [#7](https://github.com/tintinweb/pi-subagents/issues/7). Toggle via `/agents → Settings → Fleet view` (default on; pure-UI, so no LLM-context cost).

## [0.11.0] - 2026-06-23

### Added
- **`persist_session` / `session_dir` agent frontmatter — persist a subagent as a real pi session** ([#111](https://github.com/tintinweb/pi-subagents/pull/111) — thanks [@codesoda](https://github.com/codesoda)). `persist_session: true` runs the subagent through `SessionManager.create(...)` instead of `SessionManager.inMemory(...)`, so its full transcript is written to pi's normal session location (`~/.pi/agent/sessions`) — inspectable and resumable after the fact, like a top-level session — rather than living in memory only. Useful for long-running, multi-round orchestrations (plan → review → implement → verify) where each subagent's conversation is worth keeping. `session_dir` optionally overrides where the persisted session is written (absolute, `~`, or agent-cwd-relative path); omitted, persistence follows pi's own precedence — `PI_CODING_AGENT_SESSION_DIR`, then the settings manager's `getSessionDir()`, then pi's default location. Both default off/unset, so existing agents are unchanged: the in-memory path is byte-identical to before, and the sidechain `.output` transcript is still written either way.

## [0.10.4] - 2026-06-23

### Fixed
- **Background agent records lost before result is read** ([#108](https://github.com/tintinweb/pi-subagents/issues/108) — thanks [@philipmw](https://github.com/philipmw)). On session switch or `/new`/`/resume`, `clearCompleted()` removed completed agent records regardless of whether the LLM had retrieved the result, causing `get_subagent_result` to return "Agent not found" for agents that had finished but hadn't been checked yet. `clearCompleted()` now accepts a `skipUnconsumed` flag; session event handlers pass `true`, so records with `resultConsumed=false` are preserved across session transitions. The 10-minute cleanup timer handles eventual eviction. Note: a full session shutdown (`session_shutdown`) calls `dispose()` which clears all records unconditionally — that path is not affected by this fix.

### Added
- **Foreground agent lifecycle completion and conversation logging** ([#105](https://github.com/tintinweb/pi-subagents/pull/105) — thanks [@benrhodeland](https://github.com/benrhodeland)). Two gaps closed: (1) **`onComplete` now fires for foreground agents**, emitting `subagents:completed` / `subagents:failed` lifecycle events and writing a `subagents:record` entry to the parent JSONL — previously only background agents emitted these, leaving cross-extension observers with an orphaned `subagents:started` event and no matching completion. `resultConsumed` is pre-set so the callback skips notifications (the result is returned inline); no change to the tool's return value. (2) **Foreground agent conversations are now streamed to `.output` files** (same `.pi/output/agent-<id>.jsonl` path as background agents) — inline subagent transcripts were previously permanently lost after `spawnAndWait` returned.

## [0.10.3] - 2026-06-12

### Added
Expand Down
26 changes: 22 additions & 4 deletions packages/pi-subagents/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ https://github.com/user-attachments/assets/8685261b-9338-4fea-8dfe-1c590d5df543
- **Claude Code look & feel** — same tool names, calling conventions, and UI patterns (`Agent`, `get_subagent_result`, `steer_subagent`) — feels native
- **Parallel background agents** — spawn multiple agents that run concurrently with automatic queuing (configurable concurrency limit, default 4) and smart group join (consolidated notifications)
- **Live widget UI** — persistent above-editor widget with animated spinners, live tool activity, token counts, and colored status icons
- **FleetView** — Claude Code-style navigable list of `main` + every running subagent rendered below the editor (earliest-launched first). Press `↓` (or `←`) at an empty prompt to jump in, `↑`/`↓` to move the selection, `Enter` to open the selected agent's live, auto-updating conversation, `Esc` to return. Finished agents linger briefly before dropping out, and a viewer stays open through completion so you can read the final output. Toggle via `/agents → Settings → Fleet view`
- **Conversation viewer** — select any agent in `/agents` to open a live-scrolling overlay of its full conversation (auto-follows new content, scroll up to pause). Stop a still-running agent from here by pressing `x` (then `x` again to confirm) — works for background agents too
- **Custom agent types** — define agents in `.pi/agents/<name>.md` with YAML frontmatter: custom system prompts, model selection, thinking levels, tool restrictions
- **Mid-run steering** — inject messages into running agents to redirect their work without restarting
Expand Down Expand Up @@ -50,7 +51,7 @@ pi install -l npm:@weshipwork/pi-subagents
Pin an exact npm version:

```sh
pi install npm:@weshipwork/pi-subagents@0.10.3-mirror.0
pi install npm:@weshipwork/pi-subagents@0.12.0-mirror.0
```

Try it for one Pi run without writing settings:
Expand Down Expand Up @@ -150,6 +151,21 @@ The token field is annotated with two optional signals inside parens:
- **`NN%`** — context-window utilization (color-coded: <70% dim, 70–85% warning, ≥85% error). Omitted when the model has no declared `contextWindow`, or briefly right after compaction.
- **`⇊N`** — number of times the session has compacted, when > 0. Stays dim; the percent's color carries urgency.

### FleetView

While subagents are running, a Claude Code-style navigable list renders **below** the editor:

```
esc to interrupt · ← for agents · ↓ to manage

⏺ main
◯ general-purpose Sleep then report 1 11s · ↓ 13.1k tokens
◯ general-purpose Sleep then report 2 11s · ↓ 13.1k tokens
↓ 3 more
```

The list is ordered earliest-launched first, and only shows agents you can actually open (pending/queued agents with no session yet appear once they start). At an **empty prompt**, press `↓` (or `←`) to move focus from the prompt into the list — the selected row is marked `⏺`, the rest `◯`. `↑`/`↓` move the selection, `Enter` opens the selected agent's live conversation overlay (it auto-updates as the agent works), and `Esc` (or `↑` above `main`) returns to the prompt. Selecting `main` returns to the normal view. A viewer stays open when its agent finishes so you can read the final output, and finished agents linger in the list for a few seconds before dropping out. Typing anything at a non-empty prompt behaves normally — the list only captures arrow keys when the prompt is empty. Disable it entirely via `/agents → Settings → Fleet view`.

Individual agent results render Claude Code-style in the conversation:

| State | Example |
Expand All @@ -163,7 +179,7 @@ Individual agent results render Claude Code-style in the conversation:

Completed results can be expanded (ctrl+o in pi) to show the full agent output inline.

Background agent completion notifications render as styled boxes:
Both foreground and background agents stream their full conversation to a `.pi/output/agent-<id>.jsonl` transcript file. Background agent completion notifications render as styled boxes:

```
✓ Find auth files completed
Expand Down Expand Up @@ -243,6 +259,8 @@ All fields are optional — sensible defaults for everything.
| `model` | inherit parent | Model — `provider/modelId` or fuzzy name (`"haiku"`, `"sonnet"`) |
| `thinking` | inherit | off, minimal, low, medium, high, xhigh |
| `max_turns` | unlimited | Max agentic turns before graceful shutdown. `0` or omit for unlimited |
| `persist_session` | `false` | Persist this subagent as a normal pi session instead of keeping the session in memory only. The sidechain output transcript is still written either way |
| `session_dir` | pi default | Optional session directory when `persist_session: true`; omitted uses pi's normal session location, and relative paths resolve from the agent cwd |
| `prompt_mode` | `replace` | `replace`: body is the full system prompt (no AGENTS.md / CLAUDE.md inheritance). `append`: body appended to parent's prompt (agent acts as a "parent twin" — inherits parent's AGENTS.md / CLAUDE.md) |
| `inherit_context` | `false` | Fork parent conversation into agent |
| `run_in_background` | `false` | Run in background by default |
Expand Down Expand Up @@ -457,8 +475,8 @@ Agent lifecycle events are emitted via `pi.events.emit()` so other extensions ca
|-------|------|------------|
| `subagents:created` | Background agent registered | `id`, `type`, `description`, `isBackground` |
| `subagents:started` | Agent transitions to running (including queued→running) | `id`, `type`, `description` |
| `subagents:completed` | Agent finished successfully | `id`, `type`, `durationMs`, `tokens` (lifetime `{ input, output, total }`), `toolUses`, `result` |
| `subagents:failed` | Agent errored, stopped, or aborted | same as completed + `error`, `status` |
| `subagents:completed` | Agent finished successfully (background and foreground) | `id`, `type`, `durationMs`, `tokens` (lifetime `{ input, output, total }`), `toolUses`, `result` |
| `subagents:failed` | Agent errored, stopped, or aborted (background and foreground) | same as completed + `error`, `status` |
| `subagents:steered` | Steering message sent | `id`, `message` |
| `subagents:compacted` | Agent's session successfully compacted | `id`, `type`, `description`, `reason` (`"manual"` / `"threshold"` / `"overflow"`), `tokensBefore`, `compactionCount` |
| `subagents:scheduled` | Schedule lifecycle change | `{ type: "added" \| "removed" \| "updated" \| "fired" \| "error", … }` (job/agentId/error fields per type) |
Expand Down
2 changes: 2 additions & 0 deletions packages/pi-subagents/README.mirror.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

This package is a minimal fork of `@tintinweb/pi-subagents` for `@weshipwork/pi-herd` Mirror mode.

Current fork release: `0.12.0-mirror.0`, tracking upstream `@tintinweb/pi-subagents` tag `v0.12.0`.

The fork adds a read-only observation seam at `src/mirror.ts` and installs `globalThis.__piSubagentsMirrorService` so `pi-herd` can mirror Subagent snapshots and live updates without hosting or steering Subagents.

## Install for Mirror mode
Expand Down
2 changes: 1 addition & 1 deletion packages/pi-subagents/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@weshipwork/pi-subagents",
"version": "0.10.3-mirror.0",
"version": "0.12.0-mirror.0",
"description": "Fork of tintinweb pi-subagents with read-only mirror observation support for pi-herd.",
"author": "Three One Four / tintinweb",
"license": "MIT",
Expand Down
53 changes: 45 additions & 8 deletions packages/pi-subagents/src/agent-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -311,7 +311,12 @@ export class AgentManager {
}
}

if (options.isBackground) {
// Fire onComplete for foreground agents too — lifecycle symmetry.
// Mark resultConsumed so the callback skips notifications (result returned inline).
if (!options.isBackground) {
record.resultConsumed = true;
try { this.onComplete?.(record); } catch { /* ignore completion side-effect errors */ }
} else {
this.runningBackground--;
try { this.onComplete?.(record); } catch { /* ignore completion side-effect errors */ }
this.drainQueue();
Expand Down Expand Up @@ -342,7 +347,12 @@ export class AgentManager {
} catch { /* ignore cleanup errors */ }
}

if (options.isBackground) {
// Fire onComplete for foreground agents too — lifecycle symmetry.
// Mark resultConsumed so the callback skips notifications (result returned inline).
if (!options.isBackground) {
record.resultConsumed = true;
this.onComplete?.(record);
} else {
this.runningBackground--;
this.onComplete?.(record);
this.drainQueue();
Expand All @@ -351,6 +361,11 @@ export class AgentManager {
});

record.promise = promise;

// Notify caller that spawn is complete (record is in the map, promise is set).
// Called synchronously — onSessionCreated fires asynchronously inside runAgent.
// Used by spawnAndWait to let the caller set up output files before streaming starts.
this.onSpawned?.(id);
}

/** Start queued agents up to the concurrency limit. */
Expand All @@ -372,21 +387,40 @@ export class AgentManager {
}
}

/**
* Called synchronously right after spawn, before onSessionCreated fires.
* Lets the caller set up the output file path on the record.
* The record is guaranteed to be in this.agents at this point.
*/
private onSpawned?: (id: string) => void;

/**
* Spawn an agent and wait for completion (foreground use).
* Foreground agents bypass the concurrency queue.
* Returns { id, record } so callers can access the agent ID.
*
* @param onSpawned - Called synchronously after spawn(), before onSessionCreated fires.
* Use this to set record.outputFile so streamToOutputFile can pick it up.
*/
async spawnAndWait(
pi: ExtensionAPI,
ctx: ExtensionContext,
type: SubagentType,
prompt: string,
options: Omit<SpawnOptions, "isBackground">,
): Promise<AgentRecord> {
const id = this.spawn(pi, ctx, type, prompt, { ...options, isBackground: false });
const record = this.agents.get(id)!;
await record.promise;
return record;
onSpawned?: (id: string) => void,
): Promise<{ id: string; record: AgentRecord }> {
// Temporarily register the onSpawned hook so startAgent can call it.
const prevOnSpawned = this.onSpawned;
this.onSpawned = onSpawned;
try {
const id = this.spawn(pi, ctx, type, prompt, { ...options, isBackground: false });
const record = this.agents.get(id)!;
await record.promise;
return { id, record };
} finally {
this.onSpawned = prevOnSpawned;
}
}

/**
Expand Down Expand Up @@ -480,10 +514,13 @@ export class AgentManager {
/**
* Remove all completed/stopped/errored records immediately.
* Called on session start/switch so tasks from a prior session don't persist.
* Pass skipUnconsumed=true to preserve records the LLM hasn't read yet
* (resultConsumed=false) — they will be evicted by the 10-minute cleanup timer instead.
*/
clearCompleted(): void {
clearCompleted(skipUnconsumed = false): void {
for (const [id, record] of this.agents) {
if (record.status === "running" || record.status === "queued") continue;
if (skipUnconsumed && !record.resultConsumed) continue;
this.removeRecord(id, record);
}
}
Expand Down
18 changes: 16 additions & 2 deletions packages/pi-subagents/src/agent-runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,13 @@ function forwardAbortSignal(session: AgentSession, signal?: AbortSignal): () =>
return () => signal.removeEventListener("abort", onAbort);
}

function resolveConfiguredSessionDir(sessionDir: string | undefined, cwd: string): string | undefined {
if (!sessionDir) return undefined;
if (sessionDir === "~" || sessionDir.startsWith("~/")) return resolve(homedir(), sessionDir.slice(2));
if (isAbsolute(sessionDir)) return sessionDir;
return resolve(cwd, sessionDir);
}

export async function runAgent(
ctx: ExtensionContext,
type: SubagentType,
Expand Down Expand Up @@ -576,11 +583,18 @@ export async function runAgent(
return !noExtensions;
});

const settingsManager = SettingsManager.create(configCwd, agentDir);
const configuredSessionDir = resolveConfiguredSessionDir(agentConfig?.sessionDir, effectiveCwd);
const defaultSessionDir = process.env.PI_CODING_AGENT_SESSION_DIR ?? settingsManager.getSessionDir?.();
const sessionManager = agentConfig?.persistSession
? SessionManager.create(effectiveCwd, configuredSessionDir ?? defaultSessionDir)
: SessionManager.inMemory(effectiveCwd);

const sessionOpts: Parameters<typeof createAgentSession>[0] = {
cwd: effectiveCwd,
agentDir,
sessionManager: SessionManager.inMemory(effectiveCwd),
settingsManager: SettingsManager.create(configCwd, agentDir),
sessionManager,
settingsManager,
modelRegistry: ctx.modelRegistry,
model,
tools: allowedTools,
Expand Down
2 changes: 2 additions & 0 deletions packages/pi-subagents/src/custom-agents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ function loadFromDir(dir: string, agents: Map<string, AgentConfig>, source: "pro
model: str(fm.model),
thinking: str(fm.thinking) as ThinkingLevel | undefined,
maxTurns: nonNegativeInt(fm.max_turns),
persistSession: fm.persist_session != null ? fm.persist_session === true : undefined,
sessionDir: str(fm.session_dir),
systemPrompt: body.trim(),
promptMode: fm.prompt_mode === "append" ? "append" : "replace",
inheritContext: fm.inherit_context != null ? fm.inherit_context === true : undefined,
Expand Down
Loading