sitesmith: a page request replaces the page, never the whole site

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>
This commit is contained in:
shadowdaoandClaude Opus 5.5 committed 2026-11-01 16:42:49 -08:00
1 parent 648d6a3a51
commit 0fe7f07162
7 files changed
+318 -14

No files matched your search

+3
View File
@@ -68,6 +68,8 @@ export interface SitesmithSendOpts {
design?: Record<string, unknown>;
/** Uploaded assets Sitesmith may see (their filenames, validated by the backend against the site's assets). Omitted = all. */
assetFilenames?: string[];
/** The shared header/footer are still the editor's untouched starter ones (sent as chrome_default). */
chromeDefault?: boolean;
onProgress?: (phase: string) => void;
signal?: AbortSignal;
}
@@ -362,6 +364,7 @@ export function useSitesmith(siteId: number) {
if (opts?.useAssets !== undefined) startBody.use_assets = opts.useAssets;
if (opts?.intent === 'review' && opts.reviewImageIds?.length) startBody.review_image_ids = opts.reviewImageIds;
if (opts?.assetFilenames) startBody.asset_filenames = opts.assetFilenames;
if (opts?.chromeDefault && opts?.intent !== 'review') startBody.chrome_default = true;
const p = await request(`${apiBase(whpConfig.apiUrl)}?action=start`, {
method: 'POST',
credentials: 'include',
@@ -3,11 +3,13 @@ import React from 'react';
interface Props {
open: boolean;
pendingMessage?: string;
/** 'site': replace every page + header + footer. 'chrome': the page is replaced either way; also use the new header/footer? */
mode?: 'site' | 'chrome';
onConfirm: () => void;
onCancel: () => void;
}
export const ScopeConfirmDialog: React.FC<Props> = ({ open, pendingMessage, onConfirm, onCancel }) => {
export const ScopeConfirmDialog: React.FC<Props> = ({ open, pendingMessage, mode = 'site', onConfirm, onCancel }) => {
if (!open) return null;
const overlay: React.CSSProperties = { position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.7)', zIndex: 10000, display: 'flex', alignItems: 'center', justifyContent: 'center' };
const box: React.CSSProperties = { background: '#1a1a2e', border: '1px solid #3f3f46', borderRadius: 10, padding: 22, maxWidth: 480, color: '#fff' };
@@ -16,19 +18,32 @@ export const ScopeConfirmDialog: React.FC<Props> = ({ open, pendingMessage, onCo
return (
<div role="dialog" aria-modal="true" style={overlay}>
<div style={box}>
<h3 style={{ margin: 0 }}>Replace your entire site?</h3>
<p style={{ color: '#cbd5e1', fontSize: 14 }}>
Sitesmith will replace every page, your header, and your footer with the new design.
Manual edits will be lost.
</p>
{mode === 'chrome' ? (
<>
<h3 style={{ margin: 0 }}>Also use a matching header and footer?</h3>
<p style={{ color: '#cbd5e1', fontSize: 14 }}>
Your header and footer are still the starter ones ("MySite"). Sitesmith made a header and
footer that match this page. They are shared, so every page will show them. This page is
replaced either way; your other pages are not changed.
</p>
</>
) : (
<>
<h3 style={{ margin: 0 }}>Replace your entire site?</h3>
<p style={{ color: '#cbd5e1', fontSize: 14 }}>
You asked for the whole site, so Sitesmith will replace every page, your header, and your
footer with the new design. Manual edits will be lost.
</p>
</>
)}
{pendingMessage && (
<blockquote style={{ borderLeft: '3px solid #7c3aed', paddingLeft: 12, color: '#a5b4fc', fontSize: 13 }}>
{pendingMessage}
</blockquote>
)}
<div style={{ display: 'flex', gap: 10, justifyContent: 'flex-end', marginTop: 14 }}>
<button onClick={onCancel} style={cancel}>Cancel</button>
<button onClick={onConfirm} style={ok}>Replace site</button>
<button onClick={onCancel} style={cancel}>{mode === 'chrome' ? 'Keep my header and footer' : 'Cancel'}</button>
<button onClick={onConfirm} style={mode === 'chrome' ? { ...ok, background: '#4f46e5' } : ok}>{mode === 'chrome' ? 'Use the new header and footer' : 'Replace site'}</button>
</div>
</div>
</div>
@@ -370,3 +370,72 @@ describe('Sitesmith modal: picture selection is per message (E)', () => {
expect(document.body.textContent).toMatch(/Clear for everyone on this site\?/);
}, 20000);
});
// ---- scope: a page request never becomes a whole-site replace (eval 2026-11-01, "Build my home page" -> scope site)
import { usePages } from '../../state/PageContext';
describe('Sitesmith modal: replace scope follows the request', () => {
let pagesCtx: ReturnType<typeof usePages>;
const PagesSpy: React.FC = () => { pagesCtx = usePages(); return null; };
const navTree = (logo: string) => ({ type: { resolvedName: 'Navbar' }, props: { node_id: 'ai-nav-1', logoType: 'text', logoText: logo, links: [] }, nodes: [] });
const siteAnswer = () => ({
...replacePage('Site hero'), scope: 'site',
pages: [replacePage('Site hero').pages[0], { name: 'About', tree: { type: { resolvedName: 'Container' }, props: { tag: 'div' }, nodes: [] } }],
header: { tree: navTree('OLS Digital') }, footer: { tree: { type: { resolvedName: 'Footer' }, props: { node_id: 'ai-footer-1', text: '© OLS Digital' }, nodes: [] } },
});
async function mountP() {
try { window.localStorage.setItem('sitesmith.selfReview', 'off'); } catch { /* */ }
h = renderEditorHarness({
initialState: INITIAL,
wrap: (tree) => (
<EditorConfigProvider config={CFG as any}>
<SaveGuardProvider><NoticeProvider><ProjectExtrasProvider><SiteDesignProvider><SitesmithProvider>{tree}</SitesmithProvider></SiteDesignProvider></ProjectExtrasProvider></NoticeProvider></SaveGuardProvider>
</EditorConfigProvider>
),
});
h.mountChild(<PageProvider><Spy /><PagesSpy /><ProfileWizardHost /><SitesmithModal onClose={() => {}} target={null} /></PageProvider>);
h.act(() => extras.set('sitesmithProfile', PROFILE));
await flush(); await flush();
}
const dialog = () => document.querySelector('[role=dialog][aria-modal=true] h3')?.textContent ?? null;
const btn = (label: string) => Array.from(document.querySelectorAll('[role=dialog] button')).find((b) => b.textContent === label) as HTMLButtonElement;
const headerLogo = () => { const st = JSON.parse(pagesCtx.latestChrome().headerState!); return Object.values<any>(st).find((n) => n.type?.resolvedName === 'Navbar')?.props.logoText; };
test('page request answered with scope site + starter chrome: page replaced, asked about header/footer only; "keep" keeps them', async () => {
net.polls = [done(siteAnswer())];
await mountP();
await sendText('Build my home page following the layout of the attached mockup.');
await flush(1250);
expect(net.starts[0].chrome_default).toBe(true);
expect(dialog()).toBe('Also use a matching header and footer?');
expect(document.body.textContent).not.toContain('Replace your entire site?');
await h.act(async () => { btn('Keep my header and footer').click(); });
await flush(300);
expect(h.container.textContent).toContain('Site hero');
expect(pagesCtx.pages).toHaveLength(1); // the "About" page from the site answer was NOT added
expect(headerLogo()).toBe('MySite');
expect(document.body.textContent).toMatch(/only this page was replaced/);
}, 30000);
test('... and "use the new header and footer" swaps the starter chrome, still one page', async () => {
net.polls = [done(siteAnswer())];
await mountP();
await sendText('Build my home page');
await flush(1250);
await h.act(async () => { btn('Use the new header and footer').click(); });
await flush(300);
expect(pagesCtx.pages).toHaveLength(1);
expect(headerLogo()).toBe('OLS Digital');
}, 30000);
test('an explicit whole-site request keeps the whole-site confirm', async () => {
net.polls = [done(siteAnswer())];
await mountP();
await sendText('Build me a website for my company');
await flush(1250);
expect(dialog()).toBe('Replace your entire site?');
await h.act(async () => { btn('Replace site').click(); });
await flush(300);
expect(pagesCtx.pages.map((p) => p.name)).toEqual(['Home', 'About']);
expect(headerLogo()).toBe('OLS Digital');
}, 30000);
});
+29 -6
View File
@@ -31,6 +31,7 @@ import { summarizeCanvas } from '../../utils/canvas-summary';
import { SitesmithTarget } from '../../state/SitesmithContext';
import { UpgradeBanner } from './UpgradeBanner';
import { ScopeConfirmDialog } from './ScopeConfirmDialog';
import { planReplace, isDefaultChrome, withoutChrome } from '../../sitesmith/replace-plan';
import { MessageList } from './MessageList';
import { ChatInput } from './ChatInput';
import { SitesmithResponse } from '../../types/sitesmith';
@@ -64,7 +65,9 @@ export const SitesmithModal: React.FC<Props> = ({ onClose, target }) => {
const [adjust, setAdjust] = useState<{ list: ContrastAdjustment[]; notChecked: number } | null>(null);
const [reviewNote, setReviewNote] = useState<SelfReviewOutcome | null>(null);
const abortRef = useRef<AbortController | null>(null);
const [pendingReplace, setPendingReplace] = useState<{ resp: SitesmithResponse; review: boolean } | null>(null);
const [pendingReplace, setPendingReplace] = useState<{ resp: SitesmithResponse; review: boolean; mode: 'site' | 'chrome' } | null>(null);
/** What the owner typed for the job in flight (a replace's scope is decided from it, see replace-plan). */
const lastRequestRef = useRef<string | null>(null);
const [open, setOpen] = useState<Record<string, boolean>>({});
const [selMockups, setSelMockups] = useState<string[]>([]);
const [selRefs, setSelRefs] = useState<number[]>([]);
@@ -164,8 +167,12 @@ export const SitesmithModal: React.FC<Props> = ({ onClose, target }) => {
return t;
};
const chromeDefault = () => {
try { const c = pageCtx.latestChrome(); return isDefaultChrome(c.headerState, c.footerState); } catch { return false; }
};
const sendOpts = () => ({
design: buildDesignPayload(design),
chromeDefault: chromeDefault(),
mockupIds: imagesOn ? selMockups : [],
referenceIds: selRefs,
useAssets: useAssetsFlag,
@@ -251,12 +258,16 @@ export const SitesmithModal: React.FC<Props> = ({ onClose, target }) => {
setError(failureMessage(res));
return;
}
setNotes(res.notes ?? []);
if (res.response.type === 'replace' && res.response.scope === 'site') {
setPendingReplace({ resp: res.response, review });
// A whole-site replace needs an explicit whole-site request; the shared header/footer change only
// when they are untouched defaults and the owner says yes (replace-plan.ts).
const userText = lastRequestRef.current ?? [...messages].reverse().find((m) => m.role === 'user')?.content ?? null;
const plan = planReplace(res.response, { userText, chromeDefault: chromeDefault() });
setNotes([...(res.notes ?? []), ...(plan.note ? [plan.note] : [])]);
if (plan.confirm) {
setPendingReplace({ resp: plan.resp, review, mode: plan.confirm });
return;
}
await applyResponse(res.response, review);
await applyResponse(plan.resp, review);
};
const runJob = async (start: (o: { onProgress: (p: string) => void; signal: AbortSignal }) => Promise<SendResult>, review: boolean) => {
@@ -271,6 +282,7 @@ export const SitesmithModal: React.FC<Props> = ({ onClose, target }) => {
};
const handleSend = async (text: string) => {
lastRequestRef.current = text;
const canvas = summarizeCanvas(query.getSerializedNodes());
const opts = sendOpts();
await runJob((o) => send(text, canvas, targetPayload, { ...opts, ...o }), true);
@@ -291,6 +303,7 @@ export const SitesmithModal: React.FC<Props> = ({ onClose, target }) => {
if (resumedRef.current || busy || !canChat) return;
resumedRef.current = true;
setResumed(true);
lastRequestRef.current = null;
setPhase(activeJob.phase || 'generating');
void runJob((o) => resumeJob(activeJob.job_id, o), true);
}); // eslint-disable-line react-hooks/exhaustive-deps
@@ -335,6 +348,15 @@ export const SitesmithModal: React.FC<Props> = ({ onClose, target }) => {
setBusy(true);
try { await applyResponse(resp, review); } finally { setBusy(false); }
};
/** Site mode: cancel = nothing applied. Chrome mode: "keep mine" = the page only. */
const cancelReplace = async () => {
if (!pendingReplace) return;
const { resp, review, mode } = pendingReplace;
setPendingReplace(null);
if (mode !== 'chrome') return;
setBusy(true);
try { await applyResponse(withoutChrome(resp), review); } finally { setBusy(false); }
};
const cancelClearChat = () => {
if (clearChatTimeoutRef.current) clearTimeout(clearChatTimeoutRef.current);
@@ -547,8 +569,9 @@ export const SitesmithModal: React.FC<Props> = ({ onClose, target }) => {
<ScopeConfirmDialog
open={!!pendingReplace}
pendingMessage={pendingReplace && 'message' in pendingReplace.resp ? pendingReplace.resp.message : undefined}
mode={pendingReplace?.mode ?? 'site'}
onConfirm={confirmReplace}
onCancel={() => setPendingReplace(null)}
onCancel={cancelReplace}
/>
</div>
</Modal>
+90
View File
@@ -0,0 +1,90 @@
import { describe, test, expect } from 'vitest';
import { explicitSiteRequest, isDefaultChrome, planReplace } from './replace-plan';
import { DEFAULT_HEADER_STATE } from '../state/PageContext';
import type { SitesmithResponse } from '../types/sitesmith';
const tree = (name = 'Container') => ({ type: { resolvedName: name }, props: {}, nodes: [] }) as any;
const siteResp = (n = 1): SitesmithResponse => ({
type: 'replace', scope: 'site', message: 'Built it',
pages: Array.from({ length: n }, (_, i) => ({ name: i ? `P${i}` : 'Home', tree: tree() })),
header: { tree: tree('Navbar') }, footer: { tree: tree('Footer') },
});
describe('explicitSiteRequest', () => {
test.each([
'Build me a website for my bakery',
'Build my website',
'Create a new site for my plumbing business',
'Redo the whole site',
'Rebuild the entire website in a darker style',
'Make a 4 page site with home, about, menu and contact',
'Update all pages to use the new colours',
'Change the font on every page',
'Build my bakery website',
])('site: %s', (t) => expect(explicitSiteRequest(t)).toBe(true));
test.each([
'Build my home page following the layout of the attached mockup.',
'Build the home page for my site.',
'Build the home page for my site. Use the site I like as inspiration.',
'Redo the home page',
'Make this page look more premium',
'Add a pricing section',
'',
])('page: %s', (t) => expect(explicitSiteRequest(t)).toBe(false));
});
describe('isDefaultChrome', () => {
const header = (mut: (n: any) => void) => { const s = JSON.parse(DEFAULT_HEADER_STATE); mut(s); return JSON.stringify(s); };
test('the seed header + no footer is default, also after pages were added to the nav', () => {
expect(isDefaultChrome(DEFAULT_HEADER_STATE, null)).toBe(true);
expect(isDefaultChrome(header((s) => s['header-navbar'].props.links.splice(1, 0, { text: 'About', href: '/about', source: 'page', pageId: 'p2' })), null)).toBe(true);
expect(isDefaultChrome(null, null)).toBe(true);
});
test('any owner change makes it custom', () => {
expect(isDefaultChrome(header((s) => { s['header-navbar'].props.logoText = 'Acme'; }), null)).toBe(false);
expect(isDefaultChrome(header((s) => { s['header-navbar'].props.logoType = 'image'; s['header-navbar'].props.logoImage = 'assets/logo.png'; }), null)).toBe(false);
expect(isDefaultChrome(header((s) => { s['header-navbar'].props.links[1].text = 'Book now'; }), null)).toBe(false);
expect(isDefaultChrome(header((s) => { s.ROOT.nodes.push('x'); s.x = { type: { resolvedName: 'TextBlock' }, props: {}, nodes: [] }; }), null)).toBe(false);
const footer = JSON.stringify({ ROOT: { type: { resolvedName: 'Container' }, props: {}, nodes: ['f'] }, f: { type: { resolvedName: 'Footer' }, props: { text: '© 2026 Acme Ltd' }, nodes: [] } });
expect(isDefaultChrome(DEFAULT_HEADER_STATE, footer)).toBe(false);
const seedFooter = JSON.stringify({ ROOT: { type: { resolvedName: 'Container' }, props: {}, nodes: ['f'] }, f: { type: { resolvedName: 'Footer' }, props: { text: '© 2026 MySite. All rights reserved.' }, nodes: [] } });
expect(isDefaultChrome(DEFAULT_HEADER_STATE, seedFooter)).toBe(true);
expect(isDefaultChrome('{not json', null)).toBe(false);
});
});
describe('planReplace', () => {
test('eval case: "Build my home page" answered with scope site -> a PAGE replace, no site confirm', () => {
const p = planReplace(siteResp(1), { userText: 'Build my home page following the layout of the attached mockup.', chromeDefault: false });
expect(p.confirm).toBeNull();
expect(p.resp).toMatchObject({ type: 'replace', scope: 'page', pages: [{ name: 'Home' }] });
expect((p.resp as any).header).toBeUndefined();
expect((p.resp as any).footer).toBeUndefined();
expect(p.note).toMatch(/only this page/i);
});
test('page request + site answer + default chrome -> page replace that ASKS about header/footer', () => {
const p = planReplace(siteResp(3), { userText: 'Build my home page', chromeDefault: true });
expect(p.confirm).toBe('chrome');
expect(p.resp).toMatchObject({ scope: 'page', pages: [{ name: 'Home' }], header: { tree: { type: { resolvedName: 'Navbar' } } } });
expect((p.resp as any).pages).toHaveLength(1);
});
test('explicit site request keeps the site replace and its confirm', () => {
const p = planReplace(siteResp(3), { userText: 'Build me a website for my bakery', chromeDefault: false });
expect(p.confirm).toBe('site');
expect(p.resp).toEqual(siteResp(3));
});
test('page answer with header/footer: asked only when chrome is default, otherwise dropped', () => {
const resp: SitesmithResponse = { ...(siteResp(1) as any), scope: 'page' };
expect(planReplace(resp, { userText: 'Build my home page', chromeDefault: true }).confirm).toBe('chrome');
const kept = planReplace(resp, { userText: 'Build my home page', chromeDefault: false });
expect(kept.confirm).toBeNull();
expect((kept.resp as any).header).toBeUndefined();
expect(kept.note).toMatch(/kept your header and footer/i);
});
test('plain page / section / patch answers pass through untouched', () => {
const page: SitesmithResponse = { type: 'replace', scope: 'page', message: 'm', pages: [{ name: 'Home', tree: tree() }] };
expect(planReplace(page, { userText: 'x', chromeDefault: true })).toEqual({ resp: page, confirm: null });
const patch: SitesmithResponse = { type: 'patch', message: 'm', ops: [] };
expect(planReplace(patch, { userText: 'x', chromeDefault: true })).toEqual({ resp: patch, confirm: null });
});
});
+101
View File
@@ -0,0 +1,101 @@
/**
* How a Sitesmith `replace` answer is applied, decided from what the owner ASKED for -- not from the
* scope the model chose. Eval 2026-11-01: "Build my home page" came back as scope "site" and confirming
* would have replaced every page. A whole-site replace now needs an explicit whole-site request; any
* other request replaces only the current page.
*
* The shared header/footer are replaced alongside a page only when they are still the editor's untouched
* defaults ("MySite" + a "Get Started" button) and the owner confirms; otherwise they are kept.
*/
import type { SitesmithResponse } from '../types/sitesmith';
const SITE_PATTERNS: RegExp[] = [
/\b(?:whole|entire|full|complete)\s+(?:web\s*)?site\b/i,
/\b(?:all|every)\s+(?:of\s+)?(?:the\s+|my\s+|our\s+)?(?:pages|page)\b/i,
/\b(?:\d+|two|three|four|five|six|several|multiple|a few)[\s-]+pages?\b/i,
// "build me a website", "create a new site for ...", "build my bakery website"
/\b(?:build|make|create|design|redo|rebuild|generate|set\s+up)\s+(?:me\s+|us\s+)?(?:a|an|my|the|our)\s+(?:[\w'-]+\s+){0,2}?(?:web\s*)?site\b/i,
];
/** True only when the owner explicitly asked for the whole site (every page). */
export function explicitSiteRequest(text: string | null | undefined): boolean {
if (!text) return false;
return SITE_PATTERNS.some((re) => re.test(text));
}
type Node = { type?: { resolvedName?: string }; props?: Record<string, any>; nodes?: string[] };
function parseState(s: string | null): Record<string, Node> | null | 'bad' {
if (s == null || s === '') return null;
try {
const o = JSON.parse(s);
return o && typeof o === 'object' ? o : 'bad';
} catch { return 'bad'; }
}
/** Non-root nodes of a serialized Craft state. */
const contentNodes = (st: Record<string, Node>) => Object.entries(st).filter(([id]) => id !== 'ROOT').map(([, n]) => n);
/**
* The header is the seed Navbar (text logo "MySite", no image, its only non-page link the "Get Started"
* CTA) and the footer is absent or the seed Footer line. Page links the nav added by itself do not count
* as edits. Anything unreadable counts as customised (never replace what we cannot judge).
*/
export function isDefaultChrome(headerState: string | null, footerState: string | null): boolean {
const h = parseState(headerState);
const f = parseState(footerState);
if (h === 'bad' || f === 'bad') return false;
if (h) {
const nodes = contentNodes(h);
if (nodes.length !== 1 || nodes[0].type?.resolvedName !== 'Navbar') return false;
const p = nodes[0].props ?? {};
if (p.logoText !== 'MySite' || (p.logoType && p.logoType !== 'text') || p.logoImage) return false;
const own = (Array.isArray(p.links) ? p.links : []).filter((l: any) => l?.source !== 'page');
if (own.some((l: any) => !(l?.text === 'Get Started' && (l?.href ?? '#') === '#'))) return false;
}
if (f) {
const nodes = contentNodes(f);
if (nodes.length > 1) return false;
if (nodes.length === 1) {
const n = nodes[0];
if (n.type?.resolvedName !== 'Footer' || !/\bMySite\b/.test(String(n.props?.text ?? ''))) return false;
if (Array.isArray(n.props?.links) && n.props!.links.length) return false;
}
}
return true;
}
export interface ReplacePlan {
resp: SitesmithResponse;
/** 'site': confirm a whole-site replace. 'chrome': ask whether to also use the new header/footer. */
confirm: 'site' | 'chrome' | null;
/** Shown to the owner when the plan changed what Sitesmith returned. */
note?: string;
}
export function planReplace(resp: SitesmithResponse, ctx: { userText: string | null | undefined; chromeDefault: boolean }): ReplacePlan {
if (resp.type !== 'replace' || resp.scope === 'section' || !resp.pages.length) return { resp, confirm: null };
if (resp.scope === 'site' && explicitSiteRequest(ctx.userText)) return { resp, confirm: 'site' };
const notes: string[] = [];
let page: Extract<SitesmithResponse, { type: 'replace' }> = { ...resp };
if (resp.scope === 'site') {
page = { ...resp, scope: 'page', pages: [resp.pages[0]] };
notes.push(resp.pages.length > 1
? `Sitesmith built ${resp.pages.length} pages, but you asked for one page, so only this page was replaced. Ask for "the whole site" to replace every page.`
: 'You asked for one page, so only this page was replaced; your other pages were not touched.');
}
const hasChrome = !!(page.header || page.footer);
if (!hasChrome) return { resp: page, confirm: null, ...(notes.length ? { note: notes.join(' ') } : {}) };
if (ctx.chromeDefault) return { resp: page, confirm: 'chrome', ...(notes.length ? { note: notes.join(' ') } : {}) };
const { header: _h, footer: _f, ...rest } = page;
notes.push('Sitesmith also suggested a new header and footer, but yours have been edited, so it kept your header and footer.');
return { resp: rest as SitesmithResponse, confirm: null, note: notes.join(' ') };
}
/** The same page answer without the header/footer (the owner chose to keep them). */
export function withoutChrome(resp: SitesmithResponse): SitesmithResponse {
if (resp.type !== 'replace') return resp;
const { header: _h, footer: _f, ...rest } = resp;
return rest as SitesmithResponse;
}
+3
View File
@@ -165,6 +165,9 @@ export function useApplyAiResponse() {
message: 'You are editing a global section. Click Done, then ask again to replace the page.',
};
}
// Only present when the owner agreed to swap the starter header/footer (sitesmith/replace-plan.ts).
if (resp.header) pages.setHeader(resp.header.tree);
if (resp.footer) pages.setFooter(resp.footer.tree);
return done({ ok: true }, await loadedCanvas(resp.pages[0].tree));
}