Live case 2 (2026.11.8): the daily limit stopped a site build after page 1, the dialog still
said "Replace your entire site?" and accepting deleted the owner's other page. The server now
reports site_build {planned, built, missing}; when pages are missing the confirm becomes
"Sitesmith built only N of M pages" with:
- "Add these pages" (default focus): each built page replaces the page at the same address,
new ones are appended, every other page is kept (PageContext mergeAiPages / addAiPages), and
the new header is repaired to link the kept pages too;
- "Replace site and delete K pages" with the names it deletes (only when K > 0);
- Cancel.
A complete site build keeps "Replace your entire site?" and now names the pages it deletes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The ContactForm component paints its own card (default style.backgroundColor = bg token).
Contrast repair judged labelColor against the section behind it, so white labels on the
white default card (inside a dark section) went unrepaired (eval r3 case 6, ttm-likes).
A ContactForm with no style prop now resolves to the bg token; an explicit style without
a background stays transparent and is still judged against the section.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The capture stopped at 3000 CSS px, so the reviewer never saw the bottom or footer. Now the full page
height H is rendered at its normal width (1280). H <= 3000: one image, as before. Otherwise
N = min(3, ceil(H/3000)) equal-height strips (each <= 3000 px) are laid out side by side, left to
right (strip 1 = page top), with 24 px #808080 gutters, in one JPEG; H > 9000 shows the top 9000 px
(cropped). Geometry is the pure stripGeometry() (unit-tested); JPEG quality steps 0.8 -> 0.5 before
"too large" (3 MB cap, server limit 8 MB). CaptureResult gains strips/pageHeight; runSelfReview
returns them and says when only the top of a long page was reviewed. Real eval pages: 2 strips,
~0.8-1.1 s, 150-270 KB in Chromium and Firefox; e2e checks strip order/alignment/gutters/cropping.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The capture never fetches external images, so placehold.co placeholders came out as a blank grey box
(Chromium) or alt text (Firefox) and the reviewer reported a false "screenshot mismatch". checkUrl now
maps a strictly parsed placehold.co URL (https, host exactly placehold.co, /WxH with 1-4 digits, up to
two 3/6-digit hex colours, optional image extension) to a locally generated SVG data URL of the same
intrinsic size: URL bg colour (default #cccccc), ?text= label (default "W x H") in the fg colour
(default #969696), XML-escaped. Off-spec placehold.co URLs become a plain #cccccc box (no label), or
the old 1x1 grey image when the size is unknown. Works for <img> (own CSS: width, aspect-ratio,
object-fit) and CSS url() backgrounds. Only an SVG byte-identical to our generator passes the
second sanitiser pass. makePolicy/allow-list, sandbox and CSP unchanged; fetchBounded refuses
placehold.co outright. e2e: fidelity samples the boxes/labels, security asserts zero placehold.co
requests (incl. hostile ?text=) in Chromium and Firefox.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`border:1px solid var(--x); border-top:4px solid ...` (the AI's accent-top card) lost its left/right/
bottom sides in the self-review capture in Chromium AND Firefox: once a later declaration overrides
one longhand, the CSSOM can no longer serialise the var() shorthand (it and its remaining longhands
read back as ""), so the 4c1c8db recovery had nothing to emit. Recover such shorthands from the
declaration block's source text (inline value; innermost blocks of a sheet, matched to rules by
cssText), re-parse each ALONE through the CSSOM and judge it with the unchanged value/URL gate.
e2e-capture-fidelity gains the card (var/var, var/literal, stylesheet) sampled on all four sides.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- contrast repair: ContactForm labelColor / inputTextColor are repaired to a
neutral (text/bg/muted token, black, white), never the accent. Measured on
the ttm-likes eval response in Chromium: before, dark typed text, UA-grey
placeholders and an invalid border on the dark inputs; after, white text,
placeholders at 70% of it, visible border.
- a page/site build that shows none of the owner's offered uploads gets an
honest note (the brief now requires one in the hero or a feature).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Eval 2026-11-01 (ttm-likes): a ContactForm on a dark band had dark typed text
and UA-grey placeholders on a color-mix() input surface, and its inputBorder
"1px solid X" came out as the invalid "1px solid 1px solid X".
- ContactForm: new inputTextColor prop (typed text); placeholders now follow
the input's own text colour via a scoped form[data-wsb-form] ::placeholder
rule (export + editor canvas); an inputBorder that is a whole border is used
as-is. Without inputTextColor the legacy derivation is unchanged.
- contrast repair: resolves opaque color-mix(in srgb, A p%, B) surfaces (the
brief prescribes these tints; translucent / other spaces stay unchecked),
checks ContactForm labelColor against its section and the derived input text
colour against an explicitly set inputBg, writing inputTextColor on failure.
- catalog generator: (rebased onto main 01a78c9, which already omits the
clock-derived Countdown.targetDate default via VOLATILE_DEFAULTS + the
date-frozen drift test; this commit's duplicate inline check was dropped.)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>