@melihmucuk/pi-crew 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +199 -0
  3. package/agents/code-reviewer.md +145 -0
  4. package/agents/planner.md +142 -0
  5. package/agents/quality-reviewer.md +164 -0
  6. package/agents/scout.md +58 -0
  7. package/agents/worker.md +81 -0
  8. package/dist/agent-discovery.d.ts +34 -0
  9. package/dist/agent-discovery.js +527 -0
  10. package/dist/bootstrap-session.d.ts +11 -0
  11. package/dist/bootstrap-session.js +63 -0
  12. package/dist/crew-manager.d.ts +43 -0
  13. package/dist/crew-manager.js +235 -0
  14. package/dist/index.d.ts +2 -0
  15. package/dist/index.js +27 -0
  16. package/dist/integration/register-command.d.ts +3 -0
  17. package/dist/integration/register-command.js +51 -0
  18. package/dist/integration/register-renderers.d.ts +2 -0
  19. package/dist/integration/register-renderers.js +50 -0
  20. package/dist/integration/register-tools.d.ts +3 -0
  21. package/dist/integration/register-tools.js +25 -0
  22. package/dist/integration/tool-presentation.d.ts +30 -0
  23. package/dist/integration/tool-presentation.js +29 -0
  24. package/dist/integration/tools/crew-abort.d.ts +2 -0
  25. package/dist/integration/tools/crew-abort.js +79 -0
  26. package/dist/integration/tools/crew-done.d.ts +2 -0
  27. package/dist/integration/tools/crew-done.js +28 -0
  28. package/dist/integration/tools/crew-list.d.ts +2 -0
  29. package/dist/integration/tools/crew-list.js +72 -0
  30. package/dist/integration/tools/crew-respond.d.ts +2 -0
  31. package/dist/integration/tools/crew-respond.js +30 -0
  32. package/dist/integration/tools/crew-spawn.d.ts +2 -0
  33. package/dist/integration/tools/crew-spawn.js +42 -0
  34. package/dist/integration/tools/tool-deps.d.ts +8 -0
  35. package/dist/integration/tools/tool-deps.js +1 -0
  36. package/dist/integration.d.ts +3 -0
  37. package/dist/integration.js +8 -0
  38. package/dist/runtime/delivery-coordinator.d.ts +17 -0
  39. package/dist/runtime/delivery-coordinator.js +60 -0
  40. package/dist/runtime/subagent-registry.d.ts +13 -0
  41. package/dist/runtime/subagent-registry.js +55 -0
  42. package/dist/runtime/subagent-state.d.ts +34 -0
  43. package/dist/runtime/subagent-state.js +34 -0
  44. package/dist/status-widget.d.ts +3 -0
  45. package/dist/status-widget.js +84 -0
  46. package/dist/subagent-messages.d.ts +30 -0
  47. package/dist/subagent-messages.js +58 -0
  48. package/dist/tool-registry.d.ts +76 -0
  49. package/dist/tool-registry.js +17 -0
  50. package/docs/architecture.md +883 -0
  51. package/package.json +52 -0
  52. package/prompts/pi-crew:review.md +168 -0
@@ -0,0 +1,2 @@
1
+ import type { CrewToolDeps } from "./tool-deps.js";
2
+ export declare function registerCrewDoneTool({ pi, crewManager }: CrewToolDeps): void;
@@ -0,0 +1,28 @@
1
+ import { Type } from "@sinclair/typebox";
2
+ import { renderCrewCall, renderCrewResult, toolError, toolSuccess, } from "../tool-presentation.js";
3
+ export function registerCrewDoneTool({ pi, crewManager }) {
4
+ pi.registerTool({
5
+ name: "crew_done",
6
+ label: "Done with Crew",
7
+ description: "Close an interactive subagent session. Use when you no longer need to interact with the subagent.",
8
+ parameters: Type.Object({
9
+ subagent_id: Type.String({ description: "ID of the subagent to close" }),
10
+ }),
11
+ promptSnippet: "Close an interactive subagent session when done.",
12
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
13
+ const callerSessionId = ctx.sessionManager.getSessionId();
14
+ const { error } = crewManager.done(params.subagent_id, callerSessionId);
15
+ if (error)
16
+ return toolError(error);
17
+ return toolSuccess(`Subagent ${params.subagent_id} closed.`, {
18
+ id: params.subagent_id,
19
+ });
20
+ },
21
+ renderCall(args, theme, _context) {
22
+ return renderCrewCall(theme, "crew_done", args.subagent_id || "...");
23
+ },
24
+ renderResult(result, _options, theme, _context) {
25
+ return renderCrewResult(result, theme);
26
+ },
27
+ });
28
+ }
@@ -0,0 +1,2 @@
1
+ import type { CrewToolDeps } from "./tool-deps.js";
2
+ export declare function registerCrewListTool({ pi, crewManager, notifyDiscoveryWarnings, }: CrewToolDeps): void;
@@ -0,0 +1,72 @@
1
+ import { Text } from "@mariozechner/pi-tui";
2
+ import { Type } from "@sinclair/typebox";
3
+ import { discoverAgents } from "../../agent-discovery.js";
4
+ import { STATUS_ICON } from "../../subagent-messages.js";
5
+ export function registerCrewListTool({ pi, crewManager, notifyDiscoveryWarnings, }) {
6
+ pi.registerTool({
7
+ name: "crew_list",
8
+ label: "List Crew",
9
+ description: "List available subagent definitions (from <cwd>/.pi/agents/, ~/.pi/agent/agents/, and bundled agents, with optional global/project pi-crew.json overrides) and currently running subagents with their status.",
10
+ parameters: Type.Object({}),
11
+ promptSnippet: "List subagent definitions and active subagents",
12
+ async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
13
+ const { agents, warnings } = discoverAgents(ctx.cwd);
14
+ notifyDiscoveryWarnings(ctx, warnings);
15
+ const callerSessionId = ctx.sessionManager.getSessionId();
16
+ const running = crewManager.getActiveSummariesForOwner(callerSessionId);
17
+ const lines = [];
18
+ lines.push("## Available subagents");
19
+ if (agents.length === 0) {
20
+ lines.push("No valid subagent definitions found. Add `.md` files to `<cwd>/.pi/agents/` or `~/.pi/agent/agents/`.");
21
+ }
22
+ else {
23
+ for (const agent of agents) {
24
+ lines.push("");
25
+ lines.push(`**${agent.name}**`);
26
+ if (agent.description)
27
+ lines.push(` ${agent.description}`);
28
+ if (agent.model)
29
+ lines.push(` model: ${agent.model}`);
30
+ if (agent.interactive)
31
+ lines.push(" interactive: true");
32
+ if (agent.tools !== undefined) {
33
+ lines.push(` tools: ${agent.tools.length > 0 ? agent.tools.join(", ") : "none"}`);
34
+ }
35
+ if (agent.skills !== undefined) {
36
+ lines.push(` skills: ${agent.skills.length > 0 ? agent.skills.join(", ") : "none"}`);
37
+ }
38
+ }
39
+ }
40
+ if (warnings.length > 0) {
41
+ lines.push("");
42
+ lines.push("## Ignored subagent definitions");
43
+ for (const warning of warnings) {
44
+ lines.push(`- ${warning.message} (${warning.filePath})`);
45
+ }
46
+ }
47
+ lines.push("");
48
+ lines.push("## Active subagents");
49
+ if (running.length === 0) {
50
+ lines.push("No subagents currently active.");
51
+ }
52
+ else {
53
+ for (const agent of running) {
54
+ const icon = STATUS_ICON[agent.status] ?? "❓";
55
+ lines.push("");
56
+ lines.push(`**${agent.id}** (${agent.agentName}) — ${icon} ${agent.status}`);
57
+ lines.push(` task: ${agent.taskPreview}`);
58
+ lines.push(` turns: ${agent.turns}`);
59
+ }
60
+ }
61
+ const text = lines.join("\n");
62
+ return { content: [{ type: "text", text }], details: {} };
63
+ },
64
+ renderCall(_args, theme, _context) {
65
+ return new Text(theme.fg("toolTitle", theme.bold("crew_list")), 0, 0);
66
+ },
67
+ renderResult(result, _options, _theme, _context) {
68
+ const text = result.content[0];
69
+ return new Text(text?.type === "text" ? text.text : "(no output)", 0, 0);
70
+ },
71
+ });
72
+ }
@@ -0,0 +1,2 @@
1
+ import type { CrewToolDeps } from "./tool-deps.js";
2
+ export declare function registerCrewRespondTool({ pi, crewManager }: CrewToolDeps): void;
@@ -0,0 +1,30 @@
1
+ import { Type } from "@sinclair/typebox";
2
+ import { renderCrewCall, renderCrewResult, toolError, toolSuccess, truncatePreview, } from "../tool-presentation.js";
3
+ export function registerCrewRespondTool({ pi, crewManager }) {
4
+ pi.registerTool({
5
+ name: "crew_respond",
6
+ label: "Respond to Crew",
7
+ description: "Send a follow-up message to an interactive subagent that is waiting for a response. Use crew_list to see waiting subagents.",
8
+ parameters: Type.Object({
9
+ subagent_id: Type.String({
10
+ description: "ID of the waiting subagent (from crew_list or crew_spawn result)",
11
+ }),
12
+ message: Type.String({ description: "Message to send to the subagent" }),
13
+ }),
14
+ promptSnippet: "Send a follow-up message to a waiting interactive subagent.",
15
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
16
+ const callerSessionId = ctx.sessionManager.getSessionId();
17
+ const { error } = crewManager.respond(params.subagent_id, params.message, pi, callerSessionId);
18
+ if (error)
19
+ return toolError(error);
20
+ return toolSuccess(`Message sent to subagent ${params.subagent_id}. Response will be delivered as a steering message.`, { id: params.subagent_id });
21
+ },
22
+ renderCall(args, theme, _context) {
23
+ const preview = args.message ? truncatePreview(args.message, 60) : "...";
24
+ return renderCrewCall(theme, "crew_respond", args.subagent_id || "...", preview);
25
+ },
26
+ renderResult(result, _options, theme, _context) {
27
+ return renderCrewResult(result, theme);
28
+ },
29
+ });
30
+ }
@@ -0,0 +1,2 @@
1
+ import type { CrewToolDeps } from "./tool-deps.js";
2
+ export declare function registerCrewSpawnTool({ pi, crewManager, notifyDiscoveryWarnings, }: CrewToolDeps): void;
@@ -0,0 +1,42 @@
1
+ import { Type } from "@sinclair/typebox";
2
+ import { discoverAgents } from "../../agent-discovery.js";
3
+ import { renderCrewCall, renderCrewResult, toolError, toolSuccess, truncatePreview, } from "../tool-presentation.js";
4
+ export function registerCrewSpawnTool({ pi, crewManager, notifyDiscoveryWarnings, }) {
5
+ pi.registerTool({
6
+ name: "crew_spawn",
7
+ label: "Spawn Crew",
8
+ description: "Spawn a non-blocking subagent that runs in an isolated session. The subagent works independently while the current session stays interactive. Results are delivered back to the spawning session as steering messages when done. Use crew_list first to see available subagents.",
9
+ parameters: Type.Object({
10
+ subagent: Type.String({ description: "Subagent name from crew_list" }),
11
+ task: Type.String({ description: "Task to delegate to the subagent" }),
12
+ }),
13
+ promptSnippet: "Spawn a non-blocking subagent. Use crew_list first to see available subagents.",
14
+ promptGuidelines: [
15
+ "Use crew_* tools to delegate parallelizable, independent tasks to specialized subagents. For interactive multi-turn workflows, use crew_respond/crew_done. Avoid spawning for trivial, single-turn tasks.",
16
+ "crew_spawn: Always call crew_list first to see which subagents are available before spawning.",
17
+ "crew_spawn: The spawned subagent runs in a separate context window with no access to the current conversation. Include all relevant context (file paths, requirements, prior findings) directly in the task parameter.",
18
+ "crew_spawn: Results are delivered asynchronously as steering messages. Do not block or poll for completion; continue working on other tasks.",
19
+ "crew_spawn: Interactive subagents (marked with 'interactive' in crew_list) stay alive after responding. Use crew_respond to continue the conversation and crew_done to close when finished.",
20
+ "crew_spawn: When multiple subagents are spawned, each result arrives as a separate steering message. NEVER predict or fabricate results for subagents that have not yet reported back. Wait for ALL crew-result messages.",
21
+ ],
22
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
23
+ const { agents, warnings } = discoverAgents(ctx.cwd);
24
+ notifyDiscoveryWarnings(ctx, warnings);
25
+ const subagent = agents.find((candidate) => candidate.name === params.subagent);
26
+ if (!subagent) {
27
+ const available = agents.map((candidate) => candidate.name).join(", ") || "none";
28
+ return toolError(`Unknown subagent: "${params.subagent}". Available: ${available}`);
29
+ }
30
+ const ownerSessionId = ctx.sessionManager.getSessionId();
31
+ const id = crewManager.spawn(subagent, params.task, ctx.cwd, ownerSessionId, ctx, pi);
32
+ return toolSuccess(`Subagent '${subagent.name}' spawned as ${id}. Result will be delivered as a steering message when done.`, { id });
33
+ },
34
+ renderCall(args, theme, _context) {
35
+ const preview = args.task ? truncatePreview(args.task, 60) : "...";
36
+ return renderCrewCall(theme, "crew_spawn", args.subagent || "...", preview);
37
+ },
38
+ renderResult(result, _options, theme, _context) {
39
+ return renderCrewResult(result, theme);
40
+ },
41
+ });
42
+ }
@@ -0,0 +1,8 @@
1
+ import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
2
+ import type { AgentDiscoveryWarning } from "../../agent-discovery.js";
3
+ import type { CrewManager } from "../../crew-manager.js";
4
+ export interface CrewToolDeps {
5
+ pi: ExtensionAPI;
6
+ crewManager: CrewManager;
7
+ notifyDiscoveryWarnings: (ctx: ExtensionContext, warnings: AgentDiscoveryWarning[]) => void;
8
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,3 @@
1
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
2
+ import type { CrewManager } from "./crew-manager.js";
3
+ export declare function registerCrewIntegration(pi: ExtensionAPI, crewManager: CrewManager): void;
@@ -0,0 +1,8 @@
1
+ import { registerCrewCommand } from "./integration/register-command.js";
2
+ import { registerCrewMessageRenderers } from "./integration/register-renderers.js";
3
+ import { registerCrewTools } from "./integration/register-tools.js";
4
+ export function registerCrewIntegration(pi, crewManager) {
5
+ registerCrewTools(pi, crewManager);
6
+ registerCrewCommand(pi, crewManager);
7
+ registerCrewMessageRenderers(pi);
8
+ }
@@ -0,0 +1,17 @@
1
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
2
+ import { type SteeringPayload } from "../subagent-messages.js";
3
+ export declare class DeliveryCoordinator {
4
+ private currentSessionId;
5
+ private currentIsIdle;
6
+ private pendingMessages;
7
+ activateSession(sessionId: string, isIdle: () => boolean, pi: ExtensionAPI, countRunningForOwner: (ownerSessionId: string, excludeId: string) => number): void;
8
+ deliver(ownerSessionId: string, payload: SteeringPayload, pi: ExtensionAPI, countRunningForOwner: (ownerSessionId: string, excludeId: string) => number): void;
9
+ clearPendingForOwner(ownerSessionId: string): void;
10
+ private flushPending;
11
+ /**
12
+ * Result messages always go first. If more subagents are still running and the
13
+ * owner is idle, queue the result without triggering, then queue the separate
14
+ * remaining note with triggerTurn so the next turn sees both in order.
15
+ */
16
+ private send;
17
+ }
@@ -0,0 +1,60 @@
1
+ import { sendRemainingNote, sendSteeringMessage, } from "../subagent-messages.js";
2
+ export class DeliveryCoordinator {
3
+ currentSessionId;
4
+ currentIsIdle = () => true;
5
+ pendingMessages = [];
6
+ activateSession(sessionId, isIdle, pi, countRunningForOwner) {
7
+ this.currentSessionId = sessionId;
8
+ this.currentIsIdle = isIdle;
9
+ // Delay flush to next macrotask. session_switch fires before pi-core
10
+ // calls _reconnectToAgent(), so synchronous delivery would emit agent
11
+ // events while the session listener is disconnected, losing JSONL persistence.
12
+ if (this.pendingMessages.some((entry) => entry.ownerSessionId === sessionId)) {
13
+ setTimeout(() => this.flushPending(pi, countRunningForOwner), 0);
14
+ }
15
+ }
16
+ deliver(ownerSessionId, payload, pi, countRunningForOwner) {
17
+ if (ownerSessionId !== this.currentSessionId) {
18
+ this.pendingMessages.push({ ownerSessionId, payload });
19
+ return;
20
+ }
21
+ this.send(ownerSessionId, payload, pi, countRunningForOwner);
22
+ }
23
+ clearPendingForOwner(ownerSessionId) {
24
+ this.pendingMessages = this.pendingMessages.filter((entry) => entry.ownerSessionId !== ownerSessionId);
25
+ }
26
+ flushPending(pi, countRunningForOwner) {
27
+ const toDeliver = [];
28
+ const remaining = [];
29
+ for (const entry of this.pendingMessages) {
30
+ if (entry.ownerSessionId === this.currentSessionId) {
31
+ toDeliver.push(entry);
32
+ }
33
+ else {
34
+ remaining.push(entry);
35
+ }
36
+ }
37
+ this.pendingMessages = remaining;
38
+ for (const entry of toDeliver) {
39
+ this.send(entry.ownerSessionId, entry.payload, pi, countRunningForOwner);
40
+ }
41
+ }
42
+ /**
43
+ * Result messages always go first. If more subagents are still running and the
44
+ * owner is idle, queue the result without triggering, then queue the separate
45
+ * remaining note with triggerTurn so the next turn sees both in order.
46
+ */
47
+ send(ownerSessionId, payload, pi, countRunningForOwner) {
48
+ const remaining = countRunningForOwner(ownerSessionId, payload.id);
49
+ const isIdle = this.currentIsIdle();
50
+ const triggerResultTurn = !(isIdle && remaining > 0);
51
+ sendSteeringMessage(payload, pi, {
52
+ isIdle,
53
+ triggerTurn: triggerResultTurn,
54
+ });
55
+ sendRemainingNote(remaining, pi, {
56
+ isIdle,
57
+ triggerTurn: isIdle && remaining > 0,
58
+ });
59
+ }
60
+ }
@@ -0,0 +1,13 @@
1
+ import type { AgentConfig } from "../agent-discovery.js";
2
+ import type { AbortableAgentSummary, ActiveAgentSummary, SubagentState } from "./subagent-state.js";
3
+ export declare class SubagentRegistry {
4
+ private activeAgents;
5
+ create(agentConfig: AgentConfig, task: string, ownerSessionId: string): SubagentState;
6
+ get(id: string): SubagentState | undefined;
7
+ hasState(state: SubagentState): boolean;
8
+ delete(id: string): void;
9
+ countRunningForOwner(ownerSessionId: string, excludeId: string): number;
10
+ getAbortableAgents(): AbortableAgentSummary[];
11
+ getActiveSummariesForOwner(ownerSessionId: string): ActiveAgentSummary[];
12
+ getOwnedAbortableIds(ownerSessionId: string): string[];
13
+ }
@@ -0,0 +1,55 @@
1
+ import { buildAbortableAgentSummary, buildActiveAgentSummary, generateId, isAbortableStatus, } from "./subagent-state.js";
2
+ export class SubagentRegistry {
3
+ activeAgents = new Map();
4
+ create(agentConfig, task, ownerSessionId) {
5
+ const id = generateId(agentConfig.name, new Set(this.activeAgents.keys()));
6
+ const state = {
7
+ id,
8
+ agentConfig,
9
+ task,
10
+ status: "running",
11
+ ownerSessionId,
12
+ session: null,
13
+ turns: 0,
14
+ contextTokens: 0,
15
+ model: undefined,
16
+ };
17
+ this.activeAgents.set(id, state);
18
+ return state;
19
+ }
20
+ get(id) {
21
+ return this.activeAgents.get(id);
22
+ }
23
+ hasState(state) {
24
+ return this.activeAgents.get(state.id) === state;
25
+ }
26
+ delete(id) {
27
+ this.activeAgents.delete(id);
28
+ }
29
+ countRunningForOwner(ownerSessionId, excludeId) {
30
+ let count = 0;
31
+ for (const state of this.activeAgents.values()) {
32
+ if (state.id !== excludeId &&
33
+ state.ownerSessionId === ownerSessionId &&
34
+ state.status === "running") {
35
+ count++;
36
+ }
37
+ }
38
+ return count;
39
+ }
40
+ getAbortableAgents() {
41
+ return Array.from(this.activeAgents.values())
42
+ .filter((state) => isAbortableStatus(state.status))
43
+ .map(buildAbortableAgentSummary);
44
+ }
45
+ getActiveSummariesForOwner(ownerSessionId) {
46
+ return Array.from(this.activeAgents.values())
47
+ .filter((state) => isAbortableStatus(state.status) && state.ownerSessionId === ownerSessionId)
48
+ .map(buildActiveAgentSummary);
49
+ }
50
+ getOwnedAbortableIds(ownerSessionId) {
51
+ return Array.from(this.activeAgents.values())
52
+ .filter((state) => state.ownerSessionId === ownerSessionId && isAbortableStatus(state.status))
53
+ .map((state) => state.id);
54
+ }
55
+ }
@@ -0,0 +1,34 @@
1
+ import type { AgentSession } from "@mariozechner/pi-coding-agent";
2
+ import type { AgentConfig } from "../agent-discovery.js";
3
+ import type { SubagentStatus } from "../subagent-messages.js";
4
+ export interface SubagentState {
5
+ id: string;
6
+ agentConfig: AgentConfig;
7
+ task: string;
8
+ status: SubagentStatus;
9
+ ownerSessionId: string;
10
+ session: AgentSession | null;
11
+ turns: number;
12
+ contextTokens: number;
13
+ model: string | undefined;
14
+ error?: string;
15
+ result?: string;
16
+ }
17
+ export interface ActiveAgentSummary {
18
+ id: string;
19
+ agentName: string;
20
+ status: SubagentStatus;
21
+ taskPreview: string;
22
+ turns: number;
23
+ contextTokens: number;
24
+ model: string | undefined;
25
+ }
26
+ export interface AbortableAgentSummary {
27
+ id: string;
28
+ agentName: string;
29
+ }
30
+ export declare function generateId(name: string, existingIds: Set<string>): string;
31
+ export declare function isAborted(state: SubagentState): boolean;
32
+ export declare function isAbortableStatus(status: SubagentStatus): boolean;
33
+ export declare function buildActiveAgentSummary(state: SubagentState): ActiveAgentSummary;
34
+ export declare function buildAbortableAgentSummary(state: SubagentState): AbortableAgentSummary;
@@ -0,0 +1,34 @@
1
+ import { randomBytes } from "node:crypto";
2
+ export function generateId(name, existingIds) {
3
+ for (let i = 0; i < 10; i++) {
4
+ const id = `${name}-${randomBytes(4).toString("hex")}`;
5
+ if (!existingIds.has(id))
6
+ return id;
7
+ }
8
+ return `${name}-${randomBytes(8).toString("hex")}`;
9
+ }
10
+ // Status may change externally via abort(). Standalone function avoids TS narrowing.
11
+ export function isAborted(state) {
12
+ return state.status === "aborted";
13
+ }
14
+ export function isAbortableStatus(status) {
15
+ return status === "running" || status === "waiting";
16
+ }
17
+ export function buildActiveAgentSummary(state) {
18
+ const taskPreview = state.task.length > 80 ? `${state.task.slice(0, 80)}...` : state.task;
19
+ return {
20
+ id: state.id,
21
+ agentName: state.agentConfig.name,
22
+ status: state.status,
23
+ taskPreview,
24
+ turns: state.turns,
25
+ contextTokens: state.contextTokens,
26
+ model: state.model,
27
+ };
28
+ }
29
+ export function buildAbortableAgentSummary(state) {
30
+ return {
31
+ id: state.id,
32
+ agentName: state.agentConfig.name,
33
+ };
34
+ }
@@ -0,0 +1,3 @@
1
+ import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
2
+ import type { CrewManager } from "./crew-manager.js";
3
+ export declare function updateWidget(ctx: ExtensionContext, crewManager: CrewManager): void;
@@ -0,0 +1,84 @@
1
+ import { Text } from "@mariozechner/pi-tui";
2
+ const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
3
+ const SPINNER_INTERVAL_MS = 80;
4
+ function formatTokens(tokens) {
5
+ if (tokens >= 1_000_000)
6
+ return `${(tokens / 1_000_000).toFixed(1)}M`;
7
+ if (tokens >= 1_000)
8
+ return `${(tokens / 1_000).toFixed(1)}k`;
9
+ return String(tokens);
10
+ }
11
+ function buildLine(agent, frame) {
12
+ const model = agent.model ?? "…";
13
+ const icon = agent.status === "waiting" ? "⏳" : frame;
14
+ return `${icon} ${agent.id} (${model}) · turn ${agent.turns} · ${formatTokens(agent.contextTokens)} ctx`;
15
+ }
16
+ let widget;
17
+ function disposeWidget(state) {
18
+ clearInterval(state.timer);
19
+ if (widget === state) {
20
+ widget = undefined;
21
+ }
22
+ }
23
+ function clearWidget() {
24
+ const current = widget;
25
+ if (!current)
26
+ return;
27
+ disposeWidget(current);
28
+ current.ctx.ui.setWidget("crew-status", undefined);
29
+ }
30
+ function hasRunningAgent(agents) {
31
+ return agents.some((agent) => agent.status === "running");
32
+ }
33
+ function syncWidgetText(state, agents) {
34
+ const frame = SPINNER_FRAMES[state.frameIndex % SPINNER_FRAMES.length];
35
+ const lines = agents.map((agent) => buildLine(agent, frame));
36
+ state.text.setText(lines.join("\n"));
37
+ state.tui.requestRender();
38
+ }
39
+ export function updateWidget(ctx, crewManager) {
40
+ if (!ctx.hasUI) {
41
+ clearWidget();
42
+ return;
43
+ }
44
+ const ownerSessionId = ctx.sessionManager.getSessionId();
45
+ const running = crewManager.getActiveSummariesForOwner(ownerSessionId);
46
+ if (running.length === 0) {
47
+ clearWidget();
48
+ return;
49
+ }
50
+ if (widget && widget.ctx !== ctx) {
51
+ clearWidget();
52
+ }
53
+ if (widget) {
54
+ syncWidgetText(widget, running);
55
+ return;
56
+ }
57
+ ctx.ui.setWidget("crew-status", (tui, _theme) => {
58
+ const text = new Text("", 1, 0);
59
+ const state = {
60
+ ctx,
61
+ text,
62
+ tui,
63
+ frameIndex: 0,
64
+ timer: setInterval(() => {
65
+ const agents = crewManager.getActiveSummariesForOwner(ownerSessionId);
66
+ if (agents.length === 0) {
67
+ clearWidget();
68
+ return;
69
+ }
70
+ if (!hasRunningAgent(agents))
71
+ return;
72
+ state.frameIndex++;
73
+ syncWidgetText(state, agents);
74
+ }, SPINNER_INTERVAL_MS),
75
+ };
76
+ widget = state;
77
+ syncWidgetText(state, running);
78
+ return Object.assign(text, {
79
+ dispose() {
80
+ disposeWidget(state);
81
+ },
82
+ });
83
+ });
84
+ }
@@ -0,0 +1,30 @@
1
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
2
+ export type SubagentStatus = "running" | "waiting" | "done" | "error" | "aborted";
3
+ export declare const STATUS_ICON: Record<SubagentStatus, string>;
4
+ export declare const STATUS_LABEL: Record<SubagentStatus, string>;
5
+ export interface SteeringPayload {
6
+ id: string;
7
+ agentName: string;
8
+ status: SubagentStatus;
9
+ result?: string;
10
+ error?: string;
11
+ }
12
+ export interface CrewResultMessageDetails {
13
+ agentId: string;
14
+ agentName: string;
15
+ status: SubagentStatus;
16
+ body?: string;
17
+ }
18
+ export declare function getCrewResultTitle(details: {
19
+ agentId: string;
20
+ agentName: string;
21
+ status: SubagentStatus;
22
+ }): string;
23
+ export declare function sendSteeringMessage(payload: SteeringPayload, pi: ExtensionAPI, opts: {
24
+ isIdle: boolean;
25
+ triggerTurn: boolean;
26
+ }): void;
27
+ export declare function sendRemainingNote(remainingCount: number, pi: ExtensionAPI, opts: {
28
+ isIdle: boolean;
29
+ triggerTurn: boolean;
30
+ }): void;
@@ -0,0 +1,58 @@
1
+ export const STATUS_ICON = {
2
+ running: "⏳",
3
+ waiting: "⏳",
4
+ done: "✅",
5
+ error: "❌",
6
+ aborted: "⏹️",
7
+ };
8
+ export const STATUS_LABEL = {
9
+ running: "running",
10
+ waiting: "waiting for response",
11
+ done: "done",
12
+ error: "failed",
13
+ aborted: "aborted",
14
+ };
15
+ export function getCrewResultTitle(details) {
16
+ return `Subagent '${details.agentName}' (${details.agentId}) ${STATUS_LABEL[details.status]}`;
17
+ }
18
+ function getSteeringBody(payload) {
19
+ return (payload.status === "error" || payload.status === "aborted")
20
+ ? (payload.error ?? payload.result)
21
+ : (payload.result ?? payload.error);
22
+ }
23
+ export function sendSteeringMessage(payload, pi, opts) {
24
+ const body = getSteeringBody(payload);
25
+ const title = getCrewResultTitle({
26
+ agentId: payload.id,
27
+ agentName: payload.agentName,
28
+ status: payload.status,
29
+ });
30
+ const content = body
31
+ ? `**${STATUS_ICON[payload.status]} ${title}**\n\n${body}`
32
+ : `**${STATUS_ICON[payload.status]} ${title}**`;
33
+ const message = {
34
+ customType: "crew-result",
35
+ content,
36
+ display: true,
37
+ details: {
38
+ agentId: payload.id,
39
+ agentName: payload.agentName,
40
+ status: payload.status,
41
+ body,
42
+ },
43
+ };
44
+ pi.sendMessage(message, opts.isIdle
45
+ ? { triggerTurn: opts.triggerTurn }
46
+ : { deliverAs: "steer", triggerTurn: opts.triggerTurn });
47
+ }
48
+ export function sendRemainingNote(remainingCount, pi, opts) {
49
+ if (remainingCount <= 0)
50
+ return;
51
+ pi.sendMessage({
52
+ customType: "crew-remaining",
53
+ content: `⏳ ${remainingCount} subagent(s) still running`,
54
+ display: true,
55
+ }, opts.isIdle
56
+ ? { triggerTurn: opts.triggerTurn }
57
+ : { deliverAs: "steer", triggerTurn: opts.triggerTurn });
58
+ }