@giladbarnea/pi-user-agents 0.0.8 → 0.0.10
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/INTERNALS.md +11 -1
- package/PARSER_SPEC.md +10 -2
- package/README.md +20 -2
- package/agent-viewer.ts +83 -32
- package/attach.ts +35 -4
- package/command-line.ts +47 -5
- package/context-meter.ts +5 -1
- package/herdr.ts +92 -0
- package/index.ts +2 -1
- package/package.json +5 -4
- package/runner.ts +125 -22
- package/screenshots/below-editor-widget-2.png +0 -0
- package/screenshots/editor-with-autocomplete.png +0 -0
- package/screenshots/overlay-2.png +0 -0
- package/shared.ts +28 -0
- package/widget.ts +162 -45
package/herdr.ts
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import * as childProcess from "node:child_process";
|
|
2
|
+
|
|
3
|
+
/** Whether this Pi runs inside a herdr pane, so new panes can be split from it. */
|
|
4
|
+
export function insideHerdr(): boolean {
|
|
5
|
+
return process.env.HERDR_ENV === "1";
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The herdr agent name for a child session. Herdr names must be unique among live agents and
|
|
10
|
+
* match [a-z][a-z0-9_-]{0,31}; the first 13 characters of a v7 session id hold its complete
|
|
11
|
+
* millisecond timestamp, so two children never share one.
|
|
12
|
+
*
|
|
13
|
+
* @example herdrAgentName("0199c4f2-8b1a-7c3d-9e05-6a2f18d7b4ce") // "agent-0199c4f2-8b1a"
|
|
14
|
+
*/
|
|
15
|
+
export function herdrAgentName(sessionId: string): string {
|
|
16
|
+
return `agent-${sessionId.slice(0, 13)}`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Split the calling pane and start `pi <piArgs>` in the new pane; resolves with the pane id once pi is ready. */
|
|
20
|
+
export async function openPiInHerdrPane(
|
|
21
|
+
cwd: string,
|
|
22
|
+
agentName: string,
|
|
23
|
+
piArgs: readonly string[],
|
|
24
|
+
): Promise<string> {
|
|
25
|
+
const paneId = await splitPane(cwd);
|
|
26
|
+
await startPi(paneId, agentName, piArgs);
|
|
27
|
+
return paneId;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Split the calling pane without taking the user's focus; resolves with the new pane's id. */
|
|
31
|
+
export async function splitPane(cwd: string): Promise<string> {
|
|
32
|
+
const direction = await splitDirection();
|
|
33
|
+
const split = (await herdr([
|
|
34
|
+
"pane",
|
|
35
|
+
"split",
|
|
36
|
+
"--current",
|
|
37
|
+
"--direction",
|
|
38
|
+
direction,
|
|
39
|
+
"--cwd",
|
|
40
|
+
cwd,
|
|
41
|
+
"--no-focus",
|
|
42
|
+
])) as { pane: { pane_id: string } };
|
|
43
|
+
return split.pane.pane_id;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Start `pi <piArgs>` in a shell pane as a named herdr agent; resolves once pi is ready for input. */
|
|
47
|
+
export async function startPi(
|
|
48
|
+
paneId: string,
|
|
49
|
+
agentName: string,
|
|
50
|
+
piArgs: readonly string[],
|
|
51
|
+
): Promise<void> {
|
|
52
|
+
await herdr(["agent", "start", agentName, "--kind", "pi", "--pane", paneId, "--", ...piArgs]);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
type PaneLayout = {
|
|
56
|
+
panes: Array<{ pane_id: string; rect: { width: number; height: number } }>;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/** A wide pane splits to the right and a narrow or tall one down, so neither half turns unusable. */
|
|
60
|
+
async function splitDirection(): Promise<"right" | "down"> {
|
|
61
|
+
const { layout } = (await herdr(["pane", "layout", "--current"])) as { layout: PaneLayout };
|
|
62
|
+
const callerRect = layout.panes.find((pane) => pane.pane_id === process.env.HERDR_PANE_ID)?.rect;
|
|
63
|
+
if (!callerRect) return "right";
|
|
64
|
+
return callerRect.width >= callerRect.height * 2 ? "right" : "down";
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Run one herdr CLI command and return its JSON `result`. Rejects with herdr's own message. */
|
|
68
|
+
function herdr(args: readonly string[]): Promise<unknown> {
|
|
69
|
+
return new Promise((resolve, reject) => {
|
|
70
|
+
childProcess.execFile("herdr", [...args], (error, stdout, stderr) => {
|
|
71
|
+
if (error) {
|
|
72
|
+
reject(new Error(herdrFailureMessage(error, stdout, stderr)));
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
try {
|
|
76
|
+
resolve((JSON.parse(stdout) as { result: unknown }).result);
|
|
77
|
+
} catch (parseError) {
|
|
78
|
+
reject(parseError);
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Herdr reports server errors as JSON on stderr and usage errors as plain text. */
|
|
85
|
+
function herdrFailureMessage(error: Error, stdout: string, stderr: string): string {
|
|
86
|
+
const text = (stderr || stdout || error.message).trim();
|
|
87
|
+
try {
|
|
88
|
+
return (JSON.parse(text) as { error?: { message?: string } }).error?.message ?? text;
|
|
89
|
+
} catch {
|
|
90
|
+
return text;
|
|
91
|
+
}
|
|
92
|
+
}
|
package/index.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type {
|
|
|
2
2
|
MessageEndEvent,
|
|
3
3
|
MessageEndEventResult,
|
|
4
4
|
} from "@earendil-works/pi-coding-agent";
|
|
5
|
-
import { handleAttachCommand } from "./attach.js";
|
|
5
|
+
import { createHerdrDelivery, handleAttachCommand } from "./attach.js";
|
|
6
6
|
import { registerAgentAutocomplete } from "./autocomplete.js";
|
|
7
7
|
import { createRebaseDelivery, handleAgentCommand } from "./runner.js";
|
|
8
8
|
import type {
|
|
@@ -25,6 +25,7 @@ export default function userAgent(pi: ExtensionAPI): void {
|
|
|
25
25
|
(message) => pi.sendMessage(message, { triggerTurn: true }),
|
|
26
26
|
(sessionId) => pi.appendEntry<DetachedEntryData>(DETACHED_ENTRY_TYPE, { sessionId }),
|
|
27
27
|
createRebaseDelivery(pi, () => mainSessionContext),
|
|
28
|
+
createHerdrDelivery(() => mainSessionContext),
|
|
28
29
|
);
|
|
29
30
|
let shuttingDown = false;
|
|
30
31
|
let nextAgentNumber = 0;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@giladbarnea/pi-user-agents",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.10",
|
|
4
4
|
"description": "Run, view, and control background Pi agents.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"test": "bun test"
|
|
34
34
|
},
|
|
35
35
|
"peerDependencies": {
|
|
36
|
-
"@earendil-works/pi-coding-agent": "
|
|
36
|
+
"@earendil-works/pi-coding-agent": ">=1.1.0",
|
|
37
37
|
"@earendil-works/pi-tui": "*"
|
|
38
38
|
},
|
|
39
39
|
"publishConfig": {
|
|
@@ -45,7 +45,8 @@
|
|
|
45
45
|
]
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
|
-
"@earendil-works/pi-
|
|
49
|
-
"@earendil-works/pi-
|
|
48
|
+
"@earendil-works/pi-ai": "1.1.0",
|
|
49
|
+
"@earendil-works/pi-coding-agent": "1.1.0",
|
|
50
|
+
"@earendil-works/pi-tui": "0.85.1"
|
|
50
51
|
}
|
|
51
52
|
}
|
package/runner.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { existsSync, writeFileSync } from "node:fs";
|
|
1
2
|
import * as os from "node:os";
|
|
2
3
|
import * as path from "node:path";
|
|
3
4
|
import {
|
|
@@ -16,7 +17,12 @@ import {
|
|
|
16
17
|
SessionManager,
|
|
17
18
|
SettingsManager,
|
|
18
19
|
} from "@earendil-works/pi-coding-agent";
|
|
19
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
type AgentValueValidator,
|
|
22
|
+
forwardedArgsForPane,
|
|
23
|
+
parseAgentCommand,
|
|
24
|
+
} from "./command-line.js";
|
|
25
|
+
import { herdrAgentName, insideHerdr, openPiInHerdrPane } from "./herdr.js";
|
|
20
26
|
import { conversationFingerprint, mainContextFingerprint } from "./rebase.js";
|
|
21
27
|
import type {
|
|
22
28
|
AgentCommandName,
|
|
@@ -39,6 +45,7 @@ import {
|
|
|
39
45
|
errorMessage,
|
|
40
46
|
formatModel,
|
|
41
47
|
formatModelLabel,
|
|
48
|
+
herdrPaneLabel,
|
|
42
49
|
logSteering,
|
|
43
50
|
REBASED_ENTRY_TYPE,
|
|
44
51
|
} from "./shared.js";
|
|
@@ -91,20 +98,18 @@ async function startUserAgent(
|
|
|
91
98
|
ctx: ExtensionCommandContext,
|
|
92
99
|
): Promise<void> {
|
|
93
100
|
const parsed = parseAgentCommand(args, command);
|
|
101
|
+
if (parsed.herdr && !insideHerdr())
|
|
102
|
+
throw new Error(`/${command} -h needs Pi to run inside a herdr pane`);
|
|
94
103
|
const parsedForwardedArgs = parseForwardedArgs(parsed.forwardedArgs);
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
buildChildResourceLoaderOptions(parsedForwardedArgs, ctx.cwd),
|
|
98
|
-
);
|
|
99
|
-
const forwarded = resolveForwardedOptions(parsedForwardedArgs, services.modelRuntime);
|
|
104
|
+
const modelRuntime = parentModelRuntime(ctx);
|
|
105
|
+
const forwarded = resolveForwardedOptions(parsedForwardedArgs, modelRuntime);
|
|
100
106
|
const selectedModel = forwarded.model ?? ctx.model;
|
|
101
107
|
if (!selectedModel) throw new Error("No current model is selected; pass -m MODELNAME");
|
|
102
|
-
const model =
|
|
103
|
-
services.modelRuntime.getModel(selectedModel.provider, selectedModel.id) ?? selectedModel;
|
|
108
|
+
const model = modelRuntime.getModel(selectedModel.provider, selectedModel.id) ?? selectedModel;
|
|
104
109
|
const thinkingLevel = forwarded.thinkingLevel ?? pi.getThinkingLevel();
|
|
105
110
|
const inheritedMessages = parsed.isolate ? [] : buildInheritedMessages(ctx);
|
|
106
111
|
// A real session file from birth: the child outlives the widget row and stays resumable.
|
|
107
|
-
const childSessionManager = SessionManager.create(
|
|
112
|
+
const childSessionManager = SessionManager.create(ctx.cwd, undefined, {
|
|
108
113
|
parentSession: ctx.sessionManager.getSessionFile(),
|
|
109
114
|
});
|
|
110
115
|
// The dispatch record: /agent-attach restores tool and resource options from it later.
|
|
@@ -123,11 +128,32 @@ async function startUserAgent(
|
|
|
123
128
|
conversationFingerprint(inheritedMessages),
|
|
124
129
|
);
|
|
125
130
|
|
|
126
|
-
runningAgents.add(runningAgent);
|
|
127
|
-
logSteering(runningAgent.id, "agent-created", { command, taskLength: parsed.task.length });
|
|
128
131
|
if (ctx.hasUI) {
|
|
129
132
|
widget.setUI(ctx.ui);
|
|
130
133
|
for (const warning of parsed.warnings) ctx.ui.notify(warning, "warning");
|
|
134
|
+
}
|
|
135
|
+
if (parsed.herdr) {
|
|
136
|
+
await dispatchToHerdrPane(
|
|
137
|
+
ctx,
|
|
138
|
+
widget,
|
|
139
|
+
childSessionManager,
|
|
140
|
+
inheritedMessages,
|
|
141
|
+
model,
|
|
142
|
+
thinkingLevel,
|
|
143
|
+
parsed,
|
|
144
|
+
runningAgent,
|
|
145
|
+
);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// The pane's Pi loads its own resources; only a background child needs them here.
|
|
150
|
+
const services = await createChildServices(
|
|
151
|
+
ctx,
|
|
152
|
+
buildChildResourceLoaderOptions(parsedForwardedArgs, ctx.cwd),
|
|
153
|
+
);
|
|
154
|
+
runningAgents.add(runningAgent);
|
|
155
|
+
logSteering(runningAgent.id, "agent-created", { command, taskLength: parsed.task.length });
|
|
156
|
+
if (ctx.hasUI) {
|
|
131
157
|
ctx.ui.notify(formatStartNotification(runningAgent), "info");
|
|
132
158
|
widget.ensureTimer();
|
|
133
159
|
widget.update();
|
|
@@ -153,6 +179,64 @@ async function startUserAgent(
|
|
|
153
179
|
});
|
|
154
180
|
}
|
|
155
181
|
|
|
182
|
+
/**
|
|
183
|
+
* An agent born in a herdr pane: the child file is flushed here, then the pane's Pi resumes it
|
|
184
|
+
* with the dispatch options as CLI flags and takes the preambled task as its first prompt.
|
|
185
|
+
* Nothing runs in this process, so the widget row is a pointer at the pane from birth.
|
|
186
|
+
*/
|
|
187
|
+
async function dispatchToHerdrPane(
|
|
188
|
+
ctx: ExtensionCommandContext,
|
|
189
|
+
widget: UserAgentWidget,
|
|
190
|
+
childSessionManager: SessionManager,
|
|
191
|
+
inheritedMessages: AgentMessage[],
|
|
192
|
+
model: Model,
|
|
193
|
+
thinkingLevel: ThinkingLevel,
|
|
194
|
+
parsed: ParsedAgentCommand,
|
|
195
|
+
runningAgent: RunningAgent,
|
|
196
|
+
): Promise<void> {
|
|
197
|
+
persistMessages(childSessionManager, inheritedMessages, []);
|
|
198
|
+
// Pi resumes a session with the model of whichever comes last on the path, a model_change entry
|
|
199
|
+
// or an assistant message, and writes model_change itself only on its empty-session path. Record
|
|
200
|
+
// both after the snapshot, or a later /agent-attach or /resume takes main's last answer's model.
|
|
201
|
+
childSessionManager.appendModelChange(model.provider, model.id);
|
|
202
|
+
childSessionManager.appendThinkingLevelChange(thinkingLevel);
|
|
203
|
+
const sessionFile = flushSessionFile(childSessionManager);
|
|
204
|
+
// Explicit on the CLI too: an isolated child's file has no messages, and pi then takes the
|
|
205
|
+
// flags over the file.
|
|
206
|
+
const paneId = await openPiInHerdrPane(ctx.cwd, herdrAgentName(runningAgent.sessionId), [
|
|
207
|
+
...forwardedArgsForPane(parsed.forwardedArgs),
|
|
208
|
+
"--provider",
|
|
209
|
+
model.provider,
|
|
210
|
+
"--model",
|
|
211
|
+
model.id,
|
|
212
|
+
"--thinking",
|
|
213
|
+
thinkingLevel,
|
|
214
|
+
"--session",
|
|
215
|
+
sessionFile,
|
|
216
|
+
PANE_DISPATCH_PREAMBLE + parsed.task,
|
|
217
|
+
]);
|
|
218
|
+
logSteering(runningAgent.id, "agent-dispatched-to-herdr", { paneId });
|
|
219
|
+
if (!ctx.hasUI) return;
|
|
220
|
+
widget.addHerdrDispatch(runningAgent, paneId);
|
|
221
|
+
ctx.ui.notify(`Started /${runningAgent.command} in ${herdrPaneLabel(paneId)}`, "info");
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Make sure a child session file is on disk. Pi defers the file until the first assistant
|
|
226
|
+
* message, so a child whose inherited context has none is still in memory only, while a Pi in
|
|
227
|
+
* another pane can only resume a file that exists.
|
|
228
|
+
*/
|
|
229
|
+
export function flushSessionFile(sessionManager: SessionManager): string {
|
|
230
|
+
const sessionFile = sessionManager.getSessionFile();
|
|
231
|
+
if (!sessionFile) throw new Error("The child session has no file path");
|
|
232
|
+
if (existsSync(sessionFile)) return sessionFile;
|
|
233
|
+
const lines = [sessionManager.getHeader(), ...sessionManager.getEntries()].map(
|
|
234
|
+
(entry) => `${JSON.stringify(entry)}\n`,
|
|
235
|
+
);
|
|
236
|
+
writeFileSync(sessionFile, lines.join(""), { flag: "wx" });
|
|
237
|
+
return sessionFile;
|
|
238
|
+
}
|
|
239
|
+
|
|
156
240
|
function createRunningAgent(
|
|
157
241
|
sequenceNumber: number,
|
|
158
242
|
sessionId: string,
|
|
@@ -296,13 +380,19 @@ export function createChildServices(
|
|
|
296
380
|
cwd: ctx.cwd,
|
|
297
381
|
agentDir,
|
|
298
382
|
settingsManager,
|
|
299
|
-
|
|
300
|
-
// extension-registered providers (their process-global dedup guard) available to the child.
|
|
301
|
-
modelRuntime: (ctx.modelRegistry as unknown as { runtime: ModelRuntime }).runtime,
|
|
383
|
+
modelRuntime: parentModelRuntime(ctx),
|
|
302
384
|
resourceLoaderOptions,
|
|
303
385
|
});
|
|
304
386
|
}
|
|
305
387
|
|
|
388
|
+
/**
|
|
389
|
+
* The parent's ModelRuntime: the extension facade wraps it as a plain property. Sharing it keeps
|
|
390
|
+
* extension-registered providers (their process-global dedup guard) available to the child.
|
|
391
|
+
*/
|
|
392
|
+
function parentModelRuntime(ctx: ExtensionCommandContext): ModelRuntime {
|
|
393
|
+
return (ctx.modelRegistry as unknown as { runtime: ModelRuntime }).runtime;
|
|
394
|
+
}
|
|
395
|
+
|
|
306
396
|
/** Parse forwarded pi CLI tokens once before model and session resolution. */
|
|
307
397
|
export function parseForwardedArgs(forwardedArgs: string[]): Args {
|
|
308
398
|
const parsed = parseArgs(forwardedArgs);
|
|
@@ -516,9 +606,13 @@ export function reportAgentFailure(
|
|
|
516
606
|
runningAgent.status = "posted";
|
|
517
607
|
}
|
|
518
608
|
|
|
519
|
-
/** Every dispatch prefixes its task with this; attach recognizes it as the child's dispatch boundary. */
|
|
609
|
+
/** Every background dispatch prefixes its task with this; attach recognizes it as the child's dispatch boundary. */
|
|
520
610
|
export const DISPATCH_PREAMBLE =
|
|
521
611
|
"You are running in an ephemeral, forked background process now, concurrently with the main session. ";
|
|
612
|
+
/** The -h dispatch's preamble: that child is interactive, and the user may talk to it in its pane. */
|
|
613
|
+
export const PANE_DISPATCH_PREAMBLE =
|
|
614
|
+
"You are running in a forked session now, in a herdr pane beside the main session. The user may talk to you here. ";
|
|
615
|
+
export const DISPATCH_PREAMBLES = [DISPATCH_PREAMBLE, PANE_DISPATCH_PREAMBLE] as const;
|
|
522
616
|
|
|
523
617
|
/** Run turns on an initialized child session until it posts, retires, or shuts down. The initial instruction is sent verbatim. */
|
|
524
618
|
export async function runChildTurns(
|
|
@@ -531,7 +625,9 @@ export async function runChildTurns(
|
|
|
531
625
|
): Promise<void> {
|
|
532
626
|
let instruction = initialInstruction;
|
|
533
627
|
while (true) {
|
|
628
|
+
if (runningAgent.aborted) return;
|
|
534
629
|
const turnMessageStart = session.agent.state.messages.length;
|
|
630
|
+
runningAgent.runAborted = false;
|
|
535
631
|
if (!runningAgent.interruptRequested) {
|
|
536
632
|
logSteering(runningAgent.id, "turn-prompt-started", {
|
|
537
633
|
streaming: session.isStreaming,
|
|
@@ -539,8 +635,13 @@ export async function runChildTurns(
|
|
|
539
635
|
});
|
|
540
636
|
await session.prompt(instruction);
|
|
541
637
|
}
|
|
542
|
-
if (runningAgent.
|
|
543
|
-
|
|
638
|
+
if (runningAgent.aborted) return;
|
|
639
|
+
if (runningAgent.interruptRequested || runningAgent.runAborted) {
|
|
640
|
+
const response = interruptedTurnResponse(
|
|
641
|
+
session,
|
|
642
|
+
turnMessageStart,
|
|
643
|
+
runningAgent.interruptRequested ? "Interrupted by user." : "Run cancelled.",
|
|
644
|
+
);
|
|
544
645
|
runningAgent.interruptRequested = false;
|
|
545
646
|
runningAgent.mainContextState = "separate";
|
|
546
647
|
runningAgent.status = "done-waiting-to-post";
|
|
@@ -599,14 +700,14 @@ export async function runChildTurns(
|
|
|
599
700
|
}
|
|
600
701
|
}
|
|
601
702
|
|
|
602
|
-
function interruptedTurnResponse(session: AgentSession, turnMessageStart: number): string {
|
|
703
|
+
function interruptedTurnResponse(session: AgentSession, turnMessageStart: number, reason: string): string {
|
|
603
704
|
const assistantMessages = session.agent.state.messages
|
|
604
705
|
.slice(turnMessageStart)
|
|
605
706
|
.filter((message) => message.role === "assistant");
|
|
606
707
|
const lastAssistantMessage = assistantMessages.at(-1);
|
|
607
708
|
const partialResponse = lastAssistantMessage ? assistantText(lastAssistantMessage).trim() : "";
|
|
608
|
-
if (!partialResponse) return
|
|
609
|
-
return
|
|
709
|
+
if (!partialResponse) return reason;
|
|
710
|
+
return `${reason}\n\n${partialResponse}`;
|
|
610
711
|
}
|
|
611
712
|
|
|
612
713
|
async function createChildSession(
|
|
@@ -741,8 +842,10 @@ export function subscribeToChildSession(
|
|
|
741
842
|
streaming: session.isStreaming,
|
|
742
843
|
messageCount: event.messages.length,
|
|
743
844
|
});
|
|
744
|
-
if (event.type === "agent_settled")
|
|
745
|
-
|
|
845
|
+
if (event.type === "agent_settled") {
|
|
846
|
+
runningAgent.runAborted = event.aborted;
|
|
847
|
+
logSteering(runningAgent.id, "child-agent-settled", { streaming: session.isStreaming, aborted: event.aborted });
|
|
848
|
+
}
|
|
746
849
|
if (event.type === "turn_start")
|
|
747
850
|
logSteering(runningAgent.id, "turn-started", { streaming: session.isStreaming });
|
|
748
851
|
if (event.type === "turn_end")
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/shared.ts
CHANGED
|
@@ -28,6 +28,8 @@ export type AgentStatus =
|
|
|
28
28
|
export type ParsedAgentCommand = {
|
|
29
29
|
isolate: boolean;
|
|
30
30
|
squash: boolean;
|
|
31
|
+
/** -h/--herdr: open the agent in a new herdr pane instead of running it in the background. */
|
|
32
|
+
herdr: boolean;
|
|
31
33
|
/** Leading pi CLI tokens (minus the extension's own options) to forward to the child, e.g. ["--thinking", "high"]. */
|
|
32
34
|
forwardedArgs: string[];
|
|
33
35
|
task: string;
|
|
@@ -142,6 +144,8 @@ export type RunningAgent = {
|
|
|
142
144
|
aborted?: boolean;
|
|
143
145
|
/** Set when the user interrupts only the active turn, leaving the child session alive. */
|
|
144
146
|
interruptRequested?: boolean;
|
|
147
|
+
/** Cancellation outcome of the current prompt, reported at session-level settlement. */
|
|
148
|
+
runAborted?: boolean;
|
|
145
149
|
};
|
|
146
150
|
|
|
147
151
|
export type CompletedAgent = {
|
|
@@ -162,6 +166,8 @@ export type CompletedAgent = {
|
|
|
162
166
|
toolUses: number;
|
|
163
167
|
turnCount: number;
|
|
164
168
|
contextPercent?: number;
|
|
169
|
+
/** The herdr pane whose Pi now owns the session, once the agent was opened there. */
|
|
170
|
+
herdrPane?: string;
|
|
165
171
|
};
|
|
166
172
|
|
|
167
173
|
/** The mechanism that fast-forwards a child conversation onto the main session, injected into the widget. */
|
|
@@ -172,6 +178,21 @@ export type RebaseDelivery = {
|
|
|
172
178
|
deliver(agent: RunningAgent | CompletedAgent, messages: AgentMessage[]): void;
|
|
173
179
|
};
|
|
174
180
|
|
|
181
|
+
/** A pane split for an agent, whose Pi starts only once the agent's session is free here. */
|
|
182
|
+
export type HerdrPane = {
|
|
183
|
+
paneId: string;
|
|
184
|
+
/** Start pi on the agent's session file in this pane; resolves once it is ready for input. */
|
|
185
|
+
start(): Promise<void>;
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
/** The mechanism that hands an agent's session to a Pi in a new herdr pane, injected into the widget. */
|
|
189
|
+
export type HerdrDelivery = {
|
|
190
|
+
/** Whether this Pi runs inside a herdr pane, so new panes can be split from it. */
|
|
191
|
+
available(): boolean;
|
|
192
|
+
/** Resolve the agent's session file and split a pane for it; rejects before splitting when there is no file. */
|
|
193
|
+
split(agent: RunningAgent | CompletedAgent): Promise<HerdrPane>;
|
|
194
|
+
};
|
|
195
|
+
|
|
175
196
|
/** How long a confirmation stays armed, and how long a transient footer notice shows. */
|
|
176
197
|
export const CONFIRMATION_WINDOW_MS = 4000;
|
|
177
198
|
|
|
@@ -266,6 +287,13 @@ export function mainContextLabel(state: MainContextState | undefined): string |
|
|
|
266
287
|
return undefined;
|
|
267
288
|
}
|
|
268
289
|
|
|
290
|
+
/**
|
|
291
|
+
* @example herdrPaneLabel("w1:p3") // "herdr pane w1:p3"
|
|
292
|
+
*/
|
|
293
|
+
export function herdrPaneLabel(paneId: string): string {
|
|
294
|
+
return `herdr pane ${paneId}`;
|
|
295
|
+
}
|
|
296
|
+
|
|
269
297
|
export function formatTurns(turnCount: number): string {
|
|
270
298
|
return `↻${turnCount}`;
|
|
271
299
|
}
|