@maxiaochao/pi-toolkit 0.10.2 → 0.10.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -0
- package/README.md +1 -1
- package/package.json +2 -2
- package/packages/herdr-subagents/CHANGELOG.md +15 -0
- package/packages/herdr-subagents/README.md +9 -6
- package/packages/herdr-subagents/TESTING.md +6 -4
- package/packages/herdr-subagents/docs/demo.webm +0 -0
- package/packages/herdr-subagents/docs/screenshot.png +0 -0
- package/packages/herdr-subagents/extensions/herdr-subagents.ts +40 -6
- package/packages/herdr-subagents/package.json +1 -1
- package/packages/herdr-subagents/src/batch-runner.ts +7 -7
- package/packages/herdr-subagents/src/blocking.ts +4 -4
- package/packages/herdr-subagents/src/dispatcher.ts +8 -0
- package/packages/herdr-subagents/src/fleet-controller.ts +167 -16
- package/packages/herdr-subagents/src/fleet.ts +119 -8
- package/packages/herdr-subagents/src/history.ts +11 -5
- package/packages/herdr-subagents/src/parameters.ts +2 -2
- package/packages/herdr-subagents/src/reconcile.ts +2 -1
- package/packages/herdr-subagents/src/records.ts +53 -1
- package/packages/herdr-subagents/src/tab-cleanup.ts +18 -2
- package/packages/herdr-subagents/src/tool.ts +30 -3
- package/packages/herdr-subagents/src/validation.ts +2 -1
- package/packages/herdr-subagents/tests/extension-contract.ts +16 -2
- package/packages/herdr-subagents/tests/run-tests.mjs +169 -8
- package/packages/todo/CHANGELOG.md +4 -0
- package/packages/todo/README.md +19 -9
- package/packages/todo/TESTING.md +9 -7
- package/packages/todo/docs/demo.gif +0 -0
- package/packages/todo/docs/screenshot.png +0 -0
- package/packages/todo/extensions/todo.ts +120 -90
- package/packages/todo/package.json +2 -2
- package/packages/todo/src/todo-state.ts +48 -49
- package/packages/todo/tests/todo-extension.mjs +134 -0
- package/packages/todo/tests/todo-state.mjs +80 -70
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
## 2026-10-08 - v0.10.4
|
|
2
|
+
|
|
3
|
+
- bundle herdr-subagents v0.4.1: targeted cancellation, live token polling, and idempotent tab cleanup
|
|
4
|
+
|
|
5
|
+
## 2026-10-08 - v0.10.3
|
|
6
|
+
|
|
7
|
+
- bundle herdr-subagents v0.4.0: add the live compact Fleet footer with authoritative metrics and refreshed media
|
|
8
|
+
- Add the responsive below-editor Herdr Fleet monitor with real personas, task states, cumulative active durations, and authoritative child Pi token totals, including event-driven live updates and safe partial-JSONL handling.
|
|
9
|
+
- Color running Fleet rows yellow and completed rows green, compact the shared metrics column instead of pushing it to the terminal edge, and update the published media with focus restoration after capture.
|
|
10
|
+
|
|
1
11
|
## 2026-10-08 - v0.10.2
|
|
2
12
|
|
|
3
13
|
- bundle herdr-subagents v0.3.0: split execution and control tools and unify task runs
|
|
@@ -183,6 +193,7 @@
|
|
|
183
193
|
|
|
184
194
|
## Unreleased
|
|
185
195
|
|
|
196
|
+
- redesign bundled Todo as a branch-local execution plan with lifecycle-only model actions, expected-current guards, serialized calls, preserved branch history, and a live current-plan widget
|
|
186
197
|
- let `/cache_export` switch between all history and individual compaction-defined Context segments, defaulting to the latest segment; cache-hit cumulative lines reset at compaction or model boundaries
|
|
187
198
|
- add the independent `@maxiaochao/pi-codex-edit` package with model-aware Codex-style `apply_patch`
|
|
188
199
|
- include the benchmark summary and interactive HTML architecture explainer
|
package/README.md
CHANGED
|
@@ -22,7 +22,7 @@ The root package embeds the child-package source snapshots listed below. Install
|
|
|
22
22
|
|
|
23
23
|
| Package | Capability | Install | Update |
|
|
24
24
|
|---|---|---|---|
|
|
25
|
-
| [pi-todo](packages/todo/README.md) |
|
|
25
|
+
| [pi-todo](packages/todo/README.md) | Branch-local execution plans, guarded advancement, `/todos` history | `pi install npm:@maxiaochao/pi-todo` | `pi update npm:@maxiaochao/pi-todo` |
|
|
26
26
|
| [pi-cache-export](packages/cache-export/README.md) | Tau-aligned interactive cache dashboard for Pi sessions | `pi install npm:@maxiaochao/pi-cache-export` | `pi update npm:@maxiaochao/pi-cache-export` |
|
|
27
27
|
| [pi-codex-edit](packages/codex-edit/README.md) | Codex-harness `apply_patch` routing for selected GPT models | `pi install npm:@maxiaochao/pi-codex-edit` | `pi update npm:@maxiaochao/pi-codex-edit` |
|
|
28
28
|
| [pi-herdr-subagents](packages/herdr-subagents/README.md) | Interactive Herdr Pi Agents with bounded concurrency, direct navigation, and recovery | `pi install npm:@maxiaochao/pi-herdr-subagents` | `pi update npm:@maxiaochao/pi-herdr-subagents` |
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@maxiaochao/pi-toolkit",
|
|
3
|
-
"version": "0.10.
|
|
4
|
-
"description": "Personal pi coding agent toolkit:
|
|
3
|
+
"version": "0.10.4",
|
|
4
|
+
"description": "Personal pi coding agent toolkit: execution plans, /btw, /cache_export and apply_patch extensions, browser automation, and bundled theme/skills.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"workspaces": [
|
|
7
7
|
"packages/*"
|
|
@@ -1,3 +1,18 @@
|
|
|
1
|
+
## 2026-10-08 - v0.4.1
|
|
2
|
+
|
|
3
|
+
- targeted cancellation, live token polling, and idempotent tab cleanup
|
|
4
|
+
- Add `cancel` control for one active or queued run while leaving unrelated runs untouched.
|
|
5
|
+
- Refresh live Fleet token totals every second as well as on file events.
|
|
6
|
+
- Treat already-missing tabs as successful cleanup and emit one warning per real cleanup failure.
|
|
7
|
+
|
|
8
|
+
## 2026-10-08 - v0.4.0
|
|
9
|
+
|
|
10
|
+
- add the live compact Fleet footer with authoritative metrics and refreshed media
|
|
11
|
+
- Replace the below-editor task list with a compact responsive Fleet monitor showing authoritative persona, task status, cumulative active time, and true child Pi usage from assistant, tool-result, standalone usage, compaction, and branch-summary entries.
|
|
12
|
+
- Update Fleet metrics from child-session file events, repaint elapsed time only while work runs, safely ignore malformed or partial JSONL tails, and dispose every watcher and timer with the widget.
|
|
13
|
+
- Preserve exact-pane keyboard focus while extending blocked-response and historical-resume activity projections with durable timing and session metadata.
|
|
14
|
+
- Use warning/yellow indicators for running Fleet rows and success/green indicators for completed rows, compact the shared metrics column instead of pushing it to the terminal edge, and publish media that demonstrates the live parent footer while restoring focus after capture.
|
|
15
|
+
|
|
1
16
|
## 2026-10-08 - v0.3.0
|
|
2
17
|
|
|
3
18
|
- split execution and control tools and unify task runs
|
|
@@ -2,16 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
A Pi extension that runs focused foreground or background work as normal interactive Pi Agents in visible Herdr panes. Full answers and sessions stay in project-local records while the parent receives only concise summaries and document paths.
|
|
4
4
|
|
|
5
|
-

|
|
6
6
|
|
|
7
7
|
## Demo
|
|
8
8
|
|
|
9
|
-
The recording shows a real Pi conversation invoking a three-task read-only batch, Herdr opening one interactive Agent pane per task, and the compact results returning to the parent.
|
|
9
|
+
The recording shows a real Pi conversation invoking a three-task read-only batch, the active Fleet footer in the parent, Herdr opening one interactive Agent pane per task, and the compact results returning to the parent.
|
|
10
10
|
|
|
11
11
|
https://github.com/user-attachments/assets/8ddf3ae6-a11a-492b-9cae-d9496ba65b67
|
|
12
12
|
|
|
13
|
-
The video starts a three-agent batch, focuses each live child pane in turn, then returns
|
|
14
|
-
to the parent summary. The reproducible source recording is retained as [`docs/demo.webm`](docs/demo.webm).
|
|
13
|
+
The video starts a three-agent batch, focuses each live child pane in turn, captures the parent Fleet footer with yellow running indicators and true usage, then returns to the parent summary. Completed Fleet rows use green indicators. The reproducible source recording is retained as [`docs/demo.webm`](docs/demo.webm).
|
|
15
14
|
|
|
16
15
|
## Requirements and install
|
|
17
16
|
|
|
@@ -73,7 +72,11 @@ A child missing required information reports `needs-input` with an exact questio
|
|
|
73
72
|
herdr_subagents_control({ action: "respond", runId: "<run-id>", answer: "Use the main branch." })
|
|
74
73
|
```
|
|
75
74
|
|
|
76
|
-
|
|
75
|
+
Cancel one active or queued run with `herdr_subagents_control({ action: "cancel", runId: "<run-id>" })`. Cancellation aborts its live child prompts, archives active or queued tasks as cancelled, closes its run tab, and leaves unrelated runs alone.
|
|
76
|
+
|
|
77
|
+
While a batch is active, a compact Fleet monitor stays below the editor. Its divider/title row summarizes the run, and each task row shows a hollow or filled status circle, the real persona and task name, status, live active duration, and cumulative child Pi token usage in one compact shared metrics column after the longest row rather than at the terminal edge. Running work uses Pi's warning/yellow semantic color and completed work uses success/green. Token totals use the child session's persisted assistant, tool-result, standalone usage, compaction, and branch-summary usage; they are never estimated. Queued tasks show neither fabricated time nor tokens. The monitor follows Pi theme semantics, truncates by visible terminal columns, and refreshes session usage from both file events and the running one-second timer.
|
|
78
|
+
|
|
79
|
+
With an empty editor, press `↓` to enter the list, `↑`/`↓` to choose a task, `Enter` to focus its exact pane, and `Esc` to return. `/herdr-subagents focus <task-number>` remains available as a fallback.
|
|
77
80
|
|
|
78
81
|
## History
|
|
79
82
|
|
|
@@ -83,7 +86,7 @@ Continue a saved conversation in a new Herdr tab with `herdr_subagents_control({
|
|
|
83
86
|
|
|
84
87
|
Read-only tasks start FIFO up to the configured concurrency limit. A pane is created only when its task starts, belongs exclusively to that task, and is never reused. Completed panes remain inspectable while another task in the batch is unsettled. Results remain in input order, and one failed task does not cancel its siblings. Any batch containing a write-capable persona is forced to concurrency one.
|
|
85
88
|
|
|
86
|
-
The widget shows one row per queued, running, blocked, failed, or completed task. Answering a blocked task refreshes
|
|
89
|
+
The widget shows one row per queued, running, blocked, failed, or completed task. Answering a blocked task or resuming saved history refreshes authoritative persona, timing, session, status, and pane projections as work advances. Active duration accumulates only while the task is running, excluding time spent queued or waiting for an answer. Completed rows remain selectable until the whole batch settles. `/herdr-subagents active` shows the same compact state. Native labels stay short: `SA · <batch>` for the tab and `<order> · <task>` for each pane. When all tasks settle, the tab closes immediately if unfocused or after the user leaves it; a deferred close failure is persisted and shown as a warning. An already-missing tab is treated as successfully cleaned, so retries are idempotent.
|
|
87
90
|
|
|
88
91
|
Foreground calls block until all children report completion, failure, or a request for input. Every child is started with Herdr's Agent lifecycle as a normal Pi TUI and prompted with wait semantics; no print-mode command or completion marker is injected into the pane.
|
|
89
92
|
|
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
|
|
5
5
|
1. `npm --prefix packages/herdr-subagents test` runs deterministic scheduler, recovery, and persona tests.
|
|
6
6
|
2. `pi -ne -ns --no-session -e ./packages/herdr-subagents/extensions/herdr-subagents.ts` verifies extension loading.
|
|
7
|
-
3. From a visible X11 Herdr client with the GStreamer X11 and VP8 plugins installed, run `HERDR_MEDIA_WINDOW_ID=<window-id> node scripts/capture-package-media.mjs`. The Herdr capture must show a parent Pi conversation, a three-task run tab, each of the three real interactive Agent panes receiving focus in turn, and the final compact parent response.
|
|
7
|
+
3. From a dedicated visible X11 Herdr client/window with the GStreamer X11 and VP8 plugins installed, run `HERDR_MEDIA_WINDOW_ID=<window-id> node scripts/capture-package-media.mjs`. Capture intentionally cycles through the live child panes, so avoid using that client/window for other work while recording; the script restores the previously focused pane afterward when it still exists. The Herdr capture must show a parent Pi conversation, a three-task run tab, each of the three real interactive Agent panes receiving focus in turn, the active parent Fleet footer with yellow running indicators and real metrics, and the final compact parent response. The published screenshot must be the parent Fleet view, while the full WebM preserves the parent → three focused children → Fleet → parent sequence.
|
|
8
8
|
4. `npm pack ./packages/herdr-subagents --dry-run` verifies package contents.
|
|
9
|
-
5. Verify the README renders the GitHub attachment video directly and live Herdr UI media `docs/screenshot.png` and `docs/demo.webm` are present in the tarball; `docs/demo.gif` must be absent. Inspect the WebM metadata and play it through once to confirm the parent → three focused children → parent sequence.
|
|
9
|
+
5. Verify the README renders the GitHub attachment video directly and live Herdr UI media `docs/screenshot.png` and `docs/demo.webm` are present in the tarball; `docs/demo.gif` must be absent. Inspect the WebM metadata and play it through once to confirm the parent → three focused children → Fleet → parent sequence.
|
|
10
10
|
|
|
11
11
|
## Cases
|
|
12
12
|
|
|
@@ -17,9 +17,11 @@
|
|
|
17
17
|
- Preserve compact reports, full results, Pi sessions, blocked questions, and historical follow-ups.
|
|
18
18
|
- Recover queued/running work after interruption without retrying post-prompt side effects, including truncated reports and recovery errors.
|
|
19
19
|
- Verify the split public contracts (`herdr_subagents` with required `tasks`, plus `herdr_subagents_control` with required `action`) and injectable extension wiring, including conditional registration only inside Herdr, Fleet rendering, editor focus ownership, terminal-input passthrough, exact-pane navigation, and stable display while another background run queues.
|
|
20
|
-
- Verify
|
|
20
|
+
- Verify Fleet divider/row formatting, warning/yellow running indicators, success/green completed indicators, persona and task projections, cumulative active duration across blocking/resume, exact Pi usage aggregation from assistant messages, tool results, standalone usage, compaction, and branch summaries, malformed/truncated JSONL tolerance, queued metric omission, and visible-column bounds at narrow widths.
|
|
21
|
+
- Verify live token updates are file-event driven and polled on each running one-second tick, elapsed repainting exists only while work runs, and widget disposal closes watchers and timers.
|
|
22
|
+
- Verify selected active/queued run cancellation, blocked-response live Fleet updates, authoritative stale-field replacement, parent-shutdown cancellation, idempotent missing-tab cleanup, cleanup notifications, atomic state replacement, non-retried uncertain startup operations, and stalled warnings.
|
|
21
23
|
- Verify resumed batches retain newly allocated pane IDs and historical follow-ups exclude simultaneous session writers.
|
|
22
24
|
|
|
23
25
|
## Latest baseline
|
|
24
26
|
|
|
25
|
-
All
|
|
27
|
+
All 168 deterministic scheduler, recovery, process-boundary, schema-validation, navigation, metrics, responsive-formatting, and persona cases must pass. Release validation also requires extension loading and a real Herdr read-only batch smoke with distinct task panes and final tab cleanup. The latest smoke completed two concurrent tasks, retained its focused run tab through settlement, and closed it automatically after focus moved away.
|
|
Binary file
|
|
Binary file
|
|
@@ -12,7 +12,7 @@ import { cancelActiveSubagentRuns, reconcileSubagentRuns } from "../src/reconcil
|
|
|
12
12
|
import { executeHerdrSubagents, executeHerdrSubagentsControl, type HerdrSubagentsBatchDetails, type HerdrSubagentsControlToolParams, type HerdrSubagentsDetails, type HerdrSubagentsRunParams } from "../src/tool.ts";
|
|
13
13
|
import { HerdrSubagentsControlParams, HerdrSubagentsParams } from "../src/parameters.ts";
|
|
14
14
|
import { HERDR_ACTIONS } from "../src/validation.ts";
|
|
15
|
-
import { FleetController } from "../src/fleet-controller.ts";
|
|
15
|
+
import { FleetController, type FleetWidgetRuntime } from "../src/fleet-controller.ts";
|
|
16
16
|
import { isSettledRunStatus } from "../src/records.ts";
|
|
17
17
|
|
|
18
18
|
const packageAgentsDir = fileURLToPath(new URL("../agents/", import.meta.url));
|
|
@@ -121,6 +121,7 @@ export interface HerdrSubagentsExtensionDependencies {
|
|
|
121
121
|
executeControl?: typeof executeHerdrSubagentsControl;
|
|
122
122
|
isEditor?: (value: unknown) => boolean;
|
|
123
123
|
createFleetWidget?: (lines: string[]) => Container;
|
|
124
|
+
fleetWidgetRuntime?: FleetWidgetRuntime;
|
|
124
125
|
}
|
|
125
126
|
|
|
126
127
|
async function recoverParentSession(cwd: string, herdr: HerdrAutomation, owner: { sessionId?: string; processId: number }, notify: (message: string) => void): Promise<void> {
|
|
@@ -157,8 +158,9 @@ export function registerHerdrSubagents(pi: ExtensionAPI, dependencies: HerdrSuba
|
|
|
157
158
|
const executeControl = dependencies.executeControl ?? executeHerdrSubagentsControl;
|
|
158
159
|
const isEditorComponent = dependencies.isEditor ?? ((value: unknown) => value instanceof Editor);
|
|
159
160
|
const dispatcher = new RunDispatcher();
|
|
160
|
-
const fleet = new FleetController(herdr, isEditorComponent, dependencies.createFleetWidget);
|
|
161
|
+
const fleet = new FleetController(herdr, isEditorComponent, dependencies.createFleetWidget, dependencies.fleetWidgetRuntime);
|
|
161
162
|
const runControllers = new Map<string, AbortController>();
|
|
163
|
+
const runPromises = new Map<string, Promise<unknown>>();
|
|
162
164
|
const inFlight = new Set<Promise<unknown>>();
|
|
163
165
|
let sessionController = new AbortController();
|
|
164
166
|
let ownerSessionId: string | undefined;
|
|
@@ -227,8 +229,9 @@ export function registerHerdrSubagents(pi: ExtensionAPI, dependencies: HerdrSuba
|
|
|
227
229
|
}
|
|
228
230
|
return result;
|
|
229
231
|
});
|
|
232
|
+
runPromises.set(runId, promise);
|
|
230
233
|
inFlight.add(promise);
|
|
231
|
-
void promise.finally(() => { inFlight.delete(promise); runControllers.delete(runId); }).catch(() => undefined);
|
|
234
|
+
void promise.finally(() => { inFlight.delete(promise); runControllers.delete(runId); runPromises.delete(runId); }).catch(() => undefined);
|
|
232
235
|
if (params.background) {
|
|
233
236
|
void promise.then((result) => {
|
|
234
237
|
if (generation !== sessionGeneration) return;
|
|
@@ -236,6 +239,10 @@ export function registerHerdrSubagents(pi: ExtensionAPI, dependencies: HerdrSuba
|
|
|
236
239
|
if (!fleet.active && !dispatcher.snapshot().activeRunId && dispatcher.snapshot().queuedRunIds.length === 0) fleet.render();
|
|
237
240
|
}).catch((error) => {
|
|
238
241
|
if (generation !== sessionGeneration) return;
|
|
242
|
+
if (controller.signal.aborted) {
|
|
243
|
+
pi.sendMessage({ customType: "herdr-subagents-run", content: `Subagent run ${runId} cancelled.`, display: true, details: { runId, status: "cancelled" } }, { triggerTurn: true, deliverAs: "followUp" });
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
239
246
|
pi.sendMessage({ customType: "herdr-subagents-run", content: `Subagent run ${runId} failed: ${error instanceof Error ? error.message : String(error)}`, display: true, details: { runId, status: "failed" } }, { triggerTurn: true, deliverAs: "followUp" });
|
|
240
247
|
});
|
|
241
248
|
return { content: [{ type: "text" as const, text: `Subagent run queued: ${runId}` }], details: { runId, status: "queued", background: true } };
|
|
@@ -252,12 +259,12 @@ export function registerHerdrSubagents(pi: ExtensionAPI, dependencies: HerdrSuba
|
|
|
252
259
|
pi.registerTool({
|
|
253
260
|
name: "herdr_subagents_control",
|
|
254
261
|
label: "Herdr Subagents Control",
|
|
255
|
-
description: "Inspect Herdr personas and run history, answer a blocked task, resume a saved task, or remove a selected archived run. Use herdr_subagents to start new work.",
|
|
262
|
+
description: "Inspect Herdr personas and run history, cancel active or queued work, answer a blocked task, resume a saved task, or remove a selected archived run. Use herdr_subagents to start new work.",
|
|
256
263
|
promptSnippet: "Inspect and control Herdr subagent runs",
|
|
257
264
|
parameters: HerdrSubagentsControlParams,
|
|
258
265
|
|
|
259
266
|
async execute(_toolCallId, params: HerdrSubagentsControlToolParams, signal, onUpdate, ctx) {
|
|
260
|
-
const executionId =
|
|
267
|
+
const executionId = randomUUID();
|
|
261
268
|
const controller = new AbortController();
|
|
262
269
|
runControllers.set(executionId, controller);
|
|
263
270
|
const runSignal = AbortSignal.any([controller.signal, sessionController.signal, ...(signal ? [signal] : [])]);
|
|
@@ -268,7 +275,34 @@ export function registerHerdrSubagents(pi: ExtensionAPI, dependencies: HerdrSuba
|
|
|
268
275
|
const updateDetails = update.details as { warning?: string };
|
|
269
276
|
if (updateDetails.warning || updateText?.includes("cleanup failed")) ctx.ui.notify(updateDetails.warning ?? updateText ?? "Subagent warning", "warning");
|
|
270
277
|
onUpdate?.(update);
|
|
271
|
-
}, ctx, {
|
|
278
|
+
}, ctx, {
|
|
279
|
+
agentsDirectory: packageAgentsDir,
|
|
280
|
+
herdr,
|
|
281
|
+
owner: { sessionId: ownerSessionId, processId: process.pid },
|
|
282
|
+
cancelRun: async (runId) => {
|
|
283
|
+
const reason = new Error("Subagent run cancelled by user.");
|
|
284
|
+
const targetController = runControllers.get(runId);
|
|
285
|
+
targetController?.abort(reason);
|
|
286
|
+
if (dispatcher.cancelQueuedRun(runId, reason)) {
|
|
287
|
+
await cancelQueuedRuns(ctx.cwd, [runId], { reason: reason.message, owner: { sessionId: ownerSessionId, processId: process.pid } });
|
|
288
|
+
fleet.clearRun(runId);
|
|
289
|
+
return { cancelled: true, queued: true };
|
|
290
|
+
}
|
|
291
|
+
const activePromise = runPromises.get(runId);
|
|
292
|
+
if (activePromise) {
|
|
293
|
+
await activePromise.catch(() => undefined);
|
|
294
|
+
fleet.clearRun(runId);
|
|
295
|
+
dispatcher.release(runId);
|
|
296
|
+
return { cancelled: true };
|
|
297
|
+
}
|
|
298
|
+
const cancellation = await cancelActiveSubagentRuns(ctx.cwd, herdr, { reason: reason.message, owner: { sessionId: ownerSessionId, processId: process.pid }, runId });
|
|
299
|
+
if (cancellation.cancelledRuns) {
|
|
300
|
+
fleet.clearRun(runId);
|
|
301
|
+
dispatcher.release(runId);
|
|
302
|
+
}
|
|
303
|
+
return { cancelled: cancellation.cancelledRuns > 0, cleanupErrors: cancellation.cleanupErrors };
|
|
304
|
+
},
|
|
305
|
+
});
|
|
272
306
|
inFlight.add(promise);
|
|
273
307
|
const result = await promise.finally(() => { inFlight.delete(promise); runControllers.delete(executionId); });
|
|
274
308
|
fleet.sync(result.details);
|
|
@@ -10,7 +10,7 @@ import { writeJsonAtomic } from "./state.ts";
|
|
|
10
10
|
import type { OwnerIdentity } from "./ownership.ts";
|
|
11
11
|
import { ownerRecord } from "./ownership.ts";
|
|
12
12
|
import { RunPaneAllocator } from "./run-pane-allocator.ts";
|
|
13
|
-
import { applyReport, applyTaskStatus, settleAttemptFailure, type PersistedReport, type RunRecord, type TaskRecord, type TaskStatus } from "./records.ts";
|
|
13
|
+
import { applyReport, applyTaskStatus, projectTaskActivity, settleAttemptFailure, type PersistedReport, type RunRecord, type TaskActivity, type TaskRecord, type TaskStatus } from "./records.ts";
|
|
14
14
|
import { reportProtocolPrompt } from "./protocol.ts";
|
|
15
15
|
import { cleanupRunTab } from "./tab-cleanup.ts";
|
|
16
16
|
import { applyRunStatus } from "./run-status.ts";
|
|
@@ -63,7 +63,7 @@ export interface BatchResult {
|
|
|
63
63
|
requestedConcurrency: number;
|
|
64
64
|
effectiveConcurrency: number;
|
|
65
65
|
tasks: BatchTaskResult[];
|
|
66
|
-
activity:
|
|
66
|
+
activity: TaskActivity[];
|
|
67
67
|
summary: string;
|
|
68
68
|
documents: HerdrSubagentsDocument[];
|
|
69
69
|
recordDirectory: string;
|
|
@@ -114,7 +114,7 @@ export async function runHerdrSubagentsBatch(tasks: BatchTask[], options: BatchO
|
|
|
114
114
|
await writeJsonAtomic(runFile, runRecord);
|
|
115
115
|
const herdr = options.herdr ?? new CliHerdrAutomation();
|
|
116
116
|
const results = new Array<BatchTaskResult>(tasks.length);
|
|
117
|
-
const activity: BatchResult["activity"] =
|
|
117
|
+
const activity: BatchResult["activity"] = contexts.map((context, index) => projectTaskActivity(context.taskRecord, index));
|
|
118
118
|
let tabId: string | undefined;
|
|
119
119
|
const paneAllocator = new RunPaneAllocator({ herdr, run: runRecord, runFile, signal: options.signal, onCleanupError: options.onCleanupError });
|
|
120
120
|
const emit = (status: BatchResult["status"], summary: string) => options.onUpdate?.({
|
|
@@ -146,7 +146,7 @@ export async function runHerdrSubagentsBatch(tasks: BatchTask[], options: BatchO
|
|
|
146
146
|
applyTaskStatus(context.taskRecord, "running");
|
|
147
147
|
Object.assign(context.taskRecord, { tabId, paneId, paneLabel, agentName, startedAt: timestamp() });
|
|
148
148
|
await writeJsonAtomic(context.taskFile, context.taskRecord);
|
|
149
|
-
activity[index] =
|
|
149
|
+
activity[index] = projectTaskActivity(context.taskRecord, index);
|
|
150
150
|
emit("running", `${settled}/${tasks.length} tasks settled.`);
|
|
151
151
|
const prompted = await promptLiveAgent({ herdr, task: { ...task, id: context.taskId, sessionFile: context.sessionFile }, paneId, prompt: task.prompt, systemPromptFile: context.systemPromptFile, agentName, signal: options.signal, stalledWarningMs: options.stalledWarningSeconds ? options.stalledWarningSeconds * 1000 : undefined, onStalled: () => emit("running", `${task.name} appears stalled; it is still running.`) });
|
|
152
152
|
agentName = prompted.agentName;
|
|
@@ -164,7 +164,7 @@ export async function runHerdrSubagentsBatch(tasks: BatchTask[], options: BatchO
|
|
|
164
164
|
Object.assign(context.taskRecord, { paneId, agentName });
|
|
165
165
|
await writeJsonAtomic(context.taskFile, context.taskRecord);
|
|
166
166
|
results[index] = { index, name: task.name, status, summary: report.summary, documents: report.documents, ...(report.error ? { error: report.error } : {}), ...(report.question ? { question: report.question } : {}), paneId, recordDirectory: context.taskDirectory, sessionFile: context.sessionFile };
|
|
167
|
-
activity[index] =
|
|
167
|
+
activity[index] = projectTaskActivity(context.taskRecord, index);
|
|
168
168
|
settled += 1;
|
|
169
169
|
emit("running", `${settled}/${tasks.length} tasks settled.`);
|
|
170
170
|
};
|
|
@@ -190,7 +190,7 @@ export async function runHerdrSubagentsBatch(tasks: BatchTask[], options: BatchO
|
|
|
190
190
|
const failed = settledResults.filter((result) => result.status === "failed").length;
|
|
191
191
|
const cancelled = options.signal?.aborted || activity.some((entry) => entry.status === "cancelled");
|
|
192
192
|
const status: BatchResult["status"] = cancelled ? "cancelled" : blocked || activity.some((entry) => entry.status === "queued") ? "blocked" : failed === 0 ? "completed" : failed === tasks.length ? "failed" : "partial";
|
|
193
|
-
const summary = status === "cancelled" ? "Subagent batch cancelled
|
|
193
|
+
const summary = status === "cancelled" ? "Subagent batch cancelled." : blocked ? `${blocked.name} needs input: ${blocked.question}` : failed ? `${tasks.length - failed}/${tasks.length} tasks completed; ${failed} failed.` : `${tasks.length}/${tasks.length} tasks completed.`;
|
|
194
194
|
applyRunStatus(runRecord, status, { question: blocked?.question });
|
|
195
195
|
await writeJsonAtomic(runFile, runRecord);
|
|
196
196
|
return { ok: status === "completed", status, runId, label: options.label, requestedConcurrency, effectiveConcurrency, tasks: settledResults, activity, summary, documents: settledResults.flatMap((result) => result.documents), recordDirectory: runDirectory, tabId };
|
|
@@ -205,7 +205,7 @@ export async function runHerdrSubagentsBatch(tasks: BatchTask[], options: BatchO
|
|
|
205
205
|
await cleanupRunTab({
|
|
206
206
|
herdr, tabId, runFile, runRecord, signal: options.signal, failureStatus: "failed",
|
|
207
207
|
errorPrefix: "Task results settled, but Herdr tab cleanup failed",
|
|
208
|
-
onError: (message) =>
|
|
208
|
+
onError: (message) => options.onCleanupError?.(message),
|
|
209
209
|
});
|
|
210
210
|
} catch (error) {
|
|
211
211
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -9,7 +9,7 @@ import { cleanupRunTab } from "./tab-cleanup.ts";
|
|
|
9
9
|
import type { OwnerIdentity } from "./ownership.ts";
|
|
10
10
|
import { ownerRecord } from "./ownership.ts";
|
|
11
11
|
import { aggregateTaskStatus, applyRunStatus } from "./run-status.ts";
|
|
12
|
-
import { applyReport, applyTaskStatus, isActiveStatus, persistFailureReport, readReport, readRunRecord, readTaskRecord, readTurnRecord, settleAttemptFailure, type PersistedDocument, type PersistedReport, type RunRecord, type TaskRecord, type TaskStatus, type TurnRecord } from "./records.ts";
|
|
12
|
+
import { applyReport, applyTaskStatus, isActiveStatus, persistFailureReport, projectTaskActivity, readReport, readRunRecord, readTaskRecord, readTurnRecord, settleAttemptFailure, type PersistedDocument, type PersistedReport, type RunRecord, type TaskActivity, type TaskRecord, type TaskStatus, type TurnRecord } from "./records.ts";
|
|
13
13
|
import { isAbortError } from "./errors.ts";
|
|
14
14
|
import { continuationSystemPrompt } from "./protocol.ts";
|
|
15
15
|
import { RunPaneAllocator } from "./run-pane-allocator.ts";
|
|
@@ -28,7 +28,7 @@ export interface ResumeBlockedResult {
|
|
|
28
28
|
requestedConcurrency: number;
|
|
29
29
|
effectiveConcurrency: number;
|
|
30
30
|
tasks?: Array<{ index: number; name: string; status: TaskStatus; summary: string; documents: PersistedDocument[]; error?: string; question?: string; paneId: string; recordDirectory: string; sessionFile: string }>;
|
|
31
|
-
activity?:
|
|
31
|
+
activity?: TaskActivity[];
|
|
32
32
|
tabId?: string;
|
|
33
33
|
}
|
|
34
34
|
|
|
@@ -45,7 +45,7 @@ async function emitProgress(options: RespondToBlockedTaskOptions, runDirectory:
|
|
|
45
45
|
recordDirectory: runDirectory,
|
|
46
46
|
requestedConcurrency: Number(run.requestedConcurrency ?? 1),
|
|
47
47
|
effectiveConcurrency: Number(run.effectiveConcurrency ?? 1),
|
|
48
|
-
activity: tasks.map((entry, index) => (
|
|
48
|
+
activity: tasks.map((entry, index) => projectTaskActivity(entry, index)),
|
|
49
49
|
...(run.tabId ? { tabId: run.tabId } : {}),
|
|
50
50
|
});
|
|
51
51
|
}
|
|
@@ -73,7 +73,7 @@ async function projectSettledRun(runDirectory: string, runFile: string, run: Run
|
|
|
73
73
|
await writeJsonAtomic(runFile, run);
|
|
74
74
|
if (runStatus !== "blocked" && run.tabId) await cleanupRunTab({ herdr, tabId: run.tabId, runFile, runRecord: run, onError: onCleanupError, failureStatus: "failed" });
|
|
75
75
|
const summary = runStatus === "completed" ? "All tasks completed after the answer." : runStatus === "blocked" ? `${finalTasks[blockedIndex]?.name ?? "Task"} needs input: ${blockedReport?.question ?? "Input required."}` : finalReports.map((entry, index) => `${finalTasks[index].name}: ${entry.summary}`).join("\n");
|
|
76
|
-
const activity = finalTasks.map((entry, index) => (
|
|
76
|
+
const activity = finalTasks.map((entry, index) => projectTaskActivity(entry, index));
|
|
77
77
|
const tasks = finalTasks.map((entry, index) => ({ index, name: activity[index].name, status: entry.status, summary: String(finalReports[index].summary ?? "Task has no report."), documents: finalReports[index].documents ?? [], ...(entry.error ? { error: String(entry.error) } : {}), ...(entry.question ? { question: String(entry.question) } : {}), paneId: String(entry.paneId ?? ""), recordDirectory: taskDirectories[index], sessionFile: String(entry.sessionFile ?? "") }));
|
|
78
78
|
return { ok: runStatus === "completed", runId: run.id, label: String(run.label ?? "batch"), status: runStatus, summary, ...(blockedReport?.question ? { question: blockedReport.question } : {}), documents: finalReports.flatMap((entry) => entry.documents ?? []), recordDirectory: runDirectory, requestedConcurrency: Number(run.requestedConcurrency ?? run.effectiveConcurrency ?? 1), effectiveConcurrency: Number(run.effectiveConcurrency ?? 1), tasks, activity, ...(run.tabId ? { tabId: String(run.tabId) } : {}) };
|
|
79
79
|
}
|
|
@@ -51,6 +51,14 @@ export class RunDispatcher {
|
|
|
51
51
|
this.changed();
|
|
52
52
|
return cancelled.map((run) => run.id);
|
|
53
53
|
}
|
|
54
|
+
cancelQueuedRun(id: string, reason: Error): boolean {
|
|
55
|
+
const index = this.queue.findIndex((run) => run.id === id);
|
|
56
|
+
if (index < 0) return false;
|
|
57
|
+
const [cancelled] = this.queue.splice(index, 1);
|
|
58
|
+
cancelled.reject(reason);
|
|
59
|
+
this.changed();
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
54
62
|
|
|
55
63
|
private changed(): void { this.onChange?.(this.snapshot()); }
|
|
56
64
|
|
|
@@ -1,40 +1,178 @@
|
|
|
1
|
+
import { watch } from "node:fs";
|
|
2
|
+
import { basename, dirname } from "node:path";
|
|
1
3
|
import type { ExtensionUIContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import {
|
|
4
|
+
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
5
|
+
import { Container, isKeyRelease, matchesKey, type Component, type TUI } from "@earendil-works/pi-tui";
|
|
3
6
|
import type { HerdrAutomation } from "./herdr.ts";
|
|
4
|
-
import { fleetEditorHasFocus, FleetSelection, fleetLines } from "./fleet.ts";
|
|
7
|
+
import { fleetEditorHasFocus, FleetSelection, fleetLines, readSessionTokens, type FleetTaskMetrics } from "./fleet.ts";
|
|
5
8
|
import { focusActiveTask } from "./monitor.ts";
|
|
6
9
|
import { isActiveStatus } from "./records.ts";
|
|
7
10
|
import type { HerdrSubagentsBatchDetails } from "./tool.ts";
|
|
8
11
|
|
|
12
|
+
interface Closable { close(): void; }
|
|
13
|
+
export interface FleetWidgetRuntime {
|
|
14
|
+
watchSession(path: string, onChange: () => void, onError: () => void): Closable;
|
|
15
|
+
setInterval(callback: () => void, ms: number): unknown;
|
|
16
|
+
clearInterval(handle: unknown): void;
|
|
17
|
+
setTimeout(callback: () => void, ms: number): unknown;
|
|
18
|
+
clearTimeout(handle: unknown): void;
|
|
19
|
+
readTokens(path: string): Promise<number | undefined>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const defaultFleetWidgetRuntime: FleetWidgetRuntime = {
|
|
23
|
+
watchSession(path, onChange, onError) {
|
|
24
|
+
const watcher = watch(dirname(path), (_event, filename) => {
|
|
25
|
+
if (!filename || filename.toString() === basename(path)) onChange();
|
|
26
|
+
});
|
|
27
|
+
watcher.on("error", () => { watcher.close(); onError(); });
|
|
28
|
+
return watcher;
|
|
29
|
+
},
|
|
30
|
+
setInterval: (callback, ms) => setInterval(callback, ms),
|
|
31
|
+
clearInterval: (handle) => clearInterval(handle as ReturnType<typeof setInterval>),
|
|
32
|
+
setTimeout: (callback, ms) => setTimeout(callback, ms),
|
|
33
|
+
clearTimeout: (handle) => clearTimeout(handle as ReturnType<typeof setTimeout>),
|
|
34
|
+
readTokens: readSessionTokens,
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export class FleetWidget implements Component {
|
|
38
|
+
private readonly tui: TUI;
|
|
39
|
+
private readonly theme: Theme;
|
|
40
|
+
private readonly selection: FleetSelection;
|
|
41
|
+
private readonly runtime: FleetWidgetRuntime;
|
|
42
|
+
private batch: HerdrSubagentsBatchDetails;
|
|
43
|
+
private readonly metrics = new Map<number, FleetTaskMetrics>();
|
|
44
|
+
private readonly watchers = new Map<string, Closable>();
|
|
45
|
+
private elapsedTimer?: unknown;
|
|
46
|
+
private refreshTimer?: unknown;
|
|
47
|
+
private generation = 0;
|
|
48
|
+
private disposed = false;
|
|
49
|
+
private sourceKey = "";
|
|
50
|
+
|
|
51
|
+
constructor(tui: TUI, theme: Theme, batch: HerdrSubagentsBatchDetails, selection: FleetSelection, runtime: FleetWidgetRuntime = defaultFleetWidgetRuntime) {
|
|
52
|
+
this.tui = tui;
|
|
53
|
+
this.theme = theme;
|
|
54
|
+
this.selection = selection;
|
|
55
|
+
this.runtime = runtime;
|
|
56
|
+
this.batch = batch;
|
|
57
|
+
this.configureLiveUpdates();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
update(batch: HerdrSubagentsBatchDetails): void {
|
|
61
|
+
if (this.disposed) return;
|
|
62
|
+
this.batch = batch;
|
|
63
|
+
this.configureLiveUpdates();
|
|
64
|
+
this.tui.requestRender();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
render(width: number): string[] {
|
|
68
|
+
return fleetLines(this.batch, this.selection, { width, metrics: this.metrics, theme: this.theme });
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
invalidate(): void {}
|
|
72
|
+
|
|
73
|
+
dispose(): void {
|
|
74
|
+
if (this.disposed) return;
|
|
75
|
+
this.disposed = true;
|
|
76
|
+
this.generation += 1;
|
|
77
|
+
for (const watcher of this.watchers.values()) watcher.close();
|
|
78
|
+
this.watchers.clear();
|
|
79
|
+
if (this.elapsedTimer) this.runtime.clearInterval(this.elapsedTimer);
|
|
80
|
+
if (this.refreshTimer) this.runtime.clearTimeout(this.refreshTimer);
|
|
81
|
+
this.elapsedTimer = undefined;
|
|
82
|
+
this.refreshTimer = undefined;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
private configureLiveUpdates(): void {
|
|
86
|
+
const sessionFiles = new Set(this.batch.activity.map((task) => task.sessionFile).filter((path): path is string => Boolean(path)));
|
|
87
|
+
const nextSourceKey = `${this.batch.runId}\n${[...sessionFiles].sort().join("\n")}`;
|
|
88
|
+
if (nextSourceKey !== this.sourceKey) {
|
|
89
|
+
this.sourceKey = nextSourceKey;
|
|
90
|
+
this.generation += 1;
|
|
91
|
+
this.metrics.clear();
|
|
92
|
+
}
|
|
93
|
+
for (const [path, watcher] of this.watchers) {
|
|
94
|
+
if (sessionFiles.has(path)) continue;
|
|
95
|
+
watcher.close();
|
|
96
|
+
this.watchers.delete(path);
|
|
97
|
+
}
|
|
98
|
+
for (const path of sessionFiles) {
|
|
99
|
+
if (this.watchers.has(path)) continue;
|
|
100
|
+
try {
|
|
101
|
+
const watcher = this.runtime.watchSession(path, () => this.scheduleMetricsRefresh(), () => this.watchers.delete(path));
|
|
102
|
+
this.watchers.set(path, watcher);
|
|
103
|
+
} catch { /* The next durable activity update can retry the watch. */ }
|
|
104
|
+
}
|
|
105
|
+
const hasRunning = this.batch.activity.some((task) => task.status === "running");
|
|
106
|
+
if (hasRunning && !this.elapsedTimer) this.elapsedTimer = this.runtime.setInterval(() => {
|
|
107
|
+
this.tui.requestRender();
|
|
108
|
+
this.scheduleMetricsRefresh(0);
|
|
109
|
+
}, 1_000);
|
|
110
|
+
else if (!hasRunning && this.elapsedTimer) { this.runtime.clearInterval(this.elapsedTimer); this.elapsedTimer = undefined; }
|
|
111
|
+
this.scheduleMetricsRefresh(0);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
private scheduleMetricsRefresh(delay = 25): void {
|
|
115
|
+
if (this.disposed) return;
|
|
116
|
+
if (this.refreshTimer) this.runtime.clearTimeout(this.refreshTimer);
|
|
117
|
+
this.refreshTimer = this.runtime.setTimeout(() => { this.refreshTimer = undefined; void this.refreshMetrics(); }, delay);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
private async refreshMetrics(): Promise<void> {
|
|
121
|
+
const generation = ++this.generation;
|
|
122
|
+
const next = await Promise.all(this.batch.activity.map(async (task) => ({ index: task.index, tokens: task.sessionFile ? await this.runtime.readTokens(task.sessionFile) : undefined })));
|
|
123
|
+
if (this.disposed || generation !== this.generation) return;
|
|
124
|
+
this.metrics.clear();
|
|
125
|
+
for (const metric of next) if (metric.tokens !== undefined) this.metrics.set(metric.index, { tokens: metric.tokens });
|
|
126
|
+
this.tui.requestRender();
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
9
130
|
export class FleetController {
|
|
131
|
+
private readonly herdr: HerdrAutomation;
|
|
132
|
+
private readonly isEditor: (value: unknown) => boolean;
|
|
133
|
+
private readonly createWidget?: (lines: string[]) => Container;
|
|
134
|
+
private readonly widgetRuntime: FleetWidgetRuntime;
|
|
10
135
|
private activeBatch?: HerdrSubagentsBatchDetails;
|
|
11
136
|
private readonly selection = new FleetSelection(matchesKey);
|
|
12
137
|
private unsubscribeInput?: () => void;
|
|
13
138
|
private ui?: ExtensionUIContext;
|
|
14
|
-
private tui?:
|
|
139
|
+
private tui?: TUI;
|
|
140
|
+
private widget?: FleetWidget;
|
|
15
141
|
|
|
16
|
-
constructor(
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
142
|
+
constructor(herdr: HerdrAutomation, isEditor: (value: unknown) => boolean, createWidget?: (lines: string[]) => Container, widgetRuntime: FleetWidgetRuntime = defaultFleetWidgetRuntime) {
|
|
143
|
+
this.herdr = herdr;
|
|
144
|
+
this.isEditor = isEditor;
|
|
145
|
+
this.createWidget = createWidget;
|
|
146
|
+
this.widgetRuntime = widgetRuntime;
|
|
147
|
+
}
|
|
21
148
|
|
|
22
149
|
get active(): HerdrSubagentsBatchDetails | undefined { return this.activeBatch; }
|
|
23
150
|
|
|
24
151
|
bind(ui: ExtensionUIContext, tuiMode: boolean): void {
|
|
25
152
|
this.unsubscribeInput?.();
|
|
153
|
+
if (this.ui && this.ui !== ui) {
|
|
154
|
+
this.ui.setWidget("herdr-subagents", undefined, { placement: "belowEditor" });
|
|
155
|
+
this.widget = undefined;
|
|
156
|
+
this.tui = undefined;
|
|
157
|
+
}
|
|
26
158
|
this.ui = ui;
|
|
27
159
|
this.unsubscribeInput = tuiMode ? ui.onTerminalInput((data) => this.handleInput(data)) : undefined;
|
|
160
|
+
if (this.activeBatch) this.render();
|
|
28
161
|
}
|
|
29
162
|
|
|
30
163
|
sync(details: unknown): void {
|
|
164
|
+
if (!this.ui) return;
|
|
31
165
|
const batch = details as Partial<HerdrSubagentsBatchDetails>;
|
|
32
166
|
if (!batch.activity || !batch.label || !batch.status) return;
|
|
167
|
+
if (this.activeBatch && batch.runId !== this.activeBatch.runId) return;
|
|
168
|
+
if (this.activeBatch?.runId !== batch.runId) this.selection.reset();
|
|
33
169
|
this.activeBatch = isActiveStatus(batch.status) ? batch as HerdrSubagentsBatchDetails : undefined;
|
|
34
170
|
this.render();
|
|
35
171
|
}
|
|
36
172
|
|
|
37
173
|
setActive(batch: HerdrSubagentsBatchDetails | undefined): void {
|
|
174
|
+
if (!this.ui) return;
|
|
175
|
+
if (this.activeBatch?.runId !== batch?.runId) this.selection.reset();
|
|
38
176
|
this.activeBatch = batch;
|
|
39
177
|
this.render();
|
|
40
178
|
}
|
|
@@ -53,18 +191,31 @@ export class FleetController {
|
|
|
53
191
|
this.activeBatch = undefined;
|
|
54
192
|
this.selection.reset();
|
|
55
193
|
this.render();
|
|
194
|
+
this.ui = undefined;
|
|
195
|
+
this.widget = undefined;
|
|
196
|
+
this.tui = undefined;
|
|
56
197
|
}
|
|
57
198
|
|
|
58
199
|
render(): void {
|
|
59
200
|
if (!this.ui) return;
|
|
60
|
-
|
|
61
|
-
this.
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
201
|
+
if (!this.activeBatch) {
|
|
202
|
+
this.widget = undefined;
|
|
203
|
+
this.ui.setWidget("herdr-subagents", undefined, { placement: "belowEditor" });
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
if (this.createWidget) {
|
|
207
|
+
this.ui.setWidget("herdr-subagents", (tui) => {
|
|
208
|
+
this.tui = tui;
|
|
209
|
+
return this.createWidget?.(fleetLines(this.activeBatch as HerdrSubagentsBatchDetails, this.selection)) ?? new Container();
|
|
210
|
+
}, { placement: "belowEditor" });
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
if (this.widget) { this.widget.update(this.activeBatch); return; }
|
|
214
|
+
this.ui.setWidget("herdr-subagents", (tui, theme) => {
|
|
215
|
+
this.tui = tui;
|
|
216
|
+
this.widget = new FleetWidget(tui, theme, this.activeBatch as HerdrSubagentsBatchDetails, this.selection, this.widgetRuntime);
|
|
217
|
+
return this.widget;
|
|
218
|
+
}, { placement: "belowEditor" });
|
|
68
219
|
}
|
|
69
220
|
|
|
70
221
|
private handleInput(data: string): { consume: true } | undefined {
|