mirror of
https://github.com/pnx/dotfiles
synced 2026-10-18 14:16:45 +02:00
add herdr
This commit is contained in:
parent
38989f191a
commit
b738eefbca
9 changed files with 1027 additions and 0 deletions
224
opencode/plugins/herdr-agent-state.js
Normal file
224
opencode/plugins/herdr-agent-state.js
Normal file
|
|
@ -0,0 +1,224 @@
|
|||
// 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() {},
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue