Something went wrong. Try again.
Monorepo for Aesthetic.Computer aesthetic.computer
Something went wrong. Try again.
JavaScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134// render-progress.mjs — progress heartbeats for long /pop renders.//// Each slow /pop render (audio = render.mjs, illy = gen-illy.mjs, video// = preview-score.mjs) writes a tiny JSON heartbeat into// ~/.ac-pop-renders/ so an external watcher — the Slab menubar — can// show a live, temporary progress bar per render and drop it the moment// the render ends.//// Lifecycle: begin({type,label}) → update(pct) → end()//// Heartbeat file ~/.ac-pop-renders/<id>.json:// { id, type, label, pct, done, total, pid, startedAt, updatedAt }// type ∈ "audio" | "illy" | "video"// pct ∈ 0..100, or null for an indeterminate render// done, total ∈ integers (e.g. frame 142 of 240, panel 3 of 11);// null when the writer only knows pct//// The reader (`pop renders` CLI, and the Swift menubar) treats a file// as stale — and sweeps it — when its pid is dead or updatedAt is older// than STALE_MS, so a crashed render never leaves a ghost bar behind.
import { mkdirSync, writeFileSync, rmSync, readdirSync, readFileSync } from "node:fs";import { execSync } from "node:child_process";import { homedir } from "node:os";import { join } from "node:path";
export const RENDERS_DIR = join(homedir(), ".ac-pop-renders");const STALE_MS = 120_000;
function ensureDir() { try { mkdirSync(RENDERS_DIR, { recursive: true }); } catch { /* ignore */ }}
// Which Claude session launched this render? Walk up the process tree to// the claude pid (the same loop slab's hooks use), then match it against// the live $SLAB_HOME/state/active-prompts markers. The heartbeat carries// the answer so the Slab menubar can flip that session's row to the pink// `rendering` state — a backgrounded build never reads as idle. "" when// the render wasn't started from a Claude session.function resolveClaudeSessionId() { try { let pid = process.pid, claude = 0; for (let i = 0; i < 12 && pid > 1; i++) { const line = execSync(`ps -o ppid=,comm= -p ${pid}`, { encoding: "utf8" }).trim(); const m = line.match(/^(\d+)\s+(.*)$/); if (!m) break; if (m[2].includes("claude")) { claude = pid; break; } pid = Number(m[1]); } if (!claude) return ""; const dir = join(process.env.SLAB_HOME || join(homedir(), ".local/share/slab"), "state", "active-prompts"); for (const f of readdirSync(dir)) { try { const j = JSON.parse(readFileSync(join(dir, f), "utf8")); if (Number(j.claude_pid) === claude) return j.session_id || ""; } catch { /* skip unparsable marker */ } } } catch { /* no ps / no markers — not a claude launch */ } return "";}
// ── writer side (the render process) ─────────────────────────────────let _id = null, _file = null, _type = null, _label = null;let _started = 0, _lastWrite = 0, _sessionId = "";
export function begin({ type, label } = {}) { ensureDir(); _type = type || "render"; _label = label || _type; _sessionId = resolveClaudeSessionId(); _started = Date.now(); _id = `${_type}-${process.pid}-${_started.toString(36)}`; _file = join(RENDERS_DIR, `${_id}.json`); _write(0); // best-effort sweep if the process exits or is interrupted const done = () => { try { rmSync(_file, { force: true }); } catch { /* ignore */ } }; process.on("exit", done); process.on("SIGINT", () => { done(); process.exit(130); }); process.on("SIGTERM", () => { done(); process.exit(143); }); return _id;}
export function update(pct, counts) { if (!_file) return; // throttle to ~3 writes/s — the menubar only polls every 2 s if (pct != null && pct < 100 && Date.now() - _lastWrite < 300) return; _write(pct, counts);}
export function end() { if (!_file) return; try { rmSync(_file, { force: true }); } catch { /* ignore */ } _file = _id = null;}
function _write(pct, counts) { if (!_file) return; _lastWrite = Date.now(); const done = counts && Number.isFinite(counts.done) ? Math.max(0, Math.round(counts.done)) : null; const total = counts && Number.isFinite(counts.total) ? Math.max(0, Math.round(counts.total)) : null; const rec = { id: _id, type: _type, label: _label, sessionId: _sessionId, pct: pct == null ? null : Math.max(0, Math.min(100, Math.round(pct))), done, total, pid: process.pid, startedAt: _started, updatedAt: _lastWrite, }; try { writeFileSync(_file, JSON.stringify(rec)); } catch { /* ignore */ }}
// ── reader side (`pop renders` CLI; mirrors the Swift menubar) ────────function _alive(pid) { if (!pid) return false; try { process.kill(pid, 0); return true; } catch (e) { return e.code === "EPERM"; } // alive but not ours}
export function list() { ensureDir(); const out = []; let names = []; try { names = readdirSync(RENDERS_DIR); } catch { return out; } for (const n of names) { if (!n.endsWith(".json")) continue; const f = join(RENDERS_DIR, n); let rec; try { rec = JSON.parse(readFileSync(f, "utf8")); } catch { continue; } const stale = Date.now() - (rec.updatedAt || 0) > STALE_MS || !_alive(rec.pid); if (stale) { try { rmSync(f, { force: true }); } catch { /* ignore */ } continue; } out.push(rec); } return out.sort((a, b) => (a.startedAt || 0) - (b.startedAt || 0));}