pi-subagents 0.53.0 → 0.55.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.
- package/CHANGELOG.md +66 -5
- package/README.md +1 -1
- package/agents/reviewer.md +12 -2
- package/docs/agents.md +4 -4
- package/docs/configuration.md +6 -6
- package/docs/extension-api.md +14 -4
- package/docs/models.md +41 -6
- package/docs/observability.md +14 -3
- package/docs/tool-reference.md +21 -6
- package/docs/workflows.md +7 -1
- package/index.ts +10 -1
- package/package.json +1 -1
- package/prompts/council.md +19 -7
- package/prompts/parallel-review.md +5 -1
- package/prompts/review-loop.md +9 -5
- package/skills/council-mode/SKILL.md +50 -26
- package/skills/pi-subagents/SKILL.md +5 -1
- package/skills/pi-subagents/references/constraints-and-recipes.md +16 -1
- package/skills/pi-subagents/references/execution-controls.md +13 -4
- package/skills/pi-subagents/references/multi-lane-orchestration.md +3 -3
- package/skills/pi-subagents/references/prompting-and-roles.md +14 -7
- package/src/agents/agent-management.ts +115 -14
- package/src/agents/agent-serializer.ts +2 -2
- package/src/agents/agents.ts +195 -47
- package/src/api/external-job-provider.ts +10 -1
- package/src/api/preflight.ts +33 -19
- package/src/api/project-panes.ts +2 -0
- package/src/extension/doctor.ts +10 -0
- package/src/extension/fanout-child.ts +3 -2
- package/src/extension/index.ts +24 -4
- package/src/extension/public-execution.ts +12 -9
- package/src/extension/rpc.ts +77 -3
- package/src/extension/schemas.ts +2 -1
- package/src/extension/tool-description.ts +9 -6
- package/src/extension/tool-result.ts +19 -0
- package/src/inspectors/herdr/client.ts +3 -3
- package/src/inspectors/herdr/focus.ts +55 -0
- package/src/inspectors/herdr/project-panes.ts +228 -44
- package/src/integrations/herdr-status.ts +26 -4
- package/src/runs/background/async-execution.ts +112 -13
- package/src/runs/background/async-job-tracker.ts +33 -14
- package/src/runs/background/async-resume.ts +20 -2
- package/src/runs/background/async-retention.ts +1 -1
- package/src/runs/background/async-status.ts +10 -0
- package/src/runs/background/chain-root-attachment.ts +5 -0
- package/src/runs/background/control-channel.ts +98 -10
- package/src/runs/background/notify.ts +62 -1
- package/src/runs/background/result-watcher.ts +4 -3
- package/src/runs/background/run-status.ts +15 -1
- package/src/runs/background/stale-run-reconciler.ts +3 -0
- package/src/runs/background/subagent-runner.ts +251 -38
- package/src/runs/background/wait-completions.ts +2 -0
- package/src/runs/background/wait-tool.ts +4 -3
- package/src/runs/foreground/async-stop-action.ts +23 -2
- package/src/runs/foreground/execution.ts +110 -8
- package/src/runs/foreground/subagent-executor.ts +361 -45
- package/src/runs/foreground/workflow-detach-reconcile.ts +3 -0
- package/src/runs/shared/acceptance.ts +1 -0
- package/src/runs/shared/child-identity.ts +36 -0
- package/src/runs/shared/completion-guard.ts +50 -1
- package/src/runs/shared/external-job-bridge.ts +53 -37
- package/src/runs/shared/external-job-runner.ts +126 -23
- package/src/runs/shared/model-fallback.ts +46 -15
- package/src/runs/shared/model-scope.ts +106 -39
- package/src/runs/shared/orca-progress-tabs.ts +71 -9
- package/src/runs/shared/parallel-utils.ts +12 -0
- package/src/runs/shared/pi-args.ts +31 -1
- package/src/runs/shared/subagent-prompt-runtime.ts +39 -10
- package/src/runs/shared/tool-availability.ts +1 -3
- package/src/shared/launch-contract.ts +6 -5
- package/src/shared/thinking-ceiling.ts +52 -0
- package/src/shared/types.ts +72 -5
- package/src/tui/fleet-status.ts +66 -13
- package/src/tui/render.ts +5 -4
- package/src/watchdog/permission-arbiter.ts +59 -51
- package/src/workflows/scripted-workflow.ts +107 -10
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { HerdrClient, HerdrErrorCode } from "./client.ts";
|
|
2
|
+
|
|
3
|
+
export type HerdrFocusErrorCode = HerdrErrorCode | "PANE_FOCUS_UNSUPPORTED" | "INVALID_PANE_RESPONSE";
|
|
4
|
+
|
|
5
|
+
export type HerdrPaneFocusResult =
|
|
6
|
+
| { ok: true; data: { paneId: string; tabId?: string; workspaceId?: string } }
|
|
7
|
+
| { ok: false; error: { code: HerdrFocusErrorCode; message: string; details?: unknown } };
|
|
8
|
+
|
|
9
|
+
export function herdrPaneRecord(value: unknown): Record<string, unknown> | undefined {
|
|
10
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
11
|
+
const record = value as Record<string, unknown>;
|
|
12
|
+
return record.pane && typeof record.pane === "object" && !Array.isArray(record.pane)
|
|
13
|
+
? record.pane as Record<string, unknown>
|
|
14
|
+
: record;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function text(record: Record<string, unknown>, ...keys: string[]): string | undefined {
|
|
18
|
+
for (const key of keys) if (typeof record[key] === "string" && record[key]) return record[key] as string;
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function herdrPaneFocusTarget(value: unknown): { paneId?: string; tabId?: string; workspaceId?: string } {
|
|
23
|
+
const pane = herdrPaneRecord(value);
|
|
24
|
+
if (!pane) return {};
|
|
25
|
+
return {
|
|
26
|
+
paneId: text(pane, "pane_id", "paneId", "id"),
|
|
27
|
+
tabId: text(pane, "tab_id", "tabId"),
|
|
28
|
+
workspaceId: text(pane, "workspace_id", "workspaceId"),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function focusHerdrPane(client: HerdrClient, paneId: string, signal?: AbortSignal): Promise<HerdrPaneFocusResult> {
|
|
33
|
+
const live = await client.run(["pane", "get", paneId], { timeoutMs: 5_000, signal });
|
|
34
|
+
if (live.ok === false) return live;
|
|
35
|
+
const target = herdrPaneFocusTarget(live.data);
|
|
36
|
+
if (!target.paneId) {
|
|
37
|
+
return { ok: false, error: { code: "INVALID_PANE_RESPONSE", message: `Herdr pane get returned no pane id for '${paneId}'.`, details: live.data } };
|
|
38
|
+
}
|
|
39
|
+
if (target.tabId) {
|
|
40
|
+
const focused = await client.run(["tab", "focus", target.tabId], { timeoutMs: 5_000, signal });
|
|
41
|
+
return focused.ok ? { ok: true, data: { paneId: target.paneId, tabId: target.tabId, ...(target.workspaceId ? { workspaceId: target.workspaceId } : {}) } } : focused;
|
|
42
|
+
}
|
|
43
|
+
if (target.workspaceId) {
|
|
44
|
+
const focused = await client.run(["workspace", "focus", target.workspaceId], { timeoutMs: 5_000, signal });
|
|
45
|
+
return focused.ok ? { ok: true, data: { paneId: target.paneId, workspaceId: target.workspaceId } } : focused;
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
ok: false,
|
|
49
|
+
error: {
|
|
50
|
+
code: "PANE_FOCUS_UNSUPPORTED",
|
|
51
|
+
message: `Herdr pane '${paneId}' has no tab_id or workspace_id. Select it in Herdr manually, or upgrade Herdr focus support.`,
|
|
52
|
+
details: live.data,
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
}
|
|
@@ -4,8 +4,9 @@ import type { AgentToolResult } from "@earendil-works/pi-agent-core";
|
|
|
4
4
|
import { getPiSpawnCommand } from "../../runs/shared/pi-spawn.ts";
|
|
5
5
|
import { getProjectSubagentsDir } from "../../shared/artifacts.ts";
|
|
6
6
|
import { writeAtomicJson } from "../../shared/atomic-json.ts";
|
|
7
|
-
import type { Details } from "../../shared/types.ts";
|
|
7
|
+
import type { Details, HerdrProjectPaneSnapshot, SubagentState } from "../../shared/types.ts";
|
|
8
8
|
import { createHerdrClient, detectHerdr, type HerdrClient, type HerdrErrorCode } from "./client.ts";
|
|
9
|
+
import { focusHerdrPane, herdrPaneFocusTarget, herdrPaneRecord } from "./focus.ts";
|
|
9
10
|
import { formatShellCommand } from "./shell-command.ts";
|
|
10
11
|
|
|
11
12
|
export const HERDR_PROJECT_PANE_ACTIONS = ["project.open", "project.status", "project.close"] as const;
|
|
@@ -34,6 +35,9 @@ export interface ProjectPaneRuntime {
|
|
|
34
35
|
cwd?: string;
|
|
35
36
|
foregroundCwd?: string;
|
|
36
37
|
focused?: boolean;
|
|
38
|
+
tabId?: string;
|
|
39
|
+
workspaceId?: string;
|
|
40
|
+
summary?: string;
|
|
37
41
|
terminalTitle?: string;
|
|
38
42
|
}
|
|
39
43
|
|
|
@@ -44,6 +48,7 @@ export type ProjectPaneErrorCode = HerdrErrorCode
|
|
|
44
48
|
| "BINDING_READ_FAILED"
|
|
45
49
|
| "BINDING_WRITE_FAILED"
|
|
46
50
|
| "BINDING_REMOVE_FAILED"
|
|
51
|
+
| "PANE_FOCUS_UNSUPPORTED"
|
|
47
52
|
| "PANE_NOT_IDLE"
|
|
48
53
|
| "PANE_OWNERSHIP_UNVERIFIED";
|
|
49
54
|
|
|
@@ -87,6 +92,13 @@ export interface CloseProjectPaneData extends ProjectPaneCommonData {
|
|
|
87
92
|
runtime?: ProjectPaneRuntime;
|
|
88
93
|
}
|
|
89
94
|
|
|
95
|
+
export interface FocusProjectPaneData extends ProjectPaneCommonData {
|
|
96
|
+
binding: HerdrProjectPaneBinding;
|
|
97
|
+
runtime: ProjectPaneRuntime;
|
|
98
|
+
ownership: "verified" | "unknown" | "mismatch";
|
|
99
|
+
focused: { paneId: string; tabId?: string; workspaceId?: string };
|
|
100
|
+
}
|
|
101
|
+
|
|
90
102
|
export interface OpenProjectPaneOptions {
|
|
91
103
|
cwd: string;
|
|
92
104
|
message?: string;
|
|
@@ -106,12 +118,7 @@ export interface CloseProjectPaneOptions {
|
|
|
106
118
|
signal?: AbortSignal;
|
|
107
119
|
}
|
|
108
120
|
|
|
109
|
-
export
|
|
110
|
-
run<T = unknown>(
|
|
111
|
-
args: string[],
|
|
112
|
-
options?: { timeoutMs?: number; signal?: AbortSignal; textOk?: boolean },
|
|
113
|
-
): Promise<{ ok: true; data: T } | { ok: false; error: { code: HerdrErrorCode; message: string; details?: unknown } }>;
|
|
114
|
-
}
|
|
121
|
+
export type ProjectPaneCommandClient = Pick<HerdrClient, "run">;
|
|
115
122
|
|
|
116
123
|
export interface ProjectPaneManagerOptions {
|
|
117
124
|
client?: ProjectPaneCommandClient;
|
|
@@ -121,6 +128,7 @@ export interface ProjectPaneManagerOptions {
|
|
|
121
128
|
export interface ProjectPaneManager {
|
|
122
129
|
open(options: OpenProjectPaneOptions): Promise<ProjectPaneResult<OpenProjectPaneData>>;
|
|
123
130
|
status(options: GetProjectPaneStatusOptions): Promise<ProjectPaneResult<ProjectPaneStatusData>>;
|
|
131
|
+
focus(options: GetProjectPaneStatusOptions): Promise<ProjectPaneResult<FocusProjectPaneData>>;
|
|
124
132
|
close(options: CloseProjectPaneOptions): Promise<ProjectPaneResult<CloseProjectPaneData>>;
|
|
125
133
|
}
|
|
126
134
|
|
|
@@ -129,6 +137,12 @@ interface InternalProjectPaneManagerOptions extends ProjectPaneManagerOptions {
|
|
|
129
137
|
legacyToolCompatibility?: boolean;
|
|
130
138
|
}
|
|
131
139
|
|
|
140
|
+
interface HerdrProjectPaneRootIndex {
|
|
141
|
+
schemaVersion: 1;
|
|
142
|
+
kind: "herdr-project-pane-roots";
|
|
143
|
+
projectRoots: string[];
|
|
144
|
+
}
|
|
145
|
+
|
|
132
146
|
interface ProjectPaneParams {
|
|
133
147
|
cwd?: string;
|
|
134
148
|
message?: string;
|
|
@@ -137,6 +151,7 @@ interface ProjectPaneParams {
|
|
|
137
151
|
|
|
138
152
|
interface ProjectPaneDeps {
|
|
139
153
|
cwd: string;
|
|
154
|
+
state?: SubagentState;
|
|
140
155
|
client?: HerdrClient;
|
|
141
156
|
signal?: AbortSignal;
|
|
142
157
|
now?: () => Date;
|
|
@@ -162,6 +177,51 @@ export function projectPaneBindingPath(projectRoot: string): string {
|
|
|
162
177
|
return path.join(projectPaneDir(projectRoot), "herdr.json");
|
|
163
178
|
}
|
|
164
179
|
|
|
180
|
+
function projectPaneRootIndexPath(ownerRoot: string): string {
|
|
181
|
+
return path.join(projectPaneDir(ownerRoot), "herdr-roots.json");
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function parseRootIndex(value: unknown): string[] {
|
|
185
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("Invalid Herdr project pane root index.");
|
|
186
|
+
const input = value as Partial<HerdrProjectPaneRootIndex>;
|
|
187
|
+
if (input.schemaVersion !== 1 || input.kind !== "herdr-project-pane-roots" || !Array.isArray(input.projectRoots)) throw new Error("Invalid Herdr project pane root index.");
|
|
188
|
+
if (!input.projectRoots.every((root) => typeof root === "string" && root.length > 0)) throw new Error("Invalid Herdr project pane root index.");
|
|
189
|
+
return input.projectRoots;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export function listHerdrProjectPaneRoots(ownerRoot: string): string[] {
|
|
193
|
+
try {
|
|
194
|
+
return parseRootIndex(JSON.parse(fs.readFileSync(projectPaneRootIndexPath(ownerRoot), "utf-8")));
|
|
195
|
+
} catch (error) {
|
|
196
|
+
const code = (error as NodeJS.ErrnoException).code;
|
|
197
|
+
if (code === "ENOENT" || code === "ENOTDIR") return [];
|
|
198
|
+
throw error;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function writeHerdrProjectPaneRoot(ownerRoot: string, projectRoot: string): void {
|
|
203
|
+
const projectRoots = [...new Set([...listHerdrProjectPaneRoots(ownerRoot), projectRoot])].sort();
|
|
204
|
+
writeAtomicJson(projectPaneRootIndexPath(ownerRoot), {
|
|
205
|
+
schemaVersion: 1,
|
|
206
|
+
kind: "herdr-project-pane-roots",
|
|
207
|
+
projectRoots,
|
|
208
|
+
} satisfies HerdrProjectPaneRootIndex);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function removeHerdrProjectPaneRoot(ownerRoot: string, projectRoot: string): void {
|
|
212
|
+
const projectRoots = listHerdrProjectPaneRoots(ownerRoot).filter((root) => root !== projectRoot);
|
|
213
|
+
const file = projectPaneRootIndexPath(ownerRoot);
|
|
214
|
+
if (projectRoots.length === 0) {
|
|
215
|
+
fs.rmSync(file, { force: true });
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
writeAtomicJson(file, {
|
|
219
|
+
schemaVersion: 1,
|
|
220
|
+
kind: "herdr-project-pane-roots",
|
|
221
|
+
projectRoots,
|
|
222
|
+
} satisfies HerdrProjectPaneRootIndex);
|
|
223
|
+
}
|
|
224
|
+
|
|
165
225
|
function parseBinding(value: unknown, strict = false): HerdrProjectPaneBinding | undefined {
|
|
166
226
|
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
167
227
|
const input = value as Partial<HerdrProjectPaneBinding>;
|
|
@@ -224,27 +284,60 @@ export function readProjectPaneBinding(projectRoot: string): ProjectPaneResult<H
|
|
|
224
284
|
return { ok: true, data: read.binding };
|
|
225
285
|
}
|
|
226
286
|
|
|
227
|
-
function
|
|
287
|
+
function herdrProjectPaneSnapshotFromBinding(binding: HerdrProjectPaneBinding, now = Date.now()): HerdrProjectPaneSnapshot {
|
|
288
|
+
return {
|
|
289
|
+
projectRoot: binding.projectRoot,
|
|
290
|
+
bindingPath: projectPaneBindingPath(binding.projectRoot),
|
|
291
|
+
paneId: binding.paneId,
|
|
292
|
+
openedAt: binding.openedAt,
|
|
293
|
+
...(binding.lastFocusedAt ? { lastFocusedAt: binding.lastFocusedAt } : {}),
|
|
294
|
+
state: "open",
|
|
295
|
+
agentStatus: "unknown",
|
|
296
|
+
ownership: "unknown",
|
|
297
|
+
safeToClose: false,
|
|
298
|
+
refreshedAt: now,
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
export function restoreHerdrProjectPaneSnapshots(state: SubagentState, projectRoots: Iterable<string>, now = Date.now()): void {
|
|
303
|
+
const restored = new Map(state.herdrProjectPanes ?? []);
|
|
304
|
+
for (const projectRoot of projectRoots) {
|
|
305
|
+
const binding = readHerdrProjectPaneBinding(projectRoot);
|
|
306
|
+
if (binding) restored.set(binding.projectRoot, herdrProjectPaneSnapshotFromBinding(binding, now));
|
|
307
|
+
}
|
|
308
|
+
state.herdrProjectPanes = restored;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function sanitizedSummary(value: unknown): string | undefined {
|
|
312
|
+
if (typeof value !== "string") return undefined;
|
|
313
|
+
const cleaned = value.replace(/[\u0000-\u001f\u007f]/g, " ").replace(/\s+/g, " ").trim();
|
|
314
|
+
return cleaned ? cleaned.slice(0, 120) : undefined;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function nestedRecord(value: unknown, key: string): Record<string, unknown> | undefined {
|
|
228
318
|
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
229
|
-
const
|
|
230
|
-
return
|
|
231
|
-
? record.pane as Record<string, unknown>
|
|
232
|
-
: record;
|
|
319
|
+
const child = (value as Record<string, unknown>)[key];
|
|
320
|
+
return child && typeof child === "object" && !Array.isArray(child) ? child as Record<string, unknown> : undefined;
|
|
233
321
|
}
|
|
234
322
|
|
|
235
|
-
function
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
323
|
+
function paneSummary(pane: Record<string, unknown>): string | undefined {
|
|
324
|
+
return sanitizedSummary(pane.summary)
|
|
325
|
+
?? sanitizedSummary(pane.state_text)
|
|
326
|
+
?? sanitizedSummary(pane.stateText)
|
|
327
|
+
?? sanitizedSummary(pane.token_summary)
|
|
328
|
+
?? sanitizedSummary(nestedRecord(pane, "tokens")?.summary)
|
|
329
|
+
?? sanitizedSummary(nestedRecord(pane, "metadata")?.summary)
|
|
330
|
+
?? sanitizedSummary(nestedRecord(nestedRecord(pane, "metadata"), "tokens")?.summary);
|
|
240
331
|
}
|
|
241
332
|
|
|
242
333
|
function projectPaneRuntime(value: unknown): ProjectPaneRuntime | undefined {
|
|
243
|
-
const pane =
|
|
244
|
-
const
|
|
334
|
+
const pane = herdrPaneRecord(value);
|
|
335
|
+
const focusTarget = herdrPaneFocusTarget(value);
|
|
336
|
+
const paneId = focusTarget.paneId;
|
|
245
337
|
if (!pane || !paneId) return undefined;
|
|
246
338
|
const text = (key: string): string | undefined => typeof pane[key] === "string" ? pane[key] as string : undefined;
|
|
247
339
|
const agentStatus = text("agent_status") ?? text("agentStatus") ?? "unknown";
|
|
340
|
+
const summary = paneSummary(pane);
|
|
248
341
|
return {
|
|
249
342
|
paneId,
|
|
250
343
|
...(text("agent") ? { agent: text("agent") } : {}),
|
|
@@ -252,12 +345,48 @@ function projectPaneRuntime(value: unknown): ProjectPaneRuntime | undefined {
|
|
|
252
345
|
...(text("cwd") ? { cwd: text("cwd") } : {}),
|
|
253
346
|
...(text("foreground_cwd") || text("foregroundCwd") ? { foregroundCwd: text("foreground_cwd") ?? text("foregroundCwd") } : {}),
|
|
254
347
|
...(typeof pane.focused === "boolean" ? { focused: pane.focused } : {}),
|
|
348
|
+
...(focusTarget.tabId ? { tabId: focusTarget.tabId } : {}),
|
|
349
|
+
...(focusTarget.workspaceId ? { workspaceId: focusTarget.workspaceId } : {}),
|
|
350
|
+
...(summary ? { summary } : {}),
|
|
255
351
|
...(text("terminal_title_stripped") || text("terminal_title") || text("terminalTitle")
|
|
256
352
|
? { terminalTitle: text("terminal_title_stripped") ?? text("terminal_title") ?? text("terminalTitle") }
|
|
257
353
|
: {}),
|
|
258
354
|
};
|
|
259
355
|
}
|
|
260
356
|
|
|
357
|
+
function herdrProjectPaneSnapshotFromStatus(data: ProjectPaneStatusData, now: number): HerdrProjectPaneSnapshot | undefined {
|
|
358
|
+
if (data.state === "absent" || !data.binding) return undefined;
|
|
359
|
+
return {
|
|
360
|
+
projectRoot: data.projectRoot,
|
|
361
|
+
bindingPath: data.bindingPath,
|
|
362
|
+
paneId: data.binding.paneId,
|
|
363
|
+
openedAt: data.binding.openedAt,
|
|
364
|
+
...(data.binding.lastFocusedAt ? { lastFocusedAt: data.binding.lastFocusedAt } : {}),
|
|
365
|
+
state: data.state,
|
|
366
|
+
agentStatus: data.runtime?.agentStatus ?? "unknown",
|
|
367
|
+
ownership: data.ownership,
|
|
368
|
+
safeToClose: data.safeToClose,
|
|
369
|
+
refreshedAt: now,
|
|
370
|
+
...(data.runtime?.summary ? { summary: data.runtime.summary } : {}),
|
|
371
|
+
...(data.runtime?.tabId ? { tabId: data.runtime.tabId } : {}),
|
|
372
|
+
...(data.runtime?.workspaceId ? { workspaceId: data.runtime.workspaceId } : {}),
|
|
373
|
+
...(data.runtime?.terminalTitle ? { terminalTitle: data.runtime.terminalTitle } : {}),
|
|
374
|
+
...(data.staleReason ? { staleReason: data.staleReason.message } : {}),
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function rememberProjectPane(state: SubagentState | undefined, data: ProjectPaneStatusData, now: number): void {
|
|
379
|
+
if (!state) return;
|
|
380
|
+
state.herdrProjectPanes ??= new Map();
|
|
381
|
+
const snapshot = herdrProjectPaneSnapshotFromStatus(data, now);
|
|
382
|
+
if (snapshot) state.herdrProjectPanes.set(data.projectRoot, snapshot);
|
|
383
|
+
else state.herdrProjectPanes.delete(data.projectRoot);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function forgetProjectPane(state: SubagentState | undefined, projectRoot: string): void {
|
|
387
|
+
state?.herdrProjectPanes?.delete(projectRoot);
|
|
388
|
+
}
|
|
389
|
+
|
|
261
390
|
function resolveProjectRoot(requested: string): ProjectPaneResult<string> {
|
|
262
391
|
const resolved = path.resolve(requested);
|
|
263
392
|
try {
|
|
@@ -379,6 +508,41 @@ function createProjectPaneManagerInternal(options: InternalProjectPaneManagerOpt
|
|
|
379
508
|
} };
|
|
380
509
|
},
|
|
381
510
|
|
|
511
|
+
async focus(input) {
|
|
512
|
+
const status = await this.status(input);
|
|
513
|
+
if (!status.ok) return projectPaneError(status.error.code, status.error.message, status.error);
|
|
514
|
+
if (status.data.state !== "open" || !status.data.binding || !status.data.runtime) {
|
|
515
|
+
return projectPaneError("PANE_GONE", `No open Herdr project pane binding exists for '${status.data.projectRoot}'.`, {
|
|
516
|
+
projectRoot: status.data.projectRoot, bindingPath: status.data.bindingPath,
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
if (status.data.ownership !== "verified") {
|
|
520
|
+
return projectPaneError("PANE_OWNERSHIP_UNVERIFIED", `Project pane '${status.data.binding.paneId}' ownership is '${status.data.ownership}' for '${status.data.projectRoot}'.`, {
|
|
521
|
+
projectRoot: status.data.projectRoot, bindingPath: status.data.bindingPath, details: status.data.runtime,
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
const focused = await focusHerdrPane(client, status.data.binding.paneId, input.signal);
|
|
525
|
+
if (!focused.ok) return projectPaneError(focused.error.code, focused.error.message, {
|
|
526
|
+
projectRoot: status.data.projectRoot, bindingPath: status.data.bindingPath, details: focused.error.details,
|
|
527
|
+
});
|
|
528
|
+
const now = (options.now?.() ?? new Date()).toISOString();
|
|
529
|
+
const binding = { ...status.data.binding, lastFocusedAt: now };
|
|
530
|
+
try {
|
|
531
|
+
writeAtomicJson(status.data.bindingPath, binding);
|
|
532
|
+
} catch (cause) {
|
|
533
|
+
return projectPaneError("BINDING_WRITE_FAILED", `Failed to update project pane binding '${status.data.bindingPath}': ${cause instanceof Error ? cause.message : String(cause)}`, {
|
|
534
|
+
projectRoot: status.data.projectRoot, bindingPath: status.data.bindingPath, details: fileSystemErrorDetails(cause),
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
return { ok: true, data: {
|
|
538
|
+
...common(status.data.projectRoot),
|
|
539
|
+
binding,
|
|
540
|
+
runtime: status.data.runtime,
|
|
541
|
+
ownership: status.data.ownership,
|
|
542
|
+
focused: focused.data,
|
|
543
|
+
} };
|
|
544
|
+
},
|
|
545
|
+
|
|
382
546
|
async open(input) {
|
|
383
547
|
const root = resolveProjectRoot(input.cwd);
|
|
384
548
|
if (!root.ok) return root;
|
|
@@ -414,7 +578,7 @@ function createProjectPaneManagerInternal(options: InternalProjectPaneManagerOpt
|
|
|
414
578
|
splitArgs.push(input.focus === true ? "--focus" : "--no-focus");
|
|
415
579
|
const split = await client.run(splitArgs, { timeoutMs: 15_000, signal: input.signal });
|
|
416
580
|
if (!split.ok) return projectPaneError(split.error.code, split.error.message, { projectRoot, details: split.error.details });
|
|
417
|
-
const paneId =
|
|
581
|
+
const paneId = herdrPaneFocusTarget(split.data).paneId;
|
|
418
582
|
if (!paneId) return projectPaneError(options.legacyToolCompatibility ? "PANE_GONE" : "INVALID_PANE_RESPONSE", "Herdr pane split returned no pane id.", { projectRoot, details: split.data });
|
|
419
583
|
const startupMessage = input.message?.trim();
|
|
420
584
|
const command = projectPaneCommand(startupMessage);
|
|
@@ -466,29 +630,26 @@ function createProjectPaneManagerInternal(options: InternalProjectPaneManagerOpt
|
|
|
466
630
|
if (!bindingResult.ok) return bindingResult;
|
|
467
631
|
const existing = bindingResult.data;
|
|
468
632
|
if (!existing) return { ok: true, data: { ...common(projectRoot), disposition: "absent" } };
|
|
469
|
-
|
|
470
|
-
if (
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
if (
|
|
474
|
-
|
|
475
|
-
if (!removed.ok) return removed;
|
|
476
|
-
return { ok: true, data: { ...common(projectRoot), disposition: "stale-binding-removed", binding: existing } };
|
|
477
|
-
}
|
|
478
|
-
return { ok: false, error: { ...live.error, projectRoot, bindingPath: projectPaneBindingPath(projectRoot) } };
|
|
479
|
-
}
|
|
480
|
-
runtime = live.data;
|
|
481
|
-
const ownership = projectPaneOwnership(runtime, existing, projectRoot);
|
|
482
|
-
if (ownership !== "verified") {
|
|
483
|
-
return projectPaneError("PANE_OWNERSHIP_UNVERIFIED", `Project pane '${existing.paneId}' ownership is '${ownership}' for '${projectRoot}'.`, {
|
|
484
|
-
projectRoot, bindingPath: projectPaneBindingPath(projectRoot), details: runtime,
|
|
485
|
-
});
|
|
486
|
-
}
|
|
487
|
-
if (runtime.agentStatus !== "idle") {
|
|
488
|
-
return projectPaneError("PANE_NOT_IDLE", `Project pane '${existing.paneId}' is '${runtime.agentStatus}', not explicitly idle.`, {
|
|
489
|
-
projectRoot, bindingPath: projectPaneBindingPath(projectRoot), details: runtime,
|
|
490
|
-
});
|
|
633
|
+
const live = await inspectPane(client, existing.paneId, input.signal);
|
|
634
|
+
if (!live.ok) {
|
|
635
|
+
if (live.error.code === "NOT_FOUND" || live.error.code === "PANE_GONE") {
|
|
636
|
+
const removed = removeProjectPaneBinding(projectRoot);
|
|
637
|
+
if (!removed.ok) return removed;
|
|
638
|
+
return { ok: true, data: { ...common(projectRoot), disposition: "stale-binding-removed", binding: existing } };
|
|
491
639
|
}
|
|
640
|
+
return { ok: false, error: { ...live.error, projectRoot, bindingPath: projectPaneBindingPath(projectRoot) } };
|
|
641
|
+
}
|
|
642
|
+
const runtime = live.data;
|
|
643
|
+
const ownership = projectPaneOwnership(runtime, existing, projectRoot);
|
|
644
|
+
if (ownership !== "verified") {
|
|
645
|
+
return projectPaneError("PANE_OWNERSHIP_UNVERIFIED", `Project pane '${existing.paneId}' ownership is '${ownership}' for '${projectRoot}'.`, {
|
|
646
|
+
projectRoot, bindingPath: projectPaneBindingPath(projectRoot), details: runtime,
|
|
647
|
+
});
|
|
648
|
+
}
|
|
649
|
+
if (runtime.agentStatus !== "idle") {
|
|
650
|
+
return projectPaneError("PANE_NOT_IDLE", `Project pane '${existing.paneId}' is '${runtime.agentStatus}', not explicitly idle.`, {
|
|
651
|
+
projectRoot, bindingPath: projectPaneBindingPath(projectRoot), details: runtime,
|
|
652
|
+
});
|
|
492
653
|
}
|
|
493
654
|
const closed = await client.run(["pane", "close", existing.paneId], { timeoutMs: 10_000, signal: input.signal });
|
|
494
655
|
if (!closed.ok && closed.error.code !== "NOT_FOUND" && closed.error.code !== "PANE_GONE") {
|
|
@@ -518,12 +679,19 @@ export async function closeProjectPane(options: CloseProjectPaneOptions): Promis
|
|
|
518
679
|
return createProjectPaneManager().close(options);
|
|
519
680
|
}
|
|
520
681
|
|
|
682
|
+
export async function focusProjectPane(options: GetProjectPaneStatusOptions): Promise<ProjectPaneResult<FocusProjectPaneData>> {
|
|
683
|
+
return createProjectPaneManager().focus(options);
|
|
684
|
+
}
|
|
685
|
+
|
|
521
686
|
export async function handleHerdrProjectPaneAction(action: HerdrProjectPaneAction, params: ProjectPaneParams, deps: ProjectPaneDeps): Promise<AgentToolResult<Details>> {
|
|
522
687
|
const requested = params.cwd?.trim() || deps.cwd;
|
|
688
|
+
const ownerRoot = path.resolve(deps.cwd);
|
|
523
689
|
const manager = createProjectPaneManagerInternal({ client: deps.client, now: deps.now, legacyToolCompatibility: true });
|
|
690
|
+
const remember = (data: ProjectPaneStatusData) => rememberProjectPane(deps.state, data, deps.now?.().getTime() ?? Date.now());
|
|
524
691
|
if (action === "project.status") {
|
|
525
692
|
const status = await manager.status({ cwd: requested, signal: deps.signal });
|
|
526
693
|
if (!status.ok) return toolResult(formatProjectPaneError(status.error), true);
|
|
694
|
+
remember(status.data);
|
|
527
695
|
if (status.data.state === "absent") return toolResult(`No Herdr project pane binding exists for ${status.data.projectRoot}.`);
|
|
528
696
|
if (status.data.state === "stale") {
|
|
529
697
|
const reason = status.data.staleReason!;
|
|
@@ -532,15 +700,31 @@ export async function handleHerdrProjectPaneAction(action: HerdrProjectPaneActio
|
|
|
532
700
|
return toolResult(`Herdr project pane ${status.data.binding!.paneId} is open for ${status.data.projectRoot}.\nBinding: ${status.data.bindingPath}`);
|
|
533
701
|
}
|
|
534
702
|
if (action === "project.close") {
|
|
535
|
-
const closed = await manager.close({ cwd: requested, signal: deps.signal });
|
|
703
|
+
const closed = await manager.close({ cwd: requested, requireIdle: true, signal: deps.signal });
|
|
536
704
|
if (!closed.ok) return toolResult(formatProjectPaneError(closed.error), true);
|
|
705
|
+
forgetProjectPane(deps.state, closed.data.projectRoot);
|
|
706
|
+
try { removeHerdrProjectPaneRoot(ownerRoot, closed.data.projectRoot); } catch (error) {
|
|
707
|
+
return toolResult(`Herdr project pane error (BINDING_REMOVE_FAILED): Failed to remove project pane root index '${projectPaneRootIndexPath(ownerRoot)}': ${error instanceof Error ? error.message : String(error)}`, true);
|
|
708
|
+
}
|
|
537
709
|
if (closed.data.disposition === "absent") return toolResult(`No Herdr project pane binding exists for ${closed.data.projectRoot}.`);
|
|
538
710
|
return toolResult(`Closed Herdr project pane ${closed.data.binding!.paneId} for ${closed.data.projectRoot}.`);
|
|
539
711
|
}
|
|
540
712
|
const opened = await manager.open({ cwd: requested, message: params.message, focus: params.focus, signal: deps.signal });
|
|
541
713
|
if (!opened.ok) return toolResult(formatProjectPaneError(opened.error), true);
|
|
714
|
+
try { writeHerdrProjectPaneRoot(ownerRoot, opened.data.projectRoot); } catch (error) {
|
|
715
|
+
return toolResult(`Herdr project pane error (BINDING_WRITE_FAILED): Failed to persist project pane root index '${projectPaneRootIndexPath(ownerRoot)}': ${error instanceof Error ? error.message : String(error)}`, true);
|
|
716
|
+
}
|
|
717
|
+
const status = await manager.status({ cwd: opened.data.projectRoot, signal: deps.signal });
|
|
718
|
+
if (status.ok) remember(status.data);
|
|
542
719
|
if (opened.data.disposition === "already-open") {
|
|
543
|
-
|
|
720
|
+
if (params.focus) {
|
|
721
|
+
const focused = await manager.focus({ cwd: opened.data.projectRoot, signal: deps.signal });
|
|
722
|
+
if (!focused.ok) return toolResult(`Herdr project pane ${opened.data.binding.paneId} is already open for ${opened.data.projectRoot}. ${formatProjectPaneError(focused.error)}`, true);
|
|
723
|
+
const focusedStatus = await manager.status({ cwd: opened.data.projectRoot, signal: deps.signal });
|
|
724
|
+
if (focusedStatus.ok) remember(focusedStatus.data);
|
|
725
|
+
return toolResult(`Herdr project pane ${opened.data.binding.paneId} is already open for ${opened.data.projectRoot}. Focused ${focused.data.focused.tabId ? `tab ${focused.data.focused.tabId}` : `workspace ${focused.data.focused.workspaceId}`}.`);
|
|
726
|
+
}
|
|
727
|
+
return toolResult(`Herdr project pane ${opened.data.binding.paneId} is already open for ${opened.data.projectRoot}.`);
|
|
544
728
|
}
|
|
545
729
|
return toolResult(`Opened Herdr project pane ${opened.data.binding.paneId} for ${opened.data.projectRoot}. The pane runs its own Pi session; subagents launched there belong to that project.`);
|
|
546
730
|
}
|
|
@@ -33,6 +33,8 @@ export interface HerdrStatusBridgeOptions {
|
|
|
33
33
|
env?: Record<string, string | undefined>;
|
|
34
34
|
/** Current authoritative active-run projection, used before TTL refresh. */
|
|
35
35
|
getRuns?: () => Iterable<HerdrStatusRun>;
|
|
36
|
+
/** Current project panes opened by this Pi session. Views are excluded. */
|
|
37
|
+
getProjectPaneCount?: () => number;
|
|
36
38
|
runHerdr: (args: readonly string[]) => void | Promise<void>;
|
|
37
39
|
ttlMs?: number;
|
|
38
40
|
refreshMs?: number;
|
|
@@ -109,12 +111,27 @@ export function registerHerdrStatusBridge(options: HerdrStatusBridgeOptions): He
|
|
|
109
111
|
let drainPromise = Promise.resolve();
|
|
110
112
|
let refreshTimer: ReturnType<typeof setInterval> | undefined;
|
|
111
113
|
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
+
const activeAgentNames = (): string[] => [...new Set([...runs.values()].flatMap((run) => run.agents?.length ? run.agents : run.agent ? [run.agent] : []))];
|
|
115
|
+
const activeSubagentCount = (): number => [...runs.values()].reduce((total, run) => total + Math.max(1, run.agents?.length ?? (run.agent ? 1 : 0)), 0);
|
|
116
|
+
|
|
117
|
+
const label = (includeAttention = false): string => {
|
|
118
|
+
const agents = activeAgentNames();
|
|
119
|
+
const activeCount = activeSubagentCount();
|
|
114
120
|
const who = agents.length > 0
|
|
115
121
|
? ` (${agents.slice(0, 3).join(", ")}${agents.length > 3 ? ", …" : ""})`
|
|
116
122
|
: "";
|
|
117
|
-
|
|
123
|
+
const panes = Math.max(0, options.getProjectPaneCount?.() ?? 0);
|
|
124
|
+
const paneText = panes > 0 ? ` · ${panes} pane${panes === 1 ? "" : "s"}` : "";
|
|
125
|
+
const attention = includeAttention && attentionLabels.size > 0 ? " ⚠" : "";
|
|
126
|
+
return `⏳ ${activeCount} subagent${activeCount === 1 ? "" : "s"}${who}${paneText}${attention}`;
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const titleSuffix = (): string | undefined => {
|
|
130
|
+
if (runs.size === 0) return undefined;
|
|
131
|
+
const agentNames = activeAgentNames();
|
|
132
|
+
const activeCount = activeSubagentCount();
|
|
133
|
+
const target = activeCount === 1 && agentNames.length === 1 ? agentNames[0]! : String(activeCount);
|
|
134
|
+
return `⏳${target}${attentionLabels.size > 0 ? "⚠" : ""}`;
|
|
118
135
|
};
|
|
119
136
|
|
|
120
137
|
const enqueue = (args: readonly string[]): void => {
|
|
@@ -150,11 +167,13 @@ export function registerHerdrStatusBridge(options: HerdrStatusBridgeOptions): He
|
|
|
150
167
|
"--applies-to-source", "herdr:pi",
|
|
151
168
|
"--clear-state-labels",
|
|
152
169
|
"--clear-token", "summary",
|
|
170
|
+
"--clear-token", "title-suffix",
|
|
153
171
|
"--seq", seq,
|
|
154
172
|
]);
|
|
155
173
|
return;
|
|
156
174
|
}
|
|
157
|
-
const text = label();
|
|
175
|
+
const text = label(true);
|
|
176
|
+
const suffix = titleSuffix();
|
|
158
177
|
published = true;
|
|
159
178
|
enqueue([
|
|
160
179
|
"pane", "report-metadata", paneId,
|
|
@@ -165,6 +184,7 @@ export function registerHerdrStatusBridge(options: HerdrStatusBridgeOptions): He
|
|
|
165
184
|
"--state-label", `done=${text}`,
|
|
166
185
|
"--state-label", `working=${text}`,
|
|
167
186
|
"--token", `summary=${text}`,
|
|
187
|
+
...(suffix ? ["--token", `title-suffix=${suffix}`] : []),
|
|
168
188
|
"--ttl-ms", String(ttlMs),
|
|
169
189
|
"--seq", seq,
|
|
170
190
|
]);
|
|
@@ -221,6 +241,7 @@ export function registerHerdrStatusBridge(options: HerdrStatusBridgeOptions): He
|
|
|
221
241
|
const clearAttention = (): void => {
|
|
222
242
|
attentionLabels.clear();
|
|
223
243
|
syncBlocked();
|
|
244
|
+
publish();
|
|
224
245
|
};
|
|
225
246
|
|
|
226
247
|
const raiseAttention = (runId: string, labelText: string): void => {
|
|
@@ -228,6 +249,7 @@ export function registerHerdrStatusBridge(options: HerdrStatusBridgeOptions): He
|
|
|
228
249
|
acknowledgedAttention.delete(runId);
|
|
229
250
|
attentionLabels.set(runId, labelText);
|
|
230
251
|
syncBlocked();
|
|
252
|
+
publish();
|
|
231
253
|
};
|
|
232
254
|
|
|
233
255
|
const replaceRuns = (nextRuns: Iterable<HerdrStatusRun>): void => {
|