From a6e8bf7b2872299035f60789d86d700c74729049 Mon Sep 17 00:00:00 2001 From: Gabriel Brown Date: Thu, 13 Aug 2026 10:29:06 -0400 Subject: [PATCH] Redesign web UI with console-style squircle message cards Chat messages render as soft-shadow squircle cards (true superellipse corners where supported) with per-agent color avatars, a route pill, and a per-message Copy button (hover on desktop, always visible on touch). The 'to' field is a dropdown fed by the agents list, and the posting name is editable and persisted in localStorage. Setup tab matches the same look with a copyable install command. --- src/server.ts | 152 +++++++++++++++++++++++++++++++++++------------ test/api.test.ts | 2 + 2 files changed, 116 insertions(+), 38 deletions(-) diff --git a/src/server.ts b/src/server.ts index 0691036..8eb8d12 100644 --- a/src/server.ts +++ b/src/server.ts @@ -111,6 +111,8 @@ function escapeHtml(text: string): string { .replaceAll('"', """); } +// "Console" look (mock C): soft-shadow squircle cards on a cool ground, +// per-agent color identity, mono for machine data, teal accent. function page(active: "setup" | "chat", content: string, script = ""): string { const tab = (id: string, href: string, label: string) => `${label}`; @@ -121,28 +123,63 @@ function page(active: "setup" | "chat", content: string, script = ""): string { agentchat @@ -162,7 +199,7 @@ function renderSetup(base: string): string {

Install the skill on a machine

${escapeHtml(install)}
- +

That installs ~/.claude/skills/agentchat/SKILL.md. The agent's name defaults to the machine's hostname; set AGENTCHAT_NAME to override. @@ -171,9 +208,9 @@ See skill.md for the API the skill teaches.

How it works

One shared log with addressing: messages go to one agent (to) or everyone. Messages are archived out of view after 24 hours; nothing is deleted. -Post as user from the Chat tab.

`, - `document.getElementById("copy").addEventListener("click", async () => { - const btn = document.getElementById("copy"); +Post from the Chat tab under any name you like.

`, + `document.getElementById("copy-install").addEventListener("click", async () => { + const btn = document.getElementById("copy-install"); await navigator.clipboard.writeText(document.getElementById("install").textContent.trim()); btn.textContent = "Copied!"; setTimeout(() => { btn.textContent = "Copy"; }, 1500); @@ -186,25 +223,51 @@ function renderChat(): string { "chat", `
loading agents… - +
- - -
posts as user
+ + +
posts as
`, `const list = document.getElementById("messages"); let lastId = 0; +function hueOf(name) { + let h = 0; + for (const c of name) h = (h * 31 + c.charCodeAt(0)) % 360; + return h; +} +function myName() { + return (localStorage.getItem("agentchat-name") || "user").trim() || "user"; +} + function render(m) { const li = document.createElement("li"); - const meta = document.createElement("span"); - meta.className = "meta"; - meta.textContent = m.ts + " \\u00b7 " + m.from + " \\u2192 " + (m.to ?? "everyone"); + li.className = "msg"; + const av = document.createElement("div"); + av.className = "avatar"; + av.style.background = "oklch(0.62 0.13 " + hueOf(m.from) + ")"; + av.textContent = m.from[0].toUpperCase(); + const right = document.createElement("div"); + const head = document.createElement("div"); + head.className = "head"; + const from = document.createElement("span"); from.className = "from"; from.textContent = m.from; + const route = document.createElement("span"); route.className = "route"; route.textContent = "\\u2192 " + (m.to ?? "everyone"); + const time = document.createElement("span"); time.className = "time"; time.textContent = m.ts.slice(11, 16) + " UTC"; time.title = m.ts; + head.append(from, route, time); const p = document.createElement("p"); p.textContent = m.body; - li.append(meta, p); + right.append(head, p); + const copy = document.createElement("button"); + copy.className = "copy"; copy.type = "button"; copy.textContent = "Copy"; + copy.addEventListener("click", async () => { + await navigator.clipboard.writeText(m.body); + copy.textContent = "Copied"; copy.classList.add("done"); + setTimeout(() => { copy.textContent = "Copy"; copy.classList.remove("done"); }, 1200); + }); + li.append(av, right, copy); list.append(li); lastId = Math.max(lastId, m.id); } @@ -216,19 +279,32 @@ async function poll() { async function loadAgents() { const res = await fetch("/api/agents"); - const names = (await res.json()).map((a) => a.name).join(", "); - document.getElementById("agents").textContent = names ? "agents: " + names : "no agents yet"; + const names = (await res.json()).map((a) => a.name); + document.getElementById("agents").textContent = names.length ? "agents: " + names.join(", ") : "no agents yet"; + const sel = document.getElementById("to"); + const current = sel.value; + while (sel.options.length > 1) sel.remove(1); + for (const n of names.filter((n) => n !== myName())) { + const o = document.createElement("option"); + o.value = n; o.textContent = "to: " + n; + sel.append(o); + } + sel.value = current; + if (sel.selectedIndex < 0) sel.value = ""; } +const nameInput = document.getElementById("myname"); +nameInput.value = myName(); +nameInput.addEventListener("input", () => localStorage.setItem("agentchat-name", nameInput.value)); + document.getElementById("send").addEventListener("submit", async (e) => { e.preventDefault(); - const to = document.getElementById("to").value.trim(); const body = document.getElementById("body").value.trim(); if (!body) return; await fetch("/api/messages", { method: "POST", headers: { "content-type": "application/json" }, - body: JSON.stringify({ from: "user", to: to || undefined, body }), + body: JSON.stringify({ from: myName(), to: document.getElementById("to").value || undefined, body }), }); document.getElementById("body").value = ""; poll(); diff --git a/test/api.test.ts b/test/api.test.ts index 4ea064a..42c4416 100644 --- a/test/api.test.ts +++ b/test/api.test.ts @@ -114,6 +114,8 @@ describe("web pages", () => { const chat = await (await handle(new Request("http://chat.example.com/chat"))).text(); expect(chat).toContain('id="send"'); expect(chat).toContain('id="clear"'); + expect(chat).toContain('id="to"'); + expect(chat).toContain('id="myname"'); }); });