| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414 |
- <!doctype html>
- <html lang="en">
- <head>
- <meta charset="UTF-8" />
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
- <meta
- http-equiv="Content-Security-Policy"
- content="default-src 'none'; script-src 'unsafe-inline' 'unsafe-eval' ${cspOrigins} ${cspSource}; style-src 'unsafe-inline' ${cspOrigins} ${cspSource}; img-src 'self' data: ${cspOrigins} https://*.vscode-cdn.net; connect-src ws://127.0.0.1:* wss://127.0.0.1:* ws://localhost:* wss://localhost:* ${cspOrigins}; font-src 'self' data: ${cspOrigins}; media-src 'self' ${cspOrigins}; frame-src ${cspOrigins}; object-src 'none'; base-uri 'none'"
- />
- <title>OpenCode</title>
- <style>
- html,
- body {
- height: 100%;
- width: 100%;
- margin: 0;
- padding: 0;
- background: #1e1e1e;
- overflow: hidden;
- }
- #loading {
- position: absolute;
- top: 0;
- right: 0;
- bottom: 0;
- left: 0;
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- color: #ccc;
- font-size: 13px;
- }
- #webui-container {
- display: none;
- position: absolute;
- top: 0;
- right: 0;
- bottom: 0;
- left: 0;
- }
- #webui-frame {
- position: absolute;
- top: 0;
- right: 0;
- bottom: 0;
- left: 0;
- width: 100%;
- height: 100%;
- border: none;
- background: #1e1e1e;
- display: block;
- }
- .spinner {
- width: 16px;
- height: 16px;
- border: 2px solid #333;
- border-top: 2px solid #007acc;
- border-radius: 50%;
- animation: spin 1s linear infinite;
- margin-bottom: 8px;
- }
- @keyframes spin {
- 0% {
- transform: rotate(0deg);
- }
- 100% {
- transform: rotate(360deg);
- }
- }
- .error-container {
- position: absolute;
- top: 0;
- right: 0;
- bottom: 0;
- left: 0;
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- color: #f48771;
- text-align: center;
- padding: 12px;
- }
- .retry-button {
- background: #0e639c;
- color: white;
- border: none;
- padding: 6px 12px;
- border-radius: 4px;
- cursor: pointer;
- }
- </style>
- </head>
- <body>
- <div id="loading">
- <div class="spinner"></div>
- <div>Starting OpenCode...</div>
- <div id="status">Initializing backend...</div>
- </div>
- <div id="webui-container">
- <iframe
- id="webui-frame"
- src="${uiUrl}"
- sandbox="allow-scripts allow-same-origin allow-forms allow-popups allow-modals allow-pointer-lock allow-top-navigation-by-user-activation"
- allow="cross-origin-isolated; autoplay; clipboard-read; clipboard-write;"
- ></iframe>
- </div>
- <script>
- window.vscode = acquireVsCodeApi()
- let uiLoaded = false
- let loadTimeout
- const iframe = document.getElementById("webui-frame")
- const loading = document.getElementById("loading")
- const container = document.getElementById("webui-container")
- const status = document.getElementById("status")
- function updateStatus(message) {
- if (status) status.textContent = message
- }
- function showError(title, message) {
- document.body.innerHTML =
- '<div class="error-container"><div style="font-weight:bold;margin-bottom:6px">' +
- title +
- '</div><div style="margin-bottom:10px">' +
- message +
- '</div><button class="retry-button" onclick="location.reload()">Retry</button></div>'
- }
- iframe.onload = function () {
- clearTimeout(loadTimeout)
- uiLoaded = true
- loading.style.display = "none"
- container.style.display = "block"
- // Send parent origin to iframe for secure communication
- try {
- const targetOrigin = new URL("${uiUrl}").origin
- iframe.contentWindow.postMessage(
- {
- type: "setParentOrigin",
- origin: window.origin,
- },
- targetOrigin,
- )
- } catch (e) {
- console.error("Failed to send parent origin to iframe:", e)
- }
- window.vscode.postMessage({ type: "uiLoaded", success: true })
- }
- iframe.onerror = function () {
- clearTimeout(loadTimeout)
- showError("Failed to Load OpenCode UI", "Could not load the web interface.")
- window.vscode.postMessage({ type: "uiLoaded", success: false, error: "iframe load error" })
- }
- loadTimeout = setTimeout(() => {
- if (!uiLoaded) {
- showError("OpenCode UI Load Timeout", "The web interface took too long to load.")
- window.vscode.postMessage({ type: "uiLoaded", success: false, error: "load timeout" })
- }
- }, 30000)
- window.addEventListener("message", (event) => {
- const message = event.data
- // Check if message is coming from the iframe (child)
- if (event.source === iframe.contentWindow) {
- // Validate origin for security - only accept messages from the expected iframe origin
- try {
- const expectedOrigin = new URL("${uiUrl}").origin
- if (event.origin !== expectedOrigin) {
- console.warn("Message from iframe rejected due to invalid origin:", event.origin)
- return
- }
- } catch (e) {
- console.error("Failed to validate iframe origin:", e)
- return
- }
- // Handle keyboard events from iframe (macOS fix)
- if (message && (message.type === "keydown-event" || message.type === "keyup-event")) {
- try {
- const { ctrlKey, metaKey, shiftKey, altKey, code, key, hasSelection, inEditable } = message.payload
- // Handle specific VSCode shortcuts that need to be forwarded
- if (message.type === "keydown-event") {
- // Command Palette (Cmd+Shift+P)
- if (metaKey && shiftKey && code === "KeyP") {
- window.vscode.postMessage({
- type: "executeCommand",
- command: "workbench.action.showCommands",
- })
- return
- }
- // Quick Open (Cmd+P)
- if (metaKey && !shiftKey && code === "KeyP") {
- window.vscode.postMessage({
- type: "executeCommand",
- command: "workbench.action.quickOpen",
- })
- return
- }
- // Save (Cmd+S)
- if (metaKey && code === "KeyS") {
- window.vscode.postMessage({
- type: "executeCommand",
- command: "workbench.action.files.save",
- })
- return
- }
- // Select All (Cmd+A) - forward to VSCode if no selection in iframe
- if (metaKey && code === "KeyA" && !hasSelection) {
- window.vscode.postMessage({
- type: "executeCommand",
- command: "editor.action.selectAll",
- })
- return
- }
- // New File (Cmd+N)
- if (metaKey && code === "KeyN") {
- window.vscode.postMessage({
- type: "executeCommand",
- command: "workbench.action.files.newUntitledFile",
- })
- return
- }
- // Find (Cmd+F)
- if (metaKey && code === "KeyF") {
- window.vscode.postMessage({
- type: "executeCommand",
- command: "actions.find",
- })
- return
- }
- // Undo (Cmd+Z)
- if (metaKey && !shiftKey && code === "KeyZ") {
- window.vscode.postMessage({
- type: "executeCommand",
- command: "undo",
- })
- return
- }
- // Redo (Cmd+Shift+Z)
- if (metaKey && shiftKey && code === "KeyZ") {
- window.vscode.postMessage({
- type: "executeCommand",
- command: "redo",
- })
- return
- }
- // Copy/Cut/Paste: forward to VSCode only when iframe isn't editing
- if (metaKey && code === "KeyC" && !inEditable && !hasSelection) {
- window.vscode.postMessage({ type: "executeCommand", command: "editor.action.clipboardCopyAction" })
- return
- }
- if (metaKey && code === "KeyX" && !inEditable && !hasSelection) {
- window.vscode.postMessage({ type: "executeCommand", command: "editor.action.clipboardCutAction" })
- return
- }
- if (metaKey && code === "KeyV" && !inEditable) {
- window.vscode.postMessage({ type: "executeCommand", command: "editor.action.clipboardPasteAction" })
- return
- }
- }
- // Forward the keyboard event to VSCode extension for any additional handling
- window.vscode.postMessage({
- type: "keyboardEvent",
- eventType: message.type,
- payload: message.payload,
- })
- } catch (e) {
- console.error("Failed to handle keyboard event from iframe:", e)
- }
- return
- }
- // Handle other messages FROM iframe - forward to VS Code extension
- if (message && message.type) {
- try {
- window.vscode.postMessage(message)
- } catch (e) {
- console.error("Failed to forward message to VS Code:", e)
- }
- }
- } else {
- // Handle messages TO iframe (from VS Code extension)
- if (message && message.type) {
- try {
- const targetOrigin = new URL("${uiUrl}").origin
- iframe.contentWindow.postMessage(message, targetOrigin)
- } catch (e) {
- console.error("Forwarding message to iframe failed", e)
- }
- }
- }
- })
- // macOS drag and drop event forwarding to iframe
- // Capture drag events on the webview container and forward them to iframe
- ;["dragenter", "dragover", "dragleave", "drop"].forEach((eventType) => {
- document.addEventListener(
- eventType,
- (event) => {
- try {
- // Prevent default to allow drop
- if (eventType === "dragover" || eventType === "dragenter") {
- event.preventDefault()
- if (event.dataTransfer) {
- event.dataTransfer.dropEffect = "copy"
- }
- }
- // Extract relevant data from the drag event
- const payload = {
- clientX: event.clientX,
- clientY: event.clientY,
- shiftKey: event.shiftKey,
- dataTransfer: null,
- }
- // Try to extract dataTransfer data (limited by security)
- if (event.dataTransfer) {
- try {
- const dataTransfer = {
- types: Array.from(event.dataTransfer.types || []),
- effectAllowed: event.dataTransfer.effectAllowed,
- dropEffect: event.dataTransfer.dropEffect,
- data: {},
- }
- // Try to get data for each type
- for (const type of dataTransfer.types) {
- try {
- dataTransfer.data[type] = event.dataTransfer.getData(type)
- } catch (e) {
- // Some data types may not be accessible due to security restrictions
- }
- }
- // Debug logging to see what we're forwarding
- // console.log('[VSCode Webview] Forwarding drag event:', {
- // eventType,
- // dataTransfer: {
- // types: dataTransfer.types,
- // effectAllowed: dataTransfer.effectAllowed,
- // dropEffect: dataTransfer.dropEffect,
- // data: dataTransfer.data,
- // hasFiles: !!event.dataTransfer.files,
- // filesLength: event.dataTransfer.files ? event.dataTransfer.files.length : 0
- // }
- // });
- payload.dataTransfer = dataTransfer
- } catch (e) {
- console.debug("Failed to extract dataTransfer data:", e)
- }
- }
- // Handle drop by asking the extension to read dropped URIs.
- // VSCode 1.108+ may no longer expose file paths to the iframe.
- if (eventType === "drop" && payload.dataTransfer && payload.dataTransfer.data) {
- const dt = payload.dataTransfer.data
- const uriList = dt["application/vnd.code.uri-list"] || dt["text/uri-list"]
- if (uriList) {
- const uris = uriList
- .split(/\r?\n/)
- .map((s) => s.trim())
- .filter((s) => s && !s.startsWith("#"))
- if (uris.length > 0) {
- window.vscode.postMessage({ type: "readUris", uris: uris })
- event.preventDefault()
- event.stopPropagation()
- return
- }
- }
- }
- // Forward the drag event to iframe
- if (iframe && iframe.contentWindow) {
- const targetOrigin = new URL("${uiUrl}").origin
- iframe.contentWindow.postMessage(
- {
- type: "drag-event",
- eventType: eventType,
- payload: payload,
- },
- targetOrigin,
- )
- }
- // For drop events, also prevent default to avoid browser handling
- if (eventType === "drop") {
- event.preventDefault()
- event.stopPropagation()
- }
- } catch (e) {
- console.debug("Failed to forward drag event to iframe:", e)
- }
- },
- true,
- ) // Use capture phase to ensure we get the events first
- })
- </script>
- </body>
- </html>
|