| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8" /> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> |
| <title>OpenHands Agent Canvas</title> |
| <style> |
| *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } |
| |
| |
| |
| |
| :root { |
| --oh-background: #0b0e14; |
| --oh-foreground: #eef2f7; |
| --oh-muted: #a3b0c4; |
| --oh-text-dim: #7e8a9e; |
| --oh-text-subtle: #626d82; |
| --oh-border: #4b5468; |
| --oh-border-subtle: #383f50; |
| --oh-surface-deep: #05070a; |
| --oh-color-danger: #e76a5e; |
| } |
| |
| html, body { |
| width: 100%; height: 100%; |
| background: var(--oh-background); |
| font-family: |
| -apple-system, "SF Pro", BlinkMacSystemFont, "Segoe UI", "Roboto", |
| "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", |
| "Helvetica Neue", sans-serif; |
| -webkit-font-smoothing: antialiased; |
| -moz-osx-font-smoothing: grayscale; |
| color: var(--oh-foreground); |
| user-select: none; |
| -webkit-app-region: drag; |
| overflow: hidden; |
| } |
| |
| body { |
| display: flex; |
| flex-direction: column; |
| } |
| |
| .container { |
| display: flex; |
| flex-direction: column; |
| align-items: center; |
| justify-content: center; |
| |
| |
| |
| height: 360px; |
| flex: none; |
| gap: 0; |
| } |
| |
| |
| |
| .logo { |
| width: 72px; |
| height: 72px; |
| margin-bottom: 20px; |
| -webkit-user-drag: none; |
| } |
| |
| h1 { |
| font-size: 22px; |
| font-weight: 600; |
| letter-spacing: -0.3px; |
| margin-bottom: 8px; |
| color: var(--oh-foreground); |
| } |
| |
| .tagline { |
| font-size: 13px; |
| color: var(--oh-muted); |
| margin-bottom: 36px; |
| } |
| |
| |
| |
| .spinner { |
| width: 25px; |
| height: 25px; |
| color: #fff; |
| animation: spin 1s linear infinite; |
| margin-bottom: 14px; |
| } |
| @keyframes spin { |
| to { transform: rotate(360deg); } |
| } |
| |
| .status { |
| font-size: 12px; |
| color: var(--oh-text-dim); |
| letter-spacing: 0.3px; |
| |
| |
| max-width: 360px; |
| text-align: center; |
| white-space: nowrap; |
| overflow: hidden; |
| text-overflow: ellipsis; |
| } |
| |
| .hint { |
| margin-top: 14px; |
| font-size: 11px; |
| color: var(--oh-text-subtle); |
| letter-spacing: 0.2px; |
| max-width: 340px; |
| text-align: center; |
| line-height: 1.5; |
| } |
| |
| |
| .dots::after { |
| content: ""; |
| animation: dots 1.5s steps(3, end) infinite; |
| } |
| @keyframes dots { |
| 0% { content: ""; } |
| 33% { content: "."; } |
| 66% { content: ".."; } |
| 100% { content: "..."; } |
| } |
| |
| .actions { |
| margin-top: 16px; |
| display: flex; |
| gap: 10px; |
| -webkit-app-region: no-drag; |
| } |
| |
| .ghost-button { |
| font: inherit; |
| font-size: 11px; |
| padding: 4px 12px; |
| color: var(--oh-muted); |
| background: transparent; |
| border: 1px solid var(--oh-border); |
| border-radius: 8px; |
| cursor: pointer; |
| -webkit-app-region: no-drag; |
| } |
| .ghost-button:hover { |
| color: var(--oh-foreground); |
| background: rgba(255, 255, 255, 0.06); |
| } |
| |
| |
| |
| |
| |
| |
| .console { |
| flex: 1 1 auto; |
| min-height: 0; |
| display: flex; |
| flex-direction: column; |
| margin: 0 16px 16px; |
| background: var(--oh-surface-deep); |
| border: 1px solid var(--oh-border-subtle); |
| border-radius: 8px; |
| overflow: hidden; |
| -webkit-app-region: no-drag; |
| } |
| |
| .console-head { |
| flex: none; |
| display: flex; |
| align-items: center; |
| justify-content: space-between; |
| padding: 5px 6px 5px 12px; |
| border-bottom: 1px solid var(--oh-border-subtle); |
| font-size: 11px; |
| color: var(--oh-muted); |
| } |
| |
| .log { |
| flex: 1 1 auto; |
| min-height: 0; |
| overflow-y: auto; |
| padding: 8px 12px; |
| |
| font-family: |
| source-code-pro, Menlo, Monaco, Consolas, "Courier New", monospace; |
| font-size: 10.5px; |
| line-height: 1.55; |
| color: var(--oh-text-dim); |
| user-select: text; |
| -webkit-user-select: text; |
| cursor: text; |
| |
| white-space: pre-wrap; |
| overflow-wrap: anywhere; |
| } |
| .log:empty::before { |
| content: "Waiting for outputβ¦"; |
| color: var(--oh-text-subtle); |
| } |
| |
| .log-name { |
| color: var(--oh-text-subtle); |
| } |
| .log-line-error { |
| color: var(--oh-color-danger); |
| } |
| |
| |
| body.failed .spinner { |
| display: none; |
| } |
| body.failed .status { |
| color: var(--oh-color-danger); |
| |
| white-space: normal; |
| line-height: 1.45; |
| } |
| body.failed .hint { |
| display: none; |
| } |
| .quit-button { |
| display: none; |
| } |
| body.failed .quit-button { |
| display: inline-block; |
| } |
| </style> |
| </head> |
| <body> |
| <div class="container"> |
| <img class="logo" src="build-resources/icon.png" alt="" draggable="false" /> |
| <h1>OpenHands Agent Canvas</h1> |
| <p class="tagline">AI coding agent interface</p> |
| <svg class="spinner" viewBox="0 0 66 66" fill="none" aria-hidden="true" |
| xmlns="http://www.w3.org/2000/svg"> |
| <path d="M63 33C63 16.4315 49.5685 3 33 3C16.4315 3 3 16.4315 3 33C3 49.5685 16.4315 63 33 63" |
| stroke="currentColor" stroke-width="6" stroke-linecap="round" /> |
| </svg> |
| <p id="status" class="status">Starting services<span class="dots"></span></p> |
| <p class="hint"> |
| First launch downloads Python + the OpenHands agent server.<br /> |
| This can take a few minutes on a fresh machine. |
| </p> |
| <div class="actions"> |
| <button id="details-toggle" type="button" class="ghost-button"> |
| Show details |
| </button> |
| <button id="quit-button" type="button" class="ghost-button quit-button"> |
| Quit |
| </button> |
| </div> |
| </div> |
| <div class="console" aria-label="Startup log"> |
| <div class="console-head"> |
| <span>Startup log</span> |
| <button id="copy-logs" type="button" class="ghost-button">Copy</button> |
| </div> |
| <div id="log" class="log"></div> |
| </div> |
| <script> |
| |
| |
| |
| window.__setLoadingStatus = function (line) { |
| var el = document.getElementById("status"); |
| if (!el) return; |
| var text = String(line == null ? "" : line); |
| el.textContent = text || ""; |
| }; |
| |
| |
| |
| |
| |
| |
| |
| (function () { |
| var boot = window.desktopBoot; |
| var logEl = document.getElementById("log"); |
| var statusEl = document.getElementById("status"); |
| var toggleBtn = document.getElementById("details-toggle"); |
| var copyBtn = document.getElementById("copy-logs"); |
| var quitBtn = document.getElementById("quit-button"); |
| |
| |
| |
| var MAX_LINES = 2000; |
| var expanded = false; |
| |
| var pinned = true; |
| |
| logEl.addEventListener("scroll", function () { |
| pinned = |
| logEl.scrollTop + logEl.clientHeight >= logEl.scrollHeight - 8; |
| }); |
| |
| function appendBatch(batch) { |
| if (!batch || !batch.length) return; |
| var frag = document.createDocumentFragment(); |
| for (var i = 0; i < batch.length; i++) { |
| var entry = batch[i]; |
| var row = document.createElement("div"); |
| row.className = |
| "log-line" + (entry.level === "error" ? " log-line-error" : ""); |
| var name = document.createElement("span"); |
| name.className = "log-name"; |
| name.textContent = "[" + entry.name + "]"; |
| row.appendChild(name); |
| row.appendChild(document.createTextNode(" " + entry.line)); |
| frag.appendChild(row); |
| } |
| logEl.appendChild(frag); |
| while (logEl.childElementCount > MAX_LINES) { |
| logEl.removeChild(logEl.firstElementChild); |
| } |
| if (pinned) logEl.scrollTop = logEl.scrollHeight; |
| } |
| |
| function setExpanded(next) { |
| expanded = next; |
| toggleBtn.textContent = expanded ? "Hide details" : "Show details"; |
| if (boot) boot.setDetailsExpanded(expanded); |
| if (expanded && pinned) logEl.scrollTop = logEl.scrollHeight; |
| } |
| |
| toggleBtn.addEventListener("click", function () { |
| setExpanded(!expanded); |
| }); |
| |
| copyBtn.addEventListener("click", function () { |
| if (!boot) return; |
| boot.copyLogs().then(function () { |
| copyBtn.textContent = "Copied"; |
| setTimeout(function () { |
| copyBtn.textContent = "Copy"; |
| }, 1200); |
| }); |
| }); |
| |
| quitBtn.addEventListener("click", function () { |
| if (boot) boot.quit(); |
| }); |
| |
| if (boot) { |
| boot.onLogBatch(appendBatch); |
| boot.onFatal(function (summary) { |
| document.body.classList.add("failed"); |
| statusEl.textContent = String(summary == null ? "" : summary); |
| |
| if (!expanded) setExpanded(true); |
| }); |
| } |
| })(); |
| </script> |
| </body> |
| </html> |
|
|