Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

Select the types of activity you want to include in your feed.

help: new public piece — rate-limited Q&A about aesthetic.computer

Fork of aa.mjs pointed at /api/help/chat on the bridge. Gated on
having a handle rather than being the admin sub, so anyone signed up
can ask questions. Stateless — drops session resume/history/reset
since each server turn is isolated.

HUD and presence line show usage left: label goes
"help <hourLeft>·<dayLeft>" (e.g. "help 19·49") after the first reply
populates the X-Help-Remaining-* headers; presence line shows
"@name · 19 left this hour". Extra states: help !@ (handle missing),
help ? (not logged in), help ⛔ (rate-limited), help … (bridge busy),
help ! (bridge down).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

+327
+327
system/public/aesthetic.computer/disks/help.mjs
··· 1 + // help, 26.04.21 2 + // Public, sandboxed chatbot about aesthetic.computer. Rate-limited, stateless, 3 + // backed by the same macbook bridge as aa but through /api/help/chat with a 4 + // locked-down claude (haiku, read-only tools, path deny-list on secrets). 5 + // Requires an Auth0 login AND a handle — small barrier to keep costs bounded. 6 + 7 + import { Chat } from "../lib/chat.mjs"; 8 + import * as chat from "./chat.mjs"; 9 + 10 + const ENDPOINT = "https://help.aesthetic.computer"; 11 + 12 + let client; 13 + let token = null; 14 + let handleOk = false; // token + handle set → eligible to talk 15 + let pending = false; 16 + let abortCtrl = null; 17 + let userHandleRef = null; 18 + let hudRef = null; 19 + let ratesLeft = null; // { hour, day } from last response, null until known 20 + let currentState = "loading"; 21 + 22 + let msgCounter = 0; 23 + const nextId = () => `help-${Date.now()}-${msgCounter++}`; 24 + 25 + // HUD status — mirrors eligibility + most recent rate remaining. 26 + // Server re-validates on every request; this is a UX cue only. 27 + function setStatus(state) { 28 + currentState = state; 29 + if (!hudRef) return; 30 + const base = { 31 + loading: ["help", undefined], 32 + ok: ["help", "lime"], // logged in + handle, ready 33 + busy: ["help …", "orange"], // in-flight or over capacity 34 + unauth: ["help ?", "yellow"], // 401 / no token 35 + nohandle: ["help !@", "yellow"], // logged in but handle unset 36 + ratelimit: ["help ⛔", "orange"], // 429 37 + down: ["help !", "yellow"], // bridge unreachable / odd status 38 + }; 39 + let [label, color] = base[state] || base.loading; 40 + if (state === "ok" && ratesLeft) { 41 + label = `help ${ratesLeft.hour}·${ratesLeft.day}`; 42 + } 43 + hudRef.label(label, color); 44 + } 45 + 46 + // Cool slate theme — distinct from chat's defaults so AA reads as a separate space. 47 + const THEME = { 48 + background: [12, 14, 22], 49 + chromeBg: [20, 22, 32], 50 + lines: [80, 90, 130, 96], 51 + scrollbar: [120, 180, 240], 52 + messageText: [232, 232, 240], 53 + messageBox: [220, 220, 235], 54 + log: [150, 200, 255], 55 + logHover: [255, 240, 120], 56 + handle: [120, 200, 255], 57 + handleHover: [255, 240, 120], 58 + url: [180, 220, 255], 59 + urlHover: [255, 240, 120], 60 + prompt: [200, 230, 255], 61 + promptContent: [180, 220, 255], 62 + promptHover: [255, 240, 120], 63 + promptContentHover:[255, 240, 120], 64 + painting: [200, 200, 230], 65 + paintingHover: [255, 240, 120], 66 + kidlisp: [230, 180, 220], 67 + kidlispHover: [255, 240, 120], 68 + timestamp: [110, 120, 150], 69 + timestampHover: [255, 240, 120], 70 + heart: [220, 200, 240], 71 + }; 72 + 73 + async function boot({ api, debug, send, hud, handle, user, authorize }) { 74 + client = new Chat(debug, send); 75 + // We feed messages in from the bridge — never connect to a chat server. 76 + // chat.mjs gates paint on `connecting`, so flip it false up front. 77 + client.system.connecting = false; 78 + 79 + hudRef = hud; 80 + setStatus("loading"); 81 + userHandleRef = handle; 82 + 83 + if (!user?.sub) { 84 + setStatus("unauth"); 85 + pushSystem("log in to ask help anything."); 86 + } else { 87 + try { 88 + token = await authorize(); 89 + if (!token) { 90 + setStatus("unauth"); 91 + pushSystem("no auth0 token — refresh and retry."); 92 + } else if (!handle?.()) { 93 + // Token is good but no handle set. The server will reject with 403 94 + // anyway, but we catch it up-front for a clearer nudge. 95 + setStatus("nohandle"); 96 + pushSystem("set a handle first — type `handle @yourname`."); 97 + } else { 98 + handleOk = true; 99 + setStatus("ok"); 100 + pushSystem("ask about pieces, commands, kidlisp, or the codebase."); 101 + } 102 + } catch (err) { 103 + setStatus("down"); 104 + pushSystem(`bridge unreachable: ${err.message}`); 105 + } 106 + } 107 + 108 + await chat.boot(api, client.system, { 109 + submitHandler: async (text) => { 110 + if (!handleOk || !token) { pushSystem("not eligible to ask yet."); return; } 111 + if (text === "/clear") { 112 + client.system.messages.length = 0; 113 + invalidate(); 114 + return; 115 + } 116 + if (text === "/cancel" || text === "/stop") { 117 + if (abortCtrl) abortCtrl.abort(); 118 + return; 119 + } 120 + await sendToBridge(text); 121 + }, 122 + }); 123 + } 124 + 125 + function paint($) { 126 + chat.paint($, { 127 + otherChat: client.system, 128 + theme: THEME, 129 + // help has no real chat presence — kill the news ticker + KPBJ radio. 130 + hideChrome: true, 131 + presenceOverride: currentPresenceLine(), 132 + }); 133 + } 134 + 135 + function currentPresenceLine() { 136 + const raw = userHandleRef?.(); 137 + const h = raw ? (raw.startsWith("@") ? raw : `@${raw}`) : null; 138 + if (handleOk && h) { 139 + if (ratesLeft) return `${h} · ${ratesLeft.hour} left this hour`; 140 + return `${h} · help is open`; 141 + } 142 + if (currentState === "nohandle") return "set a handle to ask"; 143 + if (!token) return "log in to ask"; 144 + return "…"; 145 + } 146 + 147 + function act($) { 148 + chat.act($, client.system); 149 + } 150 + 151 + function sim($) { 152 + chat.sim($); 153 + } 154 + 155 + function leave() { 156 + if (abortCtrl) abortCtrl.abort(); 157 + } 158 + 159 + function meta() { 160 + return { 161 + title: "help", 162 + desc: "Public, rate-limited chatbot that answers questions about aesthetic.computer.", 163 + }; 164 + } 165 + 166 + // ───────── message plumbing ───────── 167 + 168 + function pushMessage(from, text, { sound = false } = {}) { 169 + const msg = { from, text, id: nextId(), sub: from }; 170 + client.system.messages.push(msg); 171 + if (client.system.messages.length > 500) client.system.messages.shift(); 172 + client.system.receiver?.( 173 + msg.id, 174 + sound ? "message" : "layout-only", 175 + sound ? msg : null, 176 + { layoutChanged: true }, 177 + ); 178 + return msg; 179 + } 180 + 181 + function pushSystem(text) { 182 + return pushMessage("help", text); 183 + } 184 + 185 + function removeMessage(msg) { 186 + if (!msg) return; 187 + const idx = client.system.messages.indexOf(msg); 188 + if (idx !== -1) client.system.messages.splice(idx, 1); 189 + client.system.receiver?.(nextId(), "layout-only", null, { layoutChanged: true }); 190 + } 191 + 192 + function invalidate() { 193 + client.system.receiver?.(nextId(), "layout-only", null, { layoutChanged: true }); 194 + } 195 + 196 + async function sendToBridge(text) { 197 + if (pending) { pushSystem("one at a time — /cancel to stop."); return; } 198 + 199 + const raw = userHandleRef?.(); 200 + const me = raw ? (raw.startsWith("@") ? raw : `@${raw}`) : "anon"; 201 + pushMessage(me, text); 202 + pending = true; 203 + abortCtrl = new AbortController(); 204 + 205 + // Streamed reply: the help bubble is created lazily on the first text chunk, 206 + // then mutated in place so the reply appears progressively. 207 + let streamMsg = null; 208 + let pendingText = ""; 209 + const applyStream = () => { 210 + if (!streamMsg) streamMsg = pushMessage("help", pendingText); 211 + else { 212 + streamMsg.text = pendingText; 213 + invalidate(); 214 + } 215 + }; 216 + 217 + try { 218 + const res = await fetch(`${ENDPOINT}/api/help/chat`, { 219 + method: "POST", 220 + headers: { 221 + "Content-Type": "application/json", 222 + Authorization: `Bearer ${token}`, 223 + }, 224 + body: JSON.stringify({ message: text }), 225 + signal: abortCtrl.signal, 226 + }); 227 + 228 + // Pull remaining-quota hints out of response headers (set on all success 229 + // and some error paths) so the HUD can show "help 18·47" etc. 230 + const hH = res.headers.get("X-Help-Remaining-Hour"); 231 + const hD = res.headers.get("X-Help-Remaining-Day"); 232 + if (hH !== null && hD !== null) { 233 + ratesLeft = { hour: parseInt(hH, 10), day: parseInt(hD, 10) }; 234 + if (handleOk) setStatus("ok"); 235 + } 236 + 237 + if (!res.ok) { 238 + const body = await res.text().catch(() => `${res.status}`); 239 + if (res.status === 401) { 240 + handleOk = false; 241 + token = null; 242 + setStatus("unauth"); 243 + } else if (res.status === 403) { 244 + handleOk = false; 245 + setStatus("nohandle"); 246 + } else if (res.status === 429) { 247 + setStatus("ratelimit"); 248 + } else if (res.status === 503) { 249 + setStatus("busy"); 250 + } else { 251 + setStatus("down"); 252 + } 253 + pushSystem(`! ${res.status}: ${body.slice(0, 200)}`); 254 + return; 255 + } 256 + 257 + const reader = res.body.getReader(); 258 + const decoder = new TextDecoder(); 259 + let buf = ""; 260 + while (true) { 261 + const { done, value } = await reader.read(); 262 + if (done) break; 263 + buf += decoder.decode(value, { stream: true }); 264 + let idx; 265 + while ((idx = buf.indexOf("\n\n")) !== -1) { 266 + const block = buf.slice(0, idx); 267 + buf = buf.slice(idx + 2); 268 + const evt = parseSSE(block); 269 + if (!evt) continue; 270 + 271 + if (evt.event === "claude") { 272 + const ev = evt.data; 273 + if (ev.type === "assistant" && ev.message?.content) { 274 + for (const b of ev.message.content) { 275 + if (b.type === "text" && b.text) { 276 + pendingText += b.text; 277 + applyStream(); 278 + } 279 + // tool_use / thinking / other blocks deliberately suppressed 280 + } 281 + } else if (ev.type === "result" && ev.result) { 282 + pendingText = ev.result; 283 + applyStream(); 284 + } 285 + // tool_result (in user events) also suppressed 286 + } else if (evt.event === "error") { 287 + pushMessage("help", `! ${evt.data.message || "error"}`); 288 + } else if (evt.event === "done") { 289 + if (streamMsg && pendingText) { 290 + streamMsg.text = pendingText; 291 + client.system.receiver?.( 292 + streamMsg.id, 293 + "message", 294 + streamMsg, 295 + { layoutChanged: true }, 296 + ); 297 + } else if (streamMsg && !pendingText) { 298 + removeMessage(streamMsg); 299 + } 300 + } 301 + // stderr events ignored — bridge warnings are not user-facing 302 + } 303 + } 304 + } catch (err) { 305 + if (streamMsg && !pendingText) removeMessage(streamMsg); 306 + if (err.name === "AbortError") pushSystem("cancelled."); 307 + else pushSystem(`error: ${err.message}`); 308 + } finally { 309 + pending = false; 310 + abortCtrl = null; 311 + } 312 + } 313 + 314 + // ───────── small helpers ───────── 315 + 316 + function parseSSE(block) { 317 + let event = "message"; 318 + let data = ""; 319 + for (const line of block.split("\n")) { 320 + if (line.startsWith("event:")) event = line.slice(6).trim(); 321 + else if (line.startsWith("data:")) data += line.slice(5).trim(); 322 + } 323 + if (!data) return null; 324 + try { return { event, data: JSON.parse(data) }; } catch { return null; } 325 + } 326 + 327 + export { boot, paint, act, sim, leave, meta };