[8/8] Stop a failed task save from looking like a saved one - #280
Open
alex-clickhouse wants to merge 1 commit into
Open
[8/8] Stop a failed task save from looking like a saved one#280alex-clickhouse wants to merge 1 commit into
alex-clickhouse wants to merge 1 commit into
Conversation
`saveTaskContent` swallowed its exception and returned void, and `handleSave` cleared `dirty` regardless. So an offline save, a 500, or an expired session all ended the same way a success does: the Save button disappears and the editor looks settled, while the only copy of the text is still in the browser. The user finds out on the next reload. The lost-edit path is worse than the missing button suggests. `dirty` is also what arms the content-sync guard, so clearing it on a failure hands the unsaved text to the next re-render that carries a `content` — the edit is not merely un-persisted, it is gone from the textarea too. The store now resolves a boolean instead of hiding the outcome, and the editor keeps `dirty` set unless the write actually landed, which keeps both the retry and the guard in place. A message next to the button says so, because the detail view had no error surface at all; it clears on the next keystroke, since by then it is describing text the user has moved past. `TaskDetailBody` is the only caller, so the signature change is contained. Errors are reported rather than rethrown: the editor needs to know the write failed so it can hold on to the text, not to handle the error. `updateStatus` and the other store actions swallow errors the same way. Same class of bug, much smaller blast radius — left for its own pass rather than widening this one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR fixes a UX/data-loss issue where failed task content saves were indistinguishable from successful saves, causing the editor to clear its “dirty” state and potentially discard unsaved edits on subsequent rerenders.
Changes:
- Change
taskStore.saveTaskContentto returnPromise<boolean>so callers can detect save failure. - Update
TaskDetailBodyto keepdirtyset (and show an inlinerole="alert"message) when saves fail, clearing the error on next edit. - Add focused unit tests covering failure visibility, retry behavior, and protection against rerender clobbering.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
web/src/stores/taskStore.ts |
Return a boolean outcome from saveTaskContent and preserve existing error logging. |
web/src/components/Tasks/TaskDetailBody.tsx |
Preserve dirty on failed saves and surface an accessible inline failure message. |
web/src/components/Tasks/TaskDetailBody.test.tsx |
Add tests asserting failure UI, retry clearing, and no-clobber behavior under rerenders. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
A failed save of a task's markdown body was indistinguishable from a successful one.
taskStore.saveTaskContentcaught the exception and returnedPromise<void>, so no caller could tell.TaskDetailBody.handleSavethen cleareddirtyregardless, which hides the Save button.Offline, a 500, or an expired session therefore all ended exactly the way a success does: the button
disappears, the editor looks settled, and the only copy of the text is still in the browser. The user
finds out on the next reload.
The lost-edit path is worse than the missing button suggests.
dirtyis also what arms thecontent-sync guard added in #276 (
if (task.content != null && !dirty)). Clearing it on a failurehands the unsaved text to the next re-render that carries a
content— the edit is not merelyun-persisted, it disappears from the textarea too. There is a test for exactly this.
Why now
#276 makes
TaskDetailBodythe single editor for both the full page and the board's modal, sothis is now the only path by which task content gets written from the UI. Fixing it here covers both
surfaces; fixing it on
mainwould patchTaskDetailPage.handleSave, which #276 deletes.Cmd/Ctrl+Sroutes through the samehandleSave, so it is covered by the same change.How
taskStore.ts—saveTaskContentresolvesbooleaninstead of hiding the outcome. Reportedrather than rethrown: the editor needs to know the write failed so it can hold on to the text, not
to handle the error itself.
TaskDetailBodyis the only caller (rg saveTaskContent web/src), sothe signature change is contained.
TaskDetailBody.tsx— keepdirtyset unless the write actually landed, which keeps both theretry and the sync guard in place. A
role="alert"message sits next to the Save button, becausethe detail view had no error surface at all. It clears on the next keystroke, since by then it is
describing text the user has already moved past.
One rendered element, not two branches: because a failure now leaves
dirtytrue, a!dirtyerrorbranch would be unreachable.
Testing
Four new specs in
TaskDetailBody.test.tsx, all of which fail against the unfixed component —verified by reverting the two source files and re-running:
# rewritten elsewhere— the edit is gonenpm test— 88 passed (84 + 4 new)npm run build(tsc -b+ vite) — clean. ThePromise<boolean>change is the kindtsccatches and
vitestdoes not, so the typecheck matters here.npx eslint src— 146 problems, identical to this branch's baseline; zero in the threetouched files
.venv/bin/pytest tests/ -q— 3113 passed, unchanged (the diff is frontend-only)Out of scope
updateStatusand the other store actions swallow errors the same way. Same class of bug, muchsmaller blast radius — worth its own pass rather than widening this one.
🤖 Generated with Claude Code