pi-extended-teams 2.2.6 → 2.2.7
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/README.md +2 -0
- package/docs/access.md +2 -2
- package/extensions/agents/read-agent.ts +100 -2
- package/extensions/events/register-events.ts +15 -3
- package/extensions/runtime/herdr.ts +13 -0
- package/extensions/runtime/types.ts +1 -0
- package/extensions/team/lifecycle.ts +6 -2
- package/extensions/tools/coordination-tools.ts +2 -0
- package/extensions/ui/agent-follow-view.ts +12 -3
- package/package.json +2 -2
- package/src/utils/models.ts +2 -1
package/README.md
CHANGED
|
@@ -55,6 +55,8 @@ This works well for multi-angle code review, root-cause investigation, parallel
|
|
|
55
55
|
|
|
56
56
|
With the editor empty, press Down to open agent navigation. Use Down/Up to move, `l` to expand large tool logs, `m` to message an agent, `i` to interrupt its currently running tool command, `x` to stop the whole agent, and Escape to return.
|
|
57
57
|
|
|
58
|
+
Inside Herdr, press `h` in an ordinary direct agent's preview to move it into a focused sibling Pi pane in the same workspace. Its conversation and team communication continue there. Nested helpers and delegation-enabled or workflow agents are excluded.
|
|
59
|
+
|
|
58
60
|
Scroll the transcript with Page Up/Page Down or the mouse wheel, including under Herdr. Press End or scroll back to the bottom to follow new output.
|
|
59
61
|
|
|
60
62
|
The lead can invoke the same command-only behavior with `interrupt_teammate({ agent_name: "agent" })`. It keeps the agent's session, task context, and file claims intact so you can send follow-up work. In-process cancellation is cooperative and may report that it is still pending; for tmux-backed agents, success means Pi's Escape key was delivered, not that command settlement was independently confirmed.
|
package/docs/access.md
CHANGED
|
@@ -8,13 +8,13 @@
|
|
|
8
8
|
- Write teammates start a separate Pi process in a tmux pane. The extension builds and runs the local Pi launch command with the teammate's working directory, model and thinking configuration, selected extensions, and `PI_TEAM_NAME`, `PI_AGENT_NAME`, and `PI_LIFECYCLE_RUN_ID` environment variables.
|
|
9
9
|
- Before launching a write teammate, the extension runs a bounded local `pi models --all` preflight through `sh -c` to determine whether the configured model is available.
|
|
10
10
|
- On macOS, while at least one write teammate is active, the extension may run `/usr/bin/caffeinate -i -w <lead-pid>` to prevent idle system sleep. It terminates that helper when no write teammates remain or the extension is disposed.
|
|
11
|
-
-
|
|
11
|
+
- Inside Herdr, pressing `h` can move an eligible in-process agent into a sibling Pi pane. The extension calls the `herdr` executable to split, run, focus, or close that pane.
|
|
12
12
|
|
|
13
13
|
## Files and local state
|
|
14
14
|
|
|
15
15
|
The extension reads settings from `~/.pi/agent/pi-extended-teams/settings.json` and `<project>/.pi/pi-extended-teams.json`. Model-provider compatibility also reads the legacy `~/.pi/pi-extended-teams.json` path. Predefined agents can be read from `~/.pi/agent/agents/` and `<project>/.pi/agents/`; team templates can be read or written at `~/.pi/teams.yaml`, `~/.pi/agent/teams.yaml`, and `<project>/.pi/teams.yaml`.
|
|
16
16
|
|
|
17
|
-
For a team named `<team>`, coordination state is stored under `~/.pi/teams/<team>/`. This includes `config.json`, inboxes, runtime status, session-context references, lifecycle quarantine and tombstones, file claims, write and read-helper queues, shared memory, report events, lead-session metadata, debug logs, and private child transcripts under `agent-sessions/`. Task records are stored under `~/.pi/tasks/<team>/`, and agent reports may also be written under `~/.pi/agent/reports/`. Cleanup removes lifecycle and queue files when they are no longer needed.
|
|
17
|
+
For a team named `<team>`, coordination state is stored under `~/.pi/teams/<team>/`. This includes `config.json`, inboxes, runtime status, session-context references, lifecycle quarantine and tombstones, file claims, write and read-helper queues, shared memory, report events, lead-session metadata, debug logs, and private child transcripts and handoff prompt snapshots under `agent-sessions/`. Task records are stored under `~/.pi/tasks/<team>/`, and agent reports may also be written under `~/.pi/agent/reports/`. Cleanup removes lifecycle and queue files when they are no longer needed.
|
|
18
18
|
|
|
19
19
|
The extension reads project extension sources only when Pi marks the same working directory trusted. Teammates receive the working directory supplied at spawn time. A write teammate can use any filesystem access granted to its child Pi process and enabled tools. A read teammate's tools are restricted by the extension, but it still receives prompt and project context supplied by the lead.
|
|
20
20
|
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import type { AgentSession } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import { buildPiCommand, getPiLaunchCommand, shellQuote } from "../internal/pi-command";
|
|
6
|
+
import { herdrCommand } from "../runtime/herdr";
|
|
2
7
|
import * as runtime from "../../src/utils/runtime";
|
|
3
8
|
import * as teams from "../../src/utils/teams";
|
|
4
9
|
import * as messaging from "../../src/utils/messaging";
|
|
@@ -12,7 +17,7 @@ import { isPiPromptPlanningMember, shouldSuppressLeadReportInjection } from "../
|
|
|
12
17
|
import { canonicalPersistedModelSlot, loadSettings, requireFavoriteModelLevel } from "../../src/utils/settings";
|
|
13
18
|
import { parseQualifiedModel } from "../../src/utils/model-resolution";
|
|
14
19
|
import { closePersistedRecipient } from "../team/recipient-closure";
|
|
15
|
-
import { generateExtensionInstanceId, generateLifecycleRunId } from "../../src/utils/lifecycle-tombstone";
|
|
20
|
+
import { generateExtensionInstanceId, generateLifecycleRunId, withLifecycleTombstoneLock } from "../../src/utils/lifecycle-tombstone";
|
|
16
21
|
import { createLifecycleRuntime, type ShutdownTeammateOptions } from "../team/lifecycle";
|
|
17
22
|
import {
|
|
18
23
|
createSpawnResourcePlan,
|
|
@@ -634,6 +639,7 @@ export async function runReadAgentInProcess(
|
|
|
634
639
|
getTeamName: () => readTeamName,
|
|
635
640
|
}).shutdownTeammate;
|
|
636
641
|
|
|
642
|
+
let handoffRequested = false;
|
|
637
643
|
let submittedFinalReport: SubmittedAgentReport | undefined;
|
|
638
644
|
let finalReportSubmissionInProgress = false;
|
|
639
645
|
let childSessionManager: any;
|
|
@@ -660,6 +666,7 @@ export async function runReadAgentInProcess(
|
|
|
660
666
|
const closeRecipient = async (): Promise<ReadAgentDeliveryCloseResult> => {
|
|
661
667
|
if (pendingChildParent) pendingChildController?.cancelParent(pendingChildParent);
|
|
662
668
|
const deliveryClose = closeReadAgentMessageDelivery(state);
|
|
669
|
+
if (handoffRequested) return deliveryClose;
|
|
663
670
|
if (!state.recipientClosurePromise) {
|
|
664
671
|
state.recipientClosurePromise = closePersistedRecipient(
|
|
665
672
|
readTeamName,
|
|
@@ -959,7 +966,7 @@ export async function runReadAgentInProcess(
|
|
|
959
966
|
} as Parameters<typeof createAgentSession>[0] & { modelRuntime?: unknown });
|
|
960
967
|
|
|
961
968
|
state.session = session;
|
|
962
|
-
installReadAgentSessionLifecycle(session);
|
|
969
|
+
const sessionLifecycle = installReadAgentSessionLifecycle(session);
|
|
963
970
|
try {
|
|
964
971
|
if (typeof session.bindExtensions === "function") {
|
|
965
972
|
await (session.bindExtensions as (bindings: { mode: "print" }) => Promise<void>)({ mode: "print" });
|
|
@@ -973,6 +980,96 @@ export async function runReadAgentInProcess(
|
|
|
973
980
|
if (state.teardownState !== "persistence_failed") await state.teardownPromise;
|
|
974
981
|
return;
|
|
975
982
|
}
|
|
983
|
+
if (process.env.HERDR_ENV === "1" && (member.delegationDepth ?? 0) === 0
|
|
984
|
+
&& !member.requestedBy && !member.parentAgentName && !member.allowNestedReadAgents
|
|
985
|
+
&& !shouldSuppressLeadReportInjection(member)) {
|
|
986
|
+
let moving: Promise<void> | undefined;
|
|
987
|
+
let paneId: string | undefined;
|
|
988
|
+
let command = "";
|
|
989
|
+
let queued: string[] = [];
|
|
990
|
+
let released = false;
|
|
991
|
+
const release = Promise.all([finished, sessionLifecycle.finalized]).then(() => {
|
|
992
|
+
if (handoffRequested) { state.session = undefined; released = true; }
|
|
993
|
+
});
|
|
994
|
+
void release.catch(() => {});
|
|
995
|
+
const recordPane = (id: string | undefined) => withLifecycleTombstoneLock(readTeamName, member.name, async lock => {
|
|
996
|
+
const current = (await teams.readConfig(readTeamName)).members.find(item => item.name === member.name);
|
|
997
|
+
if (!current || current.lifecycleRunId !== state.runId || current.isActive === false || lock.read().status !== "absent") {
|
|
998
|
+
throw new Error("The agent is no longer available to move.");
|
|
999
|
+
}
|
|
1000
|
+
await teams.updateMember(readTeamName, member.name, { herdrPaneId: id, tmuxPaneId: "" });
|
|
1001
|
+
});
|
|
1002
|
+
state.moveToHerdr = () => moving ??= (async () => {
|
|
1003
|
+
if (!options.isCurrentReadAgentRun(key, state) || (!handoffRequested && !state.acceptingMessages)) {
|
|
1004
|
+
throw new Error("The agent is already finishing.");
|
|
1005
|
+
}
|
|
1006
|
+
if (handoffRequested && !released) throw new Error("The current operation is still stopping. Try h after it settles.");
|
|
1007
|
+
if (paneId) { herdrCommand("pane", "close", paneId); await recordPane(undefined); paneId = undefined; }
|
|
1008
|
+
const sessionFile = childSessionManager.getSessionFile();
|
|
1009
|
+
if (!sessionFile || !fs.existsSync(sessionFile)) throw new Error("The agent has no saved session yet.");
|
|
1010
|
+
if (!handoffRequested) {
|
|
1011
|
+
const promptDir = privateSessionDirectory ?? preparePrivateAgentSessionDirectory(readTeamName, member.name, state.runId);
|
|
1012
|
+
const promptFile = path.join(promptDir, "herdr-system-prompt.txt");
|
|
1013
|
+
fs.writeFileSync(promptFile, session.agent.state.systemPrompt.replace("running in-process so the lead can follow and control you from Pi", "running in a Herdr pane"), { mode: 0o600 });
|
|
1014
|
+
const tools = session.agent.state.tools.map(tool => tool.name).join(",");
|
|
1015
|
+
const identity = {
|
|
1016
|
+
HOME: os.homedir(), PI_CODING_AGENT_DIR: agentDir,
|
|
1017
|
+
PI_AGENT_NAME: member.name, PI_TEAM_NAME: readTeamName, PI_LIFECYCLE_RUN_ID: state.runId,
|
|
1018
|
+
PI_EXTENDED_TEAMS_HERDR_RESUME: "1",
|
|
1019
|
+
};
|
|
1020
|
+
const currentModel = session.model ?? model;
|
|
1021
|
+
const launch = buildPiCommand(getPiLaunchCommand(), `${currentModel.provider}/${currentModel.id}`,
|
|
1022
|
+
session.thinkingLevel ?? member.thinking, resourcePlan.extensionPaths,
|
|
1023
|
+
resourcePlan.trust.projectTrusted, resourcePlan.selfExtensionPath);
|
|
1024
|
+
command = [
|
|
1025
|
+
"env", ...Object.entries(identity).map(([name, value]) => `${name}=${shellQuote(value)}`), launch,
|
|
1026
|
+
"--session", shellQuote(sessionFile), "--tools", shellQuote(tools), "--system-prompt", shellQuote(promptFile),
|
|
1027
|
+
].join(" ");
|
|
1028
|
+
// Keep fresh-shell input below the PTY line limit without shortening the launch arguments.
|
|
1029
|
+
const launchFile = path.join(promptDir, "herdr-launch.sh");
|
|
1030
|
+
fs.writeFileSync(launchFile, `exec ${command}\n`, { mode: 0o600 });
|
|
1031
|
+
command = `/bin/sh ${shellQuote(launchFile)}`;
|
|
1032
|
+
}
|
|
1033
|
+
paneId = JSON.parse(herdrCommand("pane", "split", "--current", "--direction", "right", "--cwd", member.cwd, "--focus")).result?.pane?.pane_id;
|
|
1034
|
+
if (typeof paneId !== "string" || !paneId) throw new Error("Herdr did not return a pane ID.");
|
|
1035
|
+
try {
|
|
1036
|
+
if (!handoffRequested) {
|
|
1037
|
+
handoffRequested = true;
|
|
1038
|
+
state.stopRequested = true;
|
|
1039
|
+
const delivery = closeReadAgentMessageDelivery(state);
|
|
1040
|
+
const pending = session.clearQueue();
|
|
1041
|
+
queued = [...pending.steering, ...pending.followUp];
|
|
1042
|
+
signalReadAgentWake(state);
|
|
1043
|
+
const shutdown = await sessionLifecycle.requestShutdown("resume", delivery.rawDeliverySettlement);
|
|
1044
|
+
if (shutdown.status !== "settled" || shutdown.abort !== "settled" || shutdown.dispose !== "settled") {
|
|
1045
|
+
throw new Error("The current operation has not stopped. Try h after it settles.");
|
|
1046
|
+
}
|
|
1047
|
+
await release;
|
|
1048
|
+
}
|
|
1049
|
+
if (queued.length) {
|
|
1050
|
+
SessionManager.open(sessionFile).appendMessage({ role: "user", content: queued.join("\n\n"), timestamp: Date.now() });
|
|
1051
|
+
queued = [];
|
|
1052
|
+
}
|
|
1053
|
+
const previousPid = (await runtime.readRuntimeStatus(readTeamName, member.name))?.pid;
|
|
1054
|
+
await recordPane(paneId);
|
|
1055
|
+
herdrCommand("pane", "run", paneId, command);
|
|
1056
|
+
const deadline = Date.now() + 10000;
|
|
1057
|
+
for (;;) {
|
|
1058
|
+
const status = await runtime.readRuntimeStatus(readTeamName, member.name);
|
|
1059
|
+
if (status?.lifecycleRunId === state.runId && status.pid && status.pid !== previousPid && status.pid !== process.pid) break;
|
|
1060
|
+
if (Date.now() >= deadline) throw new Error("Pi did not resume. The saved session is retained; h can retry.");
|
|
1061
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
|
1062
|
+
}
|
|
1063
|
+
if (options.isCurrentReadAgentRun(key, state)) options.runningReadAgents.delete(key);
|
|
1064
|
+
options.renderReadAgentStatus();
|
|
1065
|
+
} catch (error) {
|
|
1066
|
+
herdrCommand("pane", "close", paneId);
|
|
1067
|
+
await recordPane(undefined);
|
|
1068
|
+
paneId = undefined;
|
|
1069
|
+
throw error;
|
|
1070
|
+
}
|
|
1071
|
+
})().catch(error => { moving = undefined; throw error; });
|
|
1072
|
+
}
|
|
976
1073
|
markReadAgentActivity(state, "started", "thinking");
|
|
977
1074
|
options.renderReadAgentStatus();
|
|
978
1075
|
|
|
@@ -1262,6 +1359,7 @@ export async function runReadAgentInProcess(
|
|
|
1262
1359
|
if (state.heartbeatTimer) clearInterval(state.heartbeatTimer);
|
|
1263
1360
|
state.heartbeatTimer = undefined;
|
|
1264
1361
|
closeReadAgentMessageDelivery(state);
|
|
1362
|
+
if (handoffRequested) { state.resolveFinished?.(); return; }
|
|
1265
1363
|
const teardown = await shutdownTeammate(readTeamName, member, { reason: "quit" });
|
|
1266
1364
|
if (pendingChildController && pendingChildParent) {
|
|
1267
1365
|
pendingChildController.forgetParent(pendingChildParent);
|
|
@@ -220,10 +220,14 @@ export function registerExtensionEvents(pi: any, options: RegisterEventsOptions)
|
|
|
220
220
|
}
|
|
221
221
|
|
|
222
222
|
scheduleTeammateOneShot(() => {
|
|
223
|
-
options.quietTrigger(
|
|
223
|
+
options.quietTrigger(process.env.PI_EXTENDED_TEAMS_HERDR_RESUME === "1"
|
|
224
|
+
? "Continue your existing assignment. Read any new inbox messages, then finish and report_and_exit from this pane."
|
|
225
|
+
: "read_inbox to get your instructions, then begin your work.");
|
|
224
226
|
}, 1000);
|
|
225
227
|
|
|
226
228
|
if (teamName) {
|
|
229
|
+
const herdrResume = process.env.PI_EXTENDED_TEAMS_HERDR_RESUME === "1";
|
|
230
|
+
let notifiedInbox: string | undefined;
|
|
227
231
|
let wakeInFlight = false;
|
|
228
232
|
const wakeIfUnread = async () => {
|
|
229
233
|
if (teammateInboxDisposed) return;
|
|
@@ -232,7 +236,7 @@ export function registerExtensionEvents(pi: any, options: RegisterEventsOptions)
|
|
|
232
236
|
scheduleTeammateInboxWake(250);
|
|
233
237
|
return;
|
|
234
238
|
}
|
|
235
|
-
if (!ctx.isIdle()) {
|
|
239
|
+
if (!ctx.isIdle() && !herdrResume) {
|
|
236
240
|
teammatePendingInboxWake = true;
|
|
237
241
|
scheduleTeammateInboxWake(250);
|
|
238
242
|
return;
|
|
@@ -246,7 +250,15 @@ export function registerExtensionEvents(pi: any, options: RegisterEventsOptions)
|
|
|
246
250
|
lastHeartbeatAt: Date.now(),
|
|
247
251
|
});
|
|
248
252
|
if (unread.length > 0) {
|
|
249
|
-
|
|
253
|
+
const content = `You have ${unread.length} new inbox message(s). Read them with read_inbox and act.`;
|
|
254
|
+
if (herdrResume) {
|
|
255
|
+
const inboxVersion = JSON.stringify(unread);
|
|
256
|
+
if (inboxVersion !== notifiedInbox) {
|
|
257
|
+
pi.sendMessage({ customType: "pi-extended-teams-wake", content, display: false },
|
|
258
|
+
{ triggerTurn: true, deliverAs: "steer" });
|
|
259
|
+
notifiedInbox = inboxVersion;
|
|
260
|
+
}
|
|
261
|
+
} else options.quietTrigger(content);
|
|
250
262
|
}
|
|
251
263
|
} catch (e) {
|
|
252
264
|
if (!teammateInboxDisposed) {
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
export function herdrCommand(...args: string[]): string {
|
|
4
|
+
if (process.env.HERDR_ENV !== "1") throw new Error("This action requires Herdr.");
|
|
5
|
+
const result = spawnSync("herdr", args, { encoding: "utf8", timeout: 5000 });
|
|
6
|
+
if (result.status !== 0) {
|
|
7
|
+
let code: string | undefined;
|
|
8
|
+
try { code = JSON.parse(result.stderr).error?.code; } catch { /* Non-JSON CLI failure. */ }
|
|
9
|
+
if (args[0] === "pane" && args[1] === "close" && code === "pane_not_found") return "";
|
|
10
|
+
throw new Error(result.stderr || result.error?.message || "Herdr command failed.");
|
|
11
|
+
}
|
|
12
|
+
return result.stdout;
|
|
13
|
+
}
|
|
@@ -47,6 +47,7 @@ export interface RunningReadAgent extends ManagedReadAgentLifecycleState {
|
|
|
47
47
|
/** Fail finalization closed so runtime, member, fence, and transcript remain recoverable. */
|
|
48
48
|
finalizationBlockedReason?: string;
|
|
49
49
|
session?: AgentSession;
|
|
50
|
+
moveToHerdr?(): Promise<void>;
|
|
50
51
|
finished?: Promise<void>;
|
|
51
52
|
}
|
|
52
53
|
|
|
@@ -20,7 +20,8 @@ import {
|
|
|
20
20
|
type ReadAgentTeardownResult,
|
|
21
21
|
} from "../agents/read-agent-session-lifecycle";
|
|
22
22
|
import { releaseAllClaimsForAgent } from "./roster";
|
|
23
|
-
import { cleanupPidFileProcess } from "../internal/session-files";
|
|
23
|
+
import { cleanupPidFileProcess, unlinkPidFile } from "../internal/session-files";
|
|
24
|
+
import { herdrCommand } from "../runtime/herdr";
|
|
24
25
|
import { closePersistedRecipient } from "./recipient-closure";
|
|
25
26
|
import { cleanupPrivateAgentSessionDirectory } from "../internal/agent-session-files";
|
|
26
27
|
|
|
@@ -65,7 +66,10 @@ export function createLifecycleRuntime(options: LifecycleRuntimeOptions) {
|
|
|
65
66
|
async function finalizeTeammateRuntime(teamName: string, member: Member, expectedRunId: string): Promise<void> {
|
|
66
67
|
const pidFile = path.join(paths.teamDir(teamName), `${member.name}.pid`);
|
|
67
68
|
const pidFileExisted = fs.existsSync(pidFile);
|
|
68
|
-
|
|
69
|
+
if (member.herdrPaneId) herdrCommand("pane", "close", member.herdrPaneId);
|
|
70
|
+
const pidCleanup = member.herdrPaneId
|
|
71
|
+
? { pid: undefined, killError: undefined, unlinked: !pidFileExisted || unlinkPidFile(pidFile) }
|
|
72
|
+
: cleanupPidFileProcess(pidFile, { skipPid: process.pid });
|
|
69
73
|
if (pidFileExisted && !pidCleanup.unlinked) {
|
|
70
74
|
throw new Error(`Could not remove pid file for ${member.name}.`);
|
|
71
75
|
}
|
|
@@ -147,6 +147,7 @@ export function registerCoordinationTools(pi: any, options: CoordinationToolsOpt
|
|
|
147
147
|
async execute(_toolCallId: string, params: any, _signal: AbortSignal, _onUpdate: any, ctx: any) {
|
|
148
148
|
const targetTeamName = requireCurrentSession(options);
|
|
149
149
|
if (!options.isTeammate) throw new Error("report_and_exit is only available to spawned agents.");
|
|
150
|
+
if (process.env.PI_EXTENDED_TEAMS_HERDR_RESUME === "1" && !params.content.trim()) throw new Error("Final report content must not be empty.");
|
|
150
151
|
|
|
151
152
|
const config = await teams.readConfig(targetTeamName);
|
|
152
153
|
const member = config.members.find(m => m.name === options.agentName);
|
|
@@ -271,6 +272,7 @@ export function registerCoordinationTools(pi: any, options: CoordinationToolsOpt
|
|
|
271
272
|
async execute(_toolCallId: string, params: any, _signal: AbortSignal, _onUpdate: any, _ctx: any) {
|
|
272
273
|
const targetTeamName = requireCurrentSession(options);
|
|
273
274
|
const targetAgent = params.agent_name || options.agentName;
|
|
275
|
+
if (process.env.PI_EXTENDED_TEAMS_HERDR_RESUME === "1" && targetAgent !== options.agentName) throw new Error("A resumed subagent can only read its own inbox.");
|
|
274
276
|
const markAsRead = params.mark_as_read !== false;
|
|
275
277
|
const unreadOnly = params.unread_only !== false;
|
|
276
278
|
const msgs = await messaging.readInbox(targetTeamName, targetAgent, unreadOnly, markAsRead);
|
|
@@ -605,11 +605,12 @@ export function createAgentFollowComponent(
|
|
|
605
605
|
const logAction = expandLargeToolResults ? "l collapse logs" : "l expand logs";
|
|
606
606
|
const messageAction = options.sendMessage ? " · m message" : "";
|
|
607
607
|
const interruptAction = options.interruptAgent ? " · i interrupt" : "";
|
|
608
|
-
const
|
|
608
|
+
const herdrAction = process.env.HERDR_ENV === "1" && agent.moveToHerdr ? " · h Herdr" : "";
|
|
609
|
+
const help = messageStatus && !options.sendMessage ? messageStatus : composingMessage
|
|
609
610
|
? `message ${agent.name} · enter send · esc cancel`
|
|
610
611
|
: agents.length > 1
|
|
611
|
-
? `↑ previous/main · ↓ next agent · ←/→ agent · ${logAction}${messageAction}${interruptAction} · x stop · pgup/pgdn scroll · esc main`
|
|
612
|
-
: `↑/esc main · ${logAction}${messageAction}${interruptAction} · x stop · pgup/pgdn scroll · end follow`;
|
|
612
|
+
? `↑ previous/main · ↓ next agent · ←/→ agent · ${logAction}${messageAction}${interruptAction}${herdrAction} · x stop · pgup/pgdn scroll · esc main`
|
|
613
|
+
: `↑/esc main · ${logAction}${messageAction}${interruptAction}${herdrAction} · x stop · pgup/pgdn scroll · end follow`;
|
|
613
614
|
|
|
614
615
|
const currentTranscriptWidth = Math.max(20, innerWidth);
|
|
615
616
|
if (transcriptAgent !== agent
|
|
@@ -766,6 +767,14 @@ export function createAgentFollowComponent(
|
|
|
766
767
|
tui.requestRender();
|
|
767
768
|
return;
|
|
768
769
|
}
|
|
770
|
+
if (data.toLowerCase() === "h" && process.env.HERDR_ENV === "1") {
|
|
771
|
+
const agent = currentAgent(sortedAgents(), selectedName);
|
|
772
|
+
if (agent?.moveToHerdr) void agent.moveToHerdr().then(done).catch(error => {
|
|
773
|
+
messageStatus = sanitizePlainTuiLine(error instanceof Error ? error.message : String(error));
|
|
774
|
+
tui.requestRender();
|
|
775
|
+
});
|
|
776
|
+
return;
|
|
777
|
+
}
|
|
769
778
|
if (data.toLowerCase() === "i" && options.interruptAgent) {
|
|
770
779
|
const agent = currentAgent(sortedAgents(), selectedName);
|
|
771
780
|
if (!agent || interruptingAgents.has(agent.name)) return;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-extended-teams",
|
|
3
|
-
"version": "2.2.
|
|
3
|
+
"version": "2.2.7",
|
|
4
4
|
"description": "Control-first, session-connected subagents for Pi with live navigation and intent-tier routing",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"scripts": {
|
|
29
29
|
"typecheck": "tsc --noEmit",
|
|
30
30
|
"test": "vitest run",
|
|
31
|
-
"test:focused": "vitest run src/utils/claims.test.ts src/utils/tasks.test.ts src/utils/model-resolution.test.ts src/utils/settings.test.ts src/utils/write-queue.test.ts src/utils/shared-memory.test.ts extensions/index.test.ts extensions/events/register-events.test.ts extensions/internal/session-files.test.ts extensions/team/contracts.test.ts extensions/tools/delegation-guard.test.ts extensions/tools/agent-communication-tools.test.ts extensions/tools/read-helper.test.ts extensions/agents/read-agent.test.ts extensions/agents/write-agent.test.ts extensions/tools/team-tools.read-agent.test.ts",
|
|
31
|
+
"test:focused": "vitest run src/utils/claims.test.ts src/utils/tasks.test.ts src/utils/model-resolution.test.ts src/utils/settings.test.ts src/utils/write-queue.test.ts src/utils/shared-memory.test.ts extensions/index.test.ts extensions/events/register-events.test.ts extensions/internal/session-files.test.ts extensions/team/contracts.test.ts extensions/tools/delegation-guard.test.ts extensions/tools/agent-communication-tools.test.ts extensions/tools/read-helper.test.ts extensions/agents/read-agent.test.ts extensions/agents/write-agent.test.ts extensions/tools/team-tools.read-agent.test.ts extensions/ui/agent-follow-view.test.ts extensions/tools/coordination-tools.test.ts",
|
|
32
32
|
"build": "tsc --noEmit"
|
|
33
33
|
},
|
|
34
34
|
"main": "extensions/index.ts",
|
package/src/utils/models.ts
CHANGED
|
@@ -10,6 +10,7 @@ export interface Member {
|
|
|
10
10
|
model?: string;
|
|
11
11
|
joinedAt: number;
|
|
12
12
|
tmuxPaneId: string;
|
|
13
|
+
herdrPaneId?: string;
|
|
13
14
|
windowId?: string;
|
|
14
15
|
cwd: string;
|
|
15
16
|
subscriptions: any[];
|
|
@@ -33,7 +34,7 @@ export interface Member {
|
|
|
33
34
|
parentAgentName?: string;
|
|
34
35
|
/** Runtime-owned parent lifecycle identity for a restricted nested read child. */
|
|
35
36
|
parentLifecycleRunId?: string;
|
|
36
|
-
/**
|
|
37
|
+
/** Tool role; execution may be in-process or in a terminal pane. */
|
|
37
38
|
role?: "read" | "write";
|
|
38
39
|
/** Optional category preset name from settings.json. */
|
|
39
40
|
category?: string;
|