mirror of
https://github.com/herdrdev/herdr.git
synced 2026-09-22 00:01:06 +00:00
225 lines
6.4 KiB
JavaScript
225 lines
6.4 KiB
JavaScript
// installed by herdr
|
|
// managed by herdr; reinstalling or updating the integration overwrites this file.
|
|
// add custom hooks/plugins beside this file instead of editing it.
|
|
// HERDR_INTEGRATION_ID=opencode
|
|
// HERDR_INTEGRATION_VERSION=13
|
|
|
|
import net from "node:net";
|
|
|
|
const SOURCE = "herdr:opencode";
|
|
const AGENT = "opencode";
|
|
let reportSeq = Date.now() * 1000;
|
|
let requestChain = Promise.resolve();
|
|
let reportedRootSessionID;
|
|
|
|
// Track child sessions so their events cannot replace the pane's root session.
|
|
// User prompts carry the root id to preserve its identity and cross-talk guard.
|
|
const childSessions = new Map();
|
|
const CHILD_EVENT_STATES = new Map([
|
|
["permission.asked", "blocked"],
|
|
["question.asked", "blocked"],
|
|
["permission.replied", "working"],
|
|
["question.replied", "working"],
|
|
["question.rejected", "working"],
|
|
]);
|
|
|
|
function nextReportSeq() {
|
|
reportSeq += 1;
|
|
return reportSeq;
|
|
}
|
|
|
|
function sessionIDFromProperties(properties) {
|
|
return typeof properties?.sessionID === "string" && properties.sessionID
|
|
? properties.sessionID
|
|
: undefined;
|
|
}
|
|
|
|
const SESSION_STATE_BY_STATUS = new Map([
|
|
["idle", "idle"],
|
|
["active", "working"],
|
|
["busy", "working"],
|
|
["pending", "working"],
|
|
["retry", "working"],
|
|
["running", "working"],
|
|
["streaming", "working"],
|
|
["working", "working"],
|
|
]);
|
|
|
|
function stateFromSessionStatus(status) {
|
|
const kind = typeof status === "string" ? status : status?.type;
|
|
return typeof kind === "string"
|
|
? SESSION_STATE_BY_STATUS.get(kind.toLowerCase())
|
|
: undefined;
|
|
}
|
|
|
|
function request(method, params) {
|
|
const pending = requestChain.then(() => requestOnce(method, params));
|
|
requestChain = pending.catch(() => {});
|
|
return pending;
|
|
}
|
|
|
|
function requestOnce(method, params) {
|
|
const paneId = process.env.HERDR_PANE_ID;
|
|
const socketPath = process.env.HERDR_SOCKET_PATH;
|
|
|
|
if (!paneId || !socketPath) {
|
|
return Promise.resolve();
|
|
}
|
|
|
|
const socketEndpoint =
|
|
process.platform === "win32" ? `\\\\.\\pipe\\${socketPath}` : socketPath;
|
|
|
|
const requestId = `${SOURCE}:${Date.now()}:${Math.floor(Math.random() * 1_000_000)
|
|
.toString()
|
|
.padStart(6, "0")}`;
|
|
const request = {
|
|
id: requestId,
|
|
method,
|
|
params: {
|
|
pane_id: paneId,
|
|
source: SOURCE,
|
|
agent: AGENT,
|
|
seq: nextReportSeq(),
|
|
...params,
|
|
},
|
|
};
|
|
|
|
return new Promise((resolve) => {
|
|
const client = net.createConnection(socketEndpoint, () => {
|
|
client.write(`${JSON.stringify(request)}\n`);
|
|
});
|
|
|
|
const finish = () => {
|
|
client.destroy();
|
|
resolve();
|
|
};
|
|
|
|
client.setTimeout(500, finish);
|
|
client.on("data", finish);
|
|
client.on("error", finish);
|
|
client.on("end", finish);
|
|
client.on("close", resolve);
|
|
});
|
|
}
|
|
|
|
function reportSession(sessionID) {
|
|
if (!sessionID) {
|
|
return Promise.resolve();
|
|
}
|
|
return request("pane.report_agent_session", { agent_session_id: sessionID });
|
|
}
|
|
|
|
function reportState(state, sessionID) {
|
|
const params = { state };
|
|
if (sessionID) {
|
|
reportedRootSessionID = sessionID;
|
|
params.agent_session_id = sessionID;
|
|
}
|
|
return request("pane.report_agent", params);
|
|
}
|
|
|
|
function ownsLocalLifecycle() {
|
|
const args = process.argv.slice(2);
|
|
const separator = args.indexOf("--");
|
|
if (separator !== -1) args.splice(separator);
|
|
if (args.some((arg) => arg === "--attach" || arg.startsWith("--attach="))) return false;
|
|
while (args[0] === "--print-logs" || args[0] === "--log-level" || args[0]?.startsWith("--log-level=")) {
|
|
args.splice(0, args[0] === "--log-level" ? 2 : 1);
|
|
}
|
|
// These local clients have no TUI plugin. Shared servers and the TUI worker
|
|
// cannot identify their attached panes; their lifecycle belongs to each TUI.
|
|
return args[0] === "run" ||
|
|
(!["serve", "web", "attach"].includes(args[0]) && args.includes("--mini"));
|
|
}
|
|
|
|
export const HerdrAgentStatePlugin = async () => {
|
|
if (
|
|
!ownsLocalLifecycle() ||
|
|
process.env.HERDR_ENV !== "1" ||
|
|
!process.env.HERDR_SOCKET_PATH ||
|
|
!process.env.HERDR_PANE_ID
|
|
) {
|
|
return {};
|
|
}
|
|
|
|
return {
|
|
"chat.message": async ({ sessionID }) => {
|
|
if (sessionID && childSessions.has(sessionID)) {
|
|
return;
|
|
}
|
|
await reportState("working", sessionID);
|
|
},
|
|
event: async ({ event }) => {
|
|
const type = event?.type;
|
|
const properties = event?.properties ?? {};
|
|
const sessionID = sessionIDFromProperties(properties);
|
|
|
|
const info = properties.info;
|
|
if (info?.id && info.parentID) {
|
|
childSessions.set(info.id, info.parentID);
|
|
}
|
|
if (sessionID && childSessions.has(sessionID)) {
|
|
const state = CHILD_EVENT_STATES.get(type);
|
|
if (state) {
|
|
let rootSessionID = sessionID;
|
|
while (childSessions.has(rootSessionID)) {
|
|
rootSessionID = childSessions.get(rootSessionID);
|
|
}
|
|
await reportState(state, rootSessionID);
|
|
}
|
|
return;
|
|
}
|
|
|
|
switch (type) {
|
|
case "session.created":
|
|
// Creation is server-global, so an attached client may own it. The
|
|
// TUI plugin separately reports the root selected in this pane.
|
|
reportedRootSessionID = sessionID;
|
|
break;
|
|
case "session.updated":
|
|
if (sessionID && sessionID !== reportedRootSessionID) {
|
|
await reportSession(sessionID);
|
|
}
|
|
break;
|
|
case "session.status": {
|
|
const state = stateFromSessionStatus(properties.status);
|
|
if (state) {
|
|
await reportState(state, sessionID);
|
|
} else {
|
|
await reportSession(sessionID);
|
|
}
|
|
break;
|
|
}
|
|
case "tool.execute.before":
|
|
case "tool.execute.after":
|
|
case "permission.replied":
|
|
case "question.replied":
|
|
case "question.rejected":
|
|
case "session.compacted":
|
|
await reportState("working", sessionID);
|
|
break;
|
|
case "permission.asked":
|
|
case "question.asked":
|
|
case "session.error":
|
|
await reportState("blocked", sessionID);
|
|
break;
|
|
case "session.idle":
|
|
await reportState("idle", sessionID);
|
|
break;
|
|
case "session.deleted":
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
},
|
|
};
|
|
};
|
|
|
|
// V1 local run/Mini retain their server hooks. V1/V2 full TUIs own both
|
|
// selection and lifecycle, including when attached to a shared remote server.
|
|
export default {
|
|
id: "herdr.opencode",
|
|
server: HerdrAgentStatePlugin,
|
|
setup() {},
|
|
};
|