diff --git a/packages/op-vscode/src/vscode/pen-editor-provider.ts b/packages/op-vscode/src/vscode/pen-editor-provider.ts index 8b7e2f777..172fa19dd 100644 --- a/packages/op-vscode/src/vscode/pen-editor-provider.ts +++ b/packages/op-vscode/src/vscode/pen-editor-provider.ts @@ -13,7 +13,7 @@ import { PenSession, type SessionHost } from "../session/pen-session"; import type { SessionRegistry } from "../session/session-registry"; import type { BridgeOutboundToPage } from "../protocol/bridge"; import { encodeOutbound } from "../protocol/bridge"; -import { buildBootHtml, buildWebviewHtml } from "./webview-shell"; +import { appendEmbedQuery, buildBootHtml, buildWebviewHtml } from "./webview-shell"; import { pickRestartSource, resolveDaemonBinary, type LatestDurable } from "./restart-source"; import { isShellControl, parseShellReadyOrigin } from "./shell-messages"; import { encodeFigBytes, figSaveTargetPath, isFigPath } from "./fig-source"; @@ -249,8 +249,13 @@ export class PenEditorProvider implements vscode.CustomEditorProvider { const key = document.uri.fsPath; const nonce = makeNonce(); - const external = await vscode.env.asExternalUri(vscode.Uri.parse(`${client.baseUrl}/?embed=vscode`)); - panel.webview.html = buildWebviewHtml({ iframeSrc: external.toString(), nonce }); + // The embed flag is appended AFTER asExternalUri: putting it in the Uri + // makes toString() percent-encode the "=" ("?embed%3Dvscode"), which the + // wasm-side EmbedHost::from_query correctly refuses — the editor then + // silently renders full chrome inside the plugin. + const external = await vscode.env.asExternalUri(vscode.Uri.parse(`${client.baseUrl}/`)); + const iframeSrc = appendEmbedQuery(external.toString(true)); + panel.webview.html = buildWebviewHtml({ iframeSrc, nonce }); const host = this.makeHost(document, panel, watcherState); const session = new PenSession(host, client.handshake.token, document.bootJson); diff --git a/packages/op-vscode/src/vscode/webview-shell.test.ts b/packages/op-vscode/src/vscode/webview-shell.test.ts index 9f28041f9..1a320c47d 100644 --- a/packages/op-vscode/src/vscode/webview-shell.test.ts +++ b/packages/op-vscode/src/vscode/webview-shell.test.ts @@ -1,5 +1,5 @@ -import { test, expect } from "bun:test"; -import { buildBootHtml, buildWebviewHtml, originOf } from "./webview-shell"; +import { describe, expect, test } from "bun:test"; +import { appendEmbedQuery, buildBootHtml, buildWebviewHtml, originOf } from "./webview-shell"; test("originOf extracts scheme://host:port", () => { expect(originOf("http://127.0.0.1:45001/")).toBe("http://127.0.0.1:45001"); @@ -56,3 +56,17 @@ test("nonce is not reused across boot and full unless the caller reuses it", () expect(buildBootHtml("A")).toContain('nonce="A"'); expect(buildWebviewHtml({ iframeSrc: "http://x.y:1/", nonce: "B" })).toContain('nonce="B"'); }); + +describe("appendEmbedQuery", () => { + test("appends ?embed=vscode to a bare externalized URL", () => { + expect(appendEmbedQuery("http://127.0.0.1:49793/")).toBe("http://127.0.0.1:49793/?embed=vscode"); + }); + test("appends with & when the base already has a query", () => { + expect(appendEmbedQuery("https://tunnel.example/x/?tkn=abc")).toBe( + "https://tunnel.example/x/?tkn=abc&embed=vscode", + ); + }); + test("never percent-encodes the equals sign", () => { + expect(appendEmbedQuery("http://127.0.0.1:1/")).not.toContain("%3D"); + }); +}); diff --git a/packages/op-vscode/src/vscode/webview-shell.ts b/packages/op-vscode/src/vscode/webview-shell.ts index 854406d36..9c99881d2 100644 --- a/packages/op-vscode/src/vscode/webview-shell.ts +++ b/packages/op-vscode/src/vscode/webview-shell.ts @@ -11,6 +11,15 @@ export function originOf(url: string): string { return new URL(url).origin; } +/** Append the `embed=vscode` flag to an already-externalized editor URL. + * Kept as plain string concatenation (never `vscode.Uri`): Uri.toString() + * percent-encodes the query's "=" into "%3D", which the wasm-side + * `EmbedHost::from_query` refuses by design. Tolerates a base that already + * carries a query (e.g. tunnel tokens in remote scenarios). */ +export function appendEmbedQuery(base: string): string { + return `${base}${base.includes("?") ? "&" : "?"}embed=vscode`; +} + /** Phase 1: no iframe. On load it reports window.origin so the extension can * spawn the daemon with the correct --allow-origin, then waits for the * extension to replace the HTML with the full shell. */