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=<token> 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>