Commit a819741

mo khan <mo@mokhan.ca>
2026-09-12 21:59:05
style: apply rubocop autocorrect, extract INDEX html to its own file
1 parent 866c864
Changed files (4)
exe/elelem-server
@@ -11,7 +11,10 @@ OptionParser.new do |o|
   o.banner = "Usage: elelem-server [options]"
   o.on("-p", "--provider NAME", "Provider to use (default: #{provider})") { |v| provider = v }
   o.on("--port PORT", Integer, "Port to listen on (default: #{port})") { |v| port = v }
-  o.on("-h", "--help", "Show this help") { puts o; exit }
+  o.on("-h", "--help", "Show this help") do
+    puts o
+    exit
+  end
 end.parse!(ARGV)
 
 Elelem::Server.serve(provider: provider, port: port)
lib/elelem/server/index.html
@@ -0,0 +1,144 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
+<title>elelem</title>
+<style>
+  :root { color-scheme: dark; }
+  * { box-sizing: border-box; }
+  body {
+    margin: 0; height: 100dvh; display: flex; flex-direction: column;
+    background: #16161a; color: #e6e6e6;
+    font: 16px/1.55 ui-sans-serif, system-ui, -apple-system, sans-serif;
+  }
+  #log { flex: 1; overflow-y: auto; padding: 1rem; -webkit-overflow-scrolling: touch; }
+  .msg { max-width: 46rem; margin: 0 auto 1rem; padding: .65rem .85rem; border-radius: .6rem; }
+  .user { background: #2d3a4f; }
+  .assistant { background: #22222a; }
+  .thinking { opacity: .55; font-style: italic; white-space: pre-wrap; }
+  .msg pre {
+    background: #0d0d10; padding: .7rem; border-radius: .4rem;
+    overflow-x: auto; font-size: .875rem;
+  }
+  .msg code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
+  .prompt { max-width: 46rem; margin: 0 auto 1rem; padding: .85rem; border-radius: .6rem; background: #4a3a1e; }
+  .prompt button {
+    font-size: 1rem; padding: .7rem 1.4rem; margin: .5rem .5rem 0 0;
+    border: 0; border-radius: .5rem; color: #fff; touch-action: manipulation;
+  }
+  .allow { background: #2f7d4f; }
+  .deny { background: #8c3535; }
+  form { display: flex; gap: .5rem; padding: .75rem; padding-bottom: max(.75rem, env(safe-area-inset-bottom));
+         border-top: 1px solid #2c2c33; background: #1b1b20; }
+  textarea {
+    flex: 1; resize: none; font: inherit; padding: .6rem; border-radius: .5rem;
+    border: 1px solid #35353d; background: #101014; color: inherit;
+  }
+  button.send { font-size: 1rem; padding: .6rem 1.2rem; border: 0; border-radius: .5rem; background: #3b6ea5; color: #fff; }
+</style>
+</head>
+<body>
+<div id="log"></div>
+<form id="composer">
+  <textarea id="input" rows="1" placeholder="Message elelem…" autocapitalize="sentences"></textarea>
+  <button class="send" type="submit">Send</button>
+</form>
+<script>
+const log = document.getElementById("log");
+const input = document.getElementById("input");
+let current = null;
+
+const atBottom = () => log.scrollHeight - log.scrollTop - log.clientHeight < 80;
+const scroll = (was) => { if (was) log.scrollTop = log.scrollHeight; };
+
+function escape(text) {
+  return text.replace(/[&<>]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;" })[c]);
+}
+
+function render(text) {
+  return escape(text)
+    .replace(/```(\\w*)\\n([\\s\\S]*?)```/g, (_m, _lang, code) => "<pre><code>" + code + "</code></pre>")
+    .replace(/`([^`\\n]+)`/g, "<code>$1</code>")
+    .replace(/\\*\\*([^*]+)\\*\\*/g, "<strong>$1</strong>")
+    .replace(/\\n/g, "<br>");
+}
+
+function bubble(cls) {
+  const was = atBottom();
+  const el = document.createElement("div");
+  el.className = "msg " + cls;
+  log.appendChild(el);
+  scroll(was);
+  return el;
+}
+
+function append(cls, text) {
+  if (!current || current.dataset.cls !== cls) {
+    current = bubble(cls);
+    current.dataset.cls = cls;
+    current.dataset.raw = "";
+  }
+  const was = atBottom();
+  current.dataset.raw += text;
+  current.innerHTML = render(current.dataset.raw);
+  scroll(was);
+}
+
+function askPermission(id, text) {
+  current = null;
+  const was = atBottom();
+  const el = document.createElement("div");
+  el.className = "prompt";
+  el.dataset.id = id;
+  el.innerHTML = "<div>" + escape(text) + "</div>";
+  const respond = (answer) => {
+    el.querySelectorAll("button").forEach((b) => b.remove());
+    el.insertAdjacentHTML("beforeend", "<div><em>" + answer + "</em></div>");
+    fetch("/answer", { method: "POST", body: JSON.stringify({ id: el.dataset.id, text: answer }) });
+  };
+  const allow = document.createElement("button");
+  allow.className = "allow";
+  allow.textContent = "Allow";
+  allow.onclick = () => respond("y");
+  const deny = document.createElement("button");
+  deny.className = "deny";
+  deny.textContent = "Deny";
+  deny.onclick = () => respond("n");
+  el.append(allow, deny);
+  log.appendChild(el);
+  scroll(was);
+}
+
+new EventSource("/events").onmessage = (e) => {
+  const event = JSON.parse(e.data);
+  if (event.type === "content") append("assistant", event.text);
+  else if (event.type === "thinking") append("thinking", event.text);
+  else if (event.type === "prompt") askPermission(event.id, event.text);
+  else if (event.type === "done") current = null;
+};
+
+document.getElementById("composer").onsubmit = (e) => {
+  e.preventDefault();
+  const text = input.value.trim();
+  if (!text) return;
+  append("user", text);
+  current = null;
+  input.value = "";
+  fetch("/message", { method: "POST", body: JSON.stringify({ text: text }) });
+};
+
+input.addEventListener("keydown", (e) => {
+  if (e.key === "Enter" && !e.shiftKey) {
+    e.preventDefault();
+    document.getElementById("composer").requestSubmit();
+  }
+});
+
+input.addEventListener("input", () => {
+  input.style.height = "auto";
+  input.style.height = Math.min(input.scrollHeight, 160) + "px";
+});
+</script>
+</body>
+</html>
lib/elelem/server/web_terminal.rb
@@ -24,7 +24,10 @@ module Elelem
 
       def ask(prompt)
         queue = Queue.new
-        id = @mutex.synchronize { @prompts[@prompt_counter += 1] = queue; @prompt_counter }
+        id = @mutex.synchronize do
+          @prompts[@prompt_counter += 1] = queue
+          @prompt_counter
+        end
         emit("prompt", prompt, id: id)
         queue.pop
       end
@@ -42,8 +45,7 @@ module Elelem
         say "#{state} #{name}(#{args})"
       end
 
-      def waiting
-      end
+      def waiting; end
 
       def print(text)
         emit("content", text)
lib/elelem/server.rb
@@ -9,6 +9,8 @@ require_relative "server/web_terminal"
 
 module Elelem
   class Server
+    INDEX = File.read(File.expand_path("server/index.html", __dir__))
+
     def self.serve(provider: "stub", port: 4567, toolbox: Toolbox.new)
       llm_provider = Config.build_provider(provider)
       web_terminal = WebTerminal.new
@@ -71,7 +73,7 @@ module Elelem
       text = JSON.parse(req.body)["text"].to_s
       Thread.new do
         @session.turn(text)
-      rescue => e
+      rescue StandardError => e
         @terminal.say("Error: #{e.message}")
       ensure
         @terminal.done
@@ -84,152 +86,5 @@ module Elelem
       @terminal.answer(body["id"].to_i, body["text"].to_s)
       res.status = 204
     end
-
-    INDEX = <<~HTML
-      <!doctype html>
-      <html lang="en">
-      <head>
-      <meta charset="utf-8">
-      <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
-      <title>elelem</title>
-      <style>
-        :root { color-scheme: dark; }
-        * { box-sizing: border-box; }
-        body {
-          margin: 0; height: 100dvh; display: flex; flex-direction: column;
-          background: #16161a; color: #e6e6e6;
-          font: 16px/1.55 ui-sans-serif, system-ui, -apple-system, sans-serif;
-        }
-        #log { flex: 1; overflow-y: auto; padding: 1rem; -webkit-overflow-scrolling: touch; }
-        .msg { max-width: 46rem; margin: 0 auto 1rem; padding: .65rem .85rem; border-radius: .6rem; }
-        .user { background: #2d3a4f; }
-        .assistant { background: #22222a; }
-        .thinking { opacity: .55; font-style: italic; white-space: pre-wrap; }
-        .msg pre {
-          background: #0d0d10; padding: .7rem; border-radius: .4rem;
-          overflow-x: auto; font-size: .875rem;
-        }
-        .msg code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
-        .prompt { max-width: 46rem; margin: 0 auto 1rem; padding: .85rem; border-radius: .6rem; background: #4a3a1e; }
-        .prompt button {
-          font-size: 1rem; padding: .7rem 1.4rem; margin: .5rem .5rem 0 0;
-          border: 0; border-radius: .5rem; color: #fff; touch-action: manipulation;
-        }
-        .allow { background: #2f7d4f; }
-        .deny { background: #8c3535; }
-        form { display: flex; gap: .5rem; padding: .75rem; padding-bottom: max(.75rem, env(safe-area-inset-bottom));
-               border-top: 1px solid #2c2c33; background: #1b1b20; }
-        textarea {
-          flex: 1; resize: none; font: inherit; padding: .6rem; border-radius: .5rem;
-          border: 1px solid #35353d; background: #101014; color: inherit;
-        }
-        button.send { font-size: 1rem; padding: .6rem 1.2rem; border: 0; border-radius: .5rem; background: #3b6ea5; color: #fff; }
-      </style>
-      </head>
-      <body>
-      <div id="log"></div>
-      <form id="composer">
-        <textarea id="input" rows="1" placeholder="Message elelem…" autocapitalize="sentences"></textarea>
-        <button class="send" type="submit">Send</button>
-      </form>
-      <script>
-      const log = document.getElementById("log");
-      const input = document.getElementById("input");
-      let current = null;
-
-      const atBottom = () => log.scrollHeight - log.scrollTop - log.clientHeight < 80;
-      const scroll = (was) => { if (was) log.scrollTop = log.scrollHeight; };
-
-      function escape(text) {
-        return text.replace(/[&<>]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;" })[c]);
-      }
-
-      function render(text) {
-        return escape(text)
-          .replace(/```(\\w*)\\n([\\s\\S]*?)```/g, (_m, _lang, code) => "<pre><code>" + code + "</code></pre>")
-          .replace(/`([^`\\n]+)`/g, "<code>$1</code>")
-          .replace(/\\*\\*([^*]+)\\*\\*/g, "<strong>$1</strong>")
-          .replace(/\\n/g, "<br>");
-      }
-
-      function bubble(cls) {
-        const was = atBottom();
-        const el = document.createElement("div");
-        el.className = "msg " + cls;
-        log.appendChild(el);
-        scroll(was);
-        return el;
-      }
-
-      function append(cls, text) {
-        if (!current || current.dataset.cls !== cls) {
-          current = bubble(cls);
-          current.dataset.cls = cls;
-          current.dataset.raw = "";
-        }
-        const was = atBottom();
-        current.dataset.raw += text;
-        current.innerHTML = render(current.dataset.raw);
-        scroll(was);
-      }
-
-      function askPermission(id, text) {
-        current = null;
-        const was = atBottom();
-        const el = document.createElement("div");
-        el.className = "prompt";
-        el.dataset.id = id;
-        el.innerHTML = "<div>" + escape(text) + "</div>";
-        const respond = (answer) => {
-          el.querySelectorAll("button").forEach((b) => b.remove());
-          el.insertAdjacentHTML("beforeend", "<div><em>" + answer + "</em></div>");
-          fetch("/answer", { method: "POST", body: JSON.stringify({ id: el.dataset.id, text: answer }) });
-        };
-        const allow = document.createElement("button");
-        allow.className = "allow";
-        allow.textContent = "Allow";
-        allow.onclick = () => respond("y");
-        const deny = document.createElement("button");
-        deny.className = "deny";
-        deny.textContent = "Deny";
-        deny.onclick = () => respond("n");
-        el.append(allow, deny);
-        log.appendChild(el);
-        scroll(was);
-      }
-
-      new EventSource("/events").onmessage = (e) => {
-        const event = JSON.parse(e.data);
-        if (event.type === "content") append("assistant", event.text);
-        else if (event.type === "thinking") append("thinking", event.text);
-        else if (event.type === "prompt") askPermission(event.id, event.text);
-        else if (event.type === "done") current = null;
-      };
-
-      document.getElementById("composer").onsubmit = (e) => {
-        e.preventDefault();
-        const text = input.value.trim();
-        if (!text) return;
-        append("user", text);
-        current = null;
-        input.value = "";
-        fetch("/message", { method: "POST", body: JSON.stringify({ text: text }) });
-      };
-
-      input.addEventListener("keydown", (e) => {
-        if (e.key === "Enter" && !e.shiftKey) {
-          e.preventDefault();
-          document.getElementById("composer").requestSubmit();
-        }
-      });
-
-      input.addEventListener("input", () => {
-        input.style.height = "auto";
-        input.style.height = Math.min(input.scrollHeight, 160) + "px";
-      });
-      </script>
-      </body>
-      </html>
-    HTML
   end
 end