fix(editor): never let the editor save over a style it never loaded - #1008
Merged
Merged
Conversation
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>
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.
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()isitems['styles'] || {}, so a storage read that comes back empty still letsGetStylesForPageanswer successfully with{ styles: [], defaultStyle: undefined }. The content script then:url = document.domain— which is exactly how site-wide styles are keyedcss = ''ready, so the editor opens completely normallyAny 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
SetStylenever 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:
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.cssis 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
modifiedTimeand 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:
applyCssrefuses an empty save when nothing was loadedapplyCssstill clears a style the editor was showing667 unit tests, e2e 44/44, lint and typecheck clean.
Deliberately not included
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