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;
}
-
-