diff --git a/apps/ai-game-creator-shell/src-tauri/src/preview.rs b/apps/ai-game-creator-shell/src-tauri/src/preview.rs
index f8846b851..e9f664963 100644
--- a/apps/ai-game-creator-shell/src-tauri/src/preview.rs
+++ b/apps/ai-game-creator-shell/src-tauri/src/preview.rs
@@ -89,6 +89,53 @@ const PREVIEW_REQUEST_MAX_HEADER_BYTES: usize = 32 * 1024;
const PREVIEW_REQUEST_MAX_HEADER_LINES: usize = 100;
const PREVIEW_RESPONSE_DRAIN_TIMEOUT: Duration = Duration::from_millis(250);
const PREVIEW_RESPONSE_DRAIN_MAX_BYTES: usize = 32 * 1024;
+const PREVIEW_FIT_BRIDGE_PATH: &str = "/__genarrative/local-preview-fit.js";
+const PREVIEW_FIT_BRIDGE_SCRIPT: &str = r#"(() => {
+ const messageType = 'genarrative.local-preview-size.v1';
+ let ready = document.readyState === 'complete';
+ let frame = 0;
+ const publish = () => {
+ frame = 0;
+ if (!ready || window.parent === window) return;
+ const root = document.documentElement;
+ const body = document.body;
+ window.parent.postMessage({
+ type: messageType,
+ contentWidth: Math.ceil(Math.max(root?.scrollWidth || 0, body?.scrollWidth || 0)),
+ contentHeight: Math.ceil(Math.max(root?.scrollHeight || 0, body?.scrollHeight || 0)),
+ viewportWidth: window.innerWidth,
+ viewportHeight: window.innerHeight,
+ }, '*');
+ };
+ const schedule = () => {
+ if (frame === 0) frame = window.requestAnimationFrame(publish);
+ };
+ const activate = () => {
+ ready = true;
+ const style = document.createElement('style');
+ style.dataset.genarrativePreviewFit = 'true';
+ style.textContent = 'html,body{scrollbar-width:none}html::-webkit-scrollbar,body::-webkit-scrollbar{display:none}';
+ (document.head || document.documentElement).appendChild(style);
+ schedule();
+ if (typeof ResizeObserver === 'function') {
+ const resizeObserver = new ResizeObserver(schedule);
+ resizeObserver.observe(document.documentElement);
+ if (document.body) resizeObserver.observe(document.body);
+ }
+ const mutationObserver = new MutationObserver(schedule);
+ mutationObserver.observe(document.documentElement, {
+ childList: true,
+ subtree: true,
+ attributes: true,
+ characterData: true,
+ });
+ window.addEventListener('resize', schedule, { passive: true });
+ };
+ if (ready) activate();
+ else window.addEventListener('load', activate, { once: true });
+})();"#;
+const PREVIEW_FIT_BRIDGE_TAG: &str =
+ r#""#;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) enum PreviewListenerAcceptDisposition {
@@ -560,6 +607,20 @@ pub(crate) fn build_preview_response(root: &Path, method: &str, url_path: &str)
);
}
+ if url_path.split('?').next() == Some(PREVIEW_FIT_BRIDGE_PATH) {
+ let body = if is_head {
+ Vec::new()
+ } else {
+ PREVIEW_FIT_BRIDGE_SCRIPT.as_bytes().to_vec()
+ };
+ return http_response(
+ "200 OK",
+ "text/javascript; charset=utf-8",
+ &body,
+ PREVIEW_FIT_BRIDGE_SCRIPT.len(),
+ );
+ }
+
let file_path = match resolve_preview_path(root, url_path) {
Ok(path) => path,
Err(_) => {
@@ -574,11 +635,36 @@ pub(crate) fn build_preview_response(root: &Path, method: &str, url_path: &str)
return http_response("404 Not Found", "text/plain", body, b"not found".len());
}
};
+ let body = if content_type(&file_path).starts_with("text/html") {
+ inject_preview_fit_bridge(body)
+ } else {
+ body
+ };
let content_length = body.len();
let body = if is_head { Vec::new() } else { body };
http_response("200 OK", content_type(&file_path), &body, content_length)
}
+fn inject_preview_fit_bridge(body: Vec) -> Vec {
+ let html = match String::from_utf8(body) {
+ Ok(html) => html,
+ Err(error) => return error.into_bytes(),
+ };
+ if html.contains(PREVIEW_FIT_BRIDGE_PATH) {
+ return html.into_bytes();
+ }
+ let lowercase = html.to_ascii_lowercase();
+ let insertion = lowercase
+ .rfind("