Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

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

aa: basic chat feel + fix TextInput not deactivating on submit

- chat.mjs: fire-and-forget options.submitHandler instead of awaiting
it inside the TextInput submit callback. TextInput.run → deactivate
was blocked for the full length of aa.mjs's SSE stream, so the
input panel stayed "live" and pressing Enter did nothing visible.
With fire-and-forget the callback returns immediately and the
TextInput deactivates on the next frame like any sync submit.

- aa.mjs: drop the verbose tool_use / tool_result / stderr stream —
one "thinking…" placeholder on send, removed on done/error.
Keeps the UX at the level of basic chat: you + @aa, nothing else.
History replay (renderClaudeEvent) is now also user+assistant text
only. pushMessage auto-sets msg.font="MatrixChunky8" on log rows
so system lines read as quiet chrome rather than loud chat turns.
Also clear token/isAdmin on 401/403 during chat (not just at boot).

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

+46 -60
+35 -54
system/public/aesthetic.computer/disks/aa.mjs
··· 164 164 165 165 function pushMessage(from, text, { sound = false } = {}) { 166 166 const msg = { from, text, id: nextId(), sub: from }; 167 + // Render log/system messages in the smaller MatrixChunky8 font so they 168 + // feel like unobtrusive chrome, not loud chat turns. 169 + if (from === "log") msg.font = "MatrixChunky8"; 167 170 client.system.messages.push(msg); 168 171 if (client.system.messages.length > 500) client.system.messages.shift(); 169 - // chat.mjs's receiver hook sets `messagesNeedLayout = true` whenever 170 - // extra.layoutChanged is set; type === "message" also plays the SFX. 171 172 client.system.receiver?.( 172 173 msg.id, 173 174 sound ? "message" : "layout-only", ··· 179 180 180 181 function pushSystem(text) { 181 182 return pushMessage("log", 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 }); 182 190 } 183 191 184 192 function invalidate() { ··· 206 214 const data = await res.json(); 207 215 const events = Array.isArray(data?.events) ? data.events : []; 208 216 let count = 0; 209 - for (const ev of events) count += renderClaudeEvent(ev, { fromHistory: true }); 217 + for (const ev of events) count += renderClaudeEvent(ev); 210 218 if (count) pushSystem(`loaded ${count} prior ${count === 1 ? "message" : "messages"}.`); 211 219 } catch (err) { 212 220 pushSystem(`history fetch failed: ${err.message}`); 213 221 } 214 222 } 215 223 216 - // Render one claude transcript event (from live stream or saved history) 217 - // as zero or more chat messages. Returns the number of messages produced. 218 - function renderClaudeEvent(ev, { fromHistory = false } = {}) { 224 + // Render one claude transcript event (used for history replay only — live 225 + // streaming accumulates into pendingText in sendToBridge). Keeps the 226 + // rendered history at "basic chat" level: user prompts ↔ assistant text. 227 + // tool_use / tool_result / thinking blocks are intentionally skipped. 228 + function renderClaudeEvent(ev) { 219 229 if (!ev || typeof ev !== "object") return 0; 220 230 let produced = 0; 221 231 if (ev.type === "assistant" && ev.message?.content) { 222 232 for (const b of ev.message.content) { 223 233 if (b.type === "text" && b.text) { 224 - pushMessage("@aa", b.text, { sound: !fromHistory }); 225 - produced++; 226 - } else if (b.type === "tool_use") { 227 - pushMessage("log", `→ ${b.name} ${summarizeInput(b.input)}`); 234 + pushMessage("@aa", b.text); 228 235 produced++; 229 236 } 230 237 } 231 238 } else if (ev.type === "user" && ev.message?.content) { 232 239 const c = ev.message.content; 233 - // History format: user content is usually a plain string (the prompt). 240 + const me = userHandleRef?.() || "@jeffrey"; 234 241 if (typeof c === "string") { 235 - const me = userHandleRef?.() || "@jeffrey"; 236 242 pushMessage(me, c); 237 243 produced++; 238 244 } else if (Array.isArray(c)) { 239 245 for (const b of c) { 240 - if (b.type === "tool_result") { 241 - const t = stringifyResult(b.content); 242 - if (t) { pushMessage("log", truncate(t, 600)); produced++; } 243 - } else if (b.type === "text" && b.text) { 244 - const me = userHandleRef?.() || "@jeffrey"; 246 + if (b.type === "text" && b.text) { 245 247 pushMessage(me, b.text); 246 248 produced++; 247 249 } ··· 259 261 pending = true; 260 262 abortCtrl = new AbortController(); 261 263 264 + // One unobtrusive placeholder while streaming. Removed on done/error so the 265 + // chat stays at the "basic chat" level — no tool traces, no stderr spam. 266 + const thinkingMsg = pushMessage("log", "thinking…"); 262 267 let pendingText = ""; 263 268 264 269 try { ··· 273 278 }); 274 279 if (!res.ok) { 275 280 const body = await res.text().catch(() => `${res.status}`); 281 + removeMessage(thinkingMsg); 282 + if (res.status === 401 || res.status === 403) { 283 + isAdmin = false; 284 + token = null; 285 + setStatus(res.status === 403 ? "forbidden" : "unauth"); 286 + } 276 287 pushSystem(`! ${res.status}: ${body.slice(0, 200)}`); 277 288 return; 278 289 } ··· 295 306 const ev = evt.data; 296 307 if (ev.type === "assistant" && ev.message?.content) { 297 308 for (const b of ev.message.content) { 298 - if (b.type === "text" && b.text) { 299 - pendingText += b.text; 300 - } else if (b.type === "tool_use") { 301 - pushMessage("log", `→ ${b.name} ${summarizeInput(b.input)}`); 302 - } 303 - } 304 - } else if (ev.type === "user" && ev.message?.content) { 305 - for (const b of ev.message.content) { 306 - if (b.type === "tool_result") { 307 - const t = stringifyResult(b.content); 308 - if (t) pushMessage("log", truncate(t, 600)); 309 - } 309 + if (b.type === "text" && b.text) pendingText += b.text; 310 + // tool_use / thinking / other blocks deliberately suppressed 310 311 } 311 312 } else if (ev.type === "result" && ev.result) { 312 313 pendingText = ev.result; 313 314 } 314 - } else if (evt.event === "stderr") { 315 - const t = (evt.data.text || "").trim(); 316 - if (t && !t.startsWith("Warning:")) pushMessage("log", t); 315 + // tool_result (in user events) also suppressed 317 316 } else if (evt.event === "error") { 318 317 pushMessage("log", `! ${evt.data.message || "error"}`); 319 318 } else if (evt.event === "done") { 319 + removeMessage(thinkingMsg); 320 320 if (pendingText) { 321 321 pushMessage("@aa", pendingText, { sound: true }); 322 322 pendingText = ""; 323 323 } 324 324 } 325 + // stderr events ignored — bridge warnings are not user-facing 325 326 } 326 327 } 327 328 } catch (err) { 329 + removeMessage(thinkingMsg); 328 330 if (err.name === "AbortError") pushSystem("cancelled."); 329 331 else pushSystem(`error: ${err.message}`); 330 332 } finally { 333 + removeMessage(thinkingMsg); // idempotent — no-op if already removed 331 334 pending = false; 332 335 abortCtrl = null; 333 336 } ··· 344 347 } 345 348 if (!data) return null; 346 349 try { return { event, data: JSON.parse(data) }; } catch { return null; } 347 - } 348 - 349 - function summarizeInput(input) { 350 - if (!input) return ""; 351 - if (typeof input === "string") return truncate(input, 80); 352 - const k = input.command || input.file_path || input.path || input.pattern || input.query || input.url; 353 - if (k) return truncate(String(k), 80); 354 - return truncate(JSON.stringify(input), 80); 355 - } 356 - 357 - function stringifyResult(content) { 358 - if (!content) return ""; 359 - if (typeof content === "string") return content; 360 - if (Array.isArray(content)) { 361 - return content.map((c) => (typeof c === "string" ? c : c?.text || "")).filter(Boolean).join("\n"); 362 - } 363 - return String(content); 364 - } 365 - 366 - function truncate(s, n) { 367 - s = String(s); 368 - return s.length > n ? s.slice(0, n - 1) + "…" : s; 369 350 } 370 351 371 352 export { boot, paint, act, sim, leave, meta };
+11 -6
system/public/aesthetic.computer/disks/chat.mjs
··· 558 558 559 559 // Pieces inheriting chat.mjs (e.g. aa.mjs) may pass a custom submit 560 560 // handler so they can route the typed text somewhere other than the 561 - // chat-system server. If provided, it owns the send. 561 + // chat-system server. If provided, it owns the send. Fire-and-forget 562 + // so that awaiting the submit callback (and thus TextInput.run → 563 + // deactivate) doesn't block on async handlers like aa.mjs's SSE 564 + // stream — otherwise the input panel stays "live" the whole reply. 562 565 if (options?.submitHandler) { 563 - try { 564 - await options.submitHandler(text, { token, sub: user?.sub, font: userSelectedFont }); 565 - } catch (err) { 566 - console.error("submitHandler failed:", err); 567 - } 566 + Promise.resolve( 567 + options.submitHandler(text, { 568 + token, 569 + sub: user?.sub, 570 + font: userSelectedFont, 571 + }), 572 + ).catch((err) => console.error("submitHandler failed:", err)); 568 573 } else { 569 574 const currentHandle = handle(); 570 575 if (!currentHandle) {