shariq-pi-extensions 0.2.27 → 0.2.29
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/docs/EXTENSIONS.md +2 -2
- package/extensions/background-terminals/index.ts +1 -7
- package/extensions/orchestration/README.md +1 -1
- package/extensions/orchestration/dashboard.ts +33 -26
- package/extensions/orchestration/index.ts +3 -14
- package/extensions/orchestration/settings-ui.ts +4 -2
- package/extensions/subagents/README.md +3 -3
- package/extensions/subagents/index.ts +11 -20
- package/extensions/subagents/src/catalog.ts +1 -1
- package/extensions/subagents/src/domain.ts +1 -1
- package/extensions/subagents/src/manager.ts +4 -3
- package/extensions/subagents/src/prompt.ts +2 -2
- package/extensions/subagents/src/storage.ts +17 -10
- package/extensions/subagents/src/ui/takeover.ts +7 -1
- package/extensions/task-list/index.ts +2 -8
- package/package.json +1 -1
package/docs/EXTENSIONS.md
CHANGED
|
@@ -44,13 +44,13 @@ State is stored in Pi session entries and reconstructed on resume, reload, and t
|
|
|
44
44
|
|
|
45
45
|
The subagent extension runs flat Pi child agents with profiles, capability policies, continuation, result delivery, optional worktrees, pre-warmed task dispatch, instant cascading cancellation, cross-session persistence, and a dashboard. Configuration lives in `<agent-dir>/subagents.json`; trusted projects may override it through their Pi config directory. The configured concurrency ceiling is 50.
|
|
46
46
|
|
|
47
|
-
The extension supplies tools including `spawn_agent`, `task`, `check_agent`, `list_agents`, `wait_agent`, `send_message`, `close_agent`, `reply_question`, and `apply_agent_changes`. These lifecycle operations render as compact expandable main-chat cards;
|
|
47
|
+
The extension supplies tools including `spawn_agent`, `task`, `check_agent`, `list_agents`, `wait_agent`, `send_message`, `close_agent`, `reply_question`, and `apply_agent_changes`. These lifecycle operations render as compact expandable main-chat cards; only active manager entries contribute to the bounded Active work dock, while workflow progress is intentionally omitted from the footer so persisted history cannot inflate or duplicate live status. Cancellation and reload interruption use a distinct `cancelled` state rather than `error`. All completed and cancelled production subagent snapshots and transcripts persist across Pi restarts (`<agent-dir>/subagents/runs/`), allowing `resume_from` to resume completed workers at any point. Interruption immediately cascades across all child fibers in `<10ms`. Child settlement stays in a private extension queue while the parent is active, then starts one custom-result turn at Pi's safe idle edge with the summary guaranteed in model context and never rendered as user-authored or follow-up input; status tools are for explicit inspection, not waiting.
|
|
48
48
|
|
|
49
49
|
### [Orchestration](../extensions/orchestration/README.md)
|
|
50
50
|
|
|
51
51
|
The Orchestration extension coordinates explicitly requested large tasks through a dedicated orchestrator plus configurable explorer, frontend, backend, general-worker, and reviewer models. `/orchestration` opens the dashboard and `/orchestration settings` configures global role models. It reuses the canonical Subagents runtime, maintains a 10-worker pool per project, isolates Git writers in task worktrees, resumes the original worker for substantial review fixes, and applies final reviewed changes without committing or pushing.
|
|
52
52
|
|
|
53
|
-
The model-facing `create_orchestration` tool starts planning only after an explicit orchestration request. `get_orchestration` reports status without advancing work. Both use compact expandable timeline cards, active runs share the Active work dock
|
|
53
|
+
The model-facing `create_orchestration` tool starts planning only after an explicit orchestration request. `get_orchestration` reports status without advancing work. Both use compact expandable timeline cards, and active runs share the full-screen Active work dock with explicit plan-ready, blocked, running, and paused states instead of duplicating workflow progress in the footer. The full-height framed dashboard runs editors and confirmations outside its overlay to avoid nested-input stalls. Interrupted runs recover paused under `<agent-dir>/orchestration/`.
|
|
54
54
|
|
|
55
55
|
### [Smart Compaction](../extensions/smart-compaction/README.md)
|
|
56
56
|
|
|
@@ -101,13 +101,7 @@ export default function backgroundTerminals(pi: ExtensionAPI) {
|
|
|
101
101
|
}
|
|
102
102
|
if (next === lastStatus) return;
|
|
103
103
|
lastStatus = next;
|
|
104
|
-
|
|
105
|
-
else {
|
|
106
|
-
ui.setStatus(
|
|
107
|
-
STATUS_KEY,
|
|
108
|
-
stateLabel(ui.theme, "active", next),
|
|
109
|
-
);
|
|
110
|
-
}
|
|
104
|
+
ui.setStatus(STATUS_KEY, undefined);
|
|
111
105
|
}
|
|
112
106
|
|
|
113
107
|
function flushResult(id: string): void {
|
|
@@ -31,7 +31,7 @@ Each project has up to 10 concurrent worker, explorer, or reviewer sessions. The
|
|
|
31
31
|
|
|
32
32
|
## Dashboard
|
|
33
33
|
|
|
34
|
-
Orchestration creation and inspection render as compact main-chat cards with expandable detail. Active runs appear in the shared bounded **Active work** dock; plan-ready and blocked runs are prioritized as attention states. `/orchestration` opens
|
|
34
|
+
Orchestration creation and inspection render as compact main-chat cards with expandable detail. Active runs appear in the shared bounded **Active work** dock; plan-ready and blocked runs are prioritized as attention states. `/orchestration` opens a full-height framed operations dashboard with a bounded body and stable selection. Dashboard actions close the overlay before opening editors, settings, or confirmation prompts, then return to the live dashboard so nested UI cannot stall. **New** opens the objective editor before asking for missing role models, so cancelling a draft does not force configuration; cancelling unchanged settings does not claim they were saved.
|
|
35
35
|
|
|
36
36
|
- `j` / `k`: select a run
|
|
37
37
|
- `Enter`: inspect tasks and reviews
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
visibleWidth,
|
|
10
10
|
type TUI,
|
|
11
11
|
} from "@earendil-works/pi-tui";
|
|
12
|
+
import { frameBottom, frameTop, framedRow, joinSides } from "../shared/tui-dashboard.ts";
|
|
12
13
|
import type { OrchestrationEngine } from "./engine.ts";
|
|
13
14
|
import type { OrchestrationRun } from "./types.ts";
|
|
14
15
|
|
|
@@ -102,9 +103,34 @@ class OrchestrationDashboard {
|
|
|
102
103
|
render(width: number) {
|
|
103
104
|
const runs = this.engine.list();
|
|
104
105
|
this.reconcileSelection(runs);
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
106
|
+
const selected = runs[this.selected];
|
|
107
|
+
const rows = this.tui.terminal.rows || 30;
|
|
108
|
+
const bodyHeight = Math.max(8, rows - 5);
|
|
109
|
+
const innerWidth = Math.max(1, width - 2);
|
|
110
|
+
const content = this.detail && selected
|
|
111
|
+
? this.renderDetail(selected, innerWidth)
|
|
112
|
+
: this.renderList(runs, innerWidth);
|
|
113
|
+
const body = content.length > bodyHeight
|
|
114
|
+
? [...content.slice(0, bodyHeight - 1), this.theme.fg("dim", "… more")]
|
|
115
|
+
: content;
|
|
116
|
+
const status = runs.length
|
|
117
|
+
? `${runs.filter((run) => ["planning", "running"].includes(run.status)).length} active · ${runs.length} total`
|
|
118
|
+
: "idle";
|
|
119
|
+
const controls = this.detail && selected
|
|
120
|
+
? [
|
|
121
|
+
selected.status === "awaiting-approval" ? "a approve · f feedback" : "",
|
|
122
|
+
["paused", "interrupted", "blocked"].includes(selected.status) ? "p resume" : selected.status === "running" ? "p pause" : "",
|
|
123
|
+
!["completed", "cancelled"].includes(selected.status) ? "x cancel" : "",
|
|
124
|
+
"esc back",
|
|
125
|
+
].filter(Boolean).join(" · ")
|
|
126
|
+
: "j/k select · enter open · n new · s settings · esc close";
|
|
127
|
+
const lines = [
|
|
128
|
+
joinSides(` ${this.theme.fg("accent", this.theme.bold("◆ ORCHESTRATION"))}`, `${this.theme.fg("muted", status)} `, width),
|
|
129
|
+
frameTop(this.theme, width, this.detail && selected ? `${selected.id} · RUN DETAIL` : `${runs.length} RUN${runs.length === 1 ? "" : "S"} · CONTROL CENTER`),
|
|
130
|
+
];
|
|
131
|
+
for (let row = 0; row < bodyHeight; row++) lines.push(framedRow(this.theme, body[row] ?? "", width));
|
|
132
|
+
lines.push(frameBottom(this.theme, width));
|
|
133
|
+
lines.push(truncateToWidth(this.theme.fg("dim", ` ${controls}`), width, ""));
|
|
108
134
|
return lines.map((line) => truncateToWidth(line, width, ""));
|
|
109
135
|
}
|
|
110
136
|
|
|
@@ -115,14 +141,7 @@ class OrchestrationDashboard {
|
|
|
115
141
|
}
|
|
116
142
|
|
|
117
143
|
private renderList(runs: OrchestrationRun[], width: number) {
|
|
118
|
-
const lines = [
|
|
119
|
-
this.split(
|
|
120
|
-
this.theme.fg("accent", this.theme.bold("Orchestration")),
|
|
121
|
-
this.theme.fg("dim", `${runs.length} run${runs.length === 1 ? "" : "s"}`),
|
|
122
|
-
width,
|
|
123
|
-
),
|
|
124
|
-
this.theme.fg("borderMuted", "─".repeat(Math.max(0, width))),
|
|
125
|
-
];
|
|
144
|
+
const lines: string[] = [];
|
|
126
145
|
if (!runs.length) lines.push(this.theme.fg("muted", "No runs yet. Press n to create one."));
|
|
127
146
|
const rows = this.tui.terminal.rows || 30;
|
|
128
147
|
const visibleCount = Math.max(1, Math.floor((rows - 5) / 2));
|
|
@@ -142,13 +161,10 @@ class OrchestrationDashboard {
|
|
|
142
161
|
);
|
|
143
162
|
lines.push(` ${this.theme.fg("dim", `${run.id} · ${run.cwd}`)}`);
|
|
144
163
|
}
|
|
145
|
-
if (start > 0) lines.
|
|
164
|
+
if (start > 0) lines.unshift(this.theme.fg("dim", ` ↑ ${start} more`));
|
|
146
165
|
const below = runs.length - start - visible.length;
|
|
147
166
|
if (below > 0) lines.push(this.theme.fg("dim", ` ↓ ${below} more`));
|
|
148
|
-
lines
|
|
149
|
-
const maxRows = Math.max(8, rows);
|
|
150
|
-
if (lines.length <= maxRows) return lines;
|
|
151
|
-
return [...lines.slice(0, maxRows - 2), this.theme.fg("dim", "… more runs"), lines.at(-1)!];
|
|
167
|
+
return lines;
|
|
152
168
|
}
|
|
153
169
|
|
|
154
170
|
private renderDetail(run: OrchestrationRun, width: number) {
|
|
@@ -179,16 +195,7 @@ class OrchestrationDashboard {
|
|
|
179
195
|
}
|
|
180
196
|
if (run.finalReviewSummary) lines.push("", this.theme.fg("accent", `Final review: ${run.finalReviewSummary}`));
|
|
181
197
|
if (run.error) lines.push("", this.theme.fg("error", run.error));
|
|
182
|
-
|
|
183
|
-
run.status === "awaiting-approval" ? "a approve · f feedback" : "",
|
|
184
|
-
["paused", "interrupted", "blocked"].includes(run.status) ? "p resume" : run.status === "running" ? "p pause" : "",
|
|
185
|
-
!["completed", "cancelled"].includes(run.status) ? "x cancel" : "",
|
|
186
|
-
"esc back",
|
|
187
|
-
].filter(Boolean).join(" · ");
|
|
188
|
-
lines.push("", this.theme.fg("dim", controls));
|
|
189
|
-
const maxRows = Math.max(8, (this.tui.terminal.rows || 30) - 2);
|
|
190
|
-
if (lines.length <= maxRows) return lines;
|
|
191
|
-
return [...lines.slice(0, maxRows - 2), this.theme.fg("dim", "… more tasks in run state"), lines.at(-1)!];
|
|
198
|
+
return lines;
|
|
192
199
|
}
|
|
193
200
|
}
|
|
194
201
|
|
|
@@ -7,7 +7,7 @@ import { Text } from "@earendil-works/pi-tui";
|
|
|
7
7
|
import { Type } from "typebox";
|
|
8
8
|
import { clearActivitySource, setActivitySource } from "../shared/activity-dock.ts";
|
|
9
9
|
import { toolCallCard, toolResultCard } from "../shared/tool-card.ts";
|
|
10
|
-
import { oneLine
|
|
10
|
+
import { oneLine } from "../shared/tui-dashboard.ts";
|
|
11
11
|
import {
|
|
12
12
|
requestSubagentCoordinator,
|
|
13
13
|
type SubagentCoordinator,
|
|
@@ -55,18 +55,7 @@ export default function orchestration(pi: ExtensionAPI) {
|
|
|
55
55
|
ui.setStatus("orchestration", undefined);
|
|
56
56
|
return;
|
|
57
57
|
}
|
|
58
|
-
|
|
59
|
-
const blocked = active.filter((run) => run.status === "blocked").length;
|
|
60
|
-
const running = active.filter((run) => run.status === "running" || run.status === "planning").length;
|
|
61
|
-
const paused = active.filter((run) => ["paused", "interrupted"].includes(run.status)).length;
|
|
62
|
-
const text = awaiting
|
|
63
|
-
? `${awaiting} plan${awaiting === 1 ? "" : "s"} ready · /orchestration`
|
|
64
|
-
: blocked
|
|
65
|
-
? `${blocked} blocked · /orchestration`
|
|
66
|
-
: running
|
|
67
|
-
? `${running} running · /orchestration`
|
|
68
|
-
: `${paused} paused · /orchestration`;
|
|
69
|
-
ui.setStatus("orchestration", stateLabel(ui.theme, awaiting || blocked ? "warning" : running ? "active" : "muted", `Orchestration ${text}`));
|
|
58
|
+
ui.setStatus("orchestration", undefined);
|
|
70
59
|
};
|
|
71
60
|
|
|
72
61
|
const requireEngine = () => {
|
|
@@ -91,12 +80,12 @@ export default function orchestration(pi: ExtensionAPI) {
|
|
|
91
80
|
};
|
|
92
81
|
|
|
93
82
|
const createFromUi = async (ctx: ExtensionContext) => {
|
|
94
|
-
await ensureModels(ctx);
|
|
95
83
|
const objective = await ctx.ui.editor(
|
|
96
84
|
"New orchestration objective",
|
|
97
85
|
"Describe the complete outcome, boundaries, and requirements…",
|
|
98
86
|
);
|
|
99
87
|
if (!objective?.trim()) return;
|
|
88
|
+
await ensureModels(ctx);
|
|
100
89
|
try {
|
|
101
90
|
const run = await requireEngine().create(objective, ctx.cwd);
|
|
102
91
|
ctx.ui.notify(`Started dedicated planning agent for ${run.id}.`, "info");
|
|
@@ -9,6 +9,7 @@ const THINKING = ["off", "minimal", "low", "medium", "high", "xhigh", "max"] as
|
|
|
9
9
|
|
|
10
10
|
export async function openOrchestrationSettings(ctx: ExtensionContext) {
|
|
11
11
|
const settings = loadOrchestrationSettings();
|
|
12
|
+
let changed = false;
|
|
12
13
|
for (;;) {
|
|
13
14
|
const choices = [
|
|
14
15
|
...ORCHESTRATION_ROLES.map((role) =>
|
|
@@ -35,7 +36,8 @@ export async function openOrchestrationSettings(ctx: ExtensionContext) {
|
|
|
35
36
|
thinking: (thinking as (typeof THINKING)[number] | undefined) ?? settings.roles[role].thinking,
|
|
36
37
|
};
|
|
37
38
|
saveOrchestrationSettings(settings);
|
|
39
|
+
changed = true;
|
|
38
40
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
+
if (changed) ctx.ui.notify("Orchestration settings saved.", "info");
|
|
42
|
+
return changed;
|
|
41
43
|
}
|
|
@@ -10,7 +10,7 @@ The system is deliberately flat. Only the main Pi thread can spawn subagents. Ch
|
|
|
10
10
|
|
|
11
11
|
- **Pre-Warmed Pool Dispatch:** Pre-warms unique agent IDs and allocation buffers ahead of time, eliminating string-formatting and crypto overhead on the critical path for sub-millisecond task dispatch.
|
|
12
12
|
- **Instant Cascading Cancellation:** Structured Effect-TS fiber supervision cascades immediate abort signals to all running subagents and child processes in `<10ms`, guaranteeing zero orphan processes upon interruption or parent turn cancellation.
|
|
13
|
-
- **Cross-Session Snapshot Persistence:** Full subagent snapshots and transcripts are automatically persisted under `~/.pi/agent/subagents/runs/<id>/snapshot.json`, enabling discovery and resumption (`resume_from`) across Pi restarts.
|
|
13
|
+
- **Cross-Session Snapshot Persistence:** Full production subagent snapshots and transcripts are automatically persisted under `~/.pi/agent/subagents/runs/<id>/snapshot.json`, enabling discovery and resumption (`resume_from`) across Pi restarts. Test-only manager backends do not write into user state.
|
|
14
14
|
|
|
15
15
|
## Parent tools
|
|
16
16
|
|
|
@@ -70,7 +70,7 @@ Project configuration is ignored when the project is not trusted. Concurrency is
|
|
|
70
70
|
|
|
71
71
|
New children start with independent context by default. `fork_turns` may be `all` or a positive number of recent user turns. Forking keeps user messages and final assistant text while removing thinking, tool calls, and tool results so the child never inherits an unresolved tool protocol.
|
|
72
72
|
|
|
73
|
-
Every child uses a persistent Pi session file. `resume_from` continues a completed child with its full transcript, tool state, and logical agent id, including after a parent `/reload` or resume. Non-secret metadata is stored both in the parent session and in `~/.pi/agent/subagents/catalog.json`, so children created by an ephemeral or different parent process remain discoverable. Existing legacy ids remain valid catalog keys.
|
|
73
|
+
Every child uses a persistent Pi session file. `resume_from` continues a completed or cancelled child with its full transcript, tool state, and logical agent id, including after a parent `/reload` or resume. Cancellation and reload interruption are recorded as `cancelled`, not as failures; historical snapshots remain discoverable but only active manager entries contribute to the live footer and Active work dock. Non-secret metadata is stored both in the parent session and in `~/.pi/agent/subagents/catalog.json`, so children created by an ephemeral or different parent process remain discoverable. Existing legacy ids remain valid catalog keys.
|
|
74
74
|
|
|
75
75
|
## Worktree isolation
|
|
76
76
|
|
|
@@ -80,7 +80,7 @@ Children share the requested workspace by default. Use `isolation: "worktree"` o
|
|
|
80
80
|
|
|
81
81
|
## UI
|
|
82
82
|
|
|
83
|
-
Subagent lifecycle tools render as compact main-chat cards with expandable detail. Running children appear in the shared bounded **Active work** dock with elapsed time and their current tool, then disappear when settled because the result card becomes the durable timeline record. `/subagents` or `/subagents agents` opens the live operations dashboard and takeover UI. `/btw <question>` starts a read-only, context-aware side investigation owned by the user; it opens directly in takeover view and records its answer without waking the parent model. Wide terminals use a split-pane agent list and selected-agent inspector with running/completed/failed counts, model/profile/access metadata, a context meter, current tool activity, queue state, elapsed time, turns, cwd, and latest output. Aborting a running child from the dashboard requires pressing `x` twice; Escape or navigation cancels an armed abort. The takeover view adds a live transcript, context meter, active-tool state, scrolling, child interruption, and follow-up input. `/subagents peers` shows the persistent peer-message audit trail. `/subagents profiles` browses profiles and personas, and `/subagents config` edits validated configuration.
|
|
83
|
+
Subagent lifecycle tools render as compact main-chat cards with expandable detail. Running children appear in the shared bounded **Active work** dock with elapsed time and their current tool, then disappear when settled because the result card becomes the durable timeline record. `/subagents` or `/subagents agents` opens the live operations dashboard and takeover UI. `/btw <question>` starts a read-only, context-aware side investigation owned by the user; it opens directly in takeover view and records its answer without waking the parent model. Wide terminals use a split-pane agent list and selected-agent inspector with running/completed/cancelled/failed counts, model/profile/access metadata, a context meter, current tool activity, queue state, elapsed time, turns, cwd, and latest output. Aborting a running child from the dashboard requires pressing `x` twice; Escape or navigation cancels an armed abort. The takeover view adds a live transcript, context meter, active-tool state, scrolling, child interruption, and follow-up input. `/subagents peers` shows the persistent peer-message audit trail. `/subagents profiles` browses profiles and personas, and `/subagents config` edits validated configuration.
|
|
84
84
|
|
|
85
85
|
## Architecture
|
|
86
86
|
|
|
@@ -39,10 +39,7 @@ import {
|
|
|
39
39
|
type SubagentOrigin,
|
|
40
40
|
type SubagentSnapshot,
|
|
41
41
|
} from "./src/domain.ts";
|
|
42
|
-
import {
|
|
43
|
-
formatActivityStatus,
|
|
44
|
-
formatContextUtilization,
|
|
45
|
-
} from "./src/format.ts";
|
|
42
|
+
import { formatContextUtilization } from "./src/format.ts";
|
|
46
43
|
import { SubagentManager, type SubagentManagerShape } from "./src/manager.ts";
|
|
47
44
|
import {
|
|
48
45
|
buildSubagentResultMessage,
|
|
@@ -512,9 +509,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
512
509
|
|
|
513
510
|
const updateStatus = (manager: SubagentManagerShape) => {
|
|
514
511
|
if (!ui) return;
|
|
515
|
-
const subs = manager.view.
|
|
512
|
+
const subs = manager.view.active();
|
|
516
513
|
const runningSnapshots = subs.filter((snap) => snap.status === "running");
|
|
517
|
-
const failed = subs.filter((snap) => snap.status === "error").length;
|
|
518
514
|
if (activityContext) {
|
|
519
515
|
setActivitySource(activityContext, "subagents", runningSnapshots.map((snap) => {
|
|
520
516
|
const activeTool = snap.liveTools.find((tool) => !tool.done)?.name;
|
|
@@ -528,14 +524,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
528
524
|
};
|
|
529
525
|
}));
|
|
530
526
|
}
|
|
531
|
-
|
|
532
|
-
ui.setStatus("subagents", undefined);
|
|
533
|
-
return;
|
|
534
|
-
}
|
|
535
|
-
ui.setStatus(
|
|
536
|
-
"subagents",
|
|
537
|
-
formatActivityStatus(ui.theme, { running: runningSnapshots.length, done: 0, failed }),
|
|
538
|
-
);
|
|
527
|
+
ui.setStatus("subagents", undefined);
|
|
539
528
|
};
|
|
540
529
|
|
|
541
530
|
const deliverResult = (snap: SubagentSnapshot) => {
|
|
@@ -843,7 +832,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
843
832
|
const sections: string[] = [];
|
|
844
833
|
let remainingBytes = WAIT_OUTPUT_MAX_BYTES;
|
|
845
834
|
for (const snap of settled) {
|
|
846
|
-
const verb = snap.status === "error" ? "failed" : "finished";
|
|
835
|
+
const verb = snap.status === "error" ? "failed" : snap.status === "cancelled" ? "was cancelled" : "finished";
|
|
847
836
|
let section = `## ${snap.id} "${snap.title}" ${verb}`;
|
|
848
837
|
if (snap.errorText) section += `\nError: ${snap.errorText}`;
|
|
849
838
|
const headerBytes = Buffer.byteLength(section, "utf8") + 2;
|
|
@@ -996,7 +985,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
996
985
|
},
|
|
997
986
|
renderResult(result, { expanded }, theme) {
|
|
998
987
|
const details = result.details as { id?: string; status?: string; turns?: number } | undefined;
|
|
999
|
-
const state = details?.status === "error" ? "error" : details?.status === "running" ? "active" : "success";
|
|
988
|
+
const state = details?.status === "error" ? "error" : details?.status === "running" ? "active" : details?.status === "cancelled" ? "muted" : "success";
|
|
1000
989
|
return new Text(toolResultCard(result, expanded, theme, state, details?.id ?? "subagent", `${details?.status ?? "unknown"} · ${details?.turns ?? 0} turns`), 0, 0);
|
|
1001
990
|
},
|
|
1002
991
|
});
|
|
@@ -1294,13 +1283,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
1294
1283
|
status?: string;
|
|
1295
1284
|
};
|
|
1296
1285
|
const failed = details.status === "error";
|
|
1297
|
-
const
|
|
1286
|
+
const cancelled = details.status === "cancelled";
|
|
1287
|
+
const icon = failed ? theme.fg("error", "x") : cancelled ? theme.fg("muted", "■") : theme.fg("success", "■");
|
|
1298
1288
|
const header =
|
|
1299
1289
|
`${icon} ` +
|
|
1300
1290
|
theme.fg("accent", theme.bold(`subagent ${details.id ?? "?"}`)) +
|
|
1301
1291
|
theme.fg(
|
|
1302
1292
|
"muted",
|
|
1303
|
-
` · ${details.title ?? ""} · ${failed ? "failed" : "finished"}`,
|
|
1293
|
+
` · ${details.title ?? ""} · ${failed ? "failed" : cancelled ? "cancelled" : "finished"}`,
|
|
1304
1294
|
);
|
|
1305
1295
|
|
|
1306
1296
|
const content =
|
|
@@ -1340,10 +1330,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
1340
1330
|
const data = entry.data;
|
|
1341
1331
|
if (!data) return new Text(theme.fg("warning", "By-the-way result unavailable"), 0, 0);
|
|
1342
1332
|
const failed = data.status === "error";
|
|
1333
|
+
const cancelled = data.status === "cancelled";
|
|
1343
1334
|
const header =
|
|
1344
|
-
`${theme.fg(failed ? "error" : "success", "■")} ` +
|
|
1335
|
+
`${theme.fg(failed ? "error" : cancelled ? "muted" : "success", "■")} ` +
|
|
1345
1336
|
theme.fg("accent", theme.bold(`by the way · ${data.title}`)) +
|
|
1346
|
-
theme.fg("muted", ` · ${failed ? "failed" : "answered"} · ${data.id}`);
|
|
1337
|
+
theme.fg("muted", ` · ${failed ? "failed" : cancelled ? "cancelled" : "answered"} · ${data.id}`);
|
|
1347
1338
|
const body = [data.errorText ? `Error: ${data.errorText}` : "", data.answer]
|
|
1348
1339
|
.filter(Boolean)
|
|
1349
1340
|
.join("\n\n");
|
|
@@ -48,7 +48,7 @@ function validRecord(value: unknown): value is ArchivedSubagent {
|
|
|
48
48
|
typeof record.cwd === "string" &&
|
|
49
49
|
typeof record.sessionFile === "string" &&
|
|
50
50
|
typeof record.updatedAt === "number" &&
|
|
51
|
-
(record.status === "running" || record.status === "done" || record.status === "error")
|
|
51
|
+
(record.status === "running" || record.status === "done" || record.status === "error" || record.status === "cancelled")
|
|
52
52
|
);
|
|
53
53
|
}
|
|
54
54
|
|
|
@@ -27,7 +27,7 @@ export const REASONING_EFFORTS = [
|
|
|
27
27
|
] as const;
|
|
28
28
|
export type ReasoningEffort = (typeof REASONING_EFFORTS)[number];
|
|
29
29
|
|
|
30
|
-
export type SubagentStatus = "running" | "done" | "error";
|
|
30
|
+
export type SubagentStatus = "running" | "done" | "error" | "cancelled";
|
|
31
31
|
|
|
32
32
|
/** Parent-session context resolved by the tool layer and passed opaquely. */
|
|
33
33
|
export interface PeerAgent {
|
|
@@ -24,7 +24,7 @@ import {
|
|
|
24
24
|
} from "effect";
|
|
25
25
|
import type { SubagentBackend, SubagentSession } from "./backend.ts";
|
|
26
26
|
import { BackendRegistry } from "./backend.ts";
|
|
27
|
-
import { loadPersistedSnapshots
|
|
27
|
+
import { loadPersistedSnapshots } from "./storage.ts";
|
|
28
28
|
import type {
|
|
29
29
|
BackendName,
|
|
30
30
|
LiveToolState,
|
|
@@ -95,6 +95,7 @@ interface Entry {
|
|
|
95
95
|
/** Synchronous bridge for the TUI. Snapshots are live objects; do not mutate. */
|
|
96
96
|
export interface SubagentReadModel {
|
|
97
97
|
list(): ReadonlyArray<SubagentSnapshot>;
|
|
98
|
+
active(): ReadonlyArray<SubagentSnapshot>;
|
|
98
99
|
get(id: string): SubagentSnapshot | undefined;
|
|
99
100
|
size(): number;
|
|
100
101
|
/** Any-change notification (footer status, dashboard). */
|
|
@@ -329,7 +330,7 @@ const makeManager = Effect.gen(function* () {
|
|
|
329
330
|
s.finalText = outcome.partialText ?? "";
|
|
330
331
|
break;
|
|
331
332
|
case "Interrupted":
|
|
332
|
-
s.status = "
|
|
333
|
+
s.status = "cancelled";
|
|
333
334
|
s.errorText = "Run was aborted";
|
|
334
335
|
s.finalText = outcome.partialText ?? "";
|
|
335
336
|
break;
|
|
@@ -339,7 +340,6 @@ const makeManager = Effect.gen(function* () {
|
|
|
339
340
|
s.liveTools = [];
|
|
340
341
|
s.queued = [];
|
|
341
342
|
const consumed = (waitInterest.get(s.id) ?? 0) > 0;
|
|
342
|
-
saveSnapshot(s as SubagentSnapshot);
|
|
343
343
|
notify(s.id);
|
|
344
344
|
try {
|
|
345
345
|
// During teardown, don't queue results into a shutting-down session.
|
|
@@ -776,6 +776,7 @@ const makeManager = Effect.gen(function* () {
|
|
|
776
776
|
);
|
|
777
777
|
return [...active, ...historical];
|
|
778
778
|
},
|
|
779
|
+
active: () => [...entries.values()].map((entry) => entry.snapshot),
|
|
779
780
|
get: (id) => entries.get(id)?.snapshot ?? persistedSnapshots.get(id),
|
|
780
781
|
size: () => entries.size,
|
|
781
782
|
subscribe: (listener) => {
|
|
@@ -83,11 +83,11 @@ export const SUBAGENT_LIST_TOOL_DESCRIPTION =
|
|
|
83
83
|
export function buildSubagentResultMessage(options: {
|
|
84
84
|
id: string;
|
|
85
85
|
title: string;
|
|
86
|
-
status: "running" | "done" | "error";
|
|
86
|
+
status: "running" | "done" | "error" | "cancelled";
|
|
87
87
|
errorText?: string;
|
|
88
88
|
output: string;
|
|
89
89
|
}) {
|
|
90
|
-
const verb = options.status === "error" ? "failed" : "finished";
|
|
90
|
+
const verb = options.status === "error" ? "failed" : options.status === "cancelled" ? "was cancelled" : "finished";
|
|
91
91
|
let text = `Pi subagent ${options.id} "${options.title}" ${verb}.`;
|
|
92
92
|
if (options.errorText) text += `\nError: ${options.errorText}`;
|
|
93
93
|
text += `\n\n${options.output}`;
|
|
@@ -26,6 +26,20 @@ export function saveSnapshot(snapshot: SubagentSnapshot) {
|
|
|
26
26
|
}
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
export function normalizePersistedSnapshot(snap: SubagentSnapshot): SubagentSnapshot {
|
|
30
|
+
if (snap.status === "running") {
|
|
31
|
+
return {
|
|
32
|
+
...snap,
|
|
33
|
+
status: "cancelled",
|
|
34
|
+
errorText: "Pi exited or reloaded while this subagent was active.",
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
if (snap.status === "error" && /abort|cancel|interrupt|exited or reloaded/i.test(snap.errorText ?? "")) {
|
|
38
|
+
return { ...snap, status: "cancelled" };
|
|
39
|
+
}
|
|
40
|
+
return snap;
|
|
41
|
+
}
|
|
42
|
+
|
|
29
43
|
export function loadPersistedSnapshots(): SubagentSnapshot[] {
|
|
30
44
|
let names: string[] = [];
|
|
31
45
|
try {
|
|
@@ -38,16 +52,9 @@ export function loadPersistedSnapshots(): SubagentSnapshot[] {
|
|
|
38
52
|
try {
|
|
39
53
|
const file = path.join(rootDir(), name, "snapshot.json");
|
|
40
54
|
if (!fs.existsSync(file)) continue;
|
|
41
|
-
|
|
42
|
-
if (!
|
|
43
|
-
|
|
44
|
-
snap = {
|
|
45
|
-
...snap,
|
|
46
|
-
status: "error",
|
|
47
|
-
errorText: "Pi exited or reloaded while this subagent was active.",
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
snapshots.push(snap);
|
|
55
|
+
const parsed = JSON.parse(fs.readFileSync(file, "utf8")) as SubagentSnapshot;
|
|
56
|
+
if (!parsed.id || !parsed.title) continue;
|
|
57
|
+
snapshots.push(normalizePersistedSnapshot(parsed));
|
|
51
58
|
} catch {
|
|
52
59
|
// Best effort recovery
|
|
53
60
|
}
|
|
@@ -41,6 +41,8 @@ function statusGlyph(snap: SubagentSnapshot, theme: Theme): string {
|
|
|
41
41
|
return theme.fg("success", "■");
|
|
42
42
|
case "error":
|
|
43
43
|
return theme.fg("error", "■");
|
|
44
|
+
case "cancelled":
|
|
45
|
+
return theme.fg("muted", "■");
|
|
44
46
|
}
|
|
45
47
|
}
|
|
46
48
|
|
|
@@ -52,6 +54,8 @@ function statusWord(snap: SubagentSnapshot, theme: Theme): string {
|
|
|
52
54
|
return theme.fg("success", "done");
|
|
53
55
|
case "error":
|
|
54
56
|
return theme.fg("error", "failed");
|
|
57
|
+
case "cancelled":
|
|
58
|
+
return theme.fg("muted", "cancelled");
|
|
55
59
|
}
|
|
56
60
|
}
|
|
57
61
|
|
|
@@ -314,6 +318,7 @@ export class SubagentDashboard implements Component {
|
|
|
314
318
|
const running = subs.filter((snap) => snap.status === "running").length;
|
|
315
319
|
const done = subs.filter((snap) => snap.status === "done").length;
|
|
316
320
|
const failed = subs.filter((snap) => snap.status === "error").length;
|
|
321
|
+
const cancelled = subs.filter((snap) => snap.status === "cancelled").length;
|
|
317
322
|
const selected = subs[this.selection.index];
|
|
318
323
|
|
|
319
324
|
const headerLeft = theme.fg("accent", theme.bold("Subagent operations"));
|
|
@@ -321,6 +326,7 @@ export class SubagentDashboard implements Component {
|
|
|
321
326
|
running > 0 ? stateLabel(theme, "warning", `${running} running`) : "",
|
|
322
327
|
done > 0 ? stateLabel(theme, "success", `${done} done`) : "",
|
|
323
328
|
failed > 0 ? stateLabel(theme, "error", `${failed} failed`) : "",
|
|
329
|
+
cancelled > 0 ? stateLabel(theme, "muted", `${cancelled} cancelled`) : "",
|
|
324
330
|
].filter(Boolean).join(theme.fg("dim", " · "));
|
|
325
331
|
const lines = [joinSides(` ${headerLeft}`, `${counts || theme.fg("muted", "idle")} `, width)];
|
|
326
332
|
|
|
@@ -416,7 +422,7 @@ export class SubagentDashboard implements Component {
|
|
|
416
422
|
height: number,
|
|
417
423
|
): string[] {
|
|
418
424
|
const theme = this.theme;
|
|
419
|
-
const state = snap.status === "running" ? "warning" : snap.status === "done" ? "success" : "error";
|
|
425
|
+
const state = snap.status === "running" ? "warning" : snap.status === "done" ? "success" : snap.status === "cancelled" ? "muted" : "error";
|
|
420
426
|
const percent = contextPercent(snap.usage);
|
|
421
427
|
const activeTools = snap.liveTools.filter((tool) => !tool.done);
|
|
422
428
|
const lines: string[] = [
|
|
@@ -130,15 +130,9 @@ export default function taskListExtension(pi: ExtensionAPI) {
|
|
|
130
130
|
state: (task.status === "in_progress" ? "active" : task.status === "completed" ? "success" : task.status === "blocked" ? "error" : "muted") as ActivityState,
|
|
131
131
|
priority: task.status === "blocked" ? 100 : task.status === "in_progress" ? 60 : 10,
|
|
132
132
|
})));
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
? `Tasks ${counts.completed}/${counts.total} · ${counts.blocked} blocked`
|
|
136
|
-
: `Tasks ${counts.completed}/${counts.total} · ${counts.inProgress} active`;
|
|
137
|
-
ctx.ui.setStatus(ACTIVITY_SOURCE, ctx.ui.theme.fg(counts.blocked > 0 ? "warning" : "accent", status));
|
|
138
|
-
return;
|
|
139
|
-
}
|
|
133
|
+
ctx.ui.setStatus(ACTIVITY_SOURCE, undefined);
|
|
134
|
+
if (active) return;
|
|
140
135
|
|
|
141
|
-
ctx.ui.setStatus(ACTIVITY_SOURCE, ctx.ui.theme.fg("success", `Tasks complete ${counts.completed}/${counts.total}`));
|
|
142
136
|
finishedTimer = setTimeout(() => {
|
|
143
137
|
finishedTimer = undefined;
|
|
144
138
|
if (lastCtx !== ctx || hasActiveTasks(state)) return;
|