aboutsummaryrefslogtreecommitdiff
path: root/crates/dinggerd/assets/pi/pi-agent-state.ts
diff options
context:
space:
mode:
Diffstat (limited to 'crates/dinggerd/assets/pi/pi-agent-state.ts')
-rw-r--r--crates/dinggerd/assets/pi/pi-agent-state.ts104
1 files changed, 103 insertions, 1 deletions
diff --git a/crates/dinggerd/assets/pi/pi-agent-state.ts b/crates/dinggerd/assets/pi/pi-agent-state.ts
index aae40cc..e4e357d 100644
--- a/crates/dinggerd/assets/pi/pi-agent-state.ts
+++ b/crates/dinggerd/assets/pi/pi-agent-state.ts
@@ -111,6 +111,55 @@ function savedImportance(ctx: any): Importance {
return "normal";
}
+// The draft in the input box, as last reported: dingger marks a draft you
+// left behind. Each session's draft is also kept in a file of its own, so
+// it comes back when the session does after a crash, a restart, or
+// `pi --session`.
+let draft = "";
+const draftDir = path.join(path.dirname(socketPath), "drafts");
+// Enough to find the agent by it; the file keeps all of it.
+const reportedDraftChars = 500;
+
+function draftFile(): string | undefined {
+ return sessionFile ? path.join(draftDir, `${path.basename(sessionFile)}.draft`) : undefined;
+}
+
+function saveDraft(text: string): void {
+ const file = draftFile();
+ if (!file) {
+ return;
+ }
+ try {
+ if (text.trim()) {
+ fs.mkdirSync(draftDir, { recursive: true });
+ fs.writeFileSync(file, text);
+ } else {
+ fs.unlinkSync(file);
+ }
+ } catch {
+ // None to remove, or nowhere to keep it.
+ }
+}
+
+// The session's saved draft, into an empty input box.
+function restoreDraft(ctx: any): void {
+ const file = draftFile();
+ if (!file) {
+ return;
+ }
+ try {
+ if ((ctx.ui.getEditorText() ?? "") !== "") {
+ return;
+ }
+ const saved = fs.readFileSync(file, "utf8");
+ if (saved.trim()) {
+ ctx.ui.setEditorText(saved);
+ }
+ } catch {
+ // No saved draft.
+ }
+}
+
function requestId(): string {
return `${source}:${Date.now()}:${Math.random().toString(36).slice(2)}`;
}
@@ -128,6 +177,7 @@ async function report(state: AgentState | undefined, message?: string): Promise<
importance,
extension: extensionVersion,
...(controlListening ? { control: controlPath } : {}),
+ draft: draft.slice(0, reportedDraftChars),
seq: Date.now() * 1000,
...(sessionFile ? { agent_session_path: sessionFile } : {}),
},
@@ -269,10 +319,37 @@ export default function (pi) {
},
});
- // The latest context, for the control socket's checks.
+ // The latest context, for the control socket's checks and the input box.
let latestCtx: any;
let control: net.Server | undefined;
+ // Read the input box once typing pauses; report and save it when it changed.
+ let draftTimer: ReturnType<typeof setTimeout> | undefined;
+ let stopWatchingInput: (() => void) | undefined;
+
+ function checkDraft(): void {
+ let text: string;
+ try {
+ text = latestCtx?.ui?.getEditorText?.() ?? "";
+ } catch {
+ return;
+ }
+ if (text === draft) {
+ return;
+ }
+ draft = text;
+ saveDraft(text);
+ void report(undefined);
+ }
+
+ function checkDraftSoon(): void {
+ if (draftTimer) {
+ clearTimeout(draftTimer);
+ }
+ draftTimer = setTimeout(checkDraft, 500);
+ draftTimer.unref?.();
+ }
+
// 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", {
@@ -345,6 +422,17 @@ export default function (pi) {
}
pi.on("session_shutdown", () => {
+ // The input box as it is now: a quit right after typing comes before
+ // the next check.
+ if (draftTimer) {
+ checkDraft();
+ }
+ stopWatchingInput?.();
+ stopWatchingInput = undefined;
+ if (draftTimer) {
+ clearTimeout(draftTimer);
+ draftTimer = undefined;
+ }
if (!control) {
return;
}
@@ -368,6 +456,16 @@ export default function (pi) {
// A reload can replace this extension mid-run without emitting another agent_start.
agentActive = ctx?.isIdle?.() === false;
updateSessionFile(ctx);
+ restoreDraft(ctx);
+ // Before the first report, which carries it.
+ draft = "";
+ checkDraft();
+ stopWatchingInput?.();
+ // Every key reaches this before the input box: read the box after it.
+ stopWatchingInput = ctx.ui.onTerminalInput?.(() => {
+ checkDraftSoon();
+ return undefined;
+ });
// Each session has its own: /new starts at normal, /resume brings its own back.
importance = savedImportance(ctx);
announceImportance();
@@ -379,6 +477,8 @@ export default function (pi) {
agentActive = true;
updateSessionFile(ctx);
publishState();
+ // The prompt left the input box.
+ checkDraftSoon();
});
pi.on("agent_settled", (_event, ctx) => {
@@ -389,5 +489,7 @@ export default function (pi) {
agentActive = false;
publishState();
+ // Escape puts queued messages back into the input box.
+ checkDraftSoon();
});
}