diff options
Diffstat (limited to 'crates/dinggerd/assets/pi/pi-agent-state.ts')
| -rw-r--r-- | crates/dinggerd/assets/pi/pi-agent-state.ts | 186 |
1 files changed, 186 insertions, 0 deletions
diff --git a/crates/dinggerd/assets/pi/pi-agent-state.ts b/crates/dinggerd/assets/pi/pi-agent-state.ts new file mode 100644 index 0000000..49d4b9c --- /dev/null +++ b/crates/dinggerd/assets/pi/pi-agent-state.ts @@ -0,0 +1,186 @@ +// installed by dingger +// managed by dingger; reinstalling or updating the integration overwrites this file. +// add custom hooks/plugins beside this file instead of editing it. + +import net from "node:net"; +import path from "node:path"; +import { execSync } from "node:child_process"; + +// The dingger hook receiver listens here; override with DINGGER_AGENT_SOCKET. +const socketPath = + process.env.DINGGER_AGENT_SOCKET || + `${process.env.HOME}/.local/state/dingger/agent.sock`; + +// The extension runs inside a tmux pane, so it finds its own pane id. +function discoverPaneId(): string | undefined { + try { + const out = execSync('tmux display-message -p "#{pane_id}"', { + encoding: "utf8", + stdio: ["ignore", "pipe", "ignore"], + }); + const paneId = out.trim(); + return paneId.startsWith("%") ? paneId : undefined; + } catch { + return undefined; + } +} + +const paneId = process.env.DINGGER_PANE_ID || discoverPaneId(); +const source = "dingger:pi"; + +function enabled() { + return !!paneId; +} + +function sendRequestAttempt(request: unknown, timeoutMs: number): Promise<boolean> { + if (!enabled()) { + return Promise.resolve(true); + } + + return new Promise((resolve) => { + let done = false; + let timeout: ReturnType<typeof setTimeout> | undefined; + const finish = (delivered: boolean) => { + if (done) return; + done = true; + if (timeout) { + clearTimeout(timeout); + } + socket.destroy(); + resolve(delivered); + }; + + const socket = net.createConnection(socketPath); + socket.on("error", () => finish(false)); + socket.on("connect", () => socket.write(`${JSON.stringify(request)}\n`)); + socket.on("data", () => finish(true)); + socket.on("end", () => finish(false)); + timeout = setTimeout(() => finish(false), timeoutMs); + timeout.unref?.(); + }); +} + +async function sendRequest(request: unknown): Promise<void> { + if (await sendRequestAttempt(request, 500)) { + return; + } + await sendRequestAttempt(request, 1500); +} + +type AgentState = "working" | "blocked" | "idle"; + +function report(state: AgentState, message?: string): Promise<void> { + return sendRequest({ + id: `${source}:${Date.now()}:${Math.random().toString(36).slice(2)}`, + method: "pane.report_agent", + params: { + pane_id: paneId!, + source, + agent: "pi", + state, + message, + seq: Date.now() * 1000, + }, + }); +} + +let sendInFlight = false; +let queuedState: { state: AgentState; message?: string } | undefined; + +function queueState(state: AgentState, message?: string): void { + queuedState = { state, message }; + if (!sendInFlight) { + void drainStateQueue(); + } +} + +async function drainStateQueue(): Promise<void> { + if (sendInFlight) { + return; + } + + sendInFlight = true; + try { + while (queuedState) { + const next = queuedState; + queuedState = undefined; + await report(next.state, next.message); + } + } finally { + sendInFlight = false; + if (queuedState) { + void drainStateQueue(); + } + } +} + +export default function (pi) { + if (!enabled()) { + return; + } + + let agentActive = false; + let blockedCount = 0; + let blockedMessage: string | undefined; + let lastState: AgentState | undefined; + let lastMessage: string | undefined; + + function desiredState() { + if (blockedCount > 0) { + return { state: "blocked" as const, message: blockedMessage }; + } + if (agentActive) { + return { state: "working" as const, message: undefined }; + } + return { state: "idle" as const, message: undefined }; + } + + function publishState(force = false) { + const next = desiredState(); + if (!force && next.state === lastState && next.message === lastMessage) { + return; + } + lastState = next.state; + lastMessage = next.message; + queueState(next.state, next.message); + } + + pi.events.on("blocked", (data) => { + if (!data?.active) { + blockedCount = Math.max(0, blockedCount - 1); + if (blockedCount === 0) { + blockedMessage = undefined; + } + publishState(); + return; + } + + blockedCount += 1; + blockedMessage = data.label; + publishState(); + }); + + pi.on("session_start", async (event, ctx) => { + // TUI only: RPC/JSON/print modes are headless. + if (ctx?.mode !== "tui") { + return; + } + // A reload can replace this extension mid-run without emitting another agent_start. + agentActive = ctx?.isIdle?.() === false; + publishState(true); + }); + + pi.on("agent_start", (_event, ctx) => { + agentActive = true; + publishState(); + }); + + pi.on("agent_settled", (_event, ctx) => { + if (ctx?.isIdle?.() !== true) { + return; + } + + agentActive = false; + publishState(); + }); +} |
