Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

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

menuband landing: 'Recent changes' section + path-filtered /api/version

- Adds a Recent Changes panel under What's New that fetches the last
10 commits touching slab/menuband and renders them as terse
hash + message rows. Each hash links to the commit on Tangled.
- /api/version now accepts an optional ?path= query (sandboxed via a
conservative allowlist regex) and pipes it into git log so callers
can scope the recent-commits ticker to a subtree. Other consumers
(prompt.mjs curtain UI, etc.) keep their unscoped behavior.

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

+110 -4
+20 -4
system/netlify/functions/version.mjs
··· 93 93 return remotes[0] || "origin"; 94 94 } 95 95 96 - async function getLatestFromTangled(repoRoot, deployedCommit) { 96 + async function getLatestFromTangled(repoRoot, deployedCommit, pathFilter) { 97 97 const gitRemote = await getPreferredRemote(repoRoot); 98 98 await git(["fetch", "--quiet", gitRemote, GIT_BRANCH], repoRoot); 99 99 ··· 111 111 } 112 112 } 113 113 114 - const recentRaw = await git([ 114 + // Optional path filter: callers can ask for "only commits that 115 + // touched <path>", e.g. ?path=slab/menuband for the Menu Band landing 116 + // page's recent-changes section. Validated against a conservative 117 + // allowlist so we never invoke `git log -- <user-controlled-path>` 118 + // with characters that could escape into git's argument grammar. 119 + const logArgs = [ 115 120 "log", 116 121 remoteRef, 117 122 `--max-count=${RECENT_COMMIT_COUNT}`, 118 123 "--pretty=format:%H\t%s\t%an\t%cI", 119 - ], repoRoot); 124 + ]; 125 + if (pathFilter) { 126 + logArgs.push("--", pathFilter); 127 + } 128 + const recentRaw = await git(logArgs, repoRoot); 120 129 121 130 return { 122 131 latestCommit, ··· 157 166 const deployedCommit = await getDeployedCommit(repoRoot); 158 167 const url = new URL(request.url); 159 168 const clientHash = url.searchParams.get("current"); 169 + const rawPath = url.searchParams.get("path") || ""; 170 + // Conservative allowlist: alphanumerics, dot, slash, dash, underscore. 171 + // This blocks shell metacharacters and `--` injection so a malicious 172 + // ?path= value can't slip a flag into the `git log` invocation. 173 + const pathFilter = /^[A-Za-z0-9._\/-]{1,128}$/.test(rawPath) && !rawPath.startsWith("-") 174 + ? rawPath 175 + : ""; 160 176 161 177 // Long-poll mode: if client sends ?current=<hash> matching deployed version, 162 178 // wait ~4 seconds before responding (allows near-instant new-deploy detection) ··· 181 197 behindBy, 182 198 recentCommits, 183 199 } = repoRoot 184 - ? await getLatestFromTangled(repoRoot, deployedCommit) 200 + ? await getLatestFromTangled(repoRoot, deployedCommit, pathFilter) 185 201 : await getLatestFromTangledMirror(deployedCommit); 186 202 187 203 const status = behindBy === 0 ? "current" : "behind";
+90
system/public/menuband/index.html
··· 327 327 } 328 328 ul.modes span { color: var(--ink-body); } 329 329 330 + /* Recent commits — terse, monospaced log lines. */ 331 + ul.commits { 332 + list-style: none; 333 + padding: 0; 334 + margin: 6px 0 0; 335 + font-family: "Berkeley Mono Variable", "SF Mono", Menlo, monospace; 336 + font-size: 11px; 337 + line-height: 1.55; 338 + } 339 + ul.commits li { 340 + padding: 4px 0; 341 + border-top: 1px solid var(--panel-stripe); 342 + display: grid; 343 + grid-template-columns: 60px 1fr; 344 + gap: 10px; 345 + align-items: baseline; 346 + color: var(--ink-body); 347 + } 348 + ul.commits li:first-child { border-top: 0; } 349 + ul.commits .hash { 350 + color: var(--ink-soft); 351 + letter-spacing: 0.02em; 352 + } 353 + ul.commits .msg { 354 + overflow: hidden; 355 + text-overflow: ellipsis; 356 + white-space: nowrap; 357 + } 358 + ul.commits .commits-empty { 359 + grid-template-columns: 1fr; 360 + color: var(--ink-soft); 361 + font-style: italic; 362 + } 363 + /* Inline "deployed @ HEAD" chip next to "Recent changes" header. */ 364 + .commit-chip { 365 + display: inline-block; 366 + padding: 1px 7px; 367 + margin-left: 6px; 368 + font-family: "Berkeley Mono Variable", "SF Mono", Menlo, monospace; 369 + font-size: 10px; 370 + font-weight: 600; 371 + color: var(--ink-soft); 372 + background: var(--panel-bg); 373 + border: 1px solid var(--panel-edge); 374 + border-radius: 4px; 375 + vertical-align: middle; 376 + } 377 + 330 378 /* "What's new" badge — tiny, embossed, Adium-press-quote vibe. */ 331 379 .badge { 332 380 display: inline-block; ··· 555 603 </section> 556 604 557 605 <section class="panel"> 606 + <h3>Recent changes <span id="recent-deployed" class="commit-chip" hidden></span></h3> 607 + <ul id="recent-commits" class="commits"> 608 + <li class="commits-empty">loading…</li> 609 + </ul> 610 + </section> 611 + 612 + <section class="panel"> 558 613 <h3>Requirements</h3> 559 614 <p>macOS 11+ · Apple Silicon · ⌃⌥⌘P toggles typing mode.</p> 560 615 </section> ··· 624 679 poll(); 625 680 } 626 681 poll(); 682 + })(); 683 + 684 + // ── Recent changes — fetch the last 10 commits that touched 685 + // slab/menuband and render them under "Recent changes". The 686 + // `/api/version?path=` filter keeps the list focused on actual 687 + // Menu Band work instead of every commit on the AC monorepo. ─── 688 + (function recentChanges() { 689 + const list = document.getElementById('recent-commits'); 690 + const chip = document.getElementById('recent-deployed'); 691 + if (!list) return; 692 + fetch('/api/version?path=slab/menuband', { cache: 'no-store' }) 693 + .then((r) => r.ok ? r.json() : Promise.reject(new Error('HTTP ' + r.status))) 694 + .then((data) => { 695 + if (chip && data && data.deployed && data.deployed !== 'unknown') { 696 + chip.hidden = false; 697 + chip.textContent = '@ ' + data.deployed; 698 + chip.title = data.status === 'behind' 699 + ? `${data.behindBy} commits behind tangled` 700 + : 'deployed commit'; 701 + } 702 + const commits = (data && Array.isArray(data.recentCommits)) ? data.recentCommits : []; 703 + if (!commits.length) { 704 + list.innerHTML = '<li class="commits-empty">no recent changes</li>'; 705 + return; 706 + } 707 + list.innerHTML = commits.slice(0, 10).map((c) => { 708 + const hash = String(c.hash || '').slice(0, 7); 709 + const msg = String(c.message || '').replace(/&/g,'&amp;').replace(/</g,'&lt;'); 710 + const url = `https://tangled.org/@aesthetic.computer/core/commit/${hash}`; 711 + return `<li><a class="hash" href="${url}">${hash}</a><span class="msg" title="${msg}">${msg}</span></li>`; 712 + }).join(''); 713 + }) 714 + .catch(() => { 715 + list.innerHTML = '<li class="commits-empty">couldn\'t load recent changes</li>'; 716 + }); 627 717 })(); 628 718 629 719 </script>