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)}
-
Copy
+
Copy
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…
- Clear chat
+ Clear chat
`,
`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"');
});
});