diff options
Diffstat (limited to 'crates/dinggerd/assets/pi/pi-agent-state.ts')
| -rw-r--r-- | crates/dinggerd/assets/pi/pi-agent-state.ts | 109 |
1 files changed, 109 insertions, 0 deletions
diff --git a/crates/dinggerd/assets/pi/pi-agent-state.ts b/crates/dinggerd/assets/pi/pi-agent-state.ts index fd13b55..aae40cc 100644 --- a/crates/dinggerd/assets/pi/pi-agent-state.ts +++ b/crates/dinggerd/assets/pi/pi-agent-state.ts @@ -2,7 +2,9 @@ // managed by dingger; reinstalling or updating the integration overwrites this file. // add custom hooks/plugins beside this file instead of editing it. +import fs from "node:fs"; import net from "node:net"; +import path from "node:path"; // The dingger hook receiver listens here; override with DINGGER_AGENT_SOCKET. const socketPath = @@ -15,6 +17,18 @@ const socketPath = const paneId = process.env.DINGGER_PANE_ID || process.env.TMUX_PANE; const source = "dingger:pi"; +// Filled in by `dingger install pi`: the daemon reloads an agent that +// reports another version (`dingger reload-agents`). +const extensionVersion = "__DINGGER_EXTENSION_VERSION__"; + +// Where the daemon asks this extension to reload: one socket per extension +// instance (a reload starts a new one beside the old), next to the daemon's. +const controlPath = path.join( + path.dirname(socketPath), + `pi-${process.pid}-${Math.random().toString(36).slice(2, 8)}.sock`, +); +let controlListening = false; + function enabled() { return !!paneId; } @@ -112,6 +126,8 @@ async function report(state: AgentState | undefined, message?: string): Promise< ...(state ? { state } : {}), message, importance, + extension: extensionVersion, + ...(controlListening ? { control: controlPath } : {}), seq: Date.now() * 1000, ...(sessionFile ? { agent_session_path: sessionFile } : {}), }, @@ -253,11 +269,102 @@ export default function (pi) { }, }); + // The latest context, for the control socket's checks. + let latestCtx: any; + let control: net.Server | undefined; + + // Run only by the control socket below, which checked that pi is idle + // with an empty input box: extensions can reload pi only from a command. + pi.registerCommand("dingger-reload", { + description: "Reload pi (dingger uses it to update its extension; same as /reload)", + handler: async (_args: string, ctx: any) => { + await ctx.reload(); + }, + }); + + // Why this pi should not reload now, or undefined if it can. + function reloadRefusal(): string | undefined { + try { + if (agentActive || blockedCount > 0 || latestCtx?.isIdle?.() !== true || latestCtx?.hasPendingMessages?.()) { + return "busy"; + } + if ((latestCtx?.ui?.getEditorText?.() ?? "").trim() !== "") { + return "draft"; + } + } catch { + return "no context"; + } + return undefined; + } + + function startControl() { + if (control) { + return; + } + control = net.createServer((socket) => { + let buffer = ""; + socket.on("error", () => socket.destroy()); + socket.on("data", (data) => { + buffer += data.toString(); + const newline = buffer.indexOf("\n"); + if (newline < 0) { + return; + } + let method: unknown; + try { + method = JSON.parse(buffer.slice(0, newline))?.method; + } catch { + method = undefined; + } + if (method !== "reload") { + socket.end(`${JSON.stringify({ ok: false, reason: "unknown method" })}\n`); + return; + } + const refusal = reloadRefusal(); + socket.end(`${JSON.stringify(refusal ? { ok: false, reason: refusal } : { ok: true })}\n`); + if (!refusal) { + // Checked and started in one go: no keystroke comes in between. + pi.sendUserMessage("/dingger-reload", { expandPromptTemplates: true }); + } + }); + }); + control.on("error", () => { + controlListening = false; + }); + try { + fs.unlinkSync(controlPath); + } catch { + // None left over. + } + control.listen(controlPath, () => { + controlListening = true; + void report(undefined); + }); + // The socket alone does not keep pi running. + control.unref(); + } + + pi.on("session_shutdown", () => { + if (!control) { + return; + } + control.close(); + control = undefined; + controlListening = false; + try { + fs.unlinkSync(controlPath); + } catch { + // Gone already. + } + }); + pi.on("session_start", async (event, ctx) => { // TUI only: RPC/JSON/print modes are headless. if (ctx?.mode !== "tui") { return; } + latestCtx = ctx; + startControl(); // A reload can replace this extension mid-run without emitting another agent_start. agentActive = ctx?.isIdle?.() === false; updateSessionFile(ctx); @@ -268,12 +375,14 @@ export default function (pi) { }); pi.on("agent_start", (_event, ctx) => { + latestCtx = ctx; agentActive = true; updateSessionFile(ctx); publishState(); }); pi.on("agent_settled", (_event, ctx) => { + latestCtx = ctx; if (ctx?.isIdle?.() !== true) { return; } |
