m64_fd6b2982ea67 / human/index.html
238 lines, 11.4 KBraw
1<!doctype html>2<html lang="en">3<meta charset="utf-8">4<meta name="viewport" content="width=device-width, initial-scale=1">5<title>opencode · Qwen3.8-Flash-Next on Basalt</title>6<style>7 :root { color-scheme: light dark; --fg: #1d1d1f; --bg: #f6f5f2; --card: #fff; --muted: #6b6b70; --line: #d8d6d0; --accent: #0f766e; --code: #efede8; }8 @media (prefers-color-scheme: dark) { :root { --fg: #ecebe8; --bg: #161616; --card: #1f1f1f; --muted: #9a9a9f; --line: #333; --accent: #2dd4bf; --code: #262626; } }9 * { box-sizing: border-box; }10 body { margin: 0; padding: 24px 16px; font: 15px/1.45 system-ui, sans-serif; color: var(--fg); background: var(--bg); }11 main { max-width: 820px; margin: 0 auto; }12 h1 { font-size: 20px; margin: 0 0 4px; }13 .sub { color: var(--muted); margin: 0 0 16px; font-size: 13px; }14 a { color: var(--accent); }15 .bar { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; margin-bottom: 12px; font-size: 13px; color: var(--muted); }16 .bar code { color: var(--fg); }17 #log { display: flex; flex-direction: column; gap: 10px; margin-bottom: 12px; }18 .msg { background: var(--card); border: 1px solid var(--line); border-radius: 8px; padding: 10px 12px; white-space: pre-wrap; overflow-wrap: anywhere; }19 .msg.user { border-left: 3px solid var(--muted); }20 .msg.agent { border-left: 3px solid var(--accent); }21 .msg.err { border-left: 3px solid #dc2626; color: #dc2626; }22 .meta { color: var(--muted); font-size: 12px; margin-top: 6px; white-space: normal; }23 details { margin-top: 6px; font-size: 13px; white-space: normal; }24 summary { cursor: pointer; color: var(--muted); }25 pre { background: var(--code); border-radius: 6px; padding: 8px; overflow-x: auto; font-size: 12px; margin: 4px 0; white-space: pre-wrap; }26 form { display: grid; grid-template-columns: 1fr auto auto; gap: 8px; }27 @media (max-width: 560px) { form { grid-template-columns: 1fr 1fr; } textarea { grid-column: 1 / -1; } }28 textarea, button { font: inherit; color: inherit; background: transparent; border: 1px solid var(--line); border-radius: 6px; padding: 8px 10px; }29 textarea { resize: vertical; min-height: 44px; }30 button { cursor: pointer; }31 button.primary { background: var(--accent); border-color: var(--accent); color: #fff; font-weight: 600; }32 @media (prefers-color-scheme: dark) { button.primary { color: #0b1f1d; } }33 button:disabled { opacity: .5; cursor: progress; }34 #status { margin: 10px 0; color: var(--muted); font-size: 13px; min-height: 1.4em; }35 #status.err { color: #dc2626; }36 h2 { font-size: 15px; margin: 20px 0 6px; }37 #files { list-style: none; padding: 0; margin: 0; font-size: 14px; }38 #files li { padding: 3px 0; border-bottom: 1px solid var(--line); display: flex; justify-content: space-between; gap: 8px; }39 #files .size { color: var(--muted); font-size: 12px; }40 button.link { border: 0; padding: 0; color: var(--accent); text-decoration: underline; font-size: inherit; }41 [hidden] { display: none !important; }42 .open { display: block; margin: 8px 0 20px; padding: 16px 18px; border-radius: 10px; background: var(--accent); color: #fff; font-size: 18px; font-weight: 600; text-decoration: none; text-align: center; }43 @media (prefers-color-scheme: dark) { .open { color: #0b1f1d; } }44 .open small { display: block; font-weight: 400; font-size: 12px; opacity: .85; margin-top: 2px; overflow-wrap: anywhere; }45 .cmd { display: flex; gap: 8px; align-items: stretch; margin: 4px 0 10px; }46 .cmd pre { flex: 1; margin: 0; font-size: 13px; }47 .note { color: var(--muted); font-size: 13px; margin: 6px 0; }48 details.quick { margin-top: 24px; border-top: 1px solid var(--line); padding-top: 8px; }49 details.quick > summary { font-weight: 600; color: var(--fg); }50</style>51<main>52 <h1>opencode coding agent</h1>53 <p class="sub">Qwen3.8-Flash-Next on Basalt, on this machine's RTX 5090, in a sandbox with no network access unless you served it with <code>--allow-external-network</code>. API: <a href="../agent">/agent</a></p>5455 <a id="open" class="open" href="#">Open opencode<small id="open-url">finding the app port…</small></a>56 <p class="note">opencode's own terminal interface, in your browser, on the folder <code>home</code>. Files, commands and the model all run in this capsule's container; nothing leaves this machine.</p>5758 <details class="quick">59 <summary>Quick test (simple chat)</summary>60 <p class="note">Each session gets an empty folder.</p>61 <div class="bar">Session: <code id="sid">none yet</code> <button id="new" type="button">New session</button></div>62 <div id="log"></div>63 <form id="f">64 <textarea id="text" rows="2" required>write hello.py that prints hi and run it</textarea>65 <button id="go" class="primary" type="submit">Send</button>66 <button id="stop" type="button" disabled>Stop</button>67 </form>68 <div id="status"></div>69 <h2>Files <button id="refresh" type="button" style="font-size:12px;padding:2px 8px">refresh</button></h2>70 <ul id="files"><li><span class="size">no session yet</span></li></ul>71 <section id="viewer" hidden>72 <h2><span id="viewer-name"></span> <button id="viewer-close" type="button" style="font-size:12px;padding:2px 8px">close</button></h2>73 <pre id="viewer-text"></pre>74 </section>75 </details>76</main>77<script>78const $ = (id) => document.getElementById(id);79let sid = null;80let timer = null;8182function say(text, err) { $("status").textContent = text; $("status").className = err ? "err" : ""; }83function el(tag, cls, text) { const e = document.createElement(tag); if (cls) e.className = cls; if (text != null) e.textContent = text; return e; }84function errText(d, r) {85 if (typeof d.error === "string") return d.error;86 if (d.error) return JSON.stringify(d.error);87 if (d.detail) return typeof d.detail === "string" ? d.detail : JSON.stringify(d.detail);88 return "HTTP " + r.status;89}9091async function waitReady() {92 for (;;) {93 let s;94 try { s = await (await fetch("../status")).json(); } catch (e) { s = { phase: "unreachable" }; }95 if (s.phase === "ready") { say(""); return true; }96 if (s.phase === "error" || s.phase === "stopped") { say("Service " + s.phase + ": " + (s.detail || ""), true); return false; }97 say("Starting: " + s.phase + (s.detail ? " (" + s.detail + ")" : ""));98 await new Promise((r) => setTimeout(r, 3000));99 }100}101102async function newSession() {103 if (!(await waitReady())) return false;104 const r = await fetch("../v1/sessions", { method: "POST", headers: { "Content-Type": "application/json" }, body: "{}" });105 const d = await r.json().catch(() => ({}));106 if (!r.ok) { say("Could not create a session: " + errText(d, r), true); return false; }107 sid = d.id;108 $("sid").textContent = sid;109 $("log").replaceChildren();110 await listFiles();111 return true;112}113114async function listFiles() {115 const ul = $("files");116 if (!sid) return;117 const r = await fetch("../v1/sessions/" + sid + "/files");118 const d = await r.json().catch(() => ({}));119 ul.replaceChildren();120 if (!r.ok) { ul.append(el("li", "size", errText(d, r))); return; }121 if (!d.entries.length) { ul.append(el("li", "size", "(empty)")); return; }122 for (const e of d.entries) {123 const li = el("li");124 if (e.type === "file") {125 // Agent-written bytes: offer a download, or show them as text in this page (never rendered).126 const url = "../v1/sessions/" + sid + "/files?path=" + encodeURIComponent(e.name);127 const name = el("span", null, e.name + " ");128 const view = el("button", "link", "view");129 view.type = "button";130 view.addEventListener("click", () => viewText(e.name, url));131 const a = el("a", null, "download");132 a.href = url;133 a.download = e.name;134 const left = el("span");135 left.append(name, view, " · ", a);136 li.append(left, el("span", "size", e.size + " B"));137 } else if (e.type === "dir") {138 li.append(el("span", null, e.name + "/"), el("span", "size", "dir"));139 } else {140 li.append(el("span", null, e.name), el("span", "size", e.type));141 }142 ul.append(li);143 }144}145146async function viewText(name, url) {147 const r = await fetch(url);148 const buf = await r.arrayBuffer();149 const text = r.ok ? new TextDecoder().decode(buf.slice(0, 200000)) : "HTTP " + r.status;150 $("viewer-name").textContent = name + (buf.byteLength > 200000 ? " (first 200 KB)" : "");151 $("viewer-text").textContent = text;152 $("viewer").hidden = false;153}154155function showReply(d) {156 const box = el("div", "msg agent", d.text || "(no text)");157 if (d.error) box.append(el("div", "meta", "error: " + JSON.stringify(d.error)));158 for (const t of d.tools || []) {159 const det = el("details");160 det.append(el("summary", null, t.tool + " · " + t.status));161 det.append(el("pre", null, JSON.stringify(t.input, null, 2)));162 if (t.output) det.append(el("pre", null, t.output));163 box.append(det);164 }165 box.append(el("div", "meta", (d.steps || 0) + " steps · " + d.elapsed_s + " s"));166 $("log").append(box);167}168169$("f").addEventListener("submit", async (ev) => {170 ev.preventDefault();171 const text = $("text").value.trim();172 if (!text) return;173 $("go").disabled = true;174 try {175 if (!sid && !(await newSession())) return;176 $("log").append(el("div", "msg user", text));177 $("text").value = "";178 $("stop").disabled = false;179 const t0 = performance.now();180 timer = setInterval(() => say("Agent working… " + ((performance.now() - t0) / 1000).toFixed(0) + " s"), 500);181 const r = await fetch("../v1/sessions/" + sid + "/messages", {182 method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ text }),183 });184 const d = await r.json().catch(() => ({}));185 clearInterval(timer);186 if (!r.ok) { $("log").append(el("div", "msg err", errText(d, r))); say(""); return; }187 showReply(d);188 say("");189 await listFiles();190 } catch (e) {191 clearInterval(timer);192 say("Request failed: " + e, true);193 } finally {194 $("go").disabled = false;195 $("stop").disabled = true;196 }197});198199$("stop").addEventListener("click", async () => {200 if (sid) await fetch("../v1/sessions/" + sid + "/abort", { method: "POST" });201});202$("new").addEventListener("click", () => newSession());203$("refresh").addEventListener("click", () => listFiles());204$("viewer-close").addEventListener("click", () => { $("viewer").hidden = true; });205206// The app port's URL: its port from /status (app_url), on the host this page was opened207// with (an IP or a MagicDNS name alike); without app_url, this port + 1.208async function appUrl() {209 let port = null;210 try {211 const s = await (await fetch("../status")).json();212 if (s.app_url) port = new URL(s.app_url).port;213 } catch (e) { /* fall back below */ }214 if (!port) port = String(Number(location.port || (location.protocol === "https:" ? 443 : 80)) + 1);215 return location.protocol + "//" + location.hostname.replace(/^([^\[].*:.*)$/, "[$1]") + ":" + port + "/";216}217appUrl().then((url) => {218 $("open").href = url;219 $("open-url").textContent = url;220});221for (const b of document.querySelectorAll("[data-copy]")) {222 b.addEventListener("click", async () => {223 const text = $(b.dataset.copy).textContent;224 try { await navigator.clipboard.writeText(text); }225 catch (e) { // plain http is not a secure context: select and copy the old way226 const range = document.createRange();227 range.selectNodeContents($(b.dataset.copy));228 getSelection().removeAllRanges();229 getSelection().addRange(range);230 document.execCommand("copy");231 }232 b.textContent = "copied";233 setTimeout(() => { b.textContent = "copy"; }, 1500);234 });235}236waitReady();237</script>238</html>