Adding support for herdr - agents in new tabs
This commit is contained in:
@@ -24,7 +24,9 @@ import {
|
||||
closeSurface,
|
||||
shellEscape,
|
||||
readScreen,
|
||||
} from "./tmux.ts";
|
||||
getMuxBackend,
|
||||
wrapWithCompletionFile,
|
||||
} from "./surface.ts";
|
||||
|
||||
import {
|
||||
countSessionEntryLines,
|
||||
@@ -497,10 +499,15 @@ function widgetIcon(kind: StatusSnapshot["kind"]): string {
|
||||
* (for example direnv/devenv), so the delay is configurable for users who hit
|
||||
* dropped commands. Keep the historical default at 500ms.
|
||||
*/
|
||||
function defaultShellReadyDelayMs(backend: ReturnType<typeof getMuxBackend>): number {
|
||||
return backend === "herdr" ? 1000 : 500;
|
||||
}
|
||||
|
||||
function getShellReadyDelayMs(): number {
|
||||
const raw = process.env.PI_SUBAGENT_SHELL_READY_DELAY_MS?.trim();
|
||||
const parsed = raw ? Number.parseInt(raw, 10) : Number.NaN;
|
||||
return Number.isFinite(parsed) && parsed >= 0 ? parsed : 500;
|
||||
if (Number.isFinite(parsed) && parsed >= 0) return parsed;
|
||||
return defaultShellReadyDelayMs(getMuxBackend());
|
||||
}
|
||||
|
||||
function muxUnavailableResult() {
|
||||
@@ -508,10 +515,10 @@ function muxUnavailableResult() {
|
||||
content: [
|
||||
{
|
||||
type: "text" as const,
|
||||
text: `Subagents require tmux. ${muxSetupHint()}`,
|
||||
text: `Subagents require a supported terminal multiplexer. ${muxSetupHint()}`,
|
||||
},
|
||||
],
|
||||
details: { error: "tmux not available" },
|
||||
details: { error: "terminal multiplexer not available" },
|
||||
};
|
||||
}
|
||||
|
||||
@@ -618,6 +625,7 @@ interface RunningSubagent {
|
||||
abortController?: AbortController;
|
||||
cli?: string;
|
||||
sentinelFile?: string;
|
||||
completionFile?: string;
|
||||
statusState: SubagentStatusState;
|
||||
/**
|
||||
* When true, status transitions (stalled/recovered) do not wake the parent
|
||||
@@ -1008,7 +1016,7 @@ function steerSubagent(
|
||||
} catch (error: any) {
|
||||
return {
|
||||
error:
|
||||
`Failed to deliver message to subagent "${running.name}" via tmux: ` +
|
||||
`Failed to deliver message to subagent "${running.name}" via the terminal surface: ` +
|
||||
`${error?.message ?? String(error)}`,
|
||||
};
|
||||
}
|
||||
@@ -1121,6 +1129,7 @@ function resolveResumeLaunchBehavior(): { autoExit: boolean; interactive: boolea
|
||||
|
||||
export const __test__ = {
|
||||
borderLine,
|
||||
defaultShellReadyDelayMs,
|
||||
getShellReadyDelayMs,
|
||||
renderSubagentWidgetLines,
|
||||
loadAgentDefaults,
|
||||
@@ -1141,6 +1150,7 @@ export const __test__ = {
|
||||
handleSubagentSteer,
|
||||
resolveResultPresentation,
|
||||
resolveResumeLaunchBehavior,
|
||||
wrapWithCompletionFile,
|
||||
runningSubagents,
|
||||
formatElapsed,
|
||||
formatTokens,
|
||||
@@ -1160,7 +1170,7 @@ function startWidgetRefresh() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Launch a subagent: creates the multiplexer pane, builds the command, and
|
||||
* Launch a subagent: creates a terminal surface, builds the command, and
|
||||
* sends it. Returns a RunningSubagent — does NOT poll.
|
||||
*
|
||||
* Call watchSubagent() on the returned object to observe completion.
|
||||
@@ -1184,6 +1194,8 @@ async function launchSubagent(
|
||||
if (!sessionFile) throw new Error("No session file");
|
||||
const sessionId = ctx.sessionManager.getSessionId();
|
||||
const artifactDir = getArtifactDir(ctx.sessionManager.getSessionDir(), sessionId);
|
||||
const completionFile = join(artifactDir, "subagent-exits", `${id}.exit-code`);
|
||||
mkdirSync(dirname(completionFile), { recursive: true });
|
||||
|
||||
const { effectiveCwd, localAgentDir, effectiveAgentDir } = resolveSubagentPaths(params, agentDefs);
|
||||
const targetCwdForSession = effectiveCwd ?? ctx.cwd;
|
||||
@@ -1201,14 +1213,15 @@ async function launchSubagent(
|
||||
].join("-");
|
||||
const subagentSessionFile = join(sessionDir, `${timestamp}_${uuid}.jsonl`);
|
||||
|
||||
// Use pre-created surface (parallel mode) or create a new one.
|
||||
// For new surfaces, pause briefly so the shell is ready before sending the command.
|
||||
// Use a pre-created surface or create a new one. New shells need a brief
|
||||
// startup window before terminal input is reliable.
|
||||
const surfacePreCreated = !!options?.surface;
|
||||
const surface = options?.surface ?? createSurface(params.name);
|
||||
if (!surfacePreCreated) {
|
||||
await new Promise<void>((resolve) => setTimeout(resolve, getShellReadyDelayMs()));
|
||||
}
|
||||
|
||||
try {
|
||||
const launchBehavior = resolveLaunchBehavior(params, agentDefs);
|
||||
|
||||
if (launchBehavior.seededSessionMode) {
|
||||
@@ -1271,7 +1284,7 @@ async function launchSubagent(
|
||||
cmdParts.push(shellEscape(params.task));
|
||||
|
||||
const cdPrefix = effectiveCwd ? `cd ${shellEscape(effectiveCwd)} && ` : "";
|
||||
const command = `${cdPrefix}${cmdParts.join(" ")}; echo '__SUBAGENT_DONE_'$?'__'`;
|
||||
const command = wrapWithCompletionFile(`${cdPrefix}${cmdParts.join(" ")}`, completionFile);
|
||||
|
||||
const launchScriptName = `${(params.name || "subagent")
|
||||
.toLowerCase()
|
||||
@@ -1301,6 +1314,7 @@ async function launchSubagent(
|
||||
launchScriptFile,
|
||||
cli: "claude",
|
||||
sentinelFile,
|
||||
completionFile,
|
||||
interactive: effectiveInteractive,
|
||||
statusState: createStatusState({
|
||||
source: "claude",
|
||||
@@ -1414,7 +1428,7 @@ async function launchSubagent(
|
||||
const cdPrefix = effectiveCwd ? `cd ${shellEscape(effectiveCwd)} && ` : "";
|
||||
|
||||
const piCommand = cdPrefix + envPrefix + parts.join(" ");
|
||||
const command = `${piCommand}; echo '__SUBAGENT_DONE_'$?'__'`;
|
||||
const command = wrapWithCompletionFile(piCommand, completionFile);
|
||||
const launchScriptName = `${(params.name || "subagent")
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9\s-]/g, "")
|
||||
@@ -1442,6 +1456,7 @@ async function launchSubagent(
|
||||
sessionFile: subagentSessionFile,
|
||||
launchScriptFile,
|
||||
activityFile,
|
||||
completionFile,
|
||||
interactive: effectiveInteractive,
|
||||
statusState: createStatusState({
|
||||
source: "pi",
|
||||
@@ -1451,6 +1466,17 @@ async function launchSubagent(
|
||||
|
||||
runningSubagents.set(id, running);
|
||||
return running;
|
||||
} catch (error) {
|
||||
runningSubagents.delete(id);
|
||||
if (!surfacePreCreated) {
|
||||
try {
|
||||
closeSurface(surface);
|
||||
} catch {
|
||||
// Preserve the launch error; cleanup is best-effort.
|
||||
}
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1532,6 +1558,7 @@ async function watchSubagent(
|
||||
interval: 1000,
|
||||
sessionFile,
|
||||
sentinelFile: running.sentinelFile,
|
||||
completionFile: running.completionFile,
|
||||
onTick() {
|
||||
observeRunningSubagent(running);
|
||||
deliverPendingQuestion(running);
|
||||
@@ -2152,6 +2179,7 @@ export default function subagentsExtension(pi: ExtensionAPI) {
|
||||
const surface = createSurface(name);
|
||||
await new Promise<void>((resolve) => setTimeout(resolve, getShellReadyDelayMs()));
|
||||
|
||||
try {
|
||||
// Build pi resume command
|
||||
const parts = ["pi", "--session", shellEscape(sessionPath)];
|
||||
|
||||
@@ -2162,7 +2190,9 @@ export default function subagentsExtension(pi: ExtensionAPI) {
|
||||
const sessionId = ctx.sessionManager.getSessionId();
|
||||
const artifactDir = getArtifactDir(ctx.sessionManager.getSessionDir(), sessionId);
|
||||
const activityFile = getSubagentActivityFile(artifactDir, id);
|
||||
const completionFile = join(artifactDir, "subagent-exits", `${id}.exit-code`);
|
||||
mkdirSync(dirname(activityFile), { recursive: true });
|
||||
mkdirSync(dirname(completionFile), { recursive: true });
|
||||
|
||||
// Replay the model, identity, and default-deny tool/extension sandbox.
|
||||
applySandboxToParts(parts, loadout, { artifactDir, name });
|
||||
@@ -2212,7 +2242,10 @@ export default function subagentsExtension(pi: ExtensionAPI) {
|
||||
// operate where they did before.
|
||||
const resumeCdPrefix = loadout.cwd ? `cd ${shellEscape(loadout.cwd)} && ` : "";
|
||||
|
||||
const command = `${resumeCdPrefix}${resumeEnvPrefix}${parts.join(" ")}; echo '__SUBAGENT_DONE_'$?'__'`;
|
||||
const command = wrapWithCompletionFile(
|
||||
`${resumeCdPrefix}${resumeEnvPrefix}${parts.join(" ")}`,
|
||||
completionFile,
|
||||
);
|
||||
const launchScriptFile = join(
|
||||
artifactDir,
|
||||
"subagent-scripts",
|
||||
@@ -2244,6 +2277,7 @@ export default function subagentsExtension(pi: ExtensionAPI) {
|
||||
sessionFile: sessionPath,
|
||||
launchScriptFile,
|
||||
activityFile,
|
||||
completionFile,
|
||||
interactive,
|
||||
statusState: createStatusState({
|
||||
source: "pi",
|
||||
@@ -2316,6 +2350,15 @@ export default function subagentsExtension(pi: ExtensionAPI) {
|
||||
status: "started",
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
runningSubagents.delete(id);
|
||||
try {
|
||||
closeSurface(surface);
|
||||
} catch {
|
||||
// Preserve the resume error; cleanup is best-effort.
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user