Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

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

notepat updates and emacs report

+342 -20
+95
reports/2026-01-21-emacs-stack-report.md
··· 1 + # Emacs Stack Report — Docker Container Context (2026-01-21) 2 + 3 + ## Purpose 4 + Document how the Emacs stack works inside the dev container, why the UI can appear as only `*scratch*`, and how to reliably bring back the full Aesthetic tab layout after crashes. 5 + 6 + ## Primary Entry Points 7 + 1. **Daemon start script** 8 + - The container starts Emacs in daemon mode with the repo config: 9 + - Config path: [dotfiles/dot_config/emacs.el](dotfiles/dot_config/emacs.el) 10 + - Startup logic: [ .devcontainer/config.fish](.devcontainer/config.fish) 11 + 12 + 2. **Crash monitor / watchdog** 13 + - Keeps the daemon alive and restarts it if it becomes unresponsive. 14 + - Crash monitor script: [monitor-emacs.sh](monitor-emacs.sh) 15 + - Logs: [.emacs-logs](.emacs-logs) 16 + 17 + 3. **Terminal client** 18 + - `emacsclient -t` or `ac-aesthetic` connects a terminal frame. 19 + - The first terminal frame triggers the backend that creates tabs and Eat terminals. 20 + 21 + ## Core Runtime Components 22 + ### `aesthetic-backend` 23 + Defined in [dotfiles/dot_config/emacs.el](dotfiles/dot_config/emacs.el). 24 + - Creates tab bar layout and terminal buffers. 25 + - Runs all `ac-*` commands that launch web servers, sessions, Redis, oven, etc. 26 + - Switches to a target tab after creation (default: `artery`). 27 + 28 + ### `ac--maybe-start-backend` 29 + Also in [dotfiles/dot_config/emacs.el](dotfiles/dot_config/emacs.el). 30 + - Registered via `after-make-frame-functions`. 31 + - Only runs when a **terminal frame** connects. 32 + - That’s why headless daemon restarts show `*scratch*` only until a terminal frame connects. 33 + 34 + ### Eat terminals 35 + Each tab uses Eat to run a fish command (e.g., `ac-redis`, `ac-site`, `ac-session`). 36 + If Emacs restarts without a terminal frame, **none of these processes are re-spawned** until `aesthetic-backend` runs again. 37 + 38 + ## Normal Startup Flow (Expected) 39 + 1. Daemon starts via `.devcontainer/config.fish`. 40 + 2. Terminal frame connects (`ac-aesthetic` or `emacsclient -t`). 41 + 3. `ac--maybe-start-backend` calls `aesthetic-backend`. 42 + 4. Tabs and Eat buffers are created in order. 43 + 5. After a delay, Emacs switches to the target tab (`artery`). 44 + 45 + ## Crash + Auto-Restart Flow (Observed) 46 + 1. Crash monitor restarts the daemon. 47 + 2. Daemon is alive **but no terminal frame is connected**. 48 + 3. `aesthetic-backend` does **not** run automatically. 49 + 4. User sees only `*scratch*` and no tabs. 50 + 51 + **This is expected behavior** given the current hook architecture. 52 + 53 + ## Why You See Only `*scratch*` 54 + The backend startup is tied to `after-make-frame-functions` and specifically checks for terminal frames. When the daemon restarts in the background, no terminal client connects, so the backend never runs. This leaves a minimal Emacs session without tabs or Eat buffers. 55 + 56 + ## Recovery Steps 57 + If Emacs restarts and only `*scratch*` is visible: 58 + 1. Connect a terminal frame: 59 + - `emacsclient -t` 60 + 2. If the tabs do not appear, explicitly run: 61 + - `M-x aesthetic-backend` 62 + - Or evaluate: `(aesthetic-backend "artery")` 63 + 64 + ## Signals and Logs to Verify State 65 + - **Is the daemon running?** 66 + - `pgrep -f "emacs.*daemon"` 67 + - **Did `aesthetic-backend` run?** 68 + - Check [ .emacs-logs/emacs-debug.log](.emacs-logs/emacs-debug.log) for: 69 + - `Starting aesthetic-backend with target-tab: artery` 70 + - **Did tabs get created?** 71 + - `emacsclient -e '(tab-bar-tabs)'` 72 + 73 + ## Known Failure Modes 74 + 1. **Crash without terminal reconnect** 75 + - Result: only `*scratch*` and no tabs. 76 + 2. **Backend delay warnings** 77 + - Log entry: `WARNING: aesthetic-backend has been running for 30+ seconds`. 78 + - Usually indicates slow-starting processes in Eat buffers. 79 + 3. **CDP tunnel failure** 80 + - Log entry: `CDP tunnel failed: exited abnormally with code 1`. 81 + - Non-fatal, but indicates a missing tunnel endpoint. 82 + 83 + ## Suggested Improvements (Optional) 84 + To avoid manual recovery after crashes: 85 + 1. **Crash monitor should run `aesthetic-backend` after restart.** 86 + 2. Or **auto-connect a terminal client** (`emacsclient -t`) after daemon start. 87 + 3. Add a lightweight sanity check: 88 + - If no tabs beyond `*scratch*`, trigger `aesthetic-backend` automatically. 89 + 90 + ## Quick Reference 91 + - Main config: [dotfiles/dot_config/emacs.el](dotfiles/dot_config/emacs.el) 92 + - Daemon start: [.devcontainer/config.fish](.devcontainer/config.fish) 93 + - Crash monitor: [monitor-emacs.sh](monitor-emacs.sh) 94 + - Debug log: [.emacs-logs/emacs-debug.log](.emacs-logs/emacs-debug.log) 95 + - Crash diary: [.emacs-logs/crashes.log](.emacs-logs/crashes.log)
+247 -20
system/public/aesthetic.computer/disks/notepat.mjs
··· 2 2 // Tap the pads to play musical notes, or use the keyboard keys. 3 3 4 4 /* 📝 Notes 5 - - [] Make `slide` work with `composite`. 6 - (This may require some refactoring) 5 + - [x] Make `slide` work with `composite`. 6 + (Implemented via update method on composite sound object) 7 7 - [] Add recordable samples / custom samples... per key? 8 8 - [] Stored under handle? 9 9 - [🟠] Somehow represent both of these in the graphic layout. ··· 597 597 // let qrcells; 598 598 599 599 let waveBtn, octBtn; 600 + let slideBtn, quickBtn, roomBtn; // Mode toggle buttons 600 601 let melodyAliasBtn; 601 602 let melodyAliasDown = false; 602 603 let melodyAliasActiveNote = null; ··· 791 792 792 793 buildWaveButton(api); 793 794 buildOctButton(api); 795 + buildModeButtons(api, computeButtonLayout(screen)); 794 796 795 797 const newOctave = 796 798 parseInt(colon[0]) || parseInt(colon[1]) || parseInt(colon[2]); ··· 1300 1302 wipe(bg); 1301 1303 } 1302 1304 1303 - if (slide) { 1304 - ink(undefined).write("slide", { right: 4, top: 24 }); 1305 + // 🎛️ Mode toggle buttons (quick, room, slide) 1306 + if (quickBtn && !paintPictureOverlay && !projector) { 1307 + quickBtn.paint((btn) => { 1308 + if (quickFade) { 1309 + ink(btn.down ? [80, 120, 80] : [40, 80, 40]); 1310 + } else { 1311 + ink(btn.down ? [60, 60, 60] : [30, 30, 30]); 1312 + } 1313 + box(btn.box.x, btn.box.y, btn.box.w, btn.box.h); 1314 + ink(quickFade ? (btn.down ? "lime" : "green") : (btn.down ? "gray" : "darkgray")); 1315 + write("quick", btn.box.x + 3, btn.box.y + 2); 1316 + }); 1305 1317 } 1306 1318 1307 - if (quickFade) { 1308 - ink(undefined).write("quick", { left: 6, top: 24 }); 1319 + if (roomBtn && !paintPictureOverlay && !projector) { 1320 + roomBtn.paint((btn) => { 1321 + if (roomMode) { 1322 + ink(btn.down ? [80, 80, 120] : [40, 40, 80]); 1323 + } else { 1324 + ink(btn.down ? [60, 60, 60] : [30, 30, 30]); 1325 + } 1326 + box(btn.box.x, btn.box.y, btn.box.w, btn.box.h); 1327 + ink(roomMode ? (btn.down ? "cyan" : "blue") : (btn.down ? "gray" : "darkgray")); 1328 + write("room", btn.box.x + 3, btn.box.y + 2); 1329 + }); 1309 1330 } 1310 1331 1311 - // 🏠 Room mode indicator 1312 - if (roomMode) { 1313 - ink(undefined).write("room", { center: "x", top: 24 }); 1332 + if (slideBtn && !paintPictureOverlay && !projector) { 1333 + slideBtn.paint((btn) => { 1334 + if (slide) { 1335 + ink(btn.down ? [120, 80, 80] : [80, 40, 40]); 1336 + } else { 1337 + ink(btn.down ? [60, 60, 60] : [30, 30, 30]); 1338 + } 1339 + box(btn.box.x, btn.box.y, btn.box.w, btn.box.h); 1340 + ink(slide ? (btn.down ? "orange" : "red") : (btn.down ? "gray" : "darkgray")); 1341 + write("slide", btn.box.x + 3, btn.box.y + 2); 1342 + }); 1314 1343 } 1315 1344 1316 1345 // wipe(!projector ? bg : 64); ··· 2274 2303 setupButtons(api); 2275 2304 buildWaveButton(api); 2276 2305 buildOctButton(api); 2306 + buildModeButtons(api, computeButtonLayout(screen)); 2277 2307 // Resize picture to quarter resolution (half width, half height) 2278 2308 const resizedPictureWidth = Math.max(1, Math.floor(screen.width / 2)); 2279 2309 const resizedPictureHeight = Math.max(1, Math.floor(screen.height / 2)); ··· 2718 2748 // console.log("🐦 Composite tone:", tone); 2719 2749 2720 2750 let toneA, toneB, toneC, toneD, toneE; 2721 - const baseFreq = freq(tone); 2751 + let currentBaseFreq = freq(tone); 2722 2752 2723 2753 toneA = synth({ 2724 2754 type: "sine", 2725 2755 // attack: 0.5,//attack * 8, 2726 2756 attack: 0.0025, 2727 2757 decay: 0.9, 2728 - tone: baseFreq, 2729 - // tone: baseFreq + 280 + num.randIntRange(-10, 20), 2758 + tone: currentBaseFreq, 2759 + // tone: currentBaseFreq + 280 + num.randIntRange(-10, 20), 2730 2760 // duration: 0.18, 2731 2761 duration: "🔁", 2732 2762 volume: toneVolume * volumeScale, ··· 2735 2765 2736 2766 // TODO: Can't update straight after triggering. 2737 2767 // setTimeout(() => { 2738 - // toneA.update({ tone: baseFreq, duration: 0.02 }); 2768 + // toneA.update({ tone: currentBaseFreq, duration: 0.02 }); 2739 2769 // }, 10); 2740 2770 2741 2771 toneB = synth({ ··· 2743 2773 // attack: attack * 8, 2744 2774 attack: 0.0025, 2745 2775 // decay, 2746 - tone: baseFreq + 9 + num.randIntRange(-1, 1), //+ 8, //num.randIntRange(-5, 5), 2776 + tone: currentBaseFreq + 9 + num.randIntRange(-1, 1), //+ 8, //num.randIntRange(-5, 5), 2747 2777 duration: "🔁", 2748 2778 volume: (toneVolume / 3) * volumeScale, // / 16, 2749 2779 pan, ··· 2753 2783 type: "sawtooth", 2754 2784 attack, 2755 2785 decay: 0.9, 2756 - tone: baseFreq + num.randIntRange(-6, 6), 2786 + tone: currentBaseFreq + num.randIntRange(-6, 6), 2757 2787 duration: 0.15 + num.rand() * 0.05, 2758 2788 volume: (toneVolume / 48) * volumeScale, // / 32, 2759 2789 pan, ··· 2765 2795 type: "triangle", 2766 2796 attack: 0.999, //attack * 8, 2767 2797 // decay, 2768 - tone: baseFreq + 8 + num.randIntRange(-5, 5), 2798 + tone: currentBaseFreq + 8 + num.randIntRange(-5, 5), 2769 2799 duration: "🔁", 2770 2800 volume: (toneVolume / 32) * volumeScale, 2771 2801 pan, ··· 2775 2805 type: "square", 2776 2806 attack: 0.05, //attack * 8, 2777 2807 // decay, 2778 - tone: baseFreq + num.randIntRange(-10, 10), 2808 + tone: currentBaseFreq + num.randIntRange(-10, 10), 2779 2809 duration: "🔁", 2780 2810 volume: (toneVolume / 64) * volumeScale, 2781 2811 pan, 2782 2812 }); 2783 2813 2814 + // Store the random offsets so we can preserve them during slide updates 2815 + const offsets = { 2816 + B: 9 + num.randIntRange(-1, 1), 2817 + D: 8 + num.randIntRange(-5, 5), 2818 + E: num.randIntRange(-10, 10), 2819 + }; 2820 + 2784 2821 return { 2785 2822 startedAt: toneA?.startedAt || toneB.startedAt, 2823 + // 🎚️ Update method for slide mode - updates all composite voices 2824 + update: (payload) => { 2825 + if (payload.tone !== undefined) { 2826 + // payload.tone can be a frequency number or a note string 2827 + const newBaseFreq = typeof payload.tone === "number" 2828 + ? payload.tone 2829 + : freq(payload.tone); 2830 + currentBaseFreq = newBaseFreq; 2831 + const dur = payload.duration || 0.1; 2832 + // Update all continuous voices to new frequencies 2833 + toneA?.update?.({ tone: newBaseFreq, duration: dur }); 2834 + toneB?.update?.({ tone: newBaseFreq + offsets.B, duration: dur }); 2835 + // toneC is a one-shot, don't update it 2836 + toneD?.update?.({ tone: newBaseFreq + offsets.D, duration: dur * 1.4 }); 2837 + toneE?.update?.({ tone: newBaseFreq + offsets.E, duration: dur * 0.5 }); 2838 + } 2839 + }, 2786 2840 kill: (fade) => { 2787 2841 toneA?.kill(fade); 2788 2842 toneB?.kill(fade); ··· 3201 3255 }, 3202 3256 }); 3203 3257 3258 + // 🎛️ Mode toggle button handlers 3259 + quickBtn?.act(e, { 3260 + down: () => api.beep(400), 3261 + push: () => { 3262 + api.beep(); 3263 + quickFade = !quickFade; 3264 + }, 3265 + }); 3266 + 3267 + roomBtn?.act(e, { 3268 + down: () => api.beep(400), 3269 + push: () => { 3270 + api.beep(); 3271 + roomMode = !roomMode; 3272 + room.toggle(); 3273 + }, 3274 + }); 3275 + 3276 + slideBtn?.act(e, { 3277 + down: () => api.beep(400), 3278 + push: () => { 3279 + api.beep(); 3280 + slide = !slide; 3281 + // When enabling slide mode, kill all but the most recent note 3282 + if (slide && Object.keys(tonestack).length > 1) { 3283 + const orderedTones = orderedByCount(tonestack); 3284 + orderedTones.forEach((tone, index) => { 3285 + if (index > 0) { 3286 + sounds[tone]?.sound.kill(quickFade ? fastFade : fade); 3287 + trail[tone] = 1; 3288 + delete tonestack[tone]; 3289 + delete sounds[tone]; 3290 + if (buttons[tone]) buttons[tone].down = false; 3291 + } 3292 + }); 3293 + } 3294 + }, 3295 + }); 3296 + 3204 3297 const activateMelodyAlias = () => { 3205 3298 if (!song || paintPictureOverlay || projector) return false; 3206 3299 const currentNote = song?.[songIndex]?.[0]; ··· 3391 3484 const previousKey = orderedTones[orderedTones.length - 2]; 3392 3485 sounds[previousKey] = sounds[note]; 3393 3486 if (sounds[previousKey]) sounds[previousKey].note = previousKey; 3487 + delete sounds[note]; // Clean up old reference before tonestack cleanup 3394 3488 applyPitchBendToNotes([previousKey], { immediate: true }); 3395 3489 } else { 3396 3490 sounds[note]?.sound.kill(quickFade ? fastFade : killFade); ··· 3407 3501 } 3408 3502 3409 3503 delete tonestack[note]; // Remove this key from the notestack. 3410 - delete sounds[note]; 3504 + // Note: sounds[note] already deleted in slide branch above, or killed in else branch 3411 3505 //} else { 3412 3506 // console.log(note, sounds); 3413 3507 // sounds[key]?.sound?.update({ ··· 3854 3948 const n = noteFromKey(previousKeyRaw); 3855 3949 sounds[n] = sounds[buttonNote]; 3856 3950 if (sounds[n]) sounds[n].note = n; 3951 + delete sounds[buttonNote]; // Clean up old reference after transfer 3857 3952 applyPitchBendToNotes([previousKeyRaw], { immediate: true }); 3858 3953 // console.log("Replaced:", buttonNote, "with:", n); 3859 - 3860 - // delete sounds[buttonNote]; 3861 3954 } else if (hasSound) { 3862 3955 // Only kill the sound if we still have a reference to it 3863 3956 // console.log("Killing sound:", buttonNote); ··· 4235 4328 octBtn.id = "oct-button"; // Add identifier for debugging 4236 4329 } 4237 4330 4331 + // � Helper to compute basic layout info for button positioning 4332 + function computeButtonLayout(screen) { 4333 + const compactMode = screen.height < 200; 4334 + 4335 + if (compactMode) { 4336 + // Compact/DAW mode: split layout with center area 4337 + const margin = 2; 4338 + const whiteKeyWidth = 7; 4339 + const pianoWidth = 14 * whiteKeyWidth; 4340 + const qKeyWidth = 9; 4341 + const qKeySpacing = 1; 4342 + const qwertyWidth = 10 * (qKeyWidth + qKeySpacing); 4343 + const centerWidth = Math.max(pianoWidth, qwertyWidth) + 4; 4344 + 4345 + const notesPerSide = 12; 4346 + const buttonsPerRow = 4; 4347 + const totalRows = Math.ceil(notesPerSide / buttonsPerRow); 4348 + const hudReserved = TOP_BAR_BOTTOM; 4349 + const bottomPadding = 16; 4350 + 4351 + const availableSideWidth = (screen.width - centerWidth) / 2 - margin * 2; 4352 + const availableHeight = screen.height - hudReserved - bottomPadding - margin; 4353 + const maxButtonWidth = Math.floor(availableSideWidth / buttonsPerRow); 4354 + const maxButtonHeight = Math.floor(availableHeight / totalRows); 4355 + const buttonSize = Math.min(maxButtonWidth, maxButtonHeight); 4356 + 4357 + const leftOctaveX = margin; 4358 + const buttonBlockWidth = buttonsPerRow * buttonSize; 4359 + const rightOctaveX = screen.width - margin - buttonBlockWidth; 4360 + const centerX = leftOctaveX + buttonBlockWidth + margin; 4361 + const centerAreaWidth = rightOctaveX - centerX - margin; 4362 + 4363 + return { 4364 + compactMode: true, 4365 + splitLayout: true, 4366 + centerX, 4367 + centerAreaWidth, 4368 + }; 4369 + } 4370 + 4371 + return { 4372 + compactMode: false, 4373 + splitLayout: false, 4374 + centerX: 0, 4375 + centerAreaWidth: 0, 4376 + }; 4377 + } 4378 + 4379 + // �🎛️ Build mode toggle buttons (slide, quick, room) 4380 + function buildModeButtons({ screen, ui, typeface, geo }, layout) { 4381 + const glyphWidth = 4382 + typeface?.glyphs?.["0"]?.resolution?.[0] ?? 4383 + matrixFont?.glyphs?.["0"]?.resolution?.[0] ?? 4384 + 6; 4385 + const glyphHeight = 4386 + typeface?.glyphs?.["0"]?.resolution?.[1] ?? 4387 + matrixFont?.glyphs?.["0"]?.resolution?.[1] ?? 4388 + 8; 4389 + 4390 + const paddingX = 3; 4391 + const paddingY = 2; 4392 + const spacing = 2; // Space between buttons 4393 + const btnHeight = glyphHeight + paddingY * 2; 4394 + 4395 + // Calculate widths for each button 4396 + const quickWidth = "quick".length * glyphWidth + paddingX * 2; 4397 + const roomWidth = "room".length * glyphWidth + paddingX * 2; 4398 + const slideWidth = "slide".length * glyphWidth + paddingX * 2; 4399 + const totalWidth = quickWidth + roomWidth + slideWidth + spacing * 2; 4400 + 4401 + let startX, y; 4402 + 4403 + if (layout?.compactMode && layout?.splitLayout) { 4404 + // Split mode: center buttons horizontally in the center area, below QWERTY minimap 4405 + // The QWERTY minimap ends around y = TOP_BAR_BOTTOM + miniKeyboard + spacing + qwertyHeight 4406 + // Position below the qwerty minimap 4407 + const centerX = layout.centerX; 4408 + const centerAreaWidth = layout.centerAreaWidth; 4409 + startX = centerX + Math.floor((centerAreaWidth - totalWidth) / 2); 4410 + // Position below qwerty minimap (approximately) 4411 + const pianoY = TOP_BAR_BOTTOM + 2; 4412 + const whiteKeyHeight = 14; 4413 + const qwertyStartY = pianoY + whiteKeyHeight + QWERTY_MINIMAP_SPACING; 4414 + const qwertyHeight = 3 * (8 + 1); // 3 rows of 8px keys with 1px spacing 4415 + y = qwertyStartY + qwertyHeight + 4; 4416 + } else if (layout?.compactMode) { 4417 + // Single column compact mode: position on right side, below top bar 4418 + startX = screen.width - totalWidth - 4; 4419 + y = TOP_BAR_BOTTOM + 2; 4420 + } else { 4421 + // Normal mode: position on right side, below top bar 4422 + startX = screen.width - totalWidth - 4; 4423 + y = TOP_BAR_BOTTOM + 2; 4424 + } 4425 + 4426 + // Position buttons in a row: quick, room, slide 4427 + const quickX = startX; 4428 + const roomX = quickX + quickWidth + spacing; 4429 + const slideX = roomX + roomWidth + spacing; 4430 + 4431 + if (!quickBtn) { 4432 + quickBtn = new ui.Button(quickX, y, quickWidth, btnHeight); 4433 + quickBtn.id = "quick-button"; 4434 + } else { 4435 + quickBtn.box = new geo.Box(quickX, y, quickWidth, btnHeight); 4436 + } 4437 + 4438 + if (!roomBtn) { 4439 + roomBtn = new ui.Button(roomX, y, roomWidth, btnHeight); 4440 + roomBtn.id = "room-button"; 4441 + } else { 4442 + roomBtn.box = new geo.Box(roomX, y, roomWidth, btnHeight); 4443 + } 4444 + 4445 + if (!slideBtn) { 4446 + slideBtn = new ui.Button(slideX, y, slideWidth, btnHeight); 4447 + slideBtn.id = "slide-button"; 4448 + } else { 4449 + slideBtn.box = new geo.Box(slideX, y, slideWidth, btnHeight); 4450 + } 4451 + } 4452 + 4238 4453 let primaryColor = [0, 0, 0]; 4239 4454 let currentAverage = [0, 0, 0]; 4240 4455 ··· 4269 4484 ) 4270 4485 .map((sum) => round(sum / colors.length)); 4271 4486 } 4487 + 4488 + // 🏃 Leave - runs when exiting the piece 4489 + function leave({ sound }) { 4490 + // Reset room mode when leaving 4491 + if (roomMode) { 4492 + roomMode = false; 4493 + sound?.room?.off?.(); 4494 + } 4495 + } 4496 + 4497 + export { boot, sim, paint, act, leave }; 4498 + export { boot, sim, paint, act, leave };