aboutsummaryrefslogtreecommitdiff
path: root/crates/dinggerd/assets
diff options
context:
space:
mode:
Diffstat (limited to 'crates/dinggerd/assets')
-rw-r--r--crates/dinggerd/assets/pi/pi-agent-state.ts109
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;
}