Live CSS preview, and a warning when your change won’t hold
What this is
This release added live preview to the confirming step. When you type a target value, Lens applies it to the clicked element on the page in real time, so you see the change before you commit to a diagnosis. No save. No reload. The element just updates while you type.
It also added a dynamic warning. If the value you typed will be overridden by a higher-specificity rule, Lens says so and names the real source doing the overriding: a theme, a plugin, a page builder, an inline style, or a script. Pseudo-elements got the same treatment, so ::before and ::after now have selectable property cards, live preview, and full triage parity. The property list collapses while you type so the popup stops sitting on top of the element you’re trying to watch.
What it answers
– how do i preview a css change before applying it on my site
– why does my css change not hold even though it looks right
– what is overriding my css when my change keeps getting ignored
– how to see live css changes without editing the stylesheet
– how do i style a before or after pseudo element that won’t change
Why we built it
The diagnosis is more useful when you can watch the change land before you trust it. A value that won’t survive the cascade should say so while you’re still typing it, not after you’ve pasted it.
- Live preview applies a typed CSS value to the clicked element on the page so you see the change before committing it (popup.js, content.js)
- A dynamic warning says whether the change will hold and, when it won't, names the real source overriding it: theme, plugin, page builder, inline style, or script
- Pseudo-element (::before and ::after) diagnosis arrived, with selectable property cards, their own live preview, a pseudo-qualified diagnosis header, and full triage parity
- The property list now collapses while you type, keeping the selected property in view so the popup stops covering the element being previewed
- The prescription card now flags when an override is backed by a CSS variable, so you know the value diverges from the variable's definition
- Live preview now targets the exact element you clicked instead of the first selector match, using a direct element reference set at capture time
- Preview no longer silently fails on elements without a unique DOM id, since the matchCount guard was removed from the popup listener
- Two pseudo-preview helpers (lensPreviewApplyPseudo and lensPreviewClearPseudo) were called but never defined, throwing a silent error on every apply; both were added
- An ambiguous selector that matches more than one element now suppresses the preview and warning instead of previewing the wrong element
- Unmatched pseudo-element properties no longer crowd out authored ones, so real styles like color show up in the pseudo list
Honest CSS verdicts: Lens names the rule that’s actually winning
What this is
This release fixed the cases where Lens named the wrong rule as the winner. On Tailwind v4 and shadcn sites, a padding or margin diagnosis used to point at a 0px base reset instead of the utility class actually setting the value. Logical properties like padding-inline and margin-block now map to their physical longhands at capture, so the winner you see is the rule the browser is really applying.
The verdict copy got more honest across the board. A visibility:hidden ancestor stopped reporting the display:none “your changes have no effect” message and started getting its own finding with an overridable fix. The “only applies at this screen size” verdict now fires only when the rule actually doesn’t match your current viewport, and the engine stopped presenting any winner the browser’s computed value contradicts.
What it answers
– why does lens show a 0px padding winner on a tailwind site
– how do i find which css rule is actually winning on my page
– why does my element say my changes have no effect when it’s just hidden
– how to fix padding-inline and margin-block not matching the real value
– why does the css override work in one place but not the other
Why we built it
A diagnosis tool is worthless the moment it names the wrong rule. These fixes close the cases where the answer was confidently wrong.
- The diagnosis now reads in real human terms inside the popup fight view, not just on the triage page, so the explanation sits next to the cards
- Rules inside @supports and @container blocks are now captured, with a parse-coverage signal recording how many stylesheets could and couldn't be read
- Rule sources now carry friendly names like "Xstore theme" across the popup, fight view, and triage, instead of generic origin labels, and the inline-style and systemic-spacing notes no longer hardcode Elementor
- CSS logical properties (padding-inline, margin-block, inset, and logical border and sizing) now map to their physical longhands at capture time
- On Tailwind v4 and shadcn sites, padding and margin winners now show the real utility value instead of a 0px base reset (closes LIM-005)
- A visibility:hidden ancestor now gets its own finding and an honest, overridable verdict instead of the display:none "your changes have no effect" message
- The engine no longer names a winner the browser contradicts, and the "only applies at this screen size" verdict fires only when the rule doesn't match the current viewport
- The override box now produces an actionable fix for inline-style winners and picks a selector that beats the winning rule by construction, not by load order
- The override now shows the same result in the popup and on the triage page, with the rule engine as the single source of truth
Phase 1 ships: element coordinates captured, double-submit guard, and update interruption handling
Phase 1 of the build-out shipped. 4 correctness items that needed to be in place before any further work could begin.
The first: element coordinates are now captured with every pick. The x, y, width, and height of the element get stored in the capture alongside everything else. This is required for CDP Track 2, which will use the coordinates to resolve @layer priority on cross-origin sites in a later phase.
The second: a double-submit guard was added to the Diagnose button. Clicking it twice rapidly was able to consume 2 credits and write 2 session rows. The guard prevents startDiagnosing() from being re-entered while a diagnosis is already in flight.
The third and fourth: when an extension update happens while a capture is in progress, the service worker now writes a flag to storage. The next time the popup opens, it reads that flag, shows a notice that says “Lens updated while you were capturing. Pick your element again,” and resets to idle instead of trying to resume a capture that’s no longer valid.
What it answers
– why did lens charge me twice for one diagnosis
– what happens when lens updates while i’m capturing
– why does lens say pick your element again after an update
– what is element_rect in the lens capture file
– what shipped in lens phase 1
Why we built it
The double-submit bug was a real credit loss. The update interruption was a known bad state with no recovery path. Both needed to be closed before build-out.
- Element_rect field added to every capture: stores x, y, width, and height of the picked element. Required by CDP Track 2 in Phase 2B
- Service worker onInstalled handler now checks for an in-flight capture before clearing storage. If one is present, writes ll_update_interrupted with a timestamp
- InitPopup() reads ll_update_interrupted on open: if the flag is set, clears it, shows the update notice in the idle state, and stops there
- Double-submit guard added: _diagnosisInFlight module-level boolean prevents startDiagnosing() from being entered twice. Button is disabled for the duration and re-enabled in the finally block
Stabilisation: login loop fixed, capture race fixed, dead code removed
This release closed out the stabilisation pass before build-out begins. Nothing new shipped. Everything that was already in the extension got verified, fixed, or cleaned up.
The biggest fix was the login loop. When a diagnosis returned an auth error, the extension was routing to the login screen without clearing the capture from storage. On the next open, the capture was still there, the user clicked Diagnose again, hit the same error, and ended up in a loop. The fix adds a storage clear at both points in the code where auth failure can happen.
The Vision Capture race was also confirmed and fixed. The popup was opening and polling for capture data before content.js had finished writing it. The polling interval was widened from 200ms to 350ms, giving the write enough time to land.
Several pieces of dead code were removed: a side-panel block that was never reachable, an unused auth message handler in the service worker, and 2 popup functions that referenced HTML elements that don’t exist.
What it answers
– why does lens keep taking me back to the login screen
– why does lens show the idle state instead of my capture
– what was in the lens stabilisation pass
– why does lens not show my element after capturing it
– what dead code was removed from lens
Why we built it
The login loop was a direct user report. The Vision Capture race was confirmed live via diagnostics logs. Neither could go into build-out unfixed.
- Force Auth Failure button added to the diagnostics page to support auth debugging: clears the refresh token with a confirmation dialog
- Login loop fixed: capture keys (ll_capture_ready and ll_capture_result) now cleared before routing to the unauthenticated state on auth_invalid and account_not_found errors
- Login loop fixed on second path: same storage clear added to the no-token-after-retry path in startDiagnosing()
- Vision Capture race fixed: initPopup polling interval widened from 200ms to 350ms, extending the total window from 1,200ms to 2,100ms
- Dead side-panel block removed from initPopup(): _tabId, _isSidePanelActive, _allStorage full-storage read, _spFallback, and the unreachable isSidePanelActive block
- Dead REFRESH_AUTH_TOKEN message handler removed from service-worker.js: nothing in the codebase was sending this message
- Dead popup functions removed: getActiveTab (defined, never called) and renderAlternativeProblemOptions (referenced HTML elements that don't exist in popup.html)
Expanded confirming state, branding update, and auth stability fixes
The confirming state got a full redesign. When you capture an element, the popup now expands to 400px and shows the screenshot at full popup width, with the description field below it. The screenshot is right there in the popup: no separate preview page to open, no guessing whether the capture worked.
The extension also got a visual overhaul: new icons, a refreshed popup CSS system, and updated triage page styles.
2 auth stability fixes shipped alongside these. The first corrected a bug where any transient network failure was logging users out. The second removed a synchronous network call that was running after every successful diagnosis.
What it answers
– how do i see my screenshot in lens before diagnosing
– why does lens keep logging me out
– why does the lens popup look different
– what is the lens confirming state
– how do i know lens captured the right element
Why we built this
The screenshot existed in the confirming state but wasn’t visible without opening a separate page. The capture preview page solved that problem but added a step. The expanded popup removes the step entirely.
- Confirming state expanded to 400px with inline screenshot at full popup width and description field below
- New icons, popup CSS visual system, and triage CSS visual system shipped across the extension
- Ll_auth_expired now only written on 401 and 403 responses: transient network failures no longer trigger a logout
- RefreshCreditsAfterSuccess no longer makes a synchronous network call immediately after diagnosis
Diagnosis accuracy fixes: override routing, ancestor signals, and wrong-element hint
3 bugs in the diagnosis path were fixed in this batch, all of them causing Lens to route users to “ask a developer” when the problem was actually solvable.
The first: Lens was checking visual properties like box-sizing and transition for override safety, which caused Elementor sites to hit developer handoff on elements that were actually fixable. The check is now scoped to visual properties only.
The second: when a stylesheet’s origin couldn’t be traced, Lens was treating the element as undiagnosable. It wasn’t. The override confidence was raised and the guidance was rewritten without DevTools jargon.
The third: Lens was missing cases where an ancestor element was causing the problem, not the element you clicked. Line-height zero, inline-attribute spacing overrides, and anomalous container heights were added to the ancestor scan, and the Edge Function was updated to surface these as the primary finding when they’re present.
The wrong-element hint was also added: when the captured element has a transparent background or an unresolvable origin, a soft note tells the user the background may be coming from a parent and suggests picking one level up.
What it answers
– why is lens sending simple fixes to a developer
– why does lens say i need a developer when i think i can fix it myself
– why isn’t my elementor element getting a fix suggestion
– how does lens know when the problem is in a parent element
– what does wrong element mean in lens
Why we built it
Diagnosis accuracy is the only thing that matters. Routing a fixable problem to developer handoff is a failed diagnosis.
- Tier-1 ancestor signal pre-ranking added: LINE_HEIGHT_ZERO, INLINE_ATTRIBUTE_OVERRIDE, and ANOMALOUS_HEIGHT now surface as the primary finding when present, before secondary signals
- TOUCH_LIKELY_PROPERTIES expanded to include border-spacing, border-collapse, clip-path, mask, filter, backdrop-filter, transform, and transform-origin
- Context flags added: elementor_editor_active fires when the Elementor editor is open; inside_modal fires when the captured element is inside a dialog or modal ancestor
- SYSTEMIC_ZERO_SPACING pattern detection added: fires when 3 or more consecutive ancestors have vertical spacing zeroed by a plugin-internal rule
- Wrong-element hint added to the confirming state when the captured element has a transparent or unresolvable background
- Override_safe check scoped to visual properties only: box-sizing, transition, and other non-visual properties no longer trigger false developer handoff on Elementor sites
- Origin-unresolvable no longer treated as undiagnosable: override confidence raised to medium, DevTools jargon removed from strategy strings
- Capture_number was never incrementing between captures: storageGetSessionData now writes the next number back to storage after each use
Lens ships capture preview, security hardening, and a privacy policy rewrite
A lot shipped in this batch. The most visible change: clicking “View captured element” now opens a dedicated preview page with the full screenshot, a description field, the Diagnose button, and a privacy disclosure, instead of rendering an image inline in the popup.
The extension’s screenshot disclosure was also corrected. The old copy said screenshots were “processed and deleted within 30 days.” That wasn’t accurate. The new copy reads: “Stored locally in your browser. Sent to Anthropic only if you run a diagnosis, then deleted immediately.” The privacy policy was updated to match, with a new section on screenshot retention and a corrected Chrome Web Store Limited Use Disclosure.
On the security side, the dev diagnostics tool was accessible to any external URL. That’s been fixed, and a partial refresh token that was showing in the diagnostics page has been removed.
What it answers
– why does the lens extension keep signing me out
– how does lens handle my screenshots
– what data does lens send to anthropic
– how do i see what lens captured before running a diagnosis
– does lens store my website screenshots
- Capture preview page added: screenshot, description field, Diagnose button, Download button, and privacy disclosure, accessible from the confirming state
- Rotating diagnosis messages added during the diagnosing state: 5 messages cycling every 1,800ms, replacing an invisible status indicator
- Squarespace (YUI block IDs) and Webflow (w-node hex IDs) selector patterns added to the dynamic selector check, flagging CSS overrides as unsafe for both builders
- Crop TTL raised from 10 to 30 minutes, giving users more time between capture and diagnosis
- Idle state copy updated, Pick button renamed to "Click what looks wrong," result state visual treatment refreshed
- Random sign-outs caused by the MV3 service worker lifecycle fixed: the extension now attempts a token refresh before returning an auth error when the service worker wakes from idle
- Screenshot disclosure in popup and capture preview corrected from "deleted within 30 days" to accurate language describing local storage, Anthropic transmission, and immediate deletion
- Dev diagnostics page removed from web_accessible_resources, closing external URL access to the tool
- Partial refresh token display removed from the diagnostics page
Loupely Lens ships the Cadillac build: 6 new capture fields, mismatch detection, credit refunds, and bug fixes
What this is
The Cadillac build adds 6 structured fields to every Lens capture file: dom_structure, specificity_ladder, ancestor_scan, dangerous_properties, origin_intelligence, and session_diff. These aren’t extras. They’re what makes the developer briefing precise. Before this build, the capture file described what Lens found. Now it describes what to do about it — minimum specificity to win, ancestor constraints causing the gap above an element, properties that will make content invisible on certain backgrounds, and how the element’s CSS has changed since the last time you clicked it.
Two separate Haiku calls now run on every diagnosis. One writes the founder diagnosis in normal human terms. The other writes the situation_summary for the developer in the capture file — technical language, named selectors, specificity values, origin types. One prompt can’t satisfy both audiences. Two calls means neither output hedges for the other.
Mismatch detection runs before every diagnosis. When your description contains behavioral keywords — “won’t expand,” “doesn’t work,” “nothing happens” — and the CSS classifier found a visual problem, Lens flags the gap. The triage routes to developer handoff regardless of CSS confidence. The capture file still ships. The developer gets both the CSS picture and the flag.
The triage page is rebuilt as a 3-layer structure. Option 1 (try it yourself) appears only when the route is diy_override or diy_settings. Option 2 (developer or AI handoff) is always visible on every triage page, regardless of route, without any click to reveal. The feedback form sits collapsed behind a link. Every CSS override and settings instruction carries a permanent “In testing — verify before applying” badge.
If a diagnosis is wrong, the credit comes back. Submit the feedback form within 48 hours, select a reason, and Lens refunds 1 credit automatically. One refund per diagnosis. The feedback row writes to Supabase and fires a Resend notification on every submission — inside the window or out.
Three bugs closed. The Vision Capture timing race that left the popup showing idle instead of confirming is fixed with a 6-retry polling loop covering 1200ms. The MutationObserver false-positive that cancelled active picks on pages with widget DOM mutations is fixed with a node count threshold — 10 or more non-Loupely element nodes required to treat a mutation batch as navigation. The auth callback bug that cleared capture keys during token refresh is fixed with a preserve branch in the auth handler.
What it answers
– what’s in the loupely lens capture file
– how do i know what specificity to use to override a css rule
– why does the css fix not work when i apply it
– what does loupely lens do when something looks wrong on my site
– what happens if the lens diagnosis is wrong
Why we built it
The capture file has always carried the right raw data. The Cadillac build is the layer that turns raw data into a briefing a developer can act on without asking follow-up questions. Session diff tells you whether the last change made anything better. Ancestor scan tells you which parent element is causing the gap. Specificity ladder tells you exactly what to write to win the cascade. The data was there. Now it’s organized.
- BuildDomStructure added to capture pipeline — tag, classes, children (up to 5, sensitive elements redacted), rendered text, and child count on every capture
- BuildSpecificityLadder added to capture pipeline — winner, competitors (up to 10 per property), and override_prescription with minimum specificity, whether !important is required, and confidence rating
- BuildAncestorScan added to capture pipeline — scans ancestor chain for layout constraints, overflow conditions, and spacing sources; 7 flags including LIKELY_SPACE_CAUSE, WIDTH_CONSTRAINT, OVERFLOW_HIDDEN, and FLEX_CONSTRAINT
- BuildOriginIntelligence added to capture pipeline — aggregates all origins in stylesheet_map with editability, fix strategy, and !important count per origin
- BuildSessionDiff added to capture pipeline — diffs current capture against most recent matching capture by tag + sorted classes + URL + ancestor depth; reports changed properties with FIXED/REGRESSED/CHANGED assessment and remaining ancestor issues
- User_context block added to capture file — records founder description, timestamp, behavioral keyword match, detected problem class, and mismatch flag before the diagnose call
- Mismatch detection added to popup.js — case-insensitive substring match against 20 behavioral keywords; fires class_mismatch = true and routes to developer_handoff when description implies behavior and CSS classifier found a visual problem class
- Situation_summary added as second Haiku call in lens-diagnose.ts — technical developer briefing generated from structured capture fields, not raw CSS data; non-blocking, ships as null if call fails
- Triage.rationale added to diagnosis response — 1-2 sentences explaining why the route was chosen; generated in the same call as triage route; non-blocking
- Prompt iteration deployed to Supabase — CONSTRAINT_PROPS expanded to include padding and margin properties; diagnosis quality gate-tested on 3 captures
- Triage page rebuilt as 3-layer structure — Option 1 (diy routes only), Option 2 (developer/AI handoff, always visible, never hidden), feedback form (collapsed by default behind inline link)
- Feedback table created in Supabase — 12 columns including session_id UNIQUE constraint, denormalized problem_class/triage_route/page_builder, refund_issued flag, and product column for future Core compatibility
- Lens-feedback Edge Function deployed — JWT validation, session ownership check, duplicate guard (409 before DB constraint), denormalization, 48-hour refund window check, credits.balance +1 increment, Resend notification on every submission
- Credit refund wired end-to-end — 1 credit, within 48 hours, one per session_id; annual plan users get refund_issued = false (no credits to refund), feedback row and Resend fire on the same terms
- FORCE_CREDIT_REFRESH sent to service worker after confirmed refund — popup credit balance updates without requiring a sign-out/sign-in cycle
- Bug 1 fixed: popup showed idle state instead of confirming after a capture — Vision Capture round trip (up to 1048ms) completed after initPopup read storage; fixed with 6-retry / 1200ms polling loop after getCreditStatus; confirmed 7/7 on second test day (v1.0.78)
- Bug 2 fixed: picker overlay disappeared after first click before second click was possible — MutationObserver was firing on page widget DOM mutations (8 nodes on fibermarketexchange.com) and treating them as AJAX navigation; fixed by replacing mutations.some() with a node count threshold of 10 or more non-Loupely element nodes; confirmed with full pick on previously-failing page (v1.0.80)
- Bug 3 fixed: auth callback was clearing capture keys during token refresh — LOUPELY_LENS_AUTH_CALLBACK read ll_capture_ready before clearing and preserved it when present (v1.0.70/1.0.75)
- Bug 4 fixed: picker_pending was cancelled within 1ms on some pick attempts — same root cause as Bug 2; resolved by the same MutationObserver node count threshold fix (v1.0.80)
wp-admin guard and null result guard shipped
What this is
2 session-breaking bugs were found in live testing and fixed in a single release. The first: if you clicked an element that was part of the WordPress admin bar, Lens ran a diagnosis on it, consumed a credit, and returned a result for an element that only exists for logged-in admins. The fix built a 3-layer guard: content.js flags is_wp_admin via closest(‘#wpadminbar’), popup.js checks the flag before entering the confirming state, and lens-diagnose.ts verifies server-side. The guard was verified live against span.ab-icon, an admin bar element that had been picked in testing without the user realising it.
The second: if the AI returned an empty result, the popup showed a blank screen and still decremented the credit. The null result guard fixed this. When claudeResult is null, the session row is flagged null_result: true, no credit is decremented, and the popup shows a specific message instead of a blank screen.
What it answers
– why did loupely lens diagnose my wordpress admin bar
– does loupely lens charge me if it diagnoses the wrong element
– what happens if loupely lens returns an empty diagnosis
– does loupely lens refund credits for a null result
– how does loupely lens detect the wordpress admin bar
Why we built it
A credit charged for a null result and a credit charged for an admin bar element are both the same broken promise. Both were caught in testing and fixed before any paying user could hit them.
- Wp-admin guard added to content.js: is_wp_admin flagged via closest('#wpadminbar') and written to capture meta
- Wp-admin check added to popup.js: handleSuccessfulCapture checks is_wp_admin before showConfirmingState(), clears keys and shows idle if true
- Wp-admin guard added to lens-diagnose.ts: server-side guard uses DOM flag only (session_fields.is_wp_admin), stylesheet-origin branch removed to prevent false positives on front-end WordPress pages
- Null result guard added to lens-diagnose.ts and popup.js: empty claudeResult flags null_result: true on the session row, skips credit decrement, shows specific popup message
- Admin bar picks no longer consume a credit or run a diagnosis; popup returns to idle with a specific message
- Empty AI responses no longer consume a credit; null result guard catches the failure before it reaches the user
Ancestor traversal optimised: PERF-4 budget met on plugin-heavy sites
What this is
The ancestor traversal performance budget was exceeded in the previous release. The root cause was buildMatchedRuleMap: it ran a CSS selector match test against every rule in every stylesheet for every ancestor level, making the cost O(ancestors times total rules). On a 121-stylesheet fixture, that was about 96,000 match tests per pick, with buildMatchedRuleMap accounting for 93% of total ancestor time.
The fix restructured the function so per-rule preprocessing (selector split, pseudo-strip, origin classification) ran once per page and the result was reused across the full ancestor chain. A conservative pre-filter was added to fast-reject simple single-token rules before calling el.matches(). The output-equality contract was verified: the rule map fingerprint was identical before and after the change across 30 maps and 646 total entries.
The final performance sweep at 5 runs showed ancestor traversal at 135 to 163 milliseconds across all runs. Every run under budget, with 37 milliseconds of headroom at worst.
What it answers
– why is loupely lens slow on wordpress with many plugins
– how fast is loupely lens on a plugin heavy wordpress site
– does loupely lens work on sites with 100 plus stylesheets
– how did loupely lens fix its ancestor traversal performance
– what is the loupely lens performance budget for css capture
Why we built it
The target user has a plugin-heavy WordPress site. That is the exact profile that hits 100-plus stylesheets and would have felt the performance problem hardest. The fix was required before launch.
- BuildPreparedRuleIndex added: builds a once-per-page prepared rule array (origin, source URL, sub-selectors pre-split and pseudo-stripped), reused across all ancestor levels
- Conservative pre-filter added to buildMatchedRuleMap: simple single-token rules (tag, .class, #id) that cannot match the element are skipped before el.matches()
- Output-equality contract verified: fingerprint 3984cee64d5440c6689b63532e509c86207c9f7afc2c1d8036c6bad7c9a56e66 identical before and after across 30 rule maps
- PERF-4 final sweep: 135-163ms across 5 runs on the 121-stylesheet stress fixture, all under the 200ms budget