diff options
Diffstat (limited to 'crates/dinggerd/assets/pi/pi-agent-state.ts')
| -rw-r--r-- | crates/dinggerd/assets/pi/pi-agent-state.ts | 104 |
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(); }); } |
