motif64

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>