The page root Container's default 20px padding framed every AI-built section
in a white margin (visible in every eval screenshot). New AI page/site
replaces now set padding 0 on the page root unless the model set padding
itself. Saved pages, section inserts, patches and the Container default are
unchanged, so existing sites do not move.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Eval 2026-11-01: "Build my home page following the layout of the attached
mockup" came back as scope "site", and confirming replaced every page. The
editor now decides from what the owner asked (sitesmith/replace-plan.ts):
- scope "site" is honoured (with the existing confirm) only for an explicit
whole-site request ("build me a website", "the whole site", "every page",
"a 4 page site"); otherwise the first page replaces the current page and a
note says so.
- the shared header/footer are replaced with a page only when they are still
the untouched starter ones ("MySite" + "Get Started") AND the owner says yes
in a new dialog mode ("Use the new header and footer" / "Keep my header and
footer"); customised chrome is always kept, with a note.
- the editor sends chrome_default:true so the backend can let the model
restyle the starter chrome on a page request.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An empty patch whose message claims a change ("Fixed the hero ...") is now
status 'claimed-no-ops' with a warning note: "Sitesmith's reply describes a fix,
but it sent no changes, so nothing on your page was changed." The structured
render_mismatch flag from newer backends is read first; the existing wording
check still covers older replies.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Measured in Chromium and Firefox: the CSSOM exposes `background: var(--x)` and
`border: 1px solid var(--x)` only as pending-substitution longhands whose values
read back as "", so the capture sanitiser (which re-serialises the parsed
declarations) dropped them. The self-review screenshot then showed dark heroes,
image placeholders and card borders as white, and the model "fixed" things that
were fine on the real page.
The sanitiser now emits the declared shorthand once, at its first longhand,
through the same value/URL gate as every other declaration. Sandbox, CSP and
the single URL allow-list are unchanged.
- scripts/e2e-capture-fidelity.mjs: real-browser test through the whole capture
path (sanitise -> inline -> sandboxed frame -> html-to-image), decodes the
JPEG and samples pixels. Failed 9/12 checks per browser before, passes after.
- e2e-capture-security: hostile var() shorthand / var() fallback url() probes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Stored pages (generated at editor-save time) from before release 2026.11.5 carry
FeaturesGrid / Testimonials / NumberCounter grids without data-wsb-grid, so a
republish kept repeat(N,1fr) on phones (OLS home: body 594px wide at 360).
The <=767px block now matches the exact inline styles those emitters stored
(whole-attribute signatures; no spaces, as in git history and every stored
OLS/TTM page). Gallery shares the prefix with a free gap and keeps its columns:
the two colliding signatures carry :not(:has(> * > Gallery thumbnail)) in their
own rule (a browser without :has drops only that rule). Customer grids never
match. Mirrored byte-for-byte by WHP generateCompiledHTML (fix/legacy-grid-backend).
Tests: vitest selector contract (legacy shapes match, == current emitters minus
the hook, Gallery and customer grids never match, pretty == minified); head
parity pins updated; new real-browser suite test:browser:legacy-grid (Chromium +
Firefox, 360/390/768/1280) on the stored OLS home page + synthetic legacy grids;
colors suite folds the new rules back like the mobile-guard ones.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Contrast auto-repair read only style.backgroundColor / bgColor, so white text on a
gradient hero (style.background) was "repaired" against the white page colour.
- Resolve style.background (solid shorthand, gradient, url), HeroSimple bgGradient*/
bgColor defaults, CallToAction bgValue, BackgroundSection bgColor, opaque image overlay.
- Gradients: every stop is checked, worst stop decides; unparseable stops (color-mix,
unresolved token, translucent, layered) = not checked. An unresolvable background no
longer falls through to an outer/page colour.
- A repair is applied only if it reaches the target and strictly beats the old colour
against the worst stop.
- clamp() font sizes use their minimum for the large-text test.
- Patch path: ancestor background now comes from effectiveBackgroundOf (which learns
style.background shorthand and section props) instead of a private walker.
- Tests: real ttm-likes and ols-home responses as fixtures.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Craft's <Element id=col-N is={Container}> only adopts an existing linkedNode whose
type is Container; otherwise it creates a fresh empty Container and orphans ours.
flattenTreeForCraft now wraps a non-Container column child in a pre-created Container.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A. A page/site replace lands on the canvas in loadState's deferred
deserialize, so apply() returned before the canvas changed and the
self-review composed, captured and summarised the PREVIOUS page (the live
eval: every review said "the render doesn't match the page I built").
PageContext.whenCanvasSettled() resolves when the requested load really
ran; apply() awaits it (bounded, 5 s) and returns the applied fingerprint
(from the response tree for replaces). The review checks the live canvas
against it right before composing and skips with a plain note otherwise.
Header/footer/title come from latestChrome() (refs updated in the same
tick), not the modal's one-render-old context.
B. Review outcomes are fixed / fine / mismatch / not-applied / skipped and
quote the model. An empty patch where the model says the render was not
its page is "did not work this time", never "found nothing to fix"; an
ask is not a review; ops naming missing nodes are "none could be
applied" (applyPatch now counts ops that took effect).
E. Pictures from earlier sessions are labelled "saved earlier" and never
preselected; a line above the box lists what goes with THIS message;
multi-file uploads select every file. Clear chat says it deletes the
site's whole conversation for everyone (one thread per site, backend).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The backend now ends a job with RESPONSE_TOO_LONG when the model answer is
cut off at the output cap twice. Without the status in the union it fell
back to the generic ERROR text; now it says nothing was changed and to ask
for one page or section at a time.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Tables inside HtmlBlocks are left alone on phones: display:block (the only way to make one scroll)
shrank a fitting width:100% table's rows from 349px to 77px at 390. Wide tables stay clipped as before.
The <pre> rule (max-width:100%;overflow-x:auto, no display change) and the media rules stay.
- Browser suite: width:100% table still spans the screen (fails on cfeeef3's rule: first row 78px); the
10-column bare table moved to htmlwide (clipped, page never scrolls sideways).
- e2e-color-system: default COLOR_BASE_COMMIT ab7a100 -> 69fd34b, the first commit with the 2026.11.4
published head (ColumnLayout stacking + Navbar wrap) and none of the colour feature. ab7a100 predates
that head, so check 5 failed on main itself. This branch's own head change (mobile guard) is folded back
by exact string, asserted present once, before comparing, so the check still proves the colour feature
changed nothing else. Passes on 69fd34b; fails (as it should) against ab7a100.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review of bb4fb75: overflow-x:auto on [data-wsb-html] forced overflow-y:auto (clipped dropdowns/shadows),
broke position:sticky inside customer HTML, and display:block!important merged multi-root blocks that are
direct flex/grid items. Now the wrapper stays display:contents on every viewport and no rule targets it;
at <=767px only its descendants are touched: media max-width:100% (img/video height:auto), and a table or
<pre> becomes its own sideways scroller (not when already inside an inline overflow wrapper). No
overflow-wrap:anywhere (it shrank min-content); body{overflow-wrap:break-word} inherits through the wrapper.
Deliberately wide customer markup (fixed widths, px grids) is clipped as before; the publish warning is
its guard, and its wording now says the excess is cut off. width= attributes are no longer flagged (they
size only media and tables, which the net handles). stripMediaBlocks lowercases once.
Browser suite: htmlblocks probes at every width (wrapper display:contents, sticky sticks, absolutely
positioned dropdown visible on top, multi-root block = separate flex items / grid cells, bare table
scrolls on its own); new htmlwide doc asserts wide blocks stay clipped and the page never scrolls sideways.
All probes fail on bb4fb75's net and pass now.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- scripts/e2e-mobile-guard.mjs (npm run test:browser:mobile-guard): Chromium + Firefox at 360/390/768/1280 over
exporter-built pages (navbar, columns, FeaturesGrid/Testimonials/NumberCounter/Gallery/Pricing/Reviews, big
image, long word/URL, wide customer HtmlBlocks). Asserts body.scrollWidth and every visible box/text right edge
(html,body{overflow-x:clip} hides overflow from documentElement.scrollWidth), stacking on phones, columns kept
on tablet, navbar wrap, intentional overflow-x:auto still scrolls, and 1280 box + pixel identity with the hooks
removed (optional cross-commit baseline).
- OLS home overflow was FeaturesGrid (inline repeat(3,1fr)), not an HtmlBlock. FeaturesGrid, Testimonials and
NumberCounter grids carry data-wsb-grid; the <=767px head block makes them one column.
- HtmlBlock output is exported inside <div data-wsb-html style="display:contents"> (applied after attr
injection, toHtml().html unchanged); on phones it becomes a max-width:100% sideways-scrolling box, media
shrink, long words break. body{overflow-wrap:break-word} on phones. Navbar link wrap now up to 991px
(8 links + long logo overflowed at 768).
- Head parity pins updated (mirror in WHP generateCompiledHTML); editor.css mirrors for the mobile frame.
- publish-checks: warn when an HtmlBlock's inline/<style> CSS sets widths, flex sizes or grids over 390px or a
3+ column grid with no phone @media rule; says what is not checked.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Reproduced in the e2e mock (now stateful: history + clear_history with the
server's 409 while a job is live): the normal two-step clear works (first
click arms the confirm by design), but a confirm while a job is live -- e.g.
the self-review follow-up that starts right after a send -- was refused by the
server and the modal showed the raw code 'job_in_progress', so the chat looked
like it would not clear on the first try. Now the server's sentence is shown,
and the button is disabled (with a tooltip) while a send, resume or self-review
is running. e2e scenarios 9, 9b (failed before), 9c (Firefox + Chromium).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The render is uploaded with the new review_render_upload action (backend kind
review_render, the only image a review job accepts); mocks follow.
- summarizeCanvas() never lets a newline or a quote into a node line (the server
validates review summaries line by line); test.
- test:browser:sitesmith-cleaned (Firefox + Chromium): a page published from a
cleaned ordinary Sitesmith response requests nothing outside the allow-listed
hosts (own assets, fonts, Font Awesome CDN, images.pexels.com, placehold.co)
and has no inline handlers or javascript:/data: URL attributes; the same
response uncleaned is the control (requests example.com). All requests are
answered or aborted locally.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review findings on clean-ai-response.ts:
1. update_props is now cleaned with the TARGET node's component (resolved on the
live canvas by apply()); an unresolvable target gets the strictest rules
(no code, url/src only assets/). code is kept only on an HtmlBlock.
2. cssValueAllowed() is a positive grammar: numbers/lengths/hex/keywords, an
allow-list of functions (gradients, colour functions, calc/clamp, var(--*),
transforms, filters), url() only of assets/<file>, quoted strings only plain
font names; image-set(, src(, cross-fade(, element(, paint(, attr(, env(,
':' or '//' outside url(assets/..), backslashes and control chars rejected.
Regression: every style/colour value in the templates, section families and
palettes passes (clean-ai-response.ordinary.test.ts).
Also: tricky link forms (//host, backslashes, whitespace/control chars,
mixed-case schemes, > 2048) rejected; comments state the server-side mirror is
not implemented (deferred).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
clean-ai-response.ts runs first in useApplyAiResponse().apply() and in
applyRegeneratedSection(): link props allow ''|#anchor|relative|mailto:|tel:|
https:|http: (never //host, backslash, whitespace/control chars, > 2048);
images only assets/<file> or https on images.pexels.com / placehold.co;
videos assets/ or YouTube/Vimeo; the model can never set form destinations,
relay settings or head code (patches keep the user's values); style names
must be CSS identifiers and values may not carry url() (except assets/),
@import, expression(, behavior, -moz-binding, javascript:, < > { } ; \;
anchor ids [A-Za-z0-9_-]. HtmlBlock code keeps its existing sanitisers.
Export boundary: stripLegacyCssVectors() in cssValue/cssPropsToString
(byte-identical for ordinary content: OLS fixture + default design measured).
Server-side mirror not implemented (deferred).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On the release branch mobile-parity's <=767px head rules land before the
colour merge, so 'identical to ab7a100' cannot hold; run with
COLOR_BASE_COMMIT=<commit before the colour merge> there. Measured: vs ab7a100
the only difference is those two documented rules.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
captureFileViolations() pins the one allow-same-origin srcdoc frame: sandbox set
once to exactly allow-same-origin, no script/form/popup/navigation tokens,
srcdoc only from inlineResources(clean.html), sanitizeCaptureHtml called and its
verdict checked before srcdoc. Eight mutated copies of the real source prove
each rule fails. Plus a behavioural jsdom test of the real captureHtml frame.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The self-review follow-up sends intent:'review' + review_image_ids instead of
putting the render in mockup_ids, so the backend builds a critique+patch
instruction instead of 'build toward this mockup'. Render still deleted after.
- types/sitesmith.ts: SitesmithStatus = server statuses (adds IMAGES_BUSY,
TOO_MANY_JOBS, SERVER_BUSY, ...) + client statuses (TIMEOUT, ABORTED, ...);
network status strings are narrowed with a type guard (unknown -> ERROR);
the hook builds SendResultErr without casts.
- statusMessages.ts: one specific message per status (a Record over the union,
so a new status without a message fails tsc), used by the modal.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
buildDesignPayload numbered brand colours by list position while the page
publishes them as --wsb-brand-<slot>, so the prompt could name a different
colour than the page renders. It now uses publishedBrandSwatches (same rule as
the publisher) and sends {name?, hex, slot}.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The colour branch pinned the brand head before the <=767px stacking/nav-wrap
rules existed; the bytes are now copied from the merged WHP
generateCompiledHTML output (WHP release/sitesmith-colors).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Conflicts: package.json (kept both test:browser:mobile and the colour scripts);
head-parity test (kept E_icons from mobile-parity/main AND the E_brand_swatches /
F_swatches_no_brand cases from color-system; pinned bytes re-derived later
against the merged WHP script).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
npm run test:browser:mobile; MOBILE_PARITY_RECORD/BASELINE compare desktop boxes against a base-commit snapshot.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
- <=767px: [class^=cols_] wrappers (every ColumnLayout split) stack; a mobile flexDirection:row override still opts out.
- <=767px: navbars without a hamburger wrap instead of overflowing.
- wrapInDocument emits the same Font Awesome links as the backend sb_font_awesome_link (Preview is a sandboxed srcdoc iframe).
- head-parity pins updated (+ E_icons case).
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Found by the real-editor e2e: picking over the header or its logo box returned the page colour behind it.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>