Skip to content

fix: surface rejected-draft and stash losses, and say what Format Document did - #459

Merged
mtskf merged 8 commits into
mainfrom
chore/rescue-rejected-draft-loss
Oct 11, 2026
Merged

mtskf merged 8 commits into
mainfrom
chore/rescue-rejected-draft-loss

Conversation

@mtskf

@mtskf mtskf commented Oct 11, 2026 •

Copy link
Copy Markdown
Owner

Summary

Four outcomes that used to be silent now tell the user what happened, each by reusing an existing toast or the existing notice slot. No recovery store, no new reducer effect, no protocol change.

Changes

  • Rejected draft lost on tab close — a draft the write-gate refused never reaches the TextDocument, so VS Code shows no save prompt and revert-rescue (which compares TextDocument bytes) cannot see it. On dispose the panel now reads a rejection that is still pending when the tab closes and reopens the draft as an untitled Markdown document, with one warning toast; the rescue runs before the rest of the teardown, so an unrelated teardown failure cannot take the draft with it. The guarantee covers an ordinary tab close; a window close or reload is best effort, because nothing awaits onDidDispose. Not covered: an edit stashed behind an in-flight save and refused only when it drains after the tab has closed keeps its existing Cannot save toast and is not reopened (tracked as a follow-up).
  • Rejected draft replaced by an external write — the discard notice from fix(webview): announce a rejected draft that an external write replaces #458 gains a "Copy draft" button. The pre-replace text is held in one slot (latest wins, emptied by dismiss) and read at click time; a last-attempt-wins counter keeps a late clipboard result from mislabelling a newer draft.
  • Stash dropped after dispose — a post-dispose settlement that drops the stashed edit on an observed mismatch or a divergence now shows one toast, worded without assuming an external write. It stays silent when the observed document already carries the stash's text. The alive path is unchanged.
  • Format Document — runFormatDocument returns formatted | unchanged | readonly | too-large | failed instead of a boolean, and the shell says which. "No changes were needed" is no longer indistinguishable from a failure, and the failure text does not claim nothing changed (a dispatch that throws is not guaranteed to roll back). These notices rank below every loss notice and are cleared by a successful format.

Test-only plumbing: the E2E harness gains openRescuedDraftOverride (same pattern as the existing overrides) and installs a no-op by default, so suites that are not about the rescue never inherit a dirty untitled document. Only the rescue's own suite exercises the real open.

Not included

A general recovery store, persistence, keeping a rejected draft on screen behind an unresolved-state gate, and an untitled export for the post-dispose stash drop (notification only).

Related TODO

ARCH-05 (05a-1, 05a-2, 05a-3, 05d).

Test plan

Each slice's tests were observed red against the unchanged source first.

  • pnpm compile
  • pnpm lint
  • pnpm vitest run — 294 files, 5883 tests
  • pnpm test:browser — 82 tests
  • pnpm test:e2e — 117 passing, twice in a row
  • pnpm check:doc-sync
  • pnpm package — vsix audit clean
  • Manual: close a Quoll tab while a "Cannot save" banner is showing and confirm the draft opens as an untitled document

mtskf added 8 commits October 11, 2026 13:26
…smatch

After the tab closes, a pending edit held behind an in-flight write has no webview replay buffer to fall back on. When the settlement shows the document no longer matches what Quoll just wrote (or the executor reported a divergence), that edit was dropped with only a log line. Now the user gets a toast telling them to reopen the file, unless the document already contains the dropped text. The alive path is unchanged.
…ected draft

A draft the host refused stays on screen until another writer changes the
file; the reseed then replaced it and the discard notice could only say the
text was gone. The notice now carries a Copy draft button that puts the
replaced text on the clipboard, so the user can paste it back.

The text is held in one slot: a later replaced draft overwrites it, and
dismissing the notice or closing the editor drops it. A discard reported by
edit-sync carries no text and leaves a still-copyable draft alone.
…d, or failed

Format Document used to look identical whether the document was already
formatted, read-only, too large to format, or the formatter had thrown:
nothing happened. Each of those now shows a short notice in the existing
notice slot, and a run that does format clears it.

The feedback ranks below every loss notice, so it never replaces one. The
failure text makes no "nothing was changed" claim, because a dispatch that
throws after updating the document is not rolled back.
…s tab closes

When Quoll refuses to save an edit, the refused text exists only in the editor: the file and VS Code's document never held it, so closing the tab asked nothing and the text was gone. Closing the tab now reopens that draft as an untitled Markdown document and says why the edit could not be saved.

The guarantee covers an ordinary tab close in a live window; a window close or reload is best effort.

E2E routes the reopen through a harness override that is a no-op by default, so suites that are not about the rescue never inherit a dirty untitled document.
The rescue ran last in onDidDispose, after the disposed dispatch, the
lint-mirror removal and the revert-rescue. A throw from any of those
skipped it, and the refused draft's only copy went with the tab. It
reads state.rejection and touches nothing else, so it now runs first;
a source-order test pins the position.
The existing test only set up a discard, the top rank, so raising the
format kinds' rank left it green: nothing pinned that a format result
is declined behind the three weaker loss kinds, or leaves them alone
when a format succeeds.
…e behaviour

- rejected-draft rescue: the guarantee covers a rejection pending when
  the tab closes; a stash refused on a post-dispose drain is not covered
- host-session-core: a stash dropped post-dispose on an ok settle gets
  a toast, so the !canDrain header no longer says the output is empty
- shell showNotice: list the format-document writer and its declines
- styles: the notice surface is shared by every notice kind, and one
  modifier does carry a rule
@mtskf
mtskf merged commit 8588e76 into main Oct 11, 2026
2 checks passed
@mtskf
mtskf deleted the chore/rescue-rejected-draft-loss branch October 11, 2026 03:27
mtskf added a commit that referenced this pull request Oct 11, 2026
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.

1 participant