e22007dc50
Plan.tsx's handlers swallowed their rejections and onUpdate was typed to return void, so WishEditor could not observe a failed save. It inferred the outcome instead -- comparing the draft it sent against the next wishes prop, and reading "isSaving went false without a match" as failure. That coupled save status to refetch timing and could not tell a network error from a slow refresh. The handlers now return their promise, onUpdate accepts `void | Promise<void>` so void-returning callers still typecheck, and WishEditor awaits and catches the real outcome. The inference machinery (sentDraftRef, prevIsSavingRef and the comparison effect) is removed rather than left alongside, since two sources of truth can disagree. The save/refetch/save loop guard is untouched: it lives in the draft reseed effect, which only fills a row that has no draft yet, and saves only ever originate from an explicit onChange. Added a comment saying so. A failed row now keeps its edit and offers an explicit retry, rather than requiring another edit to retrigger the debounce. Per-row failures report inline only -- reporting them in the page banner too would double-report the same error. Also removes wishEditor.saveButton/savingButton, dead since the per-row Save button was replaced by auto-save. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RXMhc9nT6sNz1SL4j8dznL