Skip to content

[8/8] Stop a failed task save from looking like a saved one - #280

Open
alex-clickhouse wants to merge 1 commit into
alex-clickhouse/task-board-columnsfrom
alex-clickhouse/fix-silent-save-failure
Open

[8/8] Stop a failed task save from looking like a saved one#280
alex-clickhouse wants to merge 1 commit into
alex-clickhouse/task-board-columnsfrom
alex-clickhouse/fix-silent-save-failure

Conversation

@alex-clickhouse

Copy link
Copy Markdown
Collaborator

Stacked on #279. Targets alex-clickhouse/task-board-columns and must merge after it.
The diff shown against that base is the three files below; anything else is inherited from the stack.

What

A failed save of a task's markdown body was indistinguishable from a successful one.

  • taskStore.saveTaskContent caught the exception and returned Promise<void>, so no caller could tell.
  • TaskDetailBody.handleSave then cleared dirty regardless, 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. dirty is also what arms the
content-sync guard added in #276 (if (task.content != null && !dirty)). 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 disappears from the textarea too. There is a test for exactly this.

Why now

#276 makes TaskDetailBody the single editor for both the full page and the board's modal, so
this is now the only path by which task content gets written from the UI. Fixing it here covers both
surfaces; fixing it on main would patch TaskDetailPage.handleSave, which #276 deletes.

Cmd/Ctrl+S routes through the same handleSave, so it is covered by the same change.

How

  • taskStore.tssaveTaskContent resolves boolean instead of hiding the outcome. 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 itself. TaskDetailBody is the only caller (rg saveTaskContent web/src), so
    the signature change is contained.
  • TaskDetailBody.tsx — keep dirty set unless the write actually landed, which keeps both the
    retry and the sync guard in place. A role="alert" message sits next to the Save button, 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 already moved past.

One rendered element, not two branches: because a failure now leaves dirty true, a !dirty error
branch 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:

Spec Fails pre-fix with
says the save failed and keeps offering the retry no alert; Save button absent
keeps the failed edit safe from an update arriving underneath textarea reads # rewritten elsewhere — the edit is gone
clears the failure once a retry succeeds no alert
clears the failure as soon as the user edits again no alert
  • npm test88 passed (84 + 4 new)
  • npm run build (tsc -b + vite) — clean. The Promise<boolean> change is the kind tsc
    catches and vitest does not, so the typecheck matters here.
  • npx eslint src146 problems, identical to this branch's baseline; zero in the three
    touched files
  • .venv/bin/pytest tests/ -q3113 passed, unchanged (the diff is frontend-only)

Out of scope

updateStatus and the other store actions swallow errors the same way. Same class of bug, much
smaller blast radius — worth its own pass rather than widening this one.

🤖 Generated with Claude Code

`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>
@alex-clickhouse
alex-clickhouse requested a review from Copilot August 5, 2026 12:56
@alex-clickhouse
alex-clickhouse marked this pull request as ready for review August 5, 2026 12:56

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.saveTaskContent to return Promise<boolean> so callers can detect save failure.
  • Update TaskDetailBody to keep dirty set (and show an inline role="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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants