motif64

m64_a276407d7180 / human/index.html

246 lines, 12.1 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 · DeepSeek V4.1 Flash</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">DeepSeek V4.1 Flash via OpenRouter, in a sandbox that reaches the public internet (served with <code>--allow-external-network</code>: it needs openrouter.ai), never your LAN. API: <a href="../agent">/agent</a></p>5455  <a id="open" class="open" href="#">Open the full opencode interface<small id="open-url">finding the app port…</small></a>5657  <h2>Use it from your own terminal</h2>58  <p class="note">1. Install opencode 1.18.23 (either line):</p>59  <div class="cmd"><pre id="install-npm">npm i -g opencode-ai@1.18.23</pre><button type="button" data-copy="install-npm">copy</button></div>60  <div class="cmd"><pre id="install-curl">curl -fsSL https://opencode.ai/install | bash -s -- --version 1.18.23</pre><button type="button" data-copy="install-curl">copy</button></div>61  <p class="note">2. Attach to this capsule from any machine on your tailnet:</p>62  <div class="cmd"><pre id="attach">opencode attach …</pre><button type="button" data-copy="attach">copy</button></div>63  <p class="note">Files, commands and the model all run in this capsule's container; the OpenRouter key stays in the m64 gate. (If the capsule was served with a token, run it as <code>OPENCODE_SERVER_PASSWORD=&lt;token&gt; opencode attach …</code>.)</p>6465  <details class="quick">66  <summary>Quick test (simple chat)</summary>67  <p class="note">Each session gets an empty folder.</p>68  <div class="bar">Session: <code id="sid">none yet</code> <button id="new" type="button">New session</button></div>69  <div id="log"></div>70  <form id="f">71    <textarea id="text" rows="2" required>write hello.py that prints hi and run it</textarea>72    <button id="go" class="primary" type="submit">Send</button>73    <button id="stop" type="button" disabled>Stop</button>74  </form>75  <div id="status"></div>76  <h2>Files <button id="refresh" type="button" style="font-size:12px;padding:2px 8px">refresh</button></h2>77  <ul id="files"><li><span class="size">no session yet</span></li></ul>78  <section id="viewer" hidden>79    <h2><span id="viewer-name"></span> <button id="viewer-close" type="button" style="font-size:12px;padding:2px 8px">close</button></h2>80    <pre id="viewer-text"></pre>81  </section>82  </details>83</main>84<script>85const $ = (id) => document.getElementById(id);86let sid = null;87let timer = null;8889function say(text, err) { $("status").textContent = text; $("status").className = err ? "err" : ""; }90function el(tag, cls, text) { const e = document.createElement(tag); if (cls) e.className = cls; if (text != null) e.textContent = text; return e; }91function errText(d, r) {92  if (typeof d.error === "string") return d.error;93  if (d.error) return JSON.stringify(d.error);94  if (d.detail) return typeof d.detail === "string" ? d.detail : JSON.stringify(d.detail);95  return "HTTP " + r.status;96}9798async function waitReady() {99  for (;;) {100    let s;101    try { s = await (await fetch("../status")).json(); } catch (e) { s = { phase: "unreachable" }; }102    if (s.phase === "ready") { say(""); return true; }103    if (s.phase === "error" || s.phase === "stopped") { say("Service " + s.phase + ": " + (s.detail || ""), true); return false; }104    say("Starting: " + s.phase + (s.detail ? " (" + s.detail + ")" : ""));105    await new Promise((r) => setTimeout(r, 3000));106  }107}108109async function newSession() {110  if (!(await waitReady())) return false;111  const r = await fetch("../v1/sessions", { method: "POST", headers: { "Content-Type": "application/json" }, body: "{}" });112  const d = await r.json().catch(() => ({}));113  if (!r.ok) { say("Could not create a session: " + errText(d, r), true); return false; }114  sid = d.id;115  $("sid").textContent = sid;116  $("log").replaceChildren();117  await listFiles();118  return true;119}120121async function listFiles() {122  const ul = $("files");123  if (!sid) return;124  const r = await fetch("../v1/sessions/" + sid + "/files");125  const d = await r.json().catch(() => ({}));126  ul.replaceChildren();127  if (!r.ok) { ul.append(el("li", "size", errText(d, r))); return; }128  if (!d.entries.length) { ul.append(el("li", "size", "(empty)")); return; }129  for (const e of d.entries) {130    const li = el("li");131    if (e.type === "file") {132      // Agent-written bytes: offer a download, or show them as text in this page (never rendered).133      const url = "../v1/sessions/" + sid + "/files?path=" + encodeURIComponent(e.name);134      const name = el("span", null, e.name + " ");135      const view = el("button", "link", "view");136      view.type = "button";137      view.addEventListener("click", () => viewText(e.name, url));138      const a = el("a", null, "download");139      a.href = url;140      a.download = e.name;141      const left = el("span");142      left.append(name, view, " · ", a);143      li.append(left, el("span", "size", e.size + " B"));144    } else if (e.type === "dir") {145      li.append(el("span", null, e.name + "/"), el("span", "size", "dir"));146    } else {147      li.append(el("span", null, e.name), el("span", "size", e.type));148    }149    ul.append(li);150  }151}152153async function viewText(name, url) {154  const r = await fetch(url);155  const buf = await r.arrayBuffer();156  const text = r.ok ? new TextDecoder().decode(buf.slice(0, 200000)) : "HTTP " + r.status;157  $("viewer-name").textContent = name + (buf.byteLength > 200000 ? " (first 200 KB)" : "");158  $("viewer-text").textContent = text;159  $("viewer").hidden = false;160}161162function showReply(d) {163  const box = el("div", "msg agent", d.text || "(no text)");164  if (d.error) box.append(el("div", "meta", "error: " + JSON.stringify(d.error)));165  for (const t of d.tools || []) {166    const det = el("details");167    det.append(el("summary", null, t.tool + " · " + t.status));168    det.append(el("pre", null, JSON.stringify(t.input, null, 2)));169    if (t.output) det.append(el("pre", null, t.output));170    box.append(det);171  }172  box.append(el("div", "meta", (d.steps || 0) + " steps · " + d.elapsed_s + " s"));173  $("log").append(box);174}175176$("f").addEventListener("submit", async (ev) => {177  ev.preventDefault();178  const text = $("text").value.trim();179  if (!text) return;180  $("go").disabled = true;181  try {182    if (!sid && !(await newSession())) return;183    $("log").append(el("div", "msg user", text));184    $("text").value = "";185    $("stop").disabled = false;186    const t0 = performance.now();187    timer = setInterval(() => say("Agent working… " + ((performance.now() - t0) / 1000).toFixed(0) + " s"), 500);188    const r = await fetch("../v1/sessions/" + sid + "/messages", {189      method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ text }),190    });191    const d = await r.json().catch(() => ({}));192    clearInterval(timer);193    if (!r.ok) { $("log").append(el("div", "msg err", errText(d, r))); say(""); return; }194    showReply(d);195    say("");196    await listFiles();197  } catch (e) {198    clearInterval(timer);199    say("Request failed: " + e, true);200  } finally {201    $("go").disabled = false;202    $("stop").disabled = true;203  }204});205206$("stop").addEventListener("click", async () => {207  if (sid) await fetch("../v1/sessions/" + sid + "/abort", { method: "POST" });208});209$("new").addEventListener("click", () => newSession());210$("refresh").addEventListener("click", () => listFiles());211$("viewer-close").addEventListener("click", () => { $("viewer").hidden = true; });212213// The app port's URL: its port from /status (app_url), on the host this page was opened214// with (an IP or a MagicDNS name alike); without app_url, this port + 1.215async function appUrl() {216  let port = null;217  try {218    const s = await (await fetch("../status")).json();219    if (s.app_url) port = new URL(s.app_url).port;220  } catch (e) { /* fall back below */ }221  if (!port) port = String(Number(location.port || (location.protocol === "https:" ? 443 : 80)) + 1);222  return location.protocol + "//" + location.hostname.replace(/^([^\[].*:.*)$/, "[$1]") + ":" + port + "/";223}224appUrl().then((url) => {225  $("open").href = url;226  $("open-url").textContent = url;227  $("attach").textContent = "opencode attach " + url.replace(/\/$/, "");228});229for (const b of document.querySelectorAll("[data-copy]")) {230  b.addEventListener("click", async () => {231    const text = $(b.dataset.copy).textContent;232    try { await navigator.clipboard.writeText(text); }233    catch (e) {  // plain http is not a secure context: select and copy the old way234      const range = document.createRange();235      range.selectNodeContents($(b.dataset.copy));236      getSelection().removeAllRanges();237      getSelection().addRange(range);238      document.execCommand("copy");239    }240    b.textContent = "copied";241    setTimeout(() => { b.textContent = "copy"; }, 1500);242  });243}244waitReady();245</script>246</html>