Overview
Immersive mode renders the editing UI directly on the site page — no iframe, no chrome around the content. The widget (chat FAB, add-block pill, text-selection “Ask AI”, inline field prompts, undo/redo) is portalled into the site DOM. It is the intended mode for copy-heavy and single-page edits where you want to see the real page at full width. The main editor (apps/editor) and immersive mode share the same draft via
the orchestrator’s (session, siteId, slug) — switching between them does
not move or duplicate state.
Entering from the main editor
The editor header has a Maximize icon button (next to the more-options menu, before the Publish button). Clicking it opens the current page in immersive mode in a new tab, carrying over the active session, site, and slug:editorOrigin query param tells the widget where the main editor lives
so it can render the “Back” pill. The site’s __editor=1 middleware rewrites
the request to the preview-draft route internally; you do not link to
/preview-draft/... directly.
A new tab is used instead of replacing the current tab so the editor’s chat
log, model selection, and iframe scroll state all stay alive. If you prefer
a single-tab mental model, close the immersive tab when done.
Returning to the main editor
A small glass ”← Back” pill sits in the top-left corner of the immersive page. It:- only renders when
config.editorOriginis present (so embedded / public widget uses stay clean), - auto-hides when the chat panel is open (to stop it colliding with panel chrome),
- auto-hides on scroll-down and reappears on scroll-up or near the top (browser-chrome style),
- navigates same-tab to
{editorOrigin}?session=...&siteId=...&slug=....
?slug= on bootstrap, so you land on the exact page you
were editing — no flash to the home route before the iframe catches up.
What is available inside immersive mode
- Chat FAB (bottom right) with the full chat pipeline — text, structural, and image ops all route through the same orchestrator as the main editor.
- Add-block pill (bottom right, next to the chat FAB) opens the inline block picker above itself and inserts after the last block on the page.
- Inline field prompt — click any editable text field to get a tight prompt box anchored to the field.
- Text selection “Ask AI” — select a range of copy to get an inline toolbar. In text-only mode, selections route into the field prompt with the excerpt pre-filled.
- Undo / redo —
Cmd+ZandCmd+Shift+Z/Cmd+Y. Buttons also appear in the chat panel header. Proxies to the orchestrator’s/history/{undo,redo}so draft history is shared with the main editor.
Feature flags
| Flag | Default | Effect |
|---|---|---|
NEXT_PUBLIC_IMMERSIVE_TEXT_ONLY | 0 | When 1, restricts the block picker to Hero, FeatureGrid, Testimonials, FAQAccordion, CTA, RichText and routes text selections into the inline field prompt with the excerpt pre-filled. Used for the text-blocks MVP. |
Key files
| Layer | File |
|---|---|
| Widget root | packages/immersive-widget/src/ImmersiveWidget.tsx |
| Back-to-editor pill | packages/immersive-widget/src/components/BackToEditorPill.tsx |
| Add-block pill | packages/immersive-widget/src/components/AddBlockFab.tsx |
| Undo/redo hook | packages/immersive-widget/src/hooks/useUndoHistory.ts |
| Widget config type | packages/immersive-widget/src/lib/widget-state.ts |
| Site page rendering the widget | apps/site/app/preview-draft/[[...slug]]/page.tsx |
| Site wrapper | apps/site/components/immersive-wrapper.tsx |
| Editor “Open in immersive” button | apps/editor/src/App.tsx (chat-header-right) |
| Editor slug bootstrap | apps/editor/src/store/editor-store.ts — readInitialSlug() |
Embedding on an external site
The widget ships as a standalone package —@ai-site-editor/immersive-widget — so you can mount it on any Next.js 15 (App Router) site that is already integrated with the orchestrator via the site-sdk. The monorepo’s apps/site is just the first consumer; your own project can do the same thing.
Prerequisites
- Next.js 15 App Router (React 19 peer dep)
- Site already registered with the orchestrator (
npx avocado-register) — you need a validsessionandsiteId - The orchestrator reachable at a known URL from the browser (it’s called directly by the widget over SSE, not proxied through Next.js)
Install
Import the CSS
Add the widget stylesheet once, in your root layout or a global CSS file:.iw-* class names with a --iw-* CSS custom property API so they don’t conflict with your site’s own styles.
Mount the widget
Create a"use client" wrapper — the widget needs browser APIs and the Next.js router:
document.body, so its position in the tree doesn’t matter for z-index or layout.
Passing the block manifest (recommended)
Without a manifest the widget’s block picker falls back to the SDK’s built-in block list. To show only your site’s actual blocks, fetch the manifest from your existing/api/editor/blocks route and pass it in:
manifest.
Session management
WidgetConfig.session is the orchestrator session key that scopes the draft. You have a few options:
| Approach | When to use |
|---|---|
Hard-coded string (e.g. "dev") | Local dev / single editor |
NEXT_PUBLIC_* env var | Staging deploys where one session serves all visitors |
URL query param (?session=...) | Entering from the main editor — read it from useSearchParams() |
| Short-lived JWT / cookie | Production multi-user deploys (not yet shipped) |
?session=... in the URL when opening immersive mode — read it via useSearchParams() if you want the immersive view to share the editor’s session automatically.
Props reference
| Prop | Type | Required | Description |
|---|---|---|---|
config | WidgetConfig | yes | orchestratorUrl, session, siteId, optional editorOrigin |
slug | string | yes | Current page slug (without leading /) |
pathname | string | yes | Full pathname — used by the block selection overlay |
refresh | () => void | yes | Called after ops apply; use router.refresh() |
navigate | (href: string) => void | yes | Called on undo/redo when slug changes; use router.push() |
manifest | BlockManifest | null | no | Block list for the picker and AI context |
siteContext | SiteContext | no | { siteName?, purpose?, tone?, constraints? } — enriches AI prompts |
accessToken | string | no | Bearer token forwarded to the orchestrator on every request |
textOnly | boolean | no | Restricts picker to Hero, FeatureGrid, Testimonials, FAQAccordion, CTA, RichText and routes text selections to the inline field prompt |
Restricting access
The widget renders unconditionally if mounted. Gate it behind your existing auth check before rendering the component — for example, only mount it when a?__editor=1 query param is present or when a Draft Mode cookie is set: