mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-17 06:04:51 +00:00
Add standalone mode for HTML artifact downloads
When downloading HTML artifacts, the generated HTML now works standalone without requiring the extension runtime. Changes: - Add PrepareHtmlOptions config object to SandboxedIframe.prepareHtmlDocument() - Add isStandalone flag to skip runtime bridge and navigation interceptor - When isStandalone=true: - window.sendRuntimeMessage is NOT defined - Navigation interceptor is NOT injected - Artifact runtime providers fall back to embedded data - HtmlArtifact download button now uses isStandalone: true This fixes the issue where downloaded HTML artifacts would: - Try to call window.sendRuntimeMessage (which would fail silently) - Try to postMessage to non-existent parent window - Not work when opened via file:// protocol Now downloaded artifacts work completely standalone with embedded data.
This commit is contained in:
parent
63bfe95c18
commit
26b774bb04
3 changed files with 66 additions and 24 deletions
|
|
@ -61,7 +61,7 @@ export function agentLoop(
|
||||||
queuedMessages = [];
|
queuedMessages = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("agent-loop: ", [...currentContext.messages]);
|
// console.log("agent-loop: ", [...currentContext.messages]);
|
||||||
|
|
||||||
// Stream assistant response
|
// Stream assistant response
|
||||||
const message = await streamAssistantResponse(currentContext, config, signal, stream, streamFn);
|
const message = await streamAssistantResponse(currentContext, config, signal, stream, streamFn);
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,16 @@ export interface SandboxResult {
|
||||||
*/
|
*/
|
||||||
export type SandboxUrlProvider = () => string;
|
export type SandboxUrlProvider = () => string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configuration for prepareHtmlDocument
|
||||||
|
*/
|
||||||
|
export interface PrepareHtmlOptions {
|
||||||
|
/** True if this is an HTML artifact (inject into existing HTML), false if REPL (wrap in HTML) */
|
||||||
|
isHtmlArtifact: boolean;
|
||||||
|
/** True if this is a standalone download (no runtime bridge, no navigation interceptor) */
|
||||||
|
isStandalone?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Escape HTML special sequences in code to prevent premature tag closure
|
* Escape HTML special sequences in code to prevent premature tag closure
|
||||||
* @param code Code that will be injected into <script> tags
|
* @param code Code that will be injected into <script> tags
|
||||||
|
|
@ -85,8 +95,11 @@ export class SandboxIframe extends LitElement {
|
||||||
|
|
||||||
RUNTIME_MESSAGE_ROUTER.registerSandbox(sandboxId, providers, consumers);
|
RUNTIME_MESSAGE_ROUTER.registerSandbox(sandboxId, providers, consumers);
|
||||||
|
|
||||||
// loadContent is always used for HTML artifacts
|
// loadContent is always used for HTML artifacts (not standalone)
|
||||||
const completeHtml = this.prepareHtmlDocument(sandboxId, htmlContent, providers, true);
|
const completeHtml = this.prepareHtmlDocument(sandboxId, htmlContent, providers, {
|
||||||
|
isHtmlArtifact: true,
|
||||||
|
isStandalone: false,
|
||||||
|
});
|
||||||
|
|
||||||
// Validate HTML before loading
|
// Validate HTML before loading
|
||||||
const validationError = this.validateHtml(completeHtml);
|
const validationError = this.validateHtml(completeHtml);
|
||||||
|
|
@ -309,7 +322,10 @@ export class SandboxIframe extends LitElement {
|
||||||
}, 120000);
|
}, 120000);
|
||||||
|
|
||||||
// 4. Prepare HTML and create iframe
|
// 4. Prepare HTML and create iframe
|
||||||
const completeHtml = this.prepareHtmlDocument(sandboxId, code, providers, isHtmlArtifact);
|
const completeHtml = this.prepareHtmlDocument(sandboxId, code, providers, {
|
||||||
|
isHtmlArtifact,
|
||||||
|
isStandalone: false,
|
||||||
|
});
|
||||||
|
|
||||||
// 5. Validate HTML before sending to sandbox
|
// 5. Validate HTML before sending to sandbox
|
||||||
const validationError = this.validateHtml(completeHtml);
|
const validationError = this.validateHtml(completeHtml);
|
||||||
|
|
@ -411,14 +427,21 @@ export class SandboxIframe extends LitElement {
|
||||||
sandboxId: string,
|
sandboxId: string,
|
||||||
userCode: string,
|
userCode: string,
|
||||||
providers: SandboxRuntimeProvider[] = [],
|
providers: SandboxRuntimeProvider[] = [],
|
||||||
isHtmlArtifact: boolean = false,
|
options?: PrepareHtmlOptions,
|
||||||
): string {
|
): string {
|
||||||
|
// Default options
|
||||||
|
const opts: PrepareHtmlOptions = {
|
||||||
|
isHtmlArtifact: false,
|
||||||
|
isStandalone: false,
|
||||||
|
...options,
|
||||||
|
};
|
||||||
|
|
||||||
// Runtime script that will be injected
|
// Runtime script that will be injected
|
||||||
const runtime = this.getRuntimeScript(sandboxId, providers);
|
const runtime = this.getRuntimeScript(sandboxId, providers, opts.isStandalone || false);
|
||||||
|
|
||||||
// Only check for HTML tags if explicitly marked as HTML artifact
|
// Only check for HTML tags if explicitly marked as HTML artifact
|
||||||
// For javascript_repl, userCode is JavaScript that may contain HTML in string literals
|
// For javascript_repl, userCode is JavaScript that may contain HTML in string literals
|
||||||
if (isHtmlArtifact) {
|
if (opts.isHtmlArtifact) {
|
||||||
// HTML Artifact - inject runtime into existing HTML
|
// HTML Artifact - inject runtime into existing HTML
|
||||||
const headMatch = userCode.match(/<head[^>]*>/i);
|
const headMatch = userCode.match(/<head[^>]*>/i);
|
||||||
if (headMatch) {
|
if (headMatch) {
|
||||||
|
|
@ -490,19 +513,28 @@ export class SandboxIframe extends LitElement {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate runtime script from providers
|
* Generate runtime script from providers
|
||||||
|
* @param sandboxId Unique sandbox ID
|
||||||
|
* @param providers Runtime providers
|
||||||
|
* @param isStandalone If true, skip runtime bridge and navigation interceptor (for standalone downloads)
|
||||||
*/
|
*/
|
||||||
private getRuntimeScript(sandboxId: string, providers: SandboxRuntimeProvider[] = []): string {
|
private getRuntimeScript(
|
||||||
|
sandboxId: string,
|
||||||
|
providers: SandboxRuntimeProvider[] = [],
|
||||||
|
isStandalone: boolean = false,
|
||||||
|
): string {
|
||||||
// Collect all data from providers
|
// Collect all data from providers
|
||||||
const allData: Record<string, any> = {};
|
const allData: Record<string, any> = {};
|
||||||
for (const provider of providers) {
|
for (const provider of providers) {
|
||||||
Object.assign(allData, provider.getData());
|
Object.assign(allData, provider.getData());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate bridge code
|
// Generate bridge code (skip if standalone)
|
||||||
const bridgeCode = RuntimeMessageBridge.generateBridgeCode({
|
const bridgeCode = isStandalone
|
||||||
context: "sandbox-iframe",
|
? ""
|
||||||
sandboxId,
|
: RuntimeMessageBridge.generateBridgeCode({
|
||||||
});
|
context: "sandbox-iframe",
|
||||||
|
sandboxId,
|
||||||
|
});
|
||||||
|
|
||||||
// Collect all runtime functions - pass sandboxId as string literal
|
// Collect all runtime functions - pass sandboxId as string literal
|
||||||
const runtimeFunctions: string[] = [];
|
const runtimeFunctions: string[] = [];
|
||||||
|
|
@ -523,17 +555,11 @@ export class SandboxIframe extends LitElement {
|
||||||
// found in an extension context like sidepanel, settin body { font-size: 75% }. It's
|
// found in an extension context like sidepanel, settin body { font-size: 75% }. It's
|
||||||
// definitely not our code doing that.
|
// definitely not our code doing that.
|
||||||
// See https://stackoverflow.com/questions/71480433/chrome-is-injecting-some-stylesheet-in-popup-ui-which-reduces-the-font-size-to-7
|
// See https://stackoverflow.com/questions/71480433/chrome-is-injecting-some-stylesheet-in-popup-ui-which-reduces-the-font-size-to-7
|
||||||
return `<style>
|
|
||||||
html, body {
|
|
||||||
font-size: initial;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<script>
|
|
||||||
window.sandboxId = ${JSON.stringify(sandboxId)};
|
|
||||||
${dataInjection}
|
|
||||||
${bridgeCode}
|
|
||||||
${runtimeFunctions.join("\n")}
|
|
||||||
|
|
||||||
|
// Navigation interceptor (only if NOT standalone)
|
||||||
|
const navigationInterceptor = isStandalone
|
||||||
|
? ""
|
||||||
|
: `
|
||||||
// Navigation interceptor: prevent all navigation and open externally
|
// Navigation interceptor: prevent all navigation and open externally
|
||||||
(function() {
|
(function() {
|
||||||
// Intercept link clicks
|
// Intercept link clicks
|
||||||
|
|
@ -572,6 +598,19 @@ ${runtimeFunctions.join("\n")}
|
||||||
// Already defined, skip
|
// Already defined, skip
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
`;
|
||||||
|
|
||||||
|
return `<style>
|
||||||
|
html, body {
|
||||||
|
font-size: initial;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<script>
|
||||||
|
window.sandboxId = ${JSON.stringify(sandboxId)};
|
||||||
|
${dataInjection}
|
||||||
|
${bridgeCode}
|
||||||
|
${runtimeFunctions.join("\n")}
|
||||||
|
${navigationInterceptor}
|
||||||
</script>`;
|
</script>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,10 @@ export class HtmlArtifact extends ArtifactElement {
|
||||||
const sandbox = this.sandboxIframeRef.value;
|
const sandbox = this.sandboxIframeRef.value;
|
||||||
const sandboxId = `artifact-${this.filename}`;
|
const sandboxId = `artifact-${this.filename}`;
|
||||||
const downloadContent =
|
const downloadContent =
|
||||||
sandbox?.prepareHtmlDocument(sandboxId, this._content, this.runtimeProviders || [], true) || this._content;
|
sandbox?.prepareHtmlDocument(sandboxId, this._content, this.runtimeProviders || [], {
|
||||||
|
isHtmlArtifact: true,
|
||||||
|
isStandalone: true, // Skip runtime bridge and navigation interceptor for standalone downloads
|
||||||
|
}) || this._content;
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue