index.html 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406
  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' http://127.0.0.1:* https://127.0.0.1:* ${cspSource}; style-src 'unsafe-inline' http://127.0.0.1:* https://127.0.0.1:* ${cspSource}; img-src 'self' data: http://127.0.0.1:* https://127.0.0.1:* https://*.vscode-cdn.net; connect-src ws://127.0.0.1:* wss://127.0.0.1:* http://127.0.0.1:* https://127.0.0.1:*; font-src 'self' data: http://127.0.0.1:* https://127.0.0.1:*; media-src 'self' http://127.0.0.1:* https://127.0.0.1:*; frame-src http://127.0.0.1:* https://127.0.0.1:*; 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 } = 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 (Cmd+C) - macOS only
  245. if (metaKey && code === "KeyC") {
  246. window.vscode.postMessage({
  247. type: "executeCommand",
  248. command: "editor.action.clipboardCopyAction",
  249. })
  250. return
  251. }
  252. // Paste (Cmd+V) - macOS only
  253. if (metaKey && code === "KeyV") {
  254. window.vscode.postMessage({
  255. type: "executeCommand",
  256. command: "editor.action.clipboardPasteAction",
  257. })
  258. return
  259. }
  260. }
  261. // Forward the keyboard event to VSCode extension for any additional handling
  262. window.vscode.postMessage({
  263. type: "keyboardEvent",
  264. eventType: message.type,
  265. payload: message.payload,
  266. })
  267. } catch (e) {
  268. console.error("Failed to handle keyboard event from iframe:", e)
  269. }
  270. return
  271. }
  272. // Forward ideBridge UI->host messages: child posts { type: '__ideBridgeSend', json }
  273. if (message && message.type === "__ideBridgeSend" && typeof message.json === "string") {
  274. try {
  275. window.vscode.postMessage({ type: "__ideBridgeSend", json: message.json })
  276. } catch (e) {
  277. console.error("Failed to forward ideBridge message to VS Code:", e)
  278. }
  279. return
  280. }
  281. // Handle other messages FROM iframe - forward to VS Code extension
  282. if (message && message.type) {
  283. try {
  284. window.vscode.postMessage(message)
  285. } catch (e) {
  286. console.error("Failed to forward message to VS Code:", e)
  287. }
  288. }
  289. } else {
  290. // Handle messages TO iframe (from VS Code extension)
  291. if (message && message.type) {
  292. try {
  293. const targetOrigin = new URL("${uiUrl}").origin
  294. iframe.contentWindow.postMessage(message, targetOrigin)
  295. } catch (e) {
  296. console.error("Forwarding message to iframe failed", e)
  297. }
  298. }
  299. }
  300. })
  301. // macOS drag and drop event forwarding to iframe
  302. // Capture drag events on the webview container and forward them to iframe
  303. ;["dragenter", "dragover", "dragleave", "drop"].forEach((eventType) => {
  304. document.addEventListener(
  305. eventType,
  306. (event) => {
  307. try {
  308. // Prevent default to allow drop
  309. if (eventType === "dragover" || eventType === "dragenter") {
  310. event.preventDefault()
  311. if (event.dataTransfer) {
  312. event.dataTransfer.dropEffect = "copy"
  313. }
  314. }
  315. // Extract relevant data from the drag event
  316. const payload = {
  317. clientX: event.clientX,
  318. clientY: event.clientY,
  319. shiftKey: event.shiftKey,
  320. dataTransfer: null,
  321. }
  322. // Try to extract dataTransfer data (limited by security)
  323. if (event.dataTransfer) {
  324. try {
  325. const dataTransfer = {
  326. types: Array.from(event.dataTransfer.types || []),
  327. effectAllowed: event.dataTransfer.effectAllowed,
  328. dropEffect: event.dataTransfer.dropEffect,
  329. data: {},
  330. }
  331. // Try to get data for each type
  332. for (const type of dataTransfer.types) {
  333. try {
  334. dataTransfer.data[type] = event.dataTransfer.getData(type)
  335. } catch (e) {
  336. // Some data types may not be accessible due to security restrictions
  337. }
  338. }
  339. // Debug logging to see what we're forwarding
  340. // console.log('[VSCode Webview] Forwarding drag event:', {
  341. // eventType,
  342. // dataTransfer: {
  343. // types: dataTransfer.types,
  344. // effectAllowed: dataTransfer.effectAllowed,
  345. // dropEffect: dataTransfer.dropEffect,
  346. // data: dataTransfer.data,
  347. // hasFiles: !!event.dataTransfer.files,
  348. // filesLength: event.dataTransfer.files ? event.dataTransfer.files.length : 0
  349. // }
  350. // });
  351. payload.dataTransfer = dataTransfer
  352. } catch (e) {
  353. console.debug("Failed to extract dataTransfer data:", e)
  354. }
  355. }
  356. // Forward the drag event to iframe
  357. if (iframe && iframe.contentWindow) {
  358. const targetOrigin = new URL("${uiUrl}").origin
  359. iframe.contentWindow.postMessage(
  360. {
  361. type: "drag-event",
  362. eventType: eventType,
  363. payload: payload,
  364. },
  365. targetOrigin,
  366. )
  367. }
  368. // For drop events, also prevent default to avoid browser handling
  369. if (eventType === "drop") {
  370. event.preventDefault()
  371. event.stopPropagation()
  372. }
  373. } catch (e) {
  374. console.debug("Failed to forward drag event to iframe:", e)
  375. }
  376. },
  377. true,
  378. ) // Use capture phase to ensure we get the events first
  379. })
  380. </script>
  381. </body>
  382. </html>