Auto-save annotation drafts to survive server crashes (#217)

* feat: auto-save annotation drafts to survive server crashes

Adds server-side draft persistence so annotations are not lost when the
server process dies. Drafts are saved to ~/.plannotator/drafts/ keyed by
a SHA-256 content hash, debounced at 500ms. On reload, a dialog prompts
the user to restore or dismiss. Drafts are auto-deleted on approve/deny/feedback.

Closes #212

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: remove duplicate imports and update CLAUDE.md for draft feature

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: wrap handleRestoreDraft in useCallback and update shared-handlers comment

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Michael Ramos
2026-03-04 10:12:58 -08:00
committed by GitHub
parent d9c2d46113
commit aa71ede0f3
11 changed files with 443 additions and 7 deletions
+12 -1
View File
@@ -12,7 +12,8 @@
import { isRemoteSession, getServerPort } from "./remote";
import { type DiffType, type GitContext, runGitDiff } from "./git";
import { getRepoInfo } from "./repo";
import { handleImage, handleUpload, handleAgents, handleServerReady, type OpencodeClient } from "./shared-handlers";
import { handleImage, handleUpload, handleAgents, handleServerReady, handleDraftSave, handleDraftLoad, handleDraftDelete, type OpencodeClient } from "./shared-handlers";
import { contentHash, deleteDraft } from "./draft";
// Re-export utilities
export { isRemoteSession, getServerPort } from "./remote";
@@ -82,6 +83,8 @@ export async function startReviewServer(
): Promise<ReviewServerResult> {
const { htmlContent, origin, gitContext, sharingEnabled = true, shareBaseUrl, onReady } = options;
const draftKey = contentHash(options.rawPatch);
// Mutable state for diff switching
let currentPatch = options.rawPatch;
let currentGitRef = options.gitRef;
@@ -185,6 +188,13 @@ export async function startReviewServer(
return handleAgents(options.opencodeClient);
}
// API: Annotation draft persistence
if (url.pathname === "/api/draft") {
if (req.method === "POST") return handleDraftSave(req, draftKey);
if (req.method === "DELETE") return handleDraftDelete(draftKey);
return handleDraftLoad(draftKey);
}
// API: Submit review feedback
if (url.pathname === "/api/feedback" && req.method === "POST") {
try {
@@ -194,6 +204,7 @@ export async function startReviewServer(
agentSwitch?: string;
};
deleteDraft(draftKey);
resolveDecision({
feedback: body.feedback || "",
annotations: body.annotations || [],