From aa800c9aca995c90e92ec7c952fcaffce1cb27d9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 13 Sep 2025 20:58:36 +0000 Subject: [PATCH 1/6] Initial plan From 04ebf6baec679370a06c242687162d41ccd3511c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 13 Sep 2025 21:07:48 +0000 Subject: [PATCH 2/6] Fix mobile browser tombstoning detection for blob URLs Co-authored-by: danmarshall <11507384+danmarshall@users.noreply.github.com> --- docs/dist/v1/chartifact.host.umd.js | 14 ++ packages/host/src/listener.ts | 26 ++- test-tombstoning-fix.html | 273 ++++++++++++++++++++++++++++ 3 files changed, 311 insertions(+), 2 deletions(-) create mode 100644 test-tombstoning-fix.html diff --git a/docs/dist/v1/chartifact.host.umd.js b/docs/dist/v1/chartifact.host.umd.js index 15ac2446..efedff3d 100644 --- a/docs/dist/v1/chartifact.host.umd.js +++ b/docs/dist/v1/chartifact.host.umd.js @@ -3135,6 +3135,20 @@ ${details}`; if (!contentWindow || !iframe.src || iframe.src === "about:blank") { return false; } + if (iframe.src.startsWith("blob:")) { + try { + const doc = contentWindow.document; + if (!doc || !doc.body) { + return false; + } + if (doc.body.children.length === 0) { + return false; + } + return true; + } catch (error) { + return false; + } + } return true; } catch (error) { return false; diff --git a/packages/host/src/listener.ts b/packages/host/src/listener.ts index fbff34e1..0632ac7f 100644 --- a/packages/host/src/listener.ts +++ b/packages/host/src/listener.ts @@ -296,15 +296,37 @@ export class Listener { return false; } - // Try to access the iframe's content window and check if the src is still valid const iframe = this.sandbox.iframe; const contentWindow = iframe.contentWindow; - // If we can't access the content window or the src is invalid, sandbox is not functional + // Basic checks - if these fail, definitely not functional if (!contentWindow || !iframe.src || iframe.src === 'about:blank') { return false; } + // For blob URLs, we need to test if the content is actually accessible + // Mobile browsers can garbage collect blob URLs during tombstoning + if (iframe.src.startsWith('blob:')) { + try { + // Try to access the document - this will fail if blob URL is garbage collected + const doc = contentWindow.document; + if (!doc || !doc.body) { + return false; + } + + // Additional check: try to read a property that should exist in our sandbox + // Our sandbox should have the Chartifact global or at least some content + if (doc.body.children.length === 0) { + return false; + } + + return true; + } catch (error) { + // If we can't access the document, the blob URL is likely garbage collected + return false; + } + } + return true; } catch (error) { // If accessing iframe throws an error, it's not functional diff --git a/test-tombstoning-fix.html b/test-tombstoning-fix.html new file mode 100644 index 00000000..d51e17b0 --- /dev/null +++ b/test-tombstoning-fix.html @@ -0,0 +1,273 @@ + + + + Chartifact Mobile Tombstoning Fix Test + + + + +

🔧 Chartifact Mobile Tombstoning Fix Test

+ +
+

1. Load Test Document

+

First, load a test document to initialize the sandbox:

+ +
+ +
+

2. Verify Sandbox

+

Check if the sandbox is working properly:

+ +
+ +
+

3. Simulate Mobile Tombstoning

+

Simulate what happens when mobile browsers garbage collect blob URLs:

+ + +
+ +
Loading Chartifact...
+
Help: Load a document to get started
+
+ +
+ + + + + \ No newline at end of file From e3c54e4ebe89aca4cfaaa83fab567fbe4c3eff88 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 14 Sep 2025 01:00:35 +0000 Subject: [PATCH 3/6] Simplify tombstoning detection to avoid sandbox restrictions - Replace direct document access with conservative blob URL detection - Add health check message types to common messages (for future use) - Always recreate blob URL sandboxes on visibility change to ensure mobile tombstoning recovery Co-authored-by: danmarshall <11507384+danmarshall@users.noreply.github.com> --- packages/common/src/messages.ts | 10 ++++ packages/host/src/listener.ts | 60 +++++++-------------- packages/sandbox-resources/src/sandboxed.ts | 11 +++- 3 files changed, 40 insertions(+), 41 deletions(-) diff --git a/packages/common/src/messages.ts b/packages/common/src/messages.ts index 3cbb23a7..f469a7bd 100644 --- a/packages/common/src/messages.ts +++ b/packages/common/src/messages.ts @@ -74,3 +74,13 @@ export interface GuardedFetchResponseMessage { body?: string; error?: string; } + +export interface HealthCheckMessage { + type: 'healthCheck'; + healthCheckId: string; +} + +export interface HealthCheckResponseMessage { + type: 'healthCheckResponse'; + healthCheckId: string; +} diff --git a/packages/host/src/listener.ts b/packages/host/src/listener.ts index 0632ac7f..2f205d3f 100644 --- a/packages/host/src/listener.ts +++ b/packages/host/src/listener.ts @@ -288,50 +288,30 @@ export class Listener { } /** - * Check if the sandbox iframe is still functional by testing if we can access its content window + * Check if the sandbox iframe is still functional + * For blob URLs, we take a conservative approach and assume they may be garbage collected */ private isSandboxFunctional(): boolean { - try { - if (!this.sandbox || !this.sandbox.iframe) { - return false; - } - - const iframe = this.sandbox.iframe; - const contentWindow = iframe.contentWindow; - - // Basic checks - if these fail, definitely not functional - if (!contentWindow || !iframe.src || iframe.src === 'about:blank') { - return false; - } - - // For blob URLs, we need to test if the content is actually accessible - // Mobile browsers can garbage collect blob URLs during tombstoning - if (iframe.src.startsWith('blob:')) { - try { - // Try to access the document - this will fail if blob URL is garbage collected - const doc = contentWindow.document; - if (!doc || !doc.body) { - return false; - } - - // Additional check: try to read a property that should exist in our sandbox - // Our sandbox should have the Chartifact global or at least some content - if (doc.body.children.length === 0) { - return false; - } - - return true; - } catch (error) { - // If we can't access the document, the blob URL is likely garbage collected - return false; - } - } - - return true; - } catch (error) { - // If accessing iframe throws an error, it's not functional + if (!this.sandbox || !this.sandbox.iframe) { return false; } + + const iframe = this.sandbox.iframe; + const contentWindow = iframe.contentWindow; + + // Basic checks - if these fail, definitely not functional + if (!contentWindow || !iframe.src || iframe.src === 'about:blank') { + return false; + } + + // For blob URLs, we can't reliably test accessibility due to sandboxing restrictions + // The conservative approach is to always recreate on visibility change for blob URLs + // This ensures mobile tombstoning recovery works reliably + if (iframe.src.startsWith('blob:')) { + return false; + } + + return true; } /** diff --git a/packages/sandbox-resources/src/sandboxed.ts b/packages/sandbox-resources/src/sandboxed.ts index e4da8ac0..daf86b61 100644 --- a/packages/sandbox-resources/src/sandboxed.ts +++ b/packages/sandbox-resources/src/sandboxed.ts @@ -52,7 +52,7 @@ document.addEventListener('DOMContentLoaded', () => { window.addEventListener('message', (event) => { if (!event.data) return; - const message = event.data as Chartifact.common.SandboxApprovalMessage | Chartifact.common.SandboxRenderMessage; + const message = event.data as Chartifact.common.SandboxApprovalMessage | Chartifact.common.SandboxRenderMessage | Chartifact.common.HealthCheckMessage; switch (message.type) { case 'sandboxRender': { @@ -78,6 +78,15 @@ document.addEventListener('DOMContentLoaded', () => { } break; } + case 'healthCheck': { + // Respond immediately to health checks to confirm the sandbox is functional + const healthCheckResponse: Chartifact.common.HealthCheckResponseMessage = { + type: 'healthCheckResponse', + healthCheckId: message.healthCheckId + }; + window.parent.postMessage(healthCheckResponse, '*'); + break; + } } }); From 8b7ebb205511a977a872b7e0d077a7eea094a120 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 14 Sep 2025 01:12:47 +0000 Subject: [PATCH 4/6] Address feedback: revert UMD file, remove half-implemented healthcheck, make restoration less aggressive Co-authored-by: danmarshall <11507384+danmarshall@users.noreply.github.com> --- docs/dist/v1/chartifact.host.umd.js | 14 - packages/common/src/messages.ts | 8 - packages/host/src/listener.ts | 19 +- packages/sandbox-resources/src/sandboxed.ts | 11 +- test-tombstoning-fix.html | 273 -------------------- 5 files changed, 16 insertions(+), 309 deletions(-) delete mode 100644 test-tombstoning-fix.html diff --git a/docs/dist/v1/chartifact.host.umd.js b/docs/dist/v1/chartifact.host.umd.js index efedff3d..15ac2446 100644 --- a/docs/dist/v1/chartifact.host.umd.js +++ b/docs/dist/v1/chartifact.host.umd.js @@ -3135,20 +3135,6 @@ ${details}`; if (!contentWindow || !iframe.src || iframe.src === "about:blank") { return false; } - if (iframe.src.startsWith("blob:")) { - try { - const doc = contentWindow.document; - if (!doc || !doc.body) { - return false; - } - if (doc.body.children.length === 0) { - return false; - } - return true; - } catch (error) { - return false; - } - } return true; } catch (error) { return false; diff --git a/packages/common/src/messages.ts b/packages/common/src/messages.ts index f469a7bd..52c4637a 100644 --- a/packages/common/src/messages.ts +++ b/packages/common/src/messages.ts @@ -75,12 +75,4 @@ export interface GuardedFetchResponseMessage { error?: string; } -export interface HealthCheckMessage { - type: 'healthCheck'; - healthCheckId: string; -} -export interface HealthCheckResponseMessage { - type: 'healthCheckResponse'; - healthCheckId: string; -} diff --git a/packages/host/src/listener.ts b/packages/host/src/listener.ts index 2f205d3f..7222ec79 100644 --- a/packages/host/src/listener.ts +++ b/packages/host/src/listener.ts @@ -289,7 +289,7 @@ export class Listener { /** * Check if the sandbox iframe is still functional - * For blob URLs, we take a conservative approach and assume they may be garbage collected + * Uses basic checks that can be safely performed without violating sandbox restrictions */ private isSandboxFunctional(): boolean { if (!this.sandbox || !this.sandbox.iframe) { @@ -305,10 +305,21 @@ export class Listener { } // For blob URLs, we can't reliably test accessibility due to sandboxing restrictions - // The conservative approach is to always recreate on visibility change for blob URLs - // This ensures mobile tombstoning recovery works reliably + // Only recreate if we can detect actual problems without being too aggressive if (iframe.src.startsWith('blob:')) { - return false; + try { + // Try a simple property access that should work if the iframe is functional + // but won't violate sandbox restrictions + const hasWindow = !!contentWindow; + if (!hasWindow) { + return false; + } + // If we can't detect problems, assume it's functional to avoid being too aggressive + return true; + } catch (error) { + // If we get an error accessing basic properties, the iframe is likely broken + return false; + } } return true; diff --git a/packages/sandbox-resources/src/sandboxed.ts b/packages/sandbox-resources/src/sandboxed.ts index daf86b61..e4da8ac0 100644 --- a/packages/sandbox-resources/src/sandboxed.ts +++ b/packages/sandbox-resources/src/sandboxed.ts @@ -52,7 +52,7 @@ document.addEventListener('DOMContentLoaded', () => { window.addEventListener('message', (event) => { if (!event.data) return; - const message = event.data as Chartifact.common.SandboxApprovalMessage | Chartifact.common.SandboxRenderMessage | Chartifact.common.HealthCheckMessage; + const message = event.data as Chartifact.common.SandboxApprovalMessage | Chartifact.common.SandboxRenderMessage; switch (message.type) { case 'sandboxRender': { @@ -78,15 +78,6 @@ document.addEventListener('DOMContentLoaded', () => { } break; } - case 'healthCheck': { - // Respond immediately to health checks to confirm the sandbox is functional - const healthCheckResponse: Chartifact.common.HealthCheckResponseMessage = { - type: 'healthCheckResponse', - healthCheckId: message.healthCheckId - }; - window.parent.postMessage(healthCheckResponse, '*'); - break; - } } }); diff --git a/test-tombstoning-fix.html b/test-tombstoning-fix.html deleted file mode 100644 index d51e17b0..00000000 --- a/test-tombstoning-fix.html +++ /dev/null @@ -1,273 +0,0 @@ - - - - Chartifact Mobile Tombstoning Fix Test - - - - -

🔧 Chartifact Mobile Tombstoning Fix Test

- -
-

1. Load Test Document

-

First, load a test document to initialize the sandbox:

- -
- -
-

2. Verify Sandbox

-

Check if the sandbox is working properly:

- -
- -
-

3. Simulate Mobile Tombstoning

-

Simulate what happens when mobile browsers garbage collect blob URLs:

- - -
- -
Loading Chartifact...
-
Help: Load a document to get started
-
- -
- - - - - \ No newline at end of file From 307807b68fb3bfada65f4e4aac534a26b66f4f59 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 14 Sep 2025 01:20:02 +0000 Subject: [PATCH 5/6] Make tombstoning detection less aggressive to preserve user state during tab switching Co-authored-by: danmarshall <11507384+danmarshall@users.noreply.github.com> --- packages/host/src/listener.ts | 28 +++++++++------------------- 1 file changed, 9 insertions(+), 19 deletions(-) diff --git a/packages/host/src/listener.ts b/packages/host/src/listener.ts index 7222ec79..7480611e 100644 --- a/packages/host/src/listener.ts +++ b/packages/host/src/listener.ts @@ -289,7 +289,7 @@ export class Listener { /** * Check if the sandbox iframe is still functional - * Uses basic checks that can be safely performed without violating sandbox restrictions + * Conservative approach that only detects clear evidence of tombstoning */ private isSandboxFunctional(): boolean { if (!this.sandbox || !this.sandbox.iframe) { @@ -299,29 +299,19 @@ export class Listener { const iframe = this.sandbox.iframe; const contentWindow = iframe.contentWindow; - // Basic checks - if these fail, definitely not functional - if (!contentWindow || !iframe.src || iframe.src === 'about:blank') { + // Only recreate if we have clear evidence of a broken iframe + // Missing contentWindow is a clear sign of tombstoning + if (!contentWindow) { return false; } - // For blob URLs, we can't reliably test accessibility due to sandboxing restrictions - // Only recreate if we can detect actual problems without being too aggressive - if (iframe.src.startsWith('blob:')) { - try { - // Try a simple property access that should work if the iframe is functional - // but won't violate sandbox restrictions - const hasWindow = !!contentWindow; - if (!hasWindow) { - return false; - } - // If we can't detect problems, assume it's functional to avoid being too aggressive - return true; - } catch (error) { - // If we get an error accessing basic properties, the iframe is likely broken - return false; - } + // Missing or invalid src indicates a problem + if (!iframe.src || iframe.src === 'about:blank') { + return false; } + // For normal cases (including blob URLs), assume functional to preserve user state + // Only the clear failures above will trigger recreation return true; } From 415793a9d97b903574ddc217e917e64cbbc1b561 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 14 Sep 2025 01:43:17 +0000 Subject: [PATCH 6/6] Revert packages/common/src/messages.ts to remove whitespace changes Co-authored-by: danmarshall <11507384+danmarshall@users.noreply.github.com> --- packages/common/src/messages.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/common/src/messages.ts b/packages/common/src/messages.ts index 52c4637a..3cbb23a7 100644 --- a/packages/common/src/messages.ts +++ b/packages/common/src/messages.ts @@ -74,5 +74,3 @@ export interface GuardedFetchResponseMessage { body?: string; error?: string; } - -