2
0

index.html 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414
  1. <!doctype html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8" />
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  6. <meta
  7. http-equiv="Content-Security-Policy"
  8. 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'"
  9. />
  10. <title>OpenCode</title>
  11. <style>
  12. html,
  13. body {
  14. height: 100%;
  15. width: 100%;
  16. margin: 0;
  17. padding: 0;
  18. background: #1e1e1e;
  19. overflow: hidden;
  20. }
  21. #loading {
  22. position: absolute;
  23. top: 0;
  24. right: 0;
  25. bottom: 0;
  26. left: 0;
  27. display: flex;
  28. flex-direction: column;
  29. align-items: center;
  30. justify-content: center;
  31. color: #ccc;
  32. font-size: 13px;
  33. }
  34. #webui-container {
  35. display: none;
  36. position: absolute;
  37. top: 0;
  38. right: 0;
  39. bottom: 0;
  40. left: 0;
  41. }
  42. #webui-frame {
  43. position: absolute;
  44. top: 0;
  45. right: 0;
  46. bottom: 0;
  47. left: 0;
  48. width: 100%;
  49. height: 100%;
  50. border: none;
  51. background: #1e1e1e;
  52. display: block;
  53. }
  54. .spinner {
  55. width: 16px;
  56. height: 16px;
  57. border: 2px solid #333;
  58. border-top: 2px solid #007acc;
  59. border-radius: 50%;
  60. animation: spin 1s linear infinite;
  61. margin-bottom: 8px;
  62. }
  63. @keyframes spin {
  64. 0% {
  65. transform: rotate(0deg);
  66. }
  67. 100% {
  68. transform: rotate(360deg);
  69. }
  70. }
  71. .error-container {
  72. position: absolute;
  73. top: 0;
  74. right: 0;
  75. bottom: 0;
  76. left: 0;
  77. display: flex;
  78. flex-direction: column;
  79. align-items: center;
  80. justify-content: center;
  81. color: #f48771;
  82. text-align: center;
  83. padding: 12px;
  84. }
  85. .retry-button {
  86. background: #0e639c;
  87. color: white;
  88. border: none;
  89. padding: 6px 12px;
  90. border-radius: 4px;
  91. cursor: pointer;
  92. }
  93. </style>
  94. </head>
  95. <body>
  96. <div id="loading">
  97. <div class="spinner"></div>
  98. <div>Starting OpenCode...</div>
  99. <div id="status">Initializing backend...</div>
  100. </div>
  101. <div id="webui-container">
  102. <iframe
  103. id="webui-frame"
  104. src="${uiUrl}"
  105. sandbox="allow-scripts allow-same-origin allow-forms allow-popups allow-modals allow-pointer-lock allow-top-navigation-by-user-activation"
  106. allow="cross-origin-isolated; autoplay; clipboard-read; clipboard-write;"
  107. ></iframe>
  108. </div>
  109. <script>
  110. window.vscode = acquireVsCodeApi()
  111. let uiLoaded = false
  112. let loadTimeout
  113. const iframe = document.getElementById("webui-frame")
  114. const loading = document.getElementById("loading")
  115. const container = document.getElementById("webui-container")
  116. const status = document.getElementById("status")
  117. function updateStatus(message) {
  118. if (status) status.textContent = message
  119. }
  120. function showError(title, message) {
  121. document.body.innerHTML =
  122. '<div class="error-container"><div style="font-weight:bold;margin-bottom:6px">' +
  123. title +
  124. '</div><div style="margin-bottom:10px">' +
  125. message +
  126. '</div><button class="retry-button" onclick="location.reload()">Retry</button></div>'
  127. }
  128. iframe.onload = function () {
  129. clearTimeout(loadTimeout)
  130. uiLoaded = true
  131. loading.style.display = "none"
  132. container.style.display = "block"
  133. // Send parent origin to iframe for secure communication
  134. try {
  135. const targetOrigin = new URL("${uiUrl}").origin
  136. iframe.contentWindow.postMessage(
  137. {
  138. type: "setParentOrigin",
  139. origin: window.origin,
  140. },
  141. targetOrigin,
  142. )
  143. } catch (e) {
  144. console.error("Failed to send parent origin to iframe:", e)
  145. }
  146. window.vscode.postMessage({ type: "uiLoaded", success: true })
  147. }
  148. iframe.onerror = function () {
  149. clearTimeout(loadTimeout)
  150. showError("Failed to Load OpenCode UI", "Could not load the web interface.")
  151. window.vscode.postMessage({ type: "uiLoaded", success: false, error: "iframe load error" })
  152. }
  153. loadTimeout = setTimeout(() => {
  154. if (!uiLoaded) {
  155. showError("OpenCode UI Load Timeout", "The web interface took too long to load.")
  156. window.vscode.postMessage({ type: "uiLoaded", success: false, error: "load timeout" })
  157. }
  158. }, 30000)
  159. window.addEventListener("message", (event) => {
  160. const message = event.data
  161. // Check if message is coming from the iframe (child)
  162. if (event.source === iframe.contentWindow) {
  163. // Validate origin for security - only accept messages from the expected iframe origin
  164. try {
  165. const expectedOrigin = new URL("${uiUrl}").origin
  166. if (event.origin !== expectedOrigin) {
  167. console.warn("Message from iframe rejected due to invalid origin:", event.origin)
  168. return
  169. }
  170. } catch (e) {
  171. console.error("Failed to validate iframe origin:", e)
  172. return
  173. }
  174. // Handle keyboard events from iframe (macOS fix)
  175. if (message && (message.type === "keydown-event" || message.type === "keyup-event")) {
  176. try {
  177. const { ctrlKey, metaKey, shiftKey, altKey, code, key, hasSelection, inEditable } = message.payload
  178. // Handle specific VSCode shortcuts that need to be forwarded
  179. if (message.type === "keydown-event") {
  180. // Command Palette (Cmd+Shift+P)
  181. if (metaKey && shiftKey && code === "KeyP") {
  182. window.vscode.postMessage({
  183. type: "executeCommand",
  184. command: "workbench.action.showCommands",
  185. })
  186. return
  187. }
  188. // Quick Open (Cmd+P)
  189. if (metaKey && !shiftKey && code === "KeyP") {
  190. window.vscode.postMessage({
  191. type: "executeCommand",
  192. command: "workbench.action.quickOpen",
  193. })
  194. return
  195. }
  196. // Save (Cmd+S)
  197. if (metaKey && code === "KeyS") {
  198. window.vscode.postMessage({
  199. type: "executeCommand",
  200. command: "workbench.action.files.save",
  201. })
  202. return
  203. }
  204. // Select All (Cmd+A) - forward to VSCode if no selection in iframe
  205. if (metaKey && code === "KeyA" && !hasSelection) {
  206. window.vscode.postMessage({
  207. type: "executeCommand",
  208. command: "editor.action.selectAll",
  209. })
  210. return
  211. }
  212. // New File (Cmd+N)
  213. if (metaKey && code === "KeyN") {
  214. window.vscode.postMessage({
  215. type: "executeCommand",
  216. command: "workbench.action.files.newUntitledFile",
  217. })
  218. return
  219. }
  220. // Find (Cmd+F)
  221. if (metaKey && code === "KeyF") {
  222. window.vscode.postMessage({
  223. type: "executeCommand",
  224. command: "actions.find",
  225. })
  226. return
  227. }
  228. // Undo (Cmd+Z)
  229. if (metaKey && !shiftKey && code === "KeyZ") {
  230. window.vscode.postMessage({
  231. type: "executeCommand",
  232. command: "undo",
  233. })
  234. return
  235. }
  236. // Redo (Cmd+Shift+Z)
  237. if (metaKey && shiftKey && code === "KeyZ") {
  238. window.vscode.postMessage({
  239. type: "executeCommand",
  240. command: "redo",
  241. })
  242. return
  243. }
  244. // Copy/Cut/Paste: forward to VSCode only when iframe isn't editing
  245. if (metaKey && code === "KeyC" && !inEditable && !hasSelection) {
  246. window.vscode.postMessage({ type: "executeCommand", command: "editor.action.clipboardCopyAction" })
  247. return
  248. }
  249. if (metaKey && code === "KeyX" && !inEditable && !hasSelection) {
  250. window.vscode.postMessage({ type: "executeCommand", command: "editor.action.clipboardCutAction" })
  251. return
  252. }
  253. if (metaKey && code === "KeyV" && !inEditable) {
  254. window.vscode.postMessage({ type: "executeCommand", command: "editor.action.clipboardPasteAction" })
  255. return
  256. }
  257. }
  258. // Forward the keyboard event to VSCode extension for any additional handling
  259. window.vscode.postMessage({
  260. type: "keyboardEvent",
  261. eventType: message.type,
  262. payload: message.payload,
  263. })
  264. } catch (e) {
  265. console.error("Failed to handle keyboard event from iframe:", e)
  266. }
  267. return
  268. }
  269. // Handle other messages FROM iframe - forward to VS Code extension
  270. if (message && message.type) {
  271. try {
  272. window.vscode.postMessage(message)
  273. } catch (e) {
  274. console.error("Failed to forward message to VS Code:", e)
  275. }
  276. }
  277. } else {
  278. // Handle messages TO iframe (from VS Code extension)
  279. if (message && message.type) {
  280. try {
  281. const targetOrigin = new URL("${uiUrl}").origin
  282. iframe.contentWindow.postMessage(message, targetOrigin)
  283. } catch (e) {
  284. console.error("Forwarding message to iframe failed", e)
  285. }
  286. }
  287. }
  288. })
  289. // macOS drag and drop event forwarding to iframe
  290. // Capture drag events on the webview container and forward them to iframe
  291. ;["dragenter", "dragover", "dragleave", "drop"].forEach((eventType) => {
  292. document.addEventListener(
  293. eventType,
  294. (event) => {
  295. try {
  296. // Prevent default to allow drop
  297. if (eventType === "dragover" || eventType === "dragenter") {
  298. event.preventDefault()
  299. if (event.dataTransfer) {
  300. event.dataTransfer.dropEffect = "copy"
  301. }
  302. }
  303. // Extract relevant data from the drag event
  304. const payload = {
  305. clientX: event.clientX,
  306. clientY: event.clientY,
  307. shiftKey: event.shiftKey,
  308. dataTransfer: null,
  309. }
  310. // Try to extract dataTransfer data (limited by security)
  311. if (event.dataTransfer) {
  312. try {
  313. const dataTransfer = {
  314. types: Array.from(event.dataTransfer.types || []),
  315. effectAllowed: event.dataTransfer.effectAllowed,
  316. dropEffect: event.dataTransfer.dropEffect,
  317. data: {},
  318. }
  319. // Try to get data for each type
  320. for (const type of dataTransfer.types) {
  321. try {
  322. dataTransfer.data[type] = event.dataTransfer.getData(type)
  323. } catch (e) {
  324. // Some data types may not be accessible due to security restrictions
  325. }
  326. }
  327. // Debug logging to see what we're forwarding
  328. // console.log('[VSCode Webview] Forwarding drag event:', {
  329. // eventType,
  330. // dataTransfer: {
  331. // types: dataTransfer.types,
  332. // effectAllowed: dataTransfer.effectAllowed,
  333. // dropEffect: dataTransfer.dropEffect,
  334. // data: dataTransfer.data,
  335. // hasFiles: !!event.dataTransfer.files,
  336. // filesLength: event.dataTransfer.files ? event.dataTransfer.files.length : 0
  337. // }
  338. // });
  339. payload.dataTransfer = dataTransfer
  340. } catch (e) {
  341. console.debug("Failed to extract dataTransfer data:", e)
  342. }
  343. }
  344. // Handle drop by asking the extension to read dropped URIs.
  345. // VSCode 1.108+ may no longer expose file paths to the iframe.
  346. if (eventType === "drop" && payload.dataTransfer && payload.dataTransfer.data) {
  347. const dt = payload.dataTransfer.data
  348. const uriList = dt["application/vnd.code.uri-list"] || dt["text/uri-list"]
  349. if (uriList) {
  350. const uris = uriList
  351. .split(/\r?\n/)
  352. .map((s) => s.trim())
  353. .filter((s) => s && !s.startsWith("#"))
  354. if (uris.length > 0) {
  355. window.vscode.postMessage({ type: "readUris", uris: uris })
  356. event.preventDefault()
  357. event.stopPropagation()
  358. return
  359. }
  360. }
  361. }
  362. // Forward the drag event to iframe
  363. if (iframe && iframe.contentWindow) {
  364. const targetOrigin = new URL("${uiUrl}").origin
  365. iframe.contentWindow.postMessage(
  366. {
  367. type: "drag-event",
  368. eventType: eventType,
  369. payload: payload,
  370. },
  371. targetOrigin,
  372. )
  373. }
  374. // For drop events, also prevent default to avoid browser handling
  375. if (eventType === "drop") {
  376. event.preventDefault()
  377. event.stopPropagation()
  378. }
  379. } catch (e) {
  380. console.debug("Failed to forward drag event to iframe:", e)
  381. }
  382. },
  383. true,
  384. ) // Use capture phase to ensure we get the events first
  385. })
  386. </script>
  387. </body>
  388. </html>