Skip to content

fix(editor): never let the editor save over a style it never loaded - #1008

Merged
ankit merged 5 commits into
v4from
claude/stylebot-style-loss-a63078
Sep 23, 2026
Merged

ankit merged 5 commits into
v4from
claude/stylebot-style-loss-a63078

Conversation

@ankit

@ankit ankit commented Sep 23, 2026 •

Copy link
Copy Markdown
Owner

Opening Stylebot on a site could delete that site's style outright, and with sync on the deletion fanned out to Drive and other devices.

Root cause

The editor cannot tell "your style failed to load" from "this page has no style".

getAll() is items['styles'] || {}, so a storage read that comes back empty still lets GetStylesForPage answer successfully with { styles: [], defaultStyle: undefined }. The content script then:

  • keeps url = document.domain — which is exactly how site-wide styles are keyed
  • keeps css = ''
  • resolves ready, so the editor opens completely normally

Any save from that state writes empty css, and set() treats empty css as "delete this style". Opening the editor was enough to lose it. The pop-out window made it likelier, because the window is a second writer that saves on mount.

Reproduced via an equivalent route — a tab loaded before the style existed, since SetStyle never pushes to other tabs: style present, open the editor, a zero-length write lands, entry gone.

Fix

One guard, at the chokepoint every editor write passes through:

// applyCss
if (!css && !state.css) {
  return;
}

Holding no css, there is nothing to clear. Verified this alone stops the reproduction, so it covers any route in — including a failed read.

Deleting a style still works: that is always a non-empty → empty transition, so state.css is set and the guard doesn't fire. Confirmed end to end (storage ["localhost"] → [], css removed from the page), and the existing grayscale-reset and code-typing e2e tests exercise real deletes.

Also: the code editor now prunes empty rules only when that changes something, so opening it no longer re-saves identical css — which was restamping modifiedTime and scheduling a sync on every open.

Tests

Unit tests — this is store behaviour and doesn't need a browser. Both were verified to fail against the pre-fix code, not merely to pass:

  • applyCss refuses an empty save when nothing was loaded
  • applyCss still clears a style the editor was showing

667 unit tests, e2e 44/44, lint and typecheck clean.

Deliberately not included

  • Refreshing the stored style on open. An earlier revision re-read storage as the editor opened, so the panel never showed a stale or blank copy. Dropped: it isn't what prevents the deletion, and it made every open wait on a background round trip. The blank-panel symptom and the cross-tab lost update are separate bugs.
  • getAll() masking read failures. Every caller still can't distinguish a failed read from an empty profile. The editor can no longer destroy data because of it, but the underlying ambiguity remains.

🤖 Generated with Claude Code

ankit and others added 5 commits September 22, 2026 21:48
The code editor prunes leftover empty rules on mount and whenever a new
selector is picked, saving the result unconditionally. Saving empty css
deletes the style, so any time the store's css was empty the editor wiped
the style just by being opened.

That state is reachable: after an SPA navigation the editor's url stays on
the old page's style, and the next push of styles to the tab (a sync pull
does this) reads the style as removed and clears the css while keeping the
url. Opening the editor in Code mode then deleted it from storage.

Prune only when there is something to prune, which also stops every editor
open from restamping modifiedTime and scheduling a sync.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A pull that lands the style with its css emptied leaves the editor pointing
at it with nothing to show, which is the same poisoned state an SPA
navigation produces -- and needs no navigation at all.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The editor cannot tell "your style failed to load" from "this page has no
style": a storage read that comes back empty answers GetStylesForPage with
no default style, so the store keeps document.domain as its url with empty
css, and the editor opens normally on it. Saving empty css deletes the
style, so opening the editor was enough to lose it -- and with sync on, the
deletion fans out.

Guard the one chokepoint every editor write goes through: holding no css,
there is nothing to clear. Refresh the stored style as the editor opens too,
so it neither shows nor edits a stale copy -- nothing pushes a style saved
from one tab to the others.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Replaces the editor-window e2e test: these are store and listener
behaviours, so they don't need a real browser to prove.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The guard in applyCss is what prevents the deletion; the refresh only kept
the panel from opening on a stale copy, which is a separate bug and not
worth making every open wait on the background for.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@ankit
ankit merged commit 1269367 into v4 Sep 23, 2026
5 checks passed
@ankit
ankit deleted the claude/stylebot-style-loss-a63078 branch September 23, 2026 03:25
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