Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

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

ac-electron v0.1.38: always-on-top toggle, productName, CDP harness

- Always-on-top now OFF by default (was always on). Added a "Always on
Top" checkbox to the main tray menu that toggles + persists to
preferences.json + applies to all open AC windows.
- productName renamed to "Aesthetic.Computer" (dot instead of space) so
Finder/Dock/menu-bar labels read as a single token.
- New testing/cdp-latency.mjs: a standalone probe that talks to the
running app over CDP (localhost:9222 — launch with
--remote-debugging-port=9222), navigates to notepat, dispatches key
events, reads window.__notepat_perfStats.latency, and prints stats.
Enables driving/testing the installed app from outside without the
old Codespaces/VS Code artery panel.
- `npm run test:latency` script entry added.

Auto-updater validated end-to-end: installed v0.1.37 correctly
detected + began downloading v0.1.38 from the self-hosted feed on
relaunch.

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

+313 -9
+26 -4
ac-electron/main.js
··· 76 76 showTrayTitle: true, 77 77 trayTitleText: 'AC', // Short text next to tray icon 78 78 launchAtLogin: false, 79 - defaultMode: 'ac-pane' 79 + defaultMode: 'ac-pane', 80 + // Window float behavior. When true, new AC Pane / Notepat windows 81 + // open with alwaysOnTop so they float above other apps (the old 82 + // default). When false (new default), they behave like normal windows 83 + // and can be ordered underneath. Toggled from the tray menu. 84 + alwaysOnTop: false, 80 85 }; 81 86 82 87 function loadPreferences() { ··· 840 845 label: 'Open Notepat 🎹', 841 846 click: () => openNotepatWindow() 842 847 }); 843 - 848 + 849 + // Always-on-top toggle. Persisted to preferences.json and applied to 850 + // every current AC window on change; new windows inherit via their 851 + // BrowserWindow opts. 852 + menuItems.push({ 853 + label: 'Always on Top', 854 + type: 'checkbox', 855 + checked: !!preferences.alwaysOnTop, 856 + click: (item) => { 857 + preferences.alwaysOnTop = !!item.checked; 858 + savePreferences(); 859 + for (const w of BrowserWindow.getAllWindows()) { 860 + try { w.setAlwaysOnTop(preferences.alwaysOnTop); } catch (_) {} 861 + } 862 + rebuildTrayMenu(); 863 + } 864 + }); 865 + 844 866 // Quick DevTools access (especially for Windows) 845 867 menuItems.push({ 846 868 label: 'Open DevTools', ··· 1211 1233 frame: false, 1212 1234 transparent: !isPaperWM, 1213 1235 hasShadow: isPaperWM, 1214 - alwaysOnTop: !isPaperWM, 1236 + alwaysOnTop: !isPaperWM && preferences.alwaysOnTop, 1215 1237 backgroundColor: isPaperWM ? '#000000' : '#00000000', 1216 1238 webPreferences: { 1217 1239 nodeIntegration: true, ··· 1328 1350 frame: false, 1329 1351 transparent: !isPaperWM, 1330 1352 hasShadow: isPaperWM, 1331 - alwaysOnTop: !isPaperWM, 1353 + alwaysOnTop: !isPaperWM && preferences.alwaysOnTop, 1332 1354 backgroundColor: isPaperWM ? '#000000' : '#00000000', 1333 1355 webPreferences: { 1334 1356 nodeIntegration: true,
+2 -2
ac-electron/package-lock.json
··· 1 1 { 2 2 "name": "aesthetic-computer", 3 - "version": "0.1.37", 3 + "version": "0.1.38", 4 4 "lockfileVersion": 3, 5 5 "requires": true, 6 6 "packages": { 7 7 "": { 8 8 "name": "aesthetic-computer", 9 - "version": "0.1.37", 9 + "version": "0.1.38", 10 10 "license": "MIT", 11 11 "dependencies": { 12 12 "@electron/rebuild": "^4.0.2",
+4 -3
ac-electron/package.json
··· 1 1 { 2 2 "name": "aesthetic-computer", 3 - "version": "0.1.37", 3 + "version": "0.1.38", 4 4 "description": "Aesthetic Computer", 5 5 "homepage": "https://aesthetic.computer", 6 6 "author": "Aesthetic Computer <hi@aesthetic.computer>", ··· 30 30 "build:host:mac": "bash scripts/build-publish-host-mac.sh --skip-publish", 31 31 "release:host:mac": "bash scripts/build-publish-host-mac.sh", 32 32 "publish:mac": "electron-builder --mac --universal --publish always", 33 - "release": "electron-builder --mac --universal --publish always" 33 + "release": "electron-builder --mac --universal --publish always", 34 + "test:latency": "node testing/cdp-latency.mjs" 34 35 }, 35 36 "build": { 36 37 "appId": "computer.aesthetic.app", 37 - "productName": "Aesthetic Computer", 38 + "productName": "Aesthetic.Computer", 38 39 "copyright": "Copyright © 2025 Aesthetic Computer", 39 40 "directories": { 40 41 "output": "dist",
+281
ac-electron/testing/cdp-latency.mjs
··· 1 + #!/usr/bin/env node 2 + // CDP latency probe for the running Electron app. 3 + // 4 + // Connects to the Electron CDP endpoint (localhost:9222, enabled by the 5 + // --remote-debugging-port=9222 flag the app already sets in package.json 6 + // scripts), finds the notepat webview, dispatches keydown events, reads 7 + // `window.__notepat_perfStats.latency`, and reports stats. 8 + // 9 + // Prerequisite: the app must be running. Install/launch via: 10 + // open "/Applications/Aesthetic Computer.app" 11 + // Or for the dev source: 12 + // cd ac-electron && npm start 13 + // 14 + // Usage: 15 + // node testing/cdp-latency.mjs [--iterations=20] [--cooldown=200] 16 + // [--key=a] [--port=9222] 17 + // [--open-notepat] open the notepat window first 18 + // 19 + // Notes: 20 + // - Uses native fetch + WebSocket (Node 22+). No external deps. 21 + // - Measures JS-side latency: keydown → notepat's `sound.synth` call. 22 + // - Audio buffer/worklet latency is NOT included (typically +10-30ms). 23 + 24 + const DEFAULTS = { 25 + iterations: 20, 26 + cooldownMs: 200, 27 + key: "a", 28 + port: 9222, 29 + openNotepat: false, 30 + }; 31 + 32 + function parseArgs(argv) { 33 + const out = { ...DEFAULTS }; 34 + for (const raw of argv.slice(2)) { 35 + const [k, v] = raw.replace(/^--/, "").split("="); 36 + if (k === "iterations") out.iterations = Number(v); 37 + else if (k === "cooldown") out.cooldownMs = Number(v); 38 + else if (k === "key") out.key = v; 39 + else if (k === "port") out.port = Number(v); 40 + else if (k === "open-notepat") out.openNotepat = true; 41 + else throw new Error(`unknown arg: ${raw}`); 42 + } 43 + return out; 44 + } 45 + 46 + async function listTargets(port) { 47 + const r = await fetch(`http://127.0.0.1:${port}/json`); 48 + if (!r.ok) throw new Error(`CDP /json returned ${r.status}. Is the app running with --remote-debugging-port=${port}?`); 49 + return r.json(); 50 + } 51 + 52 + // Minimal CDP client over a single target's WebSocket. Keeps the wire 53 + // protocol in one place (id → promise map + event listeners) so each 54 + // target we talk to is just a session object. 55 + class CdpSession { 56 + constructor(ws) { 57 + this.ws = ws; 58 + this.nextId = 1; 59 + this.pending = new Map(); 60 + this.listeners = new Map(); 61 + ws.addEventListener("message", (m) => this.onMessage(m)); 62 + } 63 + 64 + static async connect(url) { 65 + const ws = new WebSocket(url); 66 + await new Promise((res, rej) => { 67 + ws.addEventListener("open", res, { once: true }); 68 + ws.addEventListener("error", rej, { once: true }); 69 + }); 70 + return new CdpSession(ws); 71 + } 72 + 73 + onMessage(m) { 74 + const msg = JSON.parse(m.data); 75 + if (msg.id && this.pending.has(msg.id)) { 76 + const { resolve, reject } = this.pending.get(msg.id); 77 + this.pending.delete(msg.id); 78 + if (msg.error) reject(new Error(`${msg.error.code}: ${msg.error.message}`)); 79 + else resolve(msg.result); 80 + } else if (msg.method) { 81 + const fns = this.listeners.get(msg.method); 82 + if (fns) for (const fn of fns) fn(msg.params); 83 + } 84 + } 85 + 86 + send(method, params = {}) { 87 + const id = this.nextId++; 88 + return new Promise((resolve, reject) => { 89 + this.pending.set(id, { resolve, reject }); 90 + this.ws.send(JSON.stringify({ id, method, params })); 91 + }); 92 + } 93 + 94 + on(event, fn) { 95 + if (!this.listeners.has(event)) this.listeners.set(event, new Set()); 96 + this.listeners.get(event).add(fn); 97 + } 98 + 99 + close() { this.ws.close(); } 100 + } 101 + 102 + const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); 103 + 104 + function percentile(sorted, p) { 105 + if (sorted.length === 0) return 0; 106 + const idx = Math.min(sorted.length - 1, Math.floor((p / 100) * sorted.length)); 107 + return sorted[idx]; 108 + } 109 + 110 + function stats(samples) { 111 + const s = [...samples].sort((a, b) => a - b); 112 + const n = s.length; 113 + const mean = s.reduce((a, b) => a + b, 0) / n; 114 + const variance = s.reduce((acc, v) => acc + (v - mean) ** 2, 0) / n; 115 + return { 116 + n, 117 + mean, 118 + median: n ? s[Math.floor(n / 2)] : 0, 119 + min: s[0] ?? 0, 120 + max: s[n - 1] ?? 0, 121 + p95: percentile(s, 95), 122 + p99: percentile(s, 99), 123 + stdDev: Math.sqrt(variance), 124 + }; 125 + } 126 + 127 + // Key name → VK code + DOM-style key. `Input.dispatchKeyEvent` wants a 128 + // `windowsVirtualKeyCode` for modifier-free letter keys. For lowercase 129 + // a–z, VK = uppercase ASCII code. 130 + function keyToInput(k) { 131 + const ch = k.toLowerCase(); 132 + const vk = ch.toUpperCase().charCodeAt(0); 133 + return { key: ch, code: `Key${ch.toUpperCase()}`, windowsVirtualKeyCode: vk }; 134 + } 135 + 136 + async function findNotepatTarget(port) { 137 + const targets = await listTargets(port); 138 + // Prefer a target whose URL is the notepat piece itself; fall back to 139 + // any aesthetic.computer page (whatever the AC Pane/Notepat window 140 + // happens to be showing). 141 + // AC content runs inside a <webview> element, so the CDP target type 142 + // is "webview" (not "page"). The outer BrowserWindow is "page" and 143 + // loads a local file:// harness — skip those. 144 + const isAcContent = (t) => /aesthetic\.computer/.test(t.url || "") && (t.type === "webview" || t.type === "page"); 145 + const byPiece = targets.find((t) => isAcContent(t) && /\/notepat(\W|$)/.test(t.url || "")); 146 + if (byPiece) return byPiece; 147 + return targets.find(isAcContent); 148 + } 149 + 150 + async function openNotepatViaMain(port) { 151 + // Find the main-process BrowserWindow target (notepat-view.html or 152 + // flip-view.html) so we can invoke openNotepatWindow() indirectly via 153 + // a helper exposed at runtime. For now we just log a hint; opening 154 + // the notepat window is one click on the piano tray. 155 + console.log("⚠️ --open-notepat: click the piano tray icon to open Notepat."); 156 + console.log(" Waiting up to 20s for a target matching /notepat to appear..."); 157 + const deadline = Date.now() + 20_000; 158 + while (Date.now() < deadline) { 159 + const t = await findNotepatTarget(port); 160 + if (t && /\/notepat/.test(t.url || "")) return t; 161 + await sleep(500); 162 + } 163 + return null; 164 + } 165 + 166 + async function main() { 167 + const opts = parseArgs(process.argv); 168 + console.log(`CDP latency probe — iterations=${opts.iterations} cooldown=${opts.cooldownMs}ms key=${opts.key}`); 169 + 170 + let target; 171 + if (opts.openNotepat) target = await openNotepatViaMain(opts.port) || await findNotepatTarget(opts.port); 172 + else target = await findNotepatTarget(opts.port); 173 + 174 + if (!target) { 175 + console.error("❌ No aesthetic.computer page target found on CDP :" + opts.port); 176 + console.error(" Make sure the app is running and a Notepat/AC window is open."); 177 + process.exit(1); 178 + } 179 + console.log(`→ Attaching to: ${target.url}`); 180 + 181 + const cdp = await CdpSession.connect(target.webSocketDebuggerUrl); 182 + await cdp.send("Runtime.enable"); 183 + await cdp.send("Input.enable").catch(() => {}); // Input domain doesn't exist in all CDP versions 184 + await cdp.send("Page.enable").catch(() => {}); 185 + 186 + // If we grabbed some other AC page (e.g. /prompt), navigate it to the 187 + // notepat piece via the in-app router so boot() runs and perfStats 188 + // becomes available. `jump` is the AC convention for client-side piece 189 + // navigation and works from any piece. 190 + if (!/\/notepat/.test(target.url || "")) { 191 + console.log("→ Navigating this webview to /notepat via jump('notepat')..."); 192 + await cdp.send("Runtime.evaluate", { 193 + expression: `(window.jump || (s) => (location.href = '/' + s))('notepat')`, 194 + }); 195 + } 196 + 197 + // Wait up to 8s for `window.__notepat_perfStats` to appear. 198 + const bootDeadline = Date.now() + 8000; 199 + let perfReady = false; 200 + while (Date.now() < bootDeadline) { 201 + const r = await cdp.send("Runtime.evaluate", { 202 + expression: `(typeof window.__notepat_perfStats === 'object')`, 203 + returnByValue: true, 204 + }); 205 + if (r.result.value) { perfReady = true; break; } 206 + await sleep(150); 207 + } 208 + if (!perfReady) { 209 + console.error("❌ `window.__notepat_perfStats` not found after boot wait."); 210 + process.exit(1); 211 + } 212 + // Notepat needs a user gesture to unlock the AudioContext. Without one, 213 + // sound.synth calls queue silently and latency reads won't be meaningful. 214 + // Dispatch a synthetic mouseDown/mouseUp on the center of the viewport 215 + // to satisfy the gesture requirement before the test loop starts. 216 + await cdp.send("Input.dispatchMouseEvent", { type: "mousePressed", x: 100, y: 100, button: "left", clickCount: 1 }).catch(() => {}); 217 + await sleep(30); 218 + await cdp.send("Input.dispatchMouseEvent", { type: "mouseReleased", x: 100, y: 100, button: "left", clickCount: 1 }).catch(() => {}); 219 + await sleep(300); 220 + 221 + const input = keyToInput(opts.key); 222 + const samples = []; 223 + 224 + for (let i = 0; i < opts.iterations; i++) { 225 + // Reset the stat so a stale value doesn't bleed through. 226 + await cdp.send("Runtime.evaluate", { 227 + expression: `window.__notepat_perfStats && (window.__notepat_perfStats.latency = null, window.__notepat_perfStats.lastKeyTime = null)`, 228 + }); 229 + 230 + await cdp.send("Input.dispatchKeyEvent", { 231 + type: "keyDown", 232 + key: input.key, 233 + code: input.code, 234 + windowsVirtualKeyCode: input.windowsVirtualKeyCode, 235 + }); 236 + 237 + // Wait up to 80ms for the sound trigger to update perfStats. 238 + let latency = null; 239 + for (let tries = 0; tries < 16; tries++) { 240 + await sleep(5); 241 + const r = await cdp.send("Runtime.evaluate", { 242 + expression: `window.__notepat_perfStats?.latency ?? null`, 243 + returnByValue: true, 244 + }); 245 + if (typeof r.result.value === "number") { latency = r.result.value; break; } 246 + } 247 + 248 + await cdp.send("Input.dispatchKeyEvent", { 249 + type: "keyUp", 250 + key: input.key, 251 + code: input.code, 252 + windowsVirtualKeyCode: input.windowsVirtualKeyCode, 253 + }); 254 + 255 + if (typeof latency === "number") { 256 + samples.push(latency); 257 + process.stdout.write(`${latency.toFixed(1)}ms `); 258 + } else { 259 + process.stdout.write("MISS "); 260 + } 261 + await sleep(opts.cooldownMs); 262 + } 263 + console.log(); 264 + 265 + const s = stats(samples); 266 + console.log(`\nresults (n=${s.n}/${opts.iterations}):`); 267 + console.log(` mean: ${s.mean.toFixed(1)}ms`); 268 + console.log(` median: ${s.median.toFixed(1)}ms`); 269 + console.log(` min: ${s.min.toFixed(1)}ms`); 270 + console.log(` max: ${s.max.toFixed(1)}ms`); 271 + console.log(` stdDev: ${s.stdDev.toFixed(1)}ms`); 272 + console.log(` p95: ${s.p95.toFixed(1)}ms`); 273 + console.log(` p99: ${s.p99.toFixed(1)}ms`); 274 + 275 + cdp.close(); 276 + } 277 + 278 + main().catch((e) => { 279 + console.error("fatal:", e.message); 280 + process.exit(1); 281 + });